"
22 | var strings = []
23 | var firstChild, useWhitespace
24 |
25 | if (jsonml === null) {
26 | return "
"
27 | } else if (typeof jsonml === "string") {
28 | return escapeHTMLTextContent(jsonml, parentTagName)
29 | } else if (!!jsonml && typeof jsonml.raw === "string") {
30 | return decode(jsonml.raw)
31 | } else if (!!jsonml && Array.isArray(jsonml.fragment)) {
32 | if (jsonml.fragment.length === 0) {
33 | return ""
34 | }
35 |
36 | renderChildren(jsonml.fragment)
37 | return strings.join("")
38 | } else if (typeof jsonml === "function") {
39 | return stringify(jsonml())
40 | }
41 |
42 | var selector = jsonml[0]
43 | var properties = jsonml[1]
44 | var children = jsonml[2]
45 |
46 | var tagName = unpackSelector(selector, properties)
47 |
48 | strings.push("<" + tagName + props(properties) + ">")
49 |
50 | if (children.length > 0) {
51 | renderChildren(children)
52 |
53 | strings.push("" + tagName + ">")
54 | } else {
55 | strings.push("" + tagName + ">")
56 | }
57 |
58 | return strings.join("")
59 |
60 | function renderChildren(children, indent, whitespace, newLine) {
61 | children.forEach(function (childML) {
62 | if (childML === null || childML === undefined) {
63 | throw new Error("Invalid JSONML data structure " +
64 | util.inspect(jsonml))
65 | }
66 |
67 | var text = stringify(childML, {
68 | parentTagName: tagName
69 | })
70 |
71 | strings.push(text)
72 | })
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 | // })
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------