Improved per-language spell palette and tab documentation.

This commit is contained in:
Nick Winter 2014-06-25 22:56:39 -07:00
parent 8c6ee66684
commit aee95ac5c0
11 changed files with 263 additions and 220 deletions

View file

@ -26,7 +26,16 @@ PropertyDocumentationSchema = c.object {
name: {type: 'string', title: "Name", description: "Name of the property."}
# not actual JS types, just whatever they describe...
type: c.shortString(title: "Type", description: "Intended type of the property.")
description: {title: "Description", type: 'string', description: "Description of the property.", format: 'markdown', maxLength: 1000}
description:
oneOf: [
{title: "Description", type: 'string', description: "Description of the property.", maxLength: 1000, format: 'markdown'}
{
type: 'object',
title: "Language Descriptions",
description: "Property descriptions by code language.",
additionalProperties: {type: 'string', description: "Description of the property.", maxLength: 1000, format: 'markdown'}
}
]
args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema
owner: {title: "Owner", type: 'string', description: 'Owner of the property, like "this" or "Math".'}
example:
@ -46,8 +55,26 @@ PropertyDocumentationSchema = c.object {
default: {type: 'null'}
},
type: c.shortString(title: "Type", description: "Type of the return value")
example: c.shortString(title: "Example", description: "Example return value")
description: {title: "Description", type: 'string', description: "Description of the return value.", maxLength: 1000}
example:
oneOf: [
c.shortString(title: "Example", description: "Example return value")
{
type: 'object',
title: "Language Examples",
description: "Example return values by code language.",
additionalProperties: c.shortString(description: 'Example return value.', format: 'javascript') # TODO: not JS
}
]
description:
oneOf: [
{title: "Description", type: 'string', description: "Description of the return value.", maxLength: 1000}
{
type: 'object',
title: "Language Descriptions",
description: "Example return values by code language.",
additionalProperties: {type: 'string', description: "Description of the return value.", maxLength: 1000}
}
]
DependencySchema = c.object {
title: "Component Dependency"

View file

@ -164,8 +164,26 @@ me.FunctionArgumentSchema = me.object {
name: {type: 'string', pattern: me.identifierPattern, title: "Name", description: "Name of the function argument."}
# not actual JS types, just whatever they describe...
type: me.shortString(title: "Type", description: "Intended type of the argument.")
example: me.shortString(title: "Example", description: "Example value for the argument.")
description: {title: "Description", type: 'string', description: "Description of the argument.", maxLength: 1000}
example:
oneOf: [
me.shortString(title: "Example", description: "Example value for the argument.")
{
type: 'object',
title: "Language Examples",
description: "Examples by code language.",
additionalProperties: me.shortString(description: 'Example value for the argument.')
}
]
description:
oneOf: [
{title: "Description", type: 'string', description: "Description of the argument.", maxLength: 1000}
{
type: 'object',
title: "Language Descriptions",
description: "Example argument descriptions by code language.",
additionalProperties: {type: 'string', description: "Description of the argument.", maxLength: 1000}
}
]
"default":
title: "Default"
description: "Default value of the argument. (Your code should set this.)"

View file

@ -90,4 +90,6 @@ module.exports =
properties:
spell:
type: "object"
language:
type: "string"
required: ["spell"]

View file

@ -4,54 +4,59 @@ block content
h1#site-slogan(data-i18n="home.slogan") Learn to Code by Playing a Game
.code-languages
.primary-code-languages.row
.col-md-6
.code-language#javascript(data-code-language='javascript')
.code-wizard
h2 JavaScript
p The language of the web. Great for writing websites, web apps, HTML5 games, and servers.
.col-md-6
.code-language.beta#python(data-code-language='python')
.code-wizard
.code-language-beta
h2 Python
p Simple yet powerful, Python is a great general purpose programming language.
.secondary-code-languages.row
.col-md-3
.code-language.beta#coffeescript(data-code-language='coffeescript')
.code-language-logo
.code-wizard
.code-language-beta
h3 CoffeeScript
p Nicer JavaScript syntax
.col-md-3
.code-language.beta#clojure(data-code-language='clojure')
.code-language-logo
.code-wizard
.code-language-beta
h3 Clojure
p A modern Lisp
.col-md-3
.code-language.beta#lua(data-code-language='lua')
.code-language-logo
.code-wizard
.code-language-beta
h3 Lua
p Game scripting language
.col-md-3
.code-language.beta#io(data-code-language='io')
.code-language-logo
.code-wizard
.code-language-beta
h3 Io
p Simple but obscure
if me.isAdmin()
// Admin gate until fully tested
.code-languages
.primary-code-languages.row
.col-md-6
.code-language#javascript(data-code-language='javascript')
.code-wizard
h2 JavaScript
p The language of the web. Great for writing websites, web apps, HTML5 games, and servers.
.col-md-6
.code-language.beta#python(data-code-language='python')
.code-wizard
.code-language-beta
h2 Python
p Simple yet powerful, Python is a great general purpose programming language.
.secondary-code-languages.row
.col-md-3
.code-language.beta#coffeescript(data-code-language='coffeescript')
.code-language-logo
.code-wizard
.code-language-beta
h3 CoffeeScript
p Nicer JavaScript syntax
.col-md-3
.code-language.beta#clojure(data-code-language='clojure')
.code-language-logo
.code-wizard
.code-language-beta
h3 Clojure
p A modern Lisp
.col-md-3
.code-language.beta#lua(data-code-language='lua')
.code-language-logo
.code-wizard
.code-language-beta
h3 Lua
p Game scripting language
.col-md-3
.code-language.beta#io(data-code-language='io')
.code-language-logo
.code-wizard
.code-language-beta
h3 Io
p Simple but obscure
else
// Old
#front-screenshot
img(src="/images/pages/home/front_screenshot_01.png", alt="")
.alert.alert-danger.lt-ie10
strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 9 or older. Sorry!

View file

@ -11,11 +11,22 @@ if doc.example
p.example
strong Example:
div!= marked("```\n" + doc.example + "```")
else if doc.type == 'function'
else if doc.type == 'function' && argumentExamples.length
p.example
strong Example:
div
code= doc.owner + '.' + doc.name + '(' + argumentExamples.join(', ') + ');'
if language == 'javascript'
code= doc.owner + '.' + doc.name + '(' + argumentExamples.join(', ') + ');'
else if language == 'coffeescript'
code= doc.ownerName + (doc.ownerName == '@' ? '' : '.') + doc.name + ' ' + argumentExamples.join(', ')
else if language == 'python'
code= doc.ownerName + '.' + doc.name + '(' + argumentExamples.join(', ') + ')'
else if language == 'clojure'
code= '(.' + doc.name + ' ' + doc.ownerName + ' ' + argumentExamples.join(', ') + ')'
else if language == 'lua'
code= doc.ownerName + ':' + doc.name + '(' + argumentExamples.join(', ') + ')'
else if language == 'io'
code= (doc.ownerName == 'this' ? '' : doc.ownerName + ' ') + doc.name + '(' + argumentExamples.join(', ') + ')'
if (doc.type != 'function' && doc.type != 'snippet') || doc.name == 'now'
p.value

View file

@ -0,0 +1,124 @@
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
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 'self'
when 'coffeescript' then '@'
else 'this'
if @doc.type is 'function'
sep = {clojure: ' '}[@options.language] ? ', '
argNames = (arg.name for arg in @doc.args ? []).join sep
argString = if argNames then '__ARGS__' else ''
@doc.shortName = switch @options.language
when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{@doc.name}#{if argString then ' ' + argString else '()'}"
when 'python' then "#{ownerName}.#{@doc.name}(#{argString})"
when 'lua' then "#{ownerName}:#{@doc.name}(#{argString})"
when 'clojure' then "(.#{@doc.name} #{ownerName}#{if argNames then ' ' + argString else ''})"
when 'io' then "#{if ownerName is 'this' then '' else ownerName + ' '}#{@doc.name}#{if argNames then '(' + argNames + ')' else ''}"
else "#{ownerName}.#{@doc.name}(#{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}"
when 'clojure' then "(.#{@doc.name} #{ownerName})"
when 'io' then "#{if ownerName is 'this' then '' else 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 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\./, ''
@doc.title = if @options.shortenize then @doc.shorterName else @doc.shortName
# Grab the language-specific documentation for some sub-properties, if we have it.
toTranslate = [{obj: @doc, prop: 'example'}, {obj: @doc, prop: 'returns'}]
for arg in (@doc.args ? [])
toTranslate.push {obj: arg, prop: 'example'}, {obj: arg, prop: 'description'}
for {obj, prop} in toTranslate
if val = obj[prop]?[@options.language]
obj[prop] = val
else unless _.isString obj[prop]
obj[prop] = null
formatPopover: ->
content = popoverTemplate doc: @doc, language: @options.language, value: @formatValue(), marked: marked, argumentExamples: (arg.example or arg.default or arg.name for arg in @doc.args ? [])
owner = if @doc.owner is 'this' then @options.thang else window[@doc.owner]
content = content.replace /#{spriteName}/g, @options.thang.type ? @options.thang.spriteName # Prefer type, and excluded the quotes we'd get with @formatValue
content.replace /\#\{(.*?)\}/g, (s, properties) => @formatValue downTheChain(owner, properties.split('.'))
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 == Math.round v
return v
return v.toFixed 2
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

View file

@ -34,7 +34,7 @@ module.exports = class Spell
@thangs = {}
@view = new SpellView {spell: @, session: @session, worker: @worker}
@view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, language: @language
@tabView.render()
@team = @permissions.readwrite[0] ? "common"
Backbone.Mediator.publish 'tome:spell-created', spell: @
@ -146,7 +146,7 @@ module.exports = class Spell
for thangId, spellThang of @thangs
spellThang.aether?.setLanguage @language
spellThang.castAether = null
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
workerMessage =
function: "updateLanguageAether"
newLanguage: @language

View file

@ -1,9 +1,8 @@
SpellListEntryView = require './spell_list_entry_view'
ThangAvatarView = require 'views/play/level/thang_avatar_view'
template = require 'templates/play/level/tome/spell_list_tab_entry'
popoverTemplate = require 'templates/play/level/tome/spell_palette_entry_popover'
LevelComponent = require 'models/LevelComponent'
{downTheChain} = require 'lib/world/world_utils'
DocFormatter = require './doc_formatter'
module.exports = class SpellListTabEntryView extends SpellListEntryView
template: template
@ -13,6 +12,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
'tome:spell-loaded': "onSpellLoaded"
'tome:spell-changed': "onSpellChanged"
'god:new-world-created': 'onNewWorld'
'tome:spell-changed-language': 'onSpellChangedLanguage'
events:
'click .spell-list-button': 'onDropdownClick'
@ -60,43 +60,16 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
found = true
break
return unless found
doc = _.clone doc
doc.owner = 'this'
doc.shortName = doc.shorterName = doc.title = "this.#{doc.name}();"
docFormatter = new DocFormatter doc: doc, thang: @thang, language: @options.language
@$el.find('code').popover(
animation: true
html: true
placement: 'bottom'
trigger: 'hover'
content: @formatPopover doc
content: docFormatter.formatPopover()
container: @$el.parent()
)
formatPopover: (doc) ->
content = popoverTemplate doc: doc, marked: marked, argumentExamples: (arg.example or arg.default or arg.name for arg in doc.args ? [])
owner = @thang
content = content.replace /#{spriteName}/g, @thang.type ? @thang.spriteName # Prefer type, and excluded the quotes we'd get with @formatValue
content.replace /\#\{(.*?)\}/g, (s, properties) => @formatValue downTheChain(owner, properties.split('.'))
formatValue: (v) ->
# TODO: refactor and move spell_palette_entry_view version of this somewhere else
# maybe think about making it common with what Aether does and the SpellDebugView, too
if _.isNumber v
if v == Math.round v
return v
return v.toFixed 2
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
onMouseEnterAvatar: (e) -> # Don't call super
onMouseLeaveAvatar: (e) -> # Don't call super
onClick: (e) -> # Don't call super
@ -125,6 +98,14 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
return unless e.spell is @spell
@updateReloadButton()
onSpellChangedLanguage: (e) ->
return unless e.spell is @spell
@options.language = e.language
@$el.find('code').popover 'destroy'
@render()
@docsBuilt = false
@buildDocs() if @thang
toggleControls: (e, enabled) ->
# Don't call super; do it differently
return if e.controls and not ('editor' in e.controls)

View file

@ -1,57 +1,8 @@
View = require 'views/kinds/CocoView'
template = require 'templates/play/level/tome/spell_palette_entry'
popoverTemplate = require 'templates/play/level/tome/spell_palette_entry_popover'
{me} = require 'lib/auth'
filters = require 'lib/image_filter'
{downTheChain} = require 'lib/world/world_utils'
window.Vector = require 'lib/world/vector' # So we can document it
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
# http://stackoverflow.com/a/987376/540620
$.fn.selectText = ->
el = @[0]
if document.body.createTextRange
range = document.body.createTextRange()
range.moveToElementText(el)
range.select()
else if window.getSelection
selection = window.getSelection()
range = document.createRange()
range.selectNodeContents(el)
selection.removeAllRanges()
selection.addRange(range)
DocFormatter = require './doc_formatter'
module.exports = class SpellPaletteEntryView extends View
tagName: 'div' # Could also try <code> instead of <div>, but would need to adjust colors
@ -73,52 +24,8 @@ module.exports = class SpellPaletteEntryView extends View
constructor: (options) ->
super options
@thang = options.thang
@doc = options.doc
if _.isString @doc
@doc = name: @doc, type: typeof @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 'self'
when 'coffeescript' then '@'
else 'this'
if @doc.type is 'function'
sep = {clojure: ' '}[options.language] ? ', '
argNames = (arg.name for arg in @doc.args ? []).join sep
argString = if argNames then '__ARGS__' else ''
@doc.shortName = switch options.language
when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{@doc.name}#{if argString then ' ' + argString else '()'}"
when 'python' then "#{ownerName}.#{@doc.name}(#{argString})"
when 'lua' then "#{ownerName}:#{@doc.name}(#{argString})"
when 'clojure' then "(#{@doc.name} #{ownerName}#{if argNames then ' ' + argString else ''})"
when 'io' then "#{if ownerName is 'this' then '' else ownerName + ' '}#{@doc.name}#{if argNames then '(' + argNames + ')' else ''}"
else "#{ownerName}.#{@doc.name}(#{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}"
when 'clojure' then "(#{@doc.name} #{ownerName})"
when 'io' then "#{if ownerName is 'this' then '' else 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 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\./, ''
@doc.title = if options.shortenize then @doc.shorterName else @doc.shortName
if example = @doc.example?[options.language]
@doc.example = example
else unless _.isString @doc.example
@doc.example = null
@docFormatter = new DocFormatter options
@doc = @docFormatter.doc
getRenderData: ->
c = super()
@ -133,46 +40,15 @@ module.exports = class SpellPaletteEntryView extends View
html: true
placement: 'top'
trigger: 'manual' # Hover, until they click, which will then pin it until unclick.
content: @formatPopover()
content: @docFormatter.formatPopover()
container: '#tome-view'
)
@$el.on 'show.bs.popover', =>
Backbone.Mediator.publish 'tome:palette-hovered', thang: @thang, prop: @doc.name, entry: @
formatPopover: ->
content = popoverTemplate doc: @doc, value: @formatValue(), marked: marked, argumentExamples: (arg.example or arg.default or arg.name for arg in @doc.args ? [])
owner = if @doc.owner is 'this' then @thang else window[@doc.owner]
content = content.replace /#{spriteName}/g, @thang.type ? @thang.spriteName # Prefer type, and excluded the quotes we'd get with @formatValue
content.replace /\#\{(.*?)\}/g, (s, properties) => @formatValue downTheChain(owner, properties.split('.'))
formatValue: (v) ->
return null if @doc.type is 'snippet'
return @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 = @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 == Math.round v
return v
return v.toFixed 2
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
onMouseEnter: (e) ->
# Make sure the doc has the updated Thang so it can regenerate its prop value
@$el.data('bs.popover').options.content = @formatPopover()
@$el.data('bs.popover').options.content = @docFormatter.formatPopover()
@$el.popover('setContent')
@$el.popover 'show' unless @popoverPinned or @otherPopoverPinned
@ -200,7 +76,7 @@ module.exports = class SpellPaletteEntryView extends View
onFrameChanged: (e) ->
return unless e.selectedThang?.id is @thang.id
@options.thang = @thang = e.selectedThang # Update our thang to the current version
@options.thang = @thang = @docFormatter.options.thang = e.selectedThang # Update our thang to the current version
onPaletteHovered: (e) ->
return if e.entry is @

View file

@ -57,7 +57,6 @@ module.exports = class SpellPaletteView extends View
allDocs = {}
for lc in lcs
for doc in (lc.get('propertyDocumentation') ? [])
doc = _.clone doc
allDocs['__' + doc.name] ?= []
allDocs['__' + doc.name].push doc
if doc.type is 'snippet' then doc.owner = 'snippets'

View file

@ -201,7 +201,7 @@ module.exports = class TomeView extends View
updateSpellPalette: (thang, spell) ->
return unless thang and @spellPaletteView?.thang isnt thang and thang.programmableProperties or thang.apiProperties
@spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel, programmable: spell?.canRead(), language: spell.language, session: @options.session
@spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session
@spellPaletteView.toggleControls {}, spell.view.controlsEnabled if spell # TODO: know when palette should have been disabled but didn't exist
spellFor: (thang, spellName) ->