Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
DuckDuckGo instant answer plugins based on JavaScript APIs
tag: 0.186

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
lib/DDG
share/spice
t
.gitignore
.travis.yml
Pull_Request_Template.md
README.md
dist.ini
spice2.md
weaver.ini

README.md

DuckDuckHack Spice

This documentation walks you through the process of writing a DuckDuckHack Spice plugin. Before reading this section, make sure you've read the DuckDuckHack Intro Site and the DuckDuckHack Developer's Overview. If you're here to brush up on Spice-related info, go ahead and scroll down. If you're here to learn how to write Spice plugins, head on over to the Spice Overview.

Example

quixey example

Spice Handle Functions

Spice plugins have triggers and handle functions like Goodies, as explained in the Basic tutorial. The difference is that Spice handle functions don't return an instant answer directly like Goodies. Instead, they return arguments used to call a JavaScript callback function that then returns the instant answer.

The JavaScript callback function is defined in another file and is explained in detail in the Spice callback functions section. For now let's concentrate on how it gets called via the Spice handle function.

Usually the Spice plugin flow works like this:

  • Spice plugin is triggered.
  • Spice handle function is called.
  • Spice handle function returns arguments.
  • Arguments are used to make a call to an external JSONP API.
  • The external API returns a JSON object to the Spice callback function.
  • Spice callback function returns instant answer.
  • Instant answer formatted on screen.

The following is an example that calls the XKCD API. Within your zeroclickinfo-spice fork, you would define a similar file in the /lib/DDG/Spice/ directory. This file is named Xkcd.pm.

package DDG::Spice::Xkcd;

use DDG::Spice;

triggers startend => "xkcd";

spice to => 'http://dynamic.xkcd.com/api-0/jsonp/comic/$1';
spice wrap_jsonp_callback => 1;

spice is_cached => 0;

handle remainder => sub {

  if ($_ =~ /^(\d+|r(?:andom)?)$/) {
    return int rand 1122 if $1 =~ /r/;
    return $1;
  }

  return '' if $_ eq '';
  return;
};

1;

To refresh your memory, the triggers keyword tells the plugin system when to call a plugin. In the Basic tutorial we discussed using the start keyword to specify trigger words that need to be present at the beginning of the query.

In this case we are using the trigger keyword startend to specify that the trigger word "xkcd" needs to be present at the beginning of the query or at the ending.

In the handle function we use the remainder keyword to pass along the remainder of the query, after removing the trigger word from it.

handle remainder => sub {

  if ($_ =~ /^(\d+|r(?:andom)?)$/) {
    return int rand 1122 if $1 =~ /r/;
    return $1;
  }

  return '' if $_ eq '';
  return;
};

Our handle function first uses an if statement which checks to see if the remainder matches one of two things using a regular expression:

The \d+ matches 1 or more numerical characters (0-9) while the string r(?:andom)? checks for at least the letter "r" but also checks for the letters "andom" after the "r". The regular expression's round brackets indicate our regular expression will capture whatever matches the inside expression and the pipe ("|") inside the brackets means it will match either the expression to the right or the left. So out regular expression looks to match and capture either a string of digits, or the strings "r" or "random".

The first return line : return int rand 1122 if $1 =~ /r/; returns a random integer if the captured string $1 from the regular expression in our if statement has an "r" in it. If this check evaluates to true, it means the user wants to see a random comic (because they didn't specify a number for the comic they want to use) so we generate a random number for them and return it to the API call.

If at this point the the string did not have an "r" in it, the handle function will not have returned yet, so the next line: return $1 means return whatever was captured in the if statement. Given then previous return statement, this return statement will only be reached if the remainder is only a string of digits, and so they will have been captured and stored in the $1 variable, which we are returning.

If our if statement returns false, it means the remainder* could be empty, or it could be some other string which isn't just digits or the word "random".

The next line: return "" if $_ eq "" returns a blank string when our remainder is equal to a blank string. This means the entire query was just "xkcd" and so the remainder is equal to "".

If our handle function hasn't returned by this point it can only mean that the remainder must be something else. If the original query was "xkcd comics", then the remainder would be "comics" and since this doesn't match any of the previous conditions, we return nothing, which short circuits the eventual external call.

return;

When either a number is given and returned or a random number is generated and returned, we then plug it into the spice to definition.

spice to => 'http://dynamic.xkcd.com/api-0/jsonp/comic/$1';

The $1 value (or int rand) from the return statements will get inserted into the $1 placeholder in the spice to line such that you can plug in parameters to the API call as needed. For passing multiple parameters, check out the Advanced spice handlers section.

Usually JSONP API's have a callback parameter, where we give it a value of "{{callback}}" like this: &callback={{callback}}". This {{callback}} template gets plugged in automatically with the default callback value of ddg_spice_xkcd. That last part (xkcd) is a lowercase version of the plugin name with different words separated by the _ character.

In this case however, the XKCD API doesn't allow for a callback function to be used so we use the next line spice wrap_jsonp_callback => 1; which forces the API's response to use a callback function.

At this point the response moves from the backend to the frontend. The external API sends a JSON object to the callback function that you will also define (as explained in the Spice callback functions section).

Where to go now:

Click to return to the Spice Overview.

Spice Callback Functions

Before reading this section, make sure you've read the basic tutorial, the section on spice handle functions, and the section on testing triggers.

As explained in the Spice handle functions section, a Spice plugin usually calls an external API and returns a JSON object to a callback function. This section explains what that callback function looks like.

Please note: the interface of the callback function is the most beta part of the Spice system, and will be changing soon (for the better). However, you can work away without worrying about what any changes might do to your plugins -- we'll take care of all that.

The callback function is named ddg_spice_plugin_name where plugin_name becomes the name of your plugin. For example, for the Twitter plugin the callback name is ddg_spice_twitter. For multiple word names the CamelCase in the plugin name becomes lower case and separated by _, e.g. HackerNews becomes hacker_news.

Whereas the Spice handle function went in the /lib/DDG/Spice/ directory, the callback function goes in the /share/spice/plugin_name directory. You will need to make that directory. The callback function then gets placed inside a file called spice.js.

Here's a very simple callback function used in the Expatistan Spice at /share/spice/expatistan/spice.js:

function ddg_spice_expatistan(ir) {
    var snippet = '';
    if (ir['status'] == 'OK') {
       snippet = ir['abstract'];
       items = new Array();
       items[0] = new Array();
       items[0]['a'] = snippet;
       items[0]['h'] = '';
       items[0]['s'] = 'Expatistan';
       items[0]['u'] = ir['source_url'];
       nra(items);
    }
}

The end result is a call to the nra function, an internal display function that takes what you send it and formats it for instant answer display.

nra(items);

We're sending it a JavaScript Array we created called items.

items = new Array();

The first item in the Array is the main answer. It is another JavaScript Array.

items[0] = new Array();

An item takes the following parameters.

items[0]['a'] = snippet;

The a param is the required answer. It can be pure HTML in which case it is set via innerHTML. It can also be an object (preferred), in which case onclick and other event handlers won't be destroyed.

The h param is an optional relevant (and relatively short) title.

items[0]['h'] = title;

For a big header (like this example), pass 1 to force_big_header. When using force_big_header, please use our canonical header style in the form <query> (Plugin Name).

items[0]['force_big_header'] = 1;

Source name and URL are required in the s and u blocks. These are used to make the More at X link in all instant answer boxes. Think of it as source attribution.

items[0]['s'] = 'XKCD';
items[0]['u'] = url

An optional image can be passed in the i param. If there is a thumbnail image, we will display it on the right.

items[0]['i'] = image_url

You would usually get the information to make these assignments via the object returned to the callback function. In this case we received it in the ir variable but you can name it anything.

function ddg_spice_expatistan(ir) {

Where to go now:

Click to return to the Spice Overview.

Testing Spice

You should have already tested your Spice triggers by following the Testing triggers section. Once you're confident your triggers are functioning properly, follow these steps to see your Spice plugin on a live server!

Step 1.  Go to the roof of your forked repository.

cd zeroclickinfo-spice/

Step 2.  Start the server.

duckpan server

This command will start up a small Web server running on port 5000 on your machine.

Step 3.  Visit the server in your browser.

You should now be able to go to your duckpan server via a regular Web browser and check it out. It runs code from our site and so generally looks like a real version of DuckDuckGo.

If you're running the duckpan server on the same computer as your Web browser you can navigate to:

http://127.0.0.1:5000/

If you're running the duckpan server on a remote machine, then substitute 127.0.0.1 wither either its IP address or its Fully Qualified Domain Name.

Step 4.  Search.

Given you've already tested your plugin triggers, you should be able to search and see your spice output come through the server. As requests go through the internal Web server they are printed to STDOUT (on the screen). External API calls are highlighted (if you have color turned on in your terminal).

Step 5.  Debug.

If for some reason a search doesn't hit a plugin, there is an error message displayed on the homepage saying "Sorry, no hit for your plugins."

If it does hit and you do not see something displayed on the screen, a number of things could be going wrong.

  • You have a JavaScript error of some kind. Check out the JavaScript console for details. Personally we like to use Firebug internally.

  • The external API was not called correctly. You should be able to examine the Web server output to make sure the right API is being called. If it's not you will need to revise your Spice handle function.

  • The external API did not return anything. Firebug is great for checking this as well. You should see the call in your browser and then you can examine the response.

Step 6.  Tweak the display.

Once everything is working properly (and you have stuff displayed on screen), you will want to mess with your callback function to get the display nice and perfect. Check out the Guidelines for some pointers.

Step 7.  Document.

Finally, please document as much as possible using in-line comments.

Where to go now:

Click to return to the Spice Overview.


Advanced Spice

Advanced Spice Handlers

These advanced handle function techniques are specific to Spice plugins:

Multiple parameters in spice_to call. If you need to substitute multiple parameters into the API call like how the RandWord Spice uses two numbers to specify the min and max length of the random word, you can use from keyword.

spice from => '(?:([0-9]+)\-([0-9]+)|)';

Whatever you return from the handle function gets sent to this spice from regexp, which then gets fed into the spice to API.

spice to => 'http://api.wordnik.com/v4/words.json/randomWord?minLength=$1&maxLength=$2&api_key={{ENV{DDG_SPICE_RANDWORD_APIKEY}}}&callback={{callback}}';

In this case, the two capture blocks will be put into $1 and $2 respectively.

The reason why you do not need to specify a from keyword by default is that the default is (.*), which means whatever you return gets put into $1.

Feeding multiple arguments to spice from. You can have multiple return values in your handle function like the AlternativeTo Spice.

return $prog, $platform, $license;

In this case they are URL encoded and joined together with '/' chars, e.g. in this case $prog/$platform/$license. Then that full string is fed into the spice from regexp.

spice from => '([^/]+)/?(?:([^/]+)/?(?:([^/]+)|)|)';

API Keys. Some APIs require API keys to function properly like in the RandWord Spice. You can insert an API key for testing in the callback function and replace it with a variable reference when submitting.

spice to => 'http://api.wordnik.com/v4/words.json/randomWord?minLength=$1&maxLength=$2&api_key={{ENV{DDG_SPICE_RANDWORD_APIKEY}}}&callback={{callback}}';

You can set the variable when you start duckpan server like this:

DDG_SPICE_RANDWORD_APIKEY=xyz duckpan server

JSON -> JSONP. Some APIs don't do JSONP by default, i.e. don't have the ability to return the JSON object to a callback function. In this case, first you should try to contact the API provider and see if it can be added. Where it cannot, you can tell us to wrap the JSON object return in a callback function like in the XKCD Spice.

spice wrap_jsonp_callback => 1;

Pure JS functions. Sometimes no external API is necessary to deliver the instant answer like how the Flash Version Spice just prints out your Flash Player version using an internal call.

In cases like these you can define a spice_call_type as 'self' like this:

spice call_type => 'self';

Then in the handle function you can return call, e.g.:

return $_ eq 'flash version' ? call : ();

The return of call will run whatever is in the call_type setting. self is a special keyword to just run the callback function directly, in this case ddg_spice_flash_version().

No caching of the external API call. By default, we cache return values from external providers for speed. We use nginx and get this functionality by using the proxy_cache_valid directive. You can override our default behavior by setting your own proxy_cache_valid directive like in the RandWord Spice.

spice proxy_cache_valid => "418 1d";

This is a special declaration that says don't cache. Actually it says cache only 418 HTTP return values for 1 day. Since regular return codes are 200 and 304, nothing will get cached.

If you wanted to say cache those normal values for 1h, you could do:

spice proxy_cache_valid => "200 304 1d";

Advanced Spice Callbacks

In the Spice callback functions section we walked through a simple callback function used in the Expatistan Spice.

Here are some more advanced callback techniques you may need to use:

Setting styles. We use YUI2, which is a JavaScript framework like JQuery. To set styles you can do:

YAHOO.util.Dom.setStyle(obj,'margin-top','5px');

You can also use an id directly like:

YAHOO.util.Dom.setStyle('id','margin-top','5px');

Creating images. We have an internal function for image creation called nur. In the XKCD spice it is used in this construct:

       if (nur) img = nur('',xk['alt'],xk['img']);
       if (img) {

Ignore the first argument. The second is the alt text (title); third is img URL; fourth and fifth optional arguments are explicit height and width to use (in px).

Big images. If you have a big image that may be too big like in the XKCD spice, use the class img_zero_click_big that will resize it appropriately.

You can add classes like this:

YAHOO.util.Dom.addClass(img,'img_zero_click_big');

And again you can pass in an id directly like:

YAHOO.util.Dom.addClass('id','img_zero_click_big');
Something went wrong with that request. Please try again.