Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39537
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2014 -- 2016, SageMath, Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
##########################################################################
23
#
24
# Misc. functions that are needed elsewhere.
25
#
26
##########################################################################
27
#
28
###############################################################################
29
# Copyright (C) 2016, Sagemath Inc.
30
# All rights reserved.
31
#
32
# Redistribution and use in source and binary forms, with or without
33
# modification, are permitted provided that the following conditions are met:
34
#
35
# 1. Redistributions of source code must retain the above copyright notice, this
36
# list of conditions and the following disclaimer.
37
# 2. Redistributions in binary form must reproduce the above copyright notice,
38
# this list of conditions and the following disclaimer in the documentation
39
# and/or other materials provided with the distribution.
40
#
41
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
42
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
43
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
45
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
46
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
47
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
48
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
49
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
50
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51
###############################################################################
52
53
54
_ = underscore = require('underscore')
55
56
exports.RUNNING_IN_NODE = process?.title == 'node'
57
58
{required, defaults, types} = require('./opts')
59
# We explicitly export these again for backwards compatibility
60
exports.required = required; exports.defaults = defaults; exports.types = types
61
62
# startswith(s, x) is true if s starts with the string x or any of the strings in x.
63
# It is false if s is not a string.
64
exports.startswith = (s, x) ->
65
if typeof(s) != 'string'
66
return false
67
if typeof(x) == "string"
68
return s?.indexOf(x) == 0
69
else
70
for v in x
71
if s?.indexOf(v) == 0
72
return true
73
return false
74
75
exports.endswith = (s, t) ->
76
if not s? or not t?
77
return false # undefined doesn't endswith anything...
78
return s.slice(s.length - t.length) == t
79
80
# Modifies in place the object dest so that it
81
# includes all values in objs and returns dest
82
# Rightmost object overwrites left.
83
exports.merge = (dest, objs...) ->
84
for obj in objs
85
for k, v of obj
86
dest[k] = v
87
dest
88
89
# Makes new object that is shallow copy merge of all objects.
90
exports.merge_copy = (objs...) ->
91
return exports.merge({}, objs...)
92
93
# Return a random element of an array
94
exports.random_choice = (array) -> array[Math.floor(Math.random() * array.length)]
95
96
# Given an object map {foo:bar, ...} returns an array [foo, bar] randomly
97
# chosen from the object map.
98
exports.random_choice_from_obj = (obj) ->
99
k = exports.random_choice(exports.keys(obj))
100
return [k, obj[k]]
101
102
# Returns a random integer in the range, inclusive (like in Python)
103
exports.randint = (lower, upper) ->
104
if lower > upper
105
throw new Error("randint: lower is larger than upper")
106
Math.floor(Math.random()*(upper - lower + 1)) + lower
107
108
# Like Python's string split -- splits on whitespace
109
exports.split = (s) ->
110
r = s.match(/\S+/g)
111
if r
112
return r
113
else
114
return []
115
116
# Like the exports.split method, but quoted terms are grouped together for an exact search.
117
exports.search_split = (search) ->
118
terms = []
119
search = search.split('"')
120
length = search.length
121
for element, i in search
122
element = element.trim()
123
if element.length != 0
124
# the even elements lack quotation
125
# if there are an even number of elements that means there is an unclosed quote,
126
# so the last element shouldn't be grouped.
127
if i % 2 == 0 or (i == length - 1 and length % 2 == 0)
128
terms.push(element.split(" ")...)
129
else
130
terms.push(element)
131
return terms
132
133
# s = lower case string
134
# v = array of terms as output by search_split above
135
exports.search_match = (s, v) ->
136
for x in v
137
if s.indexOf(x) == -1
138
return false
139
return true
140
141
# return true if the word contains the substring
142
exports.contains = (word, sub) ->
143
return word.indexOf(sub) isnt -1
144
145
146
# Count number of occurrences of m in s-- see http://stackoverflow.com/questions/881085/count-the-number-of-occurences-of-a-character-in-a-string-in-javascript
147
148
exports.count = (str, strsearch) ->
149
index = -1
150
count = -1
151
loop
152
index = str.indexOf(strsearch, index + 1)
153
count++
154
break if index is -1
155
return count
156
157
# modifies target in place, so that the properties of target are the
158
# same as those of upper_bound, and each is <=.
159
exports.min_object = (target, upper_bounds) ->
160
if not target?
161
target = {}
162
for prop, val of upper_bounds
163
target[prop] = if target.hasOwnProperty(prop) then target[prop] = Math.min(target[prop], upper_bounds[prop]) else upper_bounds[prop]
164
165
# Current time in milliseconds since epoch
166
exports.mswalltime = (t) ->
167
if t?
168
return (new Date()).getTime() - t
169
else
170
return (new Date()).getTime()
171
172
# Current time in seconds since epoch, as a floating point number (so much more precise than just seconds).
173
exports.walltime = (t) ->
174
if t?
175
return exports.mswalltime()/1000.0 - t
176
else
177
return exports.mswalltime()/1000.0
178
179
# We use this uuid implementation only for the browser client. For node code, use node-uuid.
180
exports.uuid = ->
181
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
182
r = Math.random() * 16 | 0
183
v = if c == 'x' then r else r & 0x3 | 0x8
184
v.toString 16
185
186
exports.is_valid_uuid_string = (uuid) ->
187
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)
188
# /[0-9a-f]{22}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(uuid)
189
190
exports.assert_uuid = (uuid) =>
191
if not exports.is_valid_uuid_string(uuid)
192
throw Error("invalid uuid='#{uuid}'")
193
return
194
195
exports.is_valid_sha1_string = (s) ->
196
return typeof(s) == 'string' and s.length == 40 and /[a-fA-F0-9]{40}/i.test(s)
197
198
# Compute a uuid v4 from the Sha-1 hash of data.
199
# If on backend, use the version in misc_node, which is faster.
200
sha1 = require('sha1')
201
exports.uuidsha1 = (data) ->
202
s = sha1(data)
203
i = -1
204
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) ->
205
i += 1
206
switch c
207
when 'x'
208
return s[i]
209
when 'y'
210
# take 8 + low order 3 bits of hex number.
211
return ((parseInt('0x'+s[i],16)&0x3)|0x8).toString(16)
212
)
213
214
zipcode = new RegExp("^\\d{5}(-\\d{4})?$")
215
exports.is_valid_zipcode = (zip) -> zipcode.test(zip)
216
217
# Return a very rough benchmark of the number of times f will run per second.
218
exports.times_per_second = (f, max_time=5, max_loops=1000) ->
219
# return number of times per second that f() can be called
220
t = exports.walltime()
221
i = 0
222
tm = 0
223
while true
224
f()
225
tm = exports.walltime() - t
226
i += 1
227
if tm >= max_time or i >= max_loops
228
break
229
return Math.ceil(i/tm)
230
231
exports.to_json = JSON.stringify
232
233
###
234
The functions to_json_socket and from_json_socket are for sending JSON data back
235
and forth in serialized form over a socket connection. They replace Date objects by the
236
object {DateEpochMS:ms_since_epoch} *only* during transit. This is much better than
237
converting to ISO, then using a regexp, since then all kinds of strings will get
238
converted that were never meant to be date objects at all, e.g., a filename that is
239
a ISO time string. Also, ms since epoch is less ambiguous regarding old/different
240
browsers, and more compact.
241
242
If you change SOCKET_DATE_KEY, then all clients and servers and projects must be
243
simultaneously restarted.
244
###
245
SOCKET_DATE_KEY = 'DateEpochMS'
246
247
socket_date_replacer = (key, value) ->
248
if this[key] instanceof Date
249
date = this[key]
250
return {"#{SOCKET_DATE_KEY}":date - 0}
251
else
252
return value
253
254
exports.to_json_socket = (x) ->
255
JSON.stringify(x, socket_date_replacer)
256
257
socket_date_parser = (key, value) ->
258
if value?[SOCKET_DATE_KEY]?
259
return new Date(value[SOCKET_DATE_KEY])
260
else
261
return value
262
263
exports.from_json_socket = (x) ->
264
try
265
JSON.parse(x, socket_date_parser)
266
catch err
267
console.debug("from_json: error parsing #{x} (=#{exports.to_json(x)}) from JSON")
268
throw err
269
270
271
272
# convert object x to a JSON string, removing any keys that have "pass" in them and
273
# any values that are potentially big -- this is meant to only be used for loging.
274
exports.to_safe_str = (x) ->
275
obj = {}
276
for key, value of x
277
sanitize = false
278
279
if key.indexOf("pass") != -1
280
sanitize = true
281
else if typeof(value)=='string' and value.slice(0,7) == "sha512$"
282
sanitize = true
283
284
if sanitize
285
obj[key] = '(unsafe)'
286
else
287
if typeof(value) == "object"
288
value = "[object]" # many objects, e.g., buffers can block for seconds to JSON...
289
else if typeof(value) == "string"
290
value = exports.trunc(value,250) # long strings are not SAFE -- since JSON'ing them for logging blocks for seconds!
291
obj[key] = value
292
293
x = exports.to_json(obj)
294
295
# convert from a JSON string to Javascript (properly dealing with ISO dates)
296
# e.g., 2016-12-12T02:12:03.239Z and 2016-12-12T02:02:53.358752
297
reISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/
298
exports.date_parser = date_parser = (k, v) ->
299
if typeof(v) == 'string' and v.length >= 20 and reISO.exec(v)
300
return ISO_to_Date(v)
301
else
302
return v
303
304
exports.ISO_to_Date = ISO_to_Date = (s) ->
305
if s.indexOf('Z') == -1
306
# Firefox assumes local time rather than UTC if there is no Z. However,
307
# our backend might possibly send a timestamp with no Z and it should be
308
# interpretted as UTC anyways.
309
# That said, with the to_json_socket/from_json_socket code, the browser
310
# shouldn't be running this parser anyways.
311
s += 'Z'
312
return new Date(s)
313
314
315
exports.from_json = (x) ->
316
try
317
JSON.parse(x, date_parser)
318
catch err
319
console.debug("from_json: error parsing #{x} (=#{exports.to_json(x)}) from JSON")
320
throw err
321
322
# Returns modified version of obj with any string
323
# that look like ISO dates to actual Date objects. This mutates
324
# obj in place as part of the process.
325
# date_keys = 'all' or list of keys in nested object whose values should be considered. Nothing else is considered!
326
exports.fix_json_dates = fix_json_dates = (obj, date_keys) ->
327
if not date_keys? # nothing to do
328
return obj
329
if exports.is_object(obj)
330
for k, v of obj
331
if typeof(v) == 'object'
332
fix_json_dates(v, date_keys)
333
else if typeof(v) == 'string' and v.length >= 20 and reISO.exec(v) and (date_keys == 'all' or k in date_keys)
334
obj[k] = new Date(v)
335
else if exports.is_array(obj)
336
for i, x of obj
337
obj[i] = fix_json_dates(x, date_keys)
338
else if typeof(obj) == 'string' and obj.length >= 20 and reISO.exec(obj) and date_keys == 'all'
339
return new Date(obj)
340
return obj
341
342
# converts a Date object to an ISO string in UTC.
343
# NOTE -- we remove the +0000 (or whatever) timezone offset, since *all* machines within
344
# the SMC servers are assumed to be on UTC.
345
exports.to_iso = (d) -> (new Date(d - d.getTimezoneOffset()*60*1000)).toISOString().slice(0,-5)
346
347
# turns a Date object into a more human readable more friendly directory name in the local timezone
348
exports.to_iso_path = (d) -> exports.to_iso(d).replace('T','-').replace(/:/g,'')
349
350
# returns true if the given object has no keys
351
exports.is_empty_object = (obj) -> Object.keys(obj).length == 0
352
353
# returns the number of keys of an object, e.g., {a:5, b:7, d:'hello'} --> 3
354
exports.len = (obj) ->
355
if not obj?
356
return 0
357
a = obj.length
358
if a?
359
return a
360
underscore.keys(obj).length
361
362
# return the keys of an object, e.g., {a:5, xyz:'10'} -> ['a', 'xyz']
363
exports.keys = underscore.keys
364
365
# returns the values of a map
366
exports.values = underscore.values
367
368
# as in python, makes a map from an array of pairs [(x,y),(z,w)] --> {x:y, z:w}
369
exports.dict = (obj) ->
370
x = {}
371
for a in obj
372
if a.length != 2
373
throw new Error("ValueError: unexpected length of tuple")
374
x[a[0]] = a[1]
375
return x
376
377
# remove first occurrence of value (just like in python);
378
# throws an exception if val not in list.
379
exports.remove = (obj, val) ->
380
for i in [0...obj.length]
381
if obj[i] == val
382
obj.splice(i, 1)
383
return
384
throw new Error("ValueError -- item not in array")
385
386
# convert an array of 2-element arrays to an object, e.g., [['a',5], ['xyz','10']] --> {a:5, xyz:'10'}
387
exports.pairs_to_obj = (v) ->
388
o = {}
389
for x in v
390
o[x[0]] = x[1]
391
return o
392
393
exports.obj_to_pairs = (obj) -> ([x,y] for x,y of obj)
394
395
# from http://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string via http://js2coffee.org/
396
exports.substring_count = (string, subString, allowOverlapping) ->
397
string += ""
398
subString += ""
399
return string.length + 1 if subString.length <= 0
400
n = 0
401
pos = 0
402
step = (if (allowOverlapping) then (1) else (subString.length))
403
loop
404
pos = string.indexOf(subString, pos)
405
if pos >= 0
406
n++
407
pos += step
408
else
409
break
410
return n
411
412
exports.max = (array) -> (array.reduce((a,b) -> Math.max(a, b)))
413
414
exports.min = (array) -> (array.reduce((a,b) -> Math.min(a, b)))
415
416
filename_extension_re = /(?:\.([^.]+))?$/
417
exports.filename_extension = (filename) ->
418
filename = exports.path_split(filename).tail
419
return filename_extension_re.exec(filename)[1] ? ''
420
421
exports.filename_extension_notilde = (filename) ->
422
ext = exports.filename_extension(filename)
423
while ext and ext[ext.length-1] == '~' # strip tildes from the end of the extension -- put there by rsync --backup, and other backup systems in UNIX.
424
ext = ext.slice(0, ext.length-1)
425
return ext
426
427
# If input name foo.bar, returns object {name:'foo', ext:'bar'}.
428
# If there is no . in input name, returns {name:name, ext:''}
429
exports.separate_file_extension = (name) ->
430
ext = exports.filename_extension(name)
431
if ext isnt ''
432
name = name[0...name.length - ext.length - 1] # remove the ext and the .
433
return {name: name, ext: ext}
434
435
# change the filename's extension to the new one.
436
# if there is no extension, add it.
437
exports.change_filename_extension = (name, new_ext) ->
438
{name, ext} = exports.separate_file_extension(name)
439
return "#{name}.#{new_ext}"
440
441
# shallow copy of a map
442
exports.copy = (obj) ->
443
if not obj? or typeof(obj) isnt 'object'
444
return obj
445
if exports.is_array(obj)
446
return obj[..]
447
r = {}
448
for x, y of obj
449
r[x] = y
450
return r
451
452
# copy of map but without some keys
453
exports.copy_without = (obj, without) ->
454
if typeof(without) == 'string'
455
without = [without]
456
r = {}
457
for x, y of obj
458
if x not in without
459
r[x] = y
460
return r
461
462
# copy of map but only with some keys
463
exports.copy_with = (obj, w) ->
464
if typeof(w) == 'string'
465
w = [w]
466
r = {}
467
for x, y of obj
468
if x in w
469
r[x] = y
470
return r
471
472
# From http://coffeescriptcookbook.com/chapters/classes_and_objects/cloning
473
exports.deep_copy = (obj) ->
474
if not obj? or typeof obj isnt 'object'
475
return obj
476
477
if obj instanceof Date
478
return new Date(obj.getTime())
479
480
if obj instanceof RegExp
481
flags = ''
482
flags += 'g' if obj.global?
483
flags += 'i' if obj.ignoreCase?
484
flags += 'm' if obj.multiline?
485
flags += 'y' if obj.sticky?
486
return new RegExp(obj.source, flags)
487
488
try
489
newInstance = new obj.constructor()
490
catch
491
newInstance = {}
492
493
for key, val of obj
494
newInstance[key] = exports.deep_copy(val)
495
496
return newInstance
497
498
# Split a pathname. Returns an object {head:..., tail:...} where tail is
499
# everything after the final slash. Either part may be empty.
500
# (Same as os.path.split in Python.)
501
exports.path_split = (path) ->
502
v = path.split('/')
503
return {head:v.slice(0,-1).join('/'), tail:v[v.length-1]}
504
505
# Takes parts to a path and intelligently merges them on '/'.
506
# Continuous non-'/' portions of each part will have at most
507
# one '/' on either side.
508
# Each part will have exactly one '/' between it and adjacent parts
509
# Does NOT resolve up-level references
510
# See misc-tests for examples.
511
exports.normalized_path_join = (parts...) ->
512
sep = '/'
513
replace = new RegExp(sep+'{1,}', 'g')
514
s = ("#{x}" for x in parts when x? and "#{x}".length > 0).join(sep).replace(replace, sep)
515
return s
516
517
# Takes a path string and file name and gives the full path to the file
518
exports.path_to_file = (path, file) ->
519
if path == ''
520
return file
521
return path + '/' + file
522
523
exports.meta_file = (path, ext) ->
524
if not path?
525
return
526
p = exports.path_split(path)
527
path = p.head
528
if p.head != ''
529
path += '/'
530
return path + "." + p.tail + ".sage-" + ext
531
532
# Given a path of the form foo/bar/.baz.ext.something returns foo/bar/baz.ext.
533
# For example:
534
# .example.ipynb.sage-jupyter --> example.ipynb
535
# tmp/.example.ipynb.sage-jupyter --> tmp/example.ipynb
536
# .foo.txt.sage-chat --> foo.txt
537
# tmp/.foo.txt.sage-chat --> tmp/foo.txt
538
539
exports.original_path = (path) ->
540
s = exports.path_split(path)
541
if s.tail[0] != '.' or s.tail.indexOf('.sage-') == -1
542
return path
543
ext = exports.filename_extension(s.tail)
544
x = s.tail.slice((if s.tail[0] == '.' then 1 else 0), s.tail.length - (ext.length+1))
545
if s.head != ''
546
x = s.head + '/' + x
547
return x
548
549
ELLIPSES = "…"
550
# "foobar" --> "foo…"
551
exports.trunc = (s, max_length=1024) ->
552
if not s?
553
return s
554
if typeof(s) != 'string'
555
s = "#{s}"
556
if s.length > max_length
557
if max_length < 1
558
throw new Error("ValueError: max_length must be >= 1")
559
return s.slice(0,max_length-1) + ELLIPSES
560
else
561
return s
562
563
# "foobar" --> "fo…ar"
564
exports.trunc_middle = (s, max_length=1024) ->
565
if not s?
566
return s
567
if typeof(s) != 'string'
568
s = "#{s}"
569
if s.length <= max_length
570
return s
571
if max_length < 1
572
throw new Error("ValueError: max_length must be >= 1")
573
n = Math.floor(max_length/2)
574
return s.slice(0, n - 1 + (if max_length%2 then 1 else 0)) + ELLIPSES + s.slice(s.length-n)
575
576
# "foobar" --> "…bar"
577
exports.trunc_left = (s, max_length=1024) ->
578
if not s?
579
return s
580
if typeof(s) != 'string'
581
s = "#{s}"
582
if s.length > max_length
583
if max_length < 1
584
throw new Error("ValueError: max_length must be >= 1")
585
return ELLIPSES + s.slice(s.length-max_length+1)
586
else
587
return s
588
589
exports.pad_left = (s, n) ->
590
if not typeof(s) == 'string'
591
s = "#{s}"
592
for i in [s.length...n]
593
s = ' ' + s
594
return s
595
596
exports.pad_right = (s, n) ->
597
if not typeof(s) == 'string'
598
s = "#{s}"
599
for i in [s.length...n]
600
s += ' '
601
return s
602
603
# gives the plural form of the word if the number should be plural
604
exports.plural = (number, singular, plural="#{singular}s") ->
605
if singular in ['GB', 'MB']
606
return singular
607
if number == 1 then singular else plural
608
609
610
exports.git_author = (first_name, last_name, email_address) -> "#{first_name} #{last_name} <#{email_address}>"
611
612
reValidEmail = (() ->
613
sQtext = "[^\\x0d\\x22\\x5c\\x80-\\xff]"
614
sDtext = "[^\\x0d\\x5b-\\x5d\\x80-\\xff]"
615
sAtom = "[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+"
616
sQuotedPair = "\\x5c[\\x00-\\x7f]"
617
sDomainLiteral = "\\x5b(" + sDtext + "|" + sQuotedPair + ")*\\x5d"
618
sQuotedString = "\\x22(" + sQtext + "|" + sQuotedPair + ")*\\x22"
619
sDomain_ref = sAtom
620
sSubDomain = "(" + sDomain_ref + "|" + sDomainLiteral + ")"
621
sWord = "(" + sAtom + "|" + sQuotedString + ")"
622
sDomain = sSubDomain + "(\\x2e" + sSubDomain + ")*"
623
sLocalPart = sWord + "(\\x2e" + sWord + ")*"
624
sAddrSpec = sLocalPart + "\\x40" + sDomain # complete RFC822 email address spec
625
sValidEmail = "^" + sAddrSpec + "$" # as whole string
626
return new RegExp(sValidEmail)
627
)()
628
629
exports.is_valid_email_address = (email) ->
630
# From http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
631
# but converted to Javascript; it's near the middle but claims to be exactly RFC822.
632
if reValidEmail.test(email)
633
return true
634
else
635
return false
636
637
# More canonical email address -- lower case and remove stuff between + and @.
638
# This is mainly used for banning users.
639
640
exports.canonicalize_email_address = (email_address) ->
641
if typeof(email_address) != 'string'
642
# silly, but we assume it is a string, and I'm concerned about a hacker attack involving that
643
email_address = JSON.stringify(email_address)
644
# remove + part from email address: [email protected]
645
i = email_address.indexOf('+')
646
if i != -1
647
j = email_address.indexOf('@')
648
if j != -1
649
email_address = email_address.slice(0,i) + email_address.slice(j)
650
# make email address lower case
651
return email_address.toLowerCase()
652
653
654
exports.lower_email_address = (email_address) ->
655
if not email_address?
656
return
657
if typeof(email_address) != 'string'
658
# silly, but we assume it is a string, and I'm concerned about a hacker attack involving that
659
email_address = JSON.stringify(email_address)
660
# make email address lower case
661
return email_address.toLowerCase()
662
663
664
# Parses a string reresenting a search of users by email or non-email
665
# Expects the string to be delimited by commas or semicolons
666
# between multiple users
667
#
668
# Non-email strings are ones without an '@' and will be split on whitespace
669
#
670
# Emails may be wrapped by angle brackets.
671
# ie. <[email protected]> is valid and understood as [email protected]
672
# (Note that <<[email protected]> will be <[email protected] which is not valid)
673
# Emails must be legal as specified by RFC822
674
#
675
# returns an object with the queries in lowercase
676
# eg.
677
# {
678
# string_queries: ["firstname", "lastname", "somestring"]
679
# email_queries: ["[email protected]", "[email protected]"]
680
# }
681
exports.parse_user_search = (query) ->
682
queries = (q.trim().toLowerCase() for q in query.split(/,|;/))
683
r = {string_queries:[], email_queries:[]}
684
email_re = /<(.*)>/
685
for x in queries
686
if x
687
# Is not an email
688
if x.indexOf('@') == -1
689
r.string_queries.push(x.split(/\s+/g))
690
else
691
# extract just the email address out
692
for a in exports.split(x)
693
# Ensures that we don't throw away emails like
694
# "<validEmail>"[email protected]
695
if a[0] == '<'
696
match = email_re.exec(a)
697
a = match?[1] ? a
698
if exports.is_valid_email_address(a)
699
r.email_queries.push(a)
700
return r
701
702
703
# Delete trailing whitespace in the string s.
704
exports.delete_trailing_whitespace = (s) ->
705
return s.replace(/[^\S\n]+$/gm, "")
706
707
708
exports.assert = (condition, mesg) ->
709
if not condition
710
if typeof mesg == 'string'
711
throw new Error(mesg)
712
throw mesg
713
714
715
exports.retry_until_success = (opts) ->
716
opts = exports.defaults opts,
717
f : exports.required # f((err) => )
718
start_delay : 100 # milliseconds
719
max_delay : 20000 # milliseconds -- stop increasing time at this point
720
factor : 1.4 # multiply delay by this each time
721
max_tries : undefined # maximum number of times to call f
722
max_time : undefined # milliseconds -- don't call f again if the call would start after this much time from first call
723
log : undefined
724
warn : undefined
725
name : ''
726
cb : undefined # called with cb() on *success*; cb(error) if max_tries is exceeded
727
728
delta = opts.start_delay
729
tries = 0
730
if opts.max_time?
731
start_time = new Date()
732
g = () ->
733
tries += 1
734
if opts.log?
735
if opts.max_tries?
736
opts.log("retry_until_success(#{opts.name}) -- try #{tries}/#{opts.max_tries}")
737
if opts.max_time?
738
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)")
739
if not opts.max_tries? and not opts.max_time?
740
opts.log("retry_until_success(#{opts.name}) -- try #{tries}")
741
opts.f (err)->
742
if err
743
if err == "not_public"
744
opts.cb?("not_public")
745
return
746
if err and opts.warn?
747
opts.warn("retry_until_success(#{opts.name}) -- err=#{JSON.stringify(err)}")
748
if opts.log?
749
opts.log("retry_until_success(#{opts.name}) -- err=#{JSON.stringify(err)}")
750
if opts.max_tries? and opts.max_tries <= tries
751
opts.cb?("maximum tries (=#{opts.max_tries}) exceeded - last error #{JSON.stringify(err)}")
752
return
753
delta = Math.min(opts.max_delay, opts.factor * delta)
754
if opts.max_time? and (new Date() - start_time) + delta > opts.max_time
755
opts.cb?("maximum time (=#{opts.max_time}ms) exceeded - last error #{JSON.stringify(err)}")
756
return
757
setTimeout(g, delta)
758
else
759
if opts.log?
760
opts.log("retry_until_success(#{opts.name}) -- success")
761
opts.cb?()
762
g()
763
764
765
# Attempt (using exponential backoff) to execute the given function.
766
# Will keep retrying until it succeeds, then call "cb()". You may
767
# call this multiple times and all callbacks will get called once the
768
# connection succeeds, since it keeps a stack of all cb's.
769
# The function f that gets called should make one attempt to do what it
770
# does, then on success do cb() and on failure cb(err).
771
# It must *NOT* call the RetryUntilSuccess callable object.
772
#
773
# Usage
774
#
775
# @foo = retry_until_success_wrapper(f:@_foo)
776
# @bar = retry_until_success_wrapper(f:@_foo, start_delay:100, max_delay:10000, exp_factor:1.5)
777
#
778
exports.retry_until_success_wrapper = (opts) ->
779
_X = new RetryUntilSuccess(opts)
780
return (cb) -> _X.call(cb)
781
782
class RetryUntilSuccess
783
constructor: (opts) ->
784
@opts = exports.defaults opts,
785
f : exports.defaults.required # f(cb); cb(err)
786
start_delay : 100 # initial delay beforing calling f again. times are all in milliseconds
787
max_delay : 20000
788
exp_factor : 1.4
789
max_tries : undefined
790
max_time : undefined # milliseconds -- don't call f again if the call would start after this much time from first call
791
min_interval : 100 # if defined, all calls to f will be separated by *at least* this amount of time (to avoid overloading services, etc.)
792
logname : undefined
793
verbose : false
794
if @opts.min_interval?
795
if @opts.start_delay < @opts.min_interval
796
@opts.start_delay = @opts.min_interval
797
@f = @opts.f
798
799
call: (cb, retry_delay) =>
800
if @opts.logname?
801
console.debug("#{@opts.logname}(... #{retry_delay})")
802
803
if not @_cb_stack?
804
@_cb_stack = []
805
if cb?
806
@_cb_stack.push(cb)
807
if @_calling
808
return
809
@_calling = true
810
if not retry_delay?
811
@attempts = 0
812
813
if @opts.logname?
814
console.debug("actually calling -- #{@opts.logname}(... #{retry_delay})")
815
816
if @opts.max_time?
817
start_time = new Date()
818
819
g = () =>
820
if @opts.min_interval?
821
@_last_call_time = exports.mswalltime()
822
@f (err) =>
823
@attempts += 1
824
@_calling = false
825
if err
826
if @opts.verbose
827
console.debug("#{@opts.logname}: error=#{err}")
828
if @opts.max_tries? and @attempts >= @opts.max_tries
829
while @_cb_stack.length > 0
830
@_cb_stack.pop()(err)
831
return
832
if not retry_delay?
833
retry_delay = @opts.start_delay
834
else
835
retry_delay = Math.min(@opts.max_delay, @opts.exp_factor*retry_delay)
836
if @opts.max_time? and (new Date() - start_time) + retry_delay > @opts.max_time
837
err = "maximum time (=#{@opts.max_time}ms) exceeded - last error #{err}"
838
while @_cb_stack.length > 0
839
@_cb_stack.pop()(err)
840
return
841
f = () =>
842
@call(undefined, retry_delay)
843
setTimeout(f, retry_delay)
844
else
845
while @_cb_stack.length > 0
846
@_cb_stack.pop()()
847
if not @_last_call_time? or not @opts.min_interval?
848
g()
849
else
850
w = exports.mswalltime(@_last_call_time)
851
if w < @opts.min_interval
852
setTimeout(g, @opts.min_interval - w)
853
else
854
g()
855
856
# WARNING: params below have different semantics than above; these are what *really* make sense....
857
exports.eval_until_defined = (opts) ->
858
opts = exports.defaults opts,
859
code : exports.required
860
start_delay : 100 # initial delay beforing calling f again. times are all in milliseconds
861
max_time : 10000 # error if total time spent trying will exceed this time
862
exp_factor : 1.4
863
cb : exports.required # cb(err, eval(code))
864
delay = undefined
865
total = 0
866
f = () ->
867
result = eval(opts.code)
868
if result?
869
opts.cb(false, result)
870
else
871
if not delay?
872
delay = opts.start_delay
873
else
874
delay *= opts.exp_factor
875
total += delay
876
if total > opts.max_time
877
opts.cb("failed to eval code within #{opts.max_time}")
878
else
879
setTimeout(f, delay)
880
f()
881
882
883
# An async debounce, kind of like the debounce in http://underscorejs.org/#debounce.
884
# Crucially, this async_debounce does NOT return a new function and store its state in a closure
885
# (like the maybe broken https://github.com/juliangruber/async-debounce), so we can use it for
886
# making async debounced methods in classes (see examples in SMC source code for how to do this).
887
888
# TODO: this is actually throttle, not debounce...
889
890
exports.async_debounce = (opts) ->
891
opts = defaults opts,
892
f : required # async function f whose *only* argument is a callback
893
interval : 1500 # call f at most this often (in milliseconds)
894
state : required # store state information about debounce in this *object*
895
cb : undefined # as if f(cb) happens -- cb may be undefined.
896
{f, interval, state, cb} = opts
897
898
call_again = ->
899
n = interval + 1 - (new Date() - state.last)
900
#console.log("starting timer for #{n}ms")
901
state.timer = setTimeout((=>delete state.timer; exports.async_debounce(f:f, interval:interval, state:state)), n)
902
903
if state.last? and (new Date() - state.last) <= interval
904
# currently running or recently ran -- put in queue for next run
905
state.next_callbacks ?= []
906
if cb?
907
state.next_callbacks.push(cb)
908
#console.log("now have state.next_callbacks of length #{state.next_callbacks.length}")
909
if not state.timer?
910
call_again()
911
return
912
913
# Not running, so start running
914
state.last = new Date() # when we started running
915
# The callbacks that we will call, since they were set before we started running:
916
callbacks = exports.copy(state.next_callbacks ? [])
917
# Plus our callback from this time.
918
if cb?
919
callbacks.push(cb)
920
# Reset next callbacks
921
delete state.next_callbacks
922
#console.log("doing run with #{callbacks.length} callbacks")
923
924
f (err) =>
925
# finished running... call callbacks
926
#console.log("finished running -- calling #{callbacks.length} callbacks", callbacks)
927
for cb in callbacks
928
cb?(err)
929
callbacks = [] # ensure these callbacks don't get called again
930
#console.log("finished -- have state.next_callbacks of length #{state.next_callbacks.length}")
931
if state.next_callbacks? and not state.timer?
932
# new calls came in since when we started, so call when we next can.
933
#console.log("new callbacks came in #{state.next_callbacks.length}")
934
call_again()
935
936
# Class to use for mapping a collection of strings to characters (e.g., for use with diff/patch/match).
937
class exports.StringCharMapping
938
constructor: (opts={}) ->
939
opts = exports.defaults opts,
940
to_char : undefined
941
to_string : undefined
942
@_to_char = {}
943
@_to_string = {}
944
@_next_char = 'A'
945
if opts.to_string?
946
for ch, st of opts.to_string
947
@_to_string[ch] = st
948
@_to_char[st] = ch
949
if opts.to_char?
950
for st,ch of opts.to_char
951
@_to_string[ch] = st
952
@_to_char[st] = ch
953
@_find_next_char()
954
955
_find_next_char: () =>
956
loop
957
@_next_char = String.fromCharCode(@_next_char.charCodeAt(0) + 1)
958
break if not @_to_string[@_next_char]?
959
960
to_string: (strings) =>
961
t = ''
962
for s in strings
963
a = @_to_char[s]
964
if a?
965
t += a
966
else
967
t += @_next_char
968
@_to_char[s] = @_next_char
969
@_to_string[@_next_char] = s
970
@_find_next_char()
971
return t
972
973
to_array: (string) =>
974
return (@_to_string[s] for s in string)
975
976
# Given a string s, return the string obtained by deleting all later duplicate characters from s.
977
exports.uniquify_string = (s) ->
978
seen_already = {}
979
t = ''
980
for c in s
981
if not seen_already[c]?
982
t += c
983
seen_already[c] = true
984
return t
985
986
987
# Return string t=s+'\n'*k so that t ends in at least n newlines.
988
# Returns s itself (so no copy made) if s already ends in n newlines (a common case).
989
### -- not used
990
exports.ensure_string_ends_in_newlines = (s, n) ->
991
j = s.length-1
992
while j >= 0 and j >= s.length-n and s[j] == '\n'
993
j -= 1
994
# Now either j = -1 or s[j] is not a newline (and it is the first character not a newline from the right).
995
console.debug(j)
996
k = n - (s.length - (j + 1))
997
console.debug(k)
998
if k == 0
999
return s
1000
else
1001
return s + Array(k+1).join('\n') # see http://stackoverflow.com/questions/1877475/repeat-character-n-times
1002
###
1003
1004
1005
1006
1007
# Used in the database, etc., for different types of users of a project
1008
1009
exports.PROJECT_GROUPS = ['owner', 'collaborator', 'viewer', 'invited_collaborator', 'invited_viewer']
1010
1011
1012
# turn an arbitrary string into a nice clean identifier that can safely be used in an URL
1013
exports.make_valid_name = (s) ->
1014
# for now we just delete anything that isn't alphanumeric.
1015
# See http://stackoverflow.com/questions/9364400/remove-not-alphanumeric-characters-from-string-having-trouble-with-the-char/9364527#9364527
1016
# whose existence surprised me!
1017
return s.replace(/\W/g, '_').toLowerCase()
1018
1019
1020
1021
# format is 2014-04-04-061502
1022
exports.parse_bup_timestamp = (s) ->
1023
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']
1024
return new Date("#{v[1]}/#{v[2]}/#{v[0]} #{v[3]}:#{v[4]}:#{v[5]} UTC")
1025
1026
exports.matches = (s, words) ->
1027
for word in words
1028
if s.indexOf(word) == -1
1029
return false
1030
return true
1031
1032
exports.hash_string = (s) ->
1033
# see http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
1034
hash = 0
1035
i = undefined
1036
chr = undefined
1037
len = undefined
1038
return hash if s.length is 0
1039
i = 0
1040
len = s.length
1041
while i < len
1042
chr = s.charCodeAt(i)
1043
hash = ((hash << 5) - hash) + chr
1044
hash |= 0 # convert to 32-bit integer
1045
i++
1046
return hash
1047
1048
1049
1050
1051
exports.parse_hashtags = (t) ->
1052
# return list of pairs (i,j) such that t.slice(i,j) is a hashtag (starting with #).
1053
v = []
1054
if not t?
1055
return v
1056
base = 0
1057
while true
1058
i = t.indexOf('#')
1059
if i == -1 or i == t.length-1
1060
return v
1061
base += i+1
1062
if t[i+1] == '#' or not (i == 0 or t[i-1].match(/\s/))
1063
t = t.slice(i+1)
1064
continue
1065
t = t.slice(i+1)
1066
# find next whitespace or non-alphanumeric or dash
1067
# TODO: this lines means hashtags must be US ASCII --
1068
# see http://stackoverflow.com/questions/1661197/valid-characters-for-javascript-variable-names
1069
i = t.match(/\s|[^A-Za-z0-9_\-]/)
1070
if i
1071
i = i.index
1072
else
1073
i = -1
1074
if i == 0
1075
# hash followed immediately by whitespace -- markdown desc
1076
base += i+1
1077
t = t.slice(i+1)
1078
else
1079
# a hash tag
1080
if i == -1
1081
# to the end
1082
v.push([base-1, base+t.length])
1083
return v
1084
else
1085
v.push([base-1, base+i])
1086
base += i+1
1087
t = t.slice(i+1)
1088
1089
# see http://docs.mathjax.org/en/latest/tex.html#environments
1090
mathjax_environments = ['align', 'align*', 'alignat', 'alignat*', 'aligned', 'alignedat', 'array', \
1091
'Bmatrix', 'bmatrix', 'cases', 'CD', 'eqnarray', 'eqnarray*', 'equation', 'equation*', \
1092
'gather', 'gather*', 'gathered', 'matrix', 'multline', 'multline*', 'pmatrix', 'smallmatrix', \
1093
'split', 'subarray', 'Vmatrix', 'vmatrix']
1094
mathjax_delim = [['$$','$$'], ['\\(','\\)'], ['\\[','\\]']]
1095
for env in mathjax_environments
1096
mathjax_delim.push(["\\begin{#{env}}", "\\end{#{env}}"])
1097
mathjax_delim.push(['$', '$']) # must be after $$, best to put it at the end
1098
1099
exports.parse_mathjax = (t) ->
1100
# Return list of pairs (i,j) such that t.slice(i,j) is a mathjax, including delimiters.
1101
# The delimiters are given in the mathjax_delim list above.
1102
v = []
1103
if not t?
1104
return v
1105
i = 0
1106
while i < t.length
1107
# escaped dollar sign, ignored
1108
if t.slice(i, i+2) == '\\$'
1109
i += 2
1110
continue
1111
for d in mathjax_delim
1112
contains_linebreak = false
1113
# start of a formula detected
1114
if t.slice(i, i + d[0].length) == d[0]
1115
# a match -- find the close
1116
j = i+1
1117
while j < t.length and t.slice(j, j + d[1].length) != d[1]
1118
next_char = t.slice(j, j+1)
1119
if next_char == "\n"
1120
contains_linebreak = true
1121
if d[0] == "$"
1122
break
1123
# deal with ending ` char in markdown (mathjax doesn't stop there)
1124
prev_char = t.slice(j-1, j)
1125
if next_char == "`" and prev_char != '\\' # implicitly also covers "```"
1126
j -= 1 # backtrack one step
1127
break
1128
j += 1
1129
j += d[1].length
1130
# filter out the case, where there is just one $ in one line (e.g. command line, USD, ...)
1131
at_end_of_string = j > t.length
1132
if !(d[0] == "$" and (contains_linebreak or at_end_of_string))
1133
v.push([i,j])
1134
i = j
1135
break
1136
i += 1
1137
return v
1138
1139
# If you're going to set some innerHTML then mathjax it,
1140
exports.mathjax_escape = (html) ->
1141
return html.replace(/&(?!#?\w+;)/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;")
1142
1143
1144
# Return true if (1) path is contained in one
1145
# of the given paths (a list of strings) -- or path without
1146
# zip extension is in paths.
1147
# Always returns false if path is undefined/null (since that might be dangerous, right)?
1148
exports.path_is_in_public_paths = (path, paths) ->
1149
return exports.containing_public_path(path, paths)?
1150
1151
# returns a string in paths if path is public because of that string
1152
# Otherwise, returns undefined.
1153
# IMPORTANT: a possible returned string is "", which is falsey but defined!
1154
exports.containing_public_path = (path, paths) ->
1155
if paths.length == 0
1156
return
1157
if not path?
1158
return
1159
if path.indexOf('../') != -1
1160
# just deny any potentially trickiery involving relative path segments (TODO: maybe too restrictive?)
1161
return
1162
for p in paths
1163
if p == "" # the whole project is public, which matches everything
1164
return ""
1165
if path == p
1166
# exact match
1167
return p
1168
if path.slice(0,p.length+1) == p + '/'
1169
return p
1170
if exports.filename_extension(path) == "zip"
1171
# is path something_public.zip ?
1172
return exports.containing_public_path(path.slice(0,path.length-4), paths)
1173
return undefined
1174
1175
# encode a UNIX path, which might have # and % in it.
1176
exports.encode_path = (path) ->
1177
path = encodeURI(path) # doesn't escape # and ?, since they are special for urls (but not unix paths)
1178
return path.replace(/#/g,'%23').replace(/\?/g,'%3F')
1179
1180
1181
# This adds a method _call_with_lock to obj, which makes it so it's easy to make it so only
1182
# one method can be called at a time of an object -- all calls until completion
1183
# of the first one get an error.
1184
1185
exports.call_lock = (opts) ->
1186
opts = exports.defaults opts,
1187
obj : exports.required
1188
timeout_s : 30 # lock expire timeout after this many seconds
1189
1190
obj = opts.obj
1191
1192
obj._call_lock = () ->
1193
obj.__call_lock = true
1194
obj.__call_lock_timeout = () ->
1195
obj.__call_lock = false
1196
delete obj.__call_lock_timeout
1197
setTimeout(obj.__call_lock_timeout, opts.timeout_s * 1000)
1198
1199
obj._call_unlock = () ->
1200
if obj.__call_lock_timeout?
1201
clearTimeout(obj.__call_lock_timeout)
1202
delete obj.__call_lock_timeout
1203
obj.__call_lock = false
1204
1205
obj._call_with_lock = (f, cb) ->
1206
if obj.__call_lock
1207
cb?("error -- hit call_lock")
1208
return
1209
obj._call_lock()
1210
f (args...) ->
1211
obj._call_unlock()
1212
cb?(args...)
1213
1214
exports.cmp = (a,b) ->
1215
if a < b
1216
return -1
1217
else if a > b
1218
return 1
1219
return 0
1220
1221
exports.cmp_array = (a,b) ->
1222
for i in [0...Math.max(a.length, b.length)]
1223
c = exports.cmp(a[i],b[i])
1224
if c
1225
return c
1226
return 0
1227
1228
exports.cmp_Date = (a,b) ->
1229
if not a?
1230
return -1
1231
if not b?
1232
return 1
1233
if a < b
1234
return -1
1235
else if a > b
1236
return 1
1237
return 0 # note: a == b for Date objects doesn't work as expected, but that's OK here.
1238
1239
exports.timestamp_cmp = (a,b,field='timestamp') ->
1240
return -exports.cmp_Date(a[field], b[field])
1241
1242
timestamp_cmp0 = (a,b,field='timestamp') ->
1243
return exports.cmp_Date(a[field], b[field])
1244
1245
exports.field_cmp = (field) ->
1246
return (a, b) -> exports.cmp(a[field], b[field])
1247
1248
#####################
1249
# temporary location for activity_log code, shared by front and backend.
1250
#####################
1251
1252
class ActivityLog
1253
constructor: (opts) ->
1254
opts = exports.defaults opts,
1255
events : undefined
1256
account_id : exports.required # user
1257
notifications : {}
1258
@notifications = opts.notifications
1259
@account_id = opts.account_id
1260
if opts.events?
1261
@process(opts.events)
1262
1263
obj: () =>
1264
return {notifications:@notifications, account_id:@account_id}
1265
1266
path: (e) => "#{e.project_id}/#{e.path}"
1267
1268
process: (events) =>
1269
#t0 = exports.mswalltime()
1270
by_path = {}
1271
for e in events
1272
##if e.account_id == @account_id # ignore our own events
1273
## continue
1274
key = @path(e)
1275
events_with_path = by_path[key]
1276
if not events_with_path?
1277
events_with_path = by_path[key] = [e]
1278
else
1279
events_with_path.push(e)
1280
for path, events_with_path of by_path
1281
events_with_path.sort(timestamp_cmp0) # oldest to newest
1282
for event in events_with_path
1283
@_process_event(event, path)
1284
#winston.debug("ActivityLog: processed #{events.length} in #{exports.mswalltime(t0)}ms")
1285
1286
_process_event: (event, path) =>
1287
# process the given event, assuming all older events have been
1288
# processed already; this updates the notifications object.
1289
if not path?
1290
path = @path(event)
1291
a = @notifications[path]
1292
if not a?
1293
@notifications[path] = a = {}
1294
a.timestamp = event.timestamp
1295
a.id = event.id
1296
#console.debug("process_event", event, path)
1297
#console.debug(event.seen_by?.indexOf(@account_id))
1298
#console.debug(event.read_by?.indexOf(@account_id))
1299
if event.seen_by? and event.seen_by.indexOf(@account_id) != -1
1300
a.seen = event.timestamp
1301
if event.read_by? and event.read_by.indexOf(@account_id) != -1
1302
a.read = event.timestamp
1303
1304
if event.action?
1305
who = a[event.action]
1306
if not who?
1307
who = a[event.action] = {}
1308
who[event.account_id] = event.timestamp
1309
# The code below (instead of the line above) would include *all* times.
1310
# I'm not sure whether or not I want to use that information, since it
1311
# could get really big.
1312
#times = who[event.account_id]
1313
#if not times?
1314
# times = who[event.account_id] = []
1315
#times.push(event.timestamp)
1316
1317
1318
exports.activity_log = (opts) -> new ActivityLog(opts)
1319
1320
# see http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript
1321
exports.replace_all = (string, search, replace) ->
1322
string.split(search).join(replace)
1323
1324
exports.remove_c_comments = (s) ->
1325
while true
1326
i = s.indexOf('/*')
1327
if i == -1
1328
return s
1329
j = s.indexOf('*/')
1330
if i >= j
1331
return s
1332
s = s.slice(0, i) + s.slice(j+2)
1333
1334
exports.date_to_snapshot_format = (d) ->
1335
if not d?
1336
d = 0
1337
if typeof(d) == "number"
1338
d = new Date(d)
1339
s = d.toJSON()
1340
s = s.replace('T','-').replace(/:/g, '')
1341
i = s.lastIndexOf('.')
1342
return s.slice(0,i)
1343
1344
exports.stripe_date = (d) ->
1345
return new Date(d*1000).toLocaleDateString( 'lookup', { year: 'numeric', month: 'long', day: 'numeric' })
1346
# fixing the locale to en-US (to pass tests) and (not necessary, but just in case) also the time zone
1347
#return new Date(d*1000).toLocaleDateString(
1348
# 'en-US',
1349
# year: 'numeric'
1350
# month: 'long'
1351
# day: 'numeric'
1352
# weekday: "long"
1353
# timeZone: 'UTC'
1354
#)
1355
1356
exports.to_money = (n) ->
1357
# see http://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-money-in-javascript
1358
# TODO: replace by using react-intl...
1359
return n.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
1360
1361
exports.stripe_amount = (units, currency) -> # input is in pennies
1362
if currency != 'usd'
1363
throw Error("not-implemented currency #{currency}")
1364
s = "$#{exports.to_money(units/100)}"
1365
if s.slice(s.length-3) == '.00'
1366
s = s.slice(0, s.length-3)
1367
return s
1368
1369
exports.capitalize = (s) ->
1370
if s?
1371
return s.charAt(0).toUpperCase() + s.slice(1)
1372
1373
exports.is_array = is_array = (obj) ->
1374
return Object.prototype.toString.call(obj) == "[object Array]"
1375
1376
exports.is_integer = Number.isInteger
1377
if not exports.is_integer?
1378
exports.is_integer = (n) -> typeof(n)=='number' and (n % 1) == 0
1379
1380
exports.is_string = (obj) ->
1381
return typeof(obj) == 'string'
1382
1383
# An object -- this is more constraining that typeof(obj) == 'object', e.g., it does
1384
# NOT include Date.
1385
exports.is_object = is_object = (obj) ->
1386
return Object.prototype.toString.call(obj) == "[object Object]"
1387
1388
exports.is_date = is_date = (obj) ->
1389
return obj instanceof Date
1390
1391
# get a subarray of all values between the two given values inclusive, provided in either order
1392
exports.get_array_range = (arr, value1, value2) ->
1393
index1 = arr.indexOf(value1)
1394
index2 = arr.indexOf(value2)
1395
if index1 > index2
1396
[index1, index2] = [index2, index1]
1397
return arr[index1..index2]
1398
1399
# Specific, easy to read: describe amount of time before right now
1400
# Use negative input for after now (i.e., in the future).
1401
exports.milliseconds_ago = (ms) -> new Date(new Date() - ms)
1402
exports.seconds_ago = (s) -> exports.milliseconds_ago(1000*s)
1403
exports.minutes_ago = (m) -> exports.seconds_ago(60*m)
1404
exports.hours_ago = (h) -> exports.minutes_ago(60*h)
1405
exports.days_ago = (d) -> exports.hours_ago(24*d)
1406
exports.weeks_ago = (w) -> exports.days_ago(7*w)
1407
exports.months_ago = (m) -> exports.days_ago(30.5*m)
1408
1409
if window?
1410
# BROWSER Versions of the above, but give the relevant point in time but
1411
# on the *server*. These are only available in the web browser.
1412
exports.server_time = () -> new Date(new Date() - parseFloat(exports.get_local_storage('clock_skew') ? 0))
1413
exports.server_milliseconds_ago = (ms) -> new Date(new Date() - ms - parseFloat(exports.get_local_storage('clock_skew') ? 0))
1414
exports.server_seconds_ago = (s) -> exports.server_milliseconds_ago(1000*s)
1415
exports.server_minutes_ago = (m) -> exports.server_seconds_ago(60*m)
1416
exports.server_hours_ago = (h) -> exports.server_minutes_ago(60*h)
1417
exports.server_days_ago = (d) -> exports.server_hours_ago(24*d)
1418
exports.server_weeks_ago = (w) -> exports.server_days_ago(7*w)
1419
exports.server_months_ago = (m) -> exports.server_days_ago(30.5*m)
1420
else
1421
# On the server, these functions are aliased to the functions above, since
1422
# we assume that the server clocks are sufficiently accurate. Providing
1423
# these functions makes it simpler to write code that runs on both the
1424
# frontend and the backend.
1425
exports.server_time = -> new Date()
1426
exports.server_milliseconds_ago = exports.milliseconds_ago
1427
exports.server_seconds_ago = exports.seconds_ago
1428
exports.server_minutes_ago = exports.minutes_ago
1429
exports.server_hours_ago = exports.hours_ago
1430
exports.server_days_ago = exports.days_ago
1431
exports.server_weeks_ago = exports.weeks_ago
1432
exports.server_months_ago = exports.months_ago
1433
1434
1435
# Specific easy to read and describe point in time before another point in time tm.
1436
# (The following work exactly as above if the second argument is excluded.)
1437
# Use negative input for first argument for that amount of time after tm.
1438
exports.milliseconds_before = (ms, tm) -> new Date((tm ? (new Date())) - ms)
1439
exports.seconds_before = (s, tm) -> exports.milliseconds_before(1000*s, tm)
1440
exports.minutes_before = (m, tm) -> exports.seconds_before(60*m, tm)
1441
exports.hours_before = (h, tm) -> exports.minutes_before(60*h, tm)
1442
exports.days_before = (d, tm) -> exports.hours_before(24*d, tm)
1443
exports.weeks_before = (d, tm) -> exports.days_before(7*d, tm)
1444
exports.months_before = (d, tm) -> exports.days_before(30.5*d, tm)
1445
1446
# time this many seconds in the future (or undefined)
1447
exports.expire_time = (s) ->
1448
if s then new Date((new Date() - 0) + s*1000)
1449
1450
exports.YEAR = new Date().getFullYear()
1451
1452
# Round the given number to 1 decimal place
1453
exports.round1 = round1 = (num) ->
1454
Math.round(num * 10) / 10
1455
1456
# Round given number to 2 decimal places
1457
exports.round2 = round2 = (num) ->
1458
# padding to fix floating point issue (see http://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-in-javascript)
1459
Math.round((num + 0.00001) * 100) / 100
1460
1461
exports.seconds2hms = seconds2hms = (secs) ->
1462
s = round2(secs % 60)
1463
m = Math.floor(secs / 60) % 60
1464
h = Math.floor(secs / 60 / 60)
1465
if h == 0 and m == 0
1466
return "#{s}s"
1467
if h > 0
1468
return "#{h}h#{m}m#{s}s"
1469
if m > 0
1470
return "#{m}m#{s}s"
1471
1472
# returns the number parsed from the input text, or undefined if invalid
1473
# rounds to the nearest 0.01 if round_number is true (default : true)
1474
# allows negative numbers if allow_negative is true (default : false)
1475
exports.parse_number_input = (input, round_number=true, allow_negative=false) ->
1476
input = (input + "").split('/')
1477
if input.length != 1 and input.length != 2
1478
return undefined
1479
if input.length == 2
1480
val = parseFloat(input[0]) / parseFloat(input[1])
1481
if input.length == 1
1482
if isNaN(input) or "#{input}".trim() is ''
1483
# Shockingly, whitespace returns false for isNaN!
1484
return undefined
1485
val = parseFloat(input)
1486
if round_number
1487
val = round2(val)
1488
if isNaN(val) or val == Infinity or (val < 0 and not allow_negative)
1489
return undefined
1490
return val
1491
1492
exports.range = (n, m) ->
1493
if not m?
1494
return [0...n]
1495
else
1496
return [n...m]
1497
1498
# arithmetic of maps with codomain numbers; missing values default to 0
1499
exports.map_sum = (a, b) ->
1500
if not a?
1501
return b
1502
if not b?
1503
return a
1504
c = {}
1505
for k, v of a
1506
c[k] = v + (b[k] ? 0)
1507
for k, v of b
1508
c[k] ?= v
1509
return c
1510
1511
exports.map_diff = (a, b) ->
1512
if not b?
1513
return a
1514
if not a?
1515
c = {}
1516
for k,v of b
1517
c[k] = -v
1518
return c
1519
c = {}
1520
for k, v of a
1521
c[k] = v - (b[k] ? 0)
1522
for k, v of b
1523
c[k] ?= -v
1524
return c
1525
1526
# limit the values in a by the values of b
1527
# or just by b if b is a number
1528
exports.map_limit = (a, b) ->
1529
c = {}
1530
if typeof b == 'number'
1531
for k, v of a
1532
c[k] = Math.min(v, b)
1533
else
1534
for k, v of a
1535
c[k] = Math.min(v, (b[k] ? Number.MAX_VALUE))
1536
return c
1537
1538
# arithmetic sum of an array
1539
exports.sum = (arr, start=0) -> underscore.reduce(arr, ((a, b) -> a+b), start)
1540
1541
# replace map in place by the result of applying f to each
1542
# element of the codomain of the map. Also return the modified map.
1543
exports.apply_function_to_map_values = apply_function_to_map_values = (map, f) ->
1544
for k, v of map
1545
map[k] = f(v)
1546
return map
1547
1548
# modify map by coercing each element of codomain to a number, with false->0 and true->1
1549
exports.coerce_codomain_to_numbers = (map) ->
1550
apply_function_to_map_values map, (x)->
1551
if typeof(x) == 'boolean'
1552
if x then 1 else 0
1553
else
1554
parseFloat(x)
1555
1556
# returns true if the given map is undefined or empty, or all the values are falsy
1557
exports.is_zero_map = (map) ->
1558
if not map?
1559
return true
1560
for k,v of map
1561
if v
1562
return false
1563
return true
1564
1565
# Returns copy of map with no undefined/null values (recursive).
1566
# Doesn't modify map. If map is an array, just returns it
1567
# with no change even if it has undefined values.
1568
exports.map_without_undefined = map_without_undefined = (map) ->
1569
if is_array(map)
1570
return map
1571
if not map?
1572
return
1573
new_map = {}
1574
for k, v of map
1575
if not v?
1576
continue
1577
else
1578
new_map[k] = if is_object(v) then map_without_undefined(v) else v
1579
return new_map
1580
1581
exports.map_mutate_out_undefined = (map) ->
1582
for k, v of map
1583
if not v?
1584
delete map[k]
1585
1586
1587
1588
# foreground; otherwise, return false.
1589
exports.should_open_in_foreground = (e) ->
1590
# for react.js synthetic mouse events, where e.which is undefined!
1591
if e.constructor.name == 'SyntheticMouseEvent'
1592
e = e.nativeEvent
1593
#console.log("e: #{e}, e.which: #{e.which}", e)
1594
return not (e.which == 2 or e.metaKey or e.altKey or e.ctrlKey)
1595
1596
# Like Python's enumerate
1597
exports.enumerate = (v) ->
1598
i = 0
1599
w = []
1600
for x in v
1601
w.push([i,x])
1602
i += 1
1603
return w
1604
1605
# escape everything in a regex
1606
exports.escapeRegExp = escapeRegExp = (str) ->
1607
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
1608
1609
# smiley-fication of an arbitrary string
1610
smileys_definition = [
1611
[':-)', "😁"],
1612
[':-(', "😞"],
1613
['<3', "β™‘", null, '\\b'],
1614
[':shrug:', "Β―\\\\_(ツ)_/Β―"],
1615
['o_o', "Χ‘ΦΌ_\Χ‘ΦΌ", '\\b', '\\b'],
1616
[':-p', "πŸ˜›", null, '\\b'],
1617
['>_<', "πŸ˜†"],
1618
['^^', "πŸ˜„", '^', '\S'],
1619
['^^ ', "πŸ˜„ "],
1620
[' ^^', " πŸ˜„"],
1621
[';-)', "πŸ˜‰"],
1622
['-_-', "πŸ˜”"],
1623
[':-\\', "😏"],
1624
[':omg:', "😱"]
1625
]
1626
1627
smileys = []
1628
1629
for smiley in smileys_definition
1630
s = escapeRegExp(smiley[0])
1631
if smiley[2]?
1632
s = smiley[2] + s
1633
if smiley[3]?
1634
s = s + smiley[3]
1635
smileys.push([RegExp(s, 'g'), smiley[1]])
1636
1637
exports.smiley = (opts) ->
1638
opts = exports.defaults opts,
1639
s : exports.required
1640
wrap : undefined
1641
# de-sanitize possible sanitized characters
1642
s = opts.s.replace(/&gt;/g, '>').replace(/&lt;/g, '<')
1643
for subs in smileys
1644
repl = subs[1]
1645
if opts.wrap
1646
repl = opts.wrap[0] + repl + opts.wrap[1]
1647
s = s.replace(subs[0], repl)
1648
return s
1649
1650
_ = underscore
1651
1652
exports.smiley_strings = () ->
1653
return _.filter(_.map(smileys_definition, _.first), (x) -> ! _.contains(['^^ ', ' ^^'], x))
1654
1655
# converts an array to a "human readable" array
1656
exports.to_human_list = (arr) ->
1657
arr = _.map(arr, (x) -> x.toString())
1658
if arr.length > 1
1659
return arr[...-1].join(", ") + " and " + arr[-1..]
1660
else if arr.length == 1
1661
return arr[0].toString()
1662
else
1663
return ""
1664
1665
exports.emoticons = exports.to_human_list(exports.smiley_strings())
1666
1667
exports.history_path = (path) ->
1668
p = exports.path_split(path)
1669
return if p.head then "#{p.head}/.#{p.tail}.sage-history" else ".#{p.tail}.sage-history"
1670
1671
# This is a convenience function to provide as a callback when working interactively.
1672
_done = (n, args...) ->
1673
start_time = new Date()
1674
f = (args...) ->
1675
if n != 1
1676
try
1677
args = [JSON.stringify(args, null, n)]
1678
catch
1679
# do nothing
1680
console.log("*** TOTALLY DONE! (#{(new Date() - start_time)/1000}s since start) ", args...)
1681
if args.length > 0
1682
f(args...)
1683
else
1684
return f
1685
1686
exports.done = (args...) -> _done(0, args...)
1687
exports.done1 = (args...) -> _done(1, args...)
1688
exports.done2 = (args...) -> _done(2, args...)
1689
1690
1691
smc_logger_timestamp = smc_logger_timestamp_last = smc_start_time = new Date().getTime() / 1000.0
1692
1693
exports.get_start_time_ts = ->
1694
return new Date(smc_start_time * 1000)
1695
1696
exports.get_uptime = ->
1697
return seconds2hms((new Date().getTime() / 1000.0) - smc_start_time)
1698
1699
exports.log = () ->
1700
smc_logger_timestamp = new Date().getTime() / 1000.0
1701
t = seconds2hms(smc_logger_timestamp - smc_start_time)
1702
dt = seconds2hms(smc_logger_timestamp - smc_logger_timestamp_last)
1703
# support for string interpolation for the actual console.log
1704
[msg, args...] = Array.prototype.slice.call(arguments)
1705
prompt = "[#{t} Ξ” #{dt}]"
1706
if _.isString(msg)
1707
prompt = "#{prompt} #{msg}"
1708
console.log_original(prompt, args...)
1709
else
1710
console.log_original(prompt, msg, args...)
1711
smc_logger_timestamp_last = smc_logger_timestamp
1712
1713
exports.wrap_log = () ->
1714
if not exports.RUNNING_IN_NODE and window?
1715
window.console.log_original = window.console.log
1716
window.console.log = exports.log
1717
1718
# to test exception handling
1719
exports.this_fails = ->
1720
return exports.op_to_function('noop')
1721
1722
# derive the console initialization filename from the console's filename
1723
# used in webapp and console_server_child
1724
exports.console_init_filename = (fn) ->
1725
x = exports.path_split(fn)
1726
x.tail = ".#{x.tail}.init"
1727
if x.head == ''
1728
return x.tail
1729
return [x.head, x.tail].join("/")
1730
1731
exports.has_null_leaf = has_null_leaf = (obj) ->
1732
for k, v of obj
1733
if v == null or (typeof(v) == 'object' and has_null_leaf(v))
1734
return true
1735
return false
1736
1737
# Peer Grading
1738
# this function takes a list of students (actually, arbitrary objects)
1739
# and a number N of the desired number of peers per student.
1740
# It returns a dictionary, mapping each student to a list of peers.
1741
exports.peer_grading = (students, N=2) ->
1742
if N <= 0
1743
throw "Number of peer assigments must be at least 1"
1744
if students.length <= N
1745
throw "You need at least #{N + 1} students"
1746
1747
asmnt = {}
1748
# make output dict keys sorted like students input array
1749
students.forEach((s) -> asmnt[s] = [])
1750
# randomize peer assignments
1751
s_random = underscore.shuffle(students)
1752
1753
# the peer groups are selected here. Think of nodes in a circular graph,
1754
# and node i is associated with i+1 up to i+N
1755
L = students.length
1756
for i in [0...L]
1757
asmnt[s_random[i]] = (s_random[(i + idx) % L] for idx in [1..N])
1758
1759
# sort each peer group by the order of the `student` input list
1760
for k, v of asmnt
1761
asmnt[k] = underscore.sortBy(v, (s) -> students.indexOf(s))
1762
return asmnt
1763
1764
# demonstration of the above; for tests see misc-test.coffee
1765
exports.peer_grading_demo = (S = 10, N = 2) ->
1766
peer_grading = exports.peer_grading
1767
students = [0...S]
1768
students = ("S-#{s}" for s in students)
1769
result = peer_grading(students, N=N)
1770
console.log("#{S} students graded by #{N} peers")
1771
for k, v of result
1772
console.log("#{k} ←→ #{v}")
1773
return result
1774
1775
# converts ticket number to support ticket url (currently zendesk)
1776
exports.ticket_id_to_ticket_url = (tid) ->
1777
return "https://sagemathcloud.zendesk.com/requests/#{tid}"
1778
1779
# Checks if the string only makes sense (heuristically) as downloadable url
1780
exports.is_only_downloadable = (string) ->
1781
string.indexOf('://') != -1 or exports.startswith(string, '[email protected]')
1782
1783
# Apply various transformations to url's before downloading a file using the "+ New" from web thing:
1784
# This is useful, since people often post a link to a page that *hosts* raw content, but isn't raw
1785
# content, e.g., ipython nbviewer, trac patches, github source files (or repos?), etc.
1786
exports.transform_get_url = (url) -> # returns something like {command:'wget', args:['http://...']}
1787
URL_TRANSFORMS =
1788
'http://trac.sagemath.org/attachment/ticket/' :'http://trac.sagemath.org/raw-attachment/ticket/'
1789
'http://nbviewer.jupyter.org/url/' :'http://'
1790
'http://nbviewer.jupyter.org/urls/' :'https://'
1791
if exports.startswith(url, "https://github.com/")
1792
if url.indexOf('/blob/') != -1
1793
url = url.replace("https://github.com", "https://raw.githubusercontent.com").replace("/blob/","/")
1794
# issue #1818: https://github.com/plotly/python-user-guide β†’ https://github.com/plotly/python-user-guide.git
1795
else if url.split('://')[1]?.split('/').length == 3
1796
url += '.git'
1797
1798
if exports.startswith(url, '[email protected]:')
1799
command = 'git' # kind of useless due to host keys...
1800
args = ['clone', url]
1801
else if url.slice(url.length-4) == ".git"
1802
command = 'git'
1803
args = ['clone', url]
1804
else
1805
# fall back
1806
for a,b of URL_TRANSFORMS
1807
url = url.replace(a,b) # only replaces first instance, unlike python. ok for us.
1808
# special case, this is only for nbviewer.../github/ URLs
1809
if exports.startswith(url, 'http://nbviewer.jupyter.org/github/')
1810
url = url.replace('http://nbviewer.jupyter.org/github/', 'https://raw.githubusercontent.com/')
1811
url = url.replace("/blob/","/")
1812
command = 'wget'
1813
args = [url]
1814
1815
return {command:command, args:args}
1816
1817
exports.ensure_bound = (x, min, max) ->
1818
return min if x < min
1819
return max if x > max
1820
return x
1821
1822
# convert a file path to the "name" of the underlying editor tab.
1823
# needed because otherwise filenames like 'log' would cause problems
1824
exports.path_to_tab = (name) ->
1825
"editor-#{name}"
1826
1827
# assumes a valid editor tab name...
1828
# If invalid or undefined, returns undefined
1829
exports.tab_to_path = (name) ->
1830
if name? and name.substring(0, 7) == "editor-"
1831
name.substring(7)
1832
1833
# suggest a new filename when duplicating it
1834
# 1. strip extension, split at '_' or '-' if it exists
1835
# try to parse a number, if it works, increment it, etc.
1836
exports.suggest_duplicate_filename = (name) ->
1837
{name, ext} = exports.separate_file_extension(name)
1838
idx_dash = name.lastIndexOf('-')
1839
idx_under = name.lastIndexOf('_')
1840
idx = exports.max([idx_dash, idx_under])
1841
new_name = null
1842
if idx > 0
1843
[prfx, ending] = [name[...idx+1], name[idx+1...]]
1844
num = parseInt(ending)
1845
if not Number.isNaN(num)
1846
new_name = "#{prfx}#{num+1}"
1847
new_name ?= "#{name}-1"
1848
if ext?.length > 0
1849
new_name += ".#{ext}"
1850
return new_name
1851
1852
1853
# Wrapper around localStorage, so we can safely touch it without raising an
1854
# exception if it is banned (like in some browser modes) or doesn't exist.
1855
# See https://github.com/sagemathinc/cocalc/issues/237
1856
1857
exports.set_local_storage = (key, val) ->
1858
try
1859
localStorage[key] = val
1860
catch e
1861
console.warn("localStorage set error -- #{e}")
1862
1863
exports.get_local_storage = (key) ->
1864
try
1865
return localStorage[key]
1866
catch e
1867
console.warn("localStorage get error -- #{e}")
1868
1869
1870
exports.delete_local_storage = (key) ->
1871
try
1872
delete localStorage[key]
1873
catch e
1874
console.warn("localStorage delete error -- #{e}")
1875
1876
1877
exports.has_local_storage = () ->
1878
try
1879
TEST = '__smc_test__'
1880
localStorage[TEST] = 'x'
1881
delete localStorage[TEST]
1882
return true
1883
catch e
1884
return false
1885
1886
exports.local_storage_length = () ->
1887
try
1888
return localStorage.length
1889
catch e
1890
return 0
1891
1892
# Takes an object representing a directed graph shaped as follows:
1893
# DAG =
1894
# node1 : []
1895
# node2 : ["node1"]
1896
# node3 : ["node1", "node2"]
1897
#
1898
# Which represents the following graph:
1899
# node1 ----> node2
1900
# | |
1901
# \|/ |
1902
# node3 <-------|
1903
#
1904
# Returns a topological ordering of the DAG
1905
# object = ["node1", "node2", "node3"]
1906
#
1907
# Throws an error if cyclic
1908
# Runs in O(N + E) where N is the number of nodes and E the number of edges
1909
# Kahn, Arthur B. (1962), "Topological sorting of large networks", Communications of the ACM
1910
exports.top_sort = (DAG, opts={omit_sources:false}) ->
1911
{omit_sources} = opts
1912
source_names = []
1913
num_edges = 0
1914
data = {}
1915
1916
# Ready the data for top sort
1917
for name, parents of DAG
1918
data[name] ?= {}
1919
node = data[name]
1920
node.name = name
1921
node.children ?= []
1922
node.parent_set = {}
1923
for parent_name in parents
1924
node.parent_set[parent_name] = true # include element in "parent_set" (see https://github.com/sagemathinc/cocalc/issues/1710)
1925
data[parent_name] ?= {}
1926
data[parent_name].children ?= []
1927
data[parent_name].children.push(node)
1928
if parents.length == 0
1929
source_names.push(name)
1930
else
1931
num_edges += parents.length
1932
1933
# Top sort! Non-recursive method since recursion is way slow in javascript
1934
path = []
1935
num_sources = source_names.length
1936
while source_names.length > 0
1937
curr_name = source_names.shift()
1938
path.push(curr_name)
1939
for child in data[curr_name].children
1940
delete child.parent_set[curr_name]
1941
num_edges -= 1
1942
if exports.len(child.parent_set) == 0
1943
source_names.push(child.name)
1944
1945
# Detect lack of sources
1946
if num_sources == 0
1947
throw new Error "No sources were detected"
1948
1949
# Detect cycles
1950
if num_edges != 0
1951
window?._DAG = DAG # so it's possible to debug in browser
1952
throw new Error "Store has a cycle in its computed values"
1953
1954
if omit_sources
1955
return path.slice(num_sources)
1956
else
1957
return path
1958
1959
# Takes an object with keys and values where
1960
# the values are functions and keys are the names
1961
# of the functions.
1962
# Dependency graph is created from the property
1963
# `dependency_names` found on the values
1964
# Returns an object shaped
1965
# DAG =
1966
# func_name1 : []
1967
# func_name2 : ["func_name1"]
1968
# func_name3 : ["func_name1", "func_name2"]
1969
#
1970
# Which represents the following graph:
1971
# func_name1 ----> func_name2
1972
# | |
1973
# \|/ |
1974
# func_name3 <-------|
1975
exports.create_dependency_graph = (object) =>
1976
DAG = {}
1977
for name, written_func of object
1978
DAG[name] = written_func.dependency_names ? []
1979
return DAG
1980
1981
# Binds all functions in objects of 'arr_objects' to 'scope'
1982
# Preserves all properties and the toString of these functions
1983
# Returns a new array of objects in the same order given
1984
# Leaves arr_objects unaltered.
1985
exports.bind_objects = (scope, arr_objects) ->
1986
return underscore.map arr_objects, (object) =>
1987
return underscore.mapObject object, (val) =>
1988
if typeof val == 'function'
1989
original_toString = val.toString()
1990
bound_func = val.bind(scope)
1991
bound_func.toString = () => original_toString
1992
Object.assign(bound_func, val)
1993
return bound_func
1994
else
1995
return val
1996
1997
# Remove all whitespace from string s.
1998
# see http://stackoverflow.com/questions/6623231/remove-all-white-spaces-from-text
1999
exports.remove_whitespace = (s) ->
2000
return s?.replace(/\s/g,'')
2001
2002
exports.is_whitespace = (s) ->
2003
return s?.trim().length == 0
2004
2005
exports.lstrip = (s) ->
2006
return s?.replace(/^\s*/g, "")
2007
2008
exports.rstrip = (s) ->
2009
return s?.replace(/\s*$/g, "")
2010
2011
# ORDER MATTERS! -- this gets looped over and searches happen -- so the 1-character ops must be last.
2012
exports.operators = ['!=', '<>', '<=', '>=', '==', '<', '>', '=']
2013
2014
exports.op_to_function = (op) ->
2015
switch op
2016
when '=', '=='
2017
return (a,b) -> a == b
2018
when '!=', '<>'
2019
return (a,b) -> a != b
2020
when '<='
2021
return (a,b) -> a <= b
2022
when '>='
2023
return (a,b) -> a >= b
2024
when '<'
2025
return (a,b) -> a < b
2026
when '>'
2027
return (a,b) -> a > b
2028
else
2029
throw Error("operator must be one of '#{JSON.stringify(exports.operators)}'")
2030
2031
# modify obj in place substituting keys as given.
2032
exports.obj_key_subs = (obj, subs) ->
2033
for k, v of obj
2034
s = subs[k]
2035
if s?
2036
delete obj[k]
2037
obj[s] = v
2038
if typeof(v) == 'object'
2039
exports.obj_key_subs(v, subs)
2040
else if typeof(v) == 'string'
2041
s = subs[v]
2042
if s?
2043
obj[k] = s
2044
2045
# this is a helper for sanitizing html. It is used in
2046
# * smc-util-node/misc_node β†’ sanitize_html
2047
# * smc-webapp/misc_page β†’ sanitize_html
2048
exports.sanitize_html_attributes = ($, node) ->
2049
$.each node.attributes, ->
2050
attrName = this.name
2051
attrValue = this.value
2052
# remove attribute name start with "on", possible unsafe, e.g.: onload, onerror...
2053
# remove attribute value start with "javascript:" pseudo protocol, possible unsafe, e.g. href="javascript:alert(1)"
2054
if attrName?.indexOf('on') == 0 or attrValue?.indexOf('javascript:') == 0
2055
$(node).removeAttr(attrName)
2056
2057
2058