aboutsummaryrefslogtreecommitdiffstats
path: root/lib/utils.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils.coffee')
-rw-r--r--lib/utils.coffee152
1 files changed, 146 insertions, 6 deletions
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 64c87842..65e26b7a 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -26,16 +26,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 +107,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 +142,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 +206,29 @@ 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
+
+
# 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 +239,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 +248,83 @@ 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.SimpleCache = SimpleCache
+root.AsyncDataFetcher = AsyncDataFetcher
+root.JobRunner = JobRunner