From 998704a7224058e9bede25d2e31b6174790cf2f0 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Wed, 16 Dec 2015 10:53:27 -0500 Subject: [PATCH] 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 | 2668 ++++++++--------- 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, 1325 insertions(+), 3206 deletions(-) delete mode 100644 bench/README.md delete mode 100644 bench/app/.gitignore delete mode 100644 bench/app/index.html delete mode 100644 bench/app/js/app.js delete mode 100644 bench/app/js/controllers/todo.js delete mode 100644 bench/app/js/models/storage.js delete mode 100644 bench/app/js/models/todo.js delete mode 100644 bench/app/js/views/footer-view.js delete mode 100644 bench/app/js/views/main-view.js delete mode 100644 bench/app/node_modules/todomvc-app-css/index.css delete mode 100644 bench/app/node_modules/todomvc-common/base.css delete mode 100644 bench/app/node_modules/todomvc-common/base.js delete mode 100644 bench/app/package.json delete mode 100644 bench/app/readme.md delete mode 100644 bench/index.html delete mode 100644 bench/resources/benchmark-runner.js delete mode 100644 bench/resources/manager.js delete mode 100644 bench/resources/tests.js diff --git a/.eslintignore b/.eslintignore index 0b24c5c3..a6ff032f 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 3f3a9e04..aeab5a02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -!bench/** archive diff --git a/bench/README.md b/bench/README.md deleted file mode 100644 index 16fcc8b9..00000000 --- a/bench/README.md +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 11c90f9c..00000000 --- a/bench/app/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index e42cfacf..00000000 --- a/bench/app/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Mithril • TodoMVC - - - - -
- - - - - - - - - - - diff --git a/bench/app/js/app.js b/bench/app/js/app.js deleted file mode 100644 index d4a8e6fb..00000000 --- a/bench/app/js/app.js +++ /dev/null @@ -1,13 +0,0 @@ -/* 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 deleted file mode 100644 index 27577689..00000000 --- a/bench/app/js/controllers/todo.js +++ /dev/null @@ -1,111 +0,0 @@ -/* 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 deleted file mode 100644 index 0e867e9a..00000000 --- a/bench/app/js/models/storage.js +++ /dev/null @@ -1,14 +0,0 @@ -(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 deleted file mode 100644 index 77af9ba2..00000000 --- a/bench/app/js/models/todo.js +++ /dev/null @@ -1,12 +0,0 @@ -/* 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 deleted file mode 100644 index 25f07f1c..00000000 --- a/bench/app/js/views/footer-view.js +++ /dev/null @@ -1,40 +0,0 @@ -/* 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 deleted file mode 100644 index 9dfd2667..00000000 --- a/bench/app/js/views/main-view.js +++ /dev/null @@ -1,119 +0,0 @@ -/* 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 deleted file mode 100644 index 54d89abd..00000000 --- a/bench/app/node_modules/todomvc-app-css/index.css +++ /dev/null @@ -1,378 +0,0 @@ -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 deleted file mode 100644 index da65968a..00000000 --- a/bench/app/node_modules/todomvc-common/base.css +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 3c6723f3..00000000 --- a/bench/app/node_modules/todomvc-common/base.js +++ /dev/null @@ -1,249 +0,0 @@ -/* 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 deleted file mode 100644 index 3b1e70fd..00000000 --- a/bench/app/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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 deleted file mode 100644 index 76a71d43..00000000 --- a/bench/app/readme.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index 6f399b3d..00000000 --- a/bench/index.html +++ /dev/null @@ -1,81 +0,0 @@ - -TodoMVC Benchmark - - - - - - -
-
-
-
- diff --git a/bench/resources/benchmark-runner.js b/bench/resources/benchmark-runner.js deleted file mode 100644 index e3d2e908..00000000 --- a/bench/resources/benchmark-runner.js +++ /dev/null @@ -1,268 +0,0 @@ -(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 deleted file mode 100644 index 3efa55e3..00000000 --- a/bench/resources/manager.js +++ /dev/null @@ -1,239 +0,0 @@ -/* 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 deleted file mode 100644 index 872fcfa4..00000000 --- a/bench/resources/tests.js +++ /dev/null @@ -1,51 +0,0 @@ -(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 8fac9557..dd3ecf5b 100644 --- a/mithril.js +++ b/mithril.js @@ -1,4 +1,4 @@ -;(function (global, factory) { // eslint-disable-line +void (function (global, factory) { // eslint-disable-line "use strict" /* eslint-disable no-undef */ var m = factory(typeof window !== "undefined" ? window : {}) @@ -13,9 +13,7 @@ })(this, function (window, undefined) { // eslint-disable-line "use strict" - m.version = function () { - return "v0.2.1" - } + var VERSION = "v0.2.1" // Save these two. var type = {}.toString @@ -40,19 +38,23 @@ function noop() {} function forEach(list, f) { - for (var i = 0; i < list.length; i++) { - f(list[i], i) + for (var i = 0; i < list.length && !f(list[i], i++);) { + // empty } } function forOwn(obj, f) { for (var prop in obj) { if (hasOwn.call(obj, prop)) { - f(obj[prop], prop) + if (f(obj[prop], prop)) break } } } + 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 @@ -74,6 +76,1118 @@ 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") { + //