├── .gitignore
├── .jshintrc
├── LICENSE
├── README.md
├── app
├── app.js
├── routes.js
└── tabs.js
├── client
├── data
│ └── quotes.json
├── index.html
└── styles
│ └── app.css
├── node_modules
├── mithril-isomorphic
│ ├── isomithril.js
│ ├── mock.js
│ ├── package.json
│ └── reviver.js
└── mithril
│ ├── mithril.js
│ └── package.json
├── package.json
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | express
26 | q
27 | q-io
28 | node_modules/mithril-isomorphic/node_modules/*
29 | __.js
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | // Settings
3 | "passfail" : false, // Stop on first error.
4 | "maxerr" : 100, // Maximum error before stopping.
5 |
6 |
7 | // Predefined globals whom JSHint will ignore.
8 | "browser" : true, // Standard browser globals e.g. `window`, `document`.
9 |
10 | "node" : false,
11 | "rhino" : false,
12 | "couch" : false,
13 |
14 | // Development.
15 | "debug" : false, // Allow debugger statements e.g. browser breakpoints.
16 | "devel" : true, // Allow developments statements e.g. `console.log();`.
17 |
18 |
19 | // ECMAScript 5.
20 | "es5" : false, // Allow ECMAScript 5 syntax.
21 | "strict" : false, // Require `use strict` pragma in every file.
22 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict').
23 |
24 |
25 | // The Good Parts.
26 | "asi" : true, // Tolerate Automatic Semicolon Insertion (no semicolons).
27 | "laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
28 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.).
29 | "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
30 | "curly" : false, // Require {} for every new block or scope.
31 | "eqeqeq" : false, // Require triple equals i.e. `===`.
32 | "eqnull" : true, // Tolerate use of `== null`.
33 | "evil" : false, // Tolerate use of `eval`.
34 | "expr" : false, // Tolerate `ExpressionStatement` as Programs.
35 | "forin" : false, // Prohibit `for in` loops without `hasOwnPrototype`.
36 | "immed" : false, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
37 | "latedef" : false, // Prohipit variable use before definition.
38 | "loopfunc" : true, // Allow functions to be defined within loops.
39 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
40 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions.
41 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
42 | "scripturl" : true, // Tolerate script-targeted URLs.
43 | "shadow" : true, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
44 | "supernew" : true, // Tolerate `new function () { ... };` and `new Object;`.
45 | "undef" : false, // Require all non-global variables be declared before they are used.
46 | "-W053": false, // Hide errors about using `new` with primitive type constructors
47 |
48 |
49 | // Personal styling preferences.
50 | "newcap" : false, // Require capitalization of all constructor functions e.g. `new F()`.
51 | "noempty" : false, // Prohibit use of empty blocks.
52 | "nonew" : false, // Prohibit use of constructors for side-effects.
53 | "nomen" : false, // Prohibit use of initial or trailing underbars in names.
54 | "onevar" : false, // Allow only one `var` statement per function.
55 | "plusplus" : false, // Prohibit use of `++` & `--`.
56 | "sub" : true, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
57 | "trailing" : true, // Prohibit trailing whitespaces.
58 | "white" : false, // Check against strict whitespace and indentation rules.
59 | "indent" : 4 // Specify indentation spacing
60 | }
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Daniel Barreiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsolete!!!
2 |
3 | (sorry)
4 |
5 | mithril-isomorphic
6 | ==================
7 |
8 | An attempt at making Mithril work both on the client and the server.
9 |
10 | Installation
11 | ============
12 |
13 | Clone this repository or download the zip file and expand.
14 |
15 | Run `npm install` in the project folder to install the dependencies.
16 |
17 | So far..
18 | ========
19 |
20 | This is a work in progress. Sorry if at any point it gets unstable.
21 |
22 | So far:
23 |
24 | If you do `node server.js` and then browse to `localhost:8000` as the console message suggests
25 | you will be able to get to a sample application.
26 |
27 | The text on the application is all in uppercase letters. This is a clue to its origin.
28 | The contents generated on the server has been turned into uppercase to tell it apart
29 | from that generated on the client.
30 |
31 | As you move across the tabs you will see the contents on the new tab panels show in lowercase letters
32 | while the labels on the tabs themselves remain in uppercase as they are not refreshed.
33 |
34 | If you reload any of those pages, reloading them from the server, an uppercased version will show.
35 |
36 | All active elements, whether in the client or server side sections, are active and should respond as expected.
37 | As a matter of fact, if you switched tabs it was because the action associated with the tabs
38 | was revived. As expected of a single page application such as this, no trips to the server are required
39 | unless you explicitly reload the page.
40 |
41 | `server.js` launches an express server which uses middleware to run the Mithril application in the server.
42 | For the default configuration, you just need to add this to the express server script:
43 |
44 | app.use(require('mithril-isomorphic')());
45 |
46 | When loading the middleware, an object with several configuration options can be given.
47 | You can see the configuration options documented [here](https://github.com/Satyam/mithril-isomorphic/blob/master/node_modules/mithril-isomorphic/isomithril.js#L125)
48 |
49 | The [`routes.js` file](https://github.com/Satyam/mithril-isomorphic/blob/master/app/routes.js)
50 | is slightly modified from the standard arguments to `m.route()`:
51 |
52 | module.exports = function (m) {
53 | m.route('/', {
54 | '/': 'app',
55 | '/:tab': 'app'
56 | });
57 | };
58 |
59 | * It should be exported as any node module.
60 | * The route configuration is missing the first argument, the document root, as there is no document yet.
61 | * The module name is given as a string (`'app'` instead of `app`) as there is no instance of it yet.
62 |
63 | The application can be made of any number of individual files.
64 | A [module](https://github.com/Satyam/mithril-isomorphic/blob/master/app/app.js) would look like this:
65 |
66 | module.exports = function (m, IsoModules) {
67 | var app = {
68 | // The regular application goes here
69 | };
70 | IsoModules.app = app;
71 | };
72 |
73 | The module is defined as normal Mithril one within the exported function which will be called with the instance of Mithril
74 | and an object which will be used to collect all the modules in the application.
75 | A single module can be split into several files or several of them placed in the same file, as long as a
76 | reference to each module, with its `controller` and `view` properties are in the `IsoModules` collection of modules.
77 | The name of the module within the collection is the one used in the `routes` file above.
78 |
79 | The [home page](https://github.com/Satyam/mithril-isomorphic/blob/master/client/index.html)
80 | `index.html` by default, should contain a placeholder `{{body}}` somewhere within the body.
81 | It will be replaced by the static version of the page plus the scripts to make it active, including Mithril itself.
82 |
83 | Only the minimum of Mithril itself has been modified to be able to access internal variables and member
84 | functions inaccessible to external applications. The `m.render()` method is the only one changed.
85 | Other methods have bene monkey-patched.
86 | The version of Mithril tagged *next* was used for this package.
87 |
88 | I have used the [mock DOM](https://github.com/lhorie/mithril.js/blob/master/tests/mock.js)
89 | used for testing with very slight modifications, instead of attempting to use a more
90 | comprehensive emulator such as PhantomJS.
91 |
--------------------------------------------------------------------------------
/app/app.js:
--------------------------------------------------------------------------------
1 | /* jshint node:true */
2 | module.exports = function (m, IsoModules) {
3 |
4 | var app = {
5 | //models
6 |
7 | // I keep a static copy of the list in the app
8 | // so it doesn't get reloaded each time the route changes
9 | // and a new app instance gets initialized
10 | list: null,
11 |
12 | //controller
13 | controller: function () {
14 |
15 | // model initialization, if does not already exists
16 | if (!app.list) {
17 | app.list = m.request({
18 | method: 'GET',
19 | url: 'data/quotes.json'
20 | });
21 | }
22 |
23 | this.title = m.prop("hello");
24 |
25 | // app properties
26 | this.color = m.prop('');
27 | this.bigFonts = m.prop(false);
28 |
29 | // event listeners
30 | this.reset = function () {
31 | this.bigFonts(false);
32 | this.color('');
33 | }.bind(this);
34 |
35 | this.randomizeColor = function () {
36 | this.color("rgb(0, " + (Math.random() * 125 | 0) + ", 0)");
37 | }.bind(this);
38 |
39 | this.tabs = new IsoModules.mc.Tabs.controller({
40 | list: {
41 | view: app.listView,
42 | ctrl: this,
43 | label: 'List'
44 | },
45 | settings: {
46 | label: 'Settings',
47 | view: app.settingsView
48 | },
49 | about: app.aboutView
50 | });
51 | },
52 |
53 |
54 | //view
55 | view: function (ctrl) {
56 | return m(
57 | "div", {
58 | class: "app " + (ctrl.bigFonts() ? "big" : ""),
59 | style: {
60 | backgroundColor: ctrl.color()
61 | }
62 | },
63 | IsoModules.mc.Tabs.view(ctrl.tabs, {
64 | _parent: '.tabs',
65 | _activeAnchor: '.selected'
66 | })
67 |
68 | );
69 | },
70 | listView: function (ctrl) {
71 | return m(
72 | "ul.itemlist",
73 | app.list().map(function (item) {
74 | // I read the properties in both lower and uppercase due to a peculiarity of the demo.
75 | // For this application on its own, the lowercase properties should suffice.
76 | return m("li", (item.quote || item.QUOTE) + ' - ' + (item.author || item.AUTHOR));
77 | })
78 | );
79 | },
80 | settingsView: function (ctrl) {
81 | return m(".settings", [
82 | m("div", [
83 | m("input[type=checkbox]", {
84 | checked: ctrl.bigFonts(),
85 | onclick: m.withAttr('checked', ctrl.bigFonts)
86 | }),
87 | "big fonts"
88 | ]),
89 | m("div", [
90 | m("button", {
91 | onclick: ctrl.randomizeColor
92 | }, "random color")
93 | ]),
94 | m("div", [
95 | m("button", {
96 | onclick: ctrl.reset
97 | }, "reset")
98 | ])
99 | ]);
100 | },
101 | aboutView: function () {
102 | return m(
103 | ".about", [
104 | "This is a sample demo",
105 | m("hr"),
106 | m(
107 | "textarea", {
108 | rows: 10,
109 | cols: 80,
110 | onchange: function () {
111 | app.list(JSON.parse(this.value));
112 | console.log(app.list());
113 | }
114 | },
115 | JSON.stringify(app.list)
116 | ),
117 | m('p', 'If you go to the [list] tab, you will see the changes at once.')
118 | ]
119 | );
120 | },
121 | leoView: function (ctrl) {
122 | return m(
123 | ".leo", [
124 | m("h1", ctrl.title()),
125 | m("input", {
126 | oninput: m.withAttr("value", ctrl.title),
127 | value: ctrl.title()
128 | })
129 | ]
130 |
131 | );
132 | }
133 | };
134 |
135 | /*
136 | Receives a reference to the controller and
137 | an object with the labels to be shown as the keys and
138 | the content as functions returning the result of an m() call.
139 |
140 | I'm using a potentially localizable string as the key to the tabs.
141 | That is not a good idea but for the example it serves
142 | */
143 | var tabs = function (ctrl, options) {
144 |
145 | // read the tab key from the URL or use the first key as default
146 | var tabKey = m.route.param('tab') || Object.keys(options)[0];
147 |
148 | var tab = function (name) {
149 | return m("li", [
150 | m("a", {
151 | class: tabKey == name ? "selected" : "",
152 | href: '/' + name,
153 | // let Mithril take care of the routing
154 | config: m.route
155 | }, name)
156 | ]);
157 | };
158 |
159 | return [
160 | // tabs:
161 | m('.tabs', [
162 | m("ul", Object.keys(options).map(tab))
163 | ]),
164 | // body:
165 | options[tabKey](ctrl)
166 | ];
167 | };
168 |
169 | IsoModules.app = app;
170 | };
--------------------------------------------------------------------------------
/app/routes.js:
--------------------------------------------------------------------------------
1 | module.exports = function (m) {
2 | m.route('/', {
3 | '/': 'app',
4 | '/:tab': 'app'
5 | });
6 | };
7 |
--------------------------------------------------------------------------------
/app/tabs.js:
--------------------------------------------------------------------------------
1 | /*jshint node:true */
2 | /*global m:false */
3 | module.exports = function (m, IsoModules) {
4 | // Select ======================================================================
5 | var mc = mc || {};
6 |
7 | mc.Tabs = {
8 | controller: function (tabs, activeTab) {
9 | this.tabs = tabs || [];
10 | this.activeTab = activeTab || '';
11 | },
12 |
13 | view: function (ctrl, selectors, attrs, overrides) {
14 | selectors = selectors || {};
15 | attrs = attrs || {};
16 | overrides = overrides || {};
17 |
18 | var tabs = normalizeTabs(overrides.tabs || ctrl.tabs, ctrl),
19 | activeTab = overrides.activeTab || ctrl.activeTab ||
20 | m.route.param('tab') || Object.keys(tabs)[0];
21 |
22 | return [
23 | m('div' + (selectors._parent || ''), attrs._parent || {},
24 | m('ul', Object.keys(tabs).map(tab))
25 | ),
26 | tabs[activeTab].view(tabs[activeTab].ctrl)
27 | ];
28 |
29 | function tab(name) {
30 | var selected = activeTab === name,
31 | selector = (selected && selectors._activeAnchor ?
32 | selectors._activeAnchor : selectors._anchor) || '',
33 | attrParam = (selected && attrs._activeAnchor ?
34 | attrs._activeAnchor : attrs._anchor) || '',
35 | attr = {};
36 |
37 | if (attrParam) {
38 | merge(attr, attrParam);
39 | }
40 | merge(attr, {
41 | href: '/' + name,
42 | config: m.route
43 | });
44 |
45 | return m('li' + (selectors._item || ''), attrs._item || {},
46 | m('a' + selector, attr, tabs[name].label || '')
47 | );
48 | }
49 |
50 | function normalizeTabs(tabs, ctrl) {
51 | var norm = {},
52 | lastCtrl = ctrl || {};
53 |
54 | Object.keys(tabs).forEach(function (key) { // depends on .keys() returning keys in order defined
55 | var tab = tabs[key];
56 | if (typeof tab === 'function') {
57 | norm[key] = {
58 | view: tab,
59 | ctrl: lastCtrl,
60 | label: key
61 | };
62 | } else {
63 | if (tab.ctrl) {
64 | lastCtrl = tab.ctrl;
65 | }
66 | norm[key] = {
67 | view: tab.view,
68 | ctrl: lastCtrl,
69 | label: tab.label || key
70 | };
71 | }
72 | });
73 |
74 | return norm;
75 | }
76 |
77 |
78 | function merge(to, from) {
79 | for (var key in from) {
80 | to[key] = from[key];
81 | } // jshint ignore:line
82 | }
83 | }
84 | };
85 |
86 | IsoModules.mc = mc;
87 | };
--------------------------------------------------------------------------------
/client/data/quotes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "author": "Albert Camus",
4 | "quote": "Those who lack the courage will always find a philosophy to justify it."
5 | },
6 | {
7 | "author": "Plato",
8 | "quote": "You can discover more about a person in an hour of play than in a year of conversation."
9 | },
10 | {
11 | "author": "Ludwig Wittgenstein",
12 | "quote": "If people never did silly things nothing intelligent would ever get done."
13 | }
14 | ]
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Isomorphic Mithril Project
7 |
8 |
9 |
10 | {{body}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/styles/app.css:
--------------------------------------------------------------------------------
1 | body{background-color:#466a5c;color:#fff}a,a:visited,a:active{color:#fff;text-decoration:none;cursor:pointer}ul{margin:0;padding:0;list-style:none}.app{margin:1.5em}.app.big{font-size:2em}.sidebar,.itemlist{color:#000;min-height:300px}.tabs{background-color:#222;margin-bottom:1em;overflow:hidden}.tabs li{float:left;margin:0 .25em;padding-top:4px;border-bottom:4px solid #BEAA8E}.tabs a{padding:.5em;display:block}.tabs a.selected{background-color:#BEAA8E}.tabs a:hover{background-color:#BEAA8E}.itemlist{color:#fff}ul.itemlist li{padding:1em;margin-bottom:1em;background-color:#222}
--------------------------------------------------------------------------------
/node_modules/mithril-isomorphic/isomithril.js:
--------------------------------------------------------------------------------
1 | /* jshint node:true */
2 | 'use strict';
3 |
4 | var path = require('path'),
5 | FS = require('q-io/fs'),
6 | Q = require('q'),
7 | Mithril = require('mithril'),
8 | mock = require('./mock.js');
9 |
10 | var page,
11 | routesFname,
12 | app,
13 | routes,
14 | def,
15 | mod,
16 | localRoot,
17 | remoteRoot,
18 | script,
19 | placeholder,
20 | rxApp,
21 | routeParams = {},
22 | isoName,
23 | IsoModules = {},
24 | modes = {
25 | pathname: "",
26 | hash: "#",
27 | search: "?"
28 | },
29 | routeMode;
30 |
31 |
32 | // Attach the mock DOM to Mithril
33 | Mithril.deps(mock.window);
34 |
35 | // Redefine the original `m.route.param`
36 | // to read from my collection of parameters
37 | // collected on the server side.
38 | Mithril.route.param = function (key) {
39 | console.log('param', key, routeParams[key]);
40 | return routeParams[key];
41 | };
42 |
43 | // Monkey patch the original `m.render` method
44 | // to read the HTML from the mock DOM once it is done rendering.
45 |
46 | Mithril.render = (function (original) {
47 |
48 | var logNode = function (node) {
49 | var prop, val,
50 | style, styles = [],
51 | html = '';
52 |
53 | if (!node.nodeName && node.nodeValue !== undefined) {
54 | // For text nodes, I return the uppercase text
55 | // so that you can tell the parts generated at the server
56 | // from the normal lowercase of the actual app when run on the client
57 | return node.nodeValue.toUpperCase();
58 | }
59 | html += '<' + node.nodeName;
60 | for (prop in node) {
61 | val = node[prop];
62 |
63 | // Ignore functions, those will be revived on the client side.
64 | if (typeof val == 'function') continue;
65 | switch (prop) {
66 | case 'nodeName':
67 | case 'parentNode':
68 | case 'childNodes':
69 | case 'pathname':
70 | case 'search':
71 | continue;
72 | case 'checked':
73 | if (val == 'false') continue;
74 | break;
75 | case 'href':
76 | val = node.pathname;
77 | break;
78 | case 'className':
79 | prop = 'class';
80 | break;
81 | case 'style':
82 | if (val) {
83 | for (style in val) {
84 | if (val[style]) {
85 | styles.push(style + ': ' + val[style]);
86 | }
87 | }
88 | if (!styles.length) continue;
89 | val = styles.join(';');
90 | }
91 | break;
92 | }
93 | html += ' ' + prop + '="' + val.replace('"', '\\"') + '"';
94 | }
95 |
96 | if (node.childNodes.length) {
97 | html += '>' + node.childNodes.reduce(function (prev, node) {
98 | return prev + logNode(node);
99 | }, '') + '' + node.nodeName + '>';
100 | } else {
101 | // I don't know why Mithril assigns the content of textareas
102 | // to its value attribute instead of the innerHTML property.
103 | // Since it doesn't have children, the closing tag has to be forced.
104 | if (node.nodeName == 'TEXTAREA') {
105 | html += '>';
106 | } else {
107 | html += '/>';
108 | }
109 | }
110 | return html;
111 | };
112 |
113 |
114 |
115 |
116 | // This is the monkey-patching part:
117 | return function () {
118 |
119 | var result = original.apply(this, arguments);
120 | mock.html = mock.window.document.body.childNodes.reduce(function (prev, node) {
121 | return prev + logNode(node);
122 | }, '');
123 | return result;
124 | };
125 | })(Mithril.render);
126 |
127 |
128 | //var wAttrList = [];
129 | //Mithril.withAttr = (function (original) {
130 | // return function () {
131 | // console.log('withAttr', arguments[0], arguments[1].toString());
132 | // var result = original.apply(this, arguments);
133 | // wAttrList.push({
134 | // attr: arguments[0],
135 | // fn: arguments[1],
136 | // wrap: result
137 | // });
138 | // return result;
139 | // };
140 | //
141 | //})(Mithril.withAttr);
142 |
143 |
144 | module.exports = function (config) {
145 |
146 | config = config || {};
147 | // Folders
148 | // Where the application resides
149 | app = config.app || './app';
150 |
151 | // The root for the static files
152 | localRoot = config.localRoot || 'client';
153 | // The root as seen from the far side
154 | remoteRoot = config.remoteRoot || '/';
155 |
156 | // Files:
157 | // The home page, located within `localRoot`
158 | page = config.page || 'index.html';
159 | // The placeholder for the body within that home page
160 | placeholder = config.placeholder || '{{body}}';
161 | // The file name for the routes configuration, located in `app`
162 | routesFname = config.routes || 'routes.js';
163 | // The name of the script file where all the application code is collected, located in `app`
164 | script = config.script || '__.js';
165 |
166 | // The name of the global object where all the modules are collected
167 | isoName = config.isoName || 'IsoModules';
168 | // The route mode as per m.routes.mode
169 | routeMode = config.routeMode || 'search';
170 |
171 |
172 |
173 | mock.window.location.pathname = localRoot;
174 | rxApp = new RegExp(path.join('/', app, '/') + '(.*)');
175 |
176 |
177 | // Read the routes file for the modified call to
178 | // `m.route` or `m.module`
179 |
180 | var r = require(path.resolve(app, routesFname))({
181 | route: function (d, r) {
182 | def = d;
183 | routes = r;
184 | },
185 | module: function (m) {
186 | mod = m;
187 | }
188 | });
189 |
190 |
191 | // Collect everything that needs to be sent to the client into one script.
192 | var out = path.resolve(app, script);
193 |
194 | // First, send Mithril itself
195 | FS.copy(require.resolve('mithril'), out).then(function () {
196 | // Add the reviver
197 | return FS.read(require.resolve('./reviver.js')).then(function (contents) {
198 | return FS.append(out, contents);
199 | });
200 | }).then(function () {
201 | // Add the declaration of the variable that will hold the collection of modules:
202 | return FS.append(out, ';\n' + isoName + ' = {};\n');
203 | }).then(function () {
204 | // Add all the modules declared
205 | // skipping over the routes file and the file where the scripts are being collected
206 | return FS.listTree(app, function (fullPath) {
207 | return !(
208 | path.extname(fullPath) != '.js' ||
209 | path.basename(fullPath) == script ||
210 | path.basename(fullPath) == routesFname
211 | );
212 | }).then(function (files) {
213 | return Q.all(files.map(function (file) {
214 | var module = require(path.resolve('./', file));
215 | module(Mithril, IsoModules);
216 | return FS.append(out, '\n;(' + module.toString() + ')(Mithril, ' + isoName + ')');
217 | }));
218 | });
219 | }).then(function () {
220 | // Add either the `m.module` or `m.route` call at the end.
221 | if (mod) {
222 | return FS.append(out, '\n;Mithril.module(document.body,' + isoName + '.' + mod + ');');
223 | }
224 | if (routes) {
225 | var rs = [],
226 | r;
227 | for (r in routes) {
228 | rs.push('"' + r + '":' + isoName + '.' + routes[r]);
229 | }
230 | return FS.append(out, '\n;Mithril.route(document.body, "' + def + '", {' + rs.join(',') + '});');
231 | }
232 | });
233 |
234 |
235 | //Entry point to the middleware
236 | return function (req, res, next) {
237 |
238 | console.log('-----------------------');
239 | console.log('req.url:', req.url);
240 |
241 | // Skip over the requests I don't need to handle
242 | var match = rxApp.exec(req.url);
243 | if (match) {
244 | if (match[1] == script) {
245 | console.log('rxmatch == script', match[1]);
246 | res.sendfile(path.join(app, script));
247 | return;
248 | }
249 | console.log('rxmatch else', match[1]);
250 | res.send('(' + require(path.resolve(app, match[1])).toString() + ')(Mithril)');
251 | }
252 |
253 | var accept = req.headers.accept;
254 | if (
255 | req.method !== 'GET' ||
256 | accept.indexOf('application/json') === 0 || !(accept.indexOf('text/html') !== -1 || accept.indexOf('*/*') !== -1)
257 | ) {
258 | console.log('first ignore');
259 | return void next();
260 | }
261 |
262 | if (req.url.indexOf('.') !== -1) {
263 | console.log('second ignore');
264 | return void next();
265 | }
266 |
267 | // This is a copy of the function by the same name
268 | // in Mithril which is inaccessible from outside.
269 | var routeByValue = function (router, path) {
270 | path = path.replace(/^\/\?/, '');
271 | routeParams = {};
272 | var replacer = function () {
273 | var keys = route.match(/:[^\/]+/g);
274 | var values = [].slice.call(arguments, 1, -2);
275 | for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]);
276 | return router[route];
277 | };
278 | for (var route in router) {
279 | if (route == path) return router[route];
280 |
281 | var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "$");
282 |
283 | if (matcher.test(path)) {
284 | return path.replace(matcher, replacer);
285 | }
286 | }
287 | };
288 |
289 | // If the routes file contained a routes declaration
290 | // find the module to be called and extract the params.
291 | if (routes) {
292 | mod = routeByValue(routes, req.url) || routes[def];
293 | console.log('rbv', mod, routeParams);
294 | }
295 | // If the routes file had a single call to `m.module`
296 | // then it would already be stored in `mod`
297 | if (!mod) {
298 | res.send(500, 'Missing routes or single module declaration');
299 | }
300 |
301 | // Call that module, with the body of the fake DOM as its root.
302 | Mithril.module(mock.window.document.body, IsoModules[mod]);
303 |
304 |
305 | // Read the template for the page normally the `index.html` file.
306 | FS.read(path.join(localRoot, page)).then(function (data) {
307 | // Locate the placeholder for the server-side content
308 | var i = data.indexOf(placeholder);
309 | // If the placeholder is not found, return the page as-is
310 | if (i == -1) return void res.send(data);
311 | // avoid 304 - Not Modified
312 | res.setHeader('Last-Modified', (new Date()).toUTCString());
313 |
314 |
315 | // console.log('\n\nvar cellCache = ', JSON.stringify(Mithril.$cellCache[0], function (key, value) {
316 | // if (key == 'nodes') value = value.length + ' [' + value.map(function (node) { return node.nodeName || node.nodeValue;}) + ']';
317 | // return value;
318 | // }));
319 | // var inController = function (value) {
320 | // var m, mod = Mithril.$controller;
321 | // for (m in mod) {
322 | // if (mod[m] == value) {
323 | // return m;
324 | // }
325 | // }
326 | // };
327 | //
328 | // console.log('\n\nvar cell = ', JSON.stringify(Mithril.$cell, function (key, value) {
329 | // var m, obj;
330 | // if (typeof value == 'function') {
331 | //
332 | // for (m in Mithril) {
333 | // if (Mithril[m] == value) {
334 | // value = '* Mithril: ' + m;
335 | // }
336 | // }
337 | //
338 | // wAttrList.forEach(function (entry) {
339 | // if (entry.wrap == value) {
340 | // m = inController(entry.fn);
341 | // value = '* withAttr ' + entry.attr + (m ? ', controller: ' + m : '');
342 | // }
343 | // });
344 | //
345 | // obj = IsoModules[mod];
346 | // for (m in obj) {
347 | // if (obj[m] == value) {
348 | // value = '* IsoModules[' + mod + ']: ' + m;
349 | // }
350 | // }
351 | // m = inController(value);
352 | // if (m) value = '* controller: ' + m;
353 | //
354 | // value = value.toString();
355 | // }
356 | // if (key == 'config' && value == Mithril.route) value = 'm.route()';
357 | // return value;
358 | // }));
359 |
360 | // Send the HTML produced server-side
361 | // inserted where the placeholder should have been.
362 | res.send(
363 | data.substr(0, i) +
364 | // send the HTML
365 | mock.html +
366 | // send code to fix the URL on the client
367 | (
368 | req.url.length > 2 && req.url.substr(0, 2) != '/?' ?
369 | '\n' :
371 | ''
372 | ) +
373 | // Send the script containing Mithril, the app, the reviver and the app.
374 | '\n' +
375 | data.substr(i + placeholder.length)
376 | );
377 |
378 | });
379 | };
380 | };
--------------------------------------------------------------------------------
/node_modules/mithril-isomorphic/mock.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true, lastsemic: true, -W033*/
2 | var fs = require('fs'),
3 | urlParse = require('url').parse,
4 | path = require('path');
5 |
6 |
7 | var mock = {}
8 | mock.used = {};
9 | mock.window = new function() {
10 | var window = {}
11 | window.document = {}
12 | window.document.childNodes = []
13 | window.document.createElement = function(tag) {
14 | return {
15 | style: {},
16 | childNodes: [],
17 | nodeName: tag.toUpperCase(),
18 | appendChild: window.document.appendChild,
19 | removeChild: window.document.removeChild,
20 | replaceChild: window.document.replaceChild,
21 | insertBefore: function(node, reference) {
22 | node.parentNode = this
23 | var referenceIndex = this.childNodes.indexOf(reference)
24 | if (referenceIndex < 0) this.childNodes.push(node)
25 | else {
26 | var index = this.childNodes.indexOf(node)
27 | this.childNodes.splice(referenceIndex, index < 0 ? 0 : 1, node)
28 | }
29 | },
30 | insertAdjacentHTML: function(position, html) {
31 |
32 | //todo: accept markup
33 | if (position == "beforebegin") {
34 | this.parentNode.insertBefore(window.document.createTextNode(html), this)
35 | }
36 | else if (position == "beforeend") {
37 | this.appendChild(window.document.createTextNode(html))
38 | }
39 | },
40 | setAttribute: function(name, value) {
41 | this[name] = value.toString()
42 | if (name == 'href') {
43 | var url = urlParse(value);
44 | this.pathname = url.pathname;
45 | if (url.search) this.search = url.search;
46 | }
47 | },
48 | setAttributeNS: function(namespace, name, value) {
49 | this.namespaceURI = namespace
50 | this[name] = value.toString()
51 | },
52 | getAttribute: function(name, value) {
53 | return this[name]
54 | },
55 | addEventListener: window.document.addEventListener,
56 | removeEventListener: window.document.removeEventListener
57 | }
58 | }
59 | window.document.createElementNS = function(namespace, tag) {
60 | var element = window.document.createElement(tag)
61 | element.namespaceURI = namespace
62 | return element
63 | }
64 | window.document.createTextNode = function(text) {
65 | return {nodeValue: text.toString()}
66 | }
67 | window.document.documentElement = window.document.createElement("html")
68 | window.document.replaceChild = function(newChild, oldChild) {
69 | var index = this.childNodes.indexOf(oldChild)
70 | if (index > -1) this.childNodes.splice(index, 1, newChild)
71 | else this.childNodes.push(newChild)
72 | newChild.parentNode = this
73 | oldChild.parentNode = null
74 | }
75 | window.document.appendChild = function(child) {
76 | var index = this.childNodes.indexOf(child)
77 | if (index > -1) this.childNodes.splice(index, 1)
78 | this.childNodes.push(child)
79 | child.parentNode = this
80 | }
81 | window.document.removeChild = function(child) {
82 | var index = this.childNodes.indexOf(child)
83 | this.childNodes.splice(index, 1)
84 | child.parentNode = null
85 | }
86 | window.document.addEventListener = function () {
87 | };
88 | window.document.removeEventListener = function () {
89 | };
90 | window.performance = {
91 | now: function() {
92 | var hrt = process.hrtime();
93 | return hrt[0] * 1000 + hrt[1] / 1e6;
94 | }
95 | }
96 | window.cancelAnimationFrame = function() {}
97 | window.requestAnimationFrame = function(callback) {
98 | process.nextTick(callback);
99 | }
100 | window.XMLHttpRequest = new function() {
101 | var request = function() {
102 | this.$headers = {}
103 | this.setRequestHeader = function(key, value) {
104 | this.$headers[key] = value
105 | }
106 |
107 | this.open = function(method, url) {
108 | this.method = method
109 | this.url = url
110 | }
111 | this.send = function() {
112 | var xhr = this;
113 | var r = '';
114 | xhr.readyState = 4
115 | xhr.status = 200
116 |
117 | request.$instances.push(this);
118 | fs.createReadStream(path.resolve(window.location.pathname, this.url), {encoding:'utf8'}).on('data', function (chunk) {
119 | r += chunk;
120 | }).on('end', function () {
121 | xhr.responseText = r;
122 | xhr.onreadystatechange();
123 | // xhr.onload({
124 | // type: 'load',
125 | // target: xhr
126 | // });
127 | });
128 | }
129 | }
130 | request.$instances = []
131 | return request
132 | }
133 | window.location = {search: "?/", pathname: "/", hash: ""};
134 | window.history = {};
135 | window.history.pushState = function(data, title, url) {
136 | window.location.pathname = window.location.search = window.location.hash = url
137 | };
138 | window.history.replaceState = function(data, title, url) {
139 | window.location.pathname = window.location.search = window.location.hash = url
140 | };
141 | var _body = window.document.createElement('body');
142 | window.document.appendChild(_body);
143 | window.document.body = _body;
144 |
145 | return window
146 | }
147 | module.exports = mock;
148 |
--------------------------------------------------------------------------------
/node_modules/mithril-isomorphic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-iso-middleware",
3 | "description": "Turning Mithril into an isomorphic framework.",
4 | "repository": "",
5 | "main": "./isomithril.js",
6 | "dependencies": {
7 | "q": "^1.0.1",
8 | "q-io": "^1.11.1"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/node_modules/mithril-isomorphic/reviver.js:
--------------------------------------------------------------------------------
1 | /* jshint browser:true, devel:true */
2 | /* global Mithril */
3 |
4 |
5 |
6 | /* The $reviver function reads the processed view in `cell` that would normally
7 | be used to produce the HTML and matches it with the HTML sent from
8 | the server starting in root.
9 | The combination of the two is stored in `cellCache`.
10 | In order to revive the functions, the controller that this view belongs to
11 | is also needed.
12 | */
13 | Mithril.$reviver = function (cellCache, root, cell, controller) {
14 |
15 | // Skips over pure whitespace nodes.
16 | // I am not sure which browsers support nextElementSibling so I do it in code.
17 | var nextEl = function (node) {
18 | if (node === null) return null;
19 | while (node.nodeName == "#text" && node.nodeValue.trim().length === 0) node = node.nextSibling;
20 | return node;
21 | };
22 |
23 |
24 |
25 | // Revives the function `fn` that was set in the attribute `attr`
26 | // and attaches it to `node`
27 | var reviveFn = function (node, attrName, fn) {
28 |
29 | // Take care to properly redirect calls to m.route
30 | if (attrName == 'config' && fn == Mithril.route) {
31 | node.onclick = Mithril.$routeUnobtrusive;
32 | return;
33 | }
34 |
35 | // All the rest of the functions just need to be wrapped
36 | // by `autoredraw` so as to notfy Mithril that a refresh is in order
37 | node[attrName] = Mithril.$autoRedraw(fn);
38 |
39 |
40 | };
41 |
42 | // Revives a single level of nodes in the hierarchy.
43 | // It is called recursively for each level down.
44 | // The `cell` argument is the processed node as returned by the view.
45 | // `parentNode` is an actual DOM node.
46 | var revive = function (cell, parentNode) {
47 | var i, cache, node = parentNode && nextEl(parentNode.firstChild);
48 |
49 | // If the cell contains nothing but a string,
50 | // return it with a reference to the matching node
51 | if (typeof cell == 'string') {
52 | cell = new ''.constructor(cell);
53 | cell.nodes = [node];
54 | return cell;
55 | }
56 |
57 | // copies the attributes from a single `cell` as
58 | // would have been produced by a call to `m()`
59 | // and returns the corresponding level to be
60 | // stored in `cellCache`
61 |
62 | // It is mostly a matter of associating the actual DOM
63 | // node as produced from the HTML sent from the server
64 | // and reviving the functions associated to events.
65 | var copy = function (cell) {
66 | var prop, value, attrs = {}, attrN, attrV, ret = {};
67 |
68 | if (typeof cell == 'string') {
69 | ret = new ''.constructor(cell);
70 | } else {
71 | for (prop in cell) {
72 | value = cell[prop];
73 | switch (prop) {
74 | case 'children':
75 | // Recursively revive each level
76 | if (value) ret.children = revive(value, node);
77 | break;
78 | case 'attrs':
79 | for (attrN in value) {
80 | attrV = value[attrN];
81 | // If any of the attributes is a function, revive it.
82 | if (typeof attrV == 'function') {
83 | attrV = reviveFn(node, attrN, attrV);
84 | }
85 | attrs[attrN] = attrV;
86 | }
87 | ret.attrs = attrs;
88 | break;
89 | default:
90 | ret[prop] = value;
91 | }
92 | }
93 | }
94 | // Associate the DOM node
95 | ret.nodes = [node];
96 | // move to the next node
97 | node = node && nextEl(node.nextSibling);
98 | // returned the representation of this branch
99 | return ret;
100 | };
101 |
102 | // handle either an array of cells or a single cell
103 | if (Array.isArray(cell)) {
104 | cache = cell.map(copy);
105 | // If it didn't get DOM nodes associated with it
106 | // (which normally would not)
107 | // collect the nodes associated with its children
108 | // which should
109 | if (!cache.nodes) {
110 | cache.nodes = cache.map(function (cell) {
111 | return cell.nodes[0];
112 | });
113 | }
114 | } else {
115 | cache = copy(cell);
116 | }
117 | return cache;
118 | };
119 | var c = revive(cell, root);
120 | cellCache[0] = c;
121 | // console.log('\n\nvar cellCacheR = ', JSON.stringify(c, function (key, value) {
122 | // if (key == 'nodes') value = value.length + ' [' + value.map(function (node) {
123 | // return (node ? node.nodeName || node.nodeValue : 'null*node');
124 | // }) + ']';
125 | // return value;
126 | // }));
127 | };
--------------------------------------------------------------------------------
/node_modules/mithril/mithril.js:
--------------------------------------------------------------------------------
1 | /* jshint -W033 */
2 | // Search for method `m.render` which is the only one changed in this file
3 |
4 |
5 |
6 | // This is based on the version of Mithril of Feb 18th, 2015:
7 | // https://github.com/lhorie/mithril.js/blob/550fe9871aede73bd065da7b08f315e224aeb873/mithril.js
8 |
9 | var Mithril = m = (function app(window, undefined) {
10 | var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function";
11 | var type = {}.toString;
12 | var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
13 | var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
14 |
15 | // caching commonly used variables
16 | var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
17 |
18 | // self invoking function needed because of the way mocks work
19 | function initialize(window){
20 | $document = window.document;
21 | $location = window.location;
22 | $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout;
23 | $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout;
24 | }
25 |
26 | initialize(window);
27 |
28 |
29 | /**
30 | * @typedef {String} Tag
31 | * A string that looks like -> div.classname#id[param=one][param2=two]
32 | * Which describes a DOM node
33 | */
34 |
35 | /**
36 | *
37 | * @param {Tag} The DOM node tag
38 | * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
39 | * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional)
40 | *
41 | */
42 | function m() {
43 | var args = [].slice.call(arguments);
44 | var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]);
45 | var attrs = hasAttrs ? args[1] : {};
46 | var classAttrName = "class" in attrs ? "class" : "className";
47 | var cell = {tag: "div", attrs: {}};
48 | var match, classes = [];
49 | if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string")
50 | while (match = parser.exec(args[0])) {
51 | if (match[1] === "" && match[2]) cell.tag = match[2];
52 | else if (match[1] === "#") cell.attrs.id = match[2];
53 | else if (match[1] === ".") classes.push(match[2]);
54 | else if (match[3][0] === "[") {
55 | var pair = attrParser.exec(match[3]);
56 | cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
57 | }
58 | }
59 | if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
60 |
61 |
62 | var children = hasAttrs ? args.slice(2) : args.slice(1);
63 | if (children.length === 1 && type.call(children[0]) === ARRAY) {
64 | cell.children = children[0]
65 | }
66 | else {
67 | cell.children = children
68 | }
69 |
70 | for (var attrName in attrs) {
71 | if (attrName === classAttrName) {
72 | if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName];
73 | }
74 | else cell.attrs[attrName] = attrs[attrName]
75 | }
76 | return cell
77 | }
78 | function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
79 | //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached`
80 | //the diff algorithm can be summarized as this:
81 | //1 - compare `data` and `cached`
82 | //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is
83 | //3 - recursively apply this algorithm for every array and for the children of every virtual element
84 |
85 | //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions:
86 | //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element
87 | //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")`
88 | //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context)
89 | //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node
90 |
91 | //`parentElement` is a DOM element used for W3C DOM API calls
92 | //`parentTag` is only used for handling a corner case for textarea values
93 | //`parentCache` is used to remove nodes in some multi-node cases
94 | //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable
95 | //`data` and `cached` are, respectively, the new and old nodes being diffed
96 | //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent)
97 | //`editable` is a flag that indicates whether an ancestor is contenteditable
98 | //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor
99 | //`configs` is a list of config functions to run after the topmost `build` call finishes running
100 |
101 | //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings
102 | //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")}
103 | //- it simplifies diffing code
104 | //data.toString() is null if data is the return value of Console.log in Firefox
105 | if (data == null || data.toString() == null) data = "";
106 | if (data.subtree === "retain") return cached;
107 | var cachedType = type.call(cached), dataType = type.call(data);
108 | if (cached == null || cachedType !== dataType) {
109 | if (cached != null) {
110 | if (parentCache && parentCache.nodes) {
111 | var offset = index - parentIndex;
112 | var end = offset + (dataType === ARRAY ? data : cached.nodes).length;
113 | clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end))
114 | }
115 | else if (cached.nodes) clear(cached.nodes, cached)
116 | }
117 | cached = new data.constructor;
118 | if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277)
119 | cached.nodes = []
120 | }
121 |
122 | if (dataType === ARRAY) {
123 | //recursively flatten array
124 | for (var i = 0, len = data.length; i < len; i++) {
125 | if (type.call(data[i]) === ARRAY) {
126 | data = data.concat.apply([], data);
127 | i-- //check current index again and flatten until there are no more nested arrays at that index
128 | len = data.length
129 | }
130 | }
131 |
132 | var nodes = [], intact = cached.length === data.length, subArrayCount = 0;
133 |
134 | //keys algorithm: sort elements without recreating them if keys are present
135 | //1) create a map of all existing keys, and mark all for deletion
136 | //2) add new keys to map and mark them for addition
137 | //3) if key exists in new list, change action from deletion to a move
138 | //4) for each key, handle its corresponding action as marked in previous steps
139 | //5) copy unkeyed items into their respective gaps
140 | var DELETION = 1, INSERTION = 2 , MOVE = 3;
141 | var existing = {}, unkeyed = [], shouldMaintainIdentities = false;
142 | for (var i = 0; i < cached.length; i++) {
143 | if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
144 | shouldMaintainIdentities = true;
145 | existing[cached[i].attrs.key] = {action: DELETION, index: i}
146 | }
147 | }
148 | if (shouldMaintainIdentities) {
149 | if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
150 |
151 | var keysDiffer = false
152 | if (data.length != cached.length) keysDiffer = true
153 | else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
154 | if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) {
155 | keysDiffer = true
156 | break
157 | }
158 | }
159 |
160 | if (keysDiffer) {
161 | for (var i = 0, len = data.length; i < len; i++) {
162 | if (data[i] && data[i].attrs) {
163 | if (data[i].attrs.key != null) {
164 | var key = data[i].attrs.key;
165 | if (!existing[key]) existing[key] = {action: INSERTION, index: i};
166 | else existing[key] = {
167 | action: MOVE,
168 | index: i,
169 | from: existing[key].index,
170 | element: cached.nodes[existing[key].index] || $document.createElement("div")
171 | }
172 | }
173 | else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
174 | }
175 | }
176 | var actions = []
177 | for (var prop in existing) actions.push(existing[prop])
178 | var changes = actions.sort(sortChanges);
179 | var newCached = new Array(cached.length)
180 |
181 | for (var i = 0, change; change = changes[i]; i++) {
182 | if (change.action === DELETION) {
183 | clear(cached[change.index].nodes, cached[change.index]);
184 | newCached.splice(change.index, 1)
185 | }
186 | if (change.action === INSERTION) {
187 | var dummy = $document.createElement("div");
188 | dummy.key = data[change.index].attrs.key;
189 | parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
190 | newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
191 | }
192 |
193 | if (change.action === MOVE) {
194 | if (parentElement.childNodes[change.index] !== change.element && change.element !== null) {
195 | parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
196 | }
197 | newCached[change.index] = cached[change.from]
198 | }
199 | }
200 | for (var i = 0, len = unkeyed.length; i < len; i++) {
201 | var change = unkeyed[i];
202 | parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null);
203 | newCached[change.index] = cached[change.index]
204 | }
205 | cached = newCached;
206 | cached.nodes = new Array(parentElement.childNodes.length);
207 | for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
208 | }
209 | }
210 | //end key algorithm
211 |
212 | for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) {
213 | //diff each item in the array
214 | var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs);
215 | if (item === undefined) continue;
216 | if (!item.nodes.intact) intact = false;
217 | if (item.$trusted) {
218 | //fix offset of next element if item was a trusted string w/ more than one html element
219 | //the first clause in the regexp matches elements
220 | //the second clause (after the pipe) matches text nodes
221 | subArrayCount += (item.match(/<[^\/]|\>\s*[^<]|&/g) || []).length
222 | }
223 | else subArrayCount += type.call(item) === ARRAY ? item.length : 1;
224 | cached[cacheCount++] = item
225 | }
226 | if (!intact) {
227 | //diff the array itself
228 |
229 | //update the list of DOM nodes by collecting the nodes from each item
230 | for (var i = 0, len = data.length; i < len; i++) {
231 | if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes)
232 | }
233 | //remove items from the end of the array if the new array is shorter than the old one
234 | //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
235 | for (var i = 0, node; node = cached.nodes[i]; i++) {
236 | if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]])
237 | }
238 | if (data.length < cached.length) cached.length = data.length;
239 | cached.nodes = nodes
240 | }
241 | }
242 | else if (data != null && dataType === OBJECT) {
243 | if (!data.attrs) data.attrs = {};
244 | if (!cached.attrs) cached.attrs = {};
245 |
246 | var dataAttrKeys = Object.keys(data.attrs)
247 | var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
248 | //if an element is different enough from the one in cache, recreate it
249 | if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) {
250 | if (cached.nodes.length) clear(cached.nodes);
251 | if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload()
252 | }
253 | if (type.call(data.tag) != STRING) return;
254 |
255 | var node, isNew = cached.nodes.length === 0;
256 | if (data.attrs.xmlns) namespace = data.attrs.xmlns;
257 | else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg";
258 | else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML";
259 | if (isNew) {
260 | if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is);
261 | else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag);
262 | cached = {
263 | tag: data.tag,
264 | //set attributes first, then create children
265 | attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs,
266 | children: data.children != null && data.children.length > 0 ?
267 | build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) :
268 | data.children,
269 | nodes: [node]
270 | };
271 | if (cached.children && !cached.children.nodes) cached.children.nodes = [];
272 | //edge case: setting value on