PageRenderTime 82ms CodeModel.GetById 47ms RepoModel.GetById 1ms app.codeStats 0ms

/duckduckhack/spice/spice_frontend_walkthroughs.md

https://github.com/DavidMascio/duckduckgo-documentation
Markdown | 1010 lines | 783 code | 227 blank | 0 comment | 0 complexity | cfcc143a201089ec22afa0f4e572c569 MD5 | raw file
Possible License(s): Apache-2.0

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

  1. ## Spice Frontend Walkthoughs
  2. - [Walkthrough #1: Alternative.To (Simple)](#walkthrough-1-alternativeto-simple)
  3. - [Walkthrough #2: Movies (Medium)](#walkthrough-2-movies-medium)
  4. - [Walkthrough #3: Airlines (Medium)](#walkthrough-3-airlines-medium)
  5. - [Walkthrough #4: Quixey (Advanced)](#walkthrough-4-quixey-advanced)
  6. <!-- /summary -->
  7. ------
  8. ## Walkthrough #1 - Alternative.To (Simple)
  9. The Alternative.To instant answer is very similar to NPM in that it's also relatively simple. However, it returns multiple items, and so it produces a tile view, where each tile represents a single result item. Let's take a look at the code and see how this is done:
  10. <!-- /summary -->
  11. ###### alternative_to.js
  12. ```javascript
  13. (function(env) {
  14. "use strict";
  15. env.ddg_spice_alternative_to = function(api_result) {
  16. if (!api_result || !api_result.Items) {
  17. return Spice.failed('alternative_to');
  18. }
  19. Spice.add({
  20. id: 'alternative_to',
  21. name: 'Software',
  22. data: api_result.Items,
  23. signal: 'high',
  24. meta: {
  25. searchTerm: api_result.Name,
  26. itemType: 'Alternatives',
  27. sourceUrl: 'http://alternativeto.net/',
  28. sourceName: 'AlternativeTo'
  29. },
  30. normalize: function(item) {
  31. return {
  32. ShortDescription: DDG.strip_html(DDG.strip_href(item.ShortDescription)),
  33. url: item.Url,
  34. icon: item.IconUrl,
  35. title: item.Name,
  36. description: item.ShortDescription
  37. };
  38. },
  39. templates: {
  40. group: 'icon',
  41. options: {
  42. footer: Spice.alternative_to.footer
  43. }
  44. }
  45. });
  46. };
  47. Handlebars.registerHelper("AlternativeTo_getPlatform", function (platforms) {
  48. return (platforms.length > 1) ? "Multiplatform" : platforms[0];
  49. });
  50. }(this));
  51. ```
  52. Just like the NPM Spice, Alternative.To uses `Spice.add()` with most of the same properties. However, it also uses a few new properties as well. The biggest difference between the two Spices though, is that the AlternativeTo API returns an array of items, which is given to `data`, rather than a single item like the NPM API. This means first and foremost, that we'll be dealing with a tile view, so that each item can be separately displayed. In order to do that, we must specify an `item` template which determines the content of each tile. Let's begin by taking a look at the new `Spice.add()` properties used by Alternative.To:
  53. - `searchTerm` is used to indicate the term that was searched, stripped of unimportant words (e.g., "cat videos" would be "cat") or more formally, this is known as the *[noun adjunct](https://en.wikipedia.org/wiki/Noun_adjunct)*. The `searchTerm` will be used for the MetaBar wording where it reads, for example, "Showing 10 **iTunes** Alternatives", where "iTunes" is the `searchTerm` for the query "alternatives to iTunes".
  54. - `itemType` is the type of item being shown (e.g., Videos, Images, Alternatives) and this is also used in the MetaBar. In the previous example, "Showing 10 **iTunes** Alternatives", "Alternatives" is the `itemType`.
  55. - `normalize` allows you to normalize an item before it is passed on to the template. You can add or modify properties of the item that are used by your templates. In our case, we're using `normalize` to modify the `ShortDescription` property of each item, by removing HTML content from it and we also use it to add a few properties to our `item`, which our template will use.
  56. **\*\*Note:** This function uses jQuery's `$.extend()` method, so it will modify your `data` object by adding any returned properties that don't already exist, or simply overwrite the ones that do.
  57. - `templates` is used to specify the template `group` and any other templates that are being used. Template `options` may also be provided to enable or disable template components used by the chosen `group`. In our case we've specified that we're using the `icon` template group and in the `options` block, we've specified the sub-template to be used for the `footer` component.
  58. The `icon` template has a few features including a `title`, `icon` and `description`. It also has an optional `footer` feature, which is actually a sub-template. We've created a **footer.handlebars** template and placed in the AlternativeTo Spice's share directory, **/share/spice/alternative_to**. We specify in the `options` block that the template to be used for the `footer` feature is the template we've created by referencing its name: `Spice.alternative_to.footer`.
  59. ------
  60. Now, let's take a look at the Footer Handlebars template:
  61. ###### footer.handlebars (in /share/spice/alternative_to/)
  62. ```html
  63. <div>
  64. {{Votes}} likes{{#if Platforms}} &bull; {{AlternativeTo_getPlatform Platforms}}{{/if}}
  65. </div>
  66. ```
  67. As you can see, this is some fairly simple HTML, which contains a few Handlebars expressions referencing properties of `data` as well as some Handlebars **helper** functions (i.e. `if` and `AlternativeTo_getPlatform`).
  68. These helpers are really JavaScript functions, which operate on input and return their content to the template.
  69. The `if` helper is a **block helper**, which acts like a normal `if` statement. When the specified variable exists, the code contained within the block, `{{#if}}...{{/if}}`, is executed. In our case, we use it to make sure the `Platforms` property is defined for the current item (remember we're looping over each item and applying this template) and if so, we add a bullet point, ` &bull; ` and the result of `{{AlternativeTo_getPlatform Platforms}}` to the page.
  70. You may have noticed that `AlternativeTo_getPlatform` is actually defined alongside our `ddg_spice_alternative_to` callback function. Let's take a quick look at it:
  71. ###### alternative_to.js
  72. ```javascript
  73. Handlebars.registerHelper("AlternativeTo_getPlatform", function (platforms) {
  74. return (platforms.length > 1) ? "Multiplatform" : platforms[0];
  75. });
  76. ```
  77. This code demonstrates how Handlebars helpers are created and made available to the templates. Using the `Handlebars.register()` method, you are able to specify the name of the helper you are registering, as well as the function it executes. The `AlternativeTo_getPlatform` helper is very simple: it takes an array as input and depending on the length, returns either the first element in the array, or if more than one element exists, returns the string "Multiplatforms".
  78. The AlternativeTo Spice also uses a little bit of CSS to further customize and perfect the layout of the tiles. Let's take a look at the CSS:
  79. ###### alternative_to.css
  80. ```css
  81. .tile--alternative_to .tile__icon {
  82. float: right;
  83. margin-top: 0;
  84. margin-right: 0;
  85. }
  86. .tile--alternative_to .tile__body {
  87. height: 13em;
  88. }
  89. .tile--alternative_to .tile__footer {
  90. bottom: 0.6em;
  91. }
  92. ```
  93. As you can see, this Spice requires very little CSS. This layout is a bit unique and so we opted to modify the layout of the content slightly. In most case the use of templates will alleviate the need to write custom CSS, however sometimes it is necessary and can be used sparingly.
  94. The most important thing to note is that we have prefaced each of our CSS rules with the class `.tile--alternative_to`. Each Spice instant answer is wrapped in a `<div>` that has a class called `.zci--<spice_name>` where `<spice_name>` matches the Spice's package name. As well, when a tile view is used, *each tile* is wrapped in a `div` that has a class called `.tile--<spice_name>`. These allow us to **namespace** all the CSS rules for each individual Spice and their tiles. The is very important because DuckDuckGo simultaneously loads and triggers multiple Spice instant answers (depending on the query) and so namespaceing the CSS is necessary to ensure that none of our Spices' CSS rules affect other elements on the page. If your Spice requires any CSS, it **must** only target child elements of `.zci--<spice_name>` for the detail area and/or `.tile--<spice_name>` for the tiles.
  95. <More to Come...>
  96. <!-- ## Walkthrough #2: InTheaters (Medium)
  97. The **InTheaters** instant answer is a little more advanced than **NPM** and **Alternative.To**, but it's still fairly easy to understand. Let's start by looking at the JavaScript:
  98. ###### in_theaters.js
  99. -->
  100. <!--
  101. 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.add()`.
  102. 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.
  103. 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`.
  104. 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.add()` as we would in any other Spice instant answer:
  105. ###### movie.js (continued)
  106. ```javascript
  107. Spice.render({
  108. data: result,
  109. source_name: 'Rotten Tomatoes',
  110. template_normal: "movie",
  111. template_small: "movie_small",
  112. force_no_fold: 1,
  113. source_url: result.links.alternate,
  114. header1: result.title + checkYear(result.year),
  115. image_url: result.posters.thumbnail.indexOf("poster_default.gif") === -1 ? result.posters.thumbnail : ""
  116. });
  117. }
  118. ```
  119. This is a fairly simple call to `Spice.add()`, but it slightly differs from other instant answers 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.
  120. Before looking at the implementation of the Handlebars helper functions, let's first take a look at the Movie Spice's Handlebars template to see how the helper functions are used:
  121. ###### movie.handlebars
  122. ```html
  123. <div id="movie_data_box" {{#if hasContent}}class="half-width"{{/if}}>
  124. <div>
  125. {{#if ratings.critics_rating}}
  126. <span class="movie_data_item">{{ratings.critics_rating}}</span>
  127. <span class="movie_star_rating">{{{star_rating ratings.critics_score}}}</span>
  128. <div class="movie_data_description">
  129. ({{ratings.critics_score}}% critics,
  130. {{ratings.audience_score}}% audience approved)
  131. </div>
  132. {{else}}
  133. <span>No score yet...</span>
  134. <div class="movie_data_description">
  135. ({{ratings.audience_score}}% audience approved)
  136. </div>
  137. {{/if}}
  138. </div>
  139. <div><span class="movie_data_item">MPAA rating:</span>{{mpaa_rating}}</div>
  140. {{#if runtime}}
  141. <div><span class="movie_data_item">Running time:</span>{{runtime}} minutes</div>
  142. {{/if}}
  143. {{#if abridged_cast}}
  144. <div><span class="movie_data_item">Starring:</span>
  145. {{#concat abridged_cast sep=", " conj=" and "}}<a href="http://www.rottentomatoes.com/celebrity/{{id}}/">{{name}}</a>{{/concat}}.
  146. </div>
  147. {{/if}}
  148. </div>
  149. {{#if hasContent}}
  150. <span>
  151. {{#if synopsis}}
  152. {{condense synopsis maxlen="300"}}
  153. {{else}}
  154. {{condense critics_consensus maxlen="300"}}
  155. {{/if}}
  156. </span>
  157. {{/if}}
  158. ```
  159. 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.
  160. 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.
  161. Moving on, let's take a look at the implementation of `{{#star_rating}}`:
  162. ###### movie.js (continued) - star_rating helper
  163. ```javascript
  164. /* star rating */
  165. Handlebars.registerHelper("star_rating", function(score) {
  166. var r = (score / 20) - 1;
  167. var s = "";
  168. if (r > 0) {
  169. for (var i = 0; i < r; i++) {
  170. s += "&#9733;";
  171. }
  172. }
  173. if (s.length == 0) {
  174. s = "0 Stars";
  175. }
  176. return s;
  177. });
  178. ```
  179. 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.
  180. Now let's take a look at the implementation of `{{#rating_adjective}}`:
  181. ###### movie.js (continued) - rating_adjective helper
  182. ```javascript
  183. /*
  184. * rating_adjective
  185. *
  186. * help make the description of the movie grammatically correct
  187. * used in reference to the rating of the movie, as in
  188. * 'an' R rated movie, or
  189. * 'a' PG rated movie
  190. */
  191. Handlebars.registerHelper("rating_adjective", function() {
  192. return (this.mpaa_rating === "R"
  193. || this.mpaa_rating === "NC-17"
  194. || this.mpaa_rating === "Unrated") ? "an" :"a";
  195. });
  196. ```
  197. Again, this is a fairly simple function which simply returns either "a" or "an" based on the rating of the movie.
  198. Now that you've seen a more advanced instant answer and understand how to use Handlebars helpers, let's look at another advanced instant answer example.
  199. ## Walkthrough #3 - Quixey (Advanced Carousel Instant Answer)
  200. 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:
  201. ###### quixey.js
  202. ```javascript
  203. // spice callback function
  204. function ddg_spice_quixey (api_result) {
  205. if (api_result.result_count == 0) return;
  206. var q = api_result.q.replace(/\s/g, '+');
  207. var relevants = getRelevants(api_result.results);
  208. if (!relevants) return;
  209. Spice.render({
  210. data: api_result,
  211. source_name: 'Quixey',
  212. source_url: 'https://www.quixey.com/search?q=' + q,
  213. header1: api_result.q + ' (App Search)',
  214. force_big_header: true,
  215. more_logo: "quixey_logo.png",
  216. spice_name: 'quixey',
  217. template_frame: "carousel",
  218. template_options: {
  219. template_item: "quixey",
  220. template_detail: "quixey_detail",
  221. items: relevants
  222. }
  223. });
  224. ```
  225. 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 subsequently 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.
  226. 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.add()`. 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.
  227. Moving on, let's take a look at the implementation of the `getRelevants()` helper:
  228. ###### quixey.js (continued) - getRelevants function
  229. ```javascript
  230. // Check for relevant app results
  231. function getRelevants (results) {
  232. var res,
  233. apps = [],
  234. backupApps = [],
  235. 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,
  236. 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"];
  237. for (var i = 0; i < results.length; i++) {
  238. app = results[i];
  239. // check if this app result is relevant
  240. if (DDG.isRelevant(app.name.toLowerCase(), skip_words)) {
  241. apps.push(app);
  242. } else if (app.hasOwnProperty("short_desc") &&
  243. DDG.isRelevant(app.short_desc.toLowerCase(), skip_words)) {
  244. backupApps.push(app);
  245. } else if (app.custom.hasOwnProperty("category") &&
  246. DDG.isRelevant(app.custom.category.toLowerCase(), skip_words)) {
  247. backupApps.push(app);
  248. } else{
  249. continue;
  250. }
  251. }
  252. // Return highly relevant results
  253. if (apps.length > 0) {
  254. res = apps;
  255. }
  256. // Return mostly relevant results
  257. else if (backupApps.length > 0) {
  258. res = backupApps;
  259. }
  260. else {
  261. // No relevant results,
  262. // check if it was a categorical search
  263. // E.g."social apps for android"
  264. var q = DDG.get_query();
  265. res = q.match(categories) ? results : null;
  266. }
  267. return res;
  268. });
  269. ```
  270. 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 every 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.
  271. 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.
  272. Before looking at the implementation of the remaining Quixey Handlebars helpers, let's look at the template to see how the helpers are used:
  273. ###### quixey.handlebars
  274. ```html
  275. <p><img src="{{{icon_url}}}" /></p>
  276. <span>{{{condense name maxlen="40"}}}</span>
  277. ```
  278. 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 unlike **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 misaligned. 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.
  279. 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:
  280. ###### quixey\_detail.handlebars (continued)
  281. ```html
  282. <div id="quixey_preview" style="width: 100%; height: 100%;" app="{{id}}">
  283. <div class="app_info">
  284. <a href="{{{url}}}" class="app_icon_anchor">
  285. <img src="{{{icon_url}}}" class="app_icon">
  286. </a>
  287. <div class="name_wrap">
  288. <a href="{{url}}" class="name" title="{{name}}">{{name}}</a>
  289. ```
  290. 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.
  291. ###### quixey\_detail.handlebars (continued)
  292. ```html
  293. {{#if rating}}
  294. <div title="{{rating}}" class="rating">
  295. {{#loop rating}}
  296. <img src="{{quixey_star}}" class="star"></span>
  297. {{/loop}}
  298. </div>
  299. {{/if}}
  300. ```
  301. 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.
  302. ###### quixey\_detail.handlebars (continued)
  303. ```html
  304. <div class="price">{{pricerange}}</div>
  305. <div class="app_description">{{{short_desc}}}</div>
  306. <div id="details_{{id}}" class="app_details">
  307. <div class="app_editions">
  308. {{#each editions}}
  309. <div class="app_edition" title="{{name}} - Rating: {{rating}}">
  310. <a href="{{{url}}}" class="app_platform">
  311. {{#with this.platforms.[0]}}
  312. <img src="{{platform_icon icon_url}}" class="platform_icon">
  313. {{/with}}
  314. {{platform_name}}
  315. {{#if ../hasPricerange}}
  316. - {{price cents}}
  317. {{/if}}
  318. </a>
  319. </div>
  320. {{/each}}
  321. </div>
  322. </div>
  323. </div>
  324. </div>
  325. <div class="clear"></div>
  326. </div>
  327. ```
  328. 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.
  329. Now that we've seen the template and the helpers we're using, let's take a look at how they're all implemented:
  330. ###### quixey.js (continued) - qprice function
  331. ```javascript
  332. // format a price
  333. // p is expected to be a number
  334. function qprice(p) {
  335. if (p == 0) { // == type coercion is ok here
  336. return "FREE";
  337. }
  338. return "$" + (p/100).toFixed(2).toString();
  339. }
  340. ```
  341. 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.
  342. ###### quixey.js (continued) - price helper
  343. ```javascript
  344. // template helper for price formatting
  345. // {{price x}}
  346. Handlebars.registerHelper("price", function(obj) {
  347. return qprice(obj);
  348. });
  349. ```
  350. This helper function is relatively simple, it takes a number as input, calls the `qprice()` function we just saw, and returns its 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 it's 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.
  351. ###### quixey.js (continued) - pricerange helper
  352. ```javascript
  353. // template helper to format a price range
  354. Handlebars.registerHelper("pricerange", function(obj) {
  355. if (!this.editions)
  356. return "";
  357. var low = this.editions[0].cents;
  358. var high = this.editions[0].cents;
  359. var tmp, range, lowp, highp;
  360. for (var i in this.editions) {
  361. tmp = this.editions[i].cents;
  362. if (tmp < low) low = tmp;
  363. if (tmp > high) high = tmp;
  364. }
  365. lowp = qprice(low);
  366. if (high > low) {
  367. highp = qprice(high);
  368. range = lowp + " - " + highp;
  369. this.hasPricerange = true;
  370. } else {
  371. range = lowp;
  372. }
  373. return range;
  374. });
  375. ```
  376. 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()`.
  377. ###### quixey.js (continued) - platform\_icons helper
  378. ```javascript
  379. // template helper to replace iphone and ipod icons with
  380. // smaller 'Apple' icons
  381. Handlebars.registerHelper("platform_icon", function(icon_url) {
  382. if (this.id === 2004 || this.id === 2015) {
  383. return "https://icons.duckduckgo.com/i/itunes.apple.com.ico";
  384. }
  385. return "/iu/?u=" + icon_url + "&f=1";
  386. });
  387. ```
  388. 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 original icon url but adds our proxy redirect, `/iu/?u=` as previously discussed.
  389. ###### quixey.js (continued) - platform\_name helper
  390. ```javascript
  391. // template helper that returns and unifies platform names
  392. Handlebars.registerHelper("platform_name", function() {
  393. var name;
  394. var platforms = this.platforms;
  395. name = platforms[0].name;
  396. if (platforms.length > 1) {
  397. switch (platforms[0].name) {
  398. case "iPhone" :
  399. case "iPad" :
  400. name = "iOS";
  401. break;
  402. case "Blackberry":
  403. case "Blackberry 10":
  404. name = "Blackberry";
  405. break;
  406. }
  407. }
  408. return name;
  409. });
  410. ```
  411. This helper is also quite simple, it is used to return a platform name and sometimes 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 available for "iOS".
  412. ###### quixey.js (continued) - quixey\_star helper
  413. ```javascript
  414. // template helper to give url for star icon
  415. Handlebars.registerHelper("quixey_star", function() {
  416. return DDG.get_asset_path("quixey", "star.png").replace("//", "/");
  417. });
  418. ```
  419. 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 an 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.
  420. ## Walkthrough #4 - Dictionary (More Advanced Instant Answer)
  421. 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 sub-directory 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 separately 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 would each have their own sub-directories and would also each have their own respective JavaScript, Handlebars and CSS files.
  422. To begin, let's look at the first callback function definition in the Dictionary JavaScript:
  423. ###### dictionary\_definition.js
  424. ```javascript
  425. // Description:
  426. // Shows the definition of a word.
  427. //
  428. // Dependencies:
  429. // Requires SoundManager2.
  430. //
  431. // Commands:
  432. // define dictionary - gives the definition of the word "dictionary."
  433. //
  434. // Notes:
  435. // ddg_spice_dictionary_definition - gets the definitions of a given word (e.g., noun. A sound or a combination of sounds).
  436. // ddg_spice_dictionary_pronunciation - gets the pronunciation of a word (e.g., wûrd).
  437. // ddg_spice_dictionary_audio - gets the audio file.
  438. // ddg_spice_dictionary_reference - handles plural words. (Improve on this in the future.)
  439. ```
  440. 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 correlates to the name of the perl module. So `dictionary_definition()` is the callback for `DDG::Spice::Dictionary::Definition`, likewise `dictionary_audio` is for `DDG::Spice::Dictionary::Audio`, etc.
  441. 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**
  442. Moving on, let's take a look at the implementation of the `Spice.add()` call and the `dictionary_definition()` callback:
  443. ###### dictionary\_definition.js (continued) - dictionary_definition callback
  444. ```javascript
  445. // Dictionary::Definition will call this function.
  446. // This function gets the definition of a word.
  447. function ddg_spice_dictionary_definition (api_result) {
  448. "use strict";
  449. var path = "/js/spice/dictionary";
  450. // We moved Spice.render to a function because we're choosing between two contexts.
  451. var render = function(context, word, otherWord) {
  452. Spice.render({
  453. data : context,
  454. header1 : "Definition (Wordnik)",
  455. force_big_header : true,
  456. source_name : "Wordnik",
  457. source_url : "http://www.wordnik.com/words/" + word,
  458. template_normal : "dictionary_definition"
  459. });
  460. // Do not add hyphenation when we're asking for two words.
  461. // If we don't have this, we'd have results such as "black• hole".
  462. if(!word.match(/\s/)) {
  463. $.getScript(path + "/hyphenation/" + word);
  464. }
  465. // Call the Wordnik API to display the pronunciation text and the audio.
  466. $.getScript(path + "/pronunciation/" + otherWord);
  467. $.getScript(path + "/audio/" + otherWord);
  468. };
  469. ```
  470. We begin by wrapping the `Spice.add()` 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 pronunciation text, the second gets the audio file for the pronunciation of the word. As mentioned, these endpoints are used to work together as one instant answer, so, using the returns from the separate API calls, we construct one dictionary instant answer result which contains the word definition, the pronunciation text and the audio recording of the pronunciation.
  471. The reason for wrapping the `Spice.add()` call in a function is because we need to be able to call our `render()` function from both the `dictionary_definition()` callback as well as the `dictionary_reference()` callback, as you will see below:
  472. ###### dictionary\_definition.js (continued) - dictionary_definition callback
  473. ```javascript
  474. // Expose the render function.
  475. ddg_spice_dictionary_definition.render = render;
  476. // Prevent jQuery from appending "_={timestamp}" in our url when we use $.getScript.
  477. // If cache was set to false, it would be calling /js/spice/dictionary/definition/hello?_=12345
  478. // and that's something that we don't want.
  479. $.ajaxSetup({
  480. cache: true
  481. });
  482. // Check if we have results we need.
  483. if (api_result && api_result.length > 0) {
  484. // Wait, before we display the instant answer, let's check if it's a plural
  485. // such as the word "cacti."
  486. var singular = api_result[0].text.match(/^(?:A )?plural (?:form )?of <xref>([^<]+)<\/xref>/i);
  487. // If the word is plural, then we should load the definition of the word
  488. // in singular form. The definition of the singular word is usually more helpful.
  489. if(api_result.length === 1 && singular) {
  490. ddg_spice_dictionary_definition.pluralOf = api_result[0].word;
  491. $.getScript(path + "/reference/" + singular[1]);
  492. } else {
  493. // Render the instant answer if everything is fine.
  494. render(api_result, api_result[0].word, api_result[0].word);
  495. }
  496. }
  497. };
  498. ```
  499. 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.
  500. **\*\*Note:** More info on the jQuery `$.getScript()` method is available [here](http://api.jquery.com/jQuery.getScript/).
  501. ###### dictionary\_definition.js (continued) - dictionary_reference callback
  502. ```javascript
  503. // Dictionary::Reference will call this function.
  504. // This is the part where we load the definition of the
  505. // singular form of the word.
  506. function ddg_spice_dictionary_reference (api_result) {
  507. "use strict";
  508. var render = ddg_spice_dictionary_definition.render;
  509. if(api_result && api_result.length > 0) {
  510. var word = api_result[0].word;
  511. // We're doing this because we want to say:
  512. // "Cacti is the plural form of cactus."
  513. api_result[0].pluralOf = word;
  514. api_result[0].word = ddg_spice_dictionary_definition.pluralOf;
  515. // Render the instant answer.
  516. render(api_result, api_result[0].word, word);
  517. }
  518. };
  519. ```
  520. In this relatively simple callback, we begin by using the previously defined render property of the `dictionary_definition()` 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 received the singular form of the originally 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.
  521. ###### dictionary\_definition.js (continued) - dictionary_hyphenation callback
  522. ```javascript
  523. // Dictionary::Hyphenation will call this function.
  524. // We want to add hyphenation to the word, e.g., hello -> hel•lo.
  525. function ddg_spice_dictionary_hyphenation (api_result) {
  526. "use strict";
  527. var result = [];
  528. if(api_result && api_result.length > 0) {
  529. for(var i = 0; i < api_result.length; i += 1) {
  530. result.push(api_result[i].text);
  531. }
  532. // Replace the, rather lame, non-hyphenated version of the word.
  533. $("#hyphenation").html(result.join("•"));
  534. }
  535. };
  536. ```
  537. 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.
  538. ###### dictionary\_definition.js (continued) - dictionary_pronunciation callback
  539. ```javascript
  540. // Dictionary::Pronunciation will call this function.
  541. // It displays the text that tells you how to pronounce a word.
  542. function ddg_spice_dictionary_pronunciation (api_result) {
  543. "use strict";
  544. if(api_result && api_result.length > 0 && api_result[0].rawType === "ahd-legacy") {
  545. $("#pronunciation").html(api_result[0].raw);
  546. }
  547. };
  548. ```
  549. 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 **#pronunciation** `<div>`.
  550. ###### dictionary\_definition.js (continued) - dictionary_audio callback
  551. ```javascript
  552. // Dictionary::Audio will call this function.
  553. // It gets the link to an audio file.
  554. function ddg_spice_dictionary_audio (api_result) {
  555. "use strict";
  556. var isFailed = false;
  557. var url = "";
  558. var icon = $("#play-button");
  559. // Sets the icon to play.
  560. var resetIcon = function() {
  561. icon.removeClass("widget-button-press");
  562. };
  563. // Sets the icon to stop.
  564. var pressIcon = function() {
  565. icon.addClass("widget-button-press");
  566. };
  567. ```
  568. 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.
  569. ```javascript
  570. // Check if we got anything from Wordnik.
  571. if(api_result && api_result.length > 0) {
  572. icon.html("▶");
  573. icon.removeClass("widget-disappear");
  574. // Load the icon immediately if we know that the url exists.
  575. resetIcon();
  576. // Find the audio url that was created by Macmillan (it usually sounds better).
  577. for(var i = 0; i < api_result.length; i += 1) {
  578. if(api_result[i].createdBy === "macmillan" && url === "") {
  579. url = api_result[i].fileUrl;
  580. }
  581. }
  582. // If we don't find Macmillan, we use the first one.
  583. if(url === "") {
  584. url = api_result[0].fileUrl;
  585. }
  586. } else {
  587. return;
  588. }
  589. ```
  590. 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 audio file from the API response.
  591. ```javascript
  592. // Load the sound and set the icon.
  593. var isLoaded = false;
  594. var loadSound = function() {
  595. // Set the sound file.
  596. var sound = soundManager.createSound({
  597. id: "dictionary-sound",
  598. url: "/audio/?u=" + url,
  599. onfinish: function() {
  600. resetIcon();
  601. soundManager.stopAll();
  602. },
  603. ontimeout: function() {
  604. isFailed = true;
  605. resetIcon();
  606. },
  607. whileplaying: function() {
  608. // We add this just in case onfinish doesn't fire.
  609. if(this.position === this.durationEstimate) {
  610. resetIcon();
  611. soundManager.stopAll();
  612. }
  613. }
  614. });
  615. sound.load();
  616. isLoaded = true;
  617. };
  618. ```
  619. Here, we define a function, `loadSound()` that uses the [**SoundManager**](http://www.schillmania.com/projects/soundmanager2/) JavaScript library to load the audio file and also allows us to easily control the playing of the audio. An important piece of this `loadSound()` function is the use of our audio proxy: `url: "/audio/?u=" + url`. Similarly to any images used in a instant answer, any audio files must also be proxied through DuckDuckGo to ensure our users' privacy.
  620. **\*\*Note:** The use of the SoundManager library for this instant answer shouldn't be taken lightly. We chose to use a JavaScript library to ensure cross-browser compatibility but the use of 3rd party libraries is not something we advocate, however since this was an internally written instant answer, we decided to use the SoundManager library for this instant answer as well as all others which utilize audio (e.g., [Forvo](https://duckduckgo.com/?q=pronounce+awesome)).
  621. ```javascript
  622. // Initialize the soundManager object.
  623. var soundSetup = function() {
  624. window.soundManager = new SoundManager();
  625. soundManager.url = "/soundmanager2/swf/";
  626. soundManager.flashVersion = 9;
  627. soundManager.useFlashBlock = false;
  628. soundManager.useHTML5Audio = false;
  629. soundManager.useFastPolling = true;
  630. soundManager.useHighPerformance = true;
  631. soundManager.multiShotEvents = true;
  632. soundManager.ontimeout(function() {
  633. isFailed = true;
  634. resetIcon();
  635. });
  636. soundManager.beginDelayedInit();
  637. soundManager.onready(loadSound);
  638. };
  639. ```
  640. As the comment explains, this function is used to initialize SoundManager so we can then use it to control the audio on the page.
  641. ```javascript
  642. // Play the sound when the icon is clicked. Do not let the user play
  643. // without window.soundManager.
  644. icon.click(function() {
  645. if(isFailed) {
  646. pressIcon();
  647. setTimeout(resetIcon, 1000);
  648. } else if(!icon.hasClass("widget-button-press") && isLoaded) {
  649. pressIcon();
  650. soundManager.play("dictionary-sound");
  651. }
  652. });
  653. ```
  654. Here we define a click handler function using jQuery. Based on the state of the sound widget, `isFailed`, the handler either
  655. ```javascript
  656. // Check if soundManager was already loaded. If not, we should load it.
  657. // See http://www.schillmania.com/projects/soundmanager2/demo/template/sm2_defer-example.html
  658. if(!window.soundManager) {
  659. window.SM2_DEFER = true;
  660. $.getScript("/soundmanager2/script/soundmanager2-nodebug-jsmin.js", soundSetup);
  661. } else {
  662. isLoaded = true;
  663. }
  664. };
  665. ```
  666. Now that we've seen how all the API callback functions are implemented, let's take a look at the Handlebars to see what helpers are used and how the display is built:
  667. ```html
  668. <div>
  669. <b id="hyphenation">{{this.[0].word}}</b>
  670. {{#if this.[0].pluralOf}}
  671. <span> is the plural form of {{this.[0].pluralOf}}</span>
  672. {{/if}}
  673. <span id="pronunciation"></span>
  674. <button id="play-button" class="widget-button widget-disappear"></button>
  675. </div>
  676. {{#each this}}
  677. <div class="definition">
  678. <i>{{part partOfSpeech}}</i>
  679. <span>{{{format text}}}</span>
  680. </div>
  681. {{/each}}
  682. ```
  683. As you can see, the template and layout for the dictionary Spice are relatively simple. We begin by placing the term to be defined in a `<b>` tag. As you can see, to access the element from the context, we need to use a special array notation: `this.[0].word`, where the `[0]` indicates the first element in the array.
  684. We then check if the `this.[0].pluralOf` variable has been set. As you may recall, we set this variable in the `dictionary_reference()` callback function, after checking in the `dictionary_definition()` callback if the queried term is a plural. If the `pluralOf` variable has been set we then create a `<span>` tag and for a sentence to indicate which word the queried word is a plural of.
  685. Then the template creates two empty elements, a `<span>` tag to contain the phonetic spelling, which may or may not be populated by our `dictionary_pronunciation()` callback, depending on whether or not the API has a phonetic spelling for the queried word. Similarly we create an empty `<button>` tag to play an audio recording of the word pronunciation which is potentially populated by the `dictionary_audio()` callback, again if the API has an audio file for the queried word's pronunciation.
  686. The template then uses a Handlebars `{{#each}}` helper to iterate over the context (because it is an array in this case, not an object) and for each element creates a snippet of text indicating the usage of the term (e.g., noun, verb) and provides the definition of the term. This `{{#each}}` helper also uses two Handlebars helpers defined in **dictionary\_definition.js**, `{{part}}` and `{{format}}`. Let's take a look at how they're implemented:
  687. ###### dictionary\_definition.js (continued) - part helper
  688. ```javascript
  689. // We should shorten the part of speech before displaying the definition.
  690. Handlebars.registerHelper("part", function(text) {
  691. "use strict";
  692. var part_of_speech = {
  693. "interjection": "interj.",
  694. "noun": "n.",
  695. "verb-intransitive": "v.",
  696. "verb-transitive": "v.",
  697. "adjective": "adj.",
  698. "adverb": "adv.",
  699. "verb": "v.",
  700. "pronoun": "pro.",
  701. "conjunction": "conj.",
  702. "preposition": "prep.",
  703. "auxiliary-verb": "v.",
  704. "undefined": "",
  705. "noun-plural": "n.",
  706. "abbreviation": "abbr.",
  707. "proper-noun": "n."
  708. };
  709. return part_of_speech[text] || text;
  710. });
  711. ```
  712. As the comment explains, this simple helper function is used to shorten the "part of speech" word returned by the API.
  713. ###### dictionary\_definition.js (continued) - format helper
  714. ```javascript
  715. // Make sure we replace xref to an anchor tag.
  716. // <xref> comes from the Wordnik API.
  717. Handlebars.registerHelper("format", function(text) {
  718. "use strict";
  719. // Replace the xref tag with an anchor tag.
  720. text = text.replace(/<xref>([^<]+)<\/xref>/g,
  721. "<a class='reference' href='https://www.wordnik.com/words/$1'>$1</a>");
  722. return text;
  723. });
  724. ```
  725. This helper is used to create hyperlinks within the word definition text. The Wordnik API we are using for this instant answer provides definitions which often contain words or phrases that are wrapped in `<xref>` tags indicating that Wordnik also has a definition for that word or phrase. This helper is used to replace the `<xref>` tags with `<a>` tags that link to a search for that particular word on **Wordnik.com**.
  726. Now that we have seen the Handlebars template and all looked over all the JavaScript related to the dictionary instant answer, let's take a look at the CSS used to style the display of the result:
  727. ###### dictionary_definition.css
  728. ```css
  729. #spice_dictionary_definition .widget-button {
  730. background: #eee; /* Old browsers */
  731. background: #eee -moz-linear-gradient(top, rgba(255,255,255,.1) 0%, rgba(0,0,0,.1) 100%); /* FF3.6+ */
  732. background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.1)), color-stop(100%,rgba(0,0,0,.1))); /* Chrome,Safari4+ */
  733. background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.1) 0%,rgba(0,0,0,.1) 100%); /* Chrome10+,Safari5.1+ */
  734. background: #eee -o-linear-gradient(top, rgba(255,255,255,.1) 0%,rgba(0,0,0,.1) 100%); /* Opera11.10+ */
  735. background: #eee -ms-linear-gradient(top, rgba(255,255,255,.1) 0%,rgba(0,0,0,.1) 100%); /* IE10+ */
  736. background: #eee linear-gradient(top, rgba(255,255,255,.1) 0%,rgba(0,0,0,.1) 100%); /* W3C */
  737. border-left: 1px solid #ccc;
  738. border-right: 0;
  739. border-top: 0;
  740. border-bottom: 0;
  741. -webkit-border-radius: 4px;
  742. -moz-border-radius: 4px;
  743. border-radius: 4px;
  744. color: #444;
  745. display: inline-block;
  746. font-size: 11px;
  747. font-weight: bold;
  748. text-decoration: none;
  749. text-shadow: 0 1px rgba(255, 255, 255, .75);
  750. cursor: pointer;
  751. line-height: normal;
  752. padding: 2px 5px;
  753. vertical-align: text-bottom;
  754. }
  755. #spice_dictionary_definition .widget-button-press {
  756. border-color: #666;
  757. background: #ccc; /* Old browsers */
  758. background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.25) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */
  759. background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.25)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */
  760. background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.25) 0%,rgba(10,10,10,.4) 100%); /*…

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