1// Copyright 2012 Google Inc. All Rights Reserved. 2 3/** 4 * @fileoverview Provides different rules for each type of result. 5 * @author peterxiao@google.com (Peter Xiao) 6 */ 7 8goog.provide('cvox.SearchResults'); 9goog.provide('cvox.UnknownResult'); 10 11goog.require('cvox.AbstractResult'); 12goog.require('cvox.ChromeVox'); 13goog.require('cvox.SearchUtil'); 14 15/** 16 * @constructor 17 */ 18cvox.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 */ 26cvox.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 */ 46cvox.UnknownResult = function() { 47}; 48goog.inherits(cvox.UnknownResult, cvox.AbstractResult); 49 50/* Normal Result Type. */ 51/** 52 * @constructor 53 * @extends {cvox.AbstractResult} 54 */ 55cvox.NormalResult = function() { 56}; 57goog.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 */ 65cvox.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 */ 76cvox.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 */ 112cvox.WeatherResult = function() { 113}; 114goog.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 */ 122cvox.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 */ 131cvox.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 */ 155cvox.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 */ 205cvox.KnowResult = function() { 206}; 207goog.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 */ 215cvox.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 */ 226cvox.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 */ 237cvox.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 */ 248cvox.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 */ 258cvox.CalcResult = function() { 259}; 260goog.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 */ 268cvox.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 */ 279cvox.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 */ 298cvox.GameResult = function() { 299}; 300goog.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 */ 308cvox.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 */ 318cvox.ImageResult = function() { 319}; 320goog.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 */ 328cvox.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 */ 339cvox.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 */ 378cvox.CategoryResult = function() { 379}; 380goog.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 */ 388cvox.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 */ 399cvox.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 */ 414cvox.AdResult = function() { 415}; 416goog.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 */ 424cvox.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 */ 435cvox.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 */ 456cvox.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