• Jump To … +
    analytics.coffee background.coffee content.coffee i18n.coffee install.coffee log.coffee options.coffee popup.coffee store.coffee utils.coffee
  • background.coffee

  • ¶

    Template
    (c) 2014 Alasdair Mercer
    Freely distributable under the MIT license:
    http://template-extension.org/license

  • ¶

    Private constants

  • ¶

    List of blacklisted extension IDs that should be prevented from sending external messages to Template.

    BLACKLIST         = []
  • ¶

    Predefined templates to be used by default.

    DEFAULT_TEMPLATES = [
      content:  '{url}'
      enabled:  yes
      image:    'globe'
      index:    0
      key:      'PREDEFINED.00001'
      readOnly: yes
      shortcut: 'U'
      title:    i18n.get 'default_template_url'
      usage:    0
    ,
      content:  '{#shorten}{url}{/shorten}'
      enabled:  yes
      image:    'tag'
      index:    1
      key:      'PREDEFINED.00002'
      readOnly: yes
      shortcut: 'S'
      title:    i18n.get 'default_template_short'
      usage:    0
    ,
      content:  "<a href=\"{url}\"{#linksTarget} target=\"_blank\"{/linksTarget}{#linksTitle}
     title=\"\"{/linksTitle}></a>"
      enabled:  yes
      image:    'font'
      index:    2
      key:      'PREDEFINED.00003'
      readOnly: yes
      shortcut: 'A'
      title:    i18n.get 'default_template_anchor'
      usage:    0
    ,
      content:  '{#encode}{url}{/encode}'
      enabled:  yes
      image:    'lock'
      index:    3
      key:      'PREDEFINED.00004'
      readOnly: yes
      shortcut: 'E'
      title:    i18n.get 'default_template_encoded'
      usage:    0
    ,
      content:  '[url={url}]{title}[/url]'
      enabled:  no
      image:    'comment'
      index:    5
      key:      'PREDEFINED.00005'
      readOnly: yes
      shortcut: 'B'
      title:    i18n.get 'default_template_bbcode'
      usage:    0
    ,
      content:  '[{title}]({url}{#linksTitle} "{title}"{/linksTitle})'
      enabled:  no
      image:    'asterisk'
      index:    4
      key:      'PREDEFINED.00006'
      readOnly: yes
      shortcut: 'M'
      title:    i18n.get 'default_template_markdown'
      usage:    0
    ,
      content:  '{selectionMarkdown}'
      enabled:  no
      image:    'italic'
      index:    6
      key:      'PREDEFINED.00007'
      readOnly: yes
      shortcut: 'I'
      title:    i18n.get 'default_template_markdown_selection'
      usage:    0
    ]
  • ¶

    Extension ID being used by Template.

    EXTENSION_ID      = i18n.get '@@extension_id'
  • ¶

    Domain of this extension’s homepage.

    HOMEPAGE_DOMAIN   = 'template-extension.org'
  • ¶

    List of known operating systems that could be used by the user.

    OPERATING_SYSTEMS = [
      substring: 'Win'
      title:     'Windows'
    ,
      substring: 'Mac'
      title:     'Mac'
    ,
      substring: 'Linux'
      title:     'Linux'
    ]
  • ¶

    Milliseconds before the popup automatically resets or closes, depending on user preferences.

    POPUP_DELAY       = 600
  • ¶

    Regular expression used to extract useful information from different variations of names for tags that are evaluated/executed later.

    R_EXPRESSION_TAG  = /^(select|xpath)(all)?(\S*)?$/
  • ¶

    Regular expression used to detect upper case characters.

    R_UPPER_CASE      = /[A-Z]+/
  • ¶

    Very primitive regular expression used to perform simple URL validation.

    R_VALID_URL       = /^https?:\/\/\S+\.\S+/i
  • ¶

    Extension ID of the production version of Template.

    REAL_EXTENSION_ID = 'dcjnfaoifoefmnbhhlbppaebgnccfddf'
  • ¶

    List of URL shortener services supported by Template.

    SHORTENERS        = [
  • ¶

    Setup bitly.

      name: 'bitly'
      title: i18n.get 'shortener_bitly'
      method: 'GET'
    
      getHeaders: ->
        'Content-Type': 'application/x-www-form-urlencoded'
    
      getParameters: (url) ->
        params =
          format:  'json'
          longUrl: url
        if @oauth.hasAccessToken()
          params.access_token = @oauth.getAccessToken()
        else
          params.apiKey = ext.config.services.bitly.api_key
          params.login  = ext.config.services.bitly.login
        params
    
      getUsage: ->
        store.get 'bitly.usage'
    
      input: ->
        null
    
      isEnabled: ->
        store.get 'bitly.enabled'
    
      oauth: ->
        options = _.pick ext.config.services.bitly, 'client_id', 'client_secret'
        new OAuth2 'bitly', options
    
      output: (resp) ->
        JSON.parse(resp).data.url
    
      url: ->
        ext.config.services.bitly.url
    ,
  • ¶

    Setup Google URL Shortener.

      name: 'googl'
      title: i18n.get 'shortener_googl'
      method: 'POST'
    
      getHeaders: ->
        headers = 'Content-Type': 'application/json'
        headers.Authorization = "OAuth #{@oauth.getAccessToken()}" if @oauth.hasAccessToken()
        headers
    
      getParameters: ->
        unless @oauth.hasAccessToken()
          key: ext.config.services.googl.api_key
    
      getUsage: ->
        store.get 'googl.usage'
    
      input: (url) ->
        JSON.stringify longUrl: url
    
      isEnabled: ->
        store.get 'googl.enabled'
    
      oauth: ->
        options = _.pick ext.config.services.googl, 'api_scope', 'client_id', 'client_secret'
        new OAuth2 'google', options
    
      output: (resp) ->
        JSON.parse(resp).id
    
      url: ->
        ext.config.services.googl.url
    ,
  • ¶

    Setup YOURLS.

      name: 'yourls'
      title: i18n.get 'shortener_yourls'
      method: 'POST'
    
      getHeaders: ->
        'Content-Type': 'application/json'
    
      getParameters: (url) ->
        params =
          action: 'shorturl'
          format: 'json'
          url:    url
    
        {authentication, password, signature, username} = store.get 'yourls'
        switch authentication
          when 'advanced'
            params.signature = signature if signature
          when 'basic'
            params.password  = password  if password
            params.username  = username  if username
    
        params
    
      getUsage: ->
        store.get 'yourls.usage'
    
      input: ->
        null
    
      isEnabled: ->
        store.get 'yourls.enabled'
    
      output: (resp) ->
        JSON.parse(resp).shorturl
    
      url: ->
        store.get 'yourls.url'
    ]
  • ¶

    List of extensions supported by Template and used for compatibility purposes.

    SUPPORT           =
  • ¶

    Setup IE Tab.

      hehijbfgiekmjfkfjpbkbammjbdenadd: (tab) ->
        if tab.title
          str       = 'IE: '
          idx       = tab.title.indexOf str
          tab.title = tab.title.substring idx + str.length if idx isnt -1
    
        if tab.url
          str     = 'iecontainer.html#url='
          idx     = tab.url.indexOf str
          tab.url = decodeURIComponent tab.url.substring idx + str.length if idx isnt -1
  • ¶

    Setup IE Tab Classic.

      miedgcmlgpmdagojnnbemlkgidepfjfi: (tab) ->
        if tab.url
          str     = 'ie.html#'
          idx     = tab.url.indexOf str
          tab.url = tab.url.substring idx + str.length if idx isnt -1
  • ¶

    Setup IE Tab Multi (Enhance).

      fnfnbeppfinmnjnjhedifcfllpcfgeea: (tab) ->
        if tab.url
          str = 'navigate.html?chromeurl='
          idx = tab.url.indexOf str
          if idx isnt -1
            tab.url = tab.url.substring idx + str.length
            str     = '[escape]'
            tab.url = decodeURIComponent tab.url[str.length..] unless tab.url.indexOf str
  • ¶

    Setup Mozilla Gecko Tab.

      icoloanbecehinobmflpeglknkplbfbm: (tab) ->
        if tab.url
          str = 'navigate.html?chromeurl='
          idx = tab.url.indexOf str
          if idx isnt -1
            tab.url = tab.url.substring idx + str.length
            str     = '[escape]'
            tab.url = decodeURIComponent tab.url[str.length..] unless tab.url.indexOf str
  • ¶

    Used by Chrome to represent an unknown language (i.e. one that it couldn’t detect).

    UNKNOWN_LOCALE    = 'und'
  • ¶

    Private variables

  • ¶

    Details of the current browser.

    browser           =
      title:   'Chrome'
      version: ''
  • ¶

    Indicate whether or not Template has just been installed.

    isNewInstall      = no
  • ¶

    Indicate whether or not Template is currently running the production build.

    isProductionBuild = EXTENSION_ID is REAL_EXTENSION_ID
  • ¶

    Name of the user’s operating system.

    operatingSystem   = ''
  • ¶

    Private functions

  • ¶

    Inject and execute the content.coffee and install.coffee scripts within all of the tabs (where valid) of each Chrome window.

    executeScriptsInExistingWindows = ->
      log.trace()
    
      chrome.tabs.query {}, (tabs) ->
        log.info 'Checking the following tabs for content script execution...', tabs
  • ¶

    Check tabs are not displaying a protected page (i.e. one that will cause an error if an attempt is made to execute content scripts).

        for tab in tabs when not isProtectedPage tab
          chrome.tabs.executeScript tab.id, file: 'lib/content.js'
  • ¶

    Only execute inline installation content script for tabs displaying a page on Template’s homepage domain.

          chrome.tabs.executeScript tab.id, file: 'lib/install.js' if HOMEPAGE_DOMAIN in tab.url
  • ¶

    Attempt to derive the current version of the user’s browser.

    getBrowserVersion = ->
      log.trace()
    
      str = navigator.userAgent
      idx = str.indexOf browser.title
      if idx isnt -1
        str = str.substring idx + browser.title.length + 1
        idx = str.indexOf ' '
        if idx is -1 then str else str[0...idx]
  • ¶

    Build a list of shortcuts used by enabled templates.

    getHotkeys = ->
      log.trace()
    
      template.shortcut for template in ext.templates when template.enabled
  • ¶

    Derive the operating system being used by the user.

    getOperatingSystem = ->
      log.trace()
    
      return os.title for os in OPERATING_SYSTEMS when os.substring in navigator.platform
      navigator.platform
  • ¶

    Attempt to retrieve the template with the specified key.

    getTemplateWithKey = (key) ->
      log.trace()
    
      _.findWhere ext.templates, {key}
  • ¶

    Attempt to retrieve the template with the specified menuId.

    getTemplateWithMenuId = (menuId) ->
      log.trace()
    
      _.findWhere ext.templates, {menuId}
  • ¶

    Attempt to retrieve the template with the specified keyboard shortcut.
    Exclude disabled templates from this query.

    getTemplateWithShortcut = (shortcut) ->
      log.trace()
    
      _.findWhere ext.templates, {enabled: yes, shortcut}
  • ¶

    Determine whether or not the identified extension is blacklisted.

    isBlacklisted = (extensionId) ->
      log.trace()
    
      return yes for extension in BLACKLIST when extension is extensionId
      no
  • ¶

    Determine whether or not the specified extension is active on tab.

    isExtensionActive = (tab, extensionId) ->
      log.trace()
    
      log.debug "Checking activity of supported extension '#{extensionId}'"
    
      isSpecialPage(tab) and extensionId in tab.url
  • ¶

    Determine whether or not tab is currently displaying a protected page (i.e. a page that content scripts cannot be executed on).

    isProtectedPage = (tab) ->
      log.trace()
    
      isSpecialPage(tab) or isWebStore tab
  • ¶

    Determine whether or not tab is currently displaying a special page (i.e. a page that is internal to the browser).

    isSpecialPage = (tab) ->
      log.trace()
    
      return yes for protocol in ['chrome', 'view-source'] when not tab.url.indexOf protocol
      no
  • ¶

    Determine whether or not tab is currently displaying a page on the Chrome Web Store.

    isWebStore = (tab) ->
      log.trace()
    
      not tab.url.indexOf 'https://chrome.google.com/webstore'
  • ¶

    Ensure null is returned instead of object if it is empty.

    nullIfEmpty = (object) ->
      log.trace()
    
      if _.isEmpty object then null else object
  • ¶

    Listener for internal messages sent to the extension.
    External messages are also routed through here, but only after being checked that they do not originate from a blacklisted extension.

    onMessage = (message, sender, sendResponse) ->
      log.trace()
  • ¶

    Safely handle callback functionality.

      callback = utils.callback sendResponse
  • ¶

    Message type is required. Informal rejection.

      return do callback unless message.type
  • ¶

    Don’t allow shortcut requests when shortcuts are disabled.

      return do callback if message.type is 'shortcut' and not store.get 'shortcuts.enabled'
  • ¶

    Select or create a tab for the Options page.

      if message.type is 'options'
        selectOrCreateTab utils.url 'pages/options.html'
  • ¶

    Close the popup if it’s currently open. This should happen naturally but is being forced to ensure consistency.

        chrome.extension.getViews(type: 'popup')[0]?.close()
        return do callback
  • ¶

    Info requests are simple, just send some useful information back. Done!

      if message.type in ['info', 'version']
        return callback
          hotkeys: do getHotkeys
          id:      EXTENSION_ID
          version: ext.version
  • ¶

    Variables to maintain the various states for this request.

      active       = data   = output   = template = null
      editable     = link   = shortcut = no
      id           = utils.keyGen '', null, 't', no
      placeholders = {}
    
      async.series [
        (done) ->
  • ¶

    Find the active tab within the current window.

          chrome.tabs.query {active: yes, currentWindow: yes}, (tabs) ->
            log.debug 'Checking the following tabs for the active tab...', tabs
            active = _.first tabs
            do done
    
        (done) ->
  • ¶

    Return a callback function to be used by a tag to indicate that it requires further action.
    Replace the tag with the unique placeholder so that it can be rendered again later with the final value.

          getCallback = (tag) ->
            (text, render) ->
              text = render text if text
              text = @url        if tag is 'shorten' and not text
              trim = text?.trim() or ''
              log.debug "Following is the contents of a #{tag} tag...", text
  • ¶

    If trim doesn’t appear to be a valid so just return the rendered text.

              return text if not trim or tag is 'shorten' and not R_VALID_URL.test trim
  • ¶

    Search for existing placeholder to improve performance later down the line (e.g. multiple URL shortener requests for the same URL).

              for own key, val of placeholders
                if val.data is trim and val.tag is tag
                  placeholder = key
                  break
  • ¶

    Ensure the placeholder is stored correctly for later use.

              unless placeholder?
                placeholder = utils.keyGen '', null, 'c', no
                placeholders[placeholder] = {data: trim, tag}
  • ¶

    Sections are re-rendered so the context must have a property for the placeholder the replaces itself with itself so that it still when it comes to replacing with the final value.

                this[placeholder] = "{#{placeholder}}"
    
              log.debug "Replacing #{tag} tag with #{placeholder} placeholder"
              "{#{placeholder}}"
  • ¶

    If the popup is currently displayed, hide the template list and show a loading animation.

          updateProgress null, yes
    
          try
  • ¶

    Attempt to derive the contextual template data.

            template = deriveMessageTempate message
            updateProgress 10
    
            {data, editable, link, shortcut} = deriveMessageInfo message, active, getCallback
            updateProgress 20
    
            do done
          catch err
  • ¶

    Oops! Something went wrong so we should probably let the user know.

            log.error err
    
            if err instanceof AppError
              done err
            else
              done new AppError if err instanceof URIError
                'result_bad_uri_description'
              else
                'result_bad_error_description'
    
        (done) ->
          updateProgress 30
  • ¶

    Extract additional data from the environment.

          addAdditionalData active, data, id, editable, shortcut, link, ->
            updateProgress 40
  • ¶

    To complete the data, simply extend it using template.

            $.extend yes, data, {template}
  • ¶

    Ensure all properties of data are lower case.

            transformData data
            log.debug "Using the following data to render #{template.title}...", data
  • ¶

    Render the initial template output based on data.
    This may be rendered again to replace any temporary placeholders that were inserted.

            if template.content
              output = Mustache.render template.content, data
              log.debug 'The following was generated as a result...', output
    
            updateProgress 60
            output ?= ''
    
            do done
    
        (done) ->
          updateProgress 70
  • ¶

    Only proceed if any placeholders were inserted and replaced, as they need to be handled now in order to replace any placeholders with their final values.

          return do done if _.isEmpty placeholders
  • ¶

    Maps to populated with relevant data derived from their related stored placeholders.

          expressionMap = {}
          shortenMap    = {}
    
          for own placeholder, info of placeholders
            if info.tag is 'shorten'
              shortenMap[placeholder] = info.data
            else
              match = info.tag.match R_EXPRESSION_TAG
              if match
                expressionMap[placeholder] =
                  all:        match[2]?
                  convertTo:  match[3]
                  expression: info.data
                  type:       match[1]
    
          async.series [
            (done) ->
              updateProgress 80
  • ¶

    Only proceed if any expression (e.g. select, xpath) placeholders were used.

              return do done if _.isEmpty expressionMap
  • ¶

    Evaluate all of the expressions within the active tab and update expressionMap with the results.

              evaluateExpressions active, expressionMap, (err) ->
                updateProgress 85
    
                unless err
                  log.info "#{_.size expressionMap} expression(s) were evaluated"
  • ¶

    Update the corresponding placeholders with their result.

                  placeholders[placeholder] = value for own placeholder, value of expressionMap
    
                done err
    
            (done) ->
              updateProgress 90
  • ¶

    Only proceed if any URL shortener placeholders were used.

              return do done if _.isEmpty shortenMap
  • ¶

    Call the active URL shortener service for each of the placeholders.

              callUrlShortener shortenMap, (err, response) ->
                updateProgress 95
    
                unless err
                  log.info "URL shortener service was called #{_.size shortenMap} time(s)"
  • ¶

    Update the URL shortener service’s usage to ensure captured statistics are accurate.

                  updateUrlShortenerUsage response.service.name, response.oauth
  • ¶

    Update the corresponding placeholders with their result.

                  placeholders[placeholder] = value for own placeholder, value of shortenMap
    
                done err
    
          ], (err) ->
            unless err
  • ¶

    Request(s) were successful so render the output again.

              output = Mustache.render output, placeholders
              log.debug 'The follow was re-generated as a result...', output
    
            done err
    
      ], (err) ->
        updateProgress 100
  • ¶

    Transform message type into a more user-friendly form.

        type = utils.capitalize message.type
    
        analytics.track 'Requests', 'Processed', type, if shortcut then message.data.key.charCodeAt 0
  • ¶

    Ensure that the user is notified if they have attempted to copy an empty string to the system clipboard.

        if not err and not output
          err = new AppError 'result_bad_empty_description', template.title
    
        notification = ext.notification
    
        if err
  • ¶

    Notify the user that an error occurred while processing the copy request.

          log.warn err.message
    
          notification.message = err.message ? i18n.get 'result_bad_description', template.title
          notification.title   = i18n.get 'result_bad_title'
    
          do showNotification
        else
  • ¶

    Update the activated template’s usage to ensure captured statistics accurate.

          updateTemplateUsage template.key
          do updateStatistics
  • ¶

    Notify the user that the copy request was successful once it has been completed.

          notification.message = i18n.get 'result_good_description', template.title
          notification.title   = i18n.get 'result_good_title'
    
          ext.copy output
  • ¶

    Finally, allow the copied content to be pasted into a field on the active tab.
    This action is dependant on whether the page is protected, the relevant option is enabled, and that a field was within the context of the template’s activation.

          if not isProtectedPage(active) and (editable and store.get 'menu.paste') or
              (shortcut and store.get 'shortcuts.paste')
            chrome.tabs.sendMessage active.id, {contents: output, id, type: 'paste'}
    
        log.debug "Finished handling a #{type} request"
  • ¶

    Listener for external messages sent to the extension.
    Only messages sent from extensions/apps that have not been previously blacklisted routed to onMessage.

    onMessageExternal = (message, sender, sendResponse) ->
      log.trace()
  • ¶

    Safely handle callback functionality.

      callback = utils.callback sendResponse
  • ¶

    Ensure blacklisted extensions/apps are blocked.

      blocked = isBlacklisted sender.id
    
      analytics.track 'External Requests', 'Started', sender.id, Number !blocked
    
      if blocked
        log.debug "Blocked external request from #{sender.id}"
        return do callback
    
      log.debug "Accepted external request from #{sender.id}"
  • ¶

    Route message as if it was sent internally.

      onMessage message, sender, sendResponse
  • ¶

    Avoid repetitive calls to render the text contents of a Mustache section by passed callback the pre-rendered text and safely handling bad returns.

    rendered = (callback) ->
      -> (text, render) ->
        result = if arguments.length then callback(render text) else do callback
        result ? ''
  • ¶

    Attempt to select a tab in the current window displaying a page whose location begins with url.
    If no existing tab exists a new one is created.

    selectOrCreateTab = (url, callback) ->
      log.trace()
  • ¶

    Retrieve the tabs of last focused window to check for an existing one with a matching URL.

      chrome.windows.getLastFocused populate: yes, (win) ->
        {tabs} = win
    
        log.debug 'Checking the tabs of the following last focused window...', win
        log.debug 'Checking the following tabs for a matching URL...', tabs
  • ¶

    Try to find an existing tab that begins with url.

        for tab in tabs when not tab.url.indexOf url
          existing = tab
          break
    
        if existing?
  • ¶

    Found one! Now to select it.

          chrome.tabs.update existing.id, active: yes
          callback? existing
        else
  • ¶

    Ach well, let’s just create a brand-spanking new one.

          chrome.tabs.create {windowId: win.id, url, active: yes}, (tab) ->
            callback? tab
  • ¶

    Display a desktop notification informing the user on whether or not the copy request was successful.

    showNotification = ->
      log.trace()
  • ¶

    Ensure that ext is reset and that a notification is only displayed if the user has enabled the corresponding option (enabled by default).

      if store.get 'notifications.enabled'
        analytics.track 'Frames', 'Displayed', 'Notification'
    
        chrome.notifications.create '', ext.notification, (id) ->
          log.debug "Opened desktop notification: #{id}"
    
          ext.reset()
      else
        ext.reset()
  • ¶

    Reset and hide any visible progress bar on the popup, where possible.

      updateProgress null, no
  • ¶

    Convert the html into Markdown using html.md while ensuring related options are used.

    toMarkdown = (html) ->
      log.trace()
    
      {inline} = store.get 'markdown'
    
      md html, {inline}
  • ¶

    Update hotkeys stored by the content.coffee script within all of the tabs (where valid) of each Chrome window.

    updateHotkeys = ->
      log.trace()
    
      hotkeys = do getHotkeys
  • ¶

    Retrieve all open tabs (on all windows) and update their registered keyboard shortcuts.

      chrome.tabs.query {}, (tabs) ->
        log.info 'Updating the hotkeys registed in the following tabs...', tabs
  • ¶

    Check tabs are not displaying a protected page (i.e. one that will cause an error if an attempt is made to send a message to it).

        for tab in tabs when not isProtectedPage tab
          chrome.tabs.sendMessage tab.id, {hotkeys}
  • ¶

    Update the popup UI state to reflect the progress of the current request.
    Ignore percent if toggle is specified; otherwise update the percentage on the progress bar and reset the delay timer.
    toggle can be used to show the progress bar or reset/close the popup.

    updateProgress = (percent, toggle) ->
      log.trace()
    
      popup       = $ chrome.extension.getViews(type: 'popup')[0]
      templates   = if popup.length then $ '#templates', popup[0].document else $()
      loading     = if popup.length then $ '#loading',   popup[0].document else $()
      progressBar = loading.find '.bar'
    
      if toggle?
        if toggle
  • ¶

    Reset the progress bar to zero and ensure it’s visible while the templates are hidden.

          progressBar.css 'width', '0%'
          popup.delay            POPUP_DELAY
          templates.hide().delay POPUP_DELAY
          loading.show().delay   POPUP_DELAY
        else
          if store.get 'toolbar.close'
  • ¶

    Close the popup, where possible.

            popup.queue ->
              popup.dequeue()[0]?.close()
          else
  • ¶

    Reset the progress bar to zero and ensure it’s hidden while the templates are visibile.

            loading.queue ->
              loading.hide().dequeue()
              progressBar.css 'width', '0%'
            templates.queue ->
              templates.show().dequeue()
      else if percent?
  • ¶

    Update the progress bar to display the specified percent.

        log.info "Updating bar progress to #{percent}%"
    
        popup.dequeue().delay     POPUP_DELAY
        templates.dequeue().delay POPUP_DELAY
        loading.dequeue().delay   POPUP_DELAY
        progressBar.css 'width', "#{percent}%"
  • ¶

    Update the statistical information.

    updateStatistics = ->
      log.trace()
    
      log.info 'Updating statistics'
    
      store.init 'stats', {}
      store.modify 'stats', (stats) ->
  • ¶

    Determine which template has the greatest usage.

        popular = _.max ext.templates, (template) ->
          template.usage
  • ¶

    Calculate the up-to-date statistical information.

        stats.count       = ext.templates.length
        stats.customCount = stats.count - DEFAULT_TEMPLATES.length
        stats.popular     = popular?.key
  • ¶

    Increment the usage for the template with the specified key and persist the changes.

    updateTemplateUsage = (key) ->
      log.trace()
    
      template = _.findWhere ext.templates, {key}
    
      if template?
        template.usage++
        store.set 'templates', ext.templates
    
        log.info "Used the #{template.title} template"
        analytics.track 'Templates', 'Used', template.title, Number template.readOnly
  • ¶

    Increment the usage for the URL shortener service with the specified name and persist the changes.

    updateUrlShortenerUsage = (name, oauth) ->
      log.trace()
    
      shortener = _.findWhere SHORTENERS, {name}
    
      if shortener?
        store.modify name, (shortener) ->
          shortener.usage++
    
        log.info "Used the #{shortener.title} URL shortener"
        analytics.track 'Shorteners', 'Used', shortener.title, Number oauth
  • ¶

    Errors

  • ¶

    AppError allows easy identification of internal errors.

    class AppError extends Error
  • ¶

    Create a new instance of AppError with a localized message.

      constructor: (messageKey, substitutions...) ->
        @message = i18n.get messageKey, substitutions if messageKey
  • ¶

    Data functions

  • ¶

    Extract additional information from tab and add it to data.

    addAdditionalData = (tab, data, id, editable, shortcut, link, callback) ->
      log.trace()
    
      async.parallel [
        (done) ->
  • ¶

    Retrieve all of the URLs from tabs contained in the same window as tab.

          chrome.tabs.query windowId: tab.windowId, (tabs) ->
            log.debug 'Extracting the URLs from the following tabs...',   tabs
    
            tabs = _.pluck tabs, 'url'
    
            done null, {tabs}
    
        (done) ->
  • ¶

    Allow Chrome to attemt automatic language detection on tab.

          chrome.tabs.detectLanguage tab.id, (locale) ->
            log.debug "Chrome automatically detected language: #{locale}"
    
            locale = '' unless locale and locale isnt UNKNOWN_LOCALE
    
            done null, {locale}
    
        (done) ->
  • ¶

    Retrieve the geolocation from the client.

          navigator.geolocation.getCurrentPosition (position) ->
            log.debug 'Retrieved the following geolocation information...', position
    
            done null, coords: transformData position.coords, yes
          , (err) ->
            log.warn 'Ingoring error thrown when calculating geolocation', err.message
    
            done null, coords: {}
    
        (done) ->
  • ¶

    Retrieve all of the cookies the client has stored for the contextual URL.

          chrome.cookies.getAll url: data.url, (cookies) ->
            log.debug 'Found the following cookies...', cookies
    
            done null,
              cookie: rendered (text) ->
  • ¶

    Attempt to find the value for the cookie name.

                _.findWhere(cookies, name: text)?.value
              cookies: _.chain(cookies).pluck('name').uniq().value()
    
        (done) ->
  • ¶

    Try to prevent pages hanging because content script should not have been executed.

          return do done if isProtectedPage tab
  • ¶

    Call the content script running within tab and request additional data to be extracted from the page’s DOM.
    The content script also takes this oppertunity to prepare for a second request that may potentially be sent later instructing it to paste the result of this copy request into a field within the context of the template’s activation.

          chrome.tabs.sendMessage tab.id, {editable, id, link, shortcut, url: data.url}, (response) ->
            log.debug 'The following data was retrieved from the content script...', response
  • ¶

    Safety mechanism for when content scripts haven’t been updated along with the extension so the request gets stuck. This is mainly an issue in development and not production.

            response ?= {}
  • ¶

    Attempt to transform lastModified into a usable date.

            lastModified = if response.lastModified?
              time = Date.parse response.lastModified
              new Date time unless isNaN time
  • ¶

    Provide an empty map of meta data if the content script failed to return one.

            metaMap = response.metaMap ? {}
  • ¶

    Attempt to lookup the value of the meta data with the specified name.
    If csv is enabled, separate the contents by commas and return each unique value in an array.

            getMeta = (name, csv) ->
              value = metaMap[name]
              if csv and value
                _.chain(value.split ',').compact().uniq().value()
              else
                value or ''
  • ¶

    Sanitize the response so that it’s data can be cleanly integrated.

            done null,
              author:         getMeta 'author'
              characterset:   response.characterSet   ? ''
              description:    getMeta 'description'
              html:           response.html           ? ''
              images:         response.images         ? []
              keywords:       getMeta 'keywords', yes
              lastmodified:   rendered (text) ->
                lastModified?.format if text then text
              linkhtml:       response.linkHTML       ? ''
              links:          response.links          ? []
              linktext:       response.linkText       ? ''
              localstorage:   response.localStorage   ? {}
              meta:           rendered (text) ->
                metaMap[text]
              pageheight:     response.pageHeight     ? ''
              pagewidth:      response.pageWidth      ? ''
              referrer:       response.referrer       ? ''
              scripts:        response.scripts        ? []
              selectedimages: response.selectedImages ? []
              selectedlinks:  response.selectedLinks  ? []
              selection:      response.selection      ? ''
              selectionhtml:  response.selectionHTML  ? ''
  • ¶

    Deprecated since 1.0.0, use selectedLinks instead.

              selectionlinks: -> @selectedlinks
              sessionstorage: response.sessionStorage ? {}
              stylesheets:    response.styleSheets    ? []
    
      ], (err, results = []) ->
        log.error err if err
  • ¶

    Extend the original data with any additional data that was retrieved successfully.

        $.extend data, result for result in results when result?
    
        do callback
  • ¶

    Creates an object containing data based on information derived from the specified tab and menu item data.

    buildDerivedData = (tab, onClickData, getCallback) ->
      log.trace()
    
      info    = editable: onClickData.editable, link: no
      fakeTab =
        title: tab.title
        url:   if onClickData.linkUrl
            info.link = yes
            onClickData.linkUrl
          else if onClickData.srcUrl   then onClickData.srcUrl
          else if onClickData.frameUrl then onClickData.frameUrl
          else onClickData.pageUrl
      info.data = buildStandardData fakeTab, getCallback
    
      info
  • ¶

    Construct a data object based on information extracted from tab.
    The tab information is then merged with additional information relating to the URL of the tab.
    If a tag requires further action (e.g. call a URL shortening service) when parsing the templates contents later, the callback method returned by getCallback is called to handle this as we don’t want to perform these expensive tasks unless it is actually required.

    buildStandardData = (tab, getCallback) ->
      log.trace()
  • ¶

    Create a clone of the original tab of the tab to run the compatibility scripts on.

      ctab = $.extend {}, tab
  • ¶

    Check for any support extensions running on the current tab by simply checking the tabs URL.

      for own extension, handler of SUPPORT when isExtensionActive tab, extension
        log.debug "Making data compatible with #{extension}"
    
        handler? ctab
        break
  • ¶

    Build the initial URL data.

      data = {}
      url  = $.url ctab.url
  • ¶

    Create references to the base of all grouped options to improve lookup performance.

      bitly         = store.get 'bitly'
      googl         = store.get 'googl'
      links         = store.get 'links'
      markdown      = store.get 'markdown'
      menu          = store.get 'menu'
      notifications = store.get 'notifications'
      shortcuts     = store.get 'shortcuts'
      stats         = store.get 'stats'
      toolbar       = store.get 'toolbar'
      yourls        = store.get 'yourls'
  • ¶

    Attempt to extract the contents from the page source in the specified type (i.e. html or text).

      getFromPage = (source, contentType) ->
        contents = ''
    
        if source
          html = document.createElement 'html'
          html.innerHTML = source
    
          $html = $ html
          $body = $html.find 'body'
    
          contents = if $body.length then do $body[contentType] else do $html[contentType]
    
        contents
  • ¶

    Merge the initial data with the attributes of the URL parser and all of the custom Template properties.
    All properties should be in lower case so that they can be looked up ignoring case by our modified version of mustache.js.

      $.extend data, url.attr(),
  • ¶

    Deprecated since 1.2.5, use linkstarget instead.

        anchortarget:          -> @linkstarget
  • ¶

    Deprecated since 1.2.5, use linkstitle instead.

        anchortitle:           -> @linkstitle
        bitly:                 bitly.enabled
        bitlyaccount:          ->
          _.findWhere(SHORTENERS, name: 'bitly').oauth.hasAccessToken()
        browser:               browser.title
        browserversion:        browser.version
        capitalise:            -> do @capitalize
        capitalize:            rendered (text) ->
          utils.capitalize text
  • ¶

    Deprecated since 1.0.0, use menu instead.

        contextmenu:           -> @menu
        cookiesenabled:        navigator.cookieEnabled
        count:                 stats.count
        customcount:           stats.customCount
        datetime:              rendered (text) ->
          new Date().format if text then text
        decode:                rendered (text) ->
          if text then decodeURIComponent text
        depth:                 screen.colorDepth
  • ¶

    Deprecated since 1.0.0, use linkstarget instead.

        doanchortarget:        -> @linkstarget
  • ¶

    Deprecated since 1.0.0, use linkstitle instead.

        doanchortitle:         -> @linkstitle
        encode:                rendered (text) ->
          if text then encodeURIComponent text
  • ¶

    Deprecated since 0.1.0.2, use encode instead.

        encoded:               ->
          encodeURIComponent @url
        escape:                rendered (text) ->
          _.escape text
        favicon:               ctab.favIconUrl
        fparam:                rendered (text) ->
          if text then url.fparam text
        fparams:               nullIfEmpty url.fparam()
        fsegment:              rendered (text) ->
          if text then url.fsegment parseInt text, 10
        fsegments:             url.fsegment()
        googl:                 googl.enabled
        googlaccount:          ->
          _.findWhere(SHORTENERS, name: 'googl').oauth.hasAccessToken()
  • ¶

    Deprecated since 1.0.0, use googlaccount instead.

        googloauth:            -> do @googlaccount
        java:                  navigator.javaEnabled()
        length:                rendered (text) ->
          text?.length
        linkmarkdown:          ->
          toMarkdown @linkhtml
        linkstarget:           links.target
        linkstitle:            links.title
        lowercase:             rendered (text) ->
          text?.toLowerCase()
        markdown:              ->
          toMarkdown getFromPage @html, 'html'
        markdowninline:        markdown.inline
        menu:                  menu.enabled
        menuoptions:           menu.options
        menupaste:             menu.paste
        notifications:         notifications.enabled
  • ¶

    Deprecated since 1.2.7

        notificationduration:  0
        offline:               not navigator.onLine
  • ¶

    Deprecated since 0.1.0.2, use originalurl instead.

        originalsource:        -> @originalurl
        originaltitle:         tab.title or url.attr 'source'
        originalurl:           tab.url
        os:                    operatingSystem
        param:                 rendered (text) ->
          if text then url.param text
        params:                nullIfEmpty url.param()
        plugins:               _.chain(navigator.plugins).pluck('name').uniq().value()
        popular:               $.extend yes, {}, _.findWhere ext.templates, key: stats.popular
        screenheight:          screen.height
        screenwidth:           screen.width
        segment:               rendered (text) ->
          if text then url.segment parseInt text, 10
        segments:              url.segment()
        select:                ->
          getCallback 'select'
        selectall:             ->
          getCallback 'selectall'
        selectallhtml:         ->
          getCallback 'selectallhtml'
        selectallmarkdown:     ->
          getCallback 'selectallmarkdown'
        selecthtml:            ->
          getCallback 'selecthtml'
        selectionmarkdown:     ->
          toMarkdown @selectionhtml
        selectmarkdown:        ->
          getCallback 'selectmarkdown'
  • ¶

    Deprecated since 1.0.0, use shorten instead.

        short:                 -> do @shorten
        shortcuts:             shortcuts.enabled
        shortcutspaste:        shortcuts.paste
        shorten:               ->
          getCallback 'shorten'
        text:                  ->
          getFromPage @html, 'text'
        tidy:                  rendered (text) ->
          if text then text.replace(/([ \t]+)/g, ' ').trim()
        title:                 ctab.title or url.attr 'source'
        toolbarclose:          toolbar.close
  • ¶

    Deprecated since 1.0.0, use the inverse of toolbarpopup instead.

        toolbarfeature:        -> not @toolbarpopup
  • ¶

    Deprecated since 1.0.0, use toolbarstyle instead.

        toolbarfeaturedetails: -> @toolbarstyle
  • ¶

    Deprecated since 1.0.0, use toolbarkey instead.

        toolbarfeaturename:    -> @toolbarkey
        toolbarkey:            toolbar.key
        toolbaroptions:        toolbar.options
        toolbarpopup:          toolbar.popup
  • ¶

    Obsolete since 1.1.0, functionality has been removed.

        toolbarstyle:          no
        trim:                  rendered (text) ->
          text?.trim()
        trimleft:              rendered (text) ->
          text?.trimLeft()
        trimright:             rendered (text) ->
          text?.trimRight()
        unescape:              rendered (text) ->
          _.unescape text
        uppercase:             rendered (text) ->
          text?.toUpperCase()
        url:                   url.attr 'source'
        version:               ext.version
        wordcount:             rendered (text) ->
          return unless text
  • ¶

    Oddly, this is the optimal method to calculate the word count on WebKit browsers.

          count   = 0
          text    = text.trim()
          matches = text.match /\s+/g
    
          if text
            count++
            count+= matches.length if matches
    
          count
        xpath:                ->
          getCallback 'xpath'
        xpathall:             ->
          getCallback 'xpathall'
        xpathallhtml:         ->
          getCallback 'xpathallhtml'
        xpathallmarkdown:     ->
          getCallback 'xpathallmarkdown'
        xpathhtml:            ->
          getCallback 'xpathhtml'
        xpathmarkdown:        ->
          getCallback 'xpathmarkdown'
        yourls:                yourls.enabled
        yourlsauthentication:  yourls.authentication
        yourlspassword:        yourls.password
        yourlssignature:       yourls.signature
        yourlsurl:             yourls.url
        yourlsusername:        yourls.username
    
      data
  • ¶

    Derive the relevant information, including the template data, based on both message and tab.

    deriveMessageInfo = (message, tab, getCallback) ->
      log.trace()
    
      info =
        data:     null
        editable: no
        link:     no
        shortcut: no
    
      $.extend info, switch message.type
        when 'menu'
          buildDerivedData tab, message.data, getCallback
        when 'popup', 'toolbar'
          data: buildStandardData tab, getCallback
        when 'shortcut'
          data:     buildStandardData tab, getCallback
          shortcut: yes
        else
          throw new AppError 'result_bad_type_description'
  • ¶

    Derive the relevant template based on the context of message.

    deriveMessageTempate = (message) ->
      log.trace()
    
      template = switch message.type
        when 'menu'             then getTemplateWithMenuId message.data.menuItemId
        when 'popup', 'toolbar' then getTemplateWithKey message.data.key
        when 'shortcut'         then getTemplateWithShortcut message.data.key
        else throw new AppError 'result_bad_type_description'
      throw new AppError 'result_bad_template_description' unless template?
    
      template
  • ¶

    Evaluate the expressions in map within the content scripts in tab in order to obtain their corresponding values.
    Expressions can be of mixed type (i.e. CSS selector, XPath expression) and contain different instructions depending on the tag that was used by the user.
    callback will be called with the result once all of the expressions have been evaluated.

    evaluateExpressions = (tab, map, callback) ->
      log.trace()
  • ¶

    Since content scripts cannot be executed on protected pages attempting to send a message to one would result in background errors being thrown, so we’ll just have to make sure the placeholder is removed.

      if isProtectedPage tab
        map[placeholder] = '' for own placeholder of map
        return do callback
  • ¶

    Tell the content script in tab to evaluate all of the expressions in map and update it with their corresponding values.

      chrome.tabs.sendMessage tab.id, expressions: map, (response) ->
        log.debug 'The following response was returned by the content script...', response
    
        response ?= {}
    
        for own placeholder, expression of response.expressions
          {convertTo, error, result} = expression
    
          break if error
    
          result or= ''
          result   = result.join '\n' if _.isArray result
          result   = toMarkdown result if convertTo is 'markdown'
    
          map[placeholder] = result
    
        callback if error then new AppError 'result_bad_expression_description'
  • ¶

    Ensure there is a lower case variant of all properties of data, optionally only changing a clone of the specified data.

    transformData = (data, clone) ->
      log.trace()
    
      return data unless data
  • ¶

    Ensure that the result and data types match when cloning.

      result = if clone
        if _.isArray data then [] else {}
      else
        data
    
    
      transform = (value) ->
        if _.isArray(value) or _.isObject(value) then transformData value, clone else value
    
      if _.isArray result
  • ¶

    Also transform any nested objects deep within the array.

        result[i] = transform value for value, i in data
      else
  • ¶

    Transform all properties of the object, included any deep nested objects.

        for own prop, value of data
          prop = prop.toLowerCase() if clone or R_UPPER_CASE.test prop
    
          result[prop] = transform value
    
      result
  • ¶

    Configuration functions

  • ¶

    Transform specific sections of the data loaded from configuration.json so that they’re more usable and localized.

    buildConfig = ->
      log.trace()
    
      do buildIcons
  • ¶

    Transform the configuration icons into usable Icon instances.

    buildIcons = ->
      log.trace()
    
      for name, i in ext.config.icons.current
        ext.config.icons.current[i] = new Icon name
  • ¶

    HTML building functions

  • ¶

    Build the HTML to populate the popup with to optimize popup loading times.

    buildPopup = ->
      log.trace()
  • ¶

    Generate the HTML for each enabled template.

      items = $()
      items = items.add buildTemplate template for template in ext.templates when template.enabled
  • ¶

    Add a generic message to state the obvious… that the list is empty.

      unless items.length
        message = " #{i18n.get 'empty'}"
        items   = items.add $('<li/>', class: 'empty').append($ '<i/>', class: 'icon-').append message
  • ¶

    Add a link to the options page if the user doesn’t mind.

      if store.get 'toolbar.options'
        anchor = $ '<a/>',
          class:       'options'
          'data-type': 'options'
    
        anchor.append $ '<i/>', class: Icon.get('cog').style
        anchor.append " #{i18n.get 'options'}"
    
        items = items.add $ '<li/>', class: 'divider'
        items = items.add $('<li/>').append anchor
    
      ext.templatesHtml = $('<div/>').append(items).html()
  • ¶

    Create an li element to represent template.
    The element should then be inserted in to the ul element in the popup page but is created here to optimize display times for the popup.

    buildTemplate = (template) ->
      log.trace()
    
      log.debug "Creating popup list item for the #{template.title} template"
    
      anchor = $ '<a/>',
        'data-key':  template.key
        'data-type': 'popup'
  • ¶

    Ensure keyboard shortcuts are displayed correctly, where appropriate.

      if template.shortcut and store.get 'shortcuts.enabled'
        anchor.append $ '<span/>',
          class: 'pull-right muted shortcut',
          html:  "#{ext.modifiers()}#{template.shortcut}"
    
      anchor.append $ '<i/>', class: Icon.get(template.image, yes).style
      anchor.append " #{template.title}"
    
      $('<li/>').append anchor
  • ¶

    Initialization functions

  • ¶

    Handle the conversion/removal of older version of settings that may have been stored previously by ext.init.

    init_update = ->
      log.trace()
  • ¶

    Update the update progress indicator itself.

      if store.exists 'update_progress'
        store.modify 'updates', (updates) ->
          for own namespace, versions of store.remove 'update_progress'
            updates[namespace] = if versions?.length then versions.pop() else ''
  • ¶

    Create an updater for the settings namespace.

      updater      = new store.Updater 'settings'
      updater.on 'update', (version) ->
        log.info "Updating general settings for #{version}"
    
      isNewInstall = updater.isNew
  • ¶

    Define the processes for all required updates to the settings namespace.

      updater.update '0.1.0.0', ->
        store.rename 'settingNotification',      'notifications',        on
        store.rename 'settingNotificationTimer', 'notificationDuration', 3000
        store.rename 'settingShortcut',          'shortcuts',            on
        store.rename 'settingTargetAttr',        'doAnchorTarget',       off
        store.rename 'settingTitleAttr',         'doAnchorTitle',        off
        store.remove 'settingIeTabExtract',      'settingIeTabTitle'
    
      updater.update '1.0.0', ->
        if store.exists 'options_active_tab'
          optionsActiveTab = store.get 'options_active_tab'
          store.set 'options_active_tab', switch optionsActiveTab
            when 'features_nav' then 'templates_nav'
            when 'toolbar_nav' then 'general_nav'
            else optionsActiveTab
    
        store.modify 'links', (links) ->
          links.target = store.get('doAnchorTarget') ? off
          links.title  = store.get('doAnchorTitle')  ? off
        store.remove 'doAnchorTarget', 'doAnchorTitle'
    
        store.modify 'menu', (menu) ->
          menu.enabled = store.get('contextMenu') ? yes
        store.remove 'contextMenu'
    
        store.set 'notifications',
          duration: store.get('notificationDuration') ? 3000
          enabled:  store.get('notifications')        ? yes
        store.remove 'notificationDuration'
    
      updater.update '1.1.0', ->
        store.set 'shortcuts', enabled: store.get('shortcuts') ? yes
    
      updater.update '1.2.3', ->
        store.modify 'logger', (logger) ->
          delete logger.Enabled
          delete logger.Level
    
        store.modify 'menu', (menu) ->
          delete menu.Enabled
          delete menu.Options
          delete menu.Paste
    
        store.modify 'notifications', (notifications) ->
          delete notifications.Duration
          delete notifications.Enabled
    
        store.modify 'shortcuts', (shortcuts) ->
          delete shortcuts.Enabled
          delete shortcuts.Paste
    
        store.modify 'toolbar', (toolbar) ->
          delete toolbar.Close
          delete toolbar.Key
          delete toolbar.Options
    
      updater.update '1.2.5', ->
        store.modify 'links', (links) ->
          anchor = store.get('anchor') ? {}
          links.target = anchor.target ? off
          links.title  = anchor.title  ? off
        store.remove 'anchor'
    
      updater.update '1.2.7', ->
        store.modify 'notifications', (notifications) ->
          delete notifications.duration
  • ¶

    Initialize the settings related to statistical information.

    initStatistics = ->
      log.trace()
    
      do updateStatistics
  • ¶

    Initialize template and its properties, before adding it to templates to be persisted later.

    initTemplate = (template, templates) ->
      log.trace()
  • ¶

    Derive the index of template to determine whether or not it already exists.

      idx = templates.indexOf _.findWhere templates, key: template.key
    
      if idx is -1
  • ¶

    template doesn’t already exist so add it now.

        log.debug 'Adding the following predefined template...', template
    
        templates.push template
      else
  • ¶

    template exists so modify the properties to ensure they are reliable.

        log.debug 'Ensuring following template adheres to structure...', template
    
        if template.readOnly
  • ¶

    template is read-only so certain properties should always be overriden and others only when they are not already available.

          templates[idx].content   = template.content
          templates[idx].enabled  ?= template.enabled
          templates[idx].image    ?= template.image
          templates[idx].index    ?= template.index
          templates[idx].key       = template.key
          templates[idx].readOnly  = yes
          templates[idx].shortcut ?= template.shortcut
          templates[idx].title     = template.title
          templates[idx].usage    ?= template.usage
        else
  • ¶

    template is not read-only so set unavailable, but required, properties to their default value.

          templates[idx].content  ?= ''
          templates[idx].enabled  ?= yes
          templates[idx].image    ?= ''
          templates[idx].index    ?= 0
          templates[idx].key      ?= template.key
          templates[idx].readOnly  = no
          templates[idx].shortcut ?= ''
          templates[idx].title    ?= '?'
          templates[idx].usage    ?= 0
    
        template = templates[idx]
    
      template
  • ¶

    Initialize the persisted managed templates.

    initTemplates = ->
      log.trace()
    
      do initTemplates_update
    
      store.modify 'templates', (templates) ->
  • ¶

    Initialize all default templates to ensure their properties are as expected.

        initTemplate template, templates for template in DEFAULT_TEMPLATES
  • ¶

    Now, initialize all templates to ensure their properties are valid.

        initTemplate template, templates for template in templates
    
      ext.updateTemplates()
  • ¶

    Handle the conversion/removal of older version of settings that may have been stored previously by initTemplates.

    initTemplates_update = ->
      log.trace()
  • ¶

    Create updater for the features namespace and then rename it to templates.

      updater = new store.Updater 'features'
      updater.on 'update', (version) ->
        log.info "Updating template settings for #{version}"
      updater.rename 'templates'
  • ¶

    Define the processes for all required updates to the templates namespace.

      updater.update '0.1.0.0', ->
        store.rename 'copyAnchorEnabled',  'feat__anchor_enabled',  yes
        store.rename 'copyAnchorOrder',    'feat__anchor_index',    2
        store.rename 'copyBBCodeEnabled',  'feat__bbcode_enabled',  no
        store.rename 'copyBBCodeOrder',    'feat__bbcode_index',    4
        store.rename 'copyEncodedEnabled', 'feat__encoded_enabled', yes
        store.rename 'copyEncodedOrder',   'feat__encoded_index',   3
        store.rename 'copyShortEnabled',   'feat__short_enabled',   yes
        store.rename 'copyShortOrder',     'feat__short_index',     1
        store.rename 'copyUrlEnabled',     'feat__url_enabled',     yes
        store.rename 'copyUrlOrder',       'feat__url_index',       0
    
      updater.update '0.2.0.0', ->
        names = store.get('features') ? []
    
        for name in names when _.isString name
          store.rename "feat_#{name}_template", "feat_#{name}_content"
    
          image = store.get "feat_#{name}_image"
    
          if _.isString image
            if image in ['', 'spacer.gif', 'spacer.png']
              store.set "feat_#{name}_image", 0
            else
              for legacy, i in ext.config.icons.legacy
                if "#{legacy.name.replace /^tmpl/, 'feat'}.png" is image
                  store.set "feat_#{name}_image", i + 1
                  break
          else
            store.set "feat_#{name}_image", 0
    
      updater.update '1.0.0', ->
        names              = store.remove('features') ? []
        templates          = []
    
        toolbarFeatureName = store.get 'toolbarFeatureName'
    
        for name in names when _.isString name
          image = store.remove("feat_#{name}_image") ? 0
          image = if --image >= 0 then Icon.fromLegacy(image)?.name or '' else ''
    
          key = ext.getKeyForName name
    
          if toolbarFeatureName is name
            if store.exists 'toolbar'
              store.modify 'toolbar', (toolbar) ->
                toolbar.key = key
            else
              store.set 'toolbar', key: key
    
          templates.push
            content:  store.remove("feat_#{name}_content")  ? ''
            enabled:  store.remove("feat_#{name}_enabled")  ? yes
            image:    image
            index:    store.remove("feat_#{name}_index")    ? 0
            key:      key
            readOnly: store.remove("feat_#{name}_readonly") ? no
            shortcut: store.remove("feat_#{name}_shortcut") ? ''
            title:    store.remove("feat_#{name}_title")    ? '?'
            usage:    0
    
        store.set 'templates', templates
        store.remove store.search(/^feat_.*_(content|enabled|image|index|readonly|shortcut|title)$/)...
    
      updater.update '1.1.0', ->
        store.modify 'templates', (templates) ->
          for template in templates
            if template.readOnly
              break for base in DEFAULT_TEMPLATES when base.key is template.key
    
              template.image = switch template.key
                when 'PREDEFINED.00001'
                  if template.image is 'tmpl_globe' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00002'
                  if template.image is 'tmpl_link' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00003'
                  if template.image is 'tmpl_html' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00004'
                  if template.image is 'tmpl_component' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00005'
                  if template.image is 'tmpl_discussion' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00006'
                  if template.image is 'tmpl_discussion' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                when 'PREDEFINED.00007'
                  if template.image is 'tmpl_note' then base.image
                  else Icon.fromLegacy(template.image)?.name or ''
                else base.image
            else
              template.image = Icon.fromLegacy(template.image)?.name or ''
  • ¶

    Initialize the settings related to the toolbar/browser action.

    initToolbar = ->
      log.trace()
    
      do initToolbar_update
    
      store.modify 'toolbar', (toolbar) ->
        toolbar.close   ?= yes
        toolbar.key     ?= 'PREDEFINED.00001'
        toolbar.options ?= yes
        toolbar.popup   ?= yes
    
      ext.updateToolbar()
  • ¶

    Handle the conversion/removal of older version of settings that may have been stored previously by initToolbar.

    initToolbar_update = ->
      log.trace()
  • ¶

    Create updater for the toolbar namespace.

      updater = new store.Updater 'toolbar'
      updater.on 'update', (version) ->
        log.info "Updating toolbar settings for #{version}"
  • ¶

    Define the processes for all required updates to the toolbar namespace.

      updater.update '1.0.0', ->
        store.modify 'toolbar', (toolbar) ->
          toolbar.popup = store.get('toolbarPopup') ? yes
          toolbar.style = store.get('toolbarFeatureDetails') ? no
        store.remove 'toolbarFeature', 'toolbarFeatureDetails', 'toolbarFeatureName', 'toolbarPopup'
    
      updater.update '1.1.0', ->
        store.modify 'toolbar', (toolbar) ->
          delete toolbar.style
  • ¶

    Initialize the settings related to the supported URL Shortener services.

    initUrlShorteners = ->
      log.trace()
    
      store.init
        bitly:  {}
        googl:  {}
        yourls: {}
    
      do initUrlShorteners_update
    
      store.modify 'bitly', (bitly) ->
        bitly.enabled ?= yes
        bitly.usage   ?= 0
    
      store.modify 'googl', (googl) ->
        googl.enabled ?= no
        googl.usage   ?= 0
    
      store.modify 'yourls', (yourls) ->
        yourls.authentication ?= ''
        yourls.enabled        ?= no
        yourls.password       ?= ''
        yourls.signature      ?= ''
        yourls.url            ?= ''
        yourls.username       ?= ''
        yourls.usage          ?= 0
  • ¶

    Handle the conversion/removal of older version of settings that may have been stored previously by initUrlShorteners.

    initUrlShorteners_update = ->
      log.trace()
  • ¶

    Create updater for the shorteners namespace.

      updater = new store.Updater 'shorteners'
      updater.on 'update', (version) ->
        log.info "Updating URL shortener settings for #{version}"
  • ¶

    Define the processes for all required updates to the shorteners namespace.

      updater.update '0.1.0.0', ->
        store.rename 'bitlyEnabled',       'bitly',         yes
        store.rename 'bitlyXApiKey',       'bitlyApiKey',   ''
        store.rename 'bitlyXLogin',        'bitlyUsername', ''
        store.rename 'googleEnabled',      'googl',         no
        store.rename 'googleOAuthEnabled', 'googlOAuth',    yes
    
      updater.update '1.0.0', ->
        bitly = store.get 'bitly'
        store.set 'bitly',
          apiKey:    store.get('bitlyApiKey')   ? ''
          enabled:   if _.isBoolean bitly then bitly else yes
          username:  store.get('bitlyUsername') ? ''
        store.remove 'bitlyApiKey', 'bitlyUsername'
    
        googl = store.get 'googl'
        store.set 'googl',
          enabled: if _.isBoolean googl then googl else no
        store.remove 'googlOAuth'
    
        yourls = store.get 'yourls'
        store.set 'yourls',
          enabled:   if _.isBoolean yourls then yourls else no
          password:  store.get('yourlsPassword')  ? ''
          signature: store.get('yourlsSignature') ? ''
          url:       store.get('yourlsUrl')       ? ''
          username:  store.get('yourlsUsername')  ? ''
        store.remove 'yourlsPassword', 'yourlsSignature', 'yourlsUrl', 'yourlsUsername'
    
      updater.update '1.0.1', ->
        store.modify 'bitly', (bitly) ->
          delete bitly.apiKey
          delete bitly.username
    
        store.modify 'yourls', (yourls) ->
          yourls.authentication = if yourls.signature then 'advanced'
          else if yourls.password and yourls.username then 'basic'
          else ''
    
        store.remove store.search(/^oauth_token.*/)...
    
      updater.update '1.2.3', ->
        store.modify 'yourls', (yourls) ->
          delete yourls.Password
          delete yourls.Signature
          delete yourls.Url
          delete yourls.Username
  • ¶

    URL shortener functions

  • ¶

    Call the active URL shortener service for each URL in map in order to obtain their corresponding short URLs.
    callback will be called with the result once all URLs have been shortened or an error is encountered.

    callUrlShortener = (map, callback) ->
      log.trace()
    
      service  = do getActiveUrlShortener
      endpoint = service.url()
      oauth    = no
      title    = service.title
  • ¶

    Ensure the service URL exists in case it is user-defined (e.g. YOURLS).

      return callback new AppError 'shortener_config_error', title unless endpoint
    
      tasks = []
      _.each map, (url, placeholder) ->
        oauth = !!service.oauth?.hasAccessToken()
    
        tasks.push (done) ->
          async.series [
            (done) ->
  • ¶

    Ensure the OAuth adapter has been authorized.

              if oauth
                service.oauth.authorize ->
                  do done
              else
                do done
    
            (done) ->
              params    = service.getParameters url
              endpoint += "?#{$.param params}" if params?
  • ¶

    Build the HTTP asynchronous request for the URL shortener service.

              xhr = new XMLHttpRequest()
              xhr.open service.method, endpoint, yes
  • ¶

    Allow service to populate request headers.

              for own header, value of service.getHeaders() ? {}
                xhr.setRequestHeader header, value
    
              xhr.onreadystatechange = ->
  • ¶

    Wait for the response and let the service handle it before passing the result back.

                if xhr.readyState is 4
                  if xhr.status is 200
                    map[placeholder] = service.output xhr.responseText
                    do done
                  else
  • ¶

    Something went wrong so let’s tell the user.

                    done new AppError 'shortener_detailed_error', title, url
  • ¶

    Finally, send the HTTP request.

              xhr.send service.input url
    
          ], done
      async.series tasks, (err) ->
        if err
          err.message or= i18n.get 'shortener_error', service.title
          callback err
        else
          callback null, {oauth, service}
  • ¶

    Retrieve the active URL shortener service.

    getActiveUrlShortener = ->
      log.trace()
  • ¶

    Attempt to lookup enabled URL shortener service.

      shortener = _.find SHORTENERS, (shortener) ->
        shortener.isEnabled()
    
      unless shortener?
  • ¶

    Should never reach here but we’ll return bit.ly service by default after ensuring it’s the active URL shortener service from now on to save some time in the future.

        store.modify 'bitly', (bitly) ->
          bitly.enabled = yes
        shortener = do getActiveUrlShortener
    
      log.debug "Getting details for #{shortener.title} URL shortener"
    
      shortener
  • ¶

    Background page setup

    ext = window.ext = new class Extension extends utils.Class
  • ¶

    Public constants

  • ¶

    String representation of the keyboard modifiers listened to by Template on Windows/Linux platforms.

      SHORTCUT_MODIFIERS: 'Ctrl+Alt+'
  • ¶

    String representation of the keyboard modifiers listened to by Template on Mac platforms.

      SHORTCUT_MAC_MODIFIERS: '&#8679;&#8997;'
  • ¶

    Public variables

  • ¶

    Configuration data loaded at runtime.

      config: {}
  • ¶

    Information specifying what should be displaying in the notification.
    This should be reset after every copy request.

      notification:
        iconUrl: utils.url '../images/icon_64.png'
        message: ''
        title:   ''
        type:    'basic'
  • ¶

    Reference to supported URL shortener services.

      shorteners: SHORTENERS
  • ¶

    Local copy of templates being used, ordered to match that specified by the user.

      templates: []
  • ¶

    Pre-prepared HTML for the popup to be populated using.
    This should be updated whenever templates are changed/updated in any way as this is generated to improve performance and load times of the popup frame.

      templatesHtml: ''
  • ¶

    Current version of Template.

      version: ''
  • ¶

    Public functions

  • ¶

    Add str to the system clipboard.
    All successful copy requests should, at some point, call this function.
    If str is empty the contents of the system clipboard will not change.

      copy: (str, hidden) ->
        log.trace()
    
        sandbox = $('#sandbox').val(str).trigger 'select'
        document.execCommand 'copy'
        log.debug 'Copied the following string...', str
        sandbox.val ''
    
        do showNotification unless hidden
  • ¶

    Attempt to retrieve the key of the template with the specified name.
    Since only the names of predefined templates are known, return a newly generated key if it does not match any of their names.

      getKeyForName: (name, generate = yes) ->
        log.trace()
    
        key = switch name
          when '_url'      then 'PREDEFINED.00001'
          when '_short'    then 'PREDEFINED.00002'
          when '_anchor'   then 'PREDEFINED.00003'
          when '_encoded'  then 'PREDEFINED.00004'
          when '_bbcode'   then 'PREDEFINED.00005'
          when '_markdown' then 'PREDEFINED.00006'
          else utils.keyGen() if generate
    
        log.debug "Associating #{key} key with #{name} template"
    
        key
  • ¶

    Initialize the background page.
    This will involve initializing the settings and adding the message listeners.

      init: ->
        log.trace()
    
        log.info 'Initializing extension controller'
  • ¶

    Add support for analytics if the user hasn’t opted out.

        analytics.add() if store.get 'analytics'
  • ¶

    Load the configuration data from the file.

        $.getJSON utils.url('configuration.json'), (data) =>
  • ¶

    Store the configuration data and then ensure it’s data is in the most usable format.

          @config = data
          do buildConfig
  • ¶

    It’s nice knowing what version is running.

          {@version} = chrome.runtime.getManifest()
  • ¶

    Initiate OAuth for each of the applicable URL shortener services.

          shortener.oauth = shortener.oauth?() for shortener in SHORTENERS
  • ¶

    Begin initialization.

          store.init
            links:         {}
            markdown:      {}
            menu:          {}
            notifications: {}
            shortcuts:     {}
            stats:         {}
            templates:     []
            toolbar:       {}
    
          do init_update
    
          store.modify 'links', (links) ->
            links.target ?= off
            links.title  ?= off
    
          store.modify 'markdown', (markdown) ->
            markdown.inline ?= no
    
          store.modify 'menu', (menu) ->
            menu.enabled ?= yes
            menu.options ?= yes
            menu.paste   ?= no
    
          store.modify 'notifications', (notifications) ->
            notifications.enabled ?= yes
    
          store.modify 'shortcuts', (shortcuts) ->
            shortcuts.enabled ?= yes
            shortcuts.paste   ?= no
    
          do initTemplates
          do initToolbar
          do initStatistics
          do initUrlShorteners
  • ¶

    Add listener for toolbar/browser action clicks.
    This listener will be ignored whenever the popup is enabled.

          chrome.browserAction.onClicked.addListener (tab) ->
            onMessage
              data: key: store.get 'toolbar.key'
              type: 'toolbar'
  • ¶

    Add listeners for internal and external messages.

          chrome.extension.onMessage.addListener onMessage
          chrome.extension.onMessageExternal.addListener onMessageExternal
  • ¶

    Derive the browser and OS information.

          browser.version = do getBrowserVersion
          operatingSystem = do getOperatingSystem
          analytics.track 'Installs', 'New', @version, Number isProductionBuild if isNewInstall
  • ¶

    Execute content scripts now that we know the version.

          do executeScriptsInExistingWindows
  • ¶

    Determine whether or not os matches the user’s operating system.

      isThisPlatform: (os) ->
        log.trace()
    
        /// #{os} ///i.test navigator.platform
  • ¶

    Retrieve the correct string representation of the keyboard modifiers for the user’s operating system.

      modifiers: ->
        log.trace()
    
        if @isThisPlatform 'mac' then @SHORTCUT_MAC_MODIFIERS else @SHORTCUT_MODIFIERS
  • ¶

    Attempt to retrieve the contents of the system clipboard as a string.

      paste: ->
        log.trace()
    
        result  = ''
        sandbox = $('#sandbox').val('').trigger 'select'
        result  = sandbox.val() if document.execCommand 'paste'
        log.debug 'Pasted the following string...', result
        sandbox.val ''
    
        result
  • ¶

    Reset the notification information associated with the current copy request.
    This should be called when a copy request is completed regardless of its outcome.

      reset: ->
        log.trace()
    
        @notification =
          iconUrl: utils.url '../images/icon_64.png'
          message: ''
          title:   ''
          type:    'basic'
  • ¶

    Update the context menu items to reflect the currently enabled templates.
    If the context menu option has been disabled by the user, just remove all of the existing menu items.

      updateContextMenu: ->
        log.trace()
  • ¶

    Ensure that any previously added context menu items are removed.

        chrome.contextMenus.removeAll =>
  • ¶

    Called whenever a template menu item is clicked.
    Message self, passing along the available information.

          onMenuClick = (info, tab) ->
            onMessage data: info, type: 'menu'
    
          menu = store.get 'menu'
  • ¶

    Stop now if the context menu is disabled.

          return unless menu.enabled
  • ¶

    Create and add the top-level Template menu.

          parentId = chrome.contextMenus.create
            contexts: ['all']
            title:    i18n.get 'name'
  • ¶

    Create and add a sub-menu item for each enabled template.

          for template in @templates when template.enabled
            notEmpty = yes
            menuId   = chrome.contextMenus.create
              contexts: ['all']
              onclick:  onMenuClick
              parentId: parentId
              title:    template.title
            template.menuId = menuId
  • ¶

    Indicate that no templates have been enabled.

          unless notEmpty
            chrome.contextMenus.create
              contexts: ['all']
              parentId: parentId
              title:    i18n.get 'empty'
  • ¶

    Add an item to open the options page if the user doesn’t mind.

          if menu.options
            chrome.contextMenus.create
              contexts: ['all']
              parentId: parentId
              type:     'separator'
    
            chrome.contextMenus.create
              contexts: ['all']
              onclick:  (info, tab) ->
                onMessage type: 'options'
              parentId: parentId
              title:    i18n.get 'options'
  • ¶

    Update the local list of templates to reflect those persisted.
    It is very important that this is called whenever templates may have been changed in order to prepare the popup HTML and optimize performance.

      updateTemplates: ->
        log.trace()
    
        @templates = _.sortBy store.get('templates'), 'index'
    
        do buildPopup
        do @updateContextMenu
        do updateStatistics
        do updateHotkeys
  • ¶

    Update the toolbar/browser action depending on the current settings.

      updateToolbar: ->
        log.trace()
    
        key      = store.get 'toolbar.key'
        template = getTemplateWithKey key if key
    
        do buildPopup
    
        if not template or store.get 'toolbar.popup'
          log.info 'Configuring toolbar to display popup'
  • ¶

    Show the popup when the browser action is clicked.

          chrome.browserAction.setPopup popup: 'pages/popup.html'
        else
          log.info 'Configuring toolbar to activate specified template'
  • ¶

    Disable the popup, effectively enabling the listener for chrome.browserAction.onClicked.

          chrome.browserAction.setPopup popup: ''
  • ¶

    Public classes

  • ¶

    Icon provides common functionality for using icons throughout the extension.

    ext.Icon = class Icon extends utils.Class
  • ¶

    Create a new instance of Icon with the specified name.
    An appropriate localized message and CSS style is also derived.

      constructor: (@name) ->
        @message = i18n.get "icon_#{name?.replace(/-/g, '_') or 'none'}"
        @style   = "icon-#{name or ''}"
  • ¶

    Determine whether an Icon with the given name exists.

    Icon.exists = (name) ->
      Icon.get(name)?
  • ¶

    Retrieve the Icon with the given name.
    safe can be used to ensure that an Icon is always returned, although this will be empty (no name) if none had a matching name.

    Icon.get = (name, safe) ->
      icon = _.findWhere ext.config.icons.current, {name}
    
      if not icon? and safe then new Icon() else icon
  • ¶

    Attempt to retrieve the replacement for the legacy icon with the given name.
    If name is a number it will be treated as an index of the legacy icon; otherwise a simple lookup is performed.

    Icon.fromLegacy = (name) ->
      legacy = switch typeof name
        when 'number' then ext.config.icons.legacy[name]
        when 'string' then _.findWhere ext.config.icons.legacy, {name}
    
      if legacy then Icon.get legacy.icon
  • ¶

    Initialize ext when the DOM is ready.

    utils.ready -> ext.init()