forked from harc/moonchild
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmoonchild.coffee
144 lines (117 loc) · 3.84 KB
/
moonchild.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
parser = require './metadata'
_ = require 'underscore'
estraverse = require 'estraverse'
expanders = require 'expanders'
globalHooks = {}
globalExtensions = {}
globalEditor = {}
widgetExpander = expanders.createExpander('displayWidget')
exportsExpander = expanders.createExpander('extensionId')
# Encapsulates the extension API that is provided to a single extension.
class Extension
constructor: (id) ->
if id of globalExtensions
throw new Error("An extension named '#{ id }' is already registered")
@_id = id || _.uniqueId('ext-')
@_hooks = {}
@_expander = expanders.createExpander('extras')
@on = _.partial(addHook, @_id, globalHooks)
addWidget: (pos, node, type, userData) ->
# TODO: Figure out how to handle this.
if widgetExpander.has(node, 'displayWidget')
throw new Error('Conflicting widgets on node')
widgetExpander.set node, 'displayWidget',
type: type
pos: pos
data: userData
return
getWidget: (node) ->
# TODO: Should this only be exposed to certain types of extensions?
widgetExpander.get(node, 'displayWidget')
setExtras: (node, data) ->
@_expander.set node, 'extras', data
getExtras: (node, ext) ->
# If `ext` is defined, it's the exports from an extension. Use that
# object to find the actual extension.
exp = if ext
id = exportsExpander.get(ext, 'extensionId')
globalExtensions[id]._expander
else
@_expander
exp.get(node, 'extras')
# Constants for the `pos` argument to `addWidget`.
BEFORE: 'before'
AFTER: 'after'
REPLACE: 'replace'
# Allows a client to hook the action named `hookName`. Every time the action
# performed, `visitor` will be called with the hook-specific arguments.
addHook = (id, hookState, hookName, func) ->
id ?= _.uniqueId 'hook-'
hooks = hookState[hookName] ?= {}
hooks[id] ?= []
hooks[id].push(func)
return
invokeHook = (hook, args) ->
_.each globalHooks[hook], (hookFns, id) ->
_.each hookFns, (fn) ->
applySafely(fn, args)
initializeExtension = (ext, deps, initFn) ->
result = initFn?.apply(null, [ext].concat(deps))
if result?
if _.isObject(result) then return result
throw new TypeError('Invalid export from extension (must be an object)')
{}
registerExtension = (id, deps, initFn) ->
# Allow extensions with no explicit dependencies.
if !_.isArray(deps)
initFn = deps
deps = []
deps = deps.map (name) ->
if not name of globalExtensions
throw new Error "Unmet dependency #{name}"
return globalExtensions[name].exports
ext = new Extension(id)
ext.exports = initializeExtension(ext, deps, initFn)
# Allow the exports object can be traced back to the extension itself.
exportsExpander.set(ext.exports, 'extensionId', ext._id)
globalExtensions[ext._id] = ext
parse = (hooks, source) ->
tree = parser.parse(source)
invokeHook('parse', getHookArgs(tree))
tree
getHookArgs = (ast) ->
# For API convenience, the tree is currently passed as an
# Underscore-wrapped list of nodes, but this should change.
nodes = []
estraverse.traverse(ast, { enter: (node) -> nodes.push(node) })
[_.chain(nodes), _.chain(ast.comments)]
applySafely = (func, args) ->
try
func.apply(null, args)
catch e
console.log e.stack || e
onChange = (newValue) ->
try
tree = parse(globalHooks, newValue)
catch e
console.log e
return
# Run the display hooks.
# TODO: This should be moved into a function that can be invoked by the
# editor plugin.
hookArgs = getHookArgs(tree)
invokeHook('display', hookArgs)
# Run the render hooks.
invokeHook('render', hookArgs)
setEditor = (editor) ->
globalEditor = editor
getEditor = -> globalEditor
module.exports = {
on: _.partial(addHook, null, globalHooks)
onChange, # TODO: Get rid of this.
parse,
registerExtension,
traverse: estraverse.traverse,
setEditor,
getEditor
}