├── .gitignore
├── .travis.yml
├── LICENCE
├── README.md
├── docs.mli
├── dom-recur.js
├── dom.js
├── examples
├── lib
│ ├── __old-delegator.js
│ ├── computed-filter.js
│ ├── dom-delegator
│ │ ├── default-plugin.js
│ │ ├── form-data.js
│ │ ├── get-listener.js
│ │ ├── get-value.js
│ │ └── index.js
│ ├── listen-mutation.js
│ ├── observ-array-serialize.js
│ ├── observ-array.js
│ ├── plugin-either.js
│ ├── plugin-event-meta.js
│ ├── plugin-event.js
│ ├── plugin-focus.js
│ └── plugin-list.js
├── list+either
│ ├── browser.js
│ ├── server.js
│ ├── template-experiment.html
│ └── template.js
└── todomvc
│ ├── README.md
│ ├── app.js
│ ├── browser.js
│ ├── lib-template
│ ├── change-event.js
│ ├── either.js
│ ├── event-meta.js
│ ├── event.js
│ ├── focus.js
│ ├── list.js
│ └── submit-event.js
│ ├── template.js
│ ├── todo-model.js
│ └── view-model.js
├── lib
├── get-next-elements.js
├── get-plugin.js
├── is-plugin.js
├── render-property.js
├── stringify-property.js
└── unpack-selector.js
├── merge-recur.js
├── merge.js
├── normalize.js
├── old_example
├── apply-observ.js
├── browser.js
├── observable-array.js
├── render-observ.js
├── server.js
├── stringify-observ.js
└── template.js
├── package.json
├── plugins
├── fragment.js
├── loose.js
├── observ.js
└── raw.js
├── stringify-recur.js
├── stringify.js
└── test
├── dom.js
├── index.js
├── integration
└── timezone-dropdown.js
├── normalize.js
└── stringify.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .monitor
3 | .*.swp
4 | .nodemonignore
5 | releases
6 | *.log
7 | *.err
8 | fleet.json
9 | public/browserify
10 | bin/*.json
11 | .bin
12 | build
13 | compile
14 | .lock-wscript
15 | node_modules
16 | coverage
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.8
4 | - "0.10"
5 | before_script:
6 | - npm install
7 | - npm install istanbul coveralls
8 | script: npm run travis-test
9 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Raynos.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jsonml-stringify
2 |
3 | [![build status][1]][2] [![dependency status][3]][4] [![coverage report][9]][10] [![stability index][15]][16]
4 |
5 | [![npm stats][13]][14]
6 |
7 | [![browser support][5]][6]
8 |
9 | Convert jsonml arrays to html strings
10 |
11 | ## Example
12 |
13 | ```js
14 | var Stringify = require("jsonml-stringify/stringify")
15 | var stringify = Stringify([
16 | require("jsonml-stringify/plugins/loose")
17 | ])
18 | var assert = require("assert")
19 |
20 | var html = stringify(["html", [
21 | ["head", [
22 | ["meta", { charset: "utf-8" }],
23 | ["title", "Process dashboard"],
24 | ["link", { rel: "stylesheet", href: "/less/main"}]
25 | ]],
26 | ["body", { class: "main" }, [
27 | ["script", { src: "/browserify/main" }]
28 | ]]
29 | ]])
30 |
31 | assert.equal(html,
32 | "\n" +
33 | "
\n" +
34 | " \n" +
35 | " \n" +
36 | " Process dashboard\n" +
37 | " \n" +
38 | " \n" +
39 | " \n" +
40 | " \n" +
41 | " \n" +
42 | " \n" +
43 | "")
44 | ```
45 |
46 | ## stringify raw html entities
47 |
48 | ```js
49 | var Stringify = require("jsonml-stringify/stringify")
50 | var stringify = Stringify([
51 | require("jsonml-stringify/plugins/loose"),
52 | require("jsonml-stringify/plugins/raw")
53 | ])
54 | var assert = require("assert")
55 |
56 | var html = stringify(["div", { raw: "foo©" }])
57 |
58 | assert.equal(html, "\n foo©\n
")
59 | ```
60 |
61 | ## stringify fragments
62 |
63 | ```js
64 | var Stringify = require("jsonml-stringify/stringify")
65 | var stringify = Stringify([
66 | require("jsonml-stringify/plugins/loose"),
67 | require("jsonml-stringify/plugins/fragment")
68 | ])
69 | var assert = require("assert")
70 |
71 | var html = stringify(["div", [
72 | { fragment: [
73 | ["div", "one"],
74 | ["div", "two"]
75 | ] },
76 | ["div", "three"]
77 | ]])
78 |
79 | assert.equal(html, "\n" +
80 | "
\n" +
81 | " one\n" +
82 | "
\n" +
83 | "
\n" +
84 | " two\n" +
85 | "
\n\n" +
86 | "
\n" +
87 | " three\n" +
88 | "
\n" +
89 | "
")
90 | ```
91 |
92 | ## Loose JSONML definition
93 |
94 | ```ocaml
95 | (*
96 | JsonML is both loosely and strictly defined.
97 |
98 | A plugin is an object literal with either a single key / value
99 | pair or a key 'type' and some properties
100 |
101 | Loose:
102 | - null
103 | - undefined
104 | - plugin
105 | - text content
106 | - [ tagName ]
107 | - [ tagName , properties ]
108 | - [ tagName , text content ]
109 | - [ tagName , children ]
110 | - [ tagName , plugin ]
111 | - [ tagName , properties , text content ]
112 | - [ tagName , properties , children ]
113 | - [ tagname , properties , plugin ]
114 | - [ '#text' , text content ]
115 | - [ '#text' , properties , text content ]
116 |
117 | *)
118 |
119 | type JsonMLPlugin := Object | Function
120 | type JsonMLProperties :=
121 | Object
122 |
123 | type LooseJsonML :=
124 | null |
125 | undefined |
126 | JsonMLPlugin |
127 | String |
128 | [ String ] |
129 | [ String , JsonMLProperties ] |
130 | [ String , String ] |
131 | [ String , Array ] |
132 | [ String , JsonMLPlugin ] |
133 | [ "#text" , String ] |
134 | [ String , JsonMLProperties , String ] |
135 | [ String , JsonMLProperties , Array ] |
136 | [ String , JsonMLProperties , JsonMLPlugin ] |
137 | [ "#text" , JsonMLProperties , String ]
138 | ```
139 |
140 | ### Plugin definition
141 |
142 | ```ocaml
143 | type Plugin := {
144 | stringify: (JsonML, JsonMLOptions) => String,
145 | dom: (JsonML, JsonMLOptions) => DOMElement,
146 | merge: (JsonML, JsonMLMergeOptions) => void,
147 | type: String,
148 | normalize: (JsonML, JsonMLOptions) => JsonML,
149 | renderProperty: (DOMElement, value: Any, key: String, JsonMLOptions),
150 | stringifyProperty: (value: Any, key: String, JsonMLOptions) => String,
151 | mergeProperty: (DOMElement, value: Any, key: String, JsonMLMergeOptions),
152 | setProperty: (value: Any, key: String),
153 | getProperty: (value: Any, key: String) => String
154 | }
155 | ```
156 |
157 | ### Strict definition & functions
158 |
159 | ```ocaml
160 | (*
161 |
162 | Strict:
163 | - null
164 | - plugin
165 | - [ tagName , properties , children ]
166 | - [ '#text' , properties , text content ]
167 | - [ '#text' , properties , plugin ]
168 |
169 | *)
170 | type JsonMLPlugin := Object | Function
171 | type JsonMLProperties :=
172 | Object
173 |
174 | type JsonML :=
175 | null |
176 | JsonMLPlugin |
177 | [ String , JsonMLProperties , Array ] |
178 | [ "#text" , JsonMLProperties , String | JsonMLPlugin ]
179 |
180 | type JsonMLOptions := {
181 | parent: JsonML,
182 | parents: Array,
183 | plugins: Array
184 | }
185 |
186 | type JsonMLMergeOptions := JsonMLOptions & {
187 | elements: Array,
188 | root: DOMElement
189 | }
190 |
191 | stringify-recur := (JsonML, JsonMLOptions) => String
192 |
193 | dom-recur := (JsonML, JsonMLOptions) => DOMElement
194 |
195 | merge-recur := (JsonML, JsonMLMergeOptions)
196 | ```
197 |
198 | ## Installation
199 |
200 | `npm install jsonml-stringify`
201 |
202 | ## Contributors
203 |
204 | - Raynos
205 |
206 | ## MIT Licenced
207 |
208 | [1]: https://secure.travis-ci.org/Raynos/jsonml-stringify.png
209 | [2]: https://travis-ci.org/Raynos/jsonml-stringify
210 | [3]: https://david-dm.org/Raynos/jsonml-stringify.png
211 | [4]: https://david-dm.org/Raynos/jsonml-stringify
212 | [5]: https://ci.testling.com/Raynos/jsonml-stringify.png
213 | [6]: https://ci.testling.com/Raynos/jsonml-stringify
214 | [9]: https://coveralls.io/repos/Raynos/jsonml-stringify/badge.png
215 | [10]: https://coveralls.io/r/Raynos/jsonml-stringify
216 | [13]: https://nodei.co/npm/jsonml-stringify.png?downloads=true&stars=true
217 | [14]: https://nodei.co/npm/jsonml-stringify
218 | [15]: http://hughsk.github.io/stability-badges/dist/unstable.svg
219 | [16]: http://github.com/hughsk/stability-badges
220 |
221 | [7]: https://badge.fury.io/js/jsonml-stringify.png
222 | [8]: https://badge.fury.io/js/jsonml-stringify
223 | [11]: https://gemnasium.com/Raynos/jsonml-stringify.png
224 | [12]: https://gemnasium.com/Raynos/jsonml-stringify
225 |
--------------------------------------------------------------------------------
/docs.mli:
--------------------------------------------------------------------------------
1 | (*
2 | JsonML is both loosely and strictly defined.
3 |
4 | A plugin is an object literal with either a single key / value
5 | pair or a key 'type' and some properties
6 |
7 | Strict:
8 | - null
9 | - plugin
10 | - [ tagName , properties , children ]
11 | - [ '#text' , properties , text content ]
12 | - [ '#text' , properties , plugin ]
13 |
14 | Loose:
15 | - null
16 | - undefined
17 | - plugin
18 | - text content
19 | - [ tagName ]
20 | - [ tagName , properties ]
21 | - [ tagName , text content ]
22 | - [ tagName , children ]
23 | - [ tagName , plugin ]
24 | - [ tagName , properties , text content ]
25 | - [ tagName , properties , children ]
26 | - [ tagname , properties , plugin ]
27 | - [ '#text' , text content ]
28 | - [ '#text' , properties , text content ]
29 |
30 | *)
31 |
32 |
33 | type JsonMLPlugin := Object | Function
34 | type JsonMLProperties :=
35 | Object
36 |
37 | type JsonML :=
38 | null |
39 | JsonMLPlugin |
40 | [ String , JsonMLProperties , Array ] |
41 | [ "#text" , JsonMLProperties , String | JsonMLPlugin ]
42 |
43 |
44 | type LooseJsonML :=
45 | null |
46 | undefined |
47 | JsonMLPlugin |
48 | String |
49 | [ String ] |
50 | [ String , JsonMLProperties ] |
51 | [ String , String ] |
52 | [ String , Array ] |
53 | [ String , JsonMLPlugin ] |
54 | [ "#text" , String ] |
55 | [ String , JsonMLProperties , String ] |
56 | [ String , JsonMLProperties , Array ] |
57 | [ String , JsonMLProperties , JsonMLPlugin ] |
58 | [ "#text" , JsonMLProperties , String ]
59 |
60 | type JsonMLOptions := {
61 | parent: JsonML,
62 | parents: Array,
63 | plugins: Array
64 | }
65 |
66 | type JsonMLMergeOptions := JsonMLOptions & {
67 | elements: Array,
68 | root: DOMElement
69 | }
70 |
71 | type Plugin := {
72 | stringify: (JsonML, JsonMLOptions) => String,
73 | dom: (JsonML, JsonMLOptions) => DOMElement,
74 | merge: (JsonML, JsonMLMergeOptions) => void,
75 | type: String,
76 | normalize: (JsonML, JsonMLOptions) => JsonML,
77 | renderProperty: (DOMElement, value: Any, key: String, JsonMLOptions),
78 | stringifyProperty: (value: Any, key: String, JsonMLOptions) => String,
79 | mergeProperty: (DOMElement, value: Any, key: String, JsonMLMergeOptions),
80 | setProperty: (value: Any, key: String),
81 | getProperty: (value: Any, key: String) => String
82 | }
83 |
84 | stringify-recur := (JsonML, JsonMLOptions) => String
85 |
86 | dom-recur := (JsonML, JsonMLOptions) => DOMElement
87 |
88 | merge-recur := (JsonML, JsonMLMergeOptions)
89 |
--------------------------------------------------------------------------------
/dom-recur.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 | var extend = require("xtend")
3 |
4 | var isPlugin = require("./lib/is-plugin.js")
5 | var getPlugin = require("./lib/get-plugin.js")
6 | var renderProperty = require("./lib/render-property.js")
7 | var unpackSelector = require("./lib/unpack-selector.js")
8 |
9 | module.exports = domRecur
10 |
11 | function domRecur(tree, opts) {
12 | if (tree === null) {
13 | return null
14 | } else if (isPlugin(tree)) {
15 | return getPlugin(tree, opts).dom(tree, opts)
16 | }
17 |
18 | var selector = tree[0]
19 | var properties = tree[1]
20 | var children = tree[2]
21 |
22 | if (selector === "#text") {
23 | // console.log("children", children)
24 | return document.createTextNode(children)
25 | }
26 |
27 | var tagName = unpackSelector(selector, properties, opts)
28 |
29 | var elem = document.createElement(tagName.toUpperCase())
30 | Object.keys(properties).forEach(function (key) {
31 | var value = properties[key]
32 |
33 | renderProperty(elem, value, key, opts)
34 | })
35 |
36 | for (var i = 0; i < children.length; i++) {
37 | var childOpts = extend(opts, {
38 | parent: tree,
39 | parents: opts.parents.concat([tree])
40 | })
41 |
42 | var child = domRecur(children[i], childOpts)
43 |
44 | if (child !== null) {
45 | elem.appendChild(child)
46 | }
47 | }
48 |
49 | return elem
50 | }
51 |
--------------------------------------------------------------------------------
/dom.js:
--------------------------------------------------------------------------------
1 | var normalize = require("./normalize.js")
2 | var domRecur = require("./dom-recur.js")
3 |
4 | module.exports = Dom
5 |
6 | function Dom(plugins) {
7 | return function dom(tree, opts) {
8 | opts = opts || {}
9 |
10 | return domRecur(normalize(tree, opts, plugins), opts)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/lib/__old-delegator.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 | var extend = require("xtend")
3 |
4 | var FormData = require("./form-data.js")
5 |
6 | module.exports = Delegator
7 |
8 | function Delegator(root) {
9 | root = root || document.body
10 |
11 | var events = {}
12 | var changeHandled = false
13 | var submitHandled = false
14 | var listeners = {}
15 |
16 | return {
17 | on: on,
18 | change: change,
19 | submit: submit
20 | }
21 |
22 | function on(type, filter, listener) {
23 | if (!events[type]) {
24 | events[type] = true
25 |
26 | listen(type)
27 | }
28 |
29 | return setListener(type, filter, listener)
30 | }
31 |
32 | function change(filter, listener) {
33 | if (!changeHandled) {
34 | changeHandled = true
35 |
36 | listenChange()
37 | }
38 |
39 | return setListener("change", filter, listener)
40 | }
41 |
42 | function submit(filter, listener) {
43 | if (!submitHandled) {
44 | submitHandled = true
45 |
46 | listenSubmit()
47 | }
48 |
49 | return setListener("submit", filter, listener)
50 | }
51 |
52 | function listenChange() {
53 | root.addEventListener("keypress", function (ev) {
54 | var target = ev.target
55 | var listener = getListeners(target, "change")
56 | if (!listener || target.type !== "text") {
57 | return
58 | }
59 |
60 | listener.fn(extend(ev, {
61 | currentTarget: listener.target
62 | }), listener.value)
63 | }, true)
64 |
65 | root.addEventListener("change", function (ev) {
66 | var target = ev.target
67 | var listener = getListeners(ev.target, "change")
68 |
69 | if (!listener || (
70 | target.type !== "checkbox" &&
71 | target.tagName !== "SELECT"
72 | )) {
73 | return
74 | }
75 |
76 | listener.fn(extend(ev, {
77 | currentTarget: listener.target,
78 | currentValue: getValue(listener.target)
79 | }), listener.value)
80 | }, true)
81 | }
82 |
83 | function listenSubmit() {
84 | document.addEventListener("click", function (ev) {
85 | var target = ev.target
86 | var listener = getListeners(ev.target, "submit")
87 | if (!listener || target.tagName !== "BUTTON") {
88 | return
89 | }
90 |
91 | listener.fn(extend(ev, {
92 | currentTarget: listener.target,
93 | formData: FormData(listener.target)
94 | }), listener.value)
95 | })
96 | }
97 |
98 | function listen(type) {
99 | root.addEventListener(type, function (ev) {
100 | var listener = getListeners(ev.target, type)
101 | if (!listener) {
102 | return
103 | }
104 |
105 | listener.fn(extend(ev, {
106 | currentTarget: listener.target,
107 | currentValue: getValue(listener.target)
108 | }), listener.value)
109 | }, true)
110 | }
111 |
112 | function setListener(type, filter, listener) {
113 | if (!listeners[type]) {
114 | listeners[type] = {}
115 | }
116 |
117 | if (!listeners[type][filter]) {
118 | listeners[type][filter] = []
119 | }
120 |
121 | listeners[type][filter].push(listener)
122 |
123 | return function remove() {
124 | listener.splice(listeners.indexOf(listener), 1)
125 | }
126 | }
127 |
128 | function getListeners(target, type) {
129 | if (target === null) {
130 | return
131 | }
132 |
133 | var ds = target.dataset
134 | var value = ds && ds["event:" + type]
135 |
136 | if (!value) {
137 | return getListeners(target.parentNode, type)
138 | }
139 |
140 | var parts = value.split("~")
141 | var fns = listeners[type][parts[0]]
142 |
143 | if (!fns) {
144 | return getListeners(target.parentNode, type)
145 | }
146 |
147 | return {
148 | fn: function () {
149 | var args = [].slice.call(arguments)
150 | var self = this
151 | fns.forEach(function (fn) {
152 | fn.apply(self, args)
153 | })
154 | },
155 | target: target,
156 | value: JSON.parse(parts[1])
157 | }
158 | }
159 | }
160 |
161 | function getValue(target) {
162 | if (target.type === "checkbox") {
163 | return !!target.checked
164 | } else if (target.tagName === "SELECT") {
165 | return target.value
166 | } else if (target.type === "text") {
167 | return target.value
168 | } else {
169 | return target.value
170 | }
171 | }
--------------------------------------------------------------------------------
/examples/lib/computed-filter.js:
--------------------------------------------------------------------------------
1 | var ObservArray = require("./observ-array.js")
2 |
3 | var Empty = {}
4 |
5 | // computedFilter allows you to run a filter lambda
6 | // over an observable array with a list of dependencies
7 | //
8 | // dependencies can be either string keypaths where
9 | // the value of get(obs().value[0], keypath) is an observable
10 | // or its an observable
11 | //
12 | // the keypath allows you to say this observable array contains
13 | // a items which have observable values on a key path
14 | //
15 | // when anything in the list of dependencies changes we re-run
16 | // the filter on the entire array which is O(N)
17 | //
18 | // However when an item gets added or removed to the array
19 | // i.e. the raw array changed we only run the filtering lambda
20 | // on the item that was added. (removed items just get removed)
21 | // this means that array additions & removals are O(1) and we
22 | // do not recompute the filter for the entire array when it
23 | // changes
24 | //
25 | // This is purely an optimization strategy. It's also a reactive
26 | // filter. You could implement a less efficient naive reactive
27 | // filter.
28 | module.exports = computedFilter
29 |
30 | function computedFilter(obs, deps, lambda) {
31 | var filteredArray = ObservArray(obs.filter(function (item) {
32 | return callLambda(item)
33 | }))
34 | var keypathDeps = deps.filter(isString)
35 | var observDeps = deps.filter(isNotString)
36 |
37 | observDeps.forEach(applyObservFilter)
38 | keypathDeps.forEach(applyDepsFilter)
39 |
40 | obs(function (opts) {
41 | var diff = opts.diff
42 |
43 | var items = diff.slice(2)
44 |
45 | items.forEach(function (item) {
46 | applyItemFilter(item)
47 | keypathDeps.forEach(function (keypath) {
48 | applyKeypathFilter(keypath, item)
49 | })
50 | })
51 | // apply filter to diff
52 | })
53 |
54 | return filteredArray
55 |
56 | function applyObservFilter(dep) {
57 | // otherwise its an observable, for which we refilter
58 | // the entire array in a mutative fashion
59 | dep(function () {
60 | obs.forEach(applyItemFilter)
61 | })
62 | }
63 |
64 | function applyDepsFilter(keypath) {
65 | // if its a keypath then for each item filter it
66 | // by the keypath
67 | return obs.forEach(function (item) {
68 | applyKeypathFilter(dep, item)
69 | })
70 | }
71 |
72 | function callLambda(item) {
73 | var args = observDeps.map(function (obs) {
74 | return obs()
75 | })
76 |
77 | args.push(item)
78 |
79 | return lambda.apply(null, args)
80 | }
81 |
82 | // dep has changed for each item
83 | // run filter lambda again to get new true / false
84 | // then mutate filteredArray minimally to apply this
85 | // filter function
86 | function applyItemFilter(item) {
87 | var rawIndex = obs.indexOf(item)
88 |
89 | if (rawIndex === -1) {
90 | return
91 | }
92 |
93 | var index = filteredArray.indexOf(item)
94 | var keep = callLambda(item)
95 |
96 | if (keep) {
97 | if (index !== -1) {
98 | return
99 | }
100 |
101 | while (rawIndex--) {
102 | var rawItem = obs().value[rawIndex]
103 | var filteredIndex = filteredArray.indexOf(rawItem)
104 |
105 | if (filteredIndex !== -1) {
106 | filteredArray.splice(filteredIndex + 1. 0, item)
107 | return
108 | }
109 | }
110 |
111 | filteredArray.unshift(item)
112 | } else {
113 | if (index === -1) {
114 | return
115 | }
116 |
117 | filteredArray.splice(index, 1)
118 | }
119 | }
120 |
121 | function applyKeypathFilter(keypath, item) {
122 | var obs = item[keypath.substr(1)]
123 |
124 | obs(function () {
125 | applyItemFilter(item)
126 | })
127 | }
128 | }
129 |
130 | function isNotString(x) { return typeof x !== "string" }
131 | function isString(x) { return typeof x === "string" }
132 |
133 | function notEmpty(x) { return x !== Empty }
--------------------------------------------------------------------------------
/examples/lib/dom-delegator/default-plugin.js:
--------------------------------------------------------------------------------
1 | var getListener = require("./get-listener.js")
2 | var getValue = reuqire("./get-value.js")
3 |
4 | module.exports = {
5 | type: "default",
6 | registerEvent: registerEvent,
7 | eventHandler: eventHandler
8 | }
9 |
10 | function registerEvent(eventsTable, value, key) {
11 | eventsTable[value.eventName || key] = true
12 | }
13 |
14 | function eventHandler(eventName) {
15 | // TODO: make sure eventName is a DOM event. check in
16 | // a white list
17 | return function (ev) {
18 | var listener = getListener(ev.target, eventName)
19 | if (!listener) {
20 | return
21 | }
22 |
23 | emitter.emit(listener.name, {
24 | target: ev.target,
25 | currentTarget: listener.currentTarget,
26 | currentValue: getValue(listener.currentTarget)
27 | }, ev)
28 |
29 | return true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/lib/dom-delegator/form-data.js:
--------------------------------------------------------------------------------
1 | var walk = require("dom-walk")
2 |
3 | var containsArray = /\[\]$/
4 |
5 | module.exports = FormData
6 |
7 | /* like FormDataSet except takes an element and recurses
8 | it's descendants. The hash its build is based on the
9 | name property of the input elements in the descendants
10 |
11 | */
12 | function FormData(rootElem) {
13 | var data = {}
14 |
15 | walk([rootElem], function (elem) {
16 | var name = elem.name
17 | if (elem.tagName === "INPUT" && elem.type === "checkbox") {
18 | if (containsArray.test(name) && !data[name]) {
19 | data[name] = []
20 | }
21 |
22 | if (!elem.checked) {
23 | return
24 | }
25 |
26 | if (data[name]) {
27 | if (!Array.isArray(data[name])) {
28 | data[name] = [data[name]]
29 | }
30 |
31 | data[name].push(elem.value)
32 | return
33 | }
34 |
35 | if (containsArray.test(name)) {
36 | data[name] = [elem.value]
37 | } else {
38 | data[name] = elem.value
39 | }
40 | } else if (elem.tagName === "INPUT" && elem.type === "text") {
41 | data[name] = elem.value
42 | }
43 | })
44 |
45 | return data
46 | }
--------------------------------------------------------------------------------
/examples/lib/dom-delegator/get-listener.js:
--------------------------------------------------------------------------------
1 | var getEvents = require("../plugin-event.js")
2 |
3 | module.exports = getListener
4 |
5 | function getListener(target, type) {
6 | if (target === null) {
7 | return
8 | }
9 |
10 | var events = getEvents(target)
11 | var value = events[type]
12 |
13 | if (!value) {
14 | return getListener(target.parentNode, type)
15 | }
16 |
17 | return {
18 | name: value.name,
19 | currentTarget: target
20 | }
21 | }
--------------------------------------------------------------------------------
/examples/lib/dom-delegator/get-value.js:
--------------------------------------------------------------------------------
1 | var FormData = require("./form-data.js")
2 |
3 | module.exports = getValue
4 |
5 | function getValue(element) {
6 | if (element.type === "checkbox") {
7 | if (element.hasAttribute("value")) {
8 | return elem.checked ? elem.value : null
9 | } else {
10 | return !!elem.checked
11 | }
12 | } else if (target.tagName === "SELECT") {
13 | return target.value
14 | } else if (target.tagName === "INPUT") {
15 | return target.value
16 | } else if (target.tagName === "TEXTAREA") {
17 | return target.value
18 | } else {
19 | return FormData(target)
20 | }
21 | }
--------------------------------------------------------------------------------
/examples/lib/dom-delegator/index.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 | var walk = require("dom-walk")
3 | var EventEmitter = require("events").EventEmitter
4 |
5 | var defaultGetEvents = require("../plugin-event.js")
6 | var FormData = require("./form-data.js")
7 | var defaultPlugin = require("./default-plugin.js")
8 |
9 | var emitterOn = EventEmitter.prototype.on
10 | var allEvents = [
11 | "blur", "focus", "focusin", "focusout", "load", "resize",
12 | "scroll", "unload", "click", "dblclick", "mousedown",
13 | "mouseup", "change", "select", "submit", "keydown",
14 | "keypress", "keyup", "error", "contextmenu"
15 | ]
16 |
17 | /*
18 | type DelegatorPlugin := {
19 | eventHandler: (eventName: String) =>
20 | null | eventHandler: (DOMEvent) => Boolean,
21 | registerEvent:
22 | (eventsTable: Object, pluginValue: Object, key: String) => void
23 | type: String
24 | }
25 |
26 | Delegator := (Element?, options?: {
27 | plugins?: Array,
28 | events?: Object,
29 | allEvents?: Boolean
30 | walk?: Boolean
31 | }) => EventEmitter
32 |
33 | allEvents is opt in.
34 | walk is opt out.
35 |
36 | A plugin must return either an event handler or null for
37 | each event name (keypress, click, etc).
38 |
39 | The delegator will then dispatch that event (keypress, etc)
40 | to the plugin before it handles the general dispatch.
41 |
42 | This means a `submit` plugin could intercept keypress ENTER
43 | and dispatch it to it's own handlers and suppress the
44 | normal handlers.
45 |
46 | The handler returned by getListener must return `true` if
47 | it has handled the event, otherwise the default handler
48 | will fire as well!!
49 | */
50 | module.exports = Delegator
51 |
52 | function Delegator(rootNode, opts) {
53 | if (rootNode && !rootNode.nodeName) {
54 | opts = rootNode
55 | rootNode = null
56 | }
57 |
58 | rootNode = rootNode || document
59 | opts = opts || {}
60 |
61 | var emitter = new EventEmitter()
62 | var plugins = opts.plugins || []
63 | var registerEvent = opts.registerEvent || defaultPlugin.registerEvent
64 | var eventHandler = opts.eventHandler || defaultPlugin.eventHandler
65 | var eventsTable = opts.eventsTable || {}
66 | var getEvents = opts.getEvents || defaultGetEvents
67 |
68 | var pluginsHash = plugins.reduce(function (acc, plugin) {
69 | acc[plugin.type] = plugin
70 | return acc
71 | })
72 |
73 | // MONKEY PATCH on
74 | emitter.on = emitter.addListener = on
75 |
76 | // IF WALK (opt out) then walk root node
77 | // for each element find all events bound to it through
78 | // weakmap (or attributes, who cares)
79 | // for each event on the element find the plugin for that
80 | // event and register a dom event name `click`, etc on the
81 | // eventsHash
82 | if (opts.walk !== false) {
83 | walk(rootNode, function (elem) {
84 | var events = getEvents(elem)
85 | // events is Object
86 |
87 | Object.keys(events).forEach(function (domEvent) {
88 | var value = events[domEvent]
89 | var plugin = pluginsHash[domEvent]
90 |
91 | if (plugin) {
92 | plugin.registerEvent(eventsTable, value, domEvent)
93 | } else {
94 | registerEvent(eventsTable, value, domEvent)
95 | }
96 | })
97 | })
98 | }
99 |
100 | // if allEvents just enable common events on the eventsTable
101 | if (opts.allEvents) {
102 | allEvents.forEach(function (domEvent) {
103 | eventsTable[domEvent] = true
104 | })
105 | }
106 |
107 | // for each dom event name `click`, `keypress`, etc
108 | // register it as a global delegated listener
109 | Object.keys(eventsTable).forEach(listen)
110 |
111 | return emitter
112 |
113 | // when we listen to an event like `click` , `keypress` etc
114 | // we ask all plugins for a handler for that event name
115 | // and we ask the default eventHandler creator for a handler.
116 | // if there are no handlers then we dont add a root listener
117 | // if there is a handler then we add a global handler and
118 | // call each handler for the event until one returns true
119 | function listen(eventName) {
120 | var handlers = plugins.map(function (plugin) {
121 | return plugin.eventHandler(eventName)
122 | }).filter(Boolean)
123 | var handler = eventHandler(eventName)
124 | if (handler) {
125 | handlers.push(handler)
126 | }
127 |
128 | if (handlers.length === 0) {
129 | return
130 | }
131 |
132 | rootNode.addListener(eventName, function (ev) {
133 | handlers.some(function (fn) {
134 | return fn(ev)
135 | })
136 | }, true)
137 | }
138 |
139 | // like normal on EXCEPT you can pass a `domEvent` as an
140 | // argument in case you want to do like
141 | // `delegator.on('foo', 'mousemove', function () {})`
142 | // then you can opt in to expensive delegating events
143 | // at run time instead of at delegator create time
144 | function on(name, domEvent, listener) {
145 | if (typeof domEvent === "function") {
146 | listener = domEvent
147 | domEvent = null
148 | }
149 |
150 | if (!Array.isArray(domEvent)) {
151 | domEvent = [domEvent]
152 | }
153 |
154 | domEvent.forEach(function (eventName) {
155 | if (eventsTable[eventName]) {
156 | return
157 | }
158 |
159 | listen(eventName)
160 | eventsTable[eventName] = true
161 | })
162 |
163 | emitterOn.call(this, name, listener)
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/examples/lib/listen-mutation.js:
--------------------------------------------------------------------------------
1 | var MutationObserver = require("global/window").MutationObserver
2 |
3 | module.exports = listenMutation
4 |
5 | function listenMutation(elem, listener) {
6 | var observer = new MutationObserver(function (records) {
7 | records.forEach(function (record) {
8 | listener(printChange(record))
9 | })
10 | })
11 |
12 | observer.observe(elem, {
13 | childList: true,
14 | characterData: true,
15 | subtree: true,
16 | attributes: true,
17 | attributeOldValue: true,
18 | characterDataOldValue: true
19 | })
20 | }
21 |
22 | function printChange(record) {
23 | var addLen = record.addedNodes && record.addedNodes.length
24 | var remLen = record.removedNodes && record.removedNodes.length
25 | var recordType = record.type
26 | var target = record.target
27 |
28 | var type =
29 | addLen > 0 && remLen > 0 ? "replace" :
30 | addLen > 0 ? "add" :
31 | remLen > 0 ? "remove" :
32 | recordType === "characterData" ? "text" :
33 | "unknown"
34 |
35 | var elems =
36 | type === "replace" ? {
37 | added: [].slice.call(record.addedNodes),
38 | removed: [].slice.call(record.removedNodes)
39 | } :
40 | type === "add" ? [].slice.call(record.addedNodes) :
41 | type === "remove" ? [].slice.call(record.removedNodes) :
42 | []
43 |
44 | var value = type === "text" ? target.data :
45 | null
46 |
47 | return {
48 | type: type,
49 | elems: elems,
50 | value: value,
51 | oldValue: record.oldValue,
52 | operation: recordType,
53 | target: target
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/lib/observ-array-serialize.js:
--------------------------------------------------------------------------------
1 | module.exports = serialize
2 |
3 | function serialize(obs) {
4 | var array = obs().value
5 |
6 | return array.map(function (item) {
7 | return Object.keys(item).reduce(function (acc, key) {
8 | var value = item[key]
9 | acc[key] = typeof value === "function" ? value() : value
10 | }, {})
11 | })
12 | }
--------------------------------------------------------------------------------
/examples/lib/observ-array.js:
--------------------------------------------------------------------------------
1 | var Observable = require("observ")
2 |
3 | var slice = Array.prototype.slice
4 | var methods = [
5 | "concat", "every", "filter", "forEach", "indexOf",
6 | "join", "lastIndexOf", "map", "reduce", "reduceRight",
7 | "some", "sort", "toString", "toLocaleString"
8 | ]
9 |
10 | module.exports = ObservableArray
11 |
12 | /* ObservableArray := (Array) => Observable<{
13 | value: Array,
14 | diff: Array
15 | }> & {
16 | splice: (index: Number, amount: Number, rest...: T) =>
17 | Array,
18 | push: (values...: T) => Number,
19 | filter: (lambda: Function, thisValue: Any) => Array,
20 | indexOf: (item: T, fromIndex: Number) => Number
21 | }
22 | */
23 | function ObservableArray(list) {
24 | var obs = Observable({ value: list, diff: [] })
25 |
26 | obs.length = list.length
27 | obs.splice = function (index, amount) {
28 | var args = slice.call(arguments, 0)
29 | var currentList = obs().value.slwice()
30 |
31 | var removed = currentList.splice.apply(currentList, args)
32 | obs.length = currentList.length
33 |
34 | obs.set({ value: currentList, diff: args })
35 | return removed
36 | }
37 | obs.push = function () {
38 | var args = slice.call(arguments)
39 | args.unshift(obs.length, 0)
40 | obs.splice.apply(null, args)
41 |
42 | return obs.length
43 | }
44 | obs.pop = function () {
45 | return obs.splice(obs.length - 1, 1)[0]
46 | }
47 | obs.shift = function () {
48 | return obs.splice(0, 1)[0]
49 | }
50 | obs.unshift = function () {
51 | var args = slice.call(arguments)
52 | args.unshift(0, 0)
53 | obs.splice.apply(null, args)
54 |
55 | return obs.length
56 | }
57 | obs.reverse = function () {
58 | throw new Error("Pull request welcome")
59 | }
60 |
61 | obs.concat = method(obs, "concat")
62 | obs.every = method(obs, "every")
63 | obs.filter = method(obs, "filter")
64 | obs.forEach = method(obs, "forEach")
65 | obs.indexOf = method(obs, "indexOf")
66 | obs.join = method(obs, "join")
67 | obs.lastIndexOf = method(obs, "lastIndexOf")
68 | obs.map = method(obs, "map")
69 | obs.reduce = method(obs, "reduce")
70 | obs.reduceRight = method(obs, "reduceRight")
71 | obs.some = method(obs, "some")
72 | obs.sort = method(obs, "sort")
73 | obs.toString = method(obs, "toString")
74 | obs.toLocaleString = method(obs, "toLocaleString")
75 |
76 | return obs
77 | }
78 |
79 | function method(obs, name) {
80 | return function () {
81 | var list = obs().value
82 | return list[name].apply(list, arguments)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/examples/lib/plugin-either.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 |
3 | var stringifyRecur = require("../../stringify-recur.js")
4 | var domRecur = require("../../dom-recur.js")
5 | var normalize = require("../../normalize.js")
6 |
7 | module.exports = {
8 | type: "either",
9 | stringify: function (tree, opts) {
10 | return stringify(
11 | tree.bool ? tree.left : tree.right, opts)
12 | },
13 | dom: function (tree, opts) {
14 | tree.bool(function (bool) {
15 | var newElem = bool ? leftElem : rightElem
16 |
17 | currElem.parentNode.replaceChild(newElem, currElem)
18 |
19 | currElem = newElem
20 | })
21 |
22 | var leftElem = dom(tree.left, opts) || placeholder()
23 | var rightElem = dom(tree.right, opts) || placeholder()
24 |
25 | var currElem = tree.bool() ? leftElem : rightElem
26 |
27 | return currElem
28 | },
29 | merge: function (tree, opts) {
30 |
31 | }
32 | }
33 |
34 | function stringify(tree, opts) {
35 | return stringifyRecur(normalize(tree, opts), opts)
36 | }
37 |
38 | function dom(tree, opts) {
39 | return domRecur(normalize(tree, opts), opts)
40 | }
41 |
42 | function placeholder() {
43 | return document.createTextNode("")
44 | }
45 |
--------------------------------------------------------------------------------
/examples/lib/plugin-event-meta.js:
--------------------------------------------------------------------------------
1 | var WeakMap = require("weakmap")
2 |
3 | var map = WeakMap()
4 |
5 | getMeta.renderProperty = renderProperty
6 | getMeta.type = "event-meta"
7 |
8 | module.exports = getMeta
9 |
10 | function getMeta(elem) {
11 | return map.get(elem) || {}
12 | }
13 |
14 | function renderProperty(elem, value, key) {
15 | var meta = value.meta
16 | map.set(elem, meta)
17 | }
--------------------------------------------------------------------------------
/examples/lib/plugin-event.js:
--------------------------------------------------------------------------------
1 | var WeakMap = require("weakmap")
2 |
3 | var map = WeakMap()
4 |
5 | getEvents.renderProperty = renderProperty
6 | getEvents.type = "event"
7 |
8 | module.exports = getEvents
9 |
10 | function getEvents(elem) {
11 | return map.get(elem) || {}
12 | }
13 |
14 | function renderProperty(elem, value, key) {
15 | var name = value.name
16 | var eventName = value.eventName || key
17 | var state = getEvents(elem)
18 | state[eventName] = { name: name }
19 | map.set(elem, state)
20 | }
--------------------------------------------------------------------------------
/examples/lib/plugin-focus.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | renderProperty: function (elem, value, key) {
3 | value(function () {
4 | elem.focus()
5 | })
6 | },
7 | stringifyProperty: function (value, key) {
8 | return ""
9 | },
10 | type: "focus"
11 | }
--------------------------------------------------------------------------------
/examples/lib/plugin-list.js:
--------------------------------------------------------------------------------
1 | var before = require("insert/before")
2 | var remove = require("insert/remove")
3 | var document = require("global/document")
4 |
5 | var stringifyRecur = require("../../stringify-recur.js")
6 | var domRecur = require("../../dom-recur.js")
7 | var normalize = require("../../normalize.js")
8 |
9 | module.exports = {
10 | stringify: function (tree, opts) {
11 | return stringify({
12 | fragment: tree.array.map(tree.template)
13 | }, opts)
14 | },
15 | dom: function (tree, opts) {
16 | var listElem = dom({
17 | fragment: tree.array().value.map(tree.template)
18 | }, opts)
19 | var elems = [].slice.call(listElem.childNodes)
20 | var placeholderElem = placeholder()
21 |
22 | if (elems.length === 0) {
23 | listElem = placeholderElem
24 | }
25 |
26 | tree.array(function (tuple) {
27 | var diff = tuple.diff
28 |
29 | var index = diff[0]
30 | var origIndex = diff[0]
31 | var howMany = diff[1]
32 |
33 | if (howMany > 0) {
34 | var children = elems.splice(index, howMany)
35 |
36 | if (elems.length === 0 && children.length > 0) {
37 | before(children[0], placeholderElem)
38 | }
39 |
40 | // console.log("children", children)
41 | children.forEach(function (elem) {
42 | remove(elem)
43 | })
44 | }
45 |
46 | var newElems = diff.slice(2)
47 | if (newElems.length > 0) {
48 | var afterMode = false
49 | var elements = newElems
50 | .map(tree.template)
51 | .map(function (elem) {
52 | return dom(elem, opts)
53 | })
54 |
55 | var referenceElem = elems[index]
56 | if (!referenceElem) {
57 | afterMode = true
58 | }
59 |
60 | while (!referenceElem && index >= 0) {
61 | referenceElem = elems[--index]
62 | }
63 |
64 | if (!referenceElem) {
65 | referenceElem = placeholderElem
66 | }
67 |
68 | var parent = referenceElem.parentNode
69 |
70 | elements.forEach(function (elem) {
71 | if (afterMode) {
72 | parent.insertBefore(elem, null)
73 | } else {
74 | parent.insertBefore(elem, referenceElem)
75 | }
76 | })
77 |
78 | elems.splice.apply(elems, [origIndex, 0].concat(elements))
79 | }
80 | })
81 |
82 | return listElem
83 | },
84 | type: "list"
85 | }
86 |
87 | function stringify(tree, opts) {
88 | return stringifyRecur(normalize(tree, opts), opts)
89 | }
90 |
91 | function dom(tree, opts) {
92 | return domRecur(normalize(tree, opts), opts)
93 | }
94 |
95 | function placeholder() {
96 | return document.createTextNode("")
97 | }
98 |
--------------------------------------------------------------------------------
/examples/list+either/browser.js:
--------------------------------------------------------------------------------
1 | var Observ = require("observ")
2 | var JSONGlobals = require("json-globals/get")
3 | var document = require("global/document")
4 | var window = require("global/window")
5 | var console = require("console")
6 |
7 | var Dom = require("../../dom.js")
8 | var ObservableArray = require("../lib/observ-array.js")
9 | var template = require("./template.js")
10 | var listenMutation = require("../lib/listen-mutation.js")
11 |
12 | var dom = Dom([
13 | require("../../plugins/loose.js"),
14 | require("../../plugins/fragment.js"),
15 | require("../../plugins/observ.js"),
16 | require("../lib/plugin-either.js"),
17 | require("../lib/plugin-list.js"),
18 | ])
19 |
20 | var state = JSONGlobals("model")
21 | var model = window.model = Object.keys(state).reduce(function (acc, key) {
22 | var value = state[key]
23 |
24 | acc[key] = Array.isArray(value) ?
25 | ObservableArray(value) : Observ(value)
26 | return acc
27 | }, {})
28 |
29 | var elem = dom(template(model))
30 | document.body.appendChild(elem)
31 |
32 | listenMutation(document.body, function (delta) {
33 | console.log("op", delta)
34 | })
35 |
--------------------------------------------------------------------------------
/examples/list+either/server.js:
--------------------------------------------------------------------------------
1 | var http = require("http")
2 | var ServeBrowserify = require("serve-browserify")
3 | var JSONGlobals = require("json-globals")
4 |
5 | var Stringify = require("../../stringify.js")
6 | var template = require("./template")
7 |
8 | var stringify = Stringify([
9 | require("../../plugins/loose.js"),
10 | require("../../plugins/fragment.js"),
11 | require("../lib/plugin-either.js"),
12 | require("../lib/plugin-list.js")
13 | ])
14 |
15 | http.createServer(function (req, res) {
16 | if (req.url === "/browser") {
17 | ServeBrowserify({ root: __dirname })(req, res)
18 | } else {
19 | var model = {
20 | x: "x",
21 | y: "y",
22 | zs: ["1", "2", "3"]
23 | }
24 |
25 | res.setHeader("Content-Type", "text/html")
26 | res.end("" + stringify(["html", [
27 | ["head", [
28 | ["title", "Observable demo"]
29 | ]],
30 | ["body", [
31 | ["div", "Server"],
32 | ["div", { id: "main" }, [
33 | template(model)
34 | ]],
35 | ["div", "Client"],
36 | ["script", JSONGlobals({ model: model })],
37 | ["script", { src: "/browser" }]
38 | ]]
39 | ]]))
40 | }
41 | }).listen(8000, function () {
42 | console.log("listening on port 8000")
43 | })
44 |
--------------------------------------------------------------------------------
/examples/list+either/template-experiment.html:
--------------------------------------------------------------------------------
1 |
2 | {{if x}}
3 |
4 |
x: {{x}}
5 | y: {{y}}
6 |
7 | {{else}}
8 | {{/if}}
9 |
{{y}}
10 |
11 | {{list zs:z}}
12 | -
13 | item
14 | {{z}}
15 |
16 | {{/list}}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/list+either/template.js:
--------------------------------------------------------------------------------
1 | module.exports = template
2 |
3 | function template(model) {
4 | return ["div", [
5 | either(model.x, ["div", [
6 | ["li", [
7 | ["span", "x: "],
8 | ["span", model.x]
9 | ]],
10 | ["li", [
11 | ["span", "y: "],
12 | ["span", model.y]
13 | ]]
14 | ]], null),
15 | ["p", model.y],
16 | ["ol", [
17 | list(model.zs, function (value) {
18 | return ["li", [
19 | ["span", "item"],
20 | ["span", value]
21 | ]]
22 | })
23 | ]]
24 | ]]
25 | }
26 |
27 | function either(bool, left, right) {
28 | return {
29 | type: "either",
30 | bool: bool,
31 | left: left,
32 | right: right
33 | }
34 | }
35 |
36 | function list(array, generateTemplate) {
37 | return {
38 | type: "list",
39 | array: array,
40 | template: generateTemplate
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/todomvc/README.md:
--------------------------------------------------------------------------------
1 | # JsonML todomvc
2 |
3 | Uses:
4 |
5 | - jsonml for rendering
6 | - observ for managing state of the app
7 | - observ & observ primitive for reactive updates
8 | - dom-delegator & event primitive for handling DOM inputs
9 | - hash-router for dealing with routes
10 |
11 | ## Todos:
12 |
13 | - ~~implement `computedFilter`~~
14 | - ~~implement `serialize`~~
15 | - ~~implement `event`~~
16 | - ~~implement `eventMeta`~~
17 | - implement `Delegator`
--------------------------------------------------------------------------------
/examples/todomvc/app.js:
--------------------------------------------------------------------------------
1 | var ViewModel = require("./view-model.js")
2 | var TodoModel = require("./todo-model.js")
3 |
4 | module.exports = App
5 |
6 | function App(initialState, inputs) {
7 | var router = inputs.router
8 | var events = inputs.events
9 |
10 | // Model
11 | var model = ViewModel(initialState)
12 | var eventNames = model.events
13 |
14 | // store route in model
15 | router.on("route", function (ev) {
16 | model.route.set(ev.hash)
17 | })
18 |
19 | events.on(eventNames.toggleAll, function (ev) {
20 | model.todos().array.forEach(function (todo) {
21 | todo.completed.set(!todo.completed())
22 | })
23 | })
24 |
25 | TodoItem(model, events)
26 |
27 | events.on(eventNames.add, function (ev) {
28 | model.todos.push(TodoModel({
29 | title: ev.currentValue
30 | }))
31 | model.todoField.set("")
32 | })
33 |
34 | return model
35 | }
36 |
37 | function TodoItem(model, events) {
38 | var eventNames = model.events
39 |
40 | events.on(eventNames.toggle, function (ev) {
41 | ev.meta.completed.set(!ev.meta.completed())
42 | })
43 |
44 | events.on(eventNames.editing, function (ev) {
45 | ev.meta.editing.set(true)
46 | })
47 |
48 | events.on(eventNames.destroy, function (ev) {
49 | var index = model.todos.indexOf(ev.meta)
50 | model.todos.splice(index, 1)
51 | })
52 |
53 | events.on(eventNames.edit, function (ev) {
54 | ev.meta.title.set(ev.currentValue)
55 | ev.meta.editing.set(false)
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/examples/todomvc/browser.js:
--------------------------------------------------------------------------------
1 | var localStorage = require("global/window").localStorage
2 | var document = require("global/document")
3 | var HashRouter = require("hash-router")
4 | var Dom = require("../../dom")
5 | var Delegator = require("../lib/dom-delegator.js")
6 | var serialize = require("../lib/observ-array-serialize.js")
7 | // var Delegator = require("dom-delegator")
8 | // var serialize = require("observ-array/serialize")
9 |
10 | var template = require("./template.js")
11 | var App = require("./app.js")
12 |
13 | // configure Dom rendered
14 | var dom = Dom([
15 | require("../../plugins/loose"),
16 | require("../../plugins/fragment"),
17 | require("../../plugins/observ"),
18 | require("../lib/plugin-either.js"),
19 | require("../lib/plugin-list.js"),
20 | require("../lib/plugin-event.js"),
21 | require("../lib/plugin-focus.js"),
22 | require("../lib/plugin-event-meta.js")
23 | ])
24 |
25 | // Read from db
26 | var storedState = localStorage.getItem("todomvc-jsonml")
27 | var initialState = storedState ? JSON.parse(storedState) : []
28 |
29 | // Inputs
30 | var delegator = Delegator()
31 | var router = HashRouter()
32 |
33 | // Get app to generate view model from inputs
34 | var viewModel = App(initialState, {
35 | events: delegator,
36 | router: router
37 | })
38 |
39 | // Renderer
40 | var tree = template(viewModel)
41 | // Render the tree
42 | var elem = dom(tree)
43 | document.body.appendChild(elem)
44 |
45 | // Store to db
46 | viewModel.todos(function (todos) {
47 | localStorage.setItem("todomvc-jsonml",
48 | JSON.stringify(serialize(todos)))
49 | })
50 |
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/change-event.js:
--------------------------------------------------------------------------------
1 | module.exports = change
2 |
3 | function change(name) {
4 | return {
5 | type: "event",
6 | name: name,
7 | eventName: "~change"
8 | }
9 | }
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/either.js:
--------------------------------------------------------------------------------
1 | module.exports = either
2 |
3 | function either(bool, left, right) {
4 | return {
5 | type: "either",
6 | bool: bool,
7 | left: left,
8 | right: right
9 | }
10 | }
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/event-meta.js:
--------------------------------------------------------------------------------
1 | module.exports = eventMeta
2 |
3 | function eventMeta(meta) {
4 | return { type: "event-meta", meta: meta }
5 | }
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/event.js:
--------------------------------------------------------------------------------
1 | module.exports = event
2 |
3 | function event(name, eventName) {
4 | return {
5 | type: "event",
6 | name: name,
7 | eventName: eventName
8 | }
9 | }
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/focus.js:
--------------------------------------------------------------------------------
1 | module.exports = focus
2 |
3 | function focus(observ) {
4 | return { type: "focus", bool: observ }
5 | }
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/list.js:
--------------------------------------------------------------------------------
1 | module.exports = list
2 |
3 | function list(array, generateTemplate) {
4 | return {
5 | type: "list",
6 | array: array,
7 | template: generateTemplate
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/todomvc/lib-template/submit-event.js:
--------------------------------------------------------------------------------
1 | submit-event.js
--------------------------------------------------------------------------------
/examples/todomvc/template.js:
--------------------------------------------------------------------------------
1 | var computed = require("observ/computed")
2 | var either = require("./lib-template/either.js")
3 | var list = require("./lib-template/list.js")
4 | var event = require("./lib-template/event.js")
5 | var changeEvent = require("./lib-template/change-event.js")
6 | var submitEvent = require("./lib-template/submit-event.js")
7 | var focus = require("./lib-template/focus.js")
8 | var eventMeta = require("./lib-template/event-meta.js")
9 |
10 | module.exports = template
11 |
12 | function template(model) {
13 | return ["div.todomvc-wrapper", [
14 | ["section.todoapp", [
15 | header(model),
16 | mainSection(model),
17 | statsSection(model)
18 | ]],
19 | infoFooter()
20 | ]]
21 | }
22 |
23 | function mainSection(model) {
24 | return ["section.main", {
25 | hidden: either(model.todosLength, false, true)
26 | }, [
27 | ["input#toggle-all.toggle-all", {
28 | type: "checkbox",
29 | checked: model.allComplete,
30 | change: event(model.events.toggleAll)
31 | }],
32 | ["label", { htmlFor: "toggle-all" }, "Mark all as complete"],
33 | ["ul.todo-list", list(model.visibleTodos, function (item) {
34 | return todoItem(item, model)
35 | })]
36 | ]]
37 | }
38 |
39 | function todoItem(todo, model) {
40 | var className = computed([
41 | todo.completed, todo.editing
42 | ], function (completed, editing) {
43 | return (completed ? "completed " : "") +
44 | (editing ? "editing" : "")
45 | })
46 |
47 | return ["li", {
48 | className: className,
49 | // when events occur from jsonml-event
50 | // you can access the nearest bound model with
51 | // `ev.meta`
52 | meta: eventMeta(todo)
53 | }, [
54 | ["div.view", [
55 | ["input.toggle", {
56 | type: "checkbox",
57 | checked: todo.completed,
58 | change: event(model.events.toggle)
59 | }],
60 | ["label", {
61 | dblclick: event(model.events.editing)
62 | }, todo.title],
63 | ["button.destroy", {
64 | click: event(model.events.destroy)
65 | }]
66 | ]],
67 | ["input.edit", {
68 | value: todo.title,
69 | // focus primitive, when observable is triggered
70 | // it calls .focus() on this element
71 | focus: focus(todo.editing),
72 | submit: event(model.events.edit),
73 | blur: event(model.events.edit)
74 | }]
75 | ]]
76 | }
77 |
78 | function statsSection(model) {
79 | return ["footer.footer", {
80 | hidden: either(model.todosLength, false, true)
81 | }, [
82 | ["span.todo-count", [
83 | ["strong", model.todosLeft],
84 | computed([model.todosLength], function (len) {
85 | return len === 1 ? " item" : " items"
86 | }),
87 | " left"
88 | ]],
89 | ["ul.filters", [
90 | link(model, "#/", "All", "all"),
91 | link(model, "#/active", "Active", "active"),
92 | link(model, "#/completed", "Completed", "completed")
93 | ]]
94 | ]]
95 | }
96 |
97 | function link(model, uri, text, expected) {
98 | return ["li", [
99 | ["a", {
100 | className: computed([model.route], function (route) {
101 | return route === expected ? "selected" : ""
102 | }),
103 | href: uri
104 | }, text]
105 | ]]
106 | }
107 |
108 | function header(model) {
109 | return ["header.header", [
110 | ["h1", "todos"],
111 | ["input.new-todo", {
112 | placeholder: "What needs to be done?",
113 | autofocus: true,
114 | value: model.todoField,
115 | submit: event(model.events.add)
116 | }]
117 | ]]
118 | }
119 |
120 | function infoFooter() {
121 | return ["footer.info", [
122 | ["p", "Double-click to edit a todo"],
123 | ["p", [
124 | "Written by ",
125 | ["a", { href: "https://github.com/Raynos" }, "Raynos"]
126 | ]],
127 | ["p", [
128 | "Part of ",
129 | ["a", { href: "http://todomvc.com" }, "TodoMVC"]
130 | ]]
131 | ]]
132 | }
133 |
--------------------------------------------------------------------------------
/examples/todomvc/todo-model.js:
--------------------------------------------------------------------------------
1 | var Observable = require("observ")
2 | var uuid = require("uuid")
3 |
4 | module.exports = TodoModel
5 |
6 | function TodoModel(todo) {
7 | todo = todo || {}
8 |
9 | return {
10 | title: Observable(String(todo.title)),
11 | id: uuid(),
12 | completed: Observable(Boolean(todo.completed)),
13 | editing: Observable(false)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/todomvc/view-model.js:
--------------------------------------------------------------------------------
1 | var Observable = require("observ")
2 | var computed = require("observ/computed")
3 | var uuid = require("uuid")
4 | var ObservableArray = require("../lib/observ-array.js")
5 | var computedFilter = require("../lib/computed-filter.js")
6 |
7 | var TodoModel = require("./todo-model.js")
8 |
9 | var toggleAll = uuid()
10 | var toggle = uuid()
11 | var editing = uuid()
12 | var destroy = uuid()
13 | var edit = uuid()
14 | var add = uuid()
15 |
16 | module.exports = ViewModel
17 |
18 | function ViewModel(initialState) {
19 | var todos = ObservableArray(initialState.map(TodoModel))
20 | var route = Observable("all")
21 | var todoField = Observable("")
22 |
23 | var openTodos = computedFilter(todos, [".completed"],
24 | function (item) {
25 | return item.completed === false
26 | })
27 | var todosLength = computed([todos], function (todos) {
28 | return todos.array.length
29 | })
30 | var todosLeft = computed([openTodos], function (todos) {
31 | return todos.array.length
32 | })
33 |
34 | var viewModel = {
35 | // RAW
36 | todos: todos,
37 | todoField: todoField,
38 | route: route,
39 |
40 | // COMPUTED
41 | todosLength: todosLength,
42 | allComplete: computed([todos], function (todos) {
43 | return todos.every(function (todo) {
44 | return todo.completed()
45 | })
46 | }),
47 | openTodos: openTodos,
48 | todosLeft: todosLeft,
49 | todosCompleted: computed([todosLength, todosLeft],
50 | function (len, left) {
51 | return len - left
52 | }),
53 | // refilters entire array when route changes
54 | // only refilters single item in list when list operation
55 | // listens to '.computed' property on list item
56 | // doesn't refilter entire array each time anything changes!
57 | // sends minimal diffs to DOM renderer :)
58 | visibleTodos: computedFilter(todos, [route, ".completed"],
59 | function (route, todo) {
60 | return route === "completed" && todo.completed() ||
61 | route === "active" && !todo.completed() ||
62 | route === "all"
63 | }),
64 | events: {
65 | toggleAll: toggleAll,
66 | toggle: toggle,
67 | editing: editing,
68 | destroy: destroy,
69 | edit: edit,
70 | add: add
71 | }
72 | }
73 |
74 | return viewModel
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/lib/get-next-elements.js:
--------------------------------------------------------------------------------
1 | module.exports = getNextElements
2 |
3 | function getNextElements(elem, opts) {
4 | var elements = opts.elements
5 | var children = toArray(elem.childNodes)
6 |
7 | if (children.length !== 0) {
8 | return children
9 | } else if (elements.length !== 0) {
10 | return elements
11 | } else {
12 | var parent = elem.parentNode
13 | if (parent === opts.root) {
14 | return
15 | }
16 |
17 | var grandParent = parent.parentNode
18 | var siblings = toArray(grandParent.childNodes)
19 | children = siblings.slice(siblings.indexOf(parent))
20 |
21 | return children
22 | }
23 | }
24 |
25 | function toArray(list) {
26 | return [].slice.call(list)
27 | }
28 |
--------------------------------------------------------------------------------
/lib/get-plugin.js:
--------------------------------------------------------------------------------
1 | var util = require("util")
2 |
3 | getPlugin.getPluginSafe = getPluginSafe
4 |
5 | module.exports = getPlugin
6 |
7 | function getPlugin(tree, opts) {
8 | if (Array.isArray(tree)) {
9 | throw new Error("Invalid JSONML data structure " +
10 | util.inspect(tree) + " Array is not a plugin")
11 | }
12 |
13 | var type = getType(tree)
14 | var plugin = findPlugin(opts.plugins, type)
15 |
16 | if (!plugin) {
17 | throw new Error("Invalid JSONML data structure " +
18 | util.inspect(tree) + " Unknown plugin " + type)
19 | }
20 |
21 | return plugin
22 | }
23 |
24 | function getPluginSafe(tree, opts) {
25 | return !!findPlugin(opts.plugins, getType(tree))
26 | }
27 |
28 | function getType(plugin) {
29 | if (typeof plugin === "function") {
30 | return "#function"
31 | }
32 |
33 | var type = plugin.type
34 |
35 | if (!type) {
36 | var keys = Object.keys(plugin)
37 |
38 | if (keys.length !== 1) {
39 | return false
40 | }
41 |
42 | type = keys[0]
43 | }
44 |
45 | return type
46 | }
47 |
48 | function findPlugin(plugins, type) {
49 | for (var i = 0; i < plugins.length; i++) {
50 | var plugin = plugins[i]
51 |
52 | if (plugin.type === type) {
53 | return plugin
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/is-plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = isPlugin
2 |
3 | function isPlugin(obj) {
4 | return !Array.isArray(obj) && (isObject(obj) || typeof obj === "function")
5 | }
6 |
7 | function isObject(obj) {
8 | return typeof obj === "object" && obj !== null
9 | }
10 |
--------------------------------------------------------------------------------
/lib/render-property.js:
--------------------------------------------------------------------------------
1 | var DataSet = require("data-set")
2 |
3 | var isPlugin = require("./is-plugin.js")
4 | var getPlugin = require("./get-plugin.js")
5 |
6 | module.exports = renderProperty
7 |
8 | function renderProperty(elem, value, key, opts) {
9 | if (key === "class") {
10 | key = "className"
11 | }
12 |
13 | if (key === "style") {
14 | Object.keys(value).forEach(function (key) {
15 | var styleValue = value[key]
16 |
17 | if (!elem.style) {
18 | elem.style = {}
19 | }
20 |
21 | if (isPlugin(styleValue)) {
22 | getPlugin(styleValue, opts)
23 | .renderStyle(elem, styleValue, key, opts)
24 | } else {
25 | elem.style[key] = styleValue
26 | }
27 | })
28 | } else if (isPlugin(value)) {
29 | getPlugin(value, opts).renderProperty(elem, value, key, opts)
30 | } else if (key.substr(0, 5) === "data-") {
31 | DataSet(elem)[key.substr(5)] = value
32 | } else {
33 | elem[key] = value
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/stringify-property.js:
--------------------------------------------------------------------------------
1 | var getPlugin = require("./get-plugin.js")
2 | var isPlugin = require("./is-plugin.js")
3 |
4 | var isDoubleQuote = /"/g
5 | var isSingleQuote = /'/g
6 | var camelCase = /([a-z][A-Z])/g
7 |
8 | module.exports = stringifyProperty
9 |
10 | function stringifyProperty(value, key, opts) {
11 | if (key === "style") {
12 | value = stylify(value)
13 | } else if (key === "className") {
14 | key = "class"
15 | }
16 |
17 | if (value === true) {
18 | return key
19 | } else if (value === false) {
20 | return ""
21 | }
22 |
23 | if (isPlugin(value)) {
24 | return getPlugin(value, opts)
25 | .stringifyProperty(value, key, opts)
26 | }
27 |
28 | return key + "=\"" + escapeHTMLAttributes(value) + "\""
29 | }
30 |
31 | function stylify(styles) {
32 | var attr = ""
33 | Object.keys(styles).forEach(function (key) {
34 | var value = styles[key]
35 | attr += hyphenate(key) + ": " + value + ";"
36 | })
37 | return attr
38 | }
39 |
40 | function hyphenate(key) {
41 | return key.replace(camelCase, function (group) {
42 | return group[0] + "-" + group[1].toLowerCase()
43 | })
44 | }
45 |
46 | function escapeHTMLAttributes(s) {
47 | return String(s)
48 | .replace(isDoubleQuote, """)
49 | .replace(isSingleQuote, "'")
50 | }
51 |
--------------------------------------------------------------------------------
/lib/unpack-selector.js:
--------------------------------------------------------------------------------
1 | var isPlugin = require("./is-plugin.js")
2 | var getPlugin = require("./get-plugin.js")
3 |
4 | var splitSelectorRegex = /([\.#]?[a-zA-Z0-9_-]+)/
5 |
6 | module.exports = unpackSelector
7 |
8 | function unpackSelector(selector, properties, opts) {
9 | var selectorMatches = selector.split(splitSelectorRegex)
10 | var tagName = "div"
11 |
12 | selectorMatches.forEach(function (match) {
13 | var value = match.substring(1, match.length)
14 |
15 | if (match[0] === ".") {
16 | setClassName(properties, function (curr) {
17 | return curr + value + " "
18 | }, opts)
19 | } else if (match[0] === "#") {
20 | properties.id = value
21 | } else if (match.length > 0) {
22 | tagName = match
23 | }
24 | })
25 |
26 | if (properties.className) {
27 | setClassName(properties, function (curr) {
28 | return curr.trim()
29 | }, opts)
30 | }
31 |
32 | return tagName
33 | }
34 |
35 | function setClassName(properties, lambda, opts) {
36 | if (isPlugin(properties.className)) {
37 | var plugin = getPlugin(properties.className, opts)
38 | var currValue = plugin.getProperty(properties.className, "className")
39 | var newValue = lambda(currValue)
40 | plugin.setProperty(properties.className, newValue, "className")
41 | } else {
42 | properties.className = lambda(properties.className || "")
43 | }
44 | }
--------------------------------------------------------------------------------
/merge-recur.js:
--------------------------------------------------------------------------------
1 | var extend = require("xtend")
2 |
3 | var isPlugin = require("./lib/is-plugin.js")
4 | var getPlugin = require("./lib/get-plugin.js")
5 | var getNextElements = require("./lib/get-next-elements.js")
6 | var unpackSelector = require("./lib/unpack-selector.js")
7 |
8 | module.exports = mergeRecur
9 |
10 | function mergeRecur(tree, opts) {
11 | if (tree === null) {
12 | return
13 | } else if (isPlugin(tree)) {
14 | return getPlugin(tree, opts).merge(tree, opts)
15 | }
16 |
17 | var selector = tree[0]
18 | var textContent = tree[2]
19 |
20 | if (selector === "#text") {
21 | return mergeText(textContent, opts)
22 | }
23 |
24 | return mergeElement(tree, opts)
25 | }
26 |
27 | function mergeElement(tree, opts) {
28 | var selector = tree[0]
29 | var properties = tree[1]
30 | var children = tree[2]
31 |
32 | var tagName = unpackSelector(selector, properties)
33 | var elem = opts.elements.shift()
34 |
35 | if (elem.nodeType === 1 &&
36 | elem.tagName.toLowerCase() === tagName.toLowerCase() &&
37 | (!properties.id || properties.id === elem.id) &&
38 | (!properties.className || properties.className === elem.className)
39 | ) {
40 | //TODO do something with properties?
41 |
42 | for (var i = 0; i < children.length; i++) {
43 | var childOpts = extend(opts, {
44 | parent: tree,
45 | parents: opts.parents.concat([tree])
46 | })
47 |
48 | mergeRecur(children[i], childOpts)
49 | }
50 |
51 | return
52 | }
53 |
54 | var nextElements = getNextElements(elem, opts)
55 |
56 | return nextElements ? mergeElement(tree, extend(opts, {
57 | elements: nextElements
58 | })) : null
59 | }
60 |
61 | function mergeText(textContent, opts) {
62 | var elem = opts.elements.shift()
63 |
64 | if (elem.nodeType === 3) {
65 | if (elem.data !== textContent) {
66 | elem.data = textContent
67 | return
68 | }
69 | return
70 | }
71 |
72 | var nextElements = getNextElements(elem, opts)
73 |
74 | return nextElements ? mergeText(textContent, extend(opts, {
75 | elements: nextElements
76 | })) : null
77 | }
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/merge.js:
--------------------------------------------------------------------------------
1 | var normalize = require("./normalize.js")
2 | var mergeRecur = require("./merge-recur.js")
3 |
4 | module.exports = Merge
5 |
6 | function Merge(plugins) {
7 | return function merge(tree, opts) {
8 | opts = opts || {}
9 | opts.elements = opts.elements || [opts.root]
10 |
11 | return mergeRecur(normalize(tree, opts, plugins), opts)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/normalize.js:
--------------------------------------------------------------------------------
1 | module.exports = normalize
2 |
3 | function normalize(tree, opts, plugins) {
4 | opts = opts || {}
5 | opts.plugins = (opts.plugins || []).concat(plugins || [])
6 | opts.parent = opts.parent || null
7 | opts.parents = opts.parents || []
8 |
9 | opts.plugins.forEach(function (plugin) {
10 | if (typeof plugin.normalize === "function") {
11 | tree = plugin.normalize(tree, opts)
12 | }
13 | })
14 |
15 | return tree
16 | }
17 |
--------------------------------------------------------------------------------
/old_example/apply-observ.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 | var DataSet = require("data-set")
3 | var element = require("element")
4 |
5 | var renderObserv = require("./render-observ.js")
6 | var normalize = require("../normalize")
7 | var unpackSelector = require("../unpack-selector")
8 |
9 | module.exports = applyObserv
10 |
11 | // FRAGMENT NOT IMPLEMENTED
12 | function applyObserv(surface, jsonml) {
13 | jsonml = normalize(jsonml)
14 |
15 | if (jsonml === null) {
16 | return
17 | } else if (typeof jsonml === "string") {
18 | return
19 | } else if (!!jsonml && typeof jsonml.raw === "string") {
20 | return
21 | } else if (!!jsonml && Array.isArray(jsonml.fragment)) {
22 | return
23 | } else if (typeof jsonml === "function") {
24 | var elem = surface
25 |
26 | jsonml(function (jsonml) {
27 | jsonml = normalize(jsonml)
28 |
29 | elem = replaceNode(elem, jsonml)
30 | })
31 |
32 | return
33 | }
34 |
35 | var children = jsonml[2]
36 | var childNodes = [].slice.call(surface.childNodes)
37 |
38 | children.forEach(function (child, index) {
39 | var elem = childNodes[index]
40 |
41 | if (child && Array.isArray(child.fragment)) {
42 | var count = child.fragment.length
43 | elem = [elem].concat(childNodes.splice(index + 1, count - 1))
44 | } else if (child && typeof child === "function" &&
45 | child() && Array.isArray(child().fragment)
46 | ) {
47 | var count = child().fragment.length
48 | elem = [elem].concat(childNodes.splice(index + 1, count - 1))
49 | }
50 |
51 | applyObserv(elem, child)
52 | })
53 | }
54 |
55 | // replaceNode(Element | Array, JsonML)
56 | function replaceNode(elem, jsonml) {
57 | var target = renderObserv(jsonml)
58 | var targetElem = target
59 |
60 | if (target === null) {
61 | target = targetElem = placeholder()
62 | } else if (target.nodeName === "#document-fragment") {
63 | targetElem = [].slice.call(target.childNodes)
64 | }
65 |
66 | if (Array.isArray(elem)) {
67 | elem[0].parentNode.replaceChild(target, elem[0])
68 | elem.slice(1).forEach(function (elem) {
69 | elem.parentNode.removeChild(elem)
70 | })
71 | } else {
72 | elem.parentNode.replaceChild(target, elem)
73 | }
74 |
75 | return targetElem
76 | }
77 |
78 | function placeholder() {
79 | var span = document.createElement("span")
80 | span.dataset.placeholder = true
81 | return span
82 | }
83 |
84 | function getFragChildren(list, frag) {
85 | if (frag.nodeName !== "#document-fragment") {
86 | return
87 | }
88 |
89 | list.length = 0
90 | for (var i = 0; i < frag.childNodes; i++) {
91 | list[i] = frag.childNodes[i]
92 | }
93 | }
--------------------------------------------------------------------------------
/old_example/browser.js:
--------------------------------------------------------------------------------
1 | var Observ = require("observ")
2 | var renderObserv = require("./render-observ")
3 | var applyObserv = require("./apply-observ")
4 | var JSONGlobals = require("json-globals/get")
5 |
6 | var ObservableArray = require("./observable-array.js")
7 | var template = require("./template")
8 |
9 | var state = JSONGlobals("state")
10 | var model = window.model = Object.keys(state).reduce(function (acc, key) {
11 | var value = state[key]
12 |
13 | if (Array.isArray(value.value) && Array.isArray(value.diff)) {
14 | acc[key] = ObservableArray(value.value)
15 | } else {
16 | acc[key] = Observ(value)
17 | }
18 | return acc
19 | }, {})
20 | var mainElem = document.getElementById("main").firstChild
21 |
22 | if (mainElem) {
23 | console.log("APPLY")
24 | applyObserv(mainElem, template(model))
25 | } else {
26 | console.log("RENDER")
27 | var elem = renderObserv(template(model))
28 | document.body.appendChild(elem)
29 | }
--------------------------------------------------------------------------------
/old_example/observable-array.js:
--------------------------------------------------------------------------------
1 | var Observable = require("observ")
2 |
3 | var slice = Array.prototype.slice
4 |
5 | module.exports = ObservableArray
6 |
7 | /* ObservableArray := (Array) => Observable<{
8 | value: Array,
9 | diff: Array
10 | }> & {
11 | splice: (index: Number, amount: Number, rest...: T) =>
12 | Array,
13 | push: (values...: T) => Number,
14 | filter: (lambda: Function, thisValue: Any) => Array,
15 | indexOf: (item: T, fromIndex: Number) => Number
16 | }
17 | */
18 | function ObservableArray(list) {
19 | var obs = Observable({ value: list, diff: [] })
20 |
21 | var length = list.length
22 | obs.splice = function (index, amount) {
23 | var args = slice.call(arguments, 0)
24 | var currentList = obs().value.slice()
25 |
26 | var removed = currentList.splice.apply(currentList, args)
27 | length = currentList.length
28 |
29 | obs.set({ value: currentList, diff: args })
30 | return removed
31 | }
32 | obs.push = function () {
33 | var args = slice.call(arguments)
34 | args.unshift(length, 0)
35 | obs.splice.apply(null, args)
36 |
37 | return obs.length
38 | }
39 | obs.unshift = function () {
40 | var args = slice.call(arguments)
41 | args.unshift(0, 0)
42 | obs.splice.apply(null, args)
43 |
44 | return obs.length
45 | }
46 | obs.filter = function () {
47 | var list = obs().value
48 | return list.filter.apply(list, arguments)
49 | }
50 | obs.indexOf = function (item, fromIndex) {
51 | return obs().value.indexOf(item, fromIndex)
52 | }
53 |
54 | return obs
55 | }
56 |
--------------------------------------------------------------------------------
/old_example/render-observ.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 | var DataSet = require("data-set")
3 | var element = require("element")
4 |
5 | var normalize = require("../normalize")
6 | var unpackSelector = require("../unpack-selector")
7 |
8 | module.exports = renderObserv
9 |
10 | function renderObserv(jsonml) {
11 | jsonml = normalize(jsonml)
12 |
13 | if (jsonml === null) {
14 | return null
15 | } else if (typeof jsonml === "string") {
16 | return document.createTextNode(jsonml)
17 | } else if (!!jsonml && typeof jsonml.raw === "string") {
18 | return element(jsonml.raw)
19 | } else if (!!jsonml && Array.isArray(jsonml.fragment)) {
20 | var frag = document.createDocumentFragment()
21 | jsonml.fragment.forEach(function (child) {
22 | frag.appendChild(renderObserv(child))
23 | })
24 | return frag
25 | } else if (typeof jsonml === "function") {
26 | var elem = renderObserv(jsonml())
27 | var elems = []
28 |
29 | if (elem === null) {
30 | elem = placeholder()
31 | }
32 |
33 | getFragChildren(elems, elem)
34 |
35 | jsonml(function (jsonml) {
36 | var target = renderObserv(jsonml)
37 |
38 | if (target === null) {
39 | target = placeholder()
40 | }
41 |
42 | if (elems.length) {
43 | elems[0].parentNode.replaceChild(target, elem[0])
44 | elems.slice(1).forEach(function (elem) {
45 | elem.parentNode.removeChild(elem)
46 | })
47 | } else {
48 | elem.parentNode.replaceChild(target, elem)
49 | }
50 |
51 | getFragChildren(elems, target)
52 | elem = target
53 | })
54 |
55 | return elem
56 | }
57 |
58 | var selector = jsonml[0]
59 | var properties = jsonml[1]
60 | var children = jsonml[2]
61 |
62 | var tagName = unpackSelector(selector, properties)
63 |
64 | var elem = document.createElement(tagName.toUpperCase())
65 | Object.keys(properties).forEach(function (k) {
66 | if (k === "class") {
67 | elem.className = properties[k]
68 | } else if (k === "style") {
69 | var style = properties.style
70 |
71 | Object.keys(style).forEach(function (key) {
72 | elem.style[key] = style[key]
73 | })
74 | } else if (k.substr(0, 5) === "data-") {
75 | DataSet(elem)[k.substr(5)] = properties[k]
76 | } else {
77 | elem[k] = properties[k]
78 | }
79 | })
80 |
81 | children.forEach(function (child) {
82 | elem.appendChild(renderObserv(child))
83 | })
84 |
85 | return elem
86 | }
87 |
88 | function placeholder() {
89 | var span = document.createElement("span")
90 | span.dataset.placeholder = true
91 | return span
92 | }
93 |
94 | function getFragChildren(list, frag) {
95 | if (frag.nodeName !== "#document-fragment") {
96 | return
97 | }
98 |
99 | list.length = 0
100 | for (var i = 0; i < frag.childNodes; i++) {
101 | list[i] = frag.childNodes[i]
102 | }
103 | }
--------------------------------------------------------------------------------
/old_example/server.js:
--------------------------------------------------------------------------------
1 | var http = require("http")
2 | var ServeBrowserify = require("serve-browserify")
3 | var JSONGlobals = require("json-globals")
4 | var Observ = require("observ")
5 |
6 | var ObservableArray = require("./observable-array.js")
7 | var stringify = require("./stringify-observ.js")
8 | var template = require("./template")
9 |
10 | http.createServer(function (req, res) {
11 | if (req.url === "/browser") {
12 | ServeBrowserify({ root: __dirname })(req, res)
13 | } else {
14 | var model = {
15 | x: Observ("x"),
16 | y: Observ("y"),
17 | zs: ObservableArray(["1", "2", "3"])
18 | }
19 | var jsonModel = Object.keys(model).
20 | reduce(function (acc, key) {
21 | acc[key] = model[key]()
22 | return acc
23 | }, {})
24 |
25 | res.setHeader("Content-Type", "text/html")
26 | res.end("" + stringify(["html", [
27 | ["head", [
28 | ["title", "Observable demo"]
29 | ]],
30 | ["body", [
31 | ["div", { id: "main" }, [
32 | template(model)
33 | ]],
34 | ["script", JSONGlobals({ state: jsonModel })],
35 | ["script", { src: "/browser" }]
36 | ]]
37 | ]]))
38 | }
39 | }).listen(8000, function () {
40 | console.log("listening on port 8000")
41 | })
42 |
--------------------------------------------------------------------------------
/old_example/stringify-observ.js:
--------------------------------------------------------------------------------
1 | var decode = require("he").decode
2 | var util = require("util")
3 |
4 | var normalize = require("../normalize")
5 | var unpackSelector = require("../unpack-selector")
6 | var props = require("../props")
7 | var escapeHTMLTextContent = require("../escape-text-content")
8 | var whitespaceSensitive = ["pre", "textarea"]
9 |
10 | module.exports = stringify
11 |
12 | /*
13 | @require ./jsonml.types
14 |
15 | stringify := (jsonml: JsonML, opts?: Object) => String
16 | */
17 | function stringify(jsonml, opts) {
18 | opts = opts || {}
19 | jsonml = normalize(jsonml)
20 | var indentation = opts.indentation || ""
21 | var parentTagName = opts.parentTagName || ""
22 | var strings = []
23 | var firstChild, useWhitespace
24 |
25 | if (jsonml === null) {
26 | return "
"
27 | } else if (typeof jsonml === "string") {
28 | return escapeHTMLTextContent(jsonml, parentTagName)
29 | } else if (!!jsonml && typeof jsonml.raw === "string") {
30 | return decode(jsonml.raw)
31 | } else if (!!jsonml && Array.isArray(jsonml.fragment)) {
32 | if (jsonml.fragment.length === 0) {
33 | return ""
34 | }
35 |
36 | renderChildren(jsonml.fragment)
37 | return strings.join("")
38 | } else if (typeof jsonml === "function") {
39 | return stringify(jsonml())
40 | }
41 |
42 | var selector = jsonml[0]
43 | var properties = jsonml[1]
44 | var children = jsonml[2]
45 |
46 | var tagName = unpackSelector(selector, properties)
47 |
48 | strings.push("<" + tagName + props(properties) + ">")
49 |
50 | if (children.length > 0) {
51 | renderChildren(children)
52 |
53 | strings.push("" + tagName + ">")
54 | } else {
55 | strings.push("" + tagName + ">")
56 | }
57 |
58 | return strings.join("")
59 |
60 | function renderChildren(children, indent, whitespace, newLine) {
61 | children.forEach(function (childML) {
62 | if (childML === null || childML === undefined) {
63 | throw new Error("Invalid JSONML data structure " +
64 | util.inspect(jsonml))
65 | }
66 |
67 | var text = stringify(childML, {
68 | parentTagName: tagName
69 | })
70 |
71 | strings.push(text)
72 | })
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/old_example/template.js:
--------------------------------------------------------------------------------
1 | var template = function (model) {
2 | return ["div", [
3 | either(model.x, ["div", [
4 | ["li", ["x: ", model.x]],
5 | ["li", ["y: ", model.y]]
6 | ]], null),
7 | ["p", model.y],
8 | ["ol", [
9 | list(model.zs, function (value) {
10 | return ["li", [
11 | ["span", "item"],
12 | ["span", value]
13 | ]]
14 | })
15 | ]]
16 | ]]
17 | }
18 |
19 | module.exports = template
20 |
21 | function either(observ, left, right) {
22 | return map(observ, function (state) {
23 | return state ? left : right
24 | })
25 | }
26 |
27 | function list(observvArray, generateTemplate) {
28 | return map(observvArray, function (record) {
29 | if (!record) {
30 | return null
31 | }
32 |
33 | var arr = record.value
34 |
35 | return { fragment: arr.map(generateTemplate) }
36 | })
37 | }
38 |
39 |
40 | function map(obs, lambda) {
41 | var state = lambda(obs())
42 |
43 | return function observ(listener) {
44 | if (!listener) {
45 | return state
46 | }
47 |
48 | obs(function (v) {
49 | state = lambda(v)
50 | listener(state)
51 | })
52 | }
53 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsonml-stringify",
3 | "version": "1.0.1",
4 | "description": "Convert jsonml arrays to html strings",
5 | "keywords": [],
6 | "author": "Raynos
",
7 | "repository": "git://github.com/Raynos/jsonml-stringify.git",
8 | "main": "index",
9 | "homepage": "https://github.com/Raynos/jsonml-stringify",
10 | "contributors": [
11 | {
12 | "name": "Raynos"
13 | }
14 | ],
15 | "bugs": {
16 | "url": "https://github.com/Raynos/jsonml-stringify/issues",
17 | "email": "raynos2@gmail.com"
18 | },
19 | "dependencies": {
20 | "data-set": "~0.2.2",
21 | "element": "~0.1.4",
22 | "global": "~2.0.7",
23 | "he": "~0.4.1",
24 | "insert": "~1.0.1"
25 | },
26 | "devDependencies": {
27 | "tape": "~1.0.4",
28 | "beefy": "~0.4.1",
29 | "serve-browserify": "~0.3.3",
30 | "observ": "~0.1.3",
31 | "json-globals": "~0.1.3",
32 | "xtend": "~2.1.1"
33 | },
34 | "licenses": [
35 | {
36 | "type": "MIT",
37 | "url": "http://github.com/Raynos/jsonml-stringify/raw/master/LICENSE"
38 | }
39 | ],
40 | "scripts": {
41 | "test": "node ./test/index.js",
42 | "start": "node ./index.js",
43 | "watch": "nodemon -w ./index.js index.js",
44 | "travis-test": "istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)",
45 | "cover": "istanbul cover --report none --print detail ./test/index.js",
46 | "view-cover": "istanbul report html && google-chrome ./coverage/index.html",
47 | "test-browser": "testem-browser ./test/browser/index.js",
48 | "testem": "testem-both -b=./test/browser/index.js"
49 | },
50 | "testling": {
51 | "files": "test/index.js",
52 | "browsers": [
53 | "ie/8..latest",
54 | "firefox/16..latest",
55 | "firefox/nightly",
56 | "chrome/22..latest",
57 | "chrome/canary",
58 | "opera/12..latest",
59 | "opera/next",
60 | "safari/5.1..latest",
61 | "ipad/6.0..latest",
62 | "iphone/6.0..latest",
63 | "android-browser/4.2..latest"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/plugins/fragment.js:
--------------------------------------------------------------------------------
1 | var document = require("global/document")
2 |
3 | var stringifyRecur = require("../stringify-recur.js")
4 | var domRecur = require("../dom-recur.js")
5 | var normalize = require("../normalize.js")
6 |
7 | module.exports = {
8 | stringify: function (tree, opts) {
9 | var strings = []
10 |
11 | for (var i = 0; i < tree.fragment.length; i++) {
12 | strings.push(stringify(tree.fragment[i], opts))
13 | }
14 |
15 | return strings.join("")
16 | },
17 | dom: function (tree, opts) {
18 | var frag = document.createDocumentFragment()
19 | tree.fragment.forEach(function (child) {
20 | var elem = dom(child, opts)
21 |
22 | if (elem !== null) {
23 | frag.appendChild(elem)
24 | }
25 | })
26 | return frag
27 | },
28 | type: "fragment"
29 | }
30 |
31 | function stringify(tree, opts) {
32 | return stringifyRecur(normalize(tree, opts), opts)
33 | }
34 |
35 | function dom(tree, opts) {
36 | return domRecur(normalize(tree, opts), opts)
37 | }
38 |
--------------------------------------------------------------------------------
/plugins/loose.js:
--------------------------------------------------------------------------------
1 | var util = require("util")
2 | var extend = require("xtend")
3 |
4 | var isPluginFast = require("../lib/is-plugin.js")
5 | var getPluginSafe = require("../lib/get-plugin.js").getPluginSafe
6 |
7 | module.exports = {
8 | normalize: normalizeTree
9 | }
10 |
11 | function normalizeTree(tree, opts) {
12 | if (tree === null || tree === undefined) {
13 | return tree
14 | }
15 |
16 | if (typeof tree === "string") {
17 | return ["#text", {}, tree]
18 | }
19 |
20 | if (isPluginFast(tree)) {
21 | return tree
22 | }
23 |
24 | if (!Array.isArray(tree)) {
25 | throw new Error("Invalid JSONML data structure " +
26 | util.inspect(tree) + " Non array is not a valid elem")
27 | }
28 |
29 | if (tree.length === 0) {
30 | throw new Error("Invalid JSONML data structure " +
31 | util.inspect(tree) + " Empty array is not a valid elem")
32 | }
33 |
34 | var selector = tree[0]
35 | var properties = tree[1] || {}
36 | var children = tree[2] || []
37 |
38 |
39 |
40 | if (!tree[2] && isChildren(properties, opts)) {
41 | children = properties
42 | properties = {}
43 | }
44 |
45 | if (isPluginFast(children)) {
46 | children = [children]
47 | }
48 |
49 | if (typeof children === "string" && selector !== "#text") {
50 | children = [["#text", {}, children]]
51 | }
52 |
53 | var jsonml = [selector, properties, children]
54 |
55 | if (opts.recur !== false && Array.isArray(children)) {
56 | jsonml[2] = children.map(function (child) {
57 | return normalizeTree(child, extend(opts, {
58 | parent: jsonml,
59 | parents: opts.parents.concat([jsonml])
60 | }))
61 | })
62 | }
63 |
64 | if (typeof selector !== "string") {
65 | throw new Error("Invalid JSONML data structure " +
66 | util.inspect(jsonml) + " Selector is not a string")
67 | }
68 |
69 | if (!isObject(properties)) {
70 | throw new Error("Invalid JSONML data structure " +
71 | util.inspect(jsonml) + " Properties is not an object")
72 | }
73 |
74 | if (typeof selector === "#text" && typeof children !== "string") {
75 | throw new Error("Invalid JSONML data structure " +
76 | util.inspect(jsonml) + " Text node needs to contain text")
77 | }
78 |
79 | return jsonml
80 | }
81 |
82 | function isChildren(maybeChildren, opts) {
83 | return Array.isArray(maybeChildren) ||
84 | typeof maybeChildren === "string" ||
85 | !!getPluginSafe(maybeChildren, opts)
86 | }
87 |
88 | function isObject(obj) {
89 | return typeof obj === "object" && obj !== null
90 | }
91 |
--------------------------------------------------------------------------------
/plugins/observ.js:
--------------------------------------------------------------------------------
1 | var DataSet = require("data-set")
2 |
3 | var stringifyRecur = require("../stringify-recur.js")
4 | var domRecur = require("../dom-recur.js")
5 | var normalize = require("../normalize.js")
6 |
7 | module.exports = {
8 | stringify: function (tree, opts) {
9 | return stringify(tree(), opts)
10 | },
11 | dom: function (tree, opts) {
12 | var elem = dom(tree(), opts)
13 |
14 | tree(function (value) {
15 | elem.data = value
16 | })
17 |
18 | return elem
19 | },
20 | renderStyle: function (elem, styleValue, key) {
21 | var curr = styleValue()
22 |
23 | elem.style[key] = curr
24 |
25 | styleValue(function (value) {
26 | elem.style[key] = value
27 | })
28 | },
29 | renderProperty: function (elem, value, key) {
30 | var curr = value()
31 |
32 | setProperty(elem, key, curr)
33 |
34 | value(function (value) {
35 | setProperty(elem, key, value)
36 | })
37 | },
38 | getProperty: function (value, key) {
39 | return value()
40 | },
41 | setProperty: function (value, str, key) {
42 | value.set(str)
43 | },
44 | type: "#function"
45 | }
46 |
47 | function setProperty(elem, key, value) {
48 | if (key.substr(0, 5) === "data-") {
49 | DataSet(elem)[key.substr(5)] = value
50 | } else {
51 | elem[key] = value
52 | }
53 | }
54 |
55 | function stringify(tree, opts) {
56 | return stringifyRecur(normalize(tree, opts), opts)
57 | }
58 |
59 | function dom(tree, opts) {
60 | return domRecur(normalize(tree, opts), opts)
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/raw.js:
--------------------------------------------------------------------------------
1 | var decode = require("he").decode
2 | var element = require("element")
3 |
4 | module.exports = {
5 | stringify: function (tree) {
6 | return decode(tree.raw)
7 | },
8 | dom: function (tree) {
9 | return element(tree.raw)
10 | },
11 | type: "raw"
12 | }
13 |
--------------------------------------------------------------------------------
/stringify-recur.js:
--------------------------------------------------------------------------------
1 | var util = require("util")
2 | var encode = require("he").encode
3 | var extend = require("xtend")
4 |
5 | var unpackSelector = require("./lib/unpack-selector.js")
6 | var stringifyProperty = require("./lib/stringify-property.js")
7 | var isPlugin = require("./lib/is-plugin.js")
8 | var getPlugin = require("./lib/get-plugin.js")
9 |
10 | var endingScriptTag = /<\/script>/g
11 |
12 | module.exports = stringifyRecur
13 |
14 | function stringifyRecur(tree, opts) {
15 | if (tree === null) {
16 | return ""
17 | } else if (isPlugin(tree)) {
18 | return getPlugin(tree, opts).stringify(tree, opts)
19 | }
20 |
21 | var selector = tree[0]
22 | var properties = tree[1]
23 | var children = tree[2]
24 | var strings = []
25 |
26 | if (selector === "#text") {
27 | return escapeHTMLTextContent(children, opts)
28 | }
29 |
30 | var tagName = unpackSelector(selector, properties)
31 | var attrString = Object.keys(properties).map(function (key) {
32 | var value = properties[key]
33 |
34 | return stringifyProperty(value, key, opts)
35 | }).join(" ").trim()
36 | attrString = attrString === "" ? "" : " " + attrString
37 |
38 | strings.push("<" + tagName + attrString + ">")
39 |
40 | if (!children) {
41 | throw new Error("Invalid JSONML data structure " +
42 | util.inspect(tree) + " No children")
43 | }
44 |
45 | for (var i = 0; i < children.length; i++) {
46 | var childOpts = extend(opts, {
47 | parent: tree,
48 | parents: opts.parents.concat([tree])
49 | })
50 |
51 | strings.push(stringifyRecur(children[i], childOpts))
52 | }
53 |
54 | strings.push("" + tagName + ">")
55 |
56 | return strings.join("")
57 | }
58 |
59 | function escapeHTMLTextContent(string, opts) {
60 | var selector = opts.parent ? opts.parent[0] : ""
61 | var tagName = unpackSelector(selector, {})
62 |
63 | var escaped = String(string)
64 |
65 | if (tagName !== "script" && tagName !== "style") {
66 | escaped = encode(escaped)
67 | } else if (tagName === "script") {
68 | escaped = escaped.replace(endingScriptTag, "<\\\/script>")
69 | }
70 |
71 | return escaped
72 | }
73 |
--------------------------------------------------------------------------------
/stringify.js:
--------------------------------------------------------------------------------
1 | var normalize = require("./normalize.js")
2 | var stringifyRecur = require("./stringify-recur.js")
3 |
4 | module.exports = Stringify
5 |
6 | function Stringify(plugins) {
7 | return function stringify(tree, opts) {
8 | opts = opts || {}
9 |
10 | return stringifyRecur(normalize(tree, opts, plugins), opts)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/dom.js:
--------------------------------------------------------------------------------
1 | var test = require("tape")
2 | var document = require("global/document")
3 |
4 | var Dom = require("../dom")
5 | var dom = Dom([
6 | require("../plugins/loose.js"),
7 | require("../plugins/fragment.js"),
8 | require("../plugins/raw.js")
9 | ])
10 |
11 | test("dom properly converts jsonml to element", function (assert) {
12 | var elem = dom(["html", [
13 | ["head", { className: "head" }, [
14 | ["meta", { charset: "utf-8" }],
15 | ["title", "Process dashboard"],
16 | ["link", { rel: "stylesheet", href: "/less/main"}]
17 | ]],
18 | ["body", { class: "main" }, [
19 | ["script", { src: "/browserify/main" }]
20 | ]]
21 | ]])
22 |
23 | assert.ok(elem)
24 | assert.equal(elem.tagName, "HTML")
25 | assert.equal(elem.childNodes.length, 2)
26 |
27 | assert.equal(elem.childNodes[0].tagName, "HEAD")
28 | assert.equal(elem.childNodes[0].className, "head")
29 | assert.equal(elem.childNodes[1].tagName, "BODY")
30 | assert.equal(elem.childNodes[1].className, "main")
31 |
32 | assert.end()
33 | })
34 |
35 | test("allow raw data", function (assert) {
36 | if (!document.defaultView) {
37 | return assert.end()
38 | }
39 |
40 | var elem = dom(["span", [{
41 | raw: " |"
42 | }]])
43 |
44 | assert.equal(elem.childNodes[0].data, "\u00A0\u00A0\u00A0|")
45 |
46 | assert.end()
47 | })
48 |
49 | test("allow raw html", function (assert) {
50 | if (!document.defaultView) {
51 | return assert.end()
52 | }
53 |
54 | var elem = dom(["div", [{
55 | raw: "Foo
"
56 | }]])
57 |
58 | assert.equal(elem.childNodes[0].tagName, "P")
59 | assert.equal(elem.childNodes[0].childNodes[0].data, "Foo")
60 |
61 | assert.end()
62 | })
63 |
64 |
65 | test("allow raw multi html", function (assert) {
66 | if (!document.defaultView) {
67 | return assert.end()
68 | }
69 |
70 | var elem = dom(["div", [{
71 | raw: "Foo
Bar
"
72 | }]])
73 |
74 | assert.equal(elem.childNodes[0].tagName, "P")
75 | assert.equal(elem.childNodes[0].childNodes[0].data, "Foo")
76 | assert.equal(elem.childNodes[1].tagName, "P")
77 | assert.equal(elem.childNodes[1].childNodes[0].data, "Bar")
78 |
79 | assert.end()
80 | })
81 |
82 | test("style properties", function (assert) {
83 | var elem = dom(["div", {
84 | style: { borderColor: "black" }
85 | }])
86 |
87 | assert.equal(elem.style.borderColor, "black")
88 |
89 | assert.end()
90 | })
91 |
92 |
93 | test("null is a valid element", function (assert) {
94 | var elem = dom(null)
95 |
96 | assert.equal(elem, null)
97 |
98 | assert.end()
99 | })
100 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | require("./stringify.js")
2 | require("./dom.js")
3 | require("./normalize.js")
4 | require("./integration/timezone-dropdown.js")
5 |
--------------------------------------------------------------------------------
/test/integration/timezone-dropdown.js:
--------------------------------------------------------------------------------
1 | var test = require("tape")
2 |
3 | var Stringify = require("../../stringify")
4 | var stringify = Stringify([
5 | require("../../plugins/loose.js"),
6 | require("../../plugins/fragment.js"),
7 | require("../../plugins/raw.js")
8 | ])
9 | var Dom = require("../../dom")
10 | var dom = Dom([
11 | require("../../plugins/loose.js"),
12 | require("../../plugins/fragment.js"),
13 | require("../../plugins/raw.js")
14 | ])
15 |
16 | var dropdown = [".string-dropdown.form-elem", [
17 | ["label.label", {
18 | for: "b499aad0-2711-4909-8936-fe6b27c1ccb6~timezone"
19 | }, "timezone"],
20 | ["select.input", {
21 | name: "timezone",
22 | id: "b499aad0-2711-4909-8936-fe6b27c1ccb6~timezone",
23 | "data-marker": "form.timezone"
24 | }, [
25 | ["option", {
26 | selected: true,
27 | value: "",
28 | }, "Please enter your timezone"],
29 | { fragment: [
30 | ["option", {
31 | value: "Africa/Abidjan",
32 | selected: false
33 | }, "Africa/Abidjan"],
34 | ["option", {
35 | value: "Africa/Accra",
36 | selected: false
37 | }, "Africa/Accra"],
38 | ["option", {
39 | value: "Africa/Addis_Ababa",
40 | selected: false
41 | }, "Africa/Addis_Ababa"],
42 | ["option", {
43 | value: "Africa/Algiers",
44 | selected: false
45 | }, "Africa/Algiers"]
46 | ] }
47 | ]],
48 | [".error", {
49 | "data-marker": "errors.timezone"
50 | }]
51 | ]]
52 |
53 | test("timezone dropdown", function (assert) {
54 | var elem = stringify(dropdown)
55 |
56 | assert.equal(elem, "" +
57 | "" +
58 | "
" +
60 | "
" +
71 | "
" +
72 | "
")
73 |
74 | assert.end()
75 | })
76 |
77 | // test("timezone dropdown (dom)", function (assert) {
78 | // var elem = dom(dropdown)
79 |
80 | // assert.equal(dom.tagName, "div")
81 |
82 | // assert.end()
83 | // })
--------------------------------------------------------------------------------
/test/normalize.js:
--------------------------------------------------------------------------------
1 | var test = require("tape")
2 |
3 | var Stringify = require("../stringify")
4 | var stringify = Stringify([
5 | require("../plugins/loose.js"),
6 | require("../plugins/fragment.js"),
7 | require("../plugins/raw.js")
8 | ])
9 |
10 | var children = [
11 | "foo",
12 | { raw: "foo©" },
13 | null,
14 | ["span"],
15 | ["span", { raw: "foo©"} ],
16 | ["span", "foo"],
17 | ["span", { class: "foo" }]
18 | ]
19 |
20 | var childrenString =
21 | "foo" +
22 | "foo©" +
23 | "" +
24 | "foo©" +
25 | "foo" +
26 | ""
27 |
28 | var whiteChildrenString =
29 | "foo" +
30 | "foo©" +
31 | "" +
32 | "foo©" +
33 | "foo" +
34 | ""
35 |
36 | test("String is valid", function (assert) {
37 | var html = stringify("foobar")
38 |
39 | assert.equal(html, "foobar")
40 |
41 | assert.end()
42 | })
43 |
44 | test("{ raw: String } is valid", function (assert) {
45 | var html = stringify({ raw: "foo©"})
46 |
47 | assert.equal(html, "foo©")
48 |
49 | assert.end()
50 | })
51 |
52 | test("{ fragment: Array } is valid", function (assert) {
53 | var html = stringify({ fragment: children })
54 |
55 | assert.equal(html, childrenString)
56 |
57 | assert.end()
58 | })
59 |
60 | test("[String] is valid", function (assert) {
61 | var html = stringify(["span"])
62 |
63 | assert.equal(html, "")
64 |
65 | assert.end()
66 | })
67 |
68 | test("[String, { raw: String }] is valid", function (assert) {
69 | var html = stringify(["span", { raw: "foo©" }])
70 |
71 | assert.equal(html, "foo©")
72 |
73 | assert.end()
74 | })
75 |
76 | test("[String, { fragment: Array }] is valid", function (assert) {
77 | var html = stringify(["span", { fragment: children }])
78 |
79 | assert.equal(html, "" + whiteChildrenString + "")
80 |
81 | assert.end()
82 | })
83 |
84 | test("[String, Object] is valid", function (assert) {
85 | var html = stringify(["span", { class: "foobar" }])
86 |
87 | assert.equal(html, "")
88 |
89 | assert.end()
90 | })
91 |
92 | test("[String, String] is valid", function (assert) {
93 | var html = stringify(["span", "foo"])
94 |
95 | assert.equal(html, "foo")
96 |
97 | assert.end()
98 | })
99 |
100 | test("[String, Array] is valid", function (assert) {
101 | var html = stringify(["div", children])
102 |
103 | assert.equal(html, "" + childrenString + "
")
104 |
105 | assert.end()
106 | })
107 |
108 | test("[String, Object, Array] is valid", function (assert) {
109 | var html = stringify(["div", { class: "bar" }, children])
110 |
111 | assert.equal(html, "" + childrenString + "
")
112 |
113 | assert.end()
114 | })
115 |
116 | test("[String, Object, String] is valid", function (assert) {
117 | var html = stringify(["div", { class: "bar" }, "foo"])
118 |
119 | assert.equal(html, "foo
")
120 |
121 | assert.end()
122 | })
123 | test("[String, Object, { fragment: Array }]", function (assert) {
124 | var html = stringify(["div", { class: "bar" }, { fragment: children }])
125 |
126 | assert.equal(html, "" +
127 | whiteChildrenString + "
")
128 |
129 | assert.end()
130 | })
131 |
132 | test("[String, Object, { raw: String }] is valid", function (assert) {
133 | var html = stringify(["div", { class: "bar" }, { raw: "foo©" }])
134 |
135 | assert.equal(html, "foo©
")
136 |
137 | assert.end()
138 | })
139 |
140 | test("[[String, Object]] throws an exception", function (assert) {
141 | assert.throws(function () {
142 | stringify([["div", {}]])
143 | }, /Selector is not a string/)
144 |
145 | assert.end()
146 | })
147 |
148 | test("[String, String, Array] throws an exception", function (assert) {
149 | assert.throws(function () {
150 | var res = stringify(["div", "some text", ["some more text"]])
151 | console.log("response what", res)
152 | }, /Properties is not an object/)
153 |
154 | assert.end()
155 | })
156 |
157 | test("[] throws an exception", function (assert) {
158 | assert.throws(function () {
159 | stringify([])
160 | }, /Empty array is not a valid elem/)
161 |
162 | assert.end()
163 | })
164 |
--------------------------------------------------------------------------------
/test/stringify.js:
--------------------------------------------------------------------------------
1 | var test = require("tape")
2 |
3 | var Stringify = require("../stringify")
4 | var stringify = Stringify([
5 | require("../plugins/loose.js"),
6 | require("../plugins/fragment.js"),
7 | require("../plugins/raw.js")
8 | ])
9 |
10 | test("jsonml-stringify is a function", function (assert) {
11 | assert.equal(typeof stringify, "function")
12 | assert.end()
13 | })
14 |
15 | test("produces strings", function (assert) {
16 | var html = stringify(["html"])
17 |
18 | assert.equal(html, "")
19 | assert.end()
20 | })
21 |
22 | test("encodes attributes", function (assert) {
23 | var html = stringify(["meta", { charset: "utf-8" }])
24 |
25 | assert.equal(html, "")
26 | assert.end()
27 | })
28 |
29 | test("encodes text content", function (assert) {
30 | var html = stringify(["title", "Process dashboard"])
31 |
32 | assert.equal(html, "Process dashboard")
33 | assert.end()
34 | })
35 |
36 | test("encodes text content as children", function (assert) {
37 | var html = stringify(["title", ["Process dashboard"]])
38 |
39 | assert.equal(html, "Process dashboard")
40 | assert.end()
41 | })
42 |
43 | test("encodes string as text content", function (assert) {
44 | var html = stringify("some text")
45 |
46 | assert.equal(html, "some text")
47 | assert.end()
48 | })
49 |
50 | test("encodes scripts properly as text content", function (assert) {
51 | var html = stringify("")
52 |
53 | assert.equal(html, "<script>alert('no u')</script>")
54 | assert.end()
55 | })
56 |
57 | test("unpacks selector into class & id", function (assert) {
58 | var html = stringify(["span.foo#baz.bar"])
59 |
60 | assert.equal(html, "")
61 | assert.end()
62 | })
63 |
64 | test("selector without tagname defaults to div", function (assert) {
65 | var html = stringify([".foo"])
66 |
67 | assert.equal(html, "")
68 | assert.end()
69 | })
70 |
71 | test("encodes children", function (assert) {
72 | var html = stringify(["head", [
73 | ["meta", { charset: "utf-8" }],
74 | ["title", "Process dashboard"]
75 | ]])
76 |
77 | assert.equal(html, "" +
78 | "" +
79 | "Process dashboard" +
80 | "")
81 | assert.end()
82 | })
83 |
84 | test("can set value-less attributes", function (assert) {
85 | var html = stringify(["input", { autofocus: true }])
86 |
87 | assert.equal(html, "")
88 | assert.end()
89 | })
90 |
91 | test("can handle boolean attributes", function (assert) {
92 | var html = stringify(["option", { selected: false }])
93 |
94 | assert.equal(html, "")
95 | assert.end()
96 | })
97 |
98 | test("integration test", function (assert) {
99 | var html = stringify(["html", [
100 | ["head", [
101 | ["meta", { charset: "utf-8" }],
102 | ["title", "Process dashboard"],
103 | ["link", { rel: "stylesheet", href: "/less/main"}]
104 | ]],
105 | ["body", { "class": "main" }, [
106 | ["script", { src: "/browserify/main" }]
107 | ]]
108 | ]])
109 |
110 | assert.equal(html,
111 | "" +
112 | "" +
113 | "" +
114 | "Process dashboard" +
115 | "" +
116 | "" +
117 | "" +
118 | "" +
119 | "" +
120 | "")
121 | assert.end()
122 | })
123 |
124 | test("script tag with javascript is not html encoded", function (assert) {
125 | var html = stringify(["script", {
126 | type: "text/javascript"
127 | }, "var foo = \"bar\""])
128 |
129 | assert.equal(html,
130 | "")
131 |
132 | assert.end()
133 | })
134 |
135 | test("attributes are properly escaped", function (assert) {
136 | var html = stringify(["div", {
137 | "data-marker": "\"foo\""
138 | }])
139 |
140 | assert.equal(html, "")
141 |
142 | assert.end()
143 | })
144 |
145 | test("script tags in script tags get encoded properly", function (assert) {
146 | var html = stringify(["script", "var foo = \"bar \""])
147 |
148 | assert.equal(html,
149 | "")
150 |
151 | assert.end()
152 | })
153 |
154 | test("allow raw data", function (assert) {
155 | var html = stringify(["span", [{
156 | raw: " |"
157 | }]])
158 |
159 | assert.equal(html, "\u00A0\u00A0\u00A0|")
160 |
161 | assert.end()
162 | })
163 |
164 | test("style properties", function (assert) {
165 | var html = stringify(["div", {
166 | style: { borderColor: "black" }
167 | }])
168 |
169 | assert.equal(html, "")
170 |
171 | assert.end()
172 | })
173 |
174 | test("null is a valid element", function (assert) {
175 | var html = stringify(null)
176 |
177 | assert.equal(html, "")
178 |
179 | assert.end()
180 | })
181 |
--------------------------------------------------------------------------------