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