background.coffee | |
---|---|
Template | |
Extending prototypes | |
Transform the string into title case. | String::capitalize = ->
@replace /\w+/g, (word) ->
word.charAt(0).toUpperCase() + word.substr(1).toLowerCase() |
Private constants | |
List of blacklisted extension IDs that should be prevented from making external requests 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}}\"{#anchorTarget}
target=\"_blank\"{/anchorTarget}{#anchorTitle}
title=\"{{title}}\"{/anchorTitle}>{{title}}</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})'
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 = 'neocotic.com' |
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 detect upper case characters. | R_UPPER_CASE = /[A-Z]+/ |
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. | 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 = services.bitly.api_key
params.login = services.bitly.login
params
getUsage: -> store.get 'bitly.usage'
input: -> null
isEnabled: -> store.get 'bitly.enabled'
method: 'GET'
name: 'bitly'
oauth: -> new OAuth2 'bitly'
client_id: services.bitly.client_id
client_secret: services.bitly.client_secret
output: (resp) -> JSON.parse(resp).data.url
title: i18n.get 'shortener_bitly'
url: -> services.bitly.url
, |
Setup Google URL Shortener. | getHeaders: ->
headers = 'Content-Type': 'application/json'
if @oauth.hasAccessToken()
headers.Authorization = "OAuth #{@oauth.getAccessToken()}"
headers
getParameters: -> unless @oauth.hasAccessToken()
key: services.googl.api_key
getUsage: -> store.get 'googl.usage'
input: (url) -> JSON.stringify longUrl: url
isEnabled: -> store.get 'googl.enabled'
method: 'POST'
name: 'googl'
oauth: -> new OAuth2 'google'
api_scope: services.googl.api_scope
client_id: services.googl.client_id
client_secret: services.googl.client_secret
output: (resp) -> JSON.parse(resp).id
title: i18n.get 'shortener_googl'
url: -> services.googl.url
, |
Setup YOURLS. | getHeaders: -> 'Content-Type': 'application/json'
getParameters: (url) ->
params =
action: 'shorturl'
format: 'json'
url: url
yourls = store.get 'yourls'
switch yourls.authentication
when 'advanced'
params.signature = yourls.signature if yourls.signature
when 'basic'
params.password = yourls.password if yourls.password
params.username = yourls.username if yourls.username
params
getUsage: -> store.get 'yourls.usage'
input: -> null
isEnabled: -> store.get 'yourls.enabled'
method: 'POST'
name: 'yourls'
output: (resp) -> JSON.parse(resp).shorturl
title: i18n.get 'shortener_yourls'
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
if idx isnt -1
tab.url = decodeURIComponent tab.url.substring idx + str.length |
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]'
if tab.url.indexOf(str) is 0
tab.url = decodeURIComponent tab.url.substring str.length |
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]'
if tab.url.indexOf(str) is 0
tab.url = decodeURIComponent tab.url.substring str.length |
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 = '' |
Details of web services used by Template. | services = {} |
Private functions | |
Inject and execute the | executeScriptsInExistingWindows = ->
log.trace() |
Create a runner to help manage the asynchronous aspect. | runner = new utils.Runner()
runner.push chrome.windows, 'getAll', null, (windows) ->
log.info 'Retrieved the following windows...', windows
for win in windows
do (win) -> runner.push chrome.tabs, 'query', windowId: win.id, (tabs) ->
log.info 'Retrieved 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 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. | if tab.url.indexOf(HOMEPAGE_DOMAIN) isnt -1
chrome.tabs.executeScript tab.id, file: 'lib/install.js'
runner.next()
runner.next()
runner.run() |
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.substring 0, idx |
Build 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()
str = navigator.platform
for os in OPERATING_SYSTEMS when str.indexOf(os.substring) isnt -1
str = os.title
break
str |
Attempt to retrieve the template with the specified | getTemplateWithKey = (key) ->
log.trace()
ext.queryTemplate (template) -> template.key is key |
Attempt to retrieve the template with the specified | getTemplateWithMenuId = (menuId) ->
log.trace()
ext.queryTemplate (template) -> template.menuId is menuId |
Attempt to retrieve the template with the specified keyboard | getTemplateWithShortcut = (shortcut) ->
log.trace()
ext.queryTemplate (template) ->
template.enabled and template.shortcut is shortcut |
Determine whether or not the specified 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 | isExtensionActive = (tab, extensionId) ->
log.trace()
log.debug "Checking activity of #{extensionId}"
isSpecialPage(tab) and tab.url.indexOf(extensionId) isnt -1 |
Determine whether or not | isExtensionGallery = (tab) ->
log.trace()
tab.url.indexOf('https://chrome.google.com/webstore') is 0 |
Determine whether or not | isProtectedPage = (tab) ->
log.trace()
isSpecialPage(tab) or isExtensionGallery tab |
Determine whether or not | isSpecialPage = (tab) ->
log.trace()
tab.url.indexOf('chrome') is 0 |
Ensure | nullIfEmpty = (object) ->
log.trace()
if $.isEmptyObject object then null else object |
Listener for internal requests to the extension. | onRequest = (request, sender, sendResponse) ->
log.trace() |
Don't allow shortcut requests when shortcuts are disabled. | if request.type is 'shortcut' and not store.get 'shortcuts.enabled'
return sendResponse?() |
Select or create a tab for the Options page. | if request.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 sendResponse?() |
Info requests are simple, just send some useful information back. Done! | if request.type in ['info', 'version']
return sendResponse?(
hotkeys: getHotkeys()
id: EXTENSION_ID
version: ext.version
)
data = null
editable = no
id = utils.keyGen '', null, 't', no
link = no
output = null
shortcut = no
shortenMap = {}
tab = null
template = null
windowId = chrome.windows.WINDOW_ID_CURRENT |
Create a runner to manage this asynchronous mess. | runner = new utils.Runner()
unless windowId?
runner.push chrome.windows, 'getCurrent', (win) ->
log.info 'Retrieved the following window...', win
windowId = win.id
runner.next()
runner.pushPacked chrome.tabs, 'query', ->
[active: yes, windowId: windowId, (tabs) ->
log.info 'Retrieved the following tabs...', tabs
tab = tabs[0]
runner.next()
]
runner.push null, -> |
Called whenever the | shortCallback = (text, render) ->
text = if text then render text else @url
url = text.trim()
log.debug 'Following is the contents of a shorten tag...', text |
If | return text unless R_VALID_URL.test url
placeholder = key for own key, value of shortenMap when value is url
unless placeholder?
placeholder = utils.keyGen '', null, 's', no
shortenMap[placeholder] = url |
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 shortened URL. | this[placeholder] = "{#{placeholder}}"
log.debug "Replacing shorten tag with #{placeholder} placeholder"
"{#{placeholder}}" |
If the popup is currently displayed, hide the template list and show a loading animation. | updateProgress null, on |
Attempt to derive the contextual template data. | try
switch request.type
when 'menu'
template = getTemplateWithMenuId request.data.menuItemId
if template?
{data, editable, link} = buildDerivedData tab, request.data,
shortCallback
when 'popup', 'toolbar'
template = getTemplateWithKey request.data.key
data = buildStandardData tab, shortCallback if template?
when 'shortcut'
shortcut = yes
template = getTemplateWithShortcut request.data.key
data = buildStandardData tab, shortCallback if template?
updateProgress 20
if data? then runner.next() else runner.finish()
catch error
log.error error |
Oops! Something went wrong so we should probably let the user know. | runner.finish
message: i18n.get if error instanceof URIError
'result_bad_uri_description'
else
'result_bad_error_description'
success: no
runner.pushPacked null, addAdditionalData, ->
[tab, data, id, editable, shortcut, link, ->
updateProgress 40 |
Ensure all properties of | transformData data |
To complete the data, simply extend it using | $.extend data, template: template
log.debug "Using the following data to render #{template.title}...", data
if template.content
output = Mustache.to_html template.content, data
updateProgress 60
log.debug 'Following string is the rendered result...', output |
If any | if $.isEmptyObject shortenMap
runner.finish
contents: output
success: yes
template: template
else
runner.next()
else |
Display the empty contents error message if the contents of the template itself is empty. | runner.finish
contents: ''
success: yes
template: template
]
runner.push null, callUrlShortener, shortenMap, (response) ->
log.info 'URL shortener service was called one or more times'
updateProgress 80
if response.success
updateUrlShortenerUsage response.service.name, response.oauth |
Request to the URL shortener service was successful so re-render the output. | newOutput = Mustache.to_html output, shortenMap
log.debug 'Following string is the re-rendered result...', newOutput
runner.finish
contents: newOutput
success: yes
template: template
else |
Aw man, something went wrong. Let the user down gently. | response.message ?= i18n.get 'shortener_error', response.service.title
log.warn response.message
runner.finish
message: response.message
success: no
template: template
runner.run (result) ->
type = request.type[0].toUpperCase() + request.type.substr 1
value = request.data.key.charCodeAt 0 if shortcut
analytics.track 'Requests', 'Processed', type, value
updateProgress 100
if result? |
Ensure that the user is notified if they have attempted to copy an empty string to the system clipboard. | if result.success and not result.contents
result.message = i18n.get 'result_bad_empty_description',
result.template.title
result.success = no
if result.success
ext.notification.title = i18n.get 'result_good_title'
ext.notification.titleStyle = 'color: #468847'
ext.notification.description = result.message ?
i18n.get 'result_good_description', result.template.title
ext.copy result.contents
if not isProtectedPage(tab) and
(editable and store.get 'menu.paste') or
(shortcut and store.get 'shortcuts.paste')
chrome.tabs.sendRequest tab.id,
contents: result.contents, id: id, type: 'paste'
sendResponse? contents: result.contents
else
ext.notification.title = i18n.get 'result_bad_title'
ext.notification.titleStyle = 'color: #B94A48'
ext.notification.description = result.message ?
i18n.get 'result_bad_description', result.template.title
showNotification()
sendResponse?()
if result.template?
updateTemplateUsage result.template.key
updateStatistics()
else
updateProgress null, off
log.debug "Finished handling #{type} request" |
Attempt to select a tab in the current window displaying a page whose
location begins with | selectOrCreateTab = (url, callback) ->
log.trace()
tab = null
windowId = chrome.windows.WINDOW_ID_CURRENT |
Create a runner to mange the asynchronous pattern. | runner = new utils.Runner()
unless windowId?
runner.push chrome.windows, 'getCurrent', (win) ->
log.debug 'Retrieved the following window...', win
windowId = win.id
runner.next()
runner.pushPacked chrome.tabs, 'query', ->
[windowId: windowId, (tabs) ->
log.debug 'Retrieved the following tabs...', tabs
result = yes |
Try to find an existing tab. | for tab in tabs
if tab.url.indexOf(url) is 0
existingTab = tab
break
if existingTab? |
Found one! Now to select it. | chrome.tabs.update existingTab.id, active: yes
result = no
else |
Ach well, let's just create a new one. | chrome.tabs.create url: url, active: yes
runner.finish result
]
runner.run callback |
Display a desktop notification informing the user on whether or not the copy
request was successful. | showNotification = ->
log.trace()
if store.get 'notifications.enabled'
webkitNotifications.createHTMLNotification(
utils.url 'pages/notification.html'
).show()
else
ext.reset()
updateProgress null, off |
Update hotkeys stored by the | updateHotkeys = ->
log.trace()
hotkeys = getHotkeys() |
Create a runner to help manage the asynchronous aspect. | runner = new utils.Runner()
runner.push chrome.windows, 'getAll', null, (windows) ->
log.info 'Retrieved the following windows...', windows
for win in windows
do (win) -> runner.push chrome.tabs, 'query', windowId: win.id, (tabs) ->
log.info 'Retrieved 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 request to it). | for tab in tabs when not isProtectedPage tab
chrome.tabs.sendRequest tab.id, hotkeys: hotkeys
runner.next()
runner.next()
runner.run() |
Update the popup UI state to reflect the progress of the current request. | 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' |
Update the progress bar to display the specified | if toggle?
if toggle
progressBar.css 'width', '0%'
popup.delay POPUP_DELAY
templates.hide().delay POPUP_DELAY
loading.show().delay POPUP_DELAY
else
if store.get 'toolbar.close'
popup.queue -> popup.dequeue()[0]?.close()
else
loading.queue ->
loading.hide().dequeue()
progressBar.css 'width', '0%'
templates.queue -> templates.show().dequeue()
else if 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. | maxUsage = 0
utils.query ext.templates, no, (template) ->
maxUsage = template.usage if template.usage > maxUsage
no
popular = ext.queryTemplate (template) -> template.usage is maxUsage |
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 | updateTemplateUsage = (key) ->
log.trace()
for template in ext.templates when template.key is key
template.usage++
break
store.set 'templates', ext.templates
log.info "Used #{template.title} template"
analytics.track 'Templates', 'Used', template.title, Number template.readOnly |
Increment the usage for the URL shortener service with the specified | updateUrlShortenerUsage = (name, oauth) ->
log.trace()
store.modify name, (shortener) -> shortener.usage++
shortener = ext.queryUrlShortener (shortener) -> shortener.name is name
log.info "Used #{shortener.title} URL shortener"
analytics.track 'Shorteners', 'Used', shortener.title, Number oauth |
Data functions | |
Extract additional information from | addAdditionalData = (tab, data, id, editable, shortcut, link, callback) ->
log.trace()
windowId = chrome.windows.WINDOW_ID_CURRENT |
Create a runner to simplify this process. | runner = new utils.Runner()
unless windowId?
runner.push chrome.windows, 'getCurrent', (win) ->
log.info 'Retrieved the following window...', win
windowId = win.id
runner.next()
runner.pushPacked chrome.tabs, 'query', -> [windowId: windowId, (tabs) ->
log.info 'Retrieved the following tabs...', tabs
urls = []
urls.push tab.url for tab in tabs when tab.url not in urls
$.extend data, tabs: urls
runner.next()
]
runner.push navigator.geolocation, 'getCurrentPosition', (position) ->
log.debug 'Retrieved the following geolocation information...', position
coords = {}
for own prop, value of position.coords
coords[prop.toLowerCase()] = if value? then "#{value}" else ''
$.extend data, coords: coords
runner.next()
, (error) ->
log.error error.message
runner.next()
runner.push chrome.cookies, 'getAll', url: data.url, (cookies = []) ->
log.debug 'Retrieved the following cookies...', cookies
$.extend data,
cookie: -> (text, render) -> |
Attempt to find the value for the cookie name. | name = render text
return cookie.value for cookie in cookies when cookie.name is name
''
cookies: (
names = []
for cookie in cookies when cookie.name not in names
names.push cookie.name
names
) |
Try to prevent pages hanging because content script should not have been executed. | if isProtectedPage tab then runner.finish() else runner.next()
runner.push chrome.tabs, 'sendRequest', tab.id,
editable: editable, id: id, link: link, shortcut: shortcut, url: data.url,
(response) ->
log.debug 'Retrieved the following data from the content script...',
response
runner.finish response
runner.run (result = {}) ->
lastModified = if result.lastModified?
time = Date.parse result.lastModified
new Date time unless isNaN time
$.extend data,
author: result.author ? ''
characterset: result.characterSet ? ''
description: result.description ? ''
images: result.images ? []
keywords: result.keywords ? []
lastmodified: -> (text, render) ->
lastModified?.format(render(text) or undefined) ? ''
linkhtml: result.linkHTML ? ''
links: result.links ? []
linktext: result.linkText ? ''
pageheight: result.pageHeight ? ''
pagewidth: result.pageWidth ? ''
referrer: result.referrer ? ''
scripts: result.scripts ? []
selectedimages: result.selectedImages ? []
selectedlinks: result.selectedLinks ? []
selection: result.selection ? ''
selectionhtml: result.selectionHTML ? '' |
Deprecated since 1.0.0, use | selectionlinks: -> @selectedlinks
stylesheets: result.styleSheets ? []
callback() |
Creates an object containing data based on information derived from the specified tab and menu item data. | buildDerivedData = (tab, onClickData, shortCallback) ->
log.trace()
obj = editable: onClickData.editable, link: no
data =
title: tab.title
url: if onClickData.linkUrl
obj.link = yes
onClickData.linkUrl
else if onClickData.srcUrl
onClickData.srcUrl
else if onClickData.frameUrl
onClickData.frameUrl
else
onClickData.pageUrl
obj.data = buildStandardData data, shortCallback
obj |
Construct a data object based on information extracted from | buildStandardData = (tab, shortCallback) ->
log.trace() |
Create a copy of the original tab of the tab to run the compatibility scripts on. | ctab = {}
ctab[prop] = value for own prop, value of 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. | anchor = store.get 'anchor'
bitly = store.get 'bitly'
googl = store.get 'googl'
menu = store.get 'menu'
notifications = store.get 'notifications'
shortcuts = store.get 'shortcuts'
stats = store.get 'stats'
toolbar = store.get 'toolbar'
yourls = store.get 'yourls' |
Merge the initial data with the attributes of the URL
parser and all of the
custom Template properties. | $.extend data, url.attr(),
anchortarget: anchor.target
anchortitle: anchor.title
bitly: bitly.enabled
bitlyaccount: ->
ext.queryUrlShortener((shortener) ->
shortener.name is 'bitly'
).oauth.hasAccessToken()
browser: browser.title
browserversion: browser.version
capitalise: -> @capitalize()
capitalize: -> (text, render) ->
render(text).capitalize() |
Deprecated since 1.0.0, use | contextmenu: -> @menu
cookiesenabled: navigator.cookieEnabled
count: stats.count
customcount: stats.customCount
datetime: -> (text, render) ->
new Date().format(render(text) or undefined) ? ''
decode: -> (text, render) ->
decodeURIComponent(render text) ? ''
depth: screen.colorDepth |
Deprecated since 1.0.0, use | doanchortarget: -> @anchortarget |
Deprecated since 1.0.0, use | doanchortitle: -> @anchortitle
encode: -> (text, render) ->
encodeURIComponent(render text) ? '' |
Deprecated since 0.1.0.2, use | encoded: -> encodeURIComponent @url
favicon: ctab.favIconUrl
fparam: -> (text, render) ->
url.fparam(render text) ? ''
fparams: nullIfEmpty url.fparam()
fsegment: -> (text, render) ->
url.fsegment(parseInt render(text), 10) ? ''
fsegments: url.fsegment()
googl: googl.enabled
googlaccount: ->
ext.queryUrlShortener((shortener) ->
shortener.name is 'googl'
).oauth.hasAccessToken() |
Deprecated since 1.0.0, use | googloauth: -> @googlaccount()
java: navigator.javaEnabled()
length: -> (text, render) ->
render(text).length
linkmarkdown: -> md @linkhtml
lowercase: -> (text, render) ->
render(text).toLowerCase()
menu: menu.enabled
menuoptions: menu.options
menupaste: menu.paste
notifications: notifications.enabled
notificationduration: notifications.duration * .001
offline: not navigator.onLine |
Deprecated since 0.1.0.2, use | originalsource: -> @originalurl
originaltitle: tab.title or url.attr 'source'
originalurl: tab.url
os: operatingSystem
param: -> (text, render) ->
url.param(render text) ? ''
params: nullIfEmpty url.param()
plugins: (
results = []
for plugin in navigator.plugins when plugin.name not in results
results.push plugin.name
results.sort()
)
popular: ext.queryTemplate (template) ->
template.key is stats.popular
screenheight: screen.height
screenwidth: screen.width
segment: -> (text, render) ->
url.segment(parseInt render(text), 10) ? ''
segments: url.segment()
selectionmarkdown: -> md @selectionhtml |
Deprecated since 1.0.0, use | short: -> @shorten()
shortcuts: shortcuts.enabled
shortcutspaste: shortcuts.paste
shorten: -> shortCallback
tidy: -> (text, render) ->
render(text).replace(/([ \t]+)/g, ' ').trim()
title: ctab.title or url.attr 'source'
toolbarclose: toolbar.close |
Deprecated since 1.0.0, use the inverse of | toolbarfeature: -> not @toolbarpopup |
Deprecated since 1.0.0, use | toolbarfeaturedetails: -> @toolbarstyle |
Deprecated since 1.0.0, use | toolbarfeaturename: -> @toolbarkey
toolbarkey: toolbar.key
toolbaroptions: toolbar.options
toolbarpopup: toolbar.popup |
Obsolete since 1.1.0, functionality has been removed. | toolbarstyle: no
trim: -> (text, render) ->
render(text).trim()
trimleft: -> (text, render) ->
render(text).trimLeft()
trimright: -> (text, render) ->
render(text).trimRight()
uppercase: -> (text, render) ->
render(text).toUpperCase()
url: url.attr 'source'
version: ext.version
yourls: yourls.enabled
yourlsauthentication: yourls.authentication
yourlspassword: yourls.password
yourlssignature: yourls.signature
yourlsurl: yourls.url
yourlsusername: yourls.username
data |
Ensure there is a lower case variant of all properties of | transformData = (data, deleteOld) ->
log.trace()
for own prop, value of data when R_UPPER_CASE.test prop
data[prop.toLowerCase()] = value
delete data[prop] if deleteOld |
HTML building functions | |
Build the HTML to populate the popup with to optimize popup loading times. | buildPopup = ->
log.trace()
items = $() |
Generate the HTML for each template. | for template in ext.templates when template.enabled
items = items.add buildTemplate template |
Add a generic message to state the obvious... that the list is empty. | if items.length is 0
items = items.add $('<li/>',
class: 'empty'
).append($ '<i/>',
class: 'icon-'
).append " #{i18n.get 'empty'}" |
Add a link to the options page if the user doesn't mind. | if store.get 'toolbar.options'
items = items.add $ '<li/>', class: 'divider'
anchor = $ '<a/>',
class: 'options'
'data-type': 'options'
anchor.append $ '<i/>', class: icons.getClass 'cog'
anchor.append " #{i18n.get 'options'}"
items = items.add $('<li/>').append anchor
ext.templatesHtml = $('<div/>').append(items).html() |
Create an | buildTemplate = (template) ->
log.trace()
log.debug "Creating popup list item for #{template.title}"
anchor = $ '<a/>',
'data-key': template.key
'data-type': 'popup'
if template.shortcut and store.get 'shortcuts.enabled'
if ext.isThisPlatform 'mac'
modifiers = ext.SHORTCUT_MAC_MODIFIERS
else
modifiers = ext.SHORTCUT_MODIFIERS
anchor.append $ '<span/>',
class: 'pull-right',
html: "#{modifiers}#{template.shortcut}"
anchor.append $ '<i/>', class: icons.getClass template.image
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 | init_update = ->
log.trace() |
Update the update progress indicator itself. | if store.exists 'update_progress'
store.modify 'updates', (updates) ->
progress = store.remove 'update_progress'
for own namespace, versions of progress
updates[namespace] = if versions?.length then versions.pop() else '' |
Create updater for the | updater = new store.Updater 'settings'
isNewInstall = updater.isNew |
Define the processes for all required updates to the | updater.update '0.1.0.0', ->
log.info 'Updating general settings for 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', ->
log.info 'Updating general settings for 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 'anchor', (anchor) ->
anchor.target = store.get('doAnchorTarget') ? off
anchor.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', ->
log.info 'Updating general settings for 1.1.0'
store.set 'shortcuts',
enabled: store.get('shortcuts') ? yes |
Initialize the settings related to statistical information. | initStatistics = ->
log.trace()
updateStatistics() |
Initialize | initTemplate = (template, templates) ->
log.trace() |
Derive the index of | idx = templates.indexOf utils.query templates, yes, (tmpl) ->
tmpl.key is template.key
if idx is -1 |
| log.debug 'Adding the following predefined template...', template
templates.push template
else |
| log.debug 'Ensuring following template adheres to structure...', template
if template.readOnly |
| 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 |
| 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()
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_update = ->
log.trace() |
Create updater for the | updater = new store.Updater 'features'
updater.rename 'templates' |
Define the processes for all required updates to the | updater.update '0.1.0.0', ->
log.info 'Updating template settings for 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', ->
log.info 'Updating template settings for 0.2.0.0'
names = store.get('features') ? []
for name in names when typeof name is 'string'
store.rename "feat_#{name}_template", "feat_#{name}_content"
image = store.get "feat_#{name}_image"
if typeof image is 'string'
if image in ['', 'spacer.gif', 'spacer.png']
store.set "feat_#{name}_image", 0
else
for legacy, i in icons.LEGACY
oldImg = legacy.image.replace /^tmpl/, 'feat'
if "#{oldImg}.png" is image
store.set "feat_#{name}_image", i + 1
break
else
store.set "feat_#{name}_image", 0
updater.update '1.0.0', ->
log.info 'Updating template settings for 1.0.0'
names = store.remove('features') ? []
templates = []
toolbarFeatureName = store.get 'toolbarFeatureName'
for name in names when typeof name is 'string'
image = store.remove("feat_#{name}_image") ? 0
image--
image = if image >= 0
icons.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', ->
log.info 'Updating template settings for 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
switch template.key
when 'PREDEFINED.00001'
template.image = if template.image is 'tmpl_globe'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00002'
template.image = if template.image is 'tmpl_link'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00003'
template.image = if template.image is 'tmpl_html'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00004'
template.image = if template.image is 'tmpl_component'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00005'
template.image = if template.image is 'tmpl_discussion'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00006'
template.image = if template.image is 'tmpl_discussion'
base.image
else
icons.fromLegacy(template.image)?.name or ''
when 'PREDEFINED.00007'
template.image = if template.image is 'tmpl_note'
base.image
else
icons.fromLegacy(template.image)?.name or ''
else template.image = base.image
else
template.image = icons.fromLegacy(template.image)?.name or '' |
Initialize the settings related to the toolbar/browser action. | initToolbar = ->
log.trace()
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_update = ->
log.trace() |
Create updater for the | updater = new store.Updater 'toolbar' |
Define the processes for all required updates to the | updater.update '1.0.0', ->
log.info 'Updating toolbar settings for 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', ->
log.info 'Updating toolbar settings for 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: {}
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_update = ->
log.trace() |
Create updater for the | updater = new store.Updater 'shorteners' |
Define the processes for all required updates to the | updater.update '0.1.0.0', ->
log.info 'Updating URL shortener settings for 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', ->
log.info 'Updating URL shortener settings for 1.0.0'
bitly = store.get 'bitly'
store.set 'bitly',
apiKey: store.get('bitlyApiKey') ? ''
enabled: if typeof bitly is 'boolean' then bitly else yes
username: store.get('bitlyUsername') ? ''
store.remove 'bitlyApiKey', 'bitlyUsername'
googl = store.get 'googl'
store.set 'googl',
enabled: if typeof googl is 'boolean' then googl else no
store.remove 'googlOAuth'
yourls = store.get 'yourls'
store.set 'yourls',
enabled: if typeof yourls is 'boolean' 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
'advanced'
else if yourls.password and yourls.username
'basic'
else
''
store.remove store.search(/^oauth_token.*/)... |
URL shortener functions | |
Call the active URL shortener service for each URL in | callUrlShortener = (map, callback) ->
log.trace()
service = getActiveUrlShortener()
endpoint = service.url()
title = service.title |
Ensure the service URL exists in case it is user-defined (e.g. YOURLS). | unless endpoint
return callback
message: i18n.get 'shortener_config_error', title
service: service
success: no |
Create a runner to control the dependencies in the asynchronous processes. | runner = new utils.Runner()
for own placeholder, url of map
do (placeholder, url) ->
oauth = !!service.oauth?.hasAccessToken()
if oauth
runner.push service.oauth, 'authorize', -> runner.next()
runner.push null, ->
params = service.getParameters url
endpoint += "?#{$.param params}" if params? |
Build the HTTP 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 to | if xhr.readyState is 4
if xhr.status is 200
map[placeholder] = service.output xhr.responseText
runner.next oauth: oauth, success: yes
else |
Something went wrong so let's tell the user. | runner.finish
message: i18n.get('shortener_detailed_error', [title, url])
success: no |
Finally, send the HTTP request. | xhr.send service.input url
runner.run (result = {}) -> callback
message: result.message
oauth: result.oauth
service: service
success: result.success |
Retrieve the active URL shortener service. | getActiveUrlShortener = ->
log.trace() |
Attempt to lookup enabled URL shortener service. | shortener = ext.queryUrlShortener (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 = 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: '⇧⌥' |
Public variables | |
Information specifying what should be displaying in the notification. | notification:
description: ''
descriptionStyle: ''
html: ''
icon: utils.url '../images/icon_48.png'
iconStyle: ''
title: ''
titleStyle: '' |
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. | templatesHtml: '' |
Current version of Template. | version: '' |
Public functions | |
Add | copy: (str, hidden) ->
log.trace()
sandbox = $('#sandbox').val(str).select()
document.execCommand 'copy'
log.debug 'Copied the following string...', str
sandbox.val ''
showNotification() unless hidden |
Attempt to retrieve the key of the template with the specified | 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. | 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' |
Create a runner to ensure asynchronous dependencies are met. | runner = new utils.Runner()
runner.push jQuery, 'getJSON', utils.url('manifest.json'), (data) => |
It's nice knowing what version is running. | @version = data.version
runner.next()
runner.push jQuery, 'getJSON', utils.url('services.json'), (data) ->
services = data
shortener.oauth = shortener.oauth?() for shortener in SHORTENERS
runner.next()
runner.run => |
Begin initialization. | store.init
anchor: {}
menu: {}
notifications: {}
shortcuts: {}
stats: {}
templates: []
toolbar: {}
init_update()
store.modify 'anchor', (anchor) ->
anchor.target ?= off
anchor.title ?= off
store.modify 'menu', (menu) ->
menu.enabled ?= yes
menu.options ?= yes
menu.paste ?= no
store.modify 'notifications', (notifications) ->
notifications.duration ?= 3000
notifications.enabled ?= yes
store.modify 'shortcuts', (shortcuts) ->
shortcuts.enabled ?= yes
shortcuts.paste ?= no
initTemplates()
initToolbar()
initStatistics()
initUrlShorteners() |
Add listener for toolbar/browser action clicks. | chrome.browserAction.onClicked.addListener (tab) -> onRequest
data: key: store.get 'toolbar.key'
type: 'toolbar' |
Add listeners for internal and external requests. | chrome.extension.onRequest.addListener onRequest
chrome.extension.onRequestExternal.addListener (req, sender, cb) ->
block = isBlacklisted sender.id
analytics.track 'External Requests', 'Started', sender.id,
Number !block
if block
log.debug "Blocking external request from #{sender.id}"
cb?()
else
log.debug "Accepting external request from #{sender.id}"
onRequest req, sender, cb |
Derive the browser and OS information. | browser.version = getBrowserVersion()
operatingSystem = getOperatingSystem()
if isNewInstall
analytics.track 'Installs', 'New', @version, Number isProductionBuild |
Execute content scripts now that we know the version. | executeScriptsInExistingWindows() |
Determine whether or not | isThisPlatform: (os) ->
log.trace()
navigator.userAgent.toLowerCase().indexOf(os) isnt -1 |
Attempt to retrieve the contents of the system clipboard as a string. | paste: ->
log.trace()
result = ''
sandbox = $('#sandbox').val('').select()
result = sandbox.val() if document.execCommand 'paste'
log.debug 'Pasted the following string...', result
sandbox.val ''
result |
Retrieve the first template that passes the specified | queryTemplate: (filter, singular = yes) ->
log.trace()
utils.query @templates, singular, filter |
Retrieve the first URL shortener service that passes the specified
| queryUrlShortener: (filter, singular = yes) ->
log.trace()
utils.query SHORTENERS, singular, filter |
Reset the notification information associated with the current copy
request. | reset: ->
log.trace()
@notification =
description: ''
descriptionStyle: ''
html: ''
icon: utils.url '../images/icon_48.png'
iconStyle: ''
title: ''
titleStyle: '' |
Update the context menu items to reflect the currently enabled templates. | updateContextMenu: ->
log.trace() |
Ensure that any previously added context menu items are removed. | chrome.contextMenus.removeAll => |
Called whenever a template menu item is clicked. | onMenuClick = (info, tab) -> onRequest data: info, type: 'menu'
menu = store.get 'menu'
if 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
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) -> onRequest type: 'options'
parentId: parentId
title: i18n.get 'options' |
Update the local list of templates to reflect those persisted. | updateTemplates: ->
log.trace()
@templates = store.get 'templates'
@templates.sort (a, b) -> a.index - b.index
buildPopup()
@updateContextMenu()
updateStatistics()
updateHotkeys() |
Update the toolbar/browser action depending on the current settings. | updateToolbar: ->
log.trace()
key = store.get 'toolbar.key'
template = getTemplateWithKey key if key
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.setPopup popup: '' |
Initialize | utils.ready -> ext.init()
|