diff options
Diffstat (limited to 'lib/utils.coffee')
| -rw-r--r-- | lib/utils.coffee | 192 |
1 files changed, 186 insertions, 6 deletions
diff --git a/lib/utils.coffee b/lib/utils.coffee index 64c87842..93045f32 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -2,6 +2,13 @@ Utils = getCurrentVersion: -> chrome.runtime.getManifest().version + # Returns true whenever the current page is from the extension's origin (and thus can access the + # extension's localStorage). + isExtensionPage: -> document.location?.origin + "/" == chrome.extension.getURL "" + + # Returns true whenever the current page is the extension's background page. + isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage() == window + # Takes a dot-notation object string and call the function # that it points to with the correct value for 'this'. invokeCommandString: (str, argArray) -> @@ -26,16 +33,30 @@ Utils = -> id += 1 hasChromePrefix: do -> - chromePrefixes = [ "about:", "view-source:", "extension:", "chrome-extension:", "data:", "javascript:" ] + chromePrefixes = [ "about:", "view-source:", "extension:", "chrome-extension:", "data:" ] (url) -> for prefix in chromePrefixes return true if url.startsWith prefix false + hasJavascriptPrefix: (url) -> + url.startsWith "javascript:" + hasFullUrlPrefix: do -> urlPrefix = new RegExp "^[a-z]{3,}://." (url) -> urlPrefix.test url + # Decode valid escape sequences in a URI. This is intended to mimic the best-effort decoding + # Chrome itself seems to apply when a Javascript URI is enetered into the omnibox (or clicked). + # See https://code.google.com/p/chromium/issues/detail?id=483000, #1611 and #1636. + decodeURIByParts: (uri) -> + uri.split(/(?=%)/).map((uriComponent) -> + try + decodeURIComponent uriComponent + catch + uriComponent + ).join "" + # Completes a partial URL (without scheme) createFullUrl: (partialUrl) -> if @hasFullUrlPrefix(partialUrl) then partialUrl else ("http://" + partialUrl) @@ -93,11 +114,32 @@ Utils = query = query.split(/\s+/) if typeof(query) == "string" query.map(encodeURIComponent).join "+" - # Creates a search URL from the given :query. - createSearchUrl: (query) -> - # It would be better to pull the default search engine from chrome itself. However, unfortunately chrome - # does not provide an API for doing so. - Settings.get("searchUrl") + @createSearchQuery query + # Create a search URL from the given :query (using either the provided search URL, or the default one). + # It would be better to pull the default search engine from chrome itself. However, chrome does not provide + # an API for doing so. + createSearchUrl: (query, searchUrl = Settings.get("searchUrl")) -> + searchUrl += "%s" unless 0 <= searchUrl.indexOf "%s" + searchUrl.replace /%s/g, @createSearchQuery query + + # Extract a query from url if it appears to be a URL created from the given search URL. + # For example, map "https://www.google.ie/search?q=star+wars&foo&bar" to "star wars". + extractQuery: do => + queryTerminator = new RegExp "[?&#/]" + httpProtocolRegexp = new RegExp "^https?://" + (searchUrl, url) -> + url = url.replace httpProtocolRegexp + searchUrl = searchUrl.replace httpProtocolRegexp + [ searchUrl, suffixTerms... ] = searchUrl.split "%s" + # We require the URL to start with the search URL. + return null unless url.startsWith searchUrl + # We require any remaining terms in the search URL to also be present in the URL. + for suffix in suffixTerms + return null unless 0 <= url.indexOf suffix + # We use try/catch because decodeURIComponent can throw an exception. + try + url[searchUrl.length..].split(queryTerminator)[0].split("+").map(decodeURIComponent).join " " + catch + null # Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters # as Chrome will do that for us. @@ -107,6 +149,8 @@ Utils = # Special-case about:[url], view-source:[url] and the like if Utils.hasChromePrefix string string + else if Utils.hasJavascriptPrefix string + Utils.decodeURIByParts string else if Utils.isUrl string Utils.createFullUrl string else @@ -169,6 +213,61 @@ Utils = delete obj[property] for property in properties obj + # Does string match any of these regexps? + matchesAnyRegexp: (regexps, string) -> + for re in regexps + return true if re.test string + false + + # Calculate the length of the longest shared prefix of a list of strings. + longestCommonPrefix: (strings) -> + return 0 unless 0 < strings.length + strings.sort (a,b) -> a.length - b.length + [ shortest, strings... ] = strings + for ch, index in shortest.split "" + for str in strings + return index if ch != str[index] + return shortest.length + + # Convenience wrapper for setTimeout (with the arguments around the other way). + setTimeout: (ms, func) -> setTimeout func, ms + + # Like Nodejs's nextTick. + nextTick: (func) -> @setTimeout 0, func + +# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the +# search-engine configuration is unchanged. +SearchEngines = + previousSearchEngines: null + searchEngines: null + + refresh: (searchEngines) -> + unless @previousSearchEngines? and searchEngines == @previousSearchEngines + @previousSearchEngines = searchEngines + @searchEngines = new AsyncDataFetcher (callback) -> + engines = {} + for line in searchEngines.split "\n" + line = line.trim() + continue if /^[#"]/.test line + tokens = line.split /\s+/ + continue unless 2 <= tokens.length + keyword = tokens[0].split(":")[0] + searchUrl = tokens[1] + description = tokens[2..].join(" ") || "search (#{keyword})" + continue unless Utils.hasFullUrlPrefix searchUrl + engines[keyword] = { keyword, searchUrl, description } + + callback engines + + # Use the parsed search-engine configuration, possibly asynchronously. + use: (callback) -> + @searchEngines.use callback + + # Both set (refresh) the search-engine configuration and use it at the same time. + refreshAndUse: (searchEngines, callback) -> + @refresh searchEngines + @use callback + # This creates a new function out of an existing function, where the new function takes fewer arguments. This # allows us to pass around functions instead of functions + a partial list of arguments. Function::curry = -> @@ -179,6 +278,8 @@ Function::curry = -> Array.copy = (array) -> Array.prototype.slice.call(array, 0) String::startsWith = (str) -> @indexOf(str) == 0 +String::ltrim = -> @replace /^\s+/, "" +String::rtrim = -> @replace /\s+$/, "" globalRoot = window ? global globalRoot.extend = (hash1, hash2) -> @@ -186,5 +287,84 @@ globalRoot.extend = (hash1, hash2) -> hash1[key] = hash2[key] hash1 +# A simple cache. Entries used within two expiry periods are retained, otherwise they are discarded. +# At most 2 * @entries entries are retained. +class SimpleCache + # expiry: expiry time in milliseconds (default, one hour) + # entries: maximum number of entries in @cache (there may be up to this many entries in @previous, too) + constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) -> + @cache = {} + @previous = {} + @lastRotation = new Date() + + has: (key) -> + @rotate() + (key of @cache) or key of @previous + + # Set value, and return that value. If value is null, then delete key. + set: (key, value = null) -> + @rotate() + delete @previous[key] + if value? + @cache[key] = value + else + delete @cache[key] + null + + get: (key) -> + @rotate() + if key of @cache + @cache[key] + else if key of @previous + @cache[key] = @previous[key] + delete @previous[key] + @cache[key] + else + null + + rotate: (force = false) -> + if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation + @lastRotation = new Date() + @previous = @cache + @cache = {} + + clear: -> + @rotate true + @rotate true + +# This is a simple class for the common case where we want to use some data value which may be immediately +# available, or for which we may have to wait. It implements a use-immediately-or-wait queue, and calls the +# fetch function to fetch the data asynchronously. +class AsyncDataFetcher + constructor: (fetch) -> + @data = null + @queue = [] + Utils.nextTick => + fetch (@data) => + callback @data for callback in @queue + @queue = null + + use: (callback) -> + if @data? then callback @data else @queue.push callback + +# This takes a list of jobs (functions) and runs them, asynchronously. Functions queued with @onReady() are +# run once all of the jobs have completed. +class JobRunner + constructor: (@jobs) -> + @fetcher = new AsyncDataFetcher (callback) => + for job in @jobs + do (job) => + Utils.nextTick => + job => + @jobs = @jobs.filter (j) -> j != job + callback true if @jobs.length == 0 + + onReady: (callback) -> + @fetcher.use callback + root = exports ? window root.Utils = Utils +root.SearchEngines = SearchEngines +root.SimpleCache = SimpleCache +root.AsyncDataFetcher = AsyncDataFetcher +root.JobRunner = JobRunner |
