1 // Copyright 2012 Google Inc. All Rights Reserved. 2 3 /** 4 * @fileoverview Provides different rules for each type of result. 5 * @author peterxiao (a] google.com (Peter Xiao) 6 */ 7 8 goog.provide('cvox.SearchResults'); 9 goog.provide('cvox.UnknownResult'); 10 11 goog.require('cvox.AbstractResult'); 12 goog.require('cvox.ChromeVox'); 13 goog.require('cvox.SearchUtil'); 14 15 /** 16 * @constructor 17 */ 18 cvox.SearchResults = function() { 19 }; 20 21 /** 22 * Speaks a result based on given selectors. 23 * @param {Element} result Search result to be spoken. 24 * @param {Array} selectTexts Array of selectors or text to speak. 25 */ 26 cvox.SearchResults.speakResultBySelectTexts = function(result, selectTexts) { 27 for (var j = 0; j < selectTexts.length; j++) { 28 var selectText = selectTexts[j]; 29 if (selectText.select) { 30 var elems = result.querySelectorAll(selectText.select); 31 for (var i = 0; i < elems.length; i++) { 32 cvox.ChromeVox.speakNode(elems.item(i), 1); 33 } 34 } 35 if (selectText.text) { 36 cvox.ChromeVox.tts.speak(selectText.text, 1); 37 } 38 } 39 }; 40 41 /** 42 * Unknown Result Type. This is used if we don't know what to do. 43 * @constructor 44 * @extends {cvox.AbstractResult} 45 */ 46 cvox.UnknownResult = function() { 47 }; 48 goog.inherits(cvox.UnknownResult, cvox.AbstractResult); 49 50 /* Normal Result Type. */ 51 /** 52 * @constructor 53 * @extends {cvox.AbstractResult} 54 */ 55 cvox.NormalResult = function() { 56 }; 57 goog.inherits(cvox.NormalResult, cvox.AbstractResult); 58 59 /** 60 * Checks the result if it is a normal result. 61 * @param {Element} result Result to be checked. 62 * @return {boolean} Whether or not the element is a normal result. 63 * @override 64 */ 65 cvox.NormalResult.prototype.isType = function(result) { 66 var NORMAL_SELECT = '.rc'; 67 return result.querySelector(NORMAL_SELECT) !== null; 68 }; 69 70 /** 71 * Speak a normal search result. 72 * @param {Element} result Normal result to be spoken. 73 * @return {boolean} Whether or not the result was spoken. 74 * @override 75 */ 76 cvox.NormalResult.prototype.speak = function(result) { 77 if (!result) { 78 return false; 79 } 80 var NORMAL_TITLE_SELECT = '.rc .r'; 81 var NORMAL_URL_SELECT = '.kv'; 82 var NORMAL_DESC_SELECT = '.rc .st'; 83 var SITE_LINK_SELECT = '.osl'; 84 var MORE_RESULTS_SELECT = '.sld'; 85 var MORE_RESULTS_LINK_SELECT = '.mrf'; 86 87 var NORMAL_SELECTORS = [ 88 { select: NORMAL_TITLE_SELECT }, 89 { select: NORMAL_DESC_SELECT }, 90 { select: NORMAL_URL_SELECT }, 91 { select: SITE_LINK_SELECT }, 92 { select: MORE_RESULTS_SELECT }, 93 { select: MORE_RESULTS_LINK_SELECT }]; 94 cvox.SearchResults.speakResultBySelectTexts(result, NORMAL_SELECTORS); 95 96 var DISCUSS_TITLE_SELECT = '.mas-1st-col div'; 97 var DISCUSS_DATE_SELECT = '.mas-col div'; 98 var discussTitles = result.querySelectorAll(DISCUSS_TITLE_SELECT); 99 var discussDates = result.querySelectorAll(DISCUSS_DATE_SELECT); 100 for (var i = 0; i < discussTitles.length; i++) { 101 cvox.ChromeVox.speakNode(discussTitles.item(i), 1); 102 cvox.ChromeVox.speakNode(discussDates.item(i), 1); 103 } 104 return true; 105 }; 106 107 /* Weather Result */ 108 /** 109 * @constructor 110 * @extends {cvox.AbstractResult} 111 */ 112 cvox.WeatherResult = function() { 113 }; 114 goog.inherits(cvox.WeatherResult, cvox.AbstractResult); 115 116 /** 117 * Checks the result if it is a weather result. 118 * @param {Element} result Result to be checked. 119 * @return {boolean} Whether or not the element is a weather result. 120 * @override 121 */ 122 cvox.WeatherResult.prototype.isType = function(result) { 123 var WEATHER_SELECT = '#wob_wc'; 124 return result.querySelector(WEATHER_SELECT) !== null; 125 }; 126 127 /** 128 * Speak a weather forecast. 129 * @param {Element} forecast Weather forecast to be spoken. 130 */ 131 cvox.WeatherResult.speakForecast = function(forecast) { 132 if (!forecast) { 133 return; 134 } 135 var FORE_DAY_SELECT = '.vk_lgy'; 136 var FORE_COND_SELECT = 'img'; 137 var FORE_HIGH_SELECT = '.vk_gy'; 138 var FORE_LOW_SELECT = '.vk_lgy'; 139 140 var FORE_SELECTORS = [ 141 { select: FORE_DAY_SELECT }, 142 { select: FORE_COND_SELECT }, 143 { select: FORE_HIGH_SELECT }, 144 { select: FORE_LOW_SELECT } 145 ]; 146 cvox.SearchResults.speakResultBySelectTexts(forecast, FORE_SELECTORS); 147 }; 148 149 /** 150 * Speak a weather search result. 151 * @param {Element} result Weather result to be spoken. 152 * @return {boolean} Whether or not the result was spoken. 153 * @override 154 */ 155 cvox.WeatherResult.prototype.speak = function(result) { 156 if (!result) { 157 return false; 158 } 159 /* TODO(peterxiao): Internationalization? */ 160 var WEATHER_INTRO = 'The weather forcast for'; 161 var WEATHER_TEMP_UNITS = 'degrees fahrenheit'; 162 var WEATHER_PREC_INTRO = 'precipitation is'; 163 var WEATHER_HUMID_INTRO = 'humidity is'; 164 var WEATHER_WIND_INTRO = 'wind is'; 165 var FORE_INTRO = 'Forecasts for this week'; 166 var WEATHER_LOC_SELECT = '.vk_h'; 167 var WEATHER_WHEN_SELECT = '#wob_dts'; 168 var WEATHER_COND_SELECT = '#wob_dc'; 169 var WEATHER_TEMP_SELECT = '#wob_tm'; 170 var WEATHER_PREC_SELECT = '#wob_pp'; 171 var WEATHER_HUMID_SELECT = '#wob_hm'; 172 var WEATHER_WIND_SELECT = '#wob_ws'; 173 174 var WEATHER_SELECT_TEXTS = [ 175 { text: WEATHER_INTRO }, 176 { select: WEATHER_LOC_SELECT }, 177 { select: WEATHER_WHEN_SELECT }, 178 { select: WEATHER_COND_SELECT }, 179 { select: WEATHER_TEMP_SELECT }, 180 { text: WEATHER_TEMP_UNITS }, 181 { text: WEATHER_PREC_INTRO }, 182 { select: WEATHER_PREC_SELECT }, 183 { text: WEATHER_HUMID_INTRO }, 184 { select: WEATHER_HUMID_SELECT }, 185 { text: WEATHER_WIND_INTRO }, 186 { select: WEATHER_WIND_SELECT } 187 ]; 188 cvox.SearchResults.speakResultBySelectTexts(result, WEATHER_SELECT_TEXTS); 189 190 var WEATHER_FORCAST_CLASS = 'wob_df'; 191 var forecasts = result.getElementsByClassName(WEATHER_FORCAST_CLASS); 192 cvox.ChromeVox.tts.speak(FORE_INTRO, 1); 193 for (var i = 0; i < forecasts.length; i++) { 194 var forecast = forecasts.item(i); 195 cvox.WeatherResult.speakForecast(forecast); 196 } 197 return true; 198 }; 199 200 /* Knowledge Panel Result */ 201 /** 202 * @constructor 203 * @extends {cvox.AbstractResult} 204 */ 205 cvox.KnowResult = function() { 206 }; 207 goog.inherits(cvox.KnowResult, cvox.AbstractResult); 208 209 /** 210 * Checks the result if it is a know result. 211 * @param {Element} result Result to be checked. 212 * @return {boolean} Whether or not the element is a know result. 213 * @override 214 */ 215 cvox.KnowResult.prototype.isType = function(result) { 216 var KNOP_SELECT = '.kno-ec'; 217 return result.querySelector(KNOP_SELECT) !== null; 218 }; 219 220 /** 221 * Speak a knowledge panel search result. 222 * @param {Element} result Knowledge panel result to be spoken. 223 * @return {boolean} Whether or not the result was spoken. 224 * @override 225 */ 226 cvox.KnowResult.prototype.speak = function(result) { 227 cvox.ChromeVox.speakNode(result, 1); 228 return true; 229 }; 230 231 /** 232 * Extracts the wikipedia URL from knowledge panel. 233 * @param {Element} result Result to extract from. 234 * @return {?string} URL. 235 * @override 236 */ 237 cvox.KnowResult.prototype.getURL = function(result) { 238 var LINK_SELECTOR = '.q'; 239 return cvox.SearchUtil.extractURL(result.querySelector(LINK_SELECTOR)); 240 }; 241 242 /** 243 * Extracts the node to sync to in the knowledge panel. 244 * @param {Element} result Result. 245 * @return {?Node} Node to sync to. 246 * @override 247 */ 248 cvox.KnowResult.prototype.getSyncNode = function(result) { 249 var HEADER_SELECTOR = '.kno-ecr-pt'; 250 return result.querySelector(HEADER_SELECTOR); 251 }; 252 253 /* Calculator Type */ 254 /** 255 * @constructor 256 * @extends {cvox.AbstractResult} 257 */ 258 cvox.CalcResult = function() { 259 }; 260 goog.inherits(cvox.CalcResult, cvox.AbstractResult); 261 262 /** 263 * Checks the result if it is a calculator result. 264 * @param {Element} result Result to be checked. 265 * @return {boolean} Whether or not the element is a calculator result. 266 * @override 267 */ 268 cvox.CalcResult.prototype.isType = function(result) { 269 var CALC_SELECT = '#cwmcwd'; 270 return result.querySelector(CALC_SELECT) !== null; 271 }; 272 273 /** 274 * Speak a calculator search result. 275 * @param {Element} result Calculator result to be spoken. 276 * @return {boolean} Whether or not the result was spoken. 277 * @override 278 */ 279 cvox.CalcResult.prototype.speak = function(result) { 280 if (!result) { 281 return false; 282 } 283 var CALC_QUERY_SELECT = '#cwles'; 284 var CALC_RESULT_SELECT = '#cwos'; 285 var CALC_SELECTORS = [ 286 { select: CALC_QUERY_SELECT }, 287 { select: CALC_RESULT_SELECT } 288 ]; 289 cvox.SearchResults.speakResultBySelectTexts(result, CALC_SELECTORS); 290 return true; 291 }; 292 293 /* Game Type */ 294 /** 295 * @constructor 296 * @extends {cvox.AbstractResult} 297 */ 298 cvox.GameResult = function() { 299 }; 300 goog.inherits(cvox.GameResult, cvox.AbstractResult); 301 302 /** 303 * Checks the result if it is a game result. 304 * @param {Element} result Result to be checked. 305 * @return {boolean} Whether or not the element is a game result. 306 * @override 307 */ 308 cvox.GameResult.prototype.isType = function(result) { 309 var GAME_SELECT = '.xpdbox'; 310 return result.querySelector(GAME_SELECT) !== null; 311 }; 312 313 /* Image Type */ 314 /** 315 * @constructor 316 * @extends {cvox.AbstractResult} 317 */ 318 cvox.ImageResult = function() { 319 }; 320 goog.inherits(cvox.ImageResult, cvox.AbstractResult); 321 322 /** 323 * Checks the result if it is a image result. 324 * @param {Element} result Result to be checked. 325 * @return {boolean} Whether or not the element is a image result. 326 * @override 327 */ 328 cvox.ImageResult.prototype.isType = function(result) { 329 var IMAGE_CLASSES = 'rg_di'; 330 return result.className === IMAGE_CLASSES; 331 }; 332 333 /** 334 * Speak an image result. 335 * @param {Element} result Image result to be spoken. 336 * @return {boolean} Whether or not the result was spoken. 337 * @override 338 */ 339 cvox.ImageResult.prototype.speak = function(result) { 340 if (!result) { 341 return false; 342 } 343 /* Grab image result metadata. */ 344 var META_CLASS = 'rg_meta'; 345 var metaDiv = result.querySelector('.' + META_CLASS); 346 var metaJSON = metaDiv.innerHTML; 347 var metaData = JSON.parse(metaJSON); 348 349 var imageSelectTexts = []; 350 351 var filename = metaData['fn']; 352 if (filename) { 353 imageSelectTexts.push({ text: filename }); 354 } 355 356 var rawDimensions = metaData['is']; 357 if (rawDimensions) { 358 /* Dimensions contain HTML codes, so we convert them. */ 359 var tmpDiv = document.createElement('div'); 360 tmpDiv.innerHTML = rawDimensions; 361 var dimensions = tmpDiv.textContent || tmpDiv.innerText; 362 imageSelectTexts.push({ text: dimensions }); 363 } 364 365 var url = metaData['isu']; 366 if (url) { 367 imageSelectTexts.push({ text: url}); 368 } 369 cvox.SearchResults.speakResultBySelectTexts(result, imageSelectTexts); 370 return true; 371 }; 372 373 /* Category Result */ 374 /** 375 * @constructor 376 * @extends {cvox.AbstractResult} 377 */ 378 cvox.CategoryResult = function() { 379 }; 380 goog.inherits(cvox.CategoryResult, cvox.AbstractResult); 381 382 /** 383 * Checks the result if it is a category result. 384 * @param {Element} result Result to be checked. 385 * @return {boolean} Whether or not the element is a category result. 386 * @override 387 */ 388 cvox.CategoryResult.prototype.isType = function(result) { 389 var CATEGORY_CLASSES = 'rg_fbl nj'; 390 return result.className === CATEGORY_CLASSES; 391 }; 392 393 /** 394 * Speak a category result. 395 * @param {Element} result Category result to be spoken. 396 * @return {boolean} Whether or not the result was spoken. 397 * @override 398 */ 399 cvox.CategoryResult.prototype.speak = function(result) { 400 if (!result) { 401 return false; 402 } 403 var LABEL_SELECT = '.rg_bb_label'; 404 var label = result.querySelector(LABEL_SELECT); 405 cvox.ChromeVox.speakNode(label, 1); 406 return true; 407 }; 408 409 /* Ad Result */ 410 /** 411 * @constructor 412 * @extends {cvox.AbstractResult} 413 */ 414 cvox.AdResult = function() { 415 }; 416 goog.inherits(cvox.AdResult, cvox.AbstractResult); 417 418 /** 419 * Checks the result if it is an ad result. 420 * @param {Element} result Result to be checked. 421 * @return {boolean} Whether or not the element is an ad result. 422 * @override 423 */ 424 cvox.AdResult.prototype.isType = function(result) { 425 var ADS_CLASS = 'ads-ad'; 426 return result.className === ADS_CLASS; 427 }; 428 429 /** 430 * Speak an ad result. 431 * @param {Element} result Ad result to be spoken. 432 * @return {boolean} Whether or not the result was spoken. 433 * @override 434 */ 435 cvox.AdResult.prototype.speak = function(result) { 436 if (!result) { 437 return false; 438 } 439 var HEADER_SELECT = 'h3'; 440 var DESC_SELECT = '.ads-creative'; 441 var URL_SELECT = '.ads-visurl'; 442 var AD_SELECTS = [ 443 { select: HEADER_SELECT }, 444 { select: DESC_SELECT }, 445 { select: URL_SELECT }]; 446 cvox.SearchResults.speakResultBySelectTexts(result, AD_SELECTS); 447 return true; 448 }; 449 450 /** 451 * To add new result types, create a new object with the following properties: 452 * isType: Function to indicate if an element is the object's type. 453 * speak: Function that takes in a result and speaks the type to the user. 454 * getURL: Function that takes in a result and extracts the URL to follow. 455 */ 456 cvox.SearchResults.RESULT_TYPES = [ 457 cvox.UnknownResult, 458 cvox.NormalResult, 459 cvox.KnowResult, 460 cvox.WeatherResult, 461 cvox.AdResult, 462 cvox.CalcResult, 463 cvox.GameResult, 464 cvox.ImageResult, 465 cvox.CategoryResult 466 ]; 467