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") {
- //