_ = underscore = require('underscore')
exports.RUNNING_IN_NODE = process?.title == 'node'
{required, defaults, types} = require('./opts')
exports.required = required; exports.defaults = defaults; exports.types = types
exports.startswith = (s, x) ->
if typeof(s) != 'string'
return false
if typeof(x) == "string"
return s?.indexOf(x) == 0
else
for v in x
if s?.indexOf(v) == 0
return true
return false
exports.endswith = (s, t) ->
if not s? or not t?
return false
return s.slice(s.length - t.length) == t
exports.merge = (dest, objs...) ->
for obj in objs
for k, v of obj
dest[k] = v
dest
exports.merge_copy = (objs...) ->
return exports.merge({}, objs...)
exports.random_choice = (array) -> array[Math.floor(Math.random() * array.length)]
exports.random_choice_from_obj = (obj) ->
k = exports.random_choice(exports.keys(obj))
return [k, obj[k]]
exports.randint = (lower, upper) ->
if lower > upper
throw new Error("randint: lower is larger than upper")
Math.floor(Math.random()*(upper - lower + 1)) + lower
exports.split = (s) ->
r = s.match(/\S+/g)
if r
return r
else
return []
exports.search_split = (search) ->
terms = []
search = search.split('"')
length = search.length
for element, i in search
element = element.trim()
if element.length != 0
if i % 2 == 0 or (i == length - 1 and length % 2 == 0)
terms.push(element.split(" ")...)
else
terms.push(element)
return terms
exports.search_match = (s, v) ->
for x in v
if s.indexOf(x) == -1
return false
return true
exports.contains = (word, sub) ->
return word.indexOf(sub) isnt -1
exports.count = (str, strsearch) ->
index = -1
count = -1
loop
index = str.indexOf(strsearch, index + 1)
count++
break if index is -1
return count
exports.min_object = (target, upper_bounds) ->
if not target?
target = {}
for prop, val of upper_bounds
target[prop] = if target.hasOwnProperty(prop) then target[prop] = Math.min(target[prop], upper_bounds[prop]) else upper_bounds[prop]
exports.mswalltime = (t) ->
if t?
return (new Date()).getTime() - t
else
return (new Date()).getTime()
exports.walltime = (t) ->
if t?
return exports.mswalltime()/1000.0 - t
else
return exports.mswalltime()/1000.0
exports.uuid = ->
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
r = Math.random() * 16 | 0
v = if c == 'x' then r else r & 0x3 | 0x8
v.toString 16
exports.is_valid_uuid_string = (uuid) ->
return typeof(uuid) == "string" and uuid.length == 36 and /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/i.test(uuid)
exports.assert_uuid = (uuid) =>
if not exports.is_valid_uuid_string(uuid)
throw Error("invalid uuid='#{uuid}'")
return
exports.is_valid_sha1_string = (s) ->
return typeof(s) == 'string' and s.length == 40 and /[a-fA-F0-9]{40}/i.test(s)
sha1 = require('sha1')
exports.uuidsha1 = (data) ->
s = sha1(data)
i = -1
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) ->
i += 1
switch c
when 'x'
return s[i]
when 'y'
return ((parseInt('0x'+s[i],16)&0x3)|0x8).toString(16)
)
zipcode = new RegExp("^\\d{5}(-\\d{4})?$")
exports.is_valid_zipcode = (zip) -> zipcode.test(zip)
exports.times_per_second = (f, max_time=5, max_loops=1000) ->
t = exports.walltime()
i = 0
tm = 0
while true
f()
tm = exports.walltime() - t
i += 1
if tm >= max_time or i >= max_loops
break
return Math.ceil(i/tm)
exports.to_json = JSON.stringify
SOCKET_DATE_KEY = 'DateEpochMS'
socket_date_replacer = (key, value) ->
if this[key] instanceof Date
date = this[key]
return {"#{SOCKET_DATE_KEY}":date - 0}
else
return value
exports.to_json_socket = (x) ->
JSON.stringify(x, socket_date_replacer)
socket_date_parser = (key, value) ->
if value?[SOCKET_DATE_KEY]?
return new Date(value[SOCKET_DATE_KEY])
else
return value
exports.from_json_socket = (x) ->
try
JSON.parse(x, socket_date_parser)
catch err
console.debug("from_json: error parsing #{x} (=#{exports.to_json(x)}) from JSON")
throw err
exports.to_safe_str = (x) ->
obj = {}
for key, value of x
sanitize = false
if key.indexOf("pass") != -1
sanitize = true
else if typeof(value)=='string' and value.slice(0,7) == "sha512$"
sanitize = true
if sanitize
obj[key] = '(unsafe)'
else
if typeof(value) == "object"
value = "[object]"
else if typeof(value) == "string"
value = exports.trunc(value,250)
obj[key] = value
x = exports.to_json(obj)
reISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/
exports.date_parser = date_parser = (k, v) ->
if typeof(v) == 'string' and v.length >= 20 and reISO.exec(v)
return ISO_to_Date(v)
else
return v
exports.ISO_to_Date = ISO_to_Date = (s) ->
if s.indexOf('Z') == -1
s += 'Z'
return new Date(s)
exports.from_json = (x) ->
try
JSON.parse(x, date_parser)
catch err
console.debug("from_json: error parsing #{x} (=#{exports.to_json(x)}) from JSON")
throw err
exports.fix_json_dates = fix_json_dates = (obj, date_keys) ->
if not date_keys?
return obj
if exports.is_object(obj)
for k, v of obj
if typeof(v) == 'object'
fix_json_dates(v, date_keys)
else if typeof(v) == 'string' and v.length >= 20 and reISO.exec(v) and (date_keys == 'all' or k in date_keys)
obj[k] = new Date(v)
else if exports.is_array(obj)
for i, x of obj
obj[i] = fix_json_dates(x, date_keys)
else if typeof(obj) == 'string' and obj.length >= 20 and reISO.exec(obj) and date_keys == 'all'
return new Date(obj)
return obj
exports.to_iso = (d) -> (new Date(d - d.getTimezoneOffset()*60*1000)).toISOString().slice(0,-5)
exports.to_iso_path = (d) -> exports.to_iso(d).replace('T','-').replace(/:/g,'')
exports.is_empty_object = (obj) -> Object.keys(obj).length == 0
exports.len = (obj) ->
if not obj?
return 0
a = obj.length
if a?
return a
underscore.keys(obj).length
exports.keys = underscore.keys
exports.values = underscore.values
exports.dict = (obj) ->
x = {}
for a in obj
if a.length != 2
throw new Error("ValueError: unexpected length of tuple")
x[a[0]] = a[1]
return x
exports.remove = (obj, val) ->
for i in [0...obj.length]
if obj[i] == val
obj.splice(i, 1)
return
throw new Error("ValueError -- item not in array")
exports.pairs_to_obj = (v) ->
o = {}
for x in v
o[x[0]] = x[1]
return o
exports.obj_to_pairs = (obj) -> ([x,y] for x,y of obj)
exports.substring_count = (string, subString, allowOverlapping) ->
string += ""
subString += ""
return string.length + 1 if subString.length <= 0
n = 0
pos = 0
step = (if (allowOverlapping) then (1) else (subString.length))
loop
pos = string.indexOf(subString, pos)
if pos >= 0
n++
pos += step
else
break
return n
exports.max = (array) -> (array.reduce((a,b) -> Math.max(a, b)))
exports.min = (array) -> (array.reduce((a,b) -> Math.min(a, b)))
filename_extension_re = /(?:\.([^.]+))?$/
exports.filename_extension = (filename) ->
filename = exports.path_split(filename).tail
return filename_extension_re.exec(filename)[1] ? ''
exports.filename_extension_notilde = (filename) ->
ext = exports.filename_extension(filename)
while ext and ext[ext.length-1] == '~'
ext = ext.slice(0, ext.length-1)
return ext
exports.separate_file_extension = (name) ->
ext = exports.filename_extension(name)
if ext isnt ''
name = name[0...name.length - ext.length - 1]
return {name: name, ext: ext}
exports.change_filename_extension = (name, new_ext) ->
{name, ext} = exports.separate_file_extension(name)
return "#{name}.#{new_ext}"
exports.copy = (obj) ->
if not obj? or typeof(obj) isnt 'object'
return obj
if exports.is_array(obj)
return obj[..]
r = {}
for x, y of obj
r[x] = y
return r
exports.copy_without = (obj, without) ->
if typeof(without) == 'string'
without = [without]
r = {}
for x, y of obj
if x not in without
r[x] = y
return r
exports.copy_with = (obj, w) ->
if typeof(w) == 'string'
w = [w]
r = {}
for x, y of obj
if x in w
r[x] = y
return r
exports.deep_copy = (obj) ->
if not obj? or typeof obj isnt 'object'
return obj
if obj instanceof Date
return new Date(obj.getTime())
if obj instanceof RegExp
flags = ''
flags += 'g' if obj.global?
flags += 'i' if obj.ignoreCase?
flags += 'm' if obj.multiline?
flags += 'y' if obj.sticky?
return new RegExp(obj.source, flags)
try
newInstance = new obj.constructor()
catch
newInstance = {}
for key, val of obj
newInstance[key] = exports.deep_copy(val)
return newInstance
exports.path_split = (path) ->
v = path.split('/')
return {head:v.slice(0,-1).join('/'), tail:v[v.length-1]}
exports.normalized_path_join = (parts...) ->
sep = '/'
replace = new RegExp(sep+'{1,}', 'g')
s = ("#{x}" for x in parts when x? and "#{x}".length > 0).join(sep).replace(replace, sep)
return s
exports.path_to_file = (path, file) ->
if path == ''
return file
return path + '/' + file
exports.meta_file = (path, ext) ->
if not path?
return
p = exports.path_split(path)
path = p.head
if p.head != ''
path += '/'
return path + "." + p.tail + ".sage-" + ext
exports.original_path = (path) ->
s = exports.path_split(path)
if s.tail[0] != '.' or s.tail.indexOf('.sage-') == -1
return path
ext = exports.filename_extension(s.tail)
x = s.tail.slice((if s.tail[0] == '.' then 1 else 0), s.tail.length - (ext.length+1))
if s.head != ''
x = s.head + '/' + x
return x
ELLIPSES = "β¦"
exports.trunc = (s, max_length=1024) ->
if not s?
return s
if typeof(s) != 'string'
s = "#{s}"
if s.length > max_length
if max_length < 1
throw new Error("ValueError: max_length must be >= 1")
return s.slice(0,max_length-1) + ELLIPSES
else
return s
exports.trunc_middle = (s, max_length=1024) ->
if not s?
return s
if typeof(s) != 'string'
s = "#{s}"
if s.length <= max_length
return s
if max_length < 1
throw new Error("ValueError: max_length must be >= 1")
n = Math.floor(max_length/2)
return s.slice(0, n - 1 + (if max_length%2 then 1 else 0)) + ELLIPSES + s.slice(s.length-n)
exports.trunc_left = (s, max_length=1024) ->
if not s?
return s
if typeof(s) != 'string'
s = "#{s}"
if s.length > max_length
if max_length < 1
throw new Error("ValueError: max_length must be >= 1")
return ELLIPSES + s.slice(s.length-max_length+1)
else
return s
exports.pad_left = (s, n) ->
if not typeof(s) == 'string'
s = "#{s}"
for i in [s.length...n]
s = ' ' + s
return s
exports.pad_right = (s, n) ->
if not typeof(s) == 'string'
s = "#{s}"
for i in [s.length...n]
s += ' '
return s
exports.plural = (number, singular, plural="#{singular}s") ->
if singular in ['GB', 'MB']
return singular
if number == 1 then singular else plural
exports.git_author = (first_name, last_name, email_address) -> "#{first_name} #{last_name} <#{email_address}>"
reValidEmail = (() ->
sQtext = "[^\\x0d\\x22\\x5c\\x80-\\xff]"
sDtext = "[^\\x0d\\x5b-\\x5d\\x80-\\xff]"
sAtom = "[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+"
sQuotedPair = "\\x5c[\\x00-\\x7f]"
sDomainLiteral = "\\x5b(" + sDtext + "|" + sQuotedPair + ")*\\x5d"
sQuotedString = "\\x22(" + sQtext + "|" + sQuotedPair + ")*\\x22"
sDomain_ref = sAtom
sSubDomain = "(" + sDomain_ref + "|" + sDomainLiteral + ")"
sWord = "(" + sAtom + "|" + sQuotedString + ")"
sDomain = sSubDomain + "(\\x2e" + sSubDomain + ")*"
sLocalPart = sWord + "(\\x2e" + sWord + ")*"
sAddrSpec = sLocalPart + "\\x40" + sDomain
sValidEmail = "^" + sAddrSpec + "$"
return new RegExp(sValidEmail)
)()
exports.is_valid_email_address = (email) ->
if reValidEmail.test(email)
return true
else
return false
exports.canonicalize_email_address = (email_address) ->
if typeof(email_address) != 'string'
email_address = JSON.stringify(email_address)
i = email_address.indexOf('+')
if i != -1
j = email_address.indexOf('@')
if j != -1
email_address = email_address.slice(0,i) + email_address.slice(j)
return email_address.toLowerCase()
exports.lower_email_address = (email_address) ->
if not email_address?
return
if typeof(email_address) != 'string'
email_address = JSON.stringify(email_address)
return email_address.toLowerCase()
exports.parse_user_search = (query) ->
queries = (q.trim().toLowerCase() for q in query.split(/,|;/))
r = {string_queries:[], email_queries:[]}
email_re = /<(.*)>/
for x in queries
if x
if x.indexOf('@') == -1
r.string_queries.push(x.split(/\s+/g))
else
for a in exports.split(x)
if a[0] == '<'
match = email_re.exec(a)
a = match?[1] ? a
if exports.is_valid_email_address(a)
r.email_queries.push(a)
return r
exports.delete_trailing_whitespace = (s) ->
return s.replace(/[^\S\n]+$/gm, "")
exports.assert = (condition, mesg) ->
if not condition
if typeof mesg == 'string'
throw new Error(mesg)
throw mesg
exports.retry_until_success = (opts) ->
opts = exports.defaults opts,
f : exports.required
start_delay : 100
max_delay : 20000
factor : 1.4
max_tries : undefined
max_time : undefined
log : undefined
warn : undefined
name : ''
cb : undefined
delta = opts.start_delay
tries = 0
if opts.max_time?
start_time = new Date()
g = () ->
tries += 1
if opts.log?
if opts.max_tries?
opts.log("retry_until_success(#{opts.name}) -- try #{tries}/#{opts.max_tries}")
if opts.max_time?
opts.log("retry_until_success(#{opts.name}) -- try #{tries} (started #{new Date() - start_time}ms ago; will stop before #{opts.max_time}ms max time)")
if not opts.max_tries? and not opts.max_time?
opts.log("retry_until_success(#{opts.name}) -- try #{tries}")
opts.f (err)->
if err
if err == "not_public"
opts.cb?("not_public")
return
if err and opts.warn?
opts.warn("retry_until_success(#{opts.name}) -- err=#{JSON.stringify(err)}")
if opts.log?
opts.log("retry_until_success(#{opts.name}) -- err=#{JSON.stringify(err)}")
if opts.max_tries? and opts.max_tries <= tries
opts.cb?("maximum tries (=#{opts.max_tries}) exceeded - last error #{JSON.stringify(err)}")
return
delta = Math.min(opts.max_delay, opts.factor * delta)
if opts.max_time? and (new Date() - start_time) + delta > opts.max_time
opts.cb?("maximum time (=#{opts.max_time}ms) exceeded - last error #{JSON.stringify(err)}")
return
setTimeout(g, delta)
else
if opts.log?
opts.log("retry_until_success(#{opts.name}) -- success")
opts.cb?()
g()
exports.retry_until_success_wrapper = (opts) ->
_X = new RetryUntilSuccess(opts)
return (cb) -> _X.call(cb)
class RetryUntilSuccess
constructor: (opts) ->
@opts = exports.defaults opts,
f : exports.defaults.required
start_delay : 100
max_delay : 20000
exp_factor : 1.4
max_tries : undefined
max_time : undefined
min_interval : 100
logname : undefined
verbose : false
if @opts.min_interval?
if @opts.start_delay < @opts.min_interval
@opts.start_delay = @opts.min_interval
@f = @opts.f
call: (cb, retry_delay) =>
if @opts.logname?
console.debug("#{@opts.logname}(... #{retry_delay})")
if not @_cb_stack?
@_cb_stack = []
if cb?
@_cb_stack.push(cb)
if @_calling
return
@_calling = true
if not retry_delay?
@attempts = 0
if @opts.logname?
console.debug("actually calling -- #{@opts.logname}(... #{retry_delay})")
if @opts.max_time?
start_time = new Date()
g = () =>
if @opts.min_interval?
@_last_call_time = exports.mswalltime()
@f (err) =>
@attempts += 1
@_calling = false
if err
if @opts.verbose
console.debug("#{@opts.logname}: error=#{err}")
if @opts.max_tries? and @attempts >= @opts.max_tries
while @_cb_stack.length > 0
@_cb_stack.pop()(err)
return
if not retry_delay?
retry_delay = @opts.start_delay
else
retry_delay = Math.min(@opts.max_delay, @opts.exp_factor*retry_delay)
if @opts.max_time? and (new Date() - start_time) + retry_delay > @opts.max_time
err = "maximum time (=#{@opts.max_time}ms) exceeded - last error #{err}"
while @_cb_stack.length > 0
@_cb_stack.pop()(err)
return
f = () =>
@call(undefined, retry_delay)
setTimeout(f, retry_delay)
else
while @_cb_stack.length > 0
@_cb_stack.pop()()
if not @_last_call_time? or not @opts.min_interval?
g()
else
w = exports.mswalltime(@_last_call_time)
if w < @opts.min_interval
setTimeout(g, @opts.min_interval - w)
else
g()
exports.eval_until_defined = (opts) ->
opts = exports.defaults opts,
code : exports.required
start_delay : 100
max_time : 10000
exp_factor : 1.4
cb : exports.required
delay = undefined
total = 0
f = () ->
result = eval(opts.code)
if result?
opts.cb(false, result)
else
if not delay?
delay = opts.start_delay
else
delay *= opts.exp_factor
total += delay
if total > opts.max_time
opts.cb("failed to eval code within #{opts.max_time}")
else
setTimeout(f, delay)
f()
exports.async_debounce = (opts) ->
opts = defaults opts,
f : required
interval : 1500
state : required
cb : undefined
{f, interval, state, cb} = opts
call_again = ->
n = interval + 1 - (new Date() - state.last)
state.timer = setTimeout((=>delete state.timer; exports.async_debounce(f:f, interval:interval, state:state)), n)
if state.last? and (new Date() - state.last) <= interval
state.next_callbacks ?= []
if cb?
state.next_callbacks.push(cb)
if not state.timer?
call_again()
return
state.last = new Date()
callbacks = exports.copy(state.next_callbacks ? [])
if cb?
callbacks.push(cb)
delete state.next_callbacks
f (err) =>
for cb in callbacks
cb?(err)
callbacks = []
if state.next_callbacks? and not state.timer?
call_again()
class exports.StringCharMapping
constructor: (opts={}) ->
opts = exports.defaults opts,
to_char : undefined
to_string : undefined
@_to_char = {}
@_to_string = {}
@_next_char = 'A'
if opts.to_string?
for ch, st of opts.to_string
@_to_string[ch] = st
@_to_char[st] = ch
if opts.to_char?
for st,ch of opts.to_char
@_to_string[ch] = st
@_to_char[st] = ch
@_find_next_char()
_find_next_char: () =>
loop
@_next_char = String.fromCharCode(@_next_char.charCodeAt(0) + 1)
break if not @_to_string[@_next_char]?
to_string: (strings) =>
t = ''
for s in strings
a = @_to_char[s]
if a?
t += a
else
t += @_next_char
@_to_char[s] = @_next_char
@_to_string[@_next_char] = s
@_find_next_char()
return t
to_array: (string) =>
return (@_to_string[s] for s in string)
exports.uniquify_string = (s) ->
seen_already = {}
t = ''
for c in s
if not seen_already[c]?
t += c
seen_already[c] = true
return t
exports.PROJECT_GROUPS = ['owner', 'collaborator', 'viewer', 'invited_collaborator', 'invited_viewer']
exports.make_valid_name = (s) ->
return s.replace(/\W/g, '_').toLowerCase()
exports.parse_bup_timestamp = (s) ->
v = [s.slice(0,4), s.slice(5,7), s.slice(8,10), s.slice(11,13), s.slice(13,15), s.slice(15,17), '0']
return new Date("#{v[1]}/#{v[2]}/#{v[0]} #{v[3]}:#{v[4]}:#{v[5]} UTC")
exports.matches = (s, words) ->
for word in words
if s.indexOf(word) == -1
return false
return true
exports.hash_string = (s) ->
hash = 0
i = undefined
chr = undefined
len = undefined
return hash if s.length is 0
i = 0
len = s.length
while i < len
chr = s.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0
i++
return hash
exports.parse_hashtags = (t) ->
v = []
if not t?
return v
base = 0
while true
i = t.indexOf('#')
if i == -1 or i == t.length-1
return v
base += i+1
if t[i+1] == '#' or not (i == 0 or t[i-1].match(/\s/))
t = t.slice(i+1)
continue
t = t.slice(i+1)
i = t.match(/\s|[^A-Za-z0-9_\-]/)
if i
i = i.index
else
i = -1
if i == 0
base += i+1
t = t.slice(i+1)
else
if i == -1
v.push([base-1, base+t.length])
return v
else
v.push([base-1, base+i])
base += i+1
t = t.slice(i+1)
mathjax_environments = ['align', 'align*', 'alignat', 'alignat*', 'aligned', 'alignedat', 'array', \
'Bmatrix', 'bmatrix', 'cases', 'CD', 'eqnarray', 'eqnarray*', 'equation', 'equation*', \
'gather', 'gather*', 'gathered', 'matrix', 'multline', 'multline*', 'pmatrix', 'smallmatrix', \
'split', 'subarray', 'Vmatrix', 'vmatrix']
mathjax_delim = [['$$','$$'], ['\\(','\\)'], ['\\[','\\]']]
for env in mathjax_environments
mathjax_delim.push(["\\begin{#{env}}", "\\end{#{env}}"])
mathjax_delim.push(['$', '$'])
exports.parse_mathjax = (t) ->
v = []
if not t?
return v
i = 0
while i < t.length
if t.slice(i, i+2) == '\\$'
i += 2
continue
for d in mathjax_delim
contains_linebreak = false
if t.slice(i, i + d[0].length) == d[0]
j = i+1
while j < t.length and t.slice(j, j + d[1].length) != d[1]
next_char = t.slice(j, j+1)
if next_char == "\n"
contains_linebreak = true
if d[0] == "$"
break
prev_char = t.slice(j-1, j)
if next_char == "`" and prev_char != '\\'
j -= 1
break
j += 1
j += d[1].length
at_end_of_string = j > t.length
if !(d[0] == "$" and (contains_linebreak or at_end_of_string))
v.push([i,j])
i = j
break
i += 1
return v
exports.mathjax_escape = (html) ->
return html.replace(/&(?!#?\w+;)/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'")
exports.path_is_in_public_paths = (path, paths) ->
return exports.containing_public_path(path, paths)?
exports.containing_public_path = (path, paths) ->
if paths.length == 0
return
if not path?
return
if path.indexOf('../') != -1
return
for p in paths
if p == ""
return ""
if path == p
return p
if path.slice(0,p.length+1) == p + '/'
return p
if exports.filename_extension(path) == "zip"
return exports.containing_public_path(path.slice(0,path.length-4), paths)
return undefined
exports.encode_path = (path) ->
path = encodeURI(path)
return path.replace(/#/g,'%23').replace(/\?/g,'%3F')
exports.call_lock = (opts) ->
opts = exports.defaults opts,
obj : exports.required
timeout_s : 30
obj = opts.obj
obj._call_lock = () ->
obj.__call_lock = true
obj.__call_lock_timeout = () ->
obj.__call_lock = false
delete obj.__call_lock_timeout
setTimeout(obj.__call_lock_timeout, opts.timeout_s * 1000)
obj._call_unlock = () ->
if obj.__call_lock_timeout?
clearTimeout(obj.__call_lock_timeout)
delete obj.__call_lock_timeout
obj.__call_lock = false
obj._call_with_lock = (f, cb) ->
if obj.__call_lock
cb?("error -- hit call_lock")
return
obj._call_lock()
f (args...) ->
obj._call_unlock()
cb?(args...)
exports.cmp = (a,b) ->
if a < b
return -1
else if a > b
return 1
return 0
exports.cmp_array = (a,b) ->
for i in [0...Math.max(a.length, b.length)]
c = exports.cmp(a[i],b[i])
if c
return c
return 0
exports.cmp_Date = (a,b) ->
if not a?
return -1
if not b?
return 1
if a < b
return -1
else if a > b
return 1
return 0
exports.timestamp_cmp = (a,b,field='timestamp') ->
return -exports.cmp_Date(a[field], b[field])
timestamp_cmp0 = (a,b,field='timestamp') ->
return exports.cmp_Date(a[field], b[field])
exports.field_cmp = (field) ->
return (a, b) -> exports.cmp(a[field], b[field])
class ActivityLog
constructor: (opts) ->
opts = exports.defaults opts,
events : undefined
account_id : exports.required
notifications : {}
@notifications = opts.notifications
@account_id = opts.account_id
if opts.events?
@process(opts.events)
obj: () =>
return {notifications:@notifications, account_id:@account_id}
path: (e) => "#{e.project_id}/#{e.path}"
process: (events) =>
by_path = {}
for e in events
key = @path(e)
events_with_path = by_path[key]
if not events_with_path?
events_with_path = by_path[key] = [e]
else
events_with_path.push(e)
for path, events_with_path of by_path
events_with_path.sort(timestamp_cmp0)
for event in events_with_path
@_process_event(event, path)
_process_event: (event, path) =>
if not path?
path = @path(event)
a = @notifications[path]
if not a?
@notifications[path] = a = {}
a.timestamp = event.timestamp
a.id = event.id
if event.seen_by? and event.seen_by.indexOf(@account_id) != -1
a.seen = event.timestamp
if event.read_by? and event.read_by.indexOf(@account_id) != -1
a.read = event.timestamp
if event.action?
who = a[event.action]
if not who?
who = a[event.action] = {}
who[event.account_id] = event.timestamp
exports.activity_log = (opts) -> new ActivityLog(opts)
exports.replace_all = (string, search, replace) ->
string.split(search).join(replace)
exports.remove_c_comments = (s) ->
while true
i = s.indexOf('/*')
if i == -1
return s
j = s.indexOf('*/')
if i >= j
return s
s = s.slice(0, i) + s.slice(j+2)
exports.date_to_snapshot_format = (d) ->
if not d?
d = 0
if typeof(d) == "number"
d = new Date(d)
s = d.toJSON()
s = s.replace('T','-').replace(/:/g, '')
i = s.lastIndexOf('.')
return s.slice(0,i)
exports.stripe_date = (d) ->
return new Date(d*1000).toLocaleDateString( 'lookup', { year: 'numeric', month: 'long', day: 'numeric' })
exports.to_money = (n) ->
return n.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
exports.stripe_amount = (units, currency) ->
if currency != 'usd'
throw Error("not-implemented currency #{currency}")
s = "$#{exports.to_money(units/100)}"
if s.slice(s.length-3) == '.00'
s = s.slice(0, s.length-3)
return s
exports.capitalize = (s) ->
if s?
return s.charAt(0).toUpperCase() + s.slice(1)
exports.is_array = is_array = (obj) ->
return Object.prototype.toString.call(obj) == "[object Array]"
exports.is_integer = Number.isInteger
if not exports.is_integer?
exports.is_integer = (n) -> typeof(n)=='number' and (n % 1) == 0
exports.is_string = (obj) ->
return typeof(obj) == 'string'
exports.is_object = is_object = (obj) ->
return Object.prototype.toString.call(obj) == "[object Object]"
exports.is_date = is_date = (obj) ->
return obj instanceof Date
exports.get_array_range = (arr, value1, value2) ->
index1 = arr.indexOf(value1)
index2 = arr.indexOf(value2)
if index1 > index2
[index1, index2] = [index2, index1]
return arr[index1..index2]
exports.milliseconds_ago = (ms) -> new Date(new Date() - ms)
exports.seconds_ago = (s) -> exports.milliseconds_ago(1000*s)
exports.minutes_ago = (m) -> exports.seconds_ago(60*m)
exports.hours_ago = (h) -> exports.minutes_ago(60*h)
exports.days_ago = (d) -> exports.hours_ago(24*d)
exports.weeks_ago = (w) -> exports.days_ago(7*w)
exports.months_ago = (m) -> exports.days_ago(30.5*m)
if window?
exports.server_time = () -> new Date(new Date() - parseFloat(exports.get_local_storage('clock_skew') ? 0))
exports.server_milliseconds_ago = (ms) -> new Date(new Date() - ms - parseFloat(exports.get_local_storage('clock_skew') ? 0))
exports.server_seconds_ago = (s) -> exports.server_milliseconds_ago(1000*s)
exports.server_minutes_ago = (m) -> exports.server_seconds_ago(60*m)
exports.server_hours_ago = (h) -> exports.server_minutes_ago(60*h)
exports.server_days_ago = (d) -> exports.server_hours_ago(24*d)
exports.server_weeks_ago = (w) -> exports.server_days_ago(7*w)
exports.server_months_ago = (m) -> exports.server_days_ago(30.5*m)
else
exports.server_time = -> new Date()
exports.server_milliseconds_ago = exports.milliseconds_ago
exports.server_seconds_ago = exports.seconds_ago
exports.server_minutes_ago = exports.minutes_ago
exports.server_hours_ago = exports.hours_ago
exports.server_days_ago = exports.days_ago
exports.server_weeks_ago = exports.weeks_ago
exports.server_months_ago = exports.months_ago
exports.milliseconds_before = (ms, tm) -> new Date((tm ? (new Date())) - ms)
exports.seconds_before = (s, tm) -> exports.milliseconds_before(1000*s, tm)
exports.minutes_before = (m, tm) -> exports.seconds_before(60*m, tm)
exports.hours_before = (h, tm) -> exports.minutes_before(60*h, tm)
exports.days_before = (d, tm) -> exports.hours_before(24*d, tm)
exports.weeks_before = (d, tm) -> exports.days_before(7*d, tm)
exports.months_before = (d, tm) -> exports.days_before(30.5*d, tm)
exports.expire_time = (s) ->
if s then new Date((new Date() - 0) + s*1000)
exports.YEAR = new Date().getFullYear()
exports.round1 = round1 = (num) ->
Math.round(num * 10) / 10
exports.round2 = round2 = (num) ->
Math.round((num + 0.00001) * 100) / 100
exports.seconds2hms = seconds2hms = (secs) ->
s = round2(secs % 60)
m = Math.floor(secs / 60) % 60
h = Math.floor(secs / 60 / 60)
if h == 0 and m == 0
return "#{s}s"
if h > 0
return "#{h}h#{m}m#{s}s"
if m > 0
return "#{m}m#{s}s"
exports.parse_number_input = (input, round_number=true, allow_negative=false) ->
input = (input + "").split('/')
if input.length != 1 and input.length != 2
return undefined
if input.length == 2
val = parseFloat(input[0]) / parseFloat(input[1])
if input.length == 1
if isNaN(input) or "#{input}".trim() is ''
return undefined
val = parseFloat(input)
if round_number
val = round2(val)
if isNaN(val) or val == Infinity or (val < 0 and not allow_negative)
return undefined
return val
exports.range = (n, m) ->
if not m?
return [0...n]
else
return [n...m]
exports.map_sum = (a, b) ->
if not a?
return b
if not b?
return a
c = {}
for k, v of a
c[k] = v + (b[k] ? 0)
for k, v of b
c[k] ?= v
return c
exports.map_diff = (a, b) ->
if not b?
return a
if not a?
c = {}
for k,v of b
c[k] = -v
return c
c = {}
for k, v of a
c[k] = v - (b[k] ? 0)
for k, v of b
c[k] ?= -v
return c
exports.map_limit = (a, b) ->
c = {}
if typeof b == 'number'
for k, v of a
c[k] = Math.min(v, b)
else
for k, v of a
c[k] = Math.min(v, (b[k] ? Number.MAX_VALUE))
return c
exports.sum = (arr, start=0) -> underscore.reduce(arr, ((a, b) -> a+b), start)
exports.apply_function_to_map_values = apply_function_to_map_values = (map, f) ->
for k, v of map
map[k] = f(v)
return map
exports.coerce_codomain_to_numbers = (map) ->
apply_function_to_map_values map, (x)->
if typeof(x) == 'boolean'
if x then 1 else 0
else
parseFloat(x)
exports.is_zero_map = (map) ->
if not map?
return true
for k,v of map
if v
return false
return true
exports.map_without_undefined = map_without_undefined = (map) ->
if is_array(map)
return map
if not map?
return
new_map = {}
for k, v of map
if not v?
continue
else
new_map[k] = if is_object(v) then map_without_undefined(v) else v
return new_map
exports.map_mutate_out_undefined = (map) ->
for k, v of map
if not v?
delete map[k]
exports.should_open_in_foreground = (e) ->
if e.constructor.name == 'SyntheticMouseEvent'
e = e.nativeEvent
return not (e.which == 2 or e.metaKey or e.altKey or e.ctrlKey)
exports.enumerate = (v) ->
i = 0
w = []
for x in v
w.push([i,x])
i += 1
return w
exports.escapeRegExp = escapeRegExp = (str) ->
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
smileys_definition = [
[':-)', "π"],
[':-(', "π"],
['<3', "β‘", null, '\\b'],
[':shrug:', "Β―\\\\_(γ)_/Β―"],
['o_o', "Χ‘ΦΌ_\Χ‘ΦΌ", '\\b', '\\b'],
[':-p', "π", null, '\\b'],
['>_<', "π"],
['^^', "π", '^', '\S'],
['^^ ', "π "],
[' ^^', " π"],
[';-)', "π"],
['-_-', "π"],
[':-\\', "π"],
[':omg:', "π±"]
]
smileys = []
for smiley in smileys_definition
s = escapeRegExp(smiley[0])
if smiley[2]?
s = smiley[2] + s
if smiley[3]?
s = s + smiley[3]
smileys.push([RegExp(s, 'g'), smiley[1]])
exports.smiley = (opts) ->
opts = exports.defaults opts,
s : exports.required
wrap : undefined
s = opts.s.replace(/>/g, '>').replace(/</g, '<')
for subs in smileys
repl = subs[1]
if opts.wrap
repl = opts.wrap[0] + repl + opts.wrap[1]
s = s.replace(subs[0], repl)
return s
_ = underscore
exports.smiley_strings = () ->
return _.filter(_.map(smileys_definition, _.first), (x) -> ! _.contains(['^^ ', ' ^^'], x))
exports.to_human_list = (arr) ->
arr = _.map(arr, (x) -> x.toString())
if arr.length > 1
return arr[...-1].join(", ") + " and " + arr[-1..]
else if arr.length == 1
return arr[0].toString()
else
return ""
exports.emoticons = exports.to_human_list(exports.smiley_strings())
exports.history_path = (path) ->
p = exports.path_split(path)
return if p.head then "#{p.head}/.#{p.tail}.sage-history" else ".#{p.tail}.sage-history"
_done = (n, args...) ->
start_time = new Date()
f = (args...) ->
if n != 1
try
args = [JSON.stringify(args, null, n)]
catch
console.log("*** TOTALLY DONE! (#{(new Date() - start_time)/1000}s since start) ", args...)
if args.length > 0
f(args...)
else
return f
exports.done = (args...) -> _done(0, args...)
exports.done1 = (args...) -> _done(1, args...)
exports.done2 = (args...) -> _done(2, args...)
smc_logger_timestamp = smc_logger_timestamp_last = smc_start_time = new Date().getTime() / 1000.0
exports.get_start_time_ts = ->
return new Date(smc_start_time * 1000)
exports.get_uptime = ->
return seconds2hms((new Date().getTime() / 1000.0) - smc_start_time)
exports.log = () ->
smc_logger_timestamp = new Date().getTime() / 1000.0
t = seconds2hms(smc_logger_timestamp - smc_start_time)
dt = seconds2hms(smc_logger_timestamp - smc_logger_timestamp_last)
[msg, args...] = Array.prototype.slice.call(arguments)
prompt = "[#{t} Ξ #{dt}]"
if _.isString(msg)
prompt = "#{prompt} #{msg}"
console.log_original(prompt, args...)
else
console.log_original(prompt, msg, args...)
smc_logger_timestamp_last = smc_logger_timestamp
exports.wrap_log = () ->
if not exports.RUNNING_IN_NODE and window?
window.console.log_original = window.console.log
window.console.log = exports.log
exports.this_fails = ->
return exports.op_to_function('noop')
exports.console_init_filename = (fn) ->
x = exports.path_split(fn)
x.tail = ".#{x.tail}.init"
if x.head == ''
return x.tail
return [x.head, x.tail].join("/")
exports.has_null_leaf = has_null_leaf = (obj) ->
for k, v of obj
if v == null or (typeof(v) == 'object' and has_null_leaf(v))
return true
return false
exports.peer_grading = (students, N=2) ->
if N <= 0
throw "Number of peer assigments must be at least 1"
if students.length <= N
throw "You need at least #{N + 1} students"
asmnt = {}
students.forEach((s) -> asmnt[s] = [])
s_random = underscore.shuffle(students)
L = students.length
for i in [0...L]
asmnt[s_random[i]] = (s_random[(i + idx) % L] for idx in [1..N])
for k, v of asmnt
asmnt[k] = underscore.sortBy(v, (s) -> students.indexOf(s))
return asmnt
exports.peer_grading_demo = (S = 10, N = 2) ->
peer_grading = exports.peer_grading
students = [0...S]
students = ("S-#{s}" for s in students)
result = peer_grading(students, N=N)
console.log("#{S} students graded by #{N} peers")
for k, v of result
console.log("#{k} ββ #{v}")
return result
exports.ticket_id_to_ticket_url = (tid) ->
return "https://sagemathcloud.zendesk.com/requests/#{tid}"
exports.is_only_downloadable = (string) ->
string.indexOf('://') != -1 or exports.startswith(string, '[email protected]')
exports.transform_get_url = (url) ->
URL_TRANSFORMS =
'http://trac.sagemath.org/attachment/ticket/' :'http://trac.sagemath.org/raw-attachment/ticket/'
'http://nbviewer.jupyter.org/url/' :'http://'
'http://nbviewer.jupyter.org/urls/' :'https://'
if exports.startswith(url, "https://github.com/")
if url.indexOf('/blob/') != -1
url = url.replace("https://github.com", "https://raw.githubusercontent.com").replace("/blob/","/")
else if url.split('://')[1]?.split('/').length == 3
url += '.git'
if exports.startswith(url, '[email protected]:')
command = 'git'
args = ['clone', url]
else if url.slice(url.length-4) == ".git"
command = 'git'
args = ['clone', url]
else
for a,b of URL_TRANSFORMS
url = url.replace(a,b)
if exports.startswith(url, 'http://nbviewer.jupyter.org/github/')
url = url.replace('http://nbviewer.jupyter.org/github/', 'https://raw.githubusercontent.com/')
url = url.replace("/blob/","/")
command = 'wget'
args = [url]
return {command:command, args:args}
exports.ensure_bound = (x, min, max) ->
return min if x < min
return max if x > max
return x
exports.path_to_tab = (name) ->
"editor-#{name}"
exports.tab_to_path = (name) ->
if name? and name.substring(0, 7) == "editor-"
name.substring(7)
exports.suggest_duplicate_filename = (name) ->
{name, ext} = exports.separate_file_extension(name)
idx_dash = name.lastIndexOf('-')
idx_under = name.lastIndexOf('_')
idx = exports.max([idx_dash, idx_under])
new_name = null
if idx > 0
[prfx, ending] = [name[...idx+1], name[idx+1...]]
num = parseInt(ending)
if not Number.isNaN(num)
new_name = "#{prfx}#{num+1}"
new_name ?= "#{name}-1"
if ext?.length > 0
new_name += ".#{ext}"
return new_name
exports.set_local_storage = (key, val) ->
try
localStorage[key] = val
catch e
console.warn("localStorage set error -- #{e}")
exports.get_local_storage = (key) ->
try
return localStorage[key]
catch e
console.warn("localStorage get error -- #{e}")
exports.delete_local_storage = (key) ->
try
delete localStorage[key]
catch e
console.warn("localStorage delete error -- #{e}")
exports.has_local_storage = () ->
try
TEST = '__smc_test__'
localStorage[TEST] = 'x'
delete localStorage[TEST]
return true
catch e
return false
exports.local_storage_length = () ->
try
return localStorage.length
catch e
return 0
exports.top_sort = (DAG, opts={omit_sources:false}) ->
{omit_sources} = opts
source_names = []
num_edges = 0
data = {}
for name, parents of DAG
data[name] ?= {}
node = data[name]
node.name = name
node.children ?= []
node.parent_set = {}
for parent_name in parents
node.parent_set[parent_name] = true
data[parent_name] ?= {}
data[parent_name].children ?= []
data[parent_name].children.push(node)
if parents.length == 0
source_names.push(name)
else
num_edges += parents.length
path = []
num_sources = source_names.length
while source_names.length > 0
curr_name = source_names.shift()
path.push(curr_name)
for child in data[curr_name].children
delete child.parent_set[curr_name]
num_edges -= 1
if exports.len(child.parent_set) == 0
source_names.push(child.name)
if num_sources == 0
throw new Error "No sources were detected"
if num_edges != 0
window?._DAG = DAG
throw new Error "Store has a cycle in its computed values"
if omit_sources
return path.slice(num_sources)
else
return path
exports.create_dependency_graph = (object) =>
DAG = {}
for name, written_func of object
DAG[name] = written_func.dependency_names ? []
return DAG
exports.bind_objects = (scope, arr_objects) ->
return underscore.map arr_objects, (object) =>
return underscore.mapObject object, (val) =>
if typeof val == 'function'
original_toString = val.toString()
bound_func = val.bind(scope)
bound_func.toString = () => original_toString
Object.assign(bound_func, val)
return bound_func
else
return val
exports.remove_whitespace = (s) ->
return s?.replace(/\s/g,'')
exports.is_whitespace = (s) ->
return s?.trim().length == 0
exports.lstrip = (s) ->
return s?.replace(/^\s*/g, "")
exports.rstrip = (s) ->
return s?.replace(/\s*$/g, "")
exports.operators = ['!=', '<>', '<=', '>=', '==', '<', '>', '=']
exports.op_to_function = (op) ->
switch op
when '=', '=='
return (a,b) -> a == b
when '!=', '<>'
return (a,b) -> a != b
when '<='
return (a,b) -> a <= b
when '>='
return (a,b) -> a >= b
when '<'
return (a,b) -> a < b
when '>'
return (a,b) -> a > b
else
throw Error("operator must be one of '#{JSON.stringify(exports.operators)}'")
exports.obj_key_subs = (obj, subs) ->
for k, v of obj
s = subs[k]
if s?
delete obj[k]
obj[s] = v
if typeof(v) == 'object'
exports.obj_key_subs(v, subs)
else if typeof(v) == 'string'
s = subs[v]
if s?
obj[k] = s
exports.sanitize_html_attributes = ($, node) ->
$.each node.attributes, ->
attrName = this.name
attrValue = this.value
if attrName?.indexOf('on') == 0 or attrValue?.indexOf('javascript:') == 0
$(node).removeAttr(attrName)