From b2faa43f9176673a573103a089db40685251812d Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Wed, 16 Dec 2015 11:04:07 -0500 Subject: [PATCH] Revert "Revert "More performance improvements + etc."" --- .eslintignore | 2 +- .gitignore | 1 + bench/README.md | 4 + bench/app/.gitignore | 6 + bench/app/index.html | 26 + bench/app/js/app.js | 13 + bench/app/js/controllers/todo.js | 111 + bench/app/js/models/storage.js | 14 + bench/app/js/models/todo.js | 12 + bench/app/js/views/footer-view.js | 40 + bench/app/js/views/main-view.js | 119 + .../node_modules/todomvc-app-css/index.css | 378 +++ .../app/node_modules/todomvc-common/base.css | 141 + bench/app/node_modules/todomvc-common/base.js | 249 ++ bench/app/package.json | 7 + bench/app/readme.md | 18 + bench/index.html | 81 + bench/resources/benchmark-runner.js | 268 ++ bench/resources/manager.js | 239 ++ bench/resources/tests.js | 51 + mithril.js | 2670 +++++++++-------- mithril.min.js | 2 +- mithril.min.js.map | 2 +- package.json | 3 +- test/index.html | 5 +- test/mithril.js | 15 + test/mithril.render.js | 44 +- test/mithril.trust.js | 12 +- 28 files changed, 3207 insertions(+), 1326 deletions(-) create mode 100644 bench/README.md create mode 100644 bench/app/.gitignore create mode 100644 bench/app/index.html create mode 100644 bench/app/js/app.js create mode 100644 bench/app/js/controllers/todo.js create mode 100644 bench/app/js/models/storage.js create mode 100644 bench/app/js/models/todo.js create mode 100644 bench/app/js/views/footer-view.js create mode 100644 bench/app/js/views/main-view.js create mode 100644 bench/app/node_modules/todomvc-app-css/index.css create mode 100644 bench/app/node_modules/todomvc-common/base.css create mode 100644 bench/app/node_modules/todomvc-common/base.js create mode 100644 bench/app/package.json create mode 100644 bench/app/readme.md create mode 100644 bench/index.html create mode 100644 bench/resources/benchmark-runner.js create mode 100644 bench/resources/manager.js create mode 100644 bench/resources/tests.js diff --git a/.eslintignore b/.eslintignore index a6ff032f..0b24c5c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,5 @@ # Most of these are build artifacts. -node_modules +**/node_modules **/*.min.js archive deploy diff --git a/.gitignore b/.gitignore index aeab5a02..3f3a9e04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules +!bench/** archive diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 00000000..16fcc8b9 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,4 @@ +This suite is taken with modifications from the Mithril example in +[Merri/nom](https://github.com/Merri/nom/tree/gh-pages/todomvc/benchmark/mithril-new). + +(For example, it uses Mithril itself to help simplify the business logic.) diff --git a/bench/app/.gitignore b/bench/app/.gitignore new file mode 100644 index 00000000..11c90f9c --- /dev/null +++ b/bench/app/.gitignore @@ -0,0 +1,6 @@ +node_modules/todomvc-app-css/* +!node_modules/todomvc-app-css/index.css + +node_modules/todomvc-common/* +!node_modules/todomvc-common/base.js +!node_modules/todomvc-common/base.css diff --git a/bench/app/index.html b/bench/app/index.html new file mode 100644 index 00000000..e42cfacf --- /dev/null +++ b/bench/app/index.html @@ -0,0 +1,26 @@ + + + + + Mithril • TodoMVC + + + + +
+ + + + + + + + + + + diff --git a/bench/app/js/app.js b/bench/app/js/app.js new file mode 100644 index 00000000..d4a8e6fb --- /dev/null +++ b/bench/app/js/app.js @@ -0,0 +1,13 @@ +/* global m */ +(function (app) { + "use strict" + + app.ENTER_KEY = 13 + app.ESC_KEY = 27 + + m.route.mode = "hash" + m.route(document.getElementById("todoapp"), "/", { + "/": app, + "/:filter": app + }) +})(this.app || (this.app = {})) diff --git a/bench/app/js/controllers/todo.js b/bench/app/js/controllers/todo.js new file mode 100644 index 00000000..27577689 --- /dev/null +++ b/bench/app/js/controllers/todo.js @@ -0,0 +1,111 @@ +/* global m */ +(function (app) { + "use strict" + + app.controller = function () { + /* eslint-disable no-warning-comments */ + // Todo collection, update with props + /* eslint-enable no-warning-comments */ + this.list = app.storage.get().map(function (item) { + return new app.Todo(item) + }) + + // Temp title placeholder + this.title = m.prop("") + + /* eslint-disable no-warning-comments */ + // Todo list filter + /* eslint-enable no-warning-comments */ + this.filter = m.prop(m.route.param("filter") || "") + + this.add = function () { + var title = this.title().trim() + if (title) { + this.list.push(new app.Todo({title: title})) + app.storage.put(this.list) + } + this.title("") + } + + this.isVisible = function (todo) { + switch (this.filter()) { + case "active": return !todo.completed() + case "completed": return todo.completed() + default: return true + } + } + + this.complete = function (todo) { + if (todo.completed()) { + todo.completed(false) + } else { + todo.completed(true) + } + app.storage.put(this.list) + } + + this.edit = function (todo) { + todo.previousTitle = todo.title() + todo.editing(true) + } + + this.doneEditing = function (todo, index) { + todo.editing(false) + todo.title(todo.title().trim()) + if (!todo.title()) { + this.list.splice(index, 1) + } + app.storage.put(this.list) + } + + this.cancelEditing = function (todo) { + todo.title(todo.previousTitle) + todo.editing(false) + } + + this.clearTitle = function () { + this.title("") + } + + this.remove = function (key) { + this.list.splice(key, 1) + app.storage.put(this.list) + } + + this.clearCompleted = function () { + for (var i = this.list.length - 1; i >= 0; i--) { + if (this.list[i].completed()) { + this.list.splice(i, 1) + } + } + app.storage.put(this.list) + } + + this.amountCompleted = function () { + var amount = 0 + for (var i = 0; i < this.list.length; i++) { + if (this.list[i].completed()) { + amount++ + } + } + return amount + } + + this.allCompleted = function () { + for (var i = 0; i < this.list.length; i++) { + if (!this.list[i].completed()) { + return false + } + } + return true + } + + this.completeAll = function () { + var allCompleted = this.allCompleted() + for (var i = 0; i < this.list.length; i++) { + this.list[i].completed(!allCompleted) + } + app.storage.put(this.list) + } + } +})(this.app || (this.app = {})) diff --git a/bench/app/js/models/storage.js b/bench/app/js/models/storage.js new file mode 100644 index 00000000..0e867e9a --- /dev/null +++ b/bench/app/js/models/storage.js @@ -0,0 +1,14 @@ +(function (app) { + "use strict" + + var STORAGE_ID = "todos-mithril" + + app.storage = { + get: function () { + return JSON.parse(localStorage.getItem(STORAGE_ID) || "[]") + }, + put: function (todos) { + localStorage.setItem(STORAGE_ID, JSON.stringify(todos)) + } + } +})(this.app || (this.app = {})) diff --git a/bench/app/js/models/todo.js b/bench/app/js/models/todo.js new file mode 100644 index 00000000..77af9ba2 --- /dev/null +++ b/bench/app/js/models/todo.js @@ -0,0 +1,12 @@ +/* global m */ + +(function (app) { + "use strict" + + // Todo Model + app.Todo = function (data) { + this.title = m.prop(data.title) + this.completed = m.prop(data.completed || false) + this.editing = m.prop(data.editing || false) + } +})(this.app || (this.app = {})) diff --git a/bench/app/js/views/footer-view.js b/bench/app/js/views/footer-view.js new file mode 100644 index 00000000..25f07f1c --- /dev/null +++ b/bench/app/js/views/footer-view.js @@ -0,0 +1,40 @@ +/* global m */ +(function (app) { + "use strict" + + var filter = { + view: function (_, ctrl, expected, name, href) { + return m("li", [ + m("a", { + href: href, + config: m.route, + class: ctrl.filter() === expected ? "selected" : "" + }, name) + ]) + } + } + + app.footer = { + view: function (_, ctrl) { + var amountCompleted = ctrl.amountCompleted() + var amountActive = ctrl.list.length - amountCompleted + + return m("footer#footer", [ + m("span#todo-count", [ + m("strong", amountActive), " item" + + (amountActive !== 1 ? "s" : "") + " left" + ]), + m("ul#filters", [ + m(filter, ctrl, "", "All", "/"), + m(filter, ctrl, "active", "Active", "/active"), + m(filter, ctrl, "completed", "Completed", "/completed") + ]), + ctrl.amountCompleted() ? m("button#clear-completed", { + onclick: function () { + ctrl.clearCompleted() + } + }, "Clear completed") : null + ]) + } + } +})(this.app || (this.app = {})) diff --git a/bench/app/js/views/main-view.js b/bench/app/js/views/main-view.js new file mode 100644 index 00000000..9dfd2667 --- /dev/null +++ b/bench/app/js/views/main-view.js @@ -0,0 +1,119 @@ +/* global m */ +(function (app) { + "use strict" + + // View utility + app.watchInput = function (onenter, onescape) { + return function (e) { + if (e.keyCode === app.ENTER_KEY) { + onenter() + } else if (e.keyCode === app.ESC_KEY) { + onescape() + } + } + } + + var header = { + controller: function () { + this.focused = false + }, + + view: function (ctrl, parentCtrl) { + return m("header#header", [ + m("h1", "todos"), + m('input#new-todo[placeholder="What needs to be done?"]', { + onkeyup: app.watchInput( + function () { + parentCtrl.add() + }, + function () { + parentCtrl.clearTitle() + }), + value: parentCtrl.title(), + oninput: m.withAttr("value", parentCtrl.title), + config: function (element) { + if (!ctrl.focused) { + element.focus() + ctrl.focused = true + } + } + }) + ]) + } + } + + var todo = { + view: function (_, parentCtrl, task, index) { + return m("li", { + class: (task.completed() ? "completed" : "") + + (task.editing() ? " editing" : "") + }, [ + m(".view", [ + m("input.toggle[type=checkbox]", { + onclick: m.withAttr("checked", function () { + parentCtrl.complete(task) + }), + checked: task.completed() + }), + m("label", { + ondblclick: function () { + parentCtrl.edit(task) + } + }, task.title()), + m("button.destroy", { + onclick: function () { + parentCtrl.remove(index) + } + }) + ]), + m("input.edit", { + value: task.title(), + onkeyup: app.watchInput( + function () { + parentCtrl.doneEditing(task, index) + }, + function () { + parentCtrl.cancelEditing(task) + }), + oninput: m.withAttr("value", task.title), + config: function (element) { + if (task.editing()) { + element.focus() + element.selectionStart = + element.value.length + } + }, + onblur: function () { + parentCtrl.doneEditing(task, index) + } + }) + ]) + } + } + + app.view = function (ctrl) { + return m("div", [ + m(header, ctrl), + m("section#main", { + style: { + display: ctrl.list.length ? "" : "none" + } + }, [ + m("input#toggle-all[type=checkbox]", { + onclick: ctrl.completeAll.bind(ctrl), + checked: ctrl.allCompleted() + }), + m("ul#todo-list", [ + ctrl.list + .filter(function () { + return ctrl.isVisible() + }) + .map(function (task, index) { + return m(todo, ctrl, task, index) + }) + ]) + ]), + ctrl.list.length === 0 ? "" : m(app.footer, ctrl) + ]) + } +})(this.app || (this.app = {})) diff --git a/bench/app/node_modules/todomvc-app-css/index.css b/bench/app/node_modules/todomvc-app-css/index.css new file mode 100644 index 00000000..54d89abd --- /dev/null +++ b/bench/app/node_modules/todomvc-app-css/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +#todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +#todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +#new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +#main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +#toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +#toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +#toggle-all:checked:before { + color: #737373; +} + +#todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +#todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +#todo-list li:last-child { + border-bottom: none; +} + +#todo-list li.editing { + border-bottom: none; + padding: 0; +} + +#todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +#todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +#todo-list li label { + white-space: pre; + word-break: break-word; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +#todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +#todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +#todo-list li .destroy:hover { + color: #af5b5e; +} + +#todo-list li .destroy:after { + content: '×'; +} + +#todo-list li:hover .destroy { + display: block; +} + +#todo-list li .edit { + display: none; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +#todo-count { + float: left; + text-align: left; +} + +#todo-count strong { + font-weight: 300; +} + +#filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +#filters li { + display: inline; +} + +#filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +#filters li a.selected, +#filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +#filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +#clear-completed, +html #clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +#clear-completed:hover { + text-decoration: underline; +} + +#info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +#info p { + line-height: 1; +} + +#info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +#info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + #toggle-all, + #todo-list li .toggle { + background: none; + } + + #todo-list li .toggle { + height: 40px; + } + + #toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + #footer { + height: 50px; + } + + #filters { + bottom: 10px; + } +} diff --git a/bench/app/node_modules/todomvc-common/base.css b/bench/app/node_modules/todomvc-common/base.css new file mode 100644 index 00000000..da65968a --- /dev/null +++ b/bench/app/node_modules/todomvc-common/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/bench/app/node_modules/todomvc-common/base.js b/bench/app/node_modules/todomvc-common/base.js new file mode 100644 index 00000000..3c6723f3 --- /dev/null +++ b/bench/app/node_modules/todomvc-common/base.js @@ -0,0 +1,249 @@ +/* global _ */ +(function () { + 'use strict'; + + /* jshint ignore:start */ + // Underscore's Template Module + // Courtesy of underscorejs.org + var _ = (function (_) { + _.defaults = function (object) { + if (!object) { + return object; + } + for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { + var iterable = arguments[argsIndex]; + if (iterable) { + for (var key in iterable) { + if (object[key] == null) { + object[key] = iterable[key]; + } + } + } + } + return object; + } + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + return _; + })({}); + + if (location.hostname === 'todomvc.com') { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-31081062-1', 'auto'); + ga('send', 'pageview'); + } + /* jshint ignore:end */ + + function redirect() { + if (location.hostname === 'tastejs.github.io') { + location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); + } + } + + function findRoot() { + var base = location.href.indexOf('examples/'); + return location.href.substr(0, base); + } + + function getFile(file, callback) { + if (!location.host) { + return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); + } + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', findRoot() + file, true); + xhr.send(); + + xhr.onload = function () { + if (xhr.status === 200 && callback) { + callback(xhr.responseText); + } + }; + } + + function Learn(learnJSON, config) { + if (!(this instanceof Learn)) { + return new Learn(learnJSON, config); + } + + var template, framework; + + if (typeof learnJSON !== 'object') { + try { + learnJSON = JSON.parse(learnJSON); + } catch (e) { + return; + } + } + + if (config) { + template = config.template; + framework = config.framework; + } + + if (!template && learnJSON.templates) { + template = learnJSON.templates.todomvc; + } + + if (!framework && document.querySelector('[data-framework]')) { + framework = document.querySelector('[data-framework]').dataset.framework; + } + + this.template = template; + + if (learnJSON.backend) { + this.frameworkJSON = learnJSON.backend; + this.frameworkJSON.issueLabel = framework; + this.append({ + backend: true + }); + } else if (learnJSON[framework]) { + this.frameworkJSON = learnJSON[framework]; + this.frameworkJSON.issueLabel = framework; + this.append(); + } + + this.fetchIssueCount(); + } + + Learn.prototype.append = function (opts) { + var aside = document.createElement('aside'); + aside.innerHTML = _.template(this.template, this.frameworkJSON); + aside.className = 'learn'; + + if (opts && opts.backend) { + // Remove demo link + var sourceLinks = aside.querySelector('.source-links'); + var heading = sourceLinks.firstElementChild; + var sourceLink = sourceLinks.lastElementChild; + // Correct link path + var href = sourceLink.getAttribute('href'); + sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); + sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; + } else { + // Localize demo links + var demoLinks = aside.querySelectorAll('.demo-link'); + Array.prototype.forEach.call(demoLinks, function (demoLink) { + if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { + demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); + } + }); + } + + document.body.className = (document.body.className + ' learn-bar').trim(); + document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); + }; + + Learn.prototype.fetchIssueCount = function () { + var issueLink = document.getElementById('issue-count-link'); + if (issueLink) { + var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onload = function (e) { + var parsedResponse = JSON.parse(e.target.responseText); + if (parsedResponse instanceof Array) { + var count = parsedResponse.length; + if (count !== 0) { + issueLink.innerHTML = 'This app has ' + count + ' open issues'; + document.getElementById('issue-count').style.display = 'inline'; + } + } + }; + xhr.send(); + } + }; + + redirect(); + getFile('learn.json', Learn); +})(); diff --git a/bench/app/package.json b/bench/app/package.json new file mode 100644 index 00000000..3b1e70fd --- /dev/null +++ b/bench/app/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "dependencies": { + "todomvc-common": "^1.0.1", + "todomvc-app-css": "^1.0.1" + } +} diff --git a/bench/app/readme.md b/bench/app/readme.md new file mode 100644 index 00000000..76a71d43 --- /dev/null +++ b/bench/app/readme.md @@ -0,0 +1,18 @@ +# Mithril TodoMVC Example + +> [Mithril](http://lhorie.github.io/mithril/) is a client-side MVC framework - a tool to organize code in a way that is easy to think about and to maintain. + +> _[Mithril - lhorie.github.io/mithril/](http://lhorie.github.io/mithril/)_ + +## Learning Mithril + +The [Mithril website](http://lhorie.github.io/mithril/getting-started.html) is a great resource for getting started. + +Here are some links you may find helpful: + +* [Official Documentation](http://lhorie.github.io/mithril/mithril.html) + +_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._ + +## Credit +This TodoMVC application was created by [taylorhakes](https://github.com/taylorhakes). \ No newline at end of file diff --git a/bench/index.html b/bench/index.html new file mode 100644 index 00000000..6f399b3d --- /dev/null +++ b/bench/index.html @@ -0,0 +1,81 @@ + +TodoMVC Benchmark + + + + + + +
+
+
+
+ diff --git a/bench/resources/benchmark-runner.js b/bench/resources/benchmark-runner.js new file mode 100644 index 00000000..e3d2e908 --- /dev/null +++ b/bench/resources/benchmark-runner.js @@ -0,0 +1,268 @@ +(function (global) { // eslint-disable-line max-statements + "use strict" + var m = global.m + + window.onhashchange = function () { + location.reload() + } + + global.BenchmarkTestStep = BenchmarkTestStep + function BenchmarkTestStep(name, run) { + this.name = name + this.run = run + } + + global.BenchmarkRunner = BenchmarkRunner + function BenchmarkRunner(suites, client) { + this._suites = suites + this._prepareReturnValue = null + this._measuredValues = {} + this._client = client + } + + BenchmarkRunner.prototype.waitForElement = function (selector) { + var deferred = m.deferred() + var contentDocument = this._frame.contentDocument + + function resolveIfReady() { + var element = contentDocument.querySelector(selector) + if (element) { + return deferred.resolve(element) + } + setTimeout(resolveIfReady, 50) + } + + resolveIfReady() + return deferred.promise + } + + BenchmarkRunner.prototype._removeFrame = function () { + if (this._frame) { + this._frame.parentNode.removeChild(this._frame) + this._frame = null + } + } + + BenchmarkRunner.prototype._appendFrame = function () { + var frame = document.createElement("iframe") + frame.style.width = "800px" + frame.style.height = "600px" + document.body.appendChild(frame) + this._frame = frame + return frame + } + + BenchmarkRunner.prototype._waitAndWarmUp = function () { + var startTime = Date.now() + + function Fibonacci(n) { + if (Date.now() - startTime > 100) return + else if (n <= 0) return 0 + else if (n === 1) return 1 + else return Fibonacci(n - 2) + Fibonacci(n - 1) + } + + var deferred = m.deferred() + setTimeout(function () { + Fibonacci(100) + deferred.resolve() + }, 200) + return deferred.promise + } + + var now = window.performance && window.performance.now ? + function () { return window.performance.now() } : + function () { return +new Date() } + + function logResults(document, expected, suite, test, callback) { + var count = document.querySelectorAll(".view").length + if (count !== expected) { + console.error([ // eslint-disable-line no-console + suite.name, + test.name, + "expected", + expected, + "got", + count + ]) + callback(NaN, NaN) + } + } + + // This function must be as simple as possible, so it doesn't interfere + // with reading the times + BenchmarkRunner.prototype._runTest = function ( + suite, + test, + prepareReturnValue, + callback + ) { + var testFunction = test.run + + var window = this._frame.contentWindow + var document = this._frame.contentDocument + var expected = this._client.numberOfItemsToAdd + + var startTime = now() + testFunction(prepareReturnValue, window, document) + var endTime = now() + var syncTime = endTime - startTime + + startTime = now() + setTimeout(function () { + setTimeout(function () { + var endTime = now() + + // if the DOM count is wrong after a test, don't report its + // results. + if (/Adding|Completing/.test(test.name)) { + logResults(document, expected, suite, test, callback) + } + + if (/Deleting/.test(test.name)) { + logResults(document, 0, suite, test, callback) + } + + callback(syncTime, endTime - startTime) + }, 0) + }, 0) + } + + function BenchmarkState(suites) { + this._suites = suites + this._suiteIndex = -1 + this._testIndex = 0 + this.next() + } + + BenchmarkState.prototype.currentSuite = function () { + return this._suites[this._suiteIndex] + } + + BenchmarkState.prototype.currentTest = function () { + var suite = this.currentSuite() + return suite ? suite.tests[this._testIndex] : null + } + + BenchmarkState.prototype.next = function () { + this._testIndex++ + + var suite = this._suites[this._suiteIndex] + if (suite && this._testIndex < suite.tests.length) { + return this + } + + this._testIndex = 0 + + var i = this._suiteIndex + var suites = this._suites + + do { + i++ + } while (i < suites.length && suites[i].disabled) + + this._suiteIndex = i + + return this + } + + BenchmarkState.prototype.isFirstTest = function () { + return !this._testIndex + } + + BenchmarkState.prototype.prepareCurrentSuite = function (runner, frame) { + var suite = this.currentSuite() + var deferred = m.deferred() + frame.onload = function () { + suite.prepare(runner, frame.contentWindow, frame.contentDocument) + .then(function (result) { + deferred.resolve(result) + }, function (err) { + deferred.reject(err) + }) + } + frame.src = suite.url + return deferred.promise + } + + BenchmarkRunner.prototype.step = function (state) { + if (!state) { + state = new BenchmarkState(this._suites) + } + + var suite = state.currentSuite() + if (!suite) { + this._finalize() + var deferred = m.deferred() + deferred.resolve() + return deferred.promise + } + + if (state.isFirstTest()) { + this._masuredValuesForCurrentSuite = {} + var self = this + return state.prepareCurrentSuite(this, this._appendFrame()) + .then(function (prepareReturnValue) { + self._prepareReturnValue = prepareReturnValue + return self._runTestAndRecordResults(state) + }) + } + + return this._runTestAndRecordResults(state) + } + + BenchmarkRunner.prototype._runTestAndRecordResults = function (state) { + var deferred = m.deferred() + var suite = state.currentSuite() + var test = state.currentTest() + + if (this._client && this._client.willRunTest) { + this._client.willRunTest(suite, test) + } + + var self = this + setTimeout(function () { + self._runTest(suite, test, self._prepareReturnValue, + function (syncTime, asyncTime) { + var suiteResults + if (self._measuredValues[suite.name]) { + suiteResults = self._measuredValues[suite.name] + } else { + suiteResults = {tests: {}, total: 0} + } + + self._measuredValues[suite.name] = suiteResults + + suiteResults.tests[test.name] = { + Sync: syncTime, + Async: asyncTime + } + + suiteResults.total += syncTime + asyncTime + + if (self._client && self._client.willRunTest) { + self._client.didRunTest(suite, test) + } + + state.next() + if (state.currentSuite() !== suite) { + self._removeFrame() + } + + deferred.resolve(state) + }) + }, 0) + return deferred.promise + } + + BenchmarkRunner.prototype._finalize = function () { + this._removeFrame() + + if (this._client && this._client.didRunSuites) { + this._client.didRunSuites(this._measuredValues) + } + + // FIXME: This should be done when we start running tests. + this._measuredValues = {} + } +})(this) diff --git a/bench/resources/manager.js b/bench/resources/manager.js new file mode 100644 index 00000000..3efa55e3 --- /dev/null +++ b/bench/resources/manager.js @@ -0,0 +1,239 @@ +/* global m */ +/* eslint no-console: 0 */ + +(function (global) { // eslint-disable-line max-statements + "use strict" + + var numberOfItemsToAdd = (~~location.hash.slice(1)) || 250 + var runs = [] + var timesRan = 0 + var runButton + + function forOwn(object, f) { + for (var key in object) { + if ({}.hasOwnProperty.call(object, key)) { + f(object[key], key) + } + } + } + + var append = Function.call.bind(function append() { + /* eslint-disable no-invalid-this */ + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i] + if (Array.isArray(arg)) append.apply(this, arg) + else this.appendChild(arg) + } + return this + /* eslint-enable no-invalid-this */ + }) + + function n(type, attrs) { + var el = document.createElement(type) + if (!attrs) return el + forOwn(attrs, function (value, attr) { + if (attr === "style") { + forOwn(value, function (k, v) { el.style[k] = v }) + } else { + el[attr] = value + } + }) + return el + } + + function createButton(text, onclick) { + return n("button", {textContent: text, onclick: onclick}) + } + + function createTest(suite, test) { + return append(n("li"), + append(test.anchor = n("a", {id: suite.name + "-" + test.name}), + document.createTextNode(suite.name + "/" + test.name))) + } + + function createSuiteCheckbox(suite) { + return n("input", { + id: suite.name, + type: "checkbox", + checked: true, + onchange: function () { + suite.disabled = !this.checked + } + }) + } + + function createSuiteLabel(suite) { + return append(n("label", {htmlFor: suite.name}), + document.createTextNode(suite.name + " " + suite.version)) + } + + function createSuite(suite) { + return append(n("li"), + createSuiteCheckbox(suite), + createSuiteLabel(suite), + append(n("ol"), suite.tests.map(createTest.bind(null, suite)))) + } + + function createUIForSuites(suites, onstep, onrun) { + return append(n("nav"), + createButton("Step Tests", onstep), + runButton = createButton("Run All", onrun), + append(n("ol"), suites.map(createSuite))) + } + + function generateResults(measured, timesToRun) { + var result = "" + var total = 0 // FIXME: Compute the total properly. + + forOwn(measured, function (suiteResults, suite) { + forOwn(suiteResults.tests, function (testResults, test) { + forOwn(testResults, function (subtestResults, subtest) { + result += suite + " : " + test + " : " + subtest + ": " + + subtestResults + " ms\n" + }) + }) + result += suite + " : " + suiteResults.total + " ms\n" + total += suiteResults.total + }) + return result + "Run " + (runs.length + 1) + "/" + timesToRun + + " - Total : " + total + " ms\n" + } + + function reportFastest() { + var results = {} + runs.forEach(function (runData) { + forOwn(runData, function (data, key) { + results[key] = Math.min( + results[key] || Infinity, + data.total + ) + }) + }) + return results + } + + global.google.load("visualization", "1", {packages: ["corechart"]}) + function drawChart(results) { + var V = global.google.visualization + + var raw = [] + forOwn(results, function (result, key) { + raw.push([key, Math.round(result), colorify(key)]) + }) + raw.sort(function (a, b) { return a[1] - b[1] }) + raw.unshift(["Project", "Time", {role: "style"}]) + + var runWord = "run" + (runs.length > 1 ? "s" : "") + var title = "Best time in milliseconds over " + runs.length + " " + + runWord + " (lower is better)" + + var view = new V.DataView(V.arrayToDataTable(raw)) + view.setColumns([0, 1, { + calc: "stringify", + sourceColumn: 1, + type: "string", + role: "annotation" + }, 2]) + + document.getElementById("analysis").style.display = "block" + + new V.BarChart(document.getElementById("barchart-values")).draw(view, { + title: "TodoMVC Benchmark", + width: 600, + height: 400, + legend: {position: "none"}, + backgroundColor: "transparent", + hAxis: {title: title}, + min: 0, + max: 1500 + }) + } + + function colorPart(n, pre) { + return Math.max( + 0, + ((n.toLowerCase().charCodeAt(pre % n.length) - 97) / 26 * 255) | 0 + ) + } + + function colorify(n) { + return "rgb(" + + colorPart(n, 3) + ", " + + colorPart(n, 4) + ", " + + colorPart(n, 5) + ")" + } + + function shuffle(ary) { + for (var i = 0; i < ary.length; i++) { + var j = Math.floor(Math.random() * (i + 1)) + var tmp = ary[i] + ary[i] = ary[j] + ary[j] = tmp + } + } + + window.addEventListener("load", function () { + var match = window.location.search.match(/[\?&]r=(\d+)/) + var timesToRun = match ? +(match[1]) : 1 + + var Suites = global.Suites.map(function (suite) { + suite = suite(numberOfItemsToAdd) + suite.disabled = false + return suite + }) + + var runner = new global.BenchmarkRunner(Suites, { + numberOfItemsToAdd: numberOfItemsToAdd, + + willRunTest: function (suite, test) { + if (test.anchor.classList) { + test.anchor.classList.add("running") + } + }, + + didRunTest: function (suite, test) { + if (test.anchor.classList) { + test.anchor.classList.remove("running") + test.anchor.classList.add("ran") + } + }, + + didRunSuites: function (measured) { + var results = generateResults(measured, timesToRun) + if (results) { + console.log(results) + + runs.push(measured) + timesRan++ + if (timesRan >= timesToRun) { + timesRan = 0 + drawChart(reportFastest()) + shuffle(Suites) + } else { + setTimeout(function () { + runButton.click() + }, 0) + } + } + } + }) + + var currentState = m.prop() + function callNextStep(state) { + runner.step(state).then(currentState).then(function (newState) { + if (newState) callNextStep(newState) + }) + } + + // Don't call step while step is already executing. + document.body.appendChild(createUIForSuites(Suites, + function () { + runner.step(currentState()).then(currentState) + }, + function () { + document.getElementById("analysis").style.display = "none" + localStorage.clear() + callNextStep(currentState()) + })) + }) +})(this) diff --git a/bench/resources/tests.js b/bench/resources/tests.js new file mode 100644 index 00000000..872fcfa4 --- /dev/null +++ b/bench/resources/tests.js @@ -0,0 +1,51 @@ +(function (global) { + "use strict" + + var BenchmarkTestStep = global.BenchmarkTestStep + var Suites = global.Suites = [] + + Suites.push(function (numberOfItemsToAdd) { + return { + name: "Mithril (TodoMVC 1.3)", + url: "app/index.html", + version: "next (dev version)", + prepare: function (runner) { + return runner.waitForElement("#new-todo") + .then(function (element) { + element.focus() + return element + }) + }, + tests: [ + new BenchmarkTestStep("Adding" + numberOfItemsToAdd + "Items", + function (newTodo) { + for (var i = 0; i < numberOfItemsToAdd; i++) { + var inputEvent = document.createEvent("Event") + inputEvent.initEvent("input", true, true) + newTodo.value = "Mithril ------- Something to do " + i + newTodo.dispatchEvent(inputEvent) + + var keydownEvent = document.createEvent("Event") + keydownEvent.initEvent("keyup", true, true) + keydownEvent.keyCode = 13 // VK_ENTER + newTodo.dispatchEvent(keydownEvent) + } + }), + new BenchmarkTestStep("CompletingAllItems", + function (newTodo, contentWindow, document) { + var checkboxes = document.getElementsByClassName("toggle") + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].click() + } + }), + new BenchmarkTestStep("DeletingAllItems", + function (newTodo, contentWindow, document) { + var buttons = document.getElementsByClassName("destroy") + for (var i = buttons.length - 1; i > -1; i--) { + buttons[i].click() + } + }) + ] + } + }) +})(this) diff --git a/mithril.js b/mithril.js index dd3ecf5b..8fac9557 100644 --- a/mithril.js +++ b/mithril.js @@ -1,4 +1,4 @@ -void (function (global, factory) { // eslint-disable-line +;(function (global, factory) { // eslint-disable-line "use strict" /* eslint-disable no-undef */ var m = factory(typeof window !== "undefined" ? window : {}) @@ -13,7 +13,9 @@ void (function (global, factory) { // eslint-disable-line })(this, function (window, undefined) { // eslint-disable-line "use strict" - var VERSION = "v0.2.1" + m.version = function () { + return "v0.2.1" + } // Save these two. var type = {}.toString @@ -38,23 +40,19 @@ void (function (global, factory) { // eslint-disable-line function noop() {} function forEach(list, f) { - for (var i = 0; i < list.length && !f(list[i], i++);) { - // empty + for (var i = 0; i < list.length; i++) { + f(list[i], i) } } function forOwn(obj, f) { for (var prop in obj) { if (hasOwn.call(obj, prop)) { - if (f(obj[prop], prop)) break + f(obj[prop], prop) } } } - var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g - var attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ - var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ // eslint-disable-line max-len - // caching commonly used variables var $document, $location, $requestAnimationFrame, $cancelAnimationFrame @@ -76,1118 +74,6 @@ void (function (global, factory) { // eslint-disable-line return window } - m.version = function () { - return VERSION - } - - /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ - - function checkForAttrs(pairs) { - return pairs != null && - isObject(pairs) && - !("tag" in pairs || "view" in pairs || "subtree" in pairs) - } - - function parseSelector(tag, cell) { - var classes = [] - var match - while ((match = parser.exec(tag)) != null) { - if (match[1] === "" && match[2]) { - cell.tag = match[2] - } else if (match[1] === "#") { - cell.attrs.id = match[2] - } else if (match[1] === ".") { - classes.push(match[2]) - } else if (match[3][0] === "[") { - var pair = attrParser.exec(match[3]) - cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true) - } - } - - return classes - } - - function getChildrenFromList(hasAttrs, args) { - var children = hasAttrs ? args.slice(1) : args - if (children.length === 1 && isArray(children[0])) { - return children[0] - } else { - return children - } - } - - function assignAttrs(cell, attrs, classAttr, classes) { - forOwn(attrs, function (value, attr) { - if (attr === classAttr && - attrs[attr] != null && - attrs[attr] !== "") { - classes.push(attrs[attr]) - - // create key in correct iteration order - cell.attrs[attr] = "" - } else { - cell.attrs[attr] = attrs[attr] - } - }) - - if (classes.length) { - cell.attrs[classAttr] = classes.join(" ") - } - } - - /** - * @param {Tag} The DOM node tag - * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs - * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, - * or splat (optional) - */ - function m(tag, pairs) { - for (var args = [], i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i] - } - - if (isObject(tag)) return parameterize(tag, args) - var hasAttrs = checkForAttrs(pairs) - var attrs = hasAttrs ? pairs : {} - var classAttr = "class" in attrs ? "class" : "className" - var cell = {tag: "div", attrs: {}} - - if (!isString(tag)) { - throw new Error("selector in m(selector, attrs, children) should " + - "be a string") - } - - var classes = parseSelector(tag, cell) - cell.children = getChildrenFromList(hasAttrs, args) - - assignAttrs(cell, attrs, classAttr, classes) - - return cell - } - - function forKeys(list, f) { - forEach(list, function (attrs, i) { - attrs = attrs && attrs.attrs - return attrs && attrs.key != null && f(attrs, i) - }) - } - - // This function was causing deopts in Chrome. - function dataToString(data) { - // data.toString() might throw or return null if data is the return - // value of Console.log in some versions of Firefox - try { - if (data != null && data.toString() != null) { - return data - } - } catch (e) { - // Swallow all errors here. - } - - return "" - } - - // This function was causing deopts in Chrome. - function injectTextNode(parent, first, index, data) { - try { - insertNode(parent, first, index) - first.nodeValue = data - } catch (e) { - // IE erroneously throws error when appending an empty text node - // after a null - } - } - - function flatten(list) { - // recursively flatten array - for (var i = 0; i < list.length; i++) { - if (isArray(list[i])) { - list = list.concat.apply([], list) - // check current index again while there is an array at this - // index. - i-- - } - } - - return list - } - - function insertNode(parent, node, index) { - parent.insertBefore(node, parent.childNodes[index] || null) - } - - var DELETION = 1 - var INSERTION = 2 - var MOVE = 3 - - function handleKeysDiffer(data, existing, cached, parent) { - forKeys(data, function (key, i) { - key = key.key - if (existing[key]) { - existing[key] = { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || - $document.createElement("div") - } - } else { - existing[key] = {action: INSERTION, index: i} - } - }) - - var actions = [] - - forOwn(existing, function (value) { - actions.push(value) - }) - - var changes = actions.sort(sortChanges) - var newCached = new Array(cached.length) - newCached.nodes = cached.nodes.slice() - - forEach(changes, function (change) { - var index = change.index - - switch (change.action) { - case DELETION: - clear(cached[index].nodes, cached[index]) - newCached.splice(index, 1) - break - - case INSERTION: - var dummy = $document.createElement("div") - dummy.key = data[index].attrs.key - insertNode(parent, dummy, index) - newCached.splice(index, 0, { - attrs: {key: data[index].attrs.key}, - nodes: [dummy] - }) - newCached.nodes[index] = dummy - break - - case MOVE: - var changeElement = change.element - var maybeChanged = parent.childNodes[index] - if (maybeChanged !== changeElement && changeElement !== null) { - parent.insertBefore(changeElement, maybeChanged || null) - } - newCached[index] = cached[change.from] - newCached.nodes[index] = changeElement - } - }) - - return newCached - } - - function diffKeys(data, cached, existing, parentElement) { - var keysDiffer = data.length !== cached.length - - if (!keysDiffer) { - forKeys(data, function (attrs, i) { - var cachedCell = cached[i] - return keysDiffer = cachedCell && - cachedCell.attrs && - cachedCell.attrs.key !== attrs.key - }) - } - - if (keysDiffer) { - return handleKeysDiffer(data, existing, cached, parentElement) - } else { - return cached - } - } - - // diffs the array itself - function diffArray(data, cached, nodes) { - // update the list of DOM nodes by collecting the nodes from each item - forEach(data, function (_, i) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) - }) - - // remove items from the end of the array if the new array is shorter - // than the old one. if errors ever happen here, the issue is most - // likely a bug in the construction of the `cached` data structure - // somewhere earlier in the program - forEach(cached.nodes, function (node, i) { - if (node.parentNode != null && nodes.indexOf(node) < 0) { - clear([node], [cached[i]]) - } - }) - - if (data.length < cached.length) cached.length = data.length - - cached.nodes = nodes - } - - function buildArrayKeys(data) { - var guid = 0 - forKeys(data, function () { - forEach(data, function (attrs) { - attrs = attrs && attrs.attrs - if (attrs && attrs.key == null) { - attrs.key = "__mithril__" + guid++ - } - }) - return true - }) - } - - // shallow array compare, sorts - function arraySortCompare(a, b) { - a.sort() - b.sort() - var len = a.length - if (len !== b.length) return false - for (var i = 0; i < len; i++) { - if (a[i] !== b[i]) return false - } - return true - } - - function elemIsDifferentEnough(data, cached, dataAttrKeys) { - if (data.tag !== cached.tag) return true - if (!arraySortCompare(dataAttrKeys, Object.keys(cached.attrs))) { - return true - } - - if (data.attrs.id !== cached.attrs.id) return true - if (data.attrs.key !== cached.attrs.key) return true - - if (m.redraw.strategy() === "all") { - return !(cached.configContext && - cached.configContext.retain === true) - } else if (m.redraw.strategy() === "diff") { - return cached.configContext && - cached.configContext.retain === false - } - } - - function maybeRecreateObject(data, cached, dataAttrKeys) { - // if an element is different enough from the one in cache, recreate it - if (elemIsDifferentEnough(data, cached, dataAttrKeys)) { - if (cached.nodes.length) clear(cached.nodes) - if (cached.configContext && - isFunction(cached.configContext.onunload)) { - cached.configContext.onunload() - } - - if (cached.controllers) { - forEach(cached.controllers, function (controller) { - if (controller.unload) { - controller.onunload({preventDefault: noop}) - } - }) - } - } - } - - function getObjectNamespace(data, namespace) { - return data.attrs.xmlns ? data.attrs.xmlns : - data.tag === "svg" ? "http://www.w3.org/2000/svg" : - data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" : - namespace - } - - var pendingRequests = 0 - m.startComputation = function () { pendingRequests++ } - m.endComputation = function () { - if (pendingRequests > 1) { - pendingRequests-- - } else { - pendingRequests = 0 - m.redraw() - } - } - - function unloadCachedControllers(cached, views, controllers) { - if (controllers.length) { - cached.views = views - cached.controllers = controllers - forEach(controllers, function (controller) { - if (controller.onunload && controller.onunload.$old) { - controller.onunload = controller.onunload.$old - } - - if (pendingRequests && controller.onunload) { - var onunload = controller.onunload - controller.onunload = noop - controller.onunload.$old = onunload - } - }) - } - } - - function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { - // schedule configs to be called. They are called after `build` finishes - // running - var config = data.attrs.config - if (isFunction(config)) { - var context = cached.configContext = cached.configContext || {} - - // bind - configs.push(function () { - return config.call(data, node, !isNew, context, cached) - }) - } - } - - function buildUpdatedNode( - cached, - data, - editable, - hasKeys, - namespace, - views, - configs, - controllers - ) { - var node = cached.nodes[0] - if (hasKeys) { - setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) - } - - cached.children = build(node, data.tag, undefined, undefined, - data.children, cached.children, false, 0, - data.attrs.contenteditable ? node : editable, namespace, configs) - - cached.nodes.intact = true - - if (controllers.length) { - cached.views = views - cached.controllers = controllers - } - - return node - } - - function handleNonexistentNodes(data, parent, index) { - var nodes - if (data.$trusted) { - nodes = injectHTML(parent, index, data) - } else { - nodes = [$document.createTextNode(data)] - if (!voidElements.test(parent.nodeName)) { - insertNode(parent, nodes[0], index) - } - } - - var cached - - if (typeof data === "string" || - typeof data === "number" || - typeof data === "boolean") { - cached = new data.constructor(data) - } else { - cached = data - } - - cached.nodes = nodes - - return cached - } - - function reattachNodes(data, - cached, - parentElement, - editable, - index, - parentTag - ) { - var nodes = cached.nodes - if (!editable || editable !== $document.activeElement) { - if (data.$trusted) { - clear(nodes, cached) - nodes = injectHTML(parentElement, index, data) - } else if (parentTag === "textarea") { - //