results.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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
10goog.provide('cvox.SearchResults');
11goog.provide('cvox.UnknownResult');
12
13goog.require('cvox.AbstractResult');
14goog.require('cvox.ChromeVox');
15goog.require('cvox.SearchUtil');
16
17/**
18 * @constructor
19 */
20cvox.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 */
28cvox.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 */
48cvox.UnknownResult = function() {
49};
50goog.inherits(cvox.UnknownResult, cvox.AbstractResult);
51
52/* Normal Result Type. */
53/**
54 * @constructor
55 * @extends {cvox.AbstractResult}
56 */
57cvox.NormalResult = function() {
58};
59goog.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 */
67cvox.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 */
78cvox.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 */
114cvox.WeatherResult = function() {
115};
116goog.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 */
124cvox.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 */
133cvox.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 */
157cvox.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 */
207cvox.KnowResult = function() {
208};
209goog.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 */
217cvox.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 */
228cvox.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 */
239cvox.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 */
250cvox.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 */
260cvox.CalcResult = function() {
261};
262goog.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 */
270cvox.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 */
281cvox.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 */
300cvox.GameResult = function() {
301};
302goog.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 */
310cvox.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 */
320cvox.ImageResult = function() {
321};
322goog.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 */
330cvox.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 */
341cvox.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 */
380cvox.CategoryResult = function() {
381};
382goog.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 */
390cvox.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 */
401cvox.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 */
416cvox.AdResult = function() {
417};
418goog.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 */
426cvox.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 */
437cvox.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 */
458cvox.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