diff --git a/.eslintignore b/.eslintignore index 62117a94..0711ad31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,5 +3,8 @@ coverage/ docs/lib/ examples/ /mithril.js +/mithril.mjs /mithril.min.js +/mithril.min.mjs +/stream/stream.mjs node_modules/ diff --git a/esm.js b/esm.js new file mode 100644 index 00000000..07dd3f95 --- /dev/null +++ b/esm.js @@ -0,0 +1,68 @@ +"use strict" + +/* + +This script will create esm compatible scripts +from the already compiled versions of: + +- mithril.js > mithril.mjs +- mithril.min.js > mithril.min.mjs +- /stream/stream.js > stream.mjs + +*/ + +var fs = require("fs") + +var namedExports = [ + "m", + "trust", + "fragment", + "mount", + "route", + "withAttr", + "render", + "redraw", + "request", + "jsonp", + "parseQueryString", + "buildQueryString", + "version", + "vnode", + "PromisePolyfill" +] + +var mithril = fs.readFileSync("mithril.js", "utf8") +fs.writeFileSync("mithril.mjs", + mithril.slice( + mithril.indexOf("\"use strict\"") + 13, + mithril.lastIndexOf("if (typeof module") + ) + + "\nexport default m" + // The exports are declared with prefixed underscores to avoid overwriting previously + // declared variables with the same name + + "\nvar " + namedExports.map(function(n) { return "_" + n + " = m." + n }).join(",") + + "\nexport {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "}" +) + +var mithrilMin = fs.readFileSync("mithril.min.js", "utf8") +var mName = mithrilMin.match(/window\.m=([a-z])}/)[1] +fs.writeFileSync("mithril.min.mjs", + mithrilMin.slice( + 12, + mithrilMin.lastIndexOf("\"undefined\"!==typeof module") + ) + + "export default " + mName + ";" + // The exports are declared with prefixed underscores to avoid overwriting previously + // declared variables with the same name + + "var " + namedExports.map(function(n) { return "_" + n + "=m." + n }).join(",") + ";" + + "export {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "};" +) + +var stream = fs.readFileSync("stream/stream.js", "utf8") +fs.writeFileSync("stream/stream.mjs", + stream.slice( + stream.indexOf("\"use strict\"") + 13, + stream.lastIndexOf("if (typeof module") + ) + + "\nexport default createStream" +) diff --git a/index.js b/index.js index 469a832b..3d86749d 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,11 @@ "use strict" -var m = require("./hyperscript") +var hyperscript = require("./hyperscript") +var m = function m() { return hyperscript.apply(this, arguments) } +m.m = hyperscript +m.trust = hyperscript.trust +m.fragment = hyperscript.fragment + var requestService = require("./request") var redrawService = require("./redraw") diff --git a/mithril.min.mjs b/mithril.min.mjs new file mode 100644 index 00000000..8f6be518 --- /dev/null +++ b/mithril.min.mjs @@ -0,0 +1,48 @@ +function x(a,d,e,g,q,k){return{tag:a,key:d,attrs:e,children:g,text:q,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0}}function Q(a){for(var d in a)if(G.call(a,d))return!1;return!0}function w(a){if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");var d=arguments[1],e=2;if(null==d)d={};else if("object"!==typeof d||null!=d.tag||Array.isArray(d))d={},e=1;if(arguments.length=== +e+1){var g=arguments[e];Array.isArray(g)||(g=[g])}else for(g=[];ec.indexOf("?")?"?":"&";c+=e+d}return c}function h(c){try{return""!==c?JSON.parse(c):null}catch(y){throw Error("Invalid JSON: "+ +c);}}function m(c){return c.responseText}function n(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;dl.status||304===l.status||Z.test(c.url))d(n(c.type,a));else{var g=Error(l.responseText);g.code=l.status;g.response=a;e(g)}}catch(aa){e(aa)}};g&&null!=c.data?l.send(c.data):l.send()});return!0===c.background?y:F(y)},jsonp:function(c,m){var h=e();c=g(c,m);var y=new d(function(d,e){var g=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,m=a.document.createElement("script"); +a[g]=function(e){m.parentNode.removeChild(m);d(n(c.type,e));delete a[g]};m.onerror=function(){m.parentNode.removeChild(m);e(Error("JSONP request failed"));delete a[g]};null==c.data&&(c.data={});c.url=q(c.url,c.data);c.data[c.callbackKey||"callback"]=g;m.src=k(c.url,c.data);a.document.documentElement.appendChild(m)});return!0===c.background?y:h(y)},setCompletionCallback:function(c){C=c}}}(window,p),V=function(a){function d(t,b){if(t.state!==b)throw Error("`vnode.state` must not be modified");}function e(t){var b= +t.state;try{return this.apply(b,arguments)}finally{d(t,b)}}function g(t,b,f,c,a,d,e){for(;f'+ +b.children+"",a=a.firstChild):a.innerHTML=b.children;b.dom=a.firstChild;b.domSize=a.childNodes.length;for(b=E.createDocumentFragment();f=a.firstChild;)b.appendChild(f);C(t,b,c)}function h(t,b,f,c,a,d){if(b!==f&&(null!=b||null!=f))if(null==b||0===b.length)g(t,f,0,f.length,c,a,d);else if(null==f||0===f.length)y(b,0,b.length);else{for(var e=0,u=0,h=null,k=null;u=u&&h>=e;)if(A=b[k],z=f[h],null==A)k--;else if(null==z)h--;else if(A.key===z.key)A!==z&&m(t,A,z,c,a,d),null!=z.dom&&(a=z.dom),k--,h--;else break;for(;k>=u&&h>=e;)if(r=b[u],v=f[e],null==r)u++;else if(null==v)e++;else if(r.key===v.key)u++,e++,r!==v&&m(t,r,v,c,l(b,u,a),d);else break;for(;k>=u&&h>=e;){if(null==r)u++;else if(null==v)e++;else if(null==A)k--;else if(null==z)h--;else if(e===h)break;else{if(r.key!== +z.key||A.key!==v.key)break;B=l(b,u,a);C(t,n(A),B);A!==v&&m(t,A,v,c,B,d);++e<=--h&&C(t,n(r),a);r!==z&&m(t,r,z,c,a,d);null!=z.dom&&(a=z.dom);u++;k--}A=b[k];z=f[h];r=b[u];v=f[e]}for(;k>=u&&h>=e;){if(null==A)k--;else if(null==z)h--;else if(A.key===z.key)A!==z&&m(t,A,z,c,a,d),null!=z.dom&&(a=z.dom),k--,h--;else break;A=b[k];z=f[h]}if(e>h)y(b,u,k+1);else if(u>k)g(t,f,e,h+1,c,a,d);else{v=a;A=h-e+1;r=Array(A);var x=2147483647,w=0;for(B=0;B=e;B--){if(null==p){p=b;A=u;z=k+1;for(var D= +{};A=e;B--)v=f[B],-1===r[B-e]?q(t,v,c,d,a):b[u]===B-e?u--:C(t,n(v),a),null!=v.dom&&(a=f[B].dom)}else for(B=h;B>=e;B--)v=f[B],-1===r[B-e]&&q(t,v,c,d,a),null!=v.dom&&(a=f[B].dom)}}else{h=b.lengthh&&y(b,e,b.length);f.length>h&&g(t,f,e,f.length,c,a,d)}}}function m(a,b,f,d,g,l){var t=b.tag;if(t=== +f.tag){f.state=b.state;f.events=b.events;var u;var y;null!=f.attrs&&"function"===typeof f.attrs.onbeforeupdate&&(u=e.call(f.attrs.onbeforeupdate,f,b));"string"!==typeof f.tag&&"function"===typeof f.state.onbeforeupdate&&(y=e.call(f.state.onbeforeupdate,f,b));void 0===u&&void 0===y||u||y?u=!1:(f.dom=b.dom,f.domSize=b.domSize,f.instance=b.instance,u=!0);if(!u)if("string"===typeof t)switch(null!=f.attrs&&L(f.attrs,f,d),t){case "#":b.children.toString()!==f.children.toString()&&(b.dom.nodeValue=f.children); +f.dom=b.dom;break;case "<":b.children!==f.children?(n(b),k(a,f,l,g)):(f.dom=b.dom,f.domSize=b.domSize);break;case "[":h(a,b.children,f.children,d,g,l);b=0;d=f.children;f.dom=null;if(null!=d){for(var p=0;p 0) attrs.className = classes.join(" ") + return selectorCache[selector] = {tag: tag, attrs: attrs} +} +function execSelector(state, attrs, children) { + var hasAttrs = false, childList, text + var classAttr = hasOwn.call(attrs, "class") ? "class" : "className" + var className = attrs[classAttr] + if (!isEmpty(state.attrs) && !isEmpty(attrs)) { + var newAttrs = {} + for(var key in attrs) { + if (hasOwn.call(attrs, key)) { + newAttrs[key] = attrs[key] + } + } + attrs = newAttrs + } + for (var key in state.attrs) { + if (hasOwn.call(state.attrs, key) && key !== "className" && !hasOwn.call(attrs, key)){ + attrs[key] = state.attrs[key] + } + } + if (className != null || state.attrs.className != null) attrs.className = + className != null + ? state.attrs.className != null + ? state.attrs.className + " " + className + : className + : state.attrs.className != null + ? state.attrs.className + : null + if (classAttr === "class") attrs.class = null + for (var key in attrs) { + if (hasOwn.call(attrs, key) && key !== "key") { + hasAttrs = true + break + } + } + if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") { + text = children[0].children + } else { + childList = children + } + return Vnode(state.tag, attrs.key, hasAttrs ? attrs : null, childList, text) +} +function hyperscript(selector) { + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { + throw Error("The selector must be either a string or a component."); + } + var attrs = arguments[1], start = 2, children + if (attrs == null) { + attrs = {} + } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { + attrs = {} + start = 1 + } + if (arguments.length === start + 1) { + children = arguments[start] + if (!Array.isArray(children)) children = [children] + } else { + children = [] + while (start < arguments.length) children.push(arguments[start++]) + } + if (typeof selector === "string") { + return execSelector(selectorCache[selector] || compileSelector(selector), attrs, Vnode.normalizeChildren(children)) + } else { + return Vnode(selector, attrs.key, attrs, children) + } +} +hyperscript.trust = function(html) { + if (html == null) html = "" + return Vnode("<", undefined, undefined, html, undefined, undefined) +} +hyperscript.fragment = function(attrs1, children0) { + return Vnode("[", attrs1.key, attrs1, Vnode.normalizeChildren(children0), undefined, undefined) +} +var m = hyperscript +/** @constructor */ +var PromisePolyfill = function(executor) { + if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`") + if (typeof executor !== "function") throw new TypeError("executor must be a function") + var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) + var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} + var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout + function handler(list, shouldAbsorb) { + return function execute(value) { + var then + try { + if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { + if (value === self) throw new TypeError("Promise can't be resolved w/ itself") + executeOnce(then.bind(value)) + } + else { + callAsync(function() { + if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value) + for (var i = 0; i < list.length; i++) list[i](value) + resolvers.length = 0, rejectors.length = 0 + instance.state = shouldAbsorb + instance.retry = function() {execute(value)} + }) + } + } + catch (e) { + rejectCurrent(e) + } + } + } + function executeOnce(then) { + var runs = 0 + function run(fn) { + return function(value) { + if (runs++ > 0) return + fn(value) + } + } + var onerror = run(rejectCurrent) + try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)} + } + executeOnce(executor) +} +PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { + var self = this, instance = self._instance + function handle(callback, list, next, state) { + list.push(function(value) { + if (typeof callback !== "function") next(value) + else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} + }) + if (typeof instance.retry === "function" && state === instance.state) instance.retry() + } + var resolveNext, rejectNext + var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) + return promise +} +PromisePolyfill.prototype.catch = function(onRejection) { + return this.then(null, onRejection) +} +PromisePolyfill.prototype.finally = function(callback) { + return this.then( + function(value) { + return PromisePolyfill.resolve(callback()).then(function() { + return value + }) + }, + function(reason) { + return PromisePolyfill.resolve(callback()).then(function() { + return PromisePolyfill.reject(reason); + }) + } + ) +} +PromisePolyfill.resolve = function(value) { + if (value instanceof PromisePolyfill) return value + return new PromisePolyfill(function(resolve) {resolve(value)}) +} +PromisePolyfill.reject = function(value) { + return new PromisePolyfill(function(resolve, reject) {reject(value)}) +} +PromisePolyfill.all = function(list) { + return new PromisePolyfill(function(resolve, reject) { + var total = list.length, count = 0, values = [] + if (list.length === 0) resolve([]) + else for (var i = 0; i < list.length; i++) { + (function(i) { + function consume(value) { + count++ + values[i] = value + if (count === total) resolve(values) + } + if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { + list[i].then(consume, reject) + } + else consume(list[i]) + })(i) + } + }) +} +PromisePolyfill.race = function(list) { + return new PromisePolyfill(function(resolve, reject) { + for (var i = 0; i < list.length; i++) { + list[i].then(resolve, reject) + } + }) +} +if (typeof window !== "undefined") { + if (typeof window.Promise === "undefined") { + window.Promise = PromisePolyfill + } else if (!window.Promise.prototype.finally) { + window.Promise.prototype.finally = PromisePolyfill.prototype.finally + } + var PromisePolyfill = window.Promise +} else if (typeof global !== "undefined") { + if (typeof global.Promise === "undefined") { + global.Promise = PromisePolyfill + } else if (!global.Promise.prototype.finally) { + global.Promise.prototype.finally = PromisePolyfill.prototype.finally + } + var PromisePolyfill = global.Promise +} else { +} +var buildQueryString = function(object) { + if (Object.prototype.toString.call(object) !== "[object Object]") return "" + var args = [] + for (var key0 in object) { + destructure(key0, object[key0]) + } + return args.join("&") + function destructure(key0, value) { + if (Array.isArray(value)) { + for (var i = 0; i < value.length; i++) { + destructure(key0 + "[" + i + "]", value[i]) + } + } + else if (Object.prototype.toString.call(value) === "[object Object]") { + for (var i in value) { + destructure(key0 + "[" + i + "]", value[i]) + } + } + else args.push(encodeURIComponent(key0) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) + } +} +var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i") +var _9 = function($window, Promise) { + var callbackCount = 0 + var oncompletion + function setCompletionCallback(callback) {oncompletion = callback} + function finalizer() { + var count = 0 + function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} + return function finalize(promise0) { + var then0 = promise0.then + promise0.then = function() { + count++ + var next = then0.apply(promise0, arguments) + next.then(complete, function(e) { + complete() + if (count === 0) throw e + }) + return finalize(next) + } + return promise0 + } + } + function normalize(args, extra) { + if (typeof args === "string") { + var url = args + args = extra || {} + if (args.url == null) args.url = url + } + return args + } + function request(args, extra) { + var finalize = finalizer() + args = normalize(args, extra) + var promise0 = new Promise(function(resolve, reject) { + if (args.method == null) args.method = "GET" + args.method = args.method.toUpperCase() + var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true) + if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify + if (typeof args.deserialize !== "function") args.deserialize = deserialize + if (typeof args.extract !== "function") args.extract = extract + args.url = interpolate(args.url, args.data) + if (useBody) args.data = args.serialize(args.data) + else args.url = assemble(args.url, args.data) + var xhr = new $window.XMLHttpRequest(), + aborted = false, + _abort = xhr.abort + xhr.abort = function abort() { + aborted = true + _abort.call(xhr) + } + xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) + if (args.serialize === JSON.stringify && useBody && !(args.headers && args.headers.hasOwnProperty("Content-Type"))) { + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") + } + if (args.deserialize === deserialize && !(args.headers && args.headers.hasOwnProperty("Accept"))) { + xhr.setRequestHeader("Accept", "application/json, text/*") + } + if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout + + if (args.responseType) xhr.responseType = args.responseType + for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { + xhr.setRequestHeader(key, args.headers[key]) + } + if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr + xhr.onreadystatechange = function() { + // Don't throw errors on xhr.abort(). + if(aborted) return + if (xhr.readyState === 4) { + try { + var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) + if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { + resolve(cast(args.type, response)) + } + else { + var error = new Error(xhr.responseText) + error.code = xhr.status + error.response = response + reject(error) + } + } + catch (e) { + reject(e) + } + } + } + if (useBody && (args.data != null)) xhr.send(args.data) + else xhr.send() + }) + return args.background === true ? promise0 : finalize(promise0) + } + function jsonp(args, extra) { + var finalize = finalizer() + args = normalize(args, extra) + var promise0 = new Promise(function(resolve, reject) { + var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ + var script = $window.document.createElement("script") + $window[callbackName] = function(data) { + script.parentNode.removeChild(script) + resolve(cast(args.type, data)) + delete $window[callbackName] + } + script.onerror = function() { + script.parentNode.removeChild(script) + reject(new Error("JSONP request failed")) + delete $window[callbackName] + } + if (args.data == null) args.data = {} + args.url = interpolate(args.url, args.data) + args.data[args.callbackKey || "callback"] = callbackName + script.src = assemble(args.url, args.data) + $window.document.documentElement.appendChild(script) + }) + return args.background === true? promise0 : finalize(promise0) + } + function interpolate(url, data) { + if (data == null) return url + var tokens = url.match(/:[^\/]+/gi) || [] + for (var i = 0; i < tokens.length; i++) { + var key = tokens[i].slice(1) + if (data[key] != null) { + url = url.replace(tokens[i], data[key]) + } + } + return url + } + function assemble(url, data) { + var querystring = buildQueryString(data) + if (querystring !== "") { + var prefix = url.indexOf("?") < 0 ? "?" : "&" + url += prefix + querystring + } + return url + } + function deserialize(data) { + try {return data !== "" ? JSON.parse(data) : null} + catch (e) {throw new Error("Invalid JSON: " + data)} + } + function extract(xhr) {return xhr.responseText} + function cast(type0, data) { + if (typeof type0 === "function") { + if (Array.isArray(data)) { + for (var i = 0; i < data.length; i++) { + data[i] = new type0(data[i]) + } + } + else return new type0(data) + } + return data + } + return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback} +} +var requestService = _9(window, PromisePolyfill) +var coreRenderer = function($window) { + var $doc = $window.document + var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + } + var onevent + function setEventCallback(callback) {return onevent = callback} + function getNameSpace(vnode) { + return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] + } + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("`vnode.state` must not be modified") + } + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) + } + } + //create + function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { + for (var i = start; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) { + createNode(parent, vnode, hooks, ns, nextSibling) + } + } + } + function createNode(parent, vnode, hooks, ns, nextSibling) { + var tag = vnode.tag + if (typeof tag === "string") { + vnode.state = {} + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + switch (tag) { + case "#": createText(parent, vnode, nextSibling); break + case "<": createHTML(parent, vnode, ns, nextSibling); break + case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break + default: createElement(parent, vnode, hooks, ns, nextSibling) + } + } + else createComponent(parent, vnode, hooks, ns, nextSibling) + } + function createText(parent, vnode, nextSibling) { + vnode.dom = $doc.createTextNode(vnode.children) + insertNode(parent, vnode.dom, nextSibling) + } + var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} + function createHTML(parent, vnode, ns, nextSibling) { + var match1 = vnode.children.match(/^\s*?<(\w+)/im) || [] + // not using the proper parent makes the child element(s) vanish. + // var div = document.createElement("div") + // div.innerHTML = "ij" + // console.log(div.innerHTML) + // --> "ij", no in sight. + var temp = $doc.createElement(possibleParents[match1[1]] || "div") + if (ns === "http://www.w3.org/2000/svg") { + temp.innerHTML = "" + vnode.children + "" + temp = temp.firstChild + } else { + temp.innerHTML = vnode.children + } + vnode.dom = temp.firstChild + vnode.domSize = temp.childNodes.length + var fragment = $doc.createDocumentFragment() + var child + while (child = temp.firstChild) { + fragment.appendChild(child) + } + insertNode(parent, fragment, nextSibling) + } + function createFragment(parent, vnode, hooks, ns, nextSibling) { + var fragment = $doc.createDocumentFragment() + if (vnode.children != null) { + var children1 = vnode.children + createNodes(fragment, children1, 0, children1.length, hooks, null, ns) + } + vnode.dom = fragment.firstChild + vnode.domSize = fragment.childNodes.length + insertNode(parent, fragment, nextSibling) + } + function createElement(parent, vnode, hooks, ns, nextSibling) { + var tag = vnode.tag + var attrs2 = vnode.attrs + var is = attrs2 && attrs2.is + ns = getNameSpace(vnode) || ns + var element = ns ? + is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) : + is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag) + vnode.dom = element + if (attrs2 != null) { + setAttrs(vnode, attrs2, ns) + } + insertNode(parent, element, nextSibling) + if (attrs2 != null && attrs2.contenteditable != null) { + setContentEditable(vnode) + } + else { + if (vnode.text != null) { + if (vnode.text !== "") element.textContent = vnode.text + else vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] + } + if (vnode.children != null) { + var children1 = vnode.children + createNodes(element, children1, 0, children1.length, hooks, null, ns) + if (vnode.tag === "select" && attrs2 != null) setLateSelectAttrs(vnode, attrs2) + } + } + } + function initComponent(vnode, hooks) { + var sentinel + if (typeof vnode.tag.view === "function") { + vnode.state = Object.create(vnode.tag) + sentinel = vnode.state.view + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + } else { + vnode.state = void 0 + sentinel = vnode.tag + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) + } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) + if (vnode.instance != null) { + createNode(parent, vnode.instance, hooks, ns, nextSibling) + vnode.dom = vnode.instance.dom + vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 + } + else { + vnode.domSize = 0 + } + } + //update + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last0 `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next0 DOM node if we're dealing with a + * fragment that is not the last0 item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. discuss DOM node operations. + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed based on the first non-null node + // of each list. + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + // ## Diffing + // + // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 + // may be good for context on longest increasing subsequence-based logic for moving nodes. + // + // In order to diff keyed lists, one has to + // + // 1) match1 nodes in both lists, per key2, and update them accordingly + // 2) create the nodes present in the new list, but absent in the old one + // 3) remove the nodes present in the old list, but absent in the new one + // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. + // + // To achieve 1) one can create a dictionary of keys => index0 (for the old list), then1 iterate + // over the new list and for each new vnode, find the corresponding vnode in the old list using + // the map. + // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new + // and must be created. + // For the removals, we actually remove the nodes that have been updated from the old list. + // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. + // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) + // algorithm. + // + // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going + // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices + // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would + // match1 the above lists, for example). + // + // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We + // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. + // + // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually + // the longest increasing subsequence *of old nodes still present in the new list*). + // + // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation + // and the construction of a `key2 => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, + // the `LIS` and a temporary one to create the LIS). + // + // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of + // the LIS and can be updated without moving them. + // + // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with + // the exception of the last0 node if the list is fully reversed). + // + // ## Finding the next0 sibling. + // + // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. + // When the list is being traversed top-down, at any index0, the DOM nodes up to the previous + // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // list. The next0 sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. + // + // In the other scenarios (swaps, upwards traversal, map-based diff), + // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the + // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node + // as the next0 sibling (cached in the `nextSibling` variable). + // ## DOM node moves + // + // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo). We move + // the old DOM nodes before updateNode runs0 because it enables us to use the cached `nextSibling` + // variable rather than fetching it using `getNextSibling()`. + // + // The fourth part of the diff currently inserts nodes unconditionally, leading to issues + // like #1791 and #1999. We need to be smarter about those situations where adjascent old + // nodes remain together in the new list in a way that isn't covered by parts one and + // three of the diff algo. + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + if (old === vnodes || old == null && vnodes == null) return + else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length) + else { + var start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null + for (; oldStart < old.length; oldStart++) { + if (old[oldStart] != null) { + isOldKeyed = old[oldStart].key != null + break + } + } + for (; start < vnodes.length; start++) { + if (vnodes[start] != null) { + isKeyed = vnodes[start].key != null + break + } + } + if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls + if (isOldKeyed !== isKeyed) { + removeNodes(old, oldStart, old.length) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else if (!isKeyed) { + // Don't index0 past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index0 on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + } + if (old.length > commonLength) removeNodes(old, start, old.length) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else { + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling + // bottom-up + while (oldEnd >= oldStart && end >= start) { + oe = old[oldEnd] + ve = vnodes[end] + if (oe == null) oldEnd-- + else if (ve == null) end-- + else if (oe.key === ve.key) { + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + } else { + break + } + } + // top-down + while (oldEnd >= oldStart && end >= start) { + o = old[oldStart] + v = vnodes[start] + if (o == null) oldStart++ + else if (v == null) start++ + else if (o.key === v.key) { + oldStart++, start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + } else { + break + } + } + // swaps and list reversals + while (oldEnd >= oldStart && end >= start) { + if (o == null) oldStart++ + else if (v == null) start++ + else if (oe == null) oldEnd-- + else if (ve == null) end-- + else if (start === end) break + else { + if (o.key !== ve.key || oe.key !== v.key) break + topSibling = getNextSibling(old, oldStart, nextSibling) + insertNode(parent, toFragment(oe), topSibling) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) + if (++start <= --end) insertNode(parent, toFragment(o), nextSibling) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldStart++; oldEnd-- + } + oe = old[oldEnd] + ve = vnodes[end] + o = old[oldStart] + v = vnodes[start] + } + // bottom up once again + while (oldEnd >= oldStart && end >= start) { + if (oe == null) oldEnd-- + else if (ve == null) end-- + else if (oe.key === ve.key) { + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + } else { + break + } + oe = old[oldEnd] + ve = vnodes[end] + } + if (start > end) removeNodes(old, oldStart, oldEnd + 1) + else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul + var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices + for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 + for (i = end; i >= start; i--) { + if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) + ve = vnodes[i] + if (ve != null) { + var oldIndex = map[ve.key] + if (oldIndex != null) { + pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered + oldIndices[i-start] = oldIndex + oe = old[oldIndex] + old[oldIndex] = null + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + matched++ + } + } + } + nextSibling = originalNextSibling + if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1) + if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + if (pos === -1) { + // the indices of the indices of the items that are part of the + // longest increasing subsequence in the oldIndices list + lisIndices = makeLisIndices(oldIndices) + li = lisIndices.length - 1 + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + else { + if (lisIndices[li] === i - start) li-- + else insertNode(parent, toFragment(v), nextSibling) + } + if (v.dom != null) nextSibling = vnodes[i].dom + } + } else { + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = vnodes[i].dom + } + } + } + } + } + } + } + function updateNode(parent, old, vnode, hooks, nextSibling, ns) { + var oldTag = old.tag, tag = vnode.tag + if (oldTag === tag) { + vnode.state = old.state + vnode.events = old.events + if (shouldNotUpdate(vnode, old)) return + if (typeof oldTag === "string") { + if (vnode.attrs != null) { + updateLifecycle(vnode.attrs, vnode, hooks) + } + switch (oldTag) { + case "#": updateText(old, vnode); break + case "<": updateHTML(parent, old, vnode, ns, nextSibling); break + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break + default: updateElement(old, vnode, hooks, ns) + } + } + else updateComponent(parent, old, vnode, hooks, nextSibling, ns) + } + else { + removeNode(old) + createNode(parent, vnode, hooks, ns, nextSibling) + } + } + function updateText(old, vnode) { + if (old.children.toString() !== vnode.children.toString()) { + old.dom.nodeValue = vnode.children + } + vnode.dom = old.dom + } + function updateHTML(parent, old, vnode, ns, nextSibling) { + if (old.children !== vnode.children) { + toFragment(old) + createHTML(parent, vnode, ns, nextSibling) + } + else vnode.dom = old.dom, vnode.domSize = old.domSize + } + function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) + var domSize = 0, children1 = vnode.children + vnode.dom = null + if (children1 != null) { + for (var i = 0; i < children1.length; i++) { + var child = children1[i] + if (child != null && child.dom != null) { + if (vnode.dom == null) vnode.dom = child.dom + domSize += child.domSize || 1 + } + } + if (domSize !== 1) vnode.domSize = domSize + } + } + function updateElement(old, vnode, hooks, ns) { + var element = vnode.dom = old.dom + ns = getNameSpace(vnode) || ns + if (vnode.tag === "textarea") { + if (vnode.attrs == null) vnode.attrs = {} + if (vnode.text != null) { + vnode.attrs.value = vnode.text //FIXME handle0 multiple children1 + vnode.text = undefined + } + } + updateAttrs(vnode, old.attrs, vnode.attrs, ns) + if (vnode.attrs != null && vnode.attrs.contenteditable != null) { + setContentEditable(vnode) + } + else if (old.text != null && vnode.text != null && vnode.text !== "") { + if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text + } + else { + if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)] + if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] + updateNodes(element, old.children, vnode.children, hooks, null, ns) + } + } + function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + if (vnode.instance != null) { + if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) + vnode.dom = vnode.instance.dom + vnode.domSize = vnode.instance.domSize + } + else if (old.instance != null) { + removeNode(old.instance) + vnode.dom = undefined + vnode.domSize = 0 + } + else { + vnode.dom = old.dom + vnode.domSize = old.domSize + } + } + function getKeyMap(vnodes, start, end) { + var map = {} + for (; start < end; start++) { + var vnode = vnodes[start] + if (vnode != null) { + var key2 = vnode.key + if (key2 != null) map[key2] = start + } + } + return map + } + // Lifted from ivi https://github.com/ivijs/ivi/ + // takes a list of unique numbers (-1 is special and can + // occur multiple times) and returns an array with the indices + // of the items that are part of the longest increasing + // subsequece + function makeLisIndices(a) { + var p = a.slice() + var result = [] + result.push(0) + var u + var v + for (var i = 0, il = a.length; i < il; ++i) { + if (a[i] === -1) { + continue + } + var j = result[result.length - 1] + if (a[j] < a[i]) { + p[i] = j + result.push(i) + continue + } + u = 0 + v = result.length - 1 + while (u < v) { + var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise + if (a[result[c]] < a[i]) { + u = c + 1 + } + else { + v = c + } + } + if (a[i] < a[result[u]]) { + if (u > 0) { + p[i] = result[u - 1] + } + result[u] = i + } + } + u = result.length + v = result[u - 1] + while (u-- > 0) { + result[u] = v + v = p[v] + } + return result + } + function toFragment(vnode) { + var count0 = vnode.domSize + if (count0 != null || vnode.dom == null) { + var fragment = $doc.createDocumentFragment() + if (count0 > 0) { + var dom = vnode.dom + while (--count0) fragment.appendChild(dom.nextSibling) + fragment.insertBefore(dom, fragment.firstChild) + } + return fragment + } + else return vnode.dom + } + function getNextSibling(vnodes, i, nextSibling) { + for (; i < vnodes.length; i++) { + if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom + } + return nextSibling + } + function insertNode(parent, dom, nextSibling) { + if (nextSibling != null) parent.insertBefore(dom, nextSibling) + else parent.appendChild(dom) + } + function setContentEditable(vnode) { + var children1 = vnode.children + if (children1 != null && children1.length === 1 && children1[0].tag === "<") { + var content = children1[0].children + if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content + } + else if (vnode.text != null || children1 != null && children1.length !== 0) throw new Error("Child node of a contenteditable must be trusted") + } + //remove + function removeNodes(vnodes, start, end) { + for (var i = start; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) removeNode(vnode) + } + } + function removeNode(vnode) { + var expected = 1, called = 0 + var original = vnode.state + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } + } + continuation() + function continuation() { + if (++called === expected) { + checkState(vnode, original) + onremove(vnode) + if (vnode.dom) { + var parent = vnode.dom.parentNode + var count0 = vnode.domSize || 1 + while (--count0) parent.removeChild(vnode.dom.nextSibling) + parent.removeChild(vnode.dom) + } + } + } + } + function onremove(vnode) { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) + if (typeof vnode.tag !== "string") { + if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) + if (vnode.instance != null) onremove(vnode.instance) + } else { + var children1 = vnode.children + if (Array.isArray(children1)) { + for (var i = 0; i < children1.length; i++) { + var child = children1[i] + if (child != null) onremove(child) + } + } + } + } + //attrs2 + function setAttrs(vnode, attrs2, ns) { + for (var key2 in attrs2) { + setAttr(vnode, key2, null, attrs2[key2], ns) + } + } + function setAttr(vnode, key2, old, value, ns) { + if (key2 === "key" || key2 === "is" || value == null || isLifecycleMethod(key2) || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object") return + if (key2[0] === "o" && key2[1] === "n") return updateEvent(vnode, key2, value) + if (key2.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(6), value) + else if (key2 === "style") updateStyle(vnode.dom, old, value) + else if (hasPropertyKey(vnode, key2, ns)) { + if (key2 === "value") { + // Only do the coercion if we're actually going to check the value. + /* eslint-disable no-implicit-coercion */ + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && vnode.dom === $doc.activeElement) return + //setting select[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return + //setting option[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "option" && old !== null && vnode.dom.value === "" + value) return + /* eslint-enable no-implicit-coercion */ + } + // If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error1 will occur. + if (vnode.tag === "input" && key2 === "type") vnode.dom.setAttribute(key2, value) + else vnode.dom[key2] = value + } else { + if (typeof value === "boolean") { + if (value) vnode.dom.setAttribute(key2, "") + else vnode.dom.removeAttribute(key2) + } + else vnode.dom.setAttribute(key2 === "className" ? "class" : key2, value) + } + } + function removeAttr(vnode, key2, old, ns) { + if (key2 === "key" || key2 === "is" || old == null || isLifecycleMethod(key2)) return + if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined) + else if (key2 === "style") updateStyle(vnode.dom, old, null) + else if ( + hasPropertyKey(vnode, key2, ns) + && key2 !== "className" + && !(vnode.tag === "option" && key2 === "value") + && !(vnode.tag === "input" && key2 === "type") + ) { + vnode.dom[key2] = null + } else { + var nsLastIndex = key2.indexOf(":") + if (nsLastIndex !== -1) key2 = key2.slice(nsLastIndex + 1) + if (old !== false) vnode.dom.removeAttribute(key2 === "className" ? "class" : key2) + } + } + function setLateSelectAttrs(vnode, attrs2) { + if ("value" in attrs2) { + if(attrs2.value === null) { + if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null + } else { + var normalized = "" + attrs2.value // eslint-disable-line no-implicit-coercion + if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { + vnode.dom.value = normalized + } + } + } + if ("selectedIndex" in attrs2) setAttr(vnode, "selectedIndex", null, attrs2.selectedIndex, undefined) + } + function updateAttrs(vnode, old, attrs2, ns) { + if (attrs2 != null) { + for (var key2 in attrs2) { + setAttr(vnode, key2, old && old[key2], attrs2[key2], ns) + } + } + var val + if (old != null) { + for (var key2 in old) { + if (((val = old[key2]) != null) && (attrs2 == null || attrs2[key2] == null)) { + removeAttr(vnode, key2, val, ns) + } + } + } + } + function isFormAttribute(vnode, attr) { + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement + } + function isLifecycleMethod(attr) { + return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" + } + function hasPropertyKey(vnode, key2, ns) { + // Filter out namespaced keys + return ns === undefined && ( + // If it's a custom element, just keep it. + vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || + // If it's a normal element, let's try to avoid a few browser bugs. + key !== "href" && key2 !== "list" && key2 !== "form" && key2 !== "width" && key2 !== "height"// && key2 !== "type" + // Defer the property check until *after* we check everything. + ) && key2 in vnode.dom + } + //style + function updateStyle(element, old, style) { + if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) { + // Both old & new are (different) objects. + // Update style properties that have changed + for (var key2 in style) { + if (style[key2] !== old[key2]) element.style[key2] = style[key2] + } + // Remove style properties that no longer exist + for (var key2 in old) { + if (!(key2 in style)) element.style[key2] = "" + } + return + } + if (old === style) element.style.cssText = "", old = null + if (style == null) element.style.cssText = "" + else if (typeof style === "string") element.style.cssText = style + else { + if (typeof old === "string") element.style.cssText = "" + for (var key2 in style) { + element.style[key2] = style[key2] + } + } + } + // Here's an explanation of how this works: + // 1. The event names are always (by design) prefixed by `on`. + // 2. The EventListener interface accepts either a function or an object + // with a `handleEvent` method. + // 3. The object does not inherit from `Object.prototype`, to avoid + // any potential interference with that (e.g. setters). + // 4. The event name is remapped to the handler0 before calling it. + // 5. In function-based event handlers, `ev.target === this`. We replicate + // that below. + // 6. In function-based event handlers, `return false` prevents the default + // action and stops event propagation. We replicate that below. + function EventDict() {} + EventDict.prototype = Object.create(null) + EventDict.prototype.handleEvent = function (ev) { + var handler0 = this["on" + ev.type] + var result + if (typeof handler0 === "function") result = handler0.call(ev.target, ev) + else if (typeof handler0.handleEvent === "function") handler0.handleEvent(ev) + if (typeof onevent === "function") onevent.call(ev.target, ev) + if (result === false) { + ev.preventDefault() + ev.stopPropagation() + } + } + //event + function updateEvent(vnode, key2, value) { + if (vnode.events != null) { + if (vnode.events[key2] === value) return + if (value != null && (typeof value === "function" || typeof value === "object")) { + if (vnode.events[key2] == null) vnode.dom.addEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = value + } else { + if (vnode.events[key2] != null) vnode.dom.removeEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = undefined + } + } else if (value != null && (typeof value === "function" || typeof value === "object")) { + vnode.events = new EventDict() + vnode.dom.addEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = value + } + } + //lifecycle + function initLifecycle(source, vnode, hooks) { + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) + } + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) + } + function shouldNotUpdate(vnode, old) { + var forceVnodeUpdate, forceComponentUpdate + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old) + } + if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { + vnode.dom = old.dom + vnode.domSize = old.domSize + vnode.instance = old.instance + return true + } + return false + } + function render(dom, vnodes) { + if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") + var hooks = [] + var active = $doc.activeElement + var namespace = dom.namespaceURI + // First time rendering0 into a node clears it out + if (dom.vnodes == null) dom.textContent = "" + if (!Array.isArray(vnodes)) vnodes = [vnodes] + updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + dom.vnodes = vnodes + // document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement + if (active != null && $doc.activeElement !== active && typeof active.focus === "function") active.focus() + for (var i = 0; i < hooks.length; i++) hooks[i]() + } + return {render: render, setEventCallback: setEventCallback} +} +function throttle(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var delay = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var elapsed = Date.now() - last + if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, delay - elapsed) + } + } +} +var _12 = function($window, throttleMock) { + var renderService = coreRenderer($window) + renderService.setEventCallback(function(e) { + if (e.redraw === false) e.redraw = undefined + else redraw() + }) + var callbacks = [] + var rendering = false + function subscribe(key1, callback) { + unsubscribe(key1) + callbacks.push(key1, callback) + } + function unsubscribe(key1) { + var index = callbacks.indexOf(key1) + if (index > -1) callbacks.splice(index, 2) + } + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)} + rendering = false + } + var redraw = (throttleMock || throttle)(sync) + redraw.sync = sync + return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} +} +var redrawService = _12(window) +requestService.setCompletionCallback(redrawService.redraw) +var _17 = function(redrawService0) { + return function(root, component) { + if (component === null) { + redrawService0.render(root, []) + redrawService0.unsubscribe(root) + return + } + + if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode") + + var run0 = function() { + redrawService0.render(root, Vnode(component)) + } + redrawService0.subscribe(root, run0) + run0() + } +} +m.mount = _17(redrawService) +var Promise = PromisePolyfill +var parseQueryString = function(string) { + if (string === "" || string == null) return {} + if (string.charAt(0) === "?") string = string.slice(1) + var entries = string.split("&"), data0 = {}, counters = {} + for (var i = 0; i < entries.length; i++) { + var entry = entries[i].split("=") + var key5 = decodeURIComponent(entry[0]) + var value = entry.length === 2 ? decodeURIComponent(entry[1]) : "" + if (value === "true") value = true + else if (value === "false") value = false + var levels = key5.split(/\]\[?|\[/) + var cursor = data0 + if (key5.indexOf("[") > -1) levels.pop() + for (var j0 = 0; j0 < levels.length; j0++) { + var level = levels[j0], nextLevel = levels[j0 + 1] + var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) + var isValue = j0 === levels.length - 1 + if (level === "") { + var key5 = levels.slice(0, j0).join() + if (counters[key5] == null) counters[key5] = 0 + level = counters[key5]++ + } + if (cursor[level] == null) { + cursor[level] = isValue ? value : isNumber ? [] : {} + } + cursor = cursor[level] + } + } + return data0 +} +var coreRouter = function($window) { + var supportsPushState = typeof $window.history.pushState === "function" + var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout + function normalize1(fragment0) { + var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) + if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data + return data + } + var asyncId + function debounceAsync(callback) { + return function() { + if (asyncId != null) return + asyncId = callAsync0(function() { + asyncId = null + callback() + }) + } + } + function parsePath(path, queryData, hashData) { + var queryIndex = path.indexOf("?") + var hashIndex = path.indexOf("#") + var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length + if (queryIndex > -1) { + var queryEnd = hashIndex > -1 ? hashIndex : path.length + var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd)) + for (var key4 in queryParams) queryData[key4] = queryParams[key4] + } + if (hashIndex > -1) { + var hashParams = parseQueryString(path.slice(hashIndex + 1)) + for (var key4 in hashParams) hashData[key4] = hashParams[key4] + } + return path.slice(0, pathEnd) + } + var router = {prefix: "#!"} + router.getPath = function() { + var type2 = router.prefix.charAt(0) + switch (type2) { + case "#": return normalize1("hash").slice(router.prefix.length) + case "?": return normalize1("search").slice(router.prefix.length) + normalize1("hash") + default: return normalize1("pathname").slice(router.prefix.length) + normalize1("search") + normalize1("hash") + } + } + router.setPath = function(path, data, options) { + var queryData = {}, hashData = {} + path = parsePath(path, queryData, hashData) + if (data != null) { + for (var key4 in data) queryData[key4] = data[key4] + path = path.replace(/:([^\/]+)/g, function(match2, token) { + delete queryData[token] + return data[token] + }) + } + var query = buildQueryString(queryData) + if (query) path += "?" + query + var hash = buildQueryString(hashData) + if (hash) path += "#" + hash + if (supportsPushState) { + var state = options ? options.state : null + var title = options ? options.title : null + $window.onpopstate() + if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path) + else $window.history.pushState(state, title, router.prefix + path) + } + else $window.location.href = router.prefix + path + } + router.defineRoutes = function(routes, resolve, reject) { + function resolveRoute() { + var path = router.getPath() + var params = {} + var pathname = parsePath(path, params, params) + var state = $window.history.state + if (state != null) { + for (var k in state) params[k] = state[k] + } + for (var route0 in routes) { + var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") + if (matcher.test(pathname)) { + pathname.replace(matcher, function() { + var keys = route0.match(/:[^\/]+/g) || [] + var values = [].slice.call(arguments, 1, -2) + for (var i = 0; i < keys.length; i++) { + params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) + } + resolve(routes[route0], params, path, route0) + }) + return + } + } + reject(path, params) + } + if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) + else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute + resolveRoute() + } + return router +} +var _21 = function($window, redrawService0) { + var routeService = coreRouter($window) + var identity = function(v0) {return v0} + var render1, component, attrs3, currentPath, lastUpdate + var route = function(root, defaultRoute, routes) { + if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") + function run1() { + if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) + } + var redraw2 = function() { + run1() + redraw2 = redrawService0.redraw + } + redrawService0.subscribe(root, run1) + var bail = function(path) { + if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) + else throw new Error("Could not resolve default route " + defaultRoute) + } + routeService.defineRoutes(routes, function(payload, params, path) { + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + attrs3 = params, currentPath = path, lastUpdate = null + render1 = (routeResolver.render || identity).bind(routeResolver) + redraw2() + } + if (payload.view || typeof payload === "function") update({}, payload) + else { + if (payload.onmatch) { + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + update(payload, resolved) + }, bail) + } + else update(payload, "div") + } + }, bail) + } + route.set = function(path, data, options) { + if (lastUpdate != null) { + options = options || {} + options.replace = true + } + lastUpdate = null + routeService.setPath(path, data, options) + } + route.get = function() {return currentPath} + route.prefix = function(prefix0) {routeService.prefix = prefix0} + var link = function(options, vnode1) { + vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href) + vnode1.dom.onclick = function(e) { + if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return + e.preventDefault() + e.redraw = false + var href = this.getAttribute("href") + if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) + route.set(href, undefined, options) + } + } + route.link = function(args0) { + if (args0.tag == null) return link.bind(link, args0) + return link({}, args0) + } + route.param = function(key3) { + if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3] + return attrs3 + } + return route +} +m.route = _21(window, redrawService) +m.withAttr = function(attrName, callback, context) { + return function(e) { + callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + } +} +var _29 = coreRenderer(window) +m.render = _29.render +m.redraw = redrawService.redraw +m.request = requestService.request +m.jsonp = requestService.jsonp +m.parseQueryString = parseQueryString +m.buildQueryString = buildQueryString +m.version = "1.1.3" +m.vnode = Vnode +m.PromisePolyfill = PromisePolyfill + +export default m +export var m = m.m,trust = m.trust,fragment = m.fragment,mount = m.mount,route = m.route,withAttr = m.withAttr,render = m.render,redraw = m.redraw,request = m.request,jsonp = m.jsonp,parseQueryString = m.parseQueryString,buildQueryString = m.buildQueryString,version = m.version,vnode = m.vnode,PromisePolyfill = m.PromisePolyfill \ No newline at end of file diff --git a/ospec/esm.js b/ospec/esm.js new file mode 100644 index 00000000..4682c747 --- /dev/null +++ b/ospec/esm.js @@ -0,0 +1,19 @@ +"use strict" + +/* + +This script will create an esm compatible script +from the already compiled version of: + +- ospec.js > ospec.mjs + +*/ + +var fs = require("fs") + +var ospec = fs.readFileSync("ospec.js", "utf8") +fs.writeFileSync("ospec.mjs", + "export default " + + ospec.slice(ospec.indexOf("})") + 2) + + "()" +) diff --git a/ospec/ospec.mjs b/ospec/ospec.mjs new file mode 100644 index 00000000..e6ea506d --- /dev/null +++ b/ospec/ospec.mjs @@ -0,0 +1,363 @@ +export default (function init(name) { + var spec = {}, subjects = [], results, only = [], ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty + var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/), timeoutStackName + var globalTimeout = noTimeoutRightNow + var currentTestError = null + if (name != null) spec[name] = ctx = {} + + function o(subject, predicate) { + if (predicate === undefined) { + if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") + return new Assert(subject) + } else { + if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`") + subject = String(subject) + if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use") + ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error)) + } + } + o.before = hook("\x01before") + o.after = hook("\x01after") + o.beforeEach = hook("\x01beforeEach") + o.afterEach = hook("\x01afterEach") + o.specTimeout = function (t) { + if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()") + if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context") + if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument") + ctx["\x01specTimeout"] = t + } + o.new = init + o.spec = function(subject, predicate) { + var parent = ctx + ctx = ctx[unique(subject)] = {} + predicate() + ctx = parent + } + o.only = function(subject, predicate, silent) { + if (!silent) console.log( + highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n", + cStyle("red"), "" + ) + only.push(predicate) + o(subject, predicate) + } + o.spy = function(fn) { + var spy = function() { + spy.this = this + spy.args = [].slice.call(arguments) + spy.callCount++ + + if (fn) return fn.apply(this, arguments) + } + if (fn) + Object.defineProperties(spy, { + length: {value: fn.length}, + name: {value: fn.name} + }) + spy.args = [] + spy.callCount = 0 + return spy + } + o.cleanStackTrace = function(error) { + // For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack + if (error.stack == null) return "" + var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack + // some environments add the name and message to the stack trace + if (error.stack.indexOf(header) === 0) { + stack = error.stack.slice(header.length).split(/\r?\n/) + stack.shift() // drop the initial empty string + } else { + stack = error.stack.split(/\r?\n/) + } + if (ospecFileName == null) return stack.join("\n") + // skip ospec-related entries on the stack + while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++ + // now we're in user code (or past the stack end) + return stack[i] + } + o.timeout = function(n) { + globalTimeout(n) + } + o.run = function(reporter) { + results = [] + start = new Date + test(spec, [], [], new Task(function() { + setTimeout(function () { + timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/) + if (typeof reporter === "function") reporter(results) + else { + var errCount = o.report(results) + if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit + } + }) + }, null), 200 /*default timeout delay*/) + + function test(spec, pre, post, finalize, defaultDelay) { + if (hasOwn.call(spec, "\x01specTimeout")) defaultDelay = spec["\x01specTimeout"] + pre = [].concat(pre, spec["\x01beforeEach"] || []) + post = [].concat(spec["\x01afterEach"] || [], post) + series([].concat(spec["\x01before"] || [], Object.keys(spec).reduce(function(tasks, key) { + if (key.charCodeAt(0) !== 1 && (only.length === 0 || only.indexOf(spec[key].fn) !== -1 || !(spec[key] instanceof Task))) { + tasks.push(new Task(function(done) { + o.timeout(Infinity) + subjects.push(key) + var pop = new Task(function pop() {subjects.pop(), done()}, null) + if (spec[key] instanceof Task) series([].concat(pre, spec[key], post, pop), defaultDelay) + else test(spec[key], pre, post, pop, defaultDelay) + }, null)) + } + return tasks + }, []), spec["\x01after"] || [], finalize), defaultDelay) + } + + function series(tasks, defaultDelay) { + var cursor = 0 + next() + + function next() { + if (cursor === tasks.length) return + + var task = tasks[cursor++] + var fn = task.fn + currentTestError = task.err + var timeout = 0, delay = defaultDelay, s = new Date + var current = cursor + var arg + + globalTimeout = setDelay + + var isDone = false + // public API, may only be called once from use code (or after returned Promise resolution) + function done(err) { + if (!isDone) isDone = true + else throw new Error("`" + arg + "()` should only be called once") + if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err)) + finalizeAsync(err) + } + // for internal use only + function finalizeAsync(err) { + if (err == null) { + if (task.err != null) succeed(new Assert) + } else { + if (err instanceof Error) fail(new Assert, err.message, err) + else fail(new Assert, String(err), null) + } + if (timeout !== undefined) timeout = clearTimeout(timeout) + if (current === cursor) next() + } + function startTimer() { + timeout = setTimeout(function() { + timeout = undefined + finalizeAsync("async test timed out after " + delay + "ms") + }, Math.min(delay, 2147483647)) + } + function setDelay (t) { + if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument") + delay = t + } + if (fn.length > 0) { + var body = fn.toString() + arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop() + if (body.indexOf(arg) === body.lastIndexOf(arg)) { + var e = new Error + e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err) + throw e + } + try { + fn(done, setDelay) + } + catch (e) { + if (task.err != null) finalizeAsync(e) + // The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown. + else throw e + } + if (timeout === 0) { + startTimer() + } + } else { + try{ + var p = fn() + if (p && p.then) { + startTimer() + p.then(function() { done() }, done) + } else { + nextTickish(next) + } + } catch (e) { + if (task.err != null) finalizeAsync(e) + // The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown. + else throw e + } + } + globalTimeout = noTimeoutRightNow + } + } + } + function unique(subject) { + if (hasOwn.call(ctx, subject)) { + console.warn("A test or a spec named `" + subject + "` was already defined") + while (hasOwn.call(ctx, subject)) subject += "*" + } + return subject + } + function hook(name) { + return function(predicate) { + if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate) + ctx[name] = new Task(predicate, ensureStackTrace(new Error)) + } + } + + define("equals", "should equal", function(a, b) {return a === b}) + define("notEquals", "should not equal", function(a, b) {return a !== b}) + define("deepEquals", "should deep equal", deepEqual) + define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)}) + + function isArguments(a) { + if ("callee" in a) { + for (var i in a) if (i === "callee") return false + return true + } + } + function deepEqual(a, b) { + if (a === b) return true + if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise + if (typeof a === "object" && typeof b === "object") { + var aIsArgs = isArguments(a), bIsArgs = isArguments(b) + if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) { + for (var i in a) { + if ((!(i in b)) || !deepEqual(a[i], b[i])) return false + } + for (var i in b) { + if (!(i in a)) return false + } + return true + } + if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) { + var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b) + if (aKeys.length !== bKeys.length) return false + for (var i = 0; i < aKeys.length; i++) { + if (!hasOwn.call(b, aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false + } + return true + } + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime() + if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer) { + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true + } + if (a.valueOf() === b.valueOf()) return true + } + return false + } + + function isRunning() {return results != null} + function Assert(value) { + this.value = value + this.i = results.length + results.push({pass: null, context: "", message: "Incomplete assertion in the test definition starting at...", error: currentTestError, testError: currentTestError}) + } + function Task(fn, err) { + this.fn = fn + this.err = err + } + function define(name, verb, compare) { + Assert.prototype[name] = function assert(value) { + if (compare(this.value, value)) succeed(this) + else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value)) + var self = this + return function(message) { + if (!self.pass) self.message = message + "\n\n" + self.message + } + } + } + function succeed(assertion) { + results[assertion.i].pass = true + } + function fail(assertion, message, error) { + results[assertion.i].pass = false + results[assertion.i].context = subjects.join(" > ") + results[assertion.i].message = message + results[assertion.i].error = error != null ? error : ensureStackTrace(new Error) + } + function serialize(value) { + if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require + if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value) + else if (typeof value === "function") return value.name || "" + try {return JSON.stringify(value)} catch (e) {return String(value)} + } + function noTimeoutRightNow() { + throw new Error("o.timeout must be called snchronously from within a test definition or a hook") + } + var colorCodes = { + red: "31m", + red2: "31;1m", + green: "32;1m" + } + function highlight(message, color) { + var code = colorCodes[color] || colorCodes.red; + return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c " + } + function cStyle(color, bold) { + return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "") + } + function ensureStackTrace(error) { + // mandatory to get a stack in IE 10 and 11 (and maybe other envs?) + if (error.stack === undefined) try { throw error } catch(e) {return e} + else return error + } + function getStackName(e, exp) { + return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null + } + + o.report = function (results) { + var errCount = 0 + for (var i = 0, r; r = results[i]; i++) { + if (r.pass == null) { + r.testError.stack = r.message + "\n" + o.cleanStackTrace(r.testError) + r.testError.message = r.message + throw r.testError + } + if (!r.pass) { + var stackTrace = o.cleanStackTrace(r.error) + var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1 + if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || "" + console.error( + (hasProcess ? "\n" : "") + + highlight(r.context + ":", "red2") + "\n" + + highlight(r.message, "red") + + (stackTrace ? "\n" + stackTrace + "\n" : ""), + + cStyle("black", true), "", // reset to default + cStyle("red"), cStyle("black") + ) + errCount++ + } + } + var pl = results.length === 1 ? "" : "s" + var resultSummary = (errCount === 0) ? + highlight((pl ? "All " : "The ") + results.length + " assertion" + pl + " passed", "green"): + highlight(errCount + " out of " + results.length + " assertion" + pl + " failed", "red2") + var runningTime = " in " + Math.round(Date.now() - start) + "ms" + + console.log( + (hasProcess ? "––––––\n" : "") + + (name ? name + ": " : "") + resultSummary + runningTime, + cStyle((errCount === 0 ? "green" : "red"), true), "" + ) + return errCount + } + + if (hasProcess) { + nextTickish = process.nextTick + } else { + nextTickish = function fakeFastNextTick(next) { + if (stack++ < 5000) next() + else setTimeout(next, stack = 0) + } + } + + return o +}) +() \ No newline at end of file diff --git a/ospec/package.json b/ospec/package.json index fa2baf42..d6282329 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -3,6 +3,7 @@ "version": "3.0.1", "description": "Noiseless testing framework", "main": "ospec.js", + "module": "ospec.mjs", "directories": { "test": "tests" }, @@ -12,6 +13,9 @@ "bin": { "ospec": "./bin/ospec" }, + "scripts": { + "prepublishOnly": "node esm.js" + }, "repository": "MithrilJS/mithril.js", "dependencies": { "glob": "^7.1.2" diff --git a/package.json b/package.json index 7932102f..5765749c 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,14 @@ "author": "Leo Horie", "license": "MIT", "main": "mithril.js", + "module": "mithril.mjs", "repository": "MithrilJS/mithril.js", "scripts": { "dev": "node bundler/cli browser.js -output mithril.js -watch", - "build": "npm run build-browser & npm run build-min", + "build": "npm run build-browser & npm run build-min && npm run build-esm", "build-browser": "node bundler/cli browser.js -output mithril.js", "build-min": "node bundler/cli browser.js -output mithril.min.js -minify", + "build-esm": "node esm.js", "precommit": "lint-staged", "lintdocs": "node docs/lint", "gendocs": "node docs/generate", diff --git a/stream/package.json b/stream/package.json index f5f4f86c..d78616b6 100644 --- a/stream/package.json +++ b/stream/package.json @@ -3,6 +3,7 @@ "version": "1.1.0", "description": "Streaming data, mithril-style", "main": "stream.js", + "module": "stream.mjs", "directories": { "test": "tests" }, diff --git a/stream/stream.mjs b/stream/stream.mjs new file mode 100644 index 00000000..940e6e5d --- /dev/null +++ b/stream/stream.mjs @@ -0,0 +1,156 @@ +/* eslint-enable */ + +var guid = 0, HALT = {} +function createStream() { + function stream() { + if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0]) + return stream._state.value + } + initStream(stream) + + if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0]) + + return stream +} +function initStream(stream) { + stream.constructor = createStream + stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined, unregister: undefined} + stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream + stream.toJSON = toJSON + + Object.defineProperties(stream, { + end: {get: function() { + if (!stream._state.endStream) { + var endStream = createStream() + endStream.map(function(value) { + if (value === true) { + unregisterStream(stream) + endStream._state.unregister = function(){unregisterStream(endStream)} + } + return value + }) + stream._state.endStream = endStream + } + return stream._state.endStream + }} + }) +} +function updateStream(stream, value) { + updateState(stream, value) + for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false) + if (stream._state.unregister != null) stream._state.unregister() + finalize(stream) +} +function updateState(stream, value) { + stream._state.value = value + stream._state.changed = true + if (stream._state.state !== 2) stream._state.state = 1 +} +function updateDependency(stream, mustSync) { + var state = stream._state, parents = state.parents + if (parents.length > 0 && parents.every(active) && (mustSync || parents.some(changed))) { + var value = stream._state.derive() + if (value === HALT) return unregisterStream(stream) + updateState(stream, value) + } +} +function finalize(stream) { + stream._state.changed = false + for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false +} + +function combine(fn, streams) { + if (!streams.every(valid)) throw new Error("Ensure that each item passed to stream.combine/stream.merge is a stream") + return initDependency(createStream(), streams, function() { + return fn.apply(this, streams.concat([streams.filter(changed)])) + }) +} + +function initDependency(dep, streams, derive) { + var state = dep._state + state.derive = derive + state.parents = streams.filter(notEnded) + + registerDependency(dep, state.parents) + updateDependency(dep, true) + + return dep +} +function registerDependency(stream, parents) { + for (var i = 0; i < parents.length; i++) { + parents[i]._state.deps[stream._state.id] = stream + registerDependency(stream, parents[i]._state.parents) + } +} +function unregisterStream(stream) { + for (var i = 0; i < stream._state.parents.length; i++) { + var parent = stream._state.parents[i] + delete parent._state.deps[stream._state.id] + } + for (var id in stream._state.deps) { + var dependent = stream._state.deps[id] + var index = dependent._state.parents.indexOf(stream) + if (index > -1) dependent._state.parents.splice(index, 1) + } + stream._state.state = 2 //ended + stream._state.deps = {} +} + +function map(fn) {return combine(function(stream) {return fn(stream())}, [this])} +function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])} +function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value} + +function valid(stream) {return stream._state } +function active(stream) {return stream._state.state === 1} +function changed(stream) {return stream._state.changed} +function notEnded(stream) {return stream._state.state !== 2} + +function merge(streams) { + return combine(function() { + return streams.map(function(s) {return s()}) + }, streams) +} + +function scan(reducer, seed, stream) { + var newStream = combine(function (s) { + var next = reducer(seed, s._state.value) + if (next !== HALT) return seed = next + return HALT + }, [stream]) + + if (newStream._state.state === 0) newStream(seed) + + return newStream +} + +function scanMerge(tuples, seed) { + var streams = tuples.map(function(tuple) { + var stream = tuple[0] + if (stream._state.state === 0) stream(undefined) + return stream + }) + + var newStream = combine(function() { + var changed = arguments[arguments.length - 1] + + streams.forEach(function(stream, idx) { + if (changed.indexOf(stream) > -1) { + seed = tuples[idx][1](seed, stream._state.value) + } + }) + + return seed + }, streams) + + return newStream +} + +createStream["fantasy-land/of"] = createStream +createStream.merge = merge +createStream.combine = combine +createStream.scan = scan +createStream.scanMerge = scanMerge +createStream.HALT = HALT + + +export default createStream \ No newline at end of file