codecombat/app/core/utils.coffee

303 lines
11 KiB
CoffeeScript

module.exports.clone = (obj) ->
return obj if obj is null or typeof (obj) isnt 'object'
temp = obj.constructor()
for key of obj
temp[key] = module.exports.clone(obj[key])
temp
module.exports.combineAncestralObject = (obj, propertyName) ->
combined = {}
while obj?[propertyName]
for key, value of obj[propertyName]
continue if combined[key]
combined[key] = value
if obj.__proto__
obj = obj.__proto__
else
# IE has no __proto__. TODO: does this even work? At most it doesn't crash.
obj = Object.getPrototypeOf(obj)
combined
module.exports.normalizeFunc = (func_thing, object) ->
# func could be a string to a function in this class
# or a function in its own right
object ?= {}
if _.isString(func_thing)
func = object[func_thing]
if not func
console.error "Could not find method #{func_thing} in object", object
return => null # always return a func, or Mediator will go boom
func_thing = func
return func_thing
module.exports.objectIdToDate = (objectID) ->
new Date(parseInt(objectID.toString().slice(0,8), 16)*1000)
module.exports.hexToHSL = (hex) ->
rgbToHsl(hexToR(hex), hexToG(hex), hexToB(hex))
hexToR = (h) -> parseInt (cutHex(h)).substring(0, 2), 16
hexToG = (h) -> parseInt (cutHex(h)).substring(2, 4), 16
hexToB = (h) -> parseInt (cutHex(h)).substring(4, 6), 16
cutHex = (h) -> (if (h.charAt(0) is '#') then h.substring(1, 7) else h)
module.exports.hslToHex = (hsl) ->
'#' + (toHex(n) for n in hslToRgb(hsl...)).join('')
toHex = (n) ->
h = Math.floor(n).toString(16)
h = '0'+h if h.length is 1
h
module.exports.i18n = (say, target, language=me.get('preferredLanguage', true), fallback='en') ->
generalResult = null
fallBackResult = null
fallForwardResult = null # If a general language isn't available, the first specific one will do.
fallSidewaysResult = null # If a specific language isn't available, its sibling specific language will do.
matches = (/\w+/gi).exec(language)
generalName = matches[0] if matches
for localeName, locale of say.i18n
continue if localeName is '-'
if target of locale
result = locale[target]
else continue
return result if localeName is language
generalResult = result if localeName is generalName
fallBackResult = result if localeName is fallback
fallForwardResult = result if localeName.indexOf(language) is 0 and not fallForwardResult?
fallSidewaysResult = result if localeName.indexOf(generalName) is 0 and not fallSidewaysResult?
return generalResult if generalResult?
return fallForwardResult if fallForwardResult?
return fallSidewaysResult if fallSidewaysResult?
return fallBackResult if fallBackResult?
return say[target] if target of say
null
module.exports.getByPath = (target, path) ->
throw new Error 'Expected an object to match a query against, instead got null' unless target
pieces = path.split('.')
obj = target
for piece in pieces
return undefined unless piece of obj
obj = obj[piece]
obj
module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
module.exports.round = _.curry (digits, n) ->
n = +n.toFixed(digits)
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
# f(x) = ax + b
createLinearFunc = (params) ->
(x) -> (params.a or 1) * x + (params.b or 0)
# f(x) = ax² + bx + c
createQuadraticFunc = (params) ->
(x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0)
# f(x) = a log(b (x + c)) + d
createLogFunc = (params) ->
(x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0
# f(x) = ax^b + c
createPowFunc = (params) ->
(x) -> (params.a or 1) * Math.pow(x, params.b or 1) + (params.c or 0)
module.exports.functionCreators =
linear: positify(createLinearFunc)
quadratic: positify(createQuadraticFunc)
logarithmic: positify(createLogFunc)
pow: positify(createPowFunc)
# Call done with true to satisfy the 'until' goal and stop repeating func
module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) ->
waitSoFar = 0
(done = (success) ->
if (waitSoFar += wait) <= totalWait and not success
_.delay (-> func done), wait) false
module.exports.grayscale = (imageData) ->
d = imageData.data
for i in [0..d.length] by 4
r = d[i]
g = d[i+1]
b = d[i+2]
v = 0.2126*r + 0.7152*g + 0.0722*b
d[i] = d[i+1] = d[i+2] = v
imageData
# Deep compares l with r, with the exception that undefined values are considered equal to missing values
# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted
module.exports.kindaEqual = compare = (l, r) ->
if _.isObject(l) and _.isObject(r)
for key in _.union Object.keys(l), Object.keys(r)
return false unless compare l[key], r[key]
return true
else if l is r
return true
else
return false
# Return UTC string "YYYYMMDD" for today + offset
module.exports.getUTCDay = (offset=0) ->
day = new Date()
day.setDate(day.getUTCDate() + offset)
partYear = day.getUTCFullYear()
partMonth = (day.getUTCMonth() + 1)
partMonth = "0" + partMonth if partMonth < 10
partDay = day.getUTCDate()
partDay = "0" + partDay if partDay < 10
"#{partYear}#{partMonth}#{partDay}"
# Fast, basic way to replace text in an element when you don't need much.
# http://stackoverflow.com/a/4962398/540620
if document?.createElement
dummy = document.createElement 'div'
dummy.innerHTML = 'text'
TEXT = if dummy.textContent is 'text' then 'textContent' else 'innerText'
module.exports.replaceText = (elems, text) ->
elem[TEXT] = text for elem in elems
null
# Add a stylesheet rule
# http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript/26230472#26230472
# Don't use wantonly, or we'll have to implement a simple mechanism for clearing out old rules.
if document?.createElement
module.exports.injectCSS = ((doc) ->
# wrapper for all injected styles and temp el to create them
wrap = doc.createElement("div")
temp = doc.createElement("div")
# rules like "a {color: red}" etc.
return (cssRules) ->
# append wrapper to the body on the first call
unless wrap.id
wrap.id = "injected-css"
wrap.style.display = "none"
doc.body.appendChild wrap
# <br> for IE: http://goo.gl/vLY4x7
temp.innerHTML = "<br><style>" + cssRules + "</style>"
wrap.appendChild temp.children[1]
return
)(document)
module.exports.getQueryVariable = getQueryVariable = (param, defaultValue) ->
query = document.location.search.substring 1
pairs = (pair.split('=') for pair in query.split '&')
for pair in pairs when pair[0] is param
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
defaultValue
module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, subCount=0, personalSub=false) ->
# 1 100%
# 2-11 80%
# 12+ 60%
# TODO: make this less confusing
return 0 unless subCount > 0
offset = if personalSub then 1 else 0
if subCount <= 1 - offset
price
else if subCount <= 11 - offset
Math.round((1 - offset) * price + (subCount - 1 + offset) * price * 0.8)
else
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
module.exports.getCourseBundlePrice = getCourseBundlePrice = (coursePrices, seats=20) ->
totalPricePerSeat = coursePrices.reduce ((a, b) -> a + b), 0
if coursePrices.length > 2
pricePerSeat = Math.round(totalPricePerSeat / 2.0)
else
pricePerSeat = parseInt(totalPricePerSeat)
seats * pricePerSeat
module.exports.getCoursePraise = getCoursePraise = ->
praise = [
{
quote: "The kids love it."
source: "Leo Joseph Tran, Athlos Leadership Academy"
},
{
quote: "My students have been using the site for a couple of weeks and they love it."
source: "Scott Hatfield, Computer Applications Teacher, School Technology Coordinator, Eastside Middle School"
},
{
quote: "Thanks for the captivating site. My eighth graders love it."
source: "Janet Cook, Ansbach Middle/High School"
},
{
quote: "My students have started working on CodeCombat and love it! I love that they are learning coding and problem solving skills without them even knowing it!!"
source: "Kristin Huff, Special Education Teacher, Webb City School District"
},
{
quote: "I recently introduced Code Combat to a few of my fifth graders and they are loving it!"
source: "Shauna Hamman, Fifth Grade Teacher, Four Peaks Elementary School"
},
{
quote: "Overall I think it's a fantastic service. Variables, arrays, loops, all covered in very fun and imaginative ways. Every kid who has tried it is a fan."
source: "Aibinder Andrew, Technology Teacher"
},
{
quote: "I love what you have created. The kids are so engaged."
source: "Desmond Smith, 4KS Academy"
},
{
quote: "My students love the website and I hope on having content structured around it in the near future."
source: "Michael Leonard, Science Teacher, Clearwater Central Catholic High School"
}
]
praise[_.random(0, praise.length - 1)]
module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=0, users=0, months=0) ->
return 0 unless users > 0 and months > 0
total = price * users * months
total
module.exports.filterMarkdownCodeLanguages = (text, language) ->
return '' unless text
currentLanguage = language or me.get('aceConfig')?.language or 'python'
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io'], currentLanguage
# Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language.
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
# Exclude language-specific images like ![python - image description](image url) for each non-target language.
imageExclusionRegex = new RegExp "!\\[(#{excludedLanguages.join('|')}) - .+?\\]\\(.+?\\)\n?", 'gm'
return text.replace(codeBlockExclusionRegex, '').replace(imageExclusionRegex, '')
module.exports.aceEditModes = aceEditModes =
'javascript': 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee'
'python': 'ace/mode/python'
'java': 'ace/mode/java'
'lua': 'ace/mode/lua'
'java': 'ace/mode/java'
module.exports.initializeACE = (el, codeLanguage) ->
contents = $(el).text().trim()
editor = ace.edit el
editor.setOptions maxLines: Infinity
editor.setReadOnly true
editor.setTheme 'ace/theme/textmate'
editor.setShowPrintMargin false
editor.setShowFoldWidgets false
editor.setHighlightActiveLine false
editor.setHighlightActiveLine false
editor.setBehavioursEnabled false
editor.renderer.setShowGutter false
editor.setValue contents
editor.clearSelection()
session = editor.getSession()
session.setUseWorker false
session.setMode aceEditModes[codeLanguage]
session.setWrapLimitRange null
session.setUseWrapMode true
session.setNewLineMode 'unix'
return editor
module.exports.capitalLanguages = capitalLanguages =
'javascript': 'JavaScript'
'coffeescript': 'CoffeeScript'
'python': 'Python'
'java': 'Java'
'lua': 'Lua'