Last active
January 19, 2021 20:18
-
-
Save neocotic/1977657 to your computer and use it in GitHub Desktop.
Google Chrome extension helpers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (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 '_', '-' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (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', {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# (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