Skip to content

Instantly share code, notes, and snippets.

@neocotic
Last active January 19, 2021 20:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save neocotic/1977657 to your computer and use it in GitHub Desktop.
Save neocotic/1977657 to your computer and use it in GitHub Desktop.
Google Chrome extension helpers
# (c) 2012 [neocotic](http://github.com/neocotic)
# Freely distributable under the MIT license.
# Private constants
# -----------------
# Code for extension's analytics account.
ACCOUNT = 'UA-12345678-1'
# Source URL of the analytics script.
SOURCE = 'https://ssl.google-analytics.com/ga.js'
# Analytics setup
# ---------------
analytics = window.analytics = new class Analytics extends utils.Class
# Public functions
# ----------------
# Add analytics to the current page.
add: ->
# Setup tracking details for analytics.
_gaq = window._gaq ?= []
_gaq.push ['_setAccount', ACCOUNT]
_gaq.push ['_trackPageview']
# Inject script to capture analytics.
ga = document.createElement 'script'
ga.async = 'async'
ga.src = SOURCE
script = document.getElementsByTagName('script')[0]
script.parentNode.insertBefore ga, script
# Determine whether or not analytics are enabled.
enabled: -> not store? or store.get 'analytics'
# Remove analytics from the current page.
remove: ->
# Delete scripts used to capture analytics.
for script in document.querySelectorAll "script[src='#{SOURCE}']"
script.parentNode.removeChild script
# Remove tracking details for analytics.
delete window._gaq
# Create an event with the information provided and track it in analytics.
track: (category, action, label, value, nonInteraction) -> if @enabled()
event = ['_trackEvent']
# Add the required information.
event.push category
event.push action
# Add the optional information where possible.
event.push label if label?
event.push value if value?
event.push nonInteraction if nonInteraction?
# Add the event to analytics.
_gaq = window._gaq ?= []
_gaq.push event
# Configuration
# -------------
# Initialize analytics.
store?.init 'analytics', yes
# (c) 2012 [neocotic](http://github.com/neocotic)
# Freely distributable under the MIT license.
# Private variables
# -----------------
# Mapping for internationalization handlers.
# Each handler represents an attribute (based on the property name) and is
# called for each attribute found within the node currently being processed.
handlers =
# Replace the HTML content of `element` with the named message looked up for
# `name`.
'i18n-content': (element, name, map) ->
subs = subst element, name, map
element.innerHTML = i18n.get name, subs
# Adds options to the select `element` with the message looked up for
# `name`.
'i18n-options': (element, name, map) ->
subs = subst element, name, map
values = i18n.get name, subs
for value in values
option = document.createElement 'option'
if typeof value is 'string'
option.text = option.value = value
else
option.text = value[1]
option.value = value[0]
element.appendChild option
# Replace the value of the properties and/or attributes of `element` with the
# messages looked up for their corresponding values.
'i18n-values': (element, value, map) ->
parts = value.replace(/\s/g, '').split ';'
for part in parts
prop = part.match /^([^:]+):(.+)$/
if prop
propName = prop[1]
propExpr = prop[2]
propSubs = subst element, propExpr, map
if propName.indexOf('.') is 0
path = propName.slice(1).split '.'
obj = element
obj = obj[path.shift()] while obj and path.length > 1
if obj
path = path[0]
obj[path] = i18n.get propExpr, propSubs
process element, map if path is 'innerHTML'
else
element.setAttribute propName, i18n.get propExpr, propSubs
# List of internationalization attributes/handlers available.
attributes = (key for own key of handlers)
# Selector containing the available internationalization attributes/handlers
# which is used by `process` to query all elements.
selector = "[#{attributes.join '],['}]"
# Private functions
# -----------------
# Find all elements to be localized and call their corresponding handler(s).
process = (node, map) -> for element in node.querySelectorAll selector
for name in attributes
attribute = element.getAttribute name
handlers[name] element, attribute, map if attribute?
# Find an array of substitution strings using the element's ID and the message
# key as the mapping.
subst = (element, value, map) ->
if map
for own prop, map2 of map when prop is element.id
for own prop2, target of map2 when prop2 is value
subs = target
break
break
subs
# Internationalization setup
# --------------------------
i18n = window.i18n = new class Internationalization extends utils.Class
# Public variables
# ----------------
# Default configuration for how internationalization is managed.
manager:
get: (name, substitutions = []) ->
message = @messages[name]
if message? and substitutions.length > 0
for sub, i in substitutions
message = message.replace new RegExp("\\$#{i + 1}", 'g'), sub
message
langs: -> []
locale: -> navigator.language
node: document
# Default container for localized messages.
messages: {}
# Public functions
# ----------------
# Localize the specified `attribute` of all the selected elements.
attribute: (selector, attribute, name, subs) ->
elements = @manager.node.querySelectorAll selector
element.setAttribute attribute, @get name, subs for element in elements
# Localize the contents of all the selected elements.
content: (selector, name, subs) ->
elements = @manager.node.querySelectorAll selector
element.innerHTML = @get name, subs for element in elements
# Add localized `option` elements to the selected elements.
options: (selector, name, subs) ->
elements = @manager.node.querySelectorAll selector
for element in elements
values = @get name, subs
for value in values
option = document.createElement 'option'
if typeof value is 'string'
option.text = option.value = value
else
option.text = value[1]
option.value = value[0]
element.appendChild option
# Get the localized message.
get: -> @manager.get arguments...
# Localize all relevant elements within the managed node (`document` by
# default).
init: (map) -> process @manager.node, map
# Retrieve the accepted languages.
langs: -> @manager.langs arguments...
# Retrieve the current locale.
locale: -> @manager.locale arguments...
# Configuration
# -------------
# Reconfigure the internationalization manager to work for Chrome extensions.
# Convenient shorthand for `chrome.i18n.getMessage`.
i18n.manager.get = -> chrome.i18n.getMessage arguments...
# Convenient shorthand for `chrome.i18n.getAcceptLanguages`.
i18n.manager.langs = -> chrome.i18n.getAcceptLanguages arguments...
# Parse the predefined `@@ui_locale` message.
i18n.manager.locale = -> i18n.get('@@ui_locale').replace '_', '-'
# (c) 2012 [neocotic](http://github.com/neocotic)
# Freely distributable under the MIT license.
# Private constants
# -----------------
# Define the different logging levels privately first.
LEVELS =
trace: 10
information: 20
debug: 30
warning: 40
error: 50
# Private variables
# -----------------
# Ensure that all logs are sent to the background pages console.
{console} = chrome.extension.getBackgroundPage()
# Private functions
# -----------------
# Determine whether or not logging is enabled for the specified `level`.
loggable = (level) -> log.config.enabled and level >= log.config.level
# Logging setup
# -------------
log = window.log = new class Log extends utils.Class
# Public constants
# ----------------
# Expose the available logging levels.
TRACE: LEVELS.trace
INFORMATION: LEVELS.information
DEBUG: LEVELS.debug
WARNING: LEVELS.warning
ERROR: LEVELS.error
# A collection of all of the levels to allow iteration.
LEVELS: (
array = []
array.push name: key, value: value for own key, value of LEVELS
array.sort (a, b) -> a.value - b.value
)
# Public variables
# ----------------
# Hold the current conguration for the logger.
config:
enabled: no
level: LEVELS.debug
# Public functions
# ----------------
# Create/increment a counter and output its current count for all `names`.
count: (names...) ->
console.count name for name in names if loggable @DEBUG
# Output all debug `entries`.
debug: (entries...) ->
console.debug entry for entry in entries if loggable @DEBUG
# Display an interactive listing of the properties of all `entries`.
dir: (entries...) ->
console.dir entry for entry in entries if loggable @DEBUG
# Output all error `entries`.
error: (entries...) ->
console.error entry for entry in entries if loggable @ERROR
# Output all informative `entries`.
info: (entries...) ->
console.info entry for entry in entries if loggable @INFORMATION
# Output all general `entries`.
out: (entries...) ->
console.log entry for entry in entries if @config.enabled
# Start a timer for all `names`.
time: (names...) ->
console.time name for name in names if loggable @DEBUG
# Stop a timer and output its elapsed time in milliseconds for all `names`.
timeEnd: (names...) ->
console.timeEnd name for name in names if loggable @DEBUG
# Output a stack trace.
trace: (caller = @trace) ->
console.log new @StackTrace(caller).stack if loggable @TRACE
# Output all warning `entries`.
warn: (entries...) ->
console.warn entry for entry in entries if loggable @WARNING
# Public classes
# --------------
# `StackTrace` allows the current stack trace to be retrieved in the easiest
# way possible.
class log.StackTrace extends utils.Class
# Create a new instance of `StackTrace` for the `caller`.
constructor: (caller = log.StackTrace) ->
# Create the stack trace and assign it to a new `stack` property.
Error.captureStackTrace this, caller
# Configuration
# -------------
# Initialize logging.
if store?
store.init 'logger', {}
store.modify 'logger', (logger) ->
logger.enabled ?= no
logger.level ?= LEVELS.debug
log.config = logger
# (c) 2012 [neocotic](http://github.com/neocotic)
# Freely distributable under the MIT license.
# Private functions
# -----------------
# Attempt to dig down in to the `root` object and stop on the parent of the
# target property.
# Return the progress of the mining as an array in this structure;
# `[root-object, base-object, base-path, target-parent, target-property]`.
dig = (root, path, force, parseFirst = yes) ->
result = [root]
if path and path.indexOf('.') isnt -1
path = path.split '.'
object = base = root[basePath = path.shift()]
object = base = tryParse object if parseFirst
while object and path.length > 1
object = object[path.shift()]
object = {} if not object? and force
result.push base, basePath, object, path.shift()
else
result.push root, path, root, path
result
# Attempt to parse `value` as a JSON object if it's not `null`; otherwise just
# return `value`.
tryParse = (value) -> if value? then JSON.parse value else value
# Attempt to stringify `value` in to a JSON string if it's not `null`;
# otherwise just return `value`.
tryStringify = (value) -> if value? then JSON.stringify value else value
# Store setup
# -----------
store = window.store = new class Store extends utils.Class
# Public functions
# ----------------
# Create a backup string containing all the information contained within
# `localStorage`.
# The data should be formatted as a JSON string and then encoded to ensure
# that it can easily be copied from/pasted to the console.
# The string created may contain sensitive user data in plain text if they
# have provided any to the extension.
backup: ->
data = {}
data[key] = value for own key, value of localStorage
encodeURIComponent JSON.stringify data
# Clear all keys from `localStorage`.
clear: -> delete localStorage[key] for own key of localStorage
# Determine whether or not the specified `keys` exist in `localStorage`.
exists: (keys...) ->
return no for key in keys when not localStorage.hasOwnProperty key
yes
# Retrieve the value associated with the specified `key` from
# `localStorage`.
# If the value is found, parse it as a JSON object before being returning
# it.
get: (key) ->
parts = dig localStorage, key
if parts[3]
value = parts[3][parts[4]]
# Ensure that the value is parsed if retrieved directly from
# `localStorage`.
value = tryParse value if parts[3] is parts[0]
value
# Initialize the value of the specified key(s) in `localStorage`.
# `keys` can either be a string for a single key (in which case
# `defaultValue` should also be specified) or a map of key/default value
# pairs.
# If the value is currently `undefined`, assign the specified default value;
# otherwise reassign itself.
init: (keys, defaultValue) -> switch typeof keys
when 'object'
@init key, defaultValue for own key, defaultValue of keys
when 'string' then @set keys, @get(keys) ? defaultValue
# For each of the specified `keys`, retrieve their value in `localStorage`
# and pass it, along with the key, to the `callback` function provided.
# This functionality is very useful when just manipulating existing values.
modify: (keys..., callback) -> for key in keys
value = @get key
callback? value, key
@set key, value
# Remove the specified `keys` from `localStorage`.
# If only one key is specified then the current value of that key is returned
# after it has been removed.
remove: (keys...) ->
if keys.length is 1
value = @get keys[0]
delete localStorage[keys[0]]
return value
delete localStorage[key] for key in keys
# Copy the value of the existing key to that of the new key then remove the
# old key from `localStorage`.
# If the old key doesn't exist in `localStorage`, assign the specified
# default value to it instead.
rename: (oldKey, newKey, defaultValue) ->
if @exists oldKey
@set newKey, @get oldKey
@remove oldKey
else
@set newKey, defaultValue
# Restore `localStorage` with data from the backup string provided.
# The string should be decoded and then parsed as a JSON string in order to
# process the data.
restore: (str) ->
data = JSON.parse decodeURIComponent str
localStorage[key] = value for own key, value of data
# Search `localStorage` for all keys that match the specified regular
# expression.
search: (regex) -> key for own key of localStorage when regex.test key
# Set the value of the specified key(s) in `localStorage`.
# `keys` can either be a string for a single key (in which case `value`
# should also be specified) or a map of key/value pairs.
# If the specified value is `undefined`, assign that value directly to the
# key; otherwise transform it to a JSON string beforehand.
set: (keys, value) -> switch typeof keys
when 'object' then @set key, value for own key, value of keys
when 'string'
oldValue = @get keys
localStorage[keys] = tryStringify value
oldValue
# Public classes
# --------------
# `Updater` simplifies the process of updating settings between updates.
# Inlcuding, but not limited to, data transformations and migration.
class store.Updater extends utils.Class
# Create a new instance of `Updater` for `namespace`.
# Also indicate whether or not `namespace` existed initially.
constructor: (@namespace) -> @isNew = not @exists()
# Determine whether or not this namespace exists.
exists: -> store.get("updates.#{@namespace}")?
# Remove this namespace.
remove: -> store.modify 'updates', (updates) => delete updates[@namespace]
# Rename this namespace to `namespace`.
rename: (namespace) -> store.modify 'updates', (updates) =>
updates[namespace] = updates[@namespace] if updates[@namespace]?
delete updates[@namespace]
@namespace = namespace
# Update this namespace to `version` using the `processor` provided when
# `version` is newer.
update: (version, processor) -> store.modify 'updates', (updates) =>
updates[@namespace] ?= ''
if updates[@namespace] < version
processor?()
updates[@namespace] = version
# Configuration
# -------------
# Initialize updates.
store.init 'updates', {}
# (c) 2012 [neocotic](http://github.com/neocotic)
# Freely distributable under the MIT license.
# Private classes
# ---------------
# `Class` makes for more readable logs etc. as it overrides `toString` to
# output the name of the implementing class.
class Class
# Override the default `toString` implementation to provide a cleaner output.
toString: -> @constructor.name
# Private variables
# -----------------
# Mapping of all timers currently being managed.
timings = {}
# Utilities setup
# ---------------
utils = window.utils = new class Utils extends Class
# Public functions
# ----------------
# Call a function asynchronously with the arguments provided and then pass
# the returned value to `callback` if it was specified.
async: (fn, args..., callback) ->
if callback? and typeof callback isnt 'function'
args.push callback
callback = null
setTimeout ->
result = fn args...
callback? result
, 0
# Generate a unique key based on the current time and using a randomly
# generated hexadecimal number of the specified length.
keyGen: (separator = '.', length = 5, prefix = '', upperCase = yes) ->
parts = []
# Populate the segment(s) to attempt uniquity.
parts.push new Date().getTime()
if length > 0
min = @repeat '1', '0', if length is 1 then 1 else length - 1
max = @repeat 'f', 'f', if length is 1 then 1 else length - 1
min = parseInt min, 16
max = parseInt max, 16
parts.push @random min, max
# Convert segments to their hexadecimal (base 16) forms.
parts[i] = part.toString 16 for part, i in parts
# Join all segments using `separator` and append to the `prefix` before
# potentially transforming it to upper case.
key = prefix + parts.join separator
if upperCase then key.toUpperCase() else key.toLowerCase()
# Retrieve the first entity/all entities that pass the specified `filter`.
query: (entities, singular, filter) ->
if singular
return entity for entity in entities when filter entity
else
entity for entity in entities when filter entity
# Generate a random number between the `min` and `max` values provided.
random: (min, max) -> Math.floor(Math.random() * (max - min + 1)) + min
# Repeat the string provided the specified number of times.
repeat: (str = '', repeatStr = str, count = 1) ->
if count isnt 0
# Repeat to the right if `count` is positive.
str += repeatStr for i in [1..count] if count > 0
# Repeat to the left if `count` is negative.
str = repeatStr + str for i in [1..count*-1] if count < 0
str
# Start a new timer for the specified `key`.
# If a timer already exists for `key`, return the time difference in
# milliseconds.
time: (key) ->
if timings.hasOwnProperty key
new Date().getTime() - timings[key]
else
timings[key] = new Date().getTime()
# End the timer for the specified `key` and return the time difference in
# milliseconds and remove the timer.
# If no timer exists for `key`, simply return `0'.
timeEnd: (key) ->
if timings.hasOwnProperty key
start = timings[key]
delete timings[key]
new Date().getTime() - start
else
0
# Convenient shorthand for `chrome.extension.getURL`.
url: -> chrome.extension.getURL arguments...
# Public classes
# --------------
# Objects within the extension should extend this class wherever possible.
utils.Class = Class
# `Runner` allows asynchronous code to be executed dependently in an
# organized manner.
class utils.Runner extends utils.Class
# Create a new instance of `Runner`.
constructor: -> @queue = []
# Finalize the process by resetting this `Runner` an then calling `onfinish`,
# if it was specified when `run` was called.
# Any arguments passed in should also be passed to the registered `onfinish`
# handler.
finish: (args...) ->
@queue = []
@started = no
@onfinish? args...
# Remove the next task from the queue and call it.
# Finish up if there are no more tasks in the queue, ensuring any `args` are
# passed along to `onfinish`.
next: (args...) ->
if @started
if @queue.length
ctx = fn = null
task = @queue.shift()
# Determine what context the function should be executed in.
switch typeof task.reference
when 'function' then fn = task.reference
when 'string'
ctx = task.context
fn = ctx[task.reference]
# Unpack the arguments where required.
if typeof task.args is 'function'
task.args = task.args.apply null
fn?.apply ctx, task.args
return yes
else
@finish args...
no
# Add a new task to the queue using the values provided.
# `reference` can either be the name of the property on the `context` object
# which references the target function or the function itself. When the
# latter, `context` is ignored and should be `null` (not omitted). All of the
# remaining `args` are passed to the function when it is called during the
# process.
push: (context, reference, args...) -> @queue.push
args: args
context: context
reference: reference
# Add a new task to the queue using the *packed* values provided.
# This method varies from `push` since the arguments are provided in the form
# of a function which is called immediately before the function, which allows
# any dependent arguments to be correctly referenced.
pushPacked: (context, reference, packedArgs) -> @queue.push
args: packedArgs
context: context
reference: reference
# Start the process by calling the first task in the queue and register the
# `onfinish` function provided.
run: (@onfinish) ->
@started = yes
@next()
# Remove the specified number of tasks from the front of the queue.
skip: (count = 1) -> @queue.splice 0, count
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment