Adding abstracts to documents

DuckDuckGo has a pretty good api for getting short abstracts given a query. I figured that it might be nice to use this to demonstrate how the caching can be used to pass in lieu of not having access to the copy/paste buffer in Google Docs. 

There is also a video version of this post if you prefer.

This example is container bound and starts with a document that has some text in it. I'm using some text from the wikipedia entry on the oscars - here' s what it looks like.

So let's say that you want to add some stuff to a document that references some of it's content. In this case, I want to be able to select the name of the film, do something, and have some additional info about the film added to my document. Let's take 'slumdog millionaire'.

First I select the text I want an abstract of. 

Then I take the menu option

Then I go to where I want the information to be placed in the document - I'm choosing at the end - and take the other menu option.

And this gets inserted

Here's how it works

Adding the menu options

Of course this could easily be developed as an Add-on, but since it's just for playing around with, I'm adding it as a custom menu item in a container document. 

function onOpen() {
  .addItem('Get abstract', 'getAbstract')
  .addItem('Insert abstract', 'insertAbstract')

Using Cache

Cache is used for two purposes - when an abstract is retrieved from the DuckDuckGo API, it stores it in cache in case the same query is made later.  The result of the latest query is also stored in the document cache so it can be used for copy pasting. 

The code

The test document and code are available at this link.

Just make a copy. 

Here's the complete solution.

function getAbstract() {
  // get text from current selection
  var query = getAbstractQuery();
  // do the query and write it to cache
  var result = Abstract.getDuck (query);

  // kind of copy paste but using cache
  Abstract.copy (result);

function insertAbstract() {
  var result = Abstract.paste();

  if (result) {
    result.blob = Abstract.getBlob (result);

* will get the active selection 
* create an abstract
* write it to cache
function getAbstractQuery() {
  // concat all the text in a range
  // var c=DocumentApp.getActiveDocument().getSelection().getRangeElements()[0]
  var selection = DocumentApp
  return selection ?
  .reduce(function(p,c) {
    return p + getElementText (
  },'') : '' ;
  * display elements and their children
  * @param {Element} elem the element
  * @param {boolean} [partial=false] element is only partial
  * @param {number} [start] start of partial element
  * @param {number} [finish] finish of partial element
  function getElementText (elem ,partial,start,finish) {
    try {
      // not all elements can be cast as text
      return partial ? 
        elem.asText().getText().slice (start, finish+1) : 
    catch(err) {
      return '';

* gets an abstract of the active selection and stores it to cache
var Abstract = Object.create(null, {
  settings: {
  paste: {
    value: function () {
      // kind of copy past but using cache
      var result = this.settings.COPY_CACHE.get (this.settings.CACHE_KEY);
      return result ? JSON.parse(result) : null;
  copy: {
    value: function (value) {
      // kind of copy past but using cache
      this.settings.COPY_CACHE.put (
        this.settings.CACHE_KEY, JSON.stringify(value)
  * insert an item at current position
  insertAbstract: {
    value:function (item) {
      var doc = DocumentApp.getActiveDocument();
      var cursor = doc.getCursor();
      if(!cursor) return null;
      var target = cursor.getElement();
      var parent = target.getParent().asBody();
      var body = doc.getBody();
      // insert the heading
        parent.getChildIndex(target), item.query
      // insert the image
      if (item.blob) {
        var image = parent.insertImage(parent.getChildIndex(target),item.blob);
        if (item.url) {
          image.setLinkUrl (item.url);
      // insert the paragraph
      parent.insertParagraph(parent.getChildIndex(target), item.abstract);
  * scale to given width
  * @param {InlineImage} inlineImage the image
  * @param {number} width the target width
  * @return {InlineImage} for chaining
  imageScale: {
    value:function (inlineImage, width) {
        inlineImage.getHeight() * width / inlineImage.getWidth()
      return inlineImage;
  * execute a duckduckgo query and return cached abstract
  * @param {string} query the string to search on
  * @param {boolean} [useCache=true] whether to attempt to use cache
  * @return {object} the query abstract and image
    value:function (query, useCache) {
      // use cache?
      var useCache = typeof useCache === typeof undefined ? true : useCache;
      var package = {};
      if (query) {
        var url = this.settings.PROVIDER +
          "?q=" + encodeURIComponent(query) + 
        // see if its in cache
        var cacheResponse = this.settings.DUCK_CACHE.get(url);
        if (!cacheResponse) {
          // if its not there, then do a query
          var response  = UrlFetchApp.fetch (url);
          if (response.getResponseCode() === 200) {
            var result = response.getContentText();
            // convert data
            var data = JSON.parse (result);
           // get and clean the image url
            var imageUrl = data.Image ? 
                data.Image.replace(/['"]+/g, '') : '';
            // get the image blob
            var img = imageUrl ? this.getImageFromUrl(imageUrl) : null;
            // convert to b64
            var b64 = img ? Utilities.base64Encode(img.getBytes()) : '';
            package = {
              abstract: data.Abstract ? data.Abstract : 'no abstract found',
              url:data.Results && data.Results.length && Array.isArray(data.Results) ? 
              data.Results[0].FirstURL : '',
                imageB64: b64,
            // cacheable
            var packageString = JSON.stringify(package);
            if (packageString.length > this.settings.MAX_SIZE) {
              package.tooBig = true;
              package.imageB64 = '';
              packageString = JSON.stringify(package);
            this.settings.DUCK_CACHE.put (url, JSON.stringify(package));

        else {
          package = JSON.parse(cacheResponse);


      return package;
  getBlob: {
    value:function (package) {
      // we already have it
      if (!package.tooBig) {
        return package.imageB64 ? Utilities.newBlob(
          Utilities.base64Decode(package.imageB64)) : null;
      else {
         return package.image ? this.getImageFromUrl(package.image) : null;
  * get an image from a url
  * @param {string} imageUrl the image url
  * @return {Blob|null} te image
  getImageFromUrl: {
    value:function (imageUrl) {
      if (!imageUrl) return null;
      return UrlFetchApp.fetch(imageUrl).getBlob() 

Of course you can use this for any subject matter that has been indexed by DuckDuckGo. This article  is adapted from my Going Gas book.

For more like this, see Google Apps Scripts snippets. Why not join our forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.