popoverTemplate = require 'templates/play/level/tome/spell_palette_entry_popover' {downTheChain} = require 'lib/world/world_utils' window.Vector = require 'lib/world/vector' # So we can document it utils = require 'core/utils' safeJSONStringify = (input, maxDepth) -> recursion = (input, path, depth) -> output = {} pPath = undefined refIdx = undefined path = path or '' depth = depth or 0 depth++ return '{depth over ' + maxDepth + '}' if maxDepth and depth > maxDepth for p of input pPath = ((if path then (path + '.') else '')) + p if typeof input[p] is 'function' output[p] = '{function}' else if typeof input[p] is 'object' refIdx = refs.indexOf(input[p]) if -1 isnt refIdx output[p] = '{reference to ' + refsPaths[refIdx] + '}' else refs.push input[p] refsPaths.push pPath output[p] = recursion(input[p], pPath, depth) else output[p] = input[p] output refs = [] refsPaths = [] maxDepth = maxDepth or 5 if typeof input is 'object' output = recursion(input) else output = input JSON.stringify output, null, 1 module.exports = class DocFormatter constructor: (@options) -> @doc = _.cloneDeep @options.doc @fillOutDoc() fillOutDoc: -> if _.isString @doc @doc = name: @doc, type: typeof @options.thang[@doc] if @options.isSnippet @doc.type = 'snippet' @doc.owner = 'snippets' @doc.shortName = @doc.shorterName = @doc.title = @doc.name else @doc.owner ?= 'this' ownerName = @doc.ownerName = if @doc.owner isnt 'this' then @doc.owner else switch @options.language when 'python', 'lua' then (if @options.useHero then 'hero' else 'self') when 'java' then 'hero' when 'coffeescript' then '@' else (if @options.useHero then 'hero' else 'this') if @doc.type is 'function' [docName, args] = @getDocNameAndArguments() argNames = args.join ', ' argString = if argNames then '__ARGS__' else '' @doc.shortName = switch @options.language when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{docName}#{if argString then ' ' + argString else '()'}" when 'python' then "#{ownerName}.#{docName}(#{argString})" when 'lua' then "#{ownerName}:#{docName}(#{argString})" else "#{ownerName}.#{docName}(#{argString});" else @doc.shortName = switch @options.language when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{@doc.name}" when 'python' then "#{ownerName}.#{@doc.name}" when 'lua' then "#{ownerName}.#{@doc.name}" else "#{ownerName}.#{@doc.name};" @doc.shorterName = @doc.shortName if @doc.type is 'function' and argString @doc.shortName = @doc.shorterName.replace argString, argNames @doc.shorterName = @doc.shorterName.replace argString, (if not /cast[A-Z]/.test(@doc.name) and argNames.length > 6 then '...' else argNames) if @options.language is 'javascript' @doc.shorterName = @doc.shortName.replace ';', '' if @doc.owner is 'this' or @options.tabbify @doc.shorterName = @doc.shorterName.replace /^(this|hero)\./, '' else if (@options.language in ['python', 'lua']) and (@doc.owner is 'this' or @options.tabbify) @doc.shorterName = @doc.shortName.replace /^(self|hero)[:.]/, '' @doc.title = if @options.shortenize then @doc.shorterName else @doc.shortName translatedName = utils.i18n(@doc, 'name') if translatedName isnt @doc.name @doc.translatedShortName = @doc.shortName.replace(@doc.name, translatedName) # Grab the language-specific documentation for some sub-properties, if we have it. toTranslate = [{obj: @doc, prop: 'description'}, {obj: @doc, prop: 'example'}] for arg in (@doc.args ? []) toTranslate.push {obj: arg, prop: 'example'}, {obj: arg, prop: 'description'} if @doc.returns toTranslate.push {obj: @doc.returns, prop: 'example'}, {obj: @doc.returns, prop: 'description'} for {obj, prop} in toTranslate # Translate into chosen code language. if val = obj[prop]?[@options.language] obj[prop] = val else unless _.isString obj[prop] obj[prop] = null # Translate into chosen spoken language. if val = originalVal = obj[prop] context = @doc.context obj[prop] = val = utils.i18n obj, prop # For multiplexed-by-both-code-and-spoken-language objects, now also get code language again. if _.isObject(val) if valByCodeLanguage = obj[prop]?[@options.language] obj[prop] = val = valByCodeLanguage else obj[prop] = originalVal # Never mind, we don't have that code language for that spoken language. if @doc.i18n spokenLanguage = me.get 'preferredLanguage' while spokenLanguage spokenLanguage = spokenLanguage.substr 0, spokenLanguage.lastIndexOf('-') if fallingBack? if spokenLanguageContext = @doc.i18n[spokenLanguage]?.context context = _.merge context, spokenLanguageContext break fallingBack = true if context try obj[prop] = _.template val, context catch e console.error "Couldn't create docs template of", val, "\nwith context", context, "\nError:", e obj[prop] = @replaceSpriteName obj[prop] # Do this before using the template, otherwise marked might get us first. # Temporary hack to replace self|this with hero until we can update the docs if @options.useHero thisToken = 'python': /self/g, 'javascript': /this/g, 'lua': /self/g if thisToken[@options.language] if @doc.example @doc.example = @doc.example.replace thisToken[@options.language], 'hero' if @doc.snippets?[@options.language]?.code @doc.snippets[@options.language].code.replace thisToken[@options.language], 'hero' if @doc.args arg.example = arg.example.replace thisToken[@options.language], 'hero' for arg in @doc.args when arg.example if @doc.shortName is 'loop' and @options.level.isType('course', 'course-ladder') @replaceSimpleLoops() replaceSimpleLoops: -> # Temporary hackery to make it look like we meant while True: in our loop: docs until we can update everything @doc.shortName = @doc.shorterName = @doc.title = @doc.name = switch @options.language when 'coffeescript' then "loop" when 'python' then "while True:" when 'lua' then "while true do" else "while (true)" for field in ['example', 'description'] [simpleLoop, whileLoop] = switch @options.language when 'coffeescript' then [/loop/g, "loop"] when 'python' then [/loop:/g, "while True:"] when 'lua' then [/loop/g, "while true do"] else [/loop/g, "while (true)"] @doc[field] = @doc[field].replace simpleLoop, whileLoop formatPopover: -> [docName, args] = @getDocNameAndArguments() argumentExamples = (arg.example or arg.default or arg.name for arg in @doc.args ? []) argumentExamples.unshift args[0] if args.length > argumentExamples.length content = popoverTemplate doc: @doc, docName: docName, language: @options.language, value: @formatValue(), marked: marked, argumentExamples: argumentExamples, writable: @options.writable, selectedMethod: @options.selectedMethod, cooldowns: @inferCooldowns(), item: @options.item owner = if @doc.owner is 'this' then @options.thang else window[@doc.owner] content = @replaceSpriteName content content = content.replace /\#\{(.*?)\}/g, (s, properties) => @formatValue downTheChain(owner, properties.split('.')) content = content.replace /{([a-z]+)}([^]*?){\/\1}/g, (s, language, text) => if language is @options.language then return text return '' replaceSpriteName: (s) -> # Prefer type, and excluded the quotes we'd get with @formatValue name = @options.thang.type ? @options.thang.spriteName name = 'hero' if /Hero Placeholder/.test @options.thang.id s.replace /#{spriteName}/g, name getDocNameAndArguments: -> return [@doc.name, []] unless @doc.type is 'function' docName = @doc.name args = (arg.name for arg in @doc.args ? []) if /cast[A-Z]/.test docName docName = 'cast' args.unshift '"' + _.string.dasherize(@doc.name).replace('cast-', '') + '"' [docName, args] formatValue: (v) -> return null if @doc.type is 'snippet' return @options.thang.now() if @doc.name is 'now' return '[Function]' if not v and @doc.type is 'function' unless v? if @doc.owner is 'this' v = @options.thang[@doc.name] else v = window[@doc.owner][@doc.name] # grab Math or Vector if @doc.type is 'number' and not _.isNaN v if v is Math.round v return v if _.isNumber v return v.toFixed 2 unless v return 'null' return '' + v if _.isString v return "\"#{v}\"" if v?.id return v.id if v?.name return v.name if _.isArray v return '[' + (@formatValue v2 for v2 in v).join(', ') + ']' if _.isPlainObject v return safeJSONStringify v, 2 v inferCooldowns: -> return null unless @doc.type is 'function' and @doc.owner is 'this' owner = @options.thang cooldowns = null spellName = @doc.name.match /^cast(.+)$/ if spellName actionName = _.string.slugify _.string.underscored spellName[1] action = owner.spells?[actionName] type = 'spell' else actionName = _.string.slugify _.string.underscored @doc.name action = owner.actions?[actionName] type = 'action' return null unless action cooldowns = cooldown: action.cooldown, specificCooldown: action.specificCooldown, name: actionName, type: type for prop in ['range', 'radius', 'duration', 'damage'] v = owner[_.string.camelize actionName + _.string.capitalize(prop)] continue if prop is 'range' and v <= 5 # Don't confuse players by showing melee ranges, they will inappropriately use distanceTo(enemy) < 3. cooldowns[prop] = v if _.isNumber(v) and v isnt Math.round v cooldowns[prop] = v.toFixed 2 cooldowns