├── .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 |
    1. 13 | item 14 | {{z}} 15 |
    2. 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("") 54 | } else { 55 | strings.push("") 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("") 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 | --------------------------------------------------------------------------------