PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/documentation/spice2.md

https://github.com/a-West/duckduckgo
Markdown | 1298 lines | 1004 code | 294 blank | 0 comment | 0 complexity | 649cd33da580e9d07a382e0911f54210 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. #Spice Frontend
  2. --------
  3. #Index
  4. - [Overview](#overview)
  5. - [Tech](#tech)
  6. - [Example #1: NPM (Basic Instant Answer)](#example-1---npm-basic-instant-answer)
  7. - [Example #2: Alternative.To (Basic Carousel Instant Answer)](#example-2---alternativeto-basic-carousel-instant-answer)
  8. - [Example #3: Movie (Advanced Instant Answer)](#example-3---movie-advanced-instant-answer)
  9. - [Example #4: Quixey (Advanced Carousel Instant Answer)](#example-4---quixey-advanced-carousel-instant-answer)
  10. - [Example #5: Dictionary (More Advanced Instant Answer)](#example-5---dictionary-more-advanced-instant-answer)
  11. - [Advanced Techniques](#advanced-techniques)
  12. - [Slurping Multiple Trigger Words](#slurping-multiple-trigger-words)
  13. - [Using API Keys](#using-api-keys)
  14. - [Using the GEO Location API](#using-the-geo-location-api)
  15. - [Common Code for Spice Endpoints (.pm's)](#common-code-for-spice-endpoints-pms)
  16. - [Common JavaScript and Handlebars Templates](#common-javascript-and-handlebars-templates)
  17. - [Using Custom CSS](#using-custom-css)
  18. - [Using images](#using-images)
  19. - [Common Pitfalls](#common-pitfalls)
  20. - [Defining Perl Variables and Functions](#defining-perl-variables-and-functions)
  21. - [StyleGuide](#styleguide)
  22. - [Formatting](#formatting)
  23. - [Naming Conventions](#naming-conventions)
  24. - [Do's & Don'ts](#dos--donts)
  25. - [FAQ](#faq)
  26. - [DDG Methods (JavaScript)](#ddg-methods-javascript)
  27. - [Spice Helpers (Handlebars)](#spice-helpers-handlebars)
  28. - [Spice Attributes (Perl)](#spice-attributes-perl)
  29. - [Spice Helper Functions (Perl)](#spice-helper-functions-perl)
  30. -------
  31. ##Overview
  32. The Spice frontend is the code that is triggered by the Perl backend (which we learned about in the previous tutorial) for your spice instant answer. It mainly consists of a function (the Spice "callback" function) that takes a JSON formatted, API response as its input, specifies which template format you'd like your result to have and uses the data to render a Spice result at the top of the DuckDuckGo search results page.
  33. The Perl part of the instant answers go in lib directory: `lib/DDG/Spice/instantAnswerName.pm`, while all of the frontend files discussed below should go in the share directory: `share/spice/instant_answer_name/`.
  34. **\*\*Note** : The file and folder names must adhere to our [naming conventions](#naming-conventions) in order for everything to function properly.
  35. ###Tech
  36. The Spice frontend uses [Handlebars](http://handlebarsjs.com) for templates and includes [jQuery](https://jquery.org) (although it's use is not required). It also allows the use of custom CSS when required.
  37. If you're not already familiar with Handlebars, *please* read the [Handlebars documentation](http://handlebarsjs.com) before continuing on. Don't worry if you don't fully understand how to use Handlebars; the examples will explain it to you. But you should, at the very least, familiarize yourself with Handlebars concepts and terminology before moving on. (Don't worry, it should only take a few minutes to read!)
  38. Below, we will walk you through several examples ranging from simple to complicated, which will explain how to use the template system and make your instant answers look awesome.
  39. ------------------------
  40. ##Example #1 - NPM (Basic Instant Answer)
  41. The NPM instant answer [[link](https://duckduckgo.com/?q=npm+uglify-js)] [[code](https://github.com/duckduckgo/zeroclickinfo-spice/tree/master/share/spice/npm)] is a great example of a basic Spice implementation. Let's walk through it line-by-line:
  42. #####npm.js
  43. ```javascript
  44. function ddg_spice_npm (api_result) {
  45. if (api_result.error) return
  46. Spice.render({
  47. data : api_result,
  48. force_big_header : true,
  49. header1 : api_result.name + ' (' + api_result.version + ')',
  50. source_name : "npmjs.org", // More at ...
  51. source_url : 'http://npmjs.org/package/' + api_result.name,
  52. template_normal : 'npm',
  53. template_small : 'npm'
  54. });
  55. }
  56. ```
  57. As mentioned, every instant answer requires a Spice callback function, for the *NPM* instant answer, the callback is the `ddg_spice_npm()` function that we defined here in *npm.js*. The *NPM* Perl module we wrote specifies this as the callback by using the name of the package `DDG::Spice::NPM` and gives this *ddg_spice_npm* name to the API call so that this funtion will be executed when the API responds using the data returned from the upstream (API) provider as the function's input.
  58. #####npm.js (continued)
  59. ```javascript
  60. if (api_result.error) return
  61. ```
  62. Pretty self-explanatory - If the error object in the API result is defined, then break out of the function and don't show any results. In the case of this API, when the error object is defined, it means no results are given, so we have no data to use for a Spice result.
  63. #####npm.js (continued)
  64. ```javascript
  65. Spice.render({
  66. data : api_result,
  67. force_big_header : true,
  68. header1 : api_result.name + ' (' + api_result.version + ')',
  69. source_name : "npmjs.org",
  70. source_url : 'http://npmjs.org/package/' + api_result.name,
  71. template_normal : 'npm',
  72. template_small : 'npm'
  73. });
  74. ```
  75. Alright, so here is the bulk of the instant answer, but it's very simple:
  76. - `Spice.render()` is a function that the instant answer system has already defined. You pass an object to it that specifies a bunch of important parameters.
  77. - `data` is perhaps the most important parameter. The object given here will be the object that is passed along to the Handlebars template. In this case, the context of the NPM template will be the **api_result** object. This is very important to understand because **only the data passed along to the template is accessible to the template**. In most cases the `data` parameter should be set to
  78. `api_result` so all the data returned from the API is accessible to the template.
  79. - `force_big_header` is related to the display formatting -- it forces the system to display the large grey header that you see when you click the example link above.
  80. - `header1` is the text of this header, i.e. the text displayed inside the large grey bar.
  81. - `source_name` is the name of the source for the "More at <source>" link that's displayed below the text of the instant answer for attribution purposes.
  82. - `source_url` is the target of the "More at" link. It's the page that the user will click through to.
  83. - `template_normal` is the name of the Handlebars template that contains the structure information for your instant answer.
  84. - `template_small` is the name of the Handlebars template to be used when you instant answer is displayed in a stacked state. This isn't required, but if your instant answer can provide a succint, one or two line answer this template should be used in the event that the instant answer appears in the stacked state. If no template is given the stacked result will simply show the header of the spice result
  85. ----
  86. Now, let's look at the NPM instant answer's Handlebars template:
  87. ######npm.handlebars
  88. ```html
  89. <div>
  90. <div>{{{description}}}</div>
  91. <pre> $ npm install {{{name}}}</pre>
  92. </div>
  93. ```
  94. As you can see, this is a special type of HTML template. Within the template, you can refer directly to objects that are returned by the API. `description` and `name` are both from the `api_result` object that we discussed earlier -- the data that's returned by the API. All of `api_result`'s sub-objects (e.g. `name`, `description`) are in the template's scope. You can access them by name using double or triple curly braces (triple braces will not escape the contents). Here, we just create a basic HTML skeleton and fill it in with the proper information.
  95. ###Conclusion
  96. We've created two files in the Spice share directory (`share/spice/npm/`) :
  97. 1. `npm.js` - which delegates the API's response and calls `Spice.render()`
  98. 2. `npm.handlebars` - which specifies the instant answer's HTML structure and determines which attributes of the API response are placed in the HTML result
  99. You may notice other instant answers also include a css file. For **NPM** the use of CSS wasn't necessary and this is also true for many other instant answers. If however CSS is needed it can be added. Please refer to the [Spice FAQ](#faq) for more inforamtion about custom css.
  100. ##Example #2 - Alternative.To (Basic Carousel Instant Answer)
  101. The Alternative.To instant answer is very similar to NPM in that it is also relatively basic, however, it uses the **Carousel** Spice Template. Let's take a look at the code and see how this is done:
  102. ###### alternative\_to.js
  103. ```javascript
  104. function ddg_spice_alternative_to(api_result) {
  105. if(!api_result || !api_result.Items || api_result.Items.length === 0) {
  106. return;
  107. }
  108. Spice.render({
  109. data : api_result,
  110. source_name : 'AlternativeTo',
  111. source_url : api_result.Url,
  112. spice_name : 'alternative_to',
  113. more_icon_offset : '-2px',
  114. template_frame : "carousel",
  115. template_options : {
  116. items : api_result.Items,
  117. template_item : "alternative_to",
  118. template_detail : "alternative_to_details",
  119. li_height : 65,
  120. single_item_handler : function(obj) { // gets called in the event of a single result
  121. obj.header1 = obj.data.Items[0].Name; // set the header
  122. obj.image_url = obj.data.Items[0].IconUrl; // set the image
  123. }
  124. },
  125. });
  126. }
  127. ```
  128. Just like the NPM instant answer, Alternative.To uses `Spice.render()` with most of the same properties, however, it also uses a few new properties as well:
  129. - `template_frame` is used to tell the Render function that the base template for this instant answer will be the **Carousel** template.
  130. **\*\*Note**: This is a template which we have already created and you don't have to worry about creating or modifying.*
  131. - `template_options` is a property which is used to specify more properties that are specifc to the current `template_frame`. In this case, we use `template_options` to define more properties for the `carosuel` template.
  132. - `items` is **required** when using the carousel template. It passes along an array or object to be iterated over by the carousel template. Each of these items becomes the context for the `alternative_to.handlebars` template which defines the content of each `<li>` in the carousel.
  133. - `template_item` tell the carousel template which sub-template should be applied to each of the objects in the `items` array. This template defines what the contents of each `<li>` in the carousel will contain. Generally for a `carousel` Instant Answer, each `<li>` should contain an image and title for the current item.
  134. - `template_detail` similarly to `template_frame` this is again another sub-template applied to each of the objects in the `items` array, however this template defines the look/layout of the **detail area**, which opens up below the carousel with more information about the selected item.
  135. - `li_height` is an optional property which defines the height of each of the carousel's `<li>`'s
  136. - `single_item_handler` is a property which defines a function that lets you modify the other properties of `Spice.render()` if and only if a single item is returned from the upstream API. In this case, we use it to set values for the `header1` and the `image_url`related to the single item returned. If these had not been set, the carousel will still automatically (by default) apply the `template_detail` to the single item returned and display that, rather than a carousel with a single item in it. This can be overridden by setting the `use_alternate_template` property to false, inside the `template_options` property.
  137. - `carousel_template_detail` is an **optional** parameter which specifies the Handlebars template to be used for the Carousel ***detail*** area - the space below the template which appears after clicking a carousel item. For Alternative.To, when a user clicks a carousel item (icon), the detail area appears and provides more information about that particular item. This is similarly used for the [Quixey instant answer](https://duckduckgo.com/?q=ios+flight+tracking+app).
  138. ----------------------------
  139. Now, let's take a look at the Alternative.To Handlebars templates:
  140. ###### alternative\_to.handlebars
  141. ```html
  142. <img src="/iu/?u={{IconUrl}}">
  143. <span>{{{condense Name maxlen="25"}}}</span>
  144. ```
  145. This simple template is used to define each of the carousel items. More specifically, it defines what the contents of each `<li>` in the carousel will be. In this case we specify an image - the result's icon - and a span tag, which contains the name of the result.
  146. You might notice that we prepend the `<img>`'s `src` url with the string `"/iu/?u="`. This is **required** for any images in your handlebars template. What this line does is proxy the image through our own servers, which ensure the user's privacy (because it forces the request to come from DuckDuckGo instead of the user).
  147. The carousel uses this template by iterating over each item in the object given to `carousel_items` and uses that item as the context of the template.
  148. Another important point is that we use `{{{condense Name maxlen="25"}}}` which demonstrates the usage of a Handlebars helper function. In this case, we are using the `condense` function (defined elsewhere, internally) which takes two parameters: `Name` (from `api_result`), which is the string to be shortened and `maxlen="25"` which specifies the length the string will be shortened to.
  149. Seeing as this is a carousel instant answer, which uses the optional carousel details area, it has another Handlebars template which defines the content for that. Let's have a look at the Alternative.To details template:
  150. ###### alternative\_to\_details.handlebars
  151. ```html
  152. {{#rt}} <a href="{{Url}}">{{Name}}</a> <span class="likes">({{Votes}} likes)</span>{{/rt}}
  153. {{#rd "Description"}} {{{ShortDescription}}}{{/rd}}
  154. {{#rd "Platforms"}} {{#concat Platforms sep=", " conj=" and "}}{{this}}{{/concat}}{{/rd}}
  155. ```
  156. This template is also relatively simple and it utilizes more of our Handlebars helpers to create a few elements and populates them with relevant information related to the carousel item that was clicked.
  157. In this case we're using the `{{#rt}}` and `{{#rd}}` Handlebars block helpers, which are perfect for this kind of data, which represent a "record" where each piece of data has a name/title. The `{{#rt}}` helper is used to define the record title. It creates a `div` tag with special css class of `rd_title`. Inside the div, a `<span>` is created with a class of `rt_val`. This `<span>` contains the content defined in the `{{#rt}}` block, which in this case is an `<a>` tag and another `<span>`.
  158. Likewise, the `{{#rd}}` helper creates a `<div>` that has a css class of `rd_normal`. Inside the `<div>` two `<span>` tags are created: the first one has a css of `rd_key` and it contains the string that was given to the `{{#rd}}` helper as input. The other `<span>` has a class of `rd_val` and it contains the content defined in the `{{#rd}}` block.
  159. In the second `{{#rd}}` you'll notice the use of another Handlebars helper function, `{{#concat}}`. This function takes an array as its first parameter and iterates over each element in the array. For each iteration, `{{#concat}}` sets the context of the block equal to the current array element and then concatenates the content of its block, joining each by the separator string (`sep=`) with the final element separated by the `conj=` string. In this case, if `Platforms` is a list of operating systems: `["windows", "linux", "mac"]`, then `concat` would return: **"widows, linux and mac"**.
  160. ##Example #3 - Movie (Advanced Instant Answer)
  161. The movie instant answer is a more advanced than **NPM** and **Alternative.To**, but most of the logic is used to obtain the most relevant movie from list given to us in `api_result`. Other than that, its relatively easy to understand, so lets start by looking at the Movie instant answer's javascript:
  162. ###### movie.js
  163. ```javascript
  164. function ddg_spice_movie (api_result) {
  165. if (api_result.total === 0) {
  166. return;
  167. }
  168. var ignore = ["movie", "film", "rotten", "rating", "rt", "tomatoes", "release date"];
  169. var result, max_score = 0;
  170. // Assign a ranking value for the movie. This isn't a complete sorting value though
  171. // also we are blindling assuming these values exist
  172. var score = function(m) {
  173. var s = m.ratings.critics_score * m.ratings.audience_score;
  174. if (s > max_score) max_score = s;
  175. // If any above are undefined, s is undefined.
  176. return s;
  177. };
  178. // returns the more relevant of the two movies
  179. var better = function(currentbest, next) {
  180. // If score() returns undefined, this is false, so we're still OK.
  181. return (score(next) > score(currentbest) &&
  182. (next.year > currentbest.year) &&
  183. DDG.isRelevant(next.title, ignore)) ? next : currentbest;
  184. };
  185. result = DDG_bestResult(api_result.movies, better);
  186. // Favor the first result if the max score is within 1% of the score for the first result.
  187. if (result !== api_result.movies[0] && Math.abs(score(api_result.movies[0]) - max_score) / max_score < 0.1) {
  188. result = api_result.movies[0];
  189. }
  190. // Check if the movie that we have is relevant enough.
  191. if (!DDG.isRelevant(result.title, ignore)) {
  192. return;
  193. }
  194. var checkYear = function(year) {
  195. if (year) {
  196. return " (" + year + ")";
  197. }
  198. return "";
  199. };
  200. if ((result.synopsis && result.synopsis.length) ||
  201. (result.critics_consensus && result.critics_consensus.length)) {
  202. result.hasContent = true;
  203. }
  204. ```
  205. We start by making sure that the `api_result` actually returned 1 or more results, if not we exit out, which will display nothing because no call has been made to `spice.render()`.
  206. We then go on to define some functions used to determine which movie is the most relevant by taking into consideration the rating, release date and relevancy of the title, compared to the search terms. We won't go into the details of how the `score()` and `better()` functions are defined, but you'll notice that inside `better()` we use the function `DDG.isRelevant()`. This function takes as input a string and has an optional second input containing an array of strings. `DDG.isRelevant` can be used to compare the given string to the search terms and returns a `boolean` which lets us know if the input is considered "relevant" to the search terms. The optional 2nd input, the array of strings is called the **skip array** and it contains words which should be ignored when considering the relevancy of the search term and the input to `DDG.isRelevant`. In this case, we are using `DDG.isRelevant` to compare the title of the movies returned from the Rotten Tomatoes API to the user's search term. The skip array contains arbitrary words which are likely to be found in the query, that we assume aren't relevant to the title of the movie.
  207. You'll also notice the use of `DDG_bestResult()`. This function takes as input a list of objects, and a comparator function. It then applies the comparator function, which takes two parameters, `currentbest` and `next` to each consecutive item in the input list. It assumes that the first item is the `currentbest`.
  208. By this point we have either determined that there is a relevant movie to display, or we have found nothing to be relevant and have exited out. If we have a relevant movie, we then call `Spice.render()` as we would in any other Spice instant answer:
  209. ###### movie.js (continued)
  210. ```javascript
  211. Spice.render({
  212. data: result,
  213. source_name: 'Rotten Tomatoes',
  214. template_normal: "movie",
  215. template_small: "movie_small",
  216. force_no_fold: 1,
  217. source_url: result.links.alternate,
  218. header1: result.title + checkYear(result.year),
  219. image_url: result.posters.thumbnail.indexOf("poster_default.gif") === -1 ? result.posters.thumbnail : ""
  220. });
  221. }
  222. ```
  223. This is a fairly simple call to `Spice.render()`, but it slightly differs from other instant answer because it not only defines `template_normal`, the default template to be used, but it also defines `template_small` which is the template to be used when this instant answer is shown in a stacked state i.e., it is shown below another zero click result, but the content is minimal, preferably a single line of text.
  224. Before looking at the implementation of the Handlebars helper functions lets first take a look at the Movie Spice's Handlebars template to see how the helper functions are used:
  225. ###### movie.handlebars
  226. ```html
  227. <div id="movie_data_box" {{#if hasContent}}class="half-width"{{/if}}>
  228. <div>
  229. {{#if ratings.critics_rating}}
  230. <span class="movie_data_item">{{ratings.critics_rating}}</span>
  231. <span class="movie_star_rating">{{{star_rating ratings.critics_score}}}</span>
  232. <div class="movie_data_description">
  233. ({{ratings.critics_score}}% critics,
  234. {{ratings.audience_score}}% audience approved)
  235. </div>
  236. {{else}}
  237. <span>No score yet...</span>
  238. <div class="movie_data_description">
  239. ({{ratings.audience_score}}% audience approved)
  240. </div>
  241. {{/if}}
  242. </div>
  243. <div><span class="movie_data_item">MPAA rating:</span>{{mpaa_rating}}</div>
  244. {{#if runtime}}
  245. <div><span class="movie_data_item">Running time:</span>{{runtime}} minutes</div>
  246. {{/if}}
  247. {{#if abridged_cast}}
  248. <div><span class="movie_data_item">Starring:</span>
  249. {{#concat abridged_cast sep=", " conj=" and "}}<a href="http://www.rottentomatoes.com/celebrity/{{id}}/">{{name}}</a>{{/concat}}.
  250. </div>
  251. {{/if}}
  252. </div>
  253. {{#if hasContent}}
  254. <span>
  255. {{#if synopsis}}
  256. {{condense synopsis maxlen="300"}}
  257. {{else}}
  258. {{condense critics_consensus maxlen="300"}}
  259. {{/if}}
  260. </span>
  261. {{/if}}
  262. ```
  263. In the template, we create a few `div`'s and reference properties of the context just like we did in **NPM** and **Alternative.To**. We also use a few more Handlebars helper functions, `{{#star_rating}}` and `{{#rating_adjective}}` which are defined in **movie.js** as well as `{{#concat}}` and `{{#condense}}`, which we've already discussed, and another block helper, `{{#if}}` (a default Handlebars helper) which should be self-explanatory.
  264. We use the `{{#if}}` helper to check if a variable exists in the current context and then adds the contents of its own block to the template when the input variable does exist. As well, the `{{if}}` block allows the use of the optional `{{else}}` block which lets you add alternate content to the template when the input variable does not exist.
  265. Moving on, let's take a look at the implementation of `{{#star_rating}}`:
  266. ###### movie.js (continued) - star_rating helper
  267. ```javascript
  268. /* star rating */
  269. Handlebars.registerHelper("star_rating", function(score) {
  270. var r = (score / 20) - 1;
  271. var s = "";
  272. if (r > 0) {
  273. for (var i = 0; i < r; i++) {
  274. s += "&#9733;";
  275. }
  276. }
  277. if (s.length == 0) {
  278. s = "0 Stars";
  279. }
  280. return s;
  281. });
  282. ```
  283. As you can see this is a pretty simple function, it takes a number as input, and use that to calculate a star rating. Then creates a string of ASCII stars and returns it to the template which will then be rendered by the browser to show a star rating of the movie.
  284. Now let's take a look at the implementation of `{{#rating_adjective}}`:
  285. ###### movie.js (continued) - rating_adjective helper
  286. ```javascript
  287. /*
  288. * rating_adjective
  289. *
  290. * help make the description of the movie gramatically correct
  291. * used in reference to the rating of the movie, as in
  292. * 'an' R rated movie, or
  293. * 'a' PG rated movie
  294. */
  295. Handlebars.registerHelper("rating_adjective", function() {
  296. return (this.mpaa_rating === "R"
  297. || this.mpaa_rating === "NC-17"
  298. || this.mpaa_rating === "Unrated") ? "an" :"a";
  299. });
  300. ```
  301. Again, this is a fairly simply function which simply returns either "a" or "an" based on the rating of the movie.
  302. Now that you've seen a more advanced instant answer and understand how to use Handlebars helpers, lets look at another advanced instant answer example.
  303. ##Example #4 - Quixey (Advanced Carousel Instant Answer)
  304. The Quixey instant answer is one of our more advanced carousel instant answers which uses a considerable amount of Handlebars helpers and similarly to the **Movie** instant answer has a relevancy checking component. Let's begin by taking a look at the Quixey instant answer's JavaScript:
  305. ###### quixey.js
  306. ```javascript
  307. // spice callback function
  308. function ddg_spice_quixey (api_result) {
  309. if (api_result.result_count == 0) return;
  310. var q = api_result.q.replace(/\s/g, '+');
  311. var relevants = getRelevants(api_result.results);
  312. if (!relevants) return;
  313. Spice.render({
  314. data: api_result,
  315. source_name: 'Quixey',
  316. source_url: 'https://www.quixey.com/search?q=' + q,
  317. header1: api_result.q + ' (App Search)',
  318. force_big_header: true,
  319. more_logo: "quixey_logo.png",
  320. spice_name: 'quixey',
  321. template_frame: "carousel",
  322. template_options: {
  323. template_item: "quixey",
  324. template_detail: "quixey_detail",
  325. items: relevants
  326. }
  327. });
  328. ```
  329. Similarly to **Alternative.To**, the Quixey instant answer uses the carousel, and sets values for all the required carousel-specific properties. However, this instant answer also uses the `force_big_header` property to create a ZeroClick header and subsquently sets the value of the header text, `header1`. Also, the `more_logo` property is set, which allows a custom image to be used instead of the `source_name` text for the "More at" link.
  330. Similarly to the **Movie** instant answer, in the **Quixey** instant answer, we use the `getRelevants()` function (defined below in **Quixey.js**), which is used to check for relevant results before calling `Spice.render()`. We are required to get relevant results in this manner so that only the results we want included in the carousel are passed on to the **quixey.handlebars** template.
  331. Moving on, let's take a look at the implementation of the `getRelevants()` helper:
  332. ###### quixey.js (continued) - getRelevants function
  333. ```javascript
  334. // Check for relevant app results
  335. function getRelevants (results) {
  336. var res,
  337. apps = [],
  338. backupApps = [],
  339. categories = /action|adventure|arcade|board|business|casino|design|developer tools|dice|education|educational|entertainment|family|finance|graphics|graphics and design|health and fitness|kids|lifestyle|medical|music|networking|news|photography|productivity|puzzle|racing|role playing|simulation|social networking|social|sports|strategy|travel|trivia|utilities|video|weather/i,
  340. skip_words = ["app", "apps", "application", "applications", "android", "droid", "google play store", "google play", "windows phone", "windows phone 8", "windows mobile", "blackberry", "apple app store", "apple app", "ipod touch", "ipod", "iphone", "ipad", "ios", "free", "search", "release", release date"];
  341. for (var i = 0; i < results.length; i++) {
  342. app = results[i];
  343. // check if this app result is relevant
  344. if (DDG.isRelevant(app.name.toLowerCase(), skip_words)) {
  345. apps.push(app);
  346. } else if (app.hasOwnProperty("short_desc") &&
  347. DDG.isRelevant(app.short_desc.toLowerCase(), skip_words)) {
  348. backupApps.push(app);
  349. } else if (app.custom.hasOwnProperty("category") &&
  350. DDG.isRelevant(app.custom.category.toLowerCase(), skip_words)) {
  351. backupApps.push(app);
  352. } else{
  353. continue;
  354. }
  355. }
  356. // Return highly relevant results
  357. if (apps.length > 0) {
  358. res = apps;
  359. }
  360. // Return mostly relevant results
  361. else if (backupApps.length > 0) {
  362. res = backupApps;
  363. }
  364. else {
  365. // No relevant results,
  366. // check if it was a categorical search
  367. // Eg."social apps for android"
  368. var q = DDG.get_query();
  369. res = q.match(categories) ? results : null;
  370. }
  371. return res;
  372. });
  373. ```
  374. We begin by defining the function and its input, `results` which is an array of apps. Then we define some variables, notable we define `skip_words`, which we will use later for a call to the `isRelevant()` function we discussed earlier. Then, we move onto a `for` loop which does the bulk of the work by iterating over ever app in the `results` array and applies a series of `isRelevant()` checks to see if either the app name, short description or category are relevant to the search query. If the name is considered to be relevant we add it to the `apps` array which contains all the relevant app results. If the name isn't relevant but the description or category is, we add it to the `backupApps` array, because we might need them later. If none of those properties are considered relevant we simply exclude that app from the set of apps that will be displayed to the user.
  375. After we've checked every app we check to see if there were any relevant apps and if so, we show them to the user. Otherwise, we check our `backupApps` array to see if there were any apps who might be relevant and show those to the user. Failing that, we check if the search was for an app category and if so, we return all the results because the Quixey API is assumed to have relevant results.
  376. Before looking at the implementation of the remaining Quixey Handlebars helpers, lets look at the template to see how the helpers are used:
  377. ###### quixey.handlebars
  378. ```html
  379. <p><img src="{{{icon_url}}}" /></p>
  380. <span>{{{condense name maxlen="40"}}}</span>
  381. ```
  382. This template is very simple, it creates an `<img>` tag, for the resulting app icon and a `<span>` tag for the app name. You may also notice that unlilke **Alternative.To**, we placed the `<img>` tag inside `<p>` tags. We do this to automatically center and align the images, through the use of carousel specific CSS that we wrote, because the images aren't all the same size and would otherwise be missalligned. So, if the images for your instant answer aren't the same size, simply wrap them in `<p>` tags and the carousel will take care of the rest. If not, simply ignore the use of the `<p>` tags.
  383. Now let's take a look at the Quixey `carousel_template_detail` template. This template is more advanced, but most of the content is basic HTML which is populated by various `api_result` properties and Handlebars helpers:
  384. ###### quixey\_detail.handlebars (continued)
  385. ```html
  386. <div id="quixey_preview" style="width: 100%; height: 100%;" app="{{id}}">
  387. <div class="app_info">
  388. <a href="{{{url}}}" class="app_icon_anchor">
  389. <img src="{{{icon_url}}}" class="app_icon">
  390. </a>
  391. <div class="name_wrap">
  392. <a href="{{url}}" class="name" title="{{name}}">{{name}}</a>
  393. ```
  394. Here we create the outer div that wraps the content in the detail area. Note the use of HTML ids and classes - this is to make the css more straightforward, modular and understandable.
  395. ###### quixey\_detail.handlebars (continued)
  396. ```html
  397. {{#if rating}}
  398. <div title="{{rating}}" class="rating">
  399. {{#loop rating}}
  400. <img src="{{quixey_star}}" class="star"></span>
  401. {{/loop}}
  402. </div>
  403. {{/if}}
  404. ```
  405. Here we use the `{{#if}}` block helper and nested inside that, we use our own `{{#loop}}` block helper (defined internally), which simply counts from 0 to the value of its input, each time applying the content of its own block. In this example, we use it to create a one or more star images to represent the app's rating.
  406. ###### quixey\_detail.handlebars (continued)
  407. ```html
  408. <div class="price">{{pricerange}}</div>
  409. <div class="app_description">{{{short_desc}}}</div>
  410. <div id="details_{{id}}" class="app_details">
  411. <div class="app_editions">
  412. {{#each editions}}
  413. <div class="app_edition" title="{{name}} - Rating: {{rating}}">
  414. <a href="{{{url}}}" class="app_platform">
  415. {{#with this.platforms.[0]}}
  416. <img src="{{platform_icon icon_url}}" class="platform_icon">
  417. {{/with}}
  418. {{platform_name}}
  419. {{#if ../hasPricerange}}
  420. - {{price cents}}
  421. {{/if}}
  422. </a>
  423. </div>
  424. {{/each}}
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. <div class="clear"></div>
  430. </div>
  431. ```
  432. Here, we create a few more `<div>`'s and then we use another block helper, `{{#each}}`, which takes an array as input, and iterates over each of the array's elements, using them as the context for the `{{#each}}` block. Nested within the `{{#each}]` helper, we also use the `#{{with}}` block helper, which takes a single object as input, and applies that object as the context for its block. One more interesting thing to note is the input we give to the `{{#if}}` block nested in our `{{#each}}` block. We use the `../` to reference the parent template's context.
  433. Now that we've seen the template and the helpers we're using, let's take a look at how they're all implemented:
  434. ###### quixey.js (continued) - qprice function
  435. ```javascript
  436. // format a price
  437. // p is expected to be a number
  438. function qprice(p) {
  439. if (p == 0) { // == type coercion is ok here
  440. return "FREE";
  441. }
  442. return "$" + (p/100).toFixed(2).toString();
  443. }
  444. ```
  445. This is a simple function that formats a price. We don't register it as a helper because we don't need to use this function directly in our templates, however our helper functions do use this function `qprice()` function.
  446. ###### quixey.js (continued) - price helper
  447. ```javascript
  448. // template helper for price formatting
  449. // {{price x}}
  450. Handlebars.registerHelper("price", function(obj) {
  451. return qprice(obj);
  452. });
  453. ```
  454. This helper function is relatively simple, it takes a number as input, calls the `qprice()` function we just saw, and returns it's output to the template. It essentially abstracts our `qprice()` function into a Handlebars helper. We do this because the next function we'll see also uses `qprice()` and its simply easier to call it as a locally defined function, rather than register it as a helper and then use the `Handlebars.helpers` object to call the `qprice()` function.
  455. ###### quixey.js (continued) - pricerange helper
  456. ```javascript
  457. // template helper to format a price range
  458. Handlebars.registerHelper("pricerange", function(obj) {
  459. if (!this.editions)
  460. return "";
  461. var low = this.editions[0].cents;
  462. var high = this.editions[0].cents;
  463. var tmp, range, lowp, highp;
  464. for (var i in this.editions) {
  465. tmp = this.editions[i].cents;
  466. if (tmp < low) low = tmp;
  467. if (tmp > high) high = tmp;
  468. }
  469. lowp = qprice(low);
  470. if (high > low) {
  471. highp = qprice(high);
  472. range = lowp + " - " + highp;
  473. this.hasPricerange = true;
  474. } else {
  475. range = lowp;
  476. }
  477. return range;
  478. });
  479. ```
  480. This function is a little more complex, it takes an object as input, iterates over the objects keys, and records the highest and lowest prices for the app. Then, it verifies that the range has different high and low values. If not, it simply returns the low price, formatted using our `qprice()` function. Otherwise, it creates a string indicating the range and formats the values with `qprice()`.
  481. ###### quixey.js (continued) - platform\_icons helper
  482. ```javascript
  483. // template helper to replace iphone and ipod icons with
  484. // smaller 'Apple' icons
  485. Handlebars.registerHelper("platform_icon", function(icon_url) {
  486. if (this.id === 2004 || this.id === 2015) {
  487. return "https://icons.duckduckgo.com/i/itunes.apple.com.ico";
  488. }
  489. return "/iu/?u=" + icon_url + "&f=1";
  490. });
  491. ```
  492. Another very simple helper function, the `platform_icon()` function simply checks if its input is equal to `2005` or `2015` and if so returns a special url for the platform icon. If not, it returns the originial icon url but adds our proxy redirect, `/iu/?u=` as previously discussed.
  493. ###### quixey.js (continued) - platform\_name helper
  494. ```javascript
  495. // template helper that returns and unifies platform names
  496. Handlebars.registerHelper("platform_name", function() {
  497. var name;
  498. var platforms = this.platforms;
  499. name = platforms[0].name;
  500. if (platforms.length > 1) {
  501. switch (platforms[0].name) {
  502. case "iPhone" :
  503. case "iPad" :
  504. name = "iOS";
  505. break;
  506. case "Blackberry":
  507. case "Blackberry 10":
  508. name = "Blackberry";
  509. break;
  510. }
  511. }
  512. return name;
  513. });
  514. ```
  515. This helper is also quite simple, it is used to return a platform name and someties also unifies the platform name when multiple platforms exist for an app. If the app is available for both 'iPhone' and 'iPad', the `switch()` will catch this and indicate the app is availabe for "iOS".
  516. ###### quixey.js (continued) - quixey\_star helper
  517. ```javascript
  518. // template helper to give url for star icon
  519. Handlebars.registerHelper("quixey_star", function() {
  520. return DDG.get_asset_path("quixey", "star.png").replace("//", "/");
  521. });
  522. ```
  523. This helper is also very simple, but it is important because it uses the `DDG.get_asset_path()` function which returns the URI for an asset stored in a instant answer's share folder. This is necessary because Spice instant answers and their content are versioned internally. So the URI returned by this function will contain the proper version number, which is required to access any assets.
  524. ##Example #5 - Dictionary (More Advanced Instant Answer)
  525. The dictionary instant answer is a more advanced instant answer than the previous examples, because it requires multiple endpoints (which means it has multiple perl modules -`.pm` files) in order to function properly. You will notice the `definition` endpoint is a subdirectory of the `dictionary` directory: `zeroclickinfo-spice/share/spice/dictionary/definition/`. In the case of the **Dictionary** instant answer, its Perl modules work together as one instant answer, however if the other endpoints worked seperately from the `definition` endpoint, such as they do in the **[Last.FM](https://github.com/duckduckgo/zeroclickinfo-spice/tree/spice2/share/spice/lastfm)** instant answer, they too would each have their own subdirectories and would also each have their own respective JavaScript, Handlebars and CSS files.
  526. To begin, lets look at the first callback function definition in the Dictionary javascript:
  527. ######dictionary_definition.js
  528. ```javascript
  529. // Description:
  530. // Shows the definition of a word.
  531. //
  532. // Dependencies:
  533. // Requires SoundManager2.
  534. //
  535. // Commands:
  536. // define dictionary - gives the definition of the word "dictionary."
  537. //
  538. // Notes:
  539. // ddg_spice_dictionary_definition - gets the definitions of a given word (e.g. noun. A sound or a combination of sounds).
  540. // ddg_spice_dictionary_pronunciation - gets the pronunciation of a word (e.g. wûrd).
  541. // ddg_spice_dictionary_audio - gets the audio file.
  542. // ddg_spice_dictionary_reference - handles plural words. (Improve on this in the future.)
  543. ```
  544. The comments at the beginning of the file explain what the various callbacks are for. Each of these callback functions is connected to a different endpoint, meaning they each belong to a different Perl module. As you can see, the name of each callback corellates to the name of the perlmodule. So `dictionary_definition()` is the callback for `DDG::Spice::Dictionary::Definition`, likewise `dictionary_audio` is for `DDG::Spice::Dictionary::Audio`, etc.
  545. Each of these endpoints are used to make different API calls (either to a different endpoint or possibly even a different API altogether), which can only be done by creating a different Perl module for each endpoint. We can make these endpoints work together for a given instant answer by using the jQuery `getScript()` function which makes an ajax call to a given endpoint, which results in a call to that endpoint's callback function. This function needs to be defined before it is called, so the Dictionary instant answer defines all **four** callback functions in **dictionary_definition.js**
  546. Moving on, let's take a look at the implementation of the `Spice.render()` call and the `dictionary_definition()` callback:
  547. ######dictionary_definition.js (continued) - dictionary_definition callback
  548. ```javascript
  549. // Dictionary::Definition will call this function.
  550. // This function gets the definition of a word.
  551. function ddg_spice_dictionary_definition (api_result) {
  552. "use strict";
  553. var path = "/js/spice/dictionary";
  554. // We moved Spice.render to a function because we're choosing between two contexts.
  555. var render = function(context, word, otherWord) {
  556. Spice.render({
  557. data : context,
  558. header1 : "Definition (Wordnik)",
  559. force_big_header : true,
  560. source_name : "Wordnik",
  561. source_url : "http://www.wordnik.com/words/" + word,
  562. template_normal : "dictionary_definition"
  563. });
  564. // Do not add hyphenation when we're asking for two words.
  565. // If we don't have this, we'd have results such as "black• hole".
  566. if(!word.match(/\s/)) {
  567. $.getScript(path + "/hyphenation/" + word);
  568. }
  569. // Call the Wordnik API to display the pronunciation text and the audio.
  570. $.getScript(path + "/pronunciation/" + otherWord);
  571. $.getScript(path + "/audio/" + otherWord);
  572. };
  573. ```
  574. We begin by wrapping the `Spice.render()` call in a function which also does a little extra work. Specifically after rendering the result it calls the Wordnik API, this time using two different API endpoints. The first gets the pronounciation text, the second gets the audio file for the pronounciation of the word. As mentioned these endpoints are used to work together as one instant answer so using the returns from the seperate API calls we construct one dictionary instant answer result which contains the word definition, the pronounciation text and the audio recording of the pronounciation.
  575. The reason for wrapping the `Spice.render()` call in a function is because we need to be able to call our `render()` function from both the `dictionary_defintion()` callback as well as the `dictionary_reference()` callback, as you will see below:
  576. ######dictionary_definition.js (continued) - dictionary_definition callback
  577. ```javascript
  578. // Expose the render function.
  579. ddg_spice_dictionary_definition.render = render;
  580. // Prevent jQuery from appending "_={timestamp}" in our url when we use $.getScript.
  581. // If cache was set to false, it would be calling /js/spice/dictionary/definition/hello?_=12345
  582. // and that's something that we don't want.
  583. $.ajaxSetup({
  584. cache: true
  585. });
  586. // Check if we have results we need.
  587. if (api_result && api_result.length > 0) {
  588. // Wait, before we display the instant answer, let's check if it's a plural
  589. // such as the word "cacti."
  590. var singular = api_result[0].text.match(/^(?:A )?plural (?:form )?of <xref>([^<]+)<\/xref>/i);
  591. // If the word is plural, then we should load the definition of the word
  592. // in singular form. The definition of the singular word is usually more helpful.
  593. if(api_result.length === 1 && singular) {
  594. ddg_spice_dictionary_definition.pluralOf = api_result[0].word;
  595. $.getScript(path + "/reference/" + singular[1]);
  596. } else {
  597. // Render the instant answer if everything is fine.
  598. render(api_result, api_result[0].word, api_result[0].word);
  599. }
  600. }
  601. };
  602. ```
  603. After defining the `render()` function we give the function a `render` property, `ddg_spice_dictionary_definition.render = render;` (so we can access the `render()` function from other callbacks) and then move on to check if we actually have any definition results returned from the API. If so, we then check if the queried word is a plural word and if so, make another API call for the singular version of the queried word. This call, `$.getScript(path + "/reference/" + singular[1]);` will result in calling the `dictionary_reference()` callback which eventually calls our `render()` function to show our Spice result on the page. If the word is not a plural, we instead immediately call the `render()` function and display our result.
  604. **\*\*Note:** More info on the jQuery `$.getScript()` method is available [here](http://api.jquery.com/jQuery.getScript/).
  605. ######dictionary_definition.js (continued) - dictionary_reference callback
  606. ```javascript
  607. // Dictionary::Reference will call this function.
  608. // This is the part where we load the definition of the
  609. // singular form of the word.
  610. function ddg_spice_dictionary_reference (api_result) {
  611. "use strict";
  612. var render = ddg_spice_dictionary_definition.render;
  613. if(api_result && api_result.length > 0) {
  614. var word = api_result[0].word;
  615. // We're doing this because we want to say:
  616. // "Cacti is the plural form of cactus."
  617. api_result[0].pluralOf = word;
  618. api_result[0].word = ddg_spice_dictionary_definition.pluralOf;
  619. // Render the instant answer.
  620. render(api_result, api_result[0].word, word);
  621. }
  622. };
  623. ```
  624. In this relatively simple callback, we begin by using the previously defined render property of the `dictionary_definiton()` function to give this callback access to the `render()` function we defined at the beginning of `quixey.js`. Then we confirm that this callback's `api_result` actually recieved the singular form of the originially searched query. If so, we add the singular and plural form of the word to our `api_result` object so we can check for and use them later in our Handlebars template.
  625. ######dictionary_definition.js (continued) - dictionary_hyphenation callback
  626. ```javascript
  627. // Dictionary::Hyphenation will call this function.
  628. // We want to add hyphenation to the word, e.g., hello -> hel•lo.
  629. function ddg_spice_dictionary_hyphenation (api_result) {
  630. "use strict";
  631. var result = [];
  632. if(api_result && api_result.length > 0) {
  633. for(var i = 0; i < api_result.length; i += 1) {
  634. result.push(api_result[i].text);
  635. }
  636. // Replace the, rather lame, non-hyphenated version of the word.
  637. $("#hyphenation").html(result.join("•"));
  638. }
  639. };
  640. ```
  641. This callback is also fairly simple. If the API returns a result for the hyphenated version of the word, we loop over the response to get the various parts of the word, then join them with the dot character "•", and inject the text into the HTML of the **#hyphenation** `<div>` using jQuery.
  642. ######dictionary_definition.js (continued) - dictionary_pronunciation callback
  643. ```javascript
  644. // Dictionary::Pronunciation will call this function.
  645. // It displays the text that tells you how to pronounce a word.
  646. function ddg_spice_dictionary_pronunciation (api_result) {
  647. "use strict";
  648. if(api_result && api_result.length > 0 && api_result[0].rawType === "ahd-legacy") {
  649. $("#pronunciation").html(api_result[0].raw);
  650. }
  651. };
  652. ```
  653. Similarly to the `dictionary_hyphenation()` callback, this callback receives a phonetic spelling of the queried word and injects it into the Spice result by using jQuery as well to modify the HTML of the **#pronounciation** `<div>`.
  654. ######dictionary_definition.js (continued) - dictionary_audio callback
  655. ```javascript
  656. // Dictionary::Audio will call this function.
  657. // It gets the link to an audio file.
  658. function ddg_spice_dictionary_audio (api_result) {
  659. "use strict";
  660. var isFailed = false;
  661. var url = "";
  662. var icon = $("#play-button");
  663. // Sets the icon to play.
  664. var resetIcon = function() {
  665. icon.removeClass("widget-button-press");
  666. };
  667. // Sets the icon to stop.
  668. var pressIcon = function() {
  669. icon.addClass("widget-button-press");
  670. };
  671. ```
  672. This callback begins by defining a few simple functions and some variables to used below. Again, jQuery is used to modify the DOM as needed in this callback.
  673. ```javascript
  674. // Check if we got anything from Wordnik.
  675. if(api_result && api_result.length > 0) {
  676. icon.html("▶");
  677. icon.removeClass("widget-disappear");
  678. // Load the icon immediately if we know that the url exists.
  679. resetIcon();
  680. // Find the audio url that was created by Macmillan (it usually sounds better).
  681. for(var i = 0; i < api_result.length; i += 1) {
  682. if(api_result[i].createdBy === "macmillan" && url === "") {
  683. url = api_result[i].fileUrl;
  684. }
  685. }
  686. // If we don't find Macmillan, we use the first one.
  687. if(url === "") {
  688. url = api_result[0].fileUrl;
  689. }
  690. } else {
  691. return;
  692. }
  693. ```
  694. The callback then verifies the API returned a pronunciation of the queried word and if so, injects a play icon, "▶" into the **#play-button** `<button>` and grabs the url for the

Large files files are truncated, but you can click here to view the full file