├── test
├── sample
│ ├── child
│ │ ├── child.html
│ │ └── child.js
│ ├── window
│ │ ├── plugins
│ │ │ └── plugin.js
│ │ └── global.js
│ ├── sample.html
│ ├── .gitignore
│ ├── sample.js
│ ├── components
│ │ └── guille-ms.js
│ │ │ ├── component.json
│ │ │ └── index.js
│ └── wrapper.js
├── test.util.js
└── test.elem.js
├── examples
├── file-explorer
│ ├── file
│ │ ├── file.jade
│ │ └── file.js
│ ├── _build
│ │ ├── file.html
│ │ ├── directory.html
│ │ ├── file
│ │ │ ├── file.html
│ │ │ ├── file.jade
│ │ │ └── file.js
│ │ ├── folder
│ │ │ └── dir.html
│ │ ├── preview
│ │ │ └── preview.js
│ │ ├── file.js
│ │ ├── body.html
│ │ ├── components
│ │ │ ├── component-jquery
│ │ │ │ └── component.json
│ │ │ ├── visionmedia-jade
│ │ │ │ ├── component.json
│ │ │ │ └── lib
│ │ │ │ │ └── runtime.js
│ │ │ └── visionmedia-page.js
│ │ │ │ ├── component.json
│ │ │ │ └── index.js
│ │ └── index.json
│ ├── folder
│ │ └── dir.html.jade
│ ├── preview
│ │ └── preview.js
│ ├── body.html.jade
│ └── components
│ │ ├── component-jquery
│ │ └── component.json
│ │ ├── visionmedia-page.js
│ │ ├── component.json
│ │ └── index.js
│ │ └── visionmedia-jade
│ │ ├── component.json
│ │ └── lib
│ │ └── runtime.js
└── helloworld
│ ├── _elem_last_build.json
│ ├── hello
│ ├── hello.txt
│ └── hello.js
│ ├── _build
│ ├── hello
│ │ ├── hello.txt
│ │ ├── assets.json
│ │ └── hello.js
│ ├── body
│ │ └── body.html
│ ├── index.html
│ ├── index.json
│ ├── components
│ │ └── visionmedia-page.js
│ │ │ ├── component.json
│ │ │ └── index.js
│ └── loader.js
│ ├── body
│ └── body.html
│ └── components
│ └── visionmedia-page.js
│ ├── component.json
│ └── index.js
├── .gitignore
├── index.js
├── boot
├── boot.html
└── loader.js
├── bin
├── .DS_Store
└── elem
├── .github
└── ISSUE_TEMPLATE.md
├── lib
├── util.js
├── converters.js
└── elem.js
├── package.json
└── README.md
/test/sample/child/child.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/file/file.jade:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/sample/window/plugins/plugin.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/file.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/folder/dir.html.jade:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/preview/preview.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/helloworld/_elem_last_build.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/test/sample/sample.html:
--------------------------------------------------------------------------------
1 |
Sample
2 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/directory.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/file/file.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/file/file.jade:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/folder/dir.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/preview/preview.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.swp
4 |
--------------------------------------------------------------------------------
/examples/helloworld/hello/hello.txt:
--------------------------------------------------------------------------------
1 | Hello World!
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/elem');
2 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/hello/hello.txt:
--------------------------------------------------------------------------------
1 | Hello World!
2 |
--------------------------------------------------------------------------------
/test/sample/.gitignore:
--------------------------------------------------------------------------------
1 | _build
2 | _elem_last_build.json
3 |
--------------------------------------------------------------------------------
/boot/boot.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/bin/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/em/elem/master/bin/.DS_Store
--------------------------------------------------------------------------------
/examples/file-explorer/file/file.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 | }
3 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/file.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function() {
3 | }
4 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/file/file.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 | }
3 |
--------------------------------------------------------------------------------
/examples/helloworld/body/body.html:
--------------------------------------------------------------------------------
1 | Click me
2 |
3 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/body/body.html:
--------------------------------------------------------------------------------
1 | Click me
2 |
3 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/sample/sample.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 | this.hello = 'hello';
3 | }
4 |
--------------------------------------------------------------------------------
/test/sample/child/child.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function() {
3 | this.hello = 'hello';
4 | }
5 |
--------------------------------------------------------------------------------
/test/sample/window/global.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a script that runs outside of the module
3 | * context before anything else.
4 | */
5 |
6 | window.hello = 'hello';
7 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/index.json:
--------------------------------------------------------------------------------
1 | {"files":["body/body.html","hello/hello.js","components/visionmedia-page.js/index.js"],"modules":{"page":"components/visionmedia-page.js/index.js"},"packages":{}}
--------------------------------------------------------------------------------
/examples/file-explorer/body.html.jade:
--------------------------------------------------------------------------------
1 | h1 Yo, I heard you like demos
2 | h2 So I made this demo that lets you look at the source for the demos so you can read the demo using the demo
3 |
4 | directory(src="/")
5 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/body.html:
--------------------------------------------------------------------------------
1 | Yo, I heard you like demos
So I made this demo that lets you look at the source for the demos so you can read the demo using the demo
--------------------------------------------------------------------------------
/examples/helloworld/_build/hello/assets.json:
--------------------------------------------------------------------------------
1 | {"hello/hello.js":"var page = require('page');\n\nmodule.exports = function(files, render) {\n page('/hello', function() {\n render(files.hello.txt);\n });\n\n page.start();\n}\n\n"}
--------------------------------------------------------------------------------
/examples/helloworld/hello/hello.js:
--------------------------------------------------------------------------------
1 | var page = require('page');
2 |
3 | module.exports = function(files, render) {
4 | page('/hello', function() {
5 | render(files.hello.txt);
6 | });
7 |
8 | page.start();
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/hello/hello.js:
--------------------------------------------------------------------------------
1 | var page = require('page');
2 |
3 | module.exports = function(files, render) {
4 | page('/hello', function() {
5 | render(files.hello.txt);
6 | });
7 |
8 | page.start();
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/examples/file-explorer/components/component-jquery/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery",
3 | "repo": "component/jquery",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "scripts": [
7 | "index.js"
8 | ],
9 | "dependencies": {}
10 | }
--------------------------------------------------------------------------------
/examples/file-explorer/_build/components/component-jquery/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery",
3 | "repo": "component/jquery",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "scripts": [
7 | "index.js"
8 | ],
9 | "dependencies": {}
10 | }
--------------------------------------------------------------------------------
/test/sample/components/guille-ms.js/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ms",
3 | "repo": "guille/ms.js",
4 | "version": "0.6.1",
5 | "description": "ms parsing / formatting",
6 | "keywords": [
7 | "ms",
8 | "parse",
9 | "format"
10 | ],
11 | "scripts": [
12 | "index.js"
13 | ],
14 | "license": "MIT"
15 | }
--------------------------------------------------------------------------------
/test/sample/wrapper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is an elem that wraps its innerHTML
3 | * in a .
4 | */
5 |
6 | module.exports = function(render) {
7 | var contents = this.innerHTML;
8 |
9 | render(''+contents+'');
10 |
11 | // Count number of calls
12 | if (window.count !== undefined) {
13 | window.count++;
14 | }
15 |
16 | this.dispatchEvent(new Event('hello'))
17 | }
18 |
--------------------------------------------------------------------------------
/examples/file-explorer/components/visionmedia-page.js/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "page",
3 | "repo": "visionmedia/page.js",
4 | "version": "1.3.7",
5 | "description": "Tiny client-side router (~1200 bytes)",
6 | "keywords": [
7 | "page",
8 | "route",
9 | "router",
10 | "routes",
11 | "pushState"
12 | ],
13 | "scripts": [
14 | "index.js"
15 | ],
16 | "license": "MIT"
17 | }
--------------------------------------------------------------------------------
/examples/helloworld/components/visionmedia-page.js/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "page",
3 | "repo": "visionmedia/page.js",
4 | "version": "1.3.7",
5 | "description": "Tiny client-side router (~1200 bytes)",
6 | "keywords": [
7 | "page",
8 | "route",
9 | "router",
10 | "routes",
11 | "pushState"
12 | ],
13 | "scripts": [
14 | "index.js"
15 | ],
16 | "license": "MIT"
17 | }
--------------------------------------------------------------------------------
/examples/file-explorer/components/visionmedia-jade/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jade",
3 | "repo": "visionmedia/jade",
4 | "description": "Jade template runtime",
5 | "version": "1.3.1",
6 | "keywords": [
7 | "template"
8 | ],
9 | "dependencies": {},
10 | "development": {},
11 | "license": "MIT",
12 | "scripts": [
13 | "lib/runtime.js"
14 | ],
15 | "main": "lib/runtime.js"
16 | }
17 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/components/visionmedia-page.js/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "page",
3 | "repo": "visionmedia/page.js",
4 | "version": "1.3.7",
5 | "description": "Tiny client-side router (~1200 bytes)",
6 | "keywords": [
7 | "page",
8 | "route",
9 | "router",
10 | "routes",
11 | "pushState"
12 | ],
13 | "scripts": [
14 | "index.js"
15 | ],
16 | "license": "MIT"
17 | }
--------------------------------------------------------------------------------
/examples/file-explorer/_build/components/visionmedia-jade/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jade",
3 | "repo": "visionmedia/jade",
4 | "description": "Jade template runtime",
5 | "version": "1.3.1",
6 | "keywords": [
7 | "template"
8 | ],
9 | "dependencies": {},
10 | "development": {},
11 | "license": "MIT",
12 | "scripts": [
13 | "lib/runtime.js"
14 | ],
15 | "main": "lib/runtime.js"
16 | }
17 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/components/visionmedia-page.js/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "page",
3 | "repo": "visionmedia/page.js",
4 | "version": "1.3.7",
5 | "description": "Tiny client-side router (~1200 bytes)",
6 | "keywords": [
7 | "page",
8 | "route",
9 | "router",
10 | "routes",
11 | "pushState"
12 | ],
13 | "scripts": [
14 | "index.js"
15 | ],
16 | "license": "MIT"
17 | }
--------------------------------------------------------------------------------
/examples/file-explorer/_build/index.json:
--------------------------------------------------------------------------------
1 | {"files":["_build/body.html","_build/file/file.js","_build/file/file.jade","_build/folder/dir.html","_build/preview/preview.js","_build/components/component-jquery/index.js","_build/components/visionmedia-page.js/index.js","_build/components/visionmedia-jade/lib/runtime.js"],"modules":{"jquery":"_build/components/component-jquery/index.js","jade":"_build/components/visionmedia-jade/lib/runtime.js","page":"_build/components/visionmedia-page.js/index.js"}}
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### Summary
2 | ...
3 |
4 | [Bug Template]
5 | #### Steps To Reproduce
6 | +
7 | +
8 | +
9 |
10 | #### Expected Results
11 | ...
12 |
13 | #### Actual Results
14 | ...
15 | [/Bug Template]
16 |
17 | [Feature/Enhancement]
18 | #### User Story
19 | As a `type_of_user`, I want `to_perform_some_task` so that I can `achieve_some_goal_benefit_or_value`.
20 |
21 | #### Behaviour of the UI
22 | ...
23 |
24 | #### Design Reference / Mockup / Sketches (optional)
25 | ...
26 |
27 | #### Error Handling
28 | ...
29 | [/Feature/Enhancement]
30 |
31 |
32 |
33 | #### Notes (optional)
34 | ...
35 |
36 | #### References (optional)
37 | - [link](link)
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var util = {};
4 | module.exports = util;
5 |
6 |
7 | /**
8 | * Returns <= 2 extensions from the
9 | * end of a filename.
10 | *
11 | * Examples:
12 | *
13 | * hello.txt.html.js => .html.js
14 | * hello.txt.js => .txt.js
15 | * hello.js => .js
16 | * hello => undefined
17 | *
18 | * @param {String} filename
19 | * @returns {String} The extensions
20 | */
21 |
22 | util.last2ext = function(filename) {
23 | var basename = path.basename(filename);
24 | var exts = basename.split('.').slice(1);
25 | if(!exts.length) return;
26 | return '.'+exts.slice(-2).join('.');
27 | }
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/test/test.util.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var util = require('../lib/util');
3 |
4 | describe('util', function() {
5 | describe('#last2ext', function() {
6 | it('returns undefined with none', function() {
7 | var ext = util.last2ext('hello');
8 | assert.equal(ext, undefined);
9 | });
10 | it('works with 1', function() {
11 | var ext = util.last2ext('hello.css');
12 | assert.equal(ext, '.css');
13 | });
14 |
15 | it('works with 2', function() {
16 | var ext = util.last2ext('hello.css.styl');
17 | assert.equal(ext, '.css.styl');
18 | });
19 |
20 | it('works with > 2', function() {
21 | var ext = util.last2ext('hello.js.css.styl');
22 | assert.equal(ext, '.css.styl');
23 | });
24 |
25 |
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/lib/converters.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '.html.jade': function(source,file) {
3 | var jade = require('jade');
4 | return jade.render(''+source, {filename: file});
5 | }
6 | , '.js.jade': function(source, file, locals) {
7 | var jade = require('jade');
8 |
9 | try {
10 | var js = ''+jade.compileClient(source);
11 | }
12 | catch(e) {
13 | console.error("error rendering " + file);
14 | console.error(e.message);
15 | var js = 'function(){console.error("'+e.message+'");}';
16 | }
17 |
18 | return '\
19 | var jade = require("jade"); \n\
20 | module.exports='+js+';';
21 | }
22 | , '.css.styl': function(source, file) {
23 | var stylus = require('stylus');
24 | var nib = require('nib');
25 |
26 | return stylus(''+source)
27 | .set('filename', file)
28 | .set('compress', true)
29 | .use(nib()).render();
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elem",
3 | "version": "0.0.60",
4 | "description": "An asset manager based on custom elements",
5 | "keywords": [
6 | "component",
7 | "asset",
8 | "manager",
9 | "packager"
10 | ],
11 | "repository": {
12 | "type": "git",
13 | "url": "git://github.com/em/elem.git"
14 | },
15 | "bin": {
16 | "elem": "./bin/elem"
17 | },
18 | "scripts": {
19 | "test": "node_modules/.bin/mocha"
20 | },
21 | "author": "Emery Denuccio",
22 | "license": "MIT",
23 | "dependencies": {
24 | "async": "~2.0.1",
25 | "chai": "^3.5.0",
26 | "commander": "^2.8.1",
27 | "connect": "~3.4.1",
28 | "express": "^4.4.5",
29 | "glob": "~7.0.5",
30 | "jade": "~1.11.0",
31 | "jquery": "^3.1.0",
32 | "jsdom": "~9.*",
33 | "mkdirp": "~0.5.0",
34 | "nib": "^1.0.3",
35 | "rimraf": "~2.5.4",
36 | "serve-static": "^1.11.1",
37 | "stylus": "~0.54.5",
38 | "uglify-js": "~2.7.3",
39 | "uid": "0.0.2"
40 | },
41 | "devDependencies": {
42 | "mocha": "~3.1.2",
43 | "expect": "~1.20.2",
44 | "assert": "~1.4.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/bin/elem:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var fs = require('fs')
4 | , program = require('commander')
5 | , path = require('path')
6 | , mkdirp = require('mkdirp')
7 | , rmdir = require('rimraf')
8 | , elem = require('../')
9 | , basename = path.basename
10 | , dirname = path.dirname
11 | , resolve = path.resolve
12 | , exists = fs.existsSync || path.existsSync;
13 |
14 | program
15 | .version(require('../package.json').version)
16 | .usage('[options] ')
17 |
18 | // program
19 | // .command('build')
20 | // .description('build')
21 | // .action(build);
22 |
23 |
24 | program
25 | .command('run')
26 | .usage('[options] ')
27 | .option('-p, --port ', 'port (default: 8000)', Number, process.env.PORT || 8000)
28 | .description('simple server for a root element')
29 | .action(serve);
30 |
31 | program
32 | .command('build')
33 | .usage('[options] ')
34 | .option('-d, --dev', 'development mode')
35 | .description('build static site')
36 | .action(build);
37 |
38 | function build() {
39 | var opts, dir = '.';
40 |
41 | if(arguments.length === 1) {
42 | opts = arguments[0];
43 | }
44 |
45 | if(arguments.length === 2) {
46 | dir = arguments[0];
47 | opts = arguments[1];
48 | }
49 |
50 | root = path.resolve(dir);
51 | console.log('base:',root);
52 |
53 | var frontend = elem(root, !opts.dev);
54 |
55 | frontend.build();
56 | }
57 |
58 | function serve(dir, opts) {
59 | if(arguments.length == 1) {
60 | opts = dir;
61 | dir = './';
62 | }
63 |
64 |
65 | root = path.resolve(dir);
66 | console.log('base:',root);
67 |
68 | var express = require('express');
69 | var app = express();
70 |
71 | var frontend = elem(root);
72 | // frontend.build();
73 |
74 | app.use('/', frontend.loader());
75 | app.get('*', frontend.boot('/'));
76 |
77 | app.listen(opts.port);
78 | console.log('listening on ' + opts.port);
79 | }
80 |
81 | program.parse(process.argv);
82 |
--------------------------------------------------------------------------------
/test/sample/components/guille-ms.js/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpers.
3 | */
4 |
5 | var s = 1000;
6 | var m = s * 60;
7 | var h = m * 60;
8 | var d = h * 24;
9 | var y = d * 365.25;
10 |
11 | /**
12 | * Parse or format the given `val`.
13 | *
14 | * Options:
15 | *
16 | * - `long` verbose formatting [false]
17 | *
18 | * @param {String|Number} val
19 | * @param {Object} options
20 | * @return {String|Number}
21 | * @api public
22 | */
23 |
24 | module.exports = function(val, options){
25 | options = options || {};
26 | if ('string' == typeof val) return parse(val);
27 | return options.long
28 | ? long(val)
29 | : short(val);
30 | };
31 |
32 | /**
33 | * Parse the given `str` and return milliseconds.
34 | *
35 | * @param {String} str
36 | * @return {Number}
37 | * @api private
38 | */
39 |
40 | function parse(str) {
41 | var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
42 | if (!match) return;
43 | var n = parseFloat(match[1]);
44 | var type = (match[2] || 'ms').toLowerCase();
45 | switch (type) {
46 | case 'years':
47 | case 'year':
48 | case 'y':
49 | return n * y;
50 | case 'days':
51 | case 'day':
52 | case 'd':
53 | return n * d;
54 | case 'hours':
55 | case 'hour':
56 | case 'h':
57 | return n * h;
58 | case 'minutes':
59 | case 'minute':
60 | case 'm':
61 | return n * m;
62 | case 'seconds':
63 | case 'second':
64 | case 's':
65 | return n * s;
66 | case 'ms':
67 | return n;
68 | }
69 | }
70 |
71 | /**
72 | * Short format for `ms`.
73 | *
74 | * @param {Number} ms
75 | * @return {String}
76 | * @api private
77 | */
78 |
79 | function short(ms) {
80 | if (ms >= d) return Math.round(ms / d) + 'd';
81 | if (ms >= h) return Math.round(ms / h) + 'h';
82 | if (ms >= m) return Math.round(ms / m) + 'm';
83 | if (ms >= s) return Math.round(ms / s) + 's';
84 | return ms + 'ms';
85 | }
86 |
87 | /**
88 | * Long format for `ms`.
89 | *
90 | * @param {Number} ms
91 | * @return {String}
92 | * @api private
93 | */
94 |
95 | function long(ms) {
96 | return plural(ms, d, 'day')
97 | || plural(ms, h, 'hour')
98 | || plural(ms, m, 'minute')
99 | || plural(ms, s, 'second')
100 | || ms + ' ms';
101 | }
102 |
103 | /**
104 | * Pluralization helper.
105 | */
106 |
107 | function plural(ms, n, name) {
108 | if (ms < n) return;
109 | if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
110 | return Math.ceil(ms / n) + ' ' + name + 's';
111 | }
112 |
--------------------------------------------------------------------------------
/examples/file-explorer/components/visionmedia-jade/lib/runtime.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Merge two attribute objects giving precedence
5 | * to values in object `b`. Classes are special-cased
6 | * allowing for arrays and merging/joining appropriately
7 | * resulting in a string.
8 | *
9 | * @param {Object} a
10 | * @param {Object} b
11 | * @return {Object} a
12 | * @api private
13 | */
14 |
15 | exports.merge = function merge(a, b) {
16 | if (arguments.length === 1) {
17 | var attrs = a[0];
18 | for (var i = 1; i < a.length; i++) {
19 | attrs = merge(attrs, a[i]);
20 | }
21 | return attrs;
22 | }
23 | var ac = a['class'];
24 | var bc = b['class'];
25 |
26 | if (ac || bc) {
27 | ac = ac || [];
28 | bc = bc || [];
29 | if (!Array.isArray(ac)) ac = [ac];
30 | if (!Array.isArray(bc)) bc = [bc];
31 | a['class'] = ac.concat(bc).filter(nulls);
32 | }
33 |
34 | for (var key in b) {
35 | if (key != 'class') {
36 | a[key] = b[key];
37 | }
38 | }
39 |
40 | return a;
41 | };
42 |
43 | /**
44 | * Filter null `val`s.
45 | *
46 | * @param {*} val
47 | * @return {Boolean}
48 | * @api private
49 | */
50 |
51 | function nulls(val) {
52 | return val != null && val !== '';
53 | }
54 |
55 | /**
56 | * join array as classes.
57 | *
58 | * @param {*} val
59 | * @return {String}
60 | */
61 | exports.joinClasses = joinClasses;
62 | function joinClasses(val) {
63 | return Array.isArray(val) ? val.map(joinClasses).filter(nulls).join(' ') : val;
64 | }
65 |
66 | /**
67 | * Render the given classes.
68 | *
69 | * @param {Array} classes
70 | * @param {Array.} escaped
71 | * @return {String}
72 | */
73 | exports.cls = function cls(classes, escaped) {
74 | var buf = [];
75 | for (var i = 0; i < classes.length; i++) {
76 | if (escaped && escaped[i]) {
77 | buf.push(exports.escape(joinClasses([classes[i]])));
78 | } else {
79 | buf.push(joinClasses(classes[i]));
80 | }
81 | }
82 | var text = joinClasses(buf);
83 | if (text.length) {
84 | return ' class="' + text + '"';
85 | } else {
86 | return '';
87 | }
88 | };
89 |
90 | /**
91 | * Render the given attribute.
92 | *
93 | * @param {String} key
94 | * @param {String} val
95 | * @param {Boolean} escaped
96 | * @param {Boolean} terse
97 | * @return {String}
98 | */
99 | exports.attr = function attr(key, val, escaped, terse) {
100 | if ('boolean' == typeof val || null == val) {
101 | if (val) {
102 | return ' ' + (terse ? key : key + '="' + key + '"');
103 | } else {
104 | return '';
105 | }
106 | } else if (0 == key.indexOf('data') && 'string' != typeof val) {
107 | return ' ' + key + "='" + JSON.stringify(val).replace(/'/g, ''') + "'";
108 | } else if (escaped) {
109 | return ' ' + key + '="' + exports.escape(val) + '"';
110 | } else {
111 | return ' ' + key + '="' + val + '"';
112 | }
113 | };
114 |
115 | /**
116 | * Render the given attributes object.
117 | *
118 | * @param {Object} obj
119 | * @param {Object} escaped
120 | * @return {String}
121 | */
122 | exports.attrs = function attrs(obj, terse){
123 | var buf = [];
124 |
125 | var keys = Object.keys(obj);
126 |
127 | if (keys.length) {
128 | for (var i = 0; i < keys.length; ++i) {
129 | var key = keys[i]
130 | , val = obj[key];
131 |
132 | if ('class' == key) {
133 | if (val = joinClasses(val)) {
134 | buf.push(' ' + key + '="' + val + '"');
135 | }
136 | } else {
137 | buf.push(exports.attr(key, val, false, terse));
138 | }
139 | }
140 | }
141 |
142 | return buf.join('');
143 | };
144 |
145 | /**
146 | * Escape the given string of `html`.
147 | *
148 | * @param {String} html
149 | * @return {String}
150 | * @api private
151 | */
152 |
153 | exports.escape = function escape(html){
154 | var result = String(html)
155 | .replace(/&/g, '&')
156 | .replace(//g, '>')
158 | .replace(/"/g, '"');
159 | if (result === '' + html) return html;
160 | else return result;
161 | };
162 |
163 | /**
164 | * Re-throw the given `err` in context to the
165 | * the jade in `filename` at the given `lineno`.
166 | *
167 | * @param {Error} err
168 | * @param {String} filename
169 | * @param {String} lineno
170 | * @api private
171 | */
172 |
173 | exports.rethrow = function rethrow(err, filename, lineno, str){
174 | if (!(err instanceof Error)) throw err;
175 | if ((typeof window != 'undefined' || !filename) && !str) {
176 | err.message += ' on line ' + lineno;
177 | throw err;
178 | }
179 | try {
180 | str = str || require('fs').readFileSync(filename, 'utf8')
181 | } catch (ex) {
182 | rethrow(err, null, lineno)
183 | }
184 | var context = 3
185 | , lines = str.split('\n')
186 | , start = Math.max(lineno - context, 0)
187 | , end = Math.min(lines.length, lineno + context);
188 |
189 | // Error context
190 | var context = lines.slice(start, end).map(function(line, i){
191 | var curr = i + start + 1;
192 | return (curr == lineno ? ' > ' : ' ')
193 | + curr
194 | + '| '
195 | + line;
196 | }).join('\n');
197 |
198 | // Alter exception message
199 | err.path = filename;
200 | err.message = (filename || 'Jade') + ':' + lineno
201 | + '\n' + context + '\n\n' + err.message;
202 | throw err;
203 | };
204 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/components/visionmedia-jade/lib/runtime.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Merge two attribute objects giving precedence
5 | * to values in object `b`. Classes are special-cased
6 | * allowing for arrays and merging/joining appropriately
7 | * resulting in a string.
8 | *
9 | * @param {Object} a
10 | * @param {Object} b
11 | * @return {Object} a
12 | * @api private
13 | */
14 |
15 | exports.merge = function merge(a, b) {
16 | if (arguments.length === 1) {
17 | var attrs = a[0];
18 | for (var i = 1; i < a.length; i++) {
19 | attrs = merge(attrs, a[i]);
20 | }
21 | return attrs;
22 | }
23 | var ac = a['class'];
24 | var bc = b['class'];
25 |
26 | if (ac || bc) {
27 | ac = ac || [];
28 | bc = bc || [];
29 | if (!Array.isArray(ac)) ac = [ac];
30 | if (!Array.isArray(bc)) bc = [bc];
31 | a['class'] = ac.concat(bc).filter(nulls);
32 | }
33 |
34 | for (var key in b) {
35 | if (key != 'class') {
36 | a[key] = b[key];
37 | }
38 | }
39 |
40 | return a;
41 | };
42 |
43 | /**
44 | * Filter null `val`s.
45 | *
46 | * @param {*} val
47 | * @return {Boolean}
48 | * @api private
49 | */
50 |
51 | function nulls(val) {
52 | return val != null && val !== '';
53 | }
54 |
55 | /**
56 | * join array as classes.
57 | *
58 | * @param {*} val
59 | * @return {String}
60 | */
61 | exports.joinClasses = joinClasses;
62 | function joinClasses(val) {
63 | return Array.isArray(val) ? val.map(joinClasses).filter(nulls).join(' ') : val;
64 | }
65 |
66 | /**
67 | * Render the given classes.
68 | *
69 | * @param {Array} classes
70 | * @param {Array.} escaped
71 | * @return {String}
72 | */
73 | exports.cls = function cls(classes, escaped) {
74 | var buf = [];
75 | for (var i = 0; i < classes.length; i++) {
76 | if (escaped && escaped[i]) {
77 | buf.push(exports.escape(joinClasses([classes[i]])));
78 | } else {
79 | buf.push(joinClasses(classes[i]));
80 | }
81 | }
82 | var text = joinClasses(buf);
83 | if (text.length) {
84 | return ' class="' + text + '"';
85 | } else {
86 | return '';
87 | }
88 | };
89 |
90 | /**
91 | * Render the given attribute.
92 | *
93 | * @param {String} key
94 | * @param {String} val
95 | * @param {Boolean} escaped
96 | * @param {Boolean} terse
97 | * @return {String}
98 | */
99 | exports.attr = function attr(key, val, escaped, terse) {
100 | if ('boolean' == typeof val || null == val) {
101 | if (val) {
102 | return ' ' + (terse ? key : key + '="' + key + '"');
103 | } else {
104 | return '';
105 | }
106 | } else if (0 == key.indexOf('data') && 'string' != typeof val) {
107 | return ' ' + key + "='" + JSON.stringify(val).replace(/'/g, ''') + "'";
108 | } else if (escaped) {
109 | return ' ' + key + '="' + exports.escape(val) + '"';
110 | } else {
111 | return ' ' + key + '="' + val + '"';
112 | }
113 | };
114 |
115 | /**
116 | * Render the given attributes object.
117 | *
118 | * @param {Object} obj
119 | * @param {Object} escaped
120 | * @return {String}
121 | */
122 | exports.attrs = function attrs(obj, terse){
123 | var buf = [];
124 |
125 | var keys = Object.keys(obj);
126 |
127 | if (keys.length) {
128 | for (var i = 0; i < keys.length; ++i) {
129 | var key = keys[i]
130 | , val = obj[key];
131 |
132 | if ('class' == key) {
133 | if (val = joinClasses(val)) {
134 | buf.push(' ' + key + '="' + val + '"');
135 | }
136 | } else {
137 | buf.push(exports.attr(key, val, false, terse));
138 | }
139 | }
140 | }
141 |
142 | return buf.join('');
143 | };
144 |
145 | /**
146 | * Escape the given string of `html`.
147 | *
148 | * @param {String} html
149 | * @return {String}
150 | * @api private
151 | */
152 |
153 | exports.escape = function escape(html){
154 | var result = String(html)
155 | .replace(/&/g, '&')
156 | .replace(//g, '>')
158 | .replace(/"/g, '"');
159 | if (result === '' + html) return html;
160 | else return result;
161 | };
162 |
163 | /**
164 | * Re-throw the given `err` in context to the
165 | * the jade in `filename` at the given `lineno`.
166 | *
167 | * @param {Error} err
168 | * @param {String} filename
169 | * @param {String} lineno
170 | * @api private
171 | */
172 |
173 | exports.rethrow = function rethrow(err, filename, lineno, str){
174 | if (!(err instanceof Error)) throw err;
175 | if ((typeof window != 'undefined' || !filename) && !str) {
176 | err.message += ' on line ' + lineno;
177 | throw err;
178 | }
179 | try {
180 | str = str || require('fs').readFileSync(filename, 'utf8')
181 | } catch (ex) {
182 | rethrow(err, null, lineno)
183 | }
184 | var context = 3
185 | , lines = str.split('\n')
186 | , start = Math.max(lineno - context, 0)
187 | , end = Math.min(lines.length, lineno + context);
188 |
189 | // Error context
190 | var context = lines.slice(start, end).map(function(line, i){
191 | var curr = i + start + 1;
192 | return (curr == lineno ? ' > ' : ' ')
193 | + curr
194 | + '| '
195 | + line;
196 | }).join('\n');
197 |
198 | // Alter exception message
199 | err.path = filename;
200 | err.message = (filename || 'Jade') + ':' + lineno
201 | + '\n' + context + '\n\n' + err.message;
202 | throw err;
203 | };
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elem
2 |
3 | An tiny and easy to use web framework based on making custom HTML elements.
4 |
5 | ## Creating Custom Elements
6 |
7 | An elem is just a folder who's name is the tag name.
8 |
9 | ```
10 | ui
11 | widget
12 | widget.js
13 | widget.html.jade
14 | widget.css.styl
15 | hello.txt
16 | window
17 | jquery.js
18 | ```
19 |
20 | - Files with extensions like `widget.html.jade` are caught by the pre-processor and become `widget.html`.
21 |
22 | - When a `` appears on the page `elem/loader` will pull down all of the files in the `widget` folder.
23 |
24 | - If there is a `widget.html` the element's inner html will be replaced with it.
25 |
26 | - If there is a `widget.css` the css will be added to the document.
27 |
28 | - If there is a `widget.js` it will be treated like a node module. Here we can export a function that applies the behavior. This function is called for every instance of `` and passed an object containing all of the file contents of the folder.
29 |
30 | *widget.js*:
31 | ```
32 | module.exports = function widget() {
33 | $(this).find('.hello').text('hello');
34 | }
35 | ```
36 |
37 | Giving the function two arguments makes it async, delaying rendering and loading child elements:
38 |
39 | ```
40 | module.exports = function widget(done) {
41 | var self = this;
42 |
43 | $.get('/content.txt', function(text) {
44 | $(self).text(text);
45 | done();
46 | });
47 | }
48 | ```
49 |
50 |
51 | ## Special Folders
52 | * `*/lib` is recursively pre-loaded before the element is applied. Put anything you want to require() in here so it is available when the element implementation.
53 |
54 | * `*/components` are parsed as installed [components](http://component.io) and can be required globally.
55 |
56 | * `*/window` is recursively pre-loaded but executed without a module.exports.
57 |
58 | This is where you would put classic global libraries like jQuery.
59 |
60 | You could also `component install component/jquery` and `require('jquery')`. But jQuery was always designed to extend window. Using it as a module breaks plugins.
61 |
62 | Everything in `window` is run in top-down order of directory depth. So to make jQuery plugins run after jQuery, you can put them in a `window/jquery-plugins/` folder.
63 |
64 | ## Express/Connect Middleware
65 |
66 | ```
67 | var express = require('express');
68 | var elem = require('elem');
69 | var server = express();
70 | var app = elem(__dirname+'/app');
71 |
72 | var production = process.env.NODE_ENV == 'production';
73 |
74 | server.use('/app', ui.loader({production: production}));
75 |
76 | // Remove this route and include
77 | // ');
67 | });
68 | });
69 |
70 |
71 | describe('generateLoaderJS', function() {
72 | it('minifies template boot/loader.js');
73 | it('adds a starter call with configs');
74 | });
75 |
76 | describe('buildLoader', function() {
77 | it('TODO');
78 | });
79 |
80 | describe('buildStaticSiteBootstrap', function() {
81 | it('TODO');
82 | });
83 |
84 |
85 | describe('loader', function() {
86 | it('TODO');
87 | });
88 |
89 | describe('pack', function() {
90 | it('combines a list of files into a single name-data map', function() {
91 | sample.build();
92 | var data = sample.pack([
93 | 'sample.html'
94 | ]);
95 |
96 | var parsed = JSON.parse(data);
97 |
98 | expect(parsed).eql({
99 | 'sample.html': 'Sample
\n'
100 | });
101 | });
102 | });
103 |
104 | describe('isOutdated', function() {
105 | it('returns false if the file has not been modified since the last build', function() {
106 | sample.build();
107 | var outdated = sample.isOutdated(__dirname+'/sample/sample.html');
108 | expect(outdated).eq(false);
109 | });
110 |
111 | it('returns true if the file has been modified since the last build', function() {
112 | sample.build();
113 |
114 | var srcfile = __dirname+'/sample/sample.html';
115 |
116 | // Overwrite the file with the same
117 | // content but should bump the mtime
118 | var tmp = fs.readFileSync(srcfile);
119 | fs.writeFileSync(srcfile, tmp);
120 |
121 | var outdated = sample.isOutdated(srcfile);
122 | expect(outdated).eq(true);
123 | });
124 | });
125 |
126 | describe('getBuildPath', function() {
127 | it('works with a base', function() {
128 | var sample = Elem('base');
129 | var path = sample.getBuildPath('base/body.html.jade', true);
130 | assert.equal(path, 'base/_build/body.html');
131 | });
132 |
133 | it('works without a base', function() {
134 | var sample = Elem();
135 | var path = sample.getBuildPath('body.html.jade', true);
136 | assert.equal(path, '_build/body.html');
137 | });
138 |
139 | it('can prune the last extension', function() {
140 | var sample = Elem('base');
141 | var path = sample.getBuildPath('base/body.html.jade', true);
142 | assert.equal(path, 'base/_build/body.html');
143 | });
144 |
145 | it('leaves singletons alone', function() {
146 | var sample = Elem('base');
147 | var path = sample.getBuildPath('base/body.html');
148 | assert.equal(path, 'base/_build/body.html');
149 | });
150 |
151 | it('leaves extensionless alone', function() {
152 | var sample = Elem('base');
153 | var path = sample.getBuildPath('base/body');
154 | assert.equal(path, 'base/_build/body');
155 | });
156 |
157 | it('can handle a path with a . in it', function() {
158 | var sample = Elem('base');
159 | var path = sample.getBuildPath('base/poop.js/body.html');
160 | assert.equal(path, 'base/_build/poop.js/body.html');
161 | });
162 |
163 | it('normalizes', function() {
164 | var sample = Elem('base');
165 | var path = sample.getBuildPath('base/widget/../body.html');
166 | assert.equal(path, 'base/_build/body.html');
167 | });
168 | });
169 |
170 | describe('buildFile', function() {
171 | it('builds one file' , function() {
172 | sample.buildFile(__dirname+'/sample/sample.html');
173 | var data = ''+fs.readFileSync(__dirname+'/sample/_build/sample.html');
174 | expect(data).eq('Sample
\n');
175 | });
176 | });
177 |
178 | describe('build', function() {
179 | it('creates _build dir', function() {
180 | sample.build();
181 | var exists = fs.existsSync(__dirname+'/sample/_build');
182 | expect(exists).eq(true);
183 | });
184 | it('writes index.json');
185 | it('writes last_build.json');
186 | });
187 |
188 | describe('clean', function() {
189 | it('deletes _build dir', function() {
190 | sample.build();
191 | sample.clean();
192 | var exists = fs.existsSync(__dirname+'/sample/_build');
193 | expect(exists).eq(false);
194 | });
195 |
196 | it('empties lastBuild', function() {
197 | sample.build();
198 | sample.clean();
199 | expect(sample.lastBuild).eql({});
200 | });
201 | });
202 |
203 | describe('simulate', function() {
204 | beforeEach(function() {
205 | sample.build();
206 | });
207 |
208 | it('runs an element with jsdom', function() {
209 | var domnode = sample.simulate();
210 | expect(domnode.hello).eq('hello');
211 | });
212 |
213 | it('returns top dom element in provided html', function() {
214 | var domnode = sample.simulate('');
215 | expect(domnode.tagName).eq('BLAH');
216 | });
217 |
218 | it('renders subelements', function() {
219 | var domnode = sample.simulate('');
220 | console.log(domnode.innerHTML);
221 | });
222 |
223 | it('renders nested content', function() {
224 | var domnode = sample.simulate('hello');
225 | expect(domnode.innerHTML).eq('\nhello');
226 | });
227 |
228 | it('does not enhance detached subelements', function() {
229 | /**
230 | * does a render(html) which replaces
231 | * all of the old innerHTML. This tests that we
232 | * do not enhance any of the removed elements.
233 | */
234 | var domnode = sample.simulate('hello', {count: 0});
235 | var window = domnode.ownerDocument.defaultView;
236 | expect(window.count).eq(2);
237 | });
238 | });
239 |
240 |
241 | });
242 |
--------------------------------------------------------------------------------
/examples/helloworld/components/visionmedia-page.js/index.js:
--------------------------------------------------------------------------------
1 |
2 | ;(function(){
3 |
4 | /**
5 | * Perform initial dispatch.
6 | */
7 |
8 | var dispatch = true;
9 |
10 | /**
11 | * Base path.
12 | */
13 |
14 | var base = '';
15 |
16 | /**
17 | * Running flag.
18 | */
19 |
20 | var running;
21 |
22 | /**
23 | * Register `path` with callback `fn()`,
24 | * or route `path`, or `page.start()`.
25 | *
26 | * page(fn);
27 | * page('*', fn);
28 | * page('/user/:id', load, user);
29 | * page('/user/' + user.id, { some: 'thing' });
30 | * page('/user/' + user.id);
31 | * page();
32 | *
33 | * @param {String|Function} path
34 | * @param {Function} fn...
35 | * @api public
36 | */
37 |
38 | function page(path, fn) {
39 | //
40 | if ('function' == typeof path) {
41 | return page('*', path);
42 | }
43 |
44 | // route to
45 | if ('function' == typeof fn) {
46 | var route = new Route(path);
47 | for (var i = 1; i < arguments.length; ++i) {
48 | page.callbacks.push(route.middleware(arguments[i]));
49 | }
50 | // show with [state]
51 | } else if ('string' == typeof path) {
52 | page.show(path, fn);
53 | // start [options]
54 | } else {
55 | page.start(path);
56 | }
57 | }
58 |
59 | /**
60 | * Callback functions.
61 | */
62 |
63 | page.callbacks = [];
64 |
65 | /**
66 | * Get or set basepath to `path`.
67 | *
68 | * @param {String} path
69 | * @api public
70 | */
71 |
72 | page.base = function(path){
73 | if (0 == arguments.length) return base;
74 | base = path;
75 | };
76 |
77 | /**
78 | * Bind with the given `options`.
79 | *
80 | * Options:
81 | *
82 | * - `click` bind to click events [true]
83 | * - `popstate` bind to popstate [true]
84 | * - `dispatch` perform initial dispatch [true]
85 | *
86 | * @param {Object} options
87 | * @api public
88 | */
89 |
90 | page.start = function(options){
91 | options = options || {};
92 | if (running) return;
93 | running = true;
94 | if (false === options.dispatch) dispatch = false;
95 | if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false);
96 | if (false !== options.click) window.addEventListener('click', onclick, false);
97 | if (!dispatch) return;
98 | var url = location.pathname + location.search + location.hash;
99 | page.replace(url, null, true, dispatch);
100 | };
101 |
102 | /**
103 | * Unbind click and popstate event handlers.
104 | *
105 | * @api public
106 | */
107 |
108 | page.stop = function(){
109 | running = false;
110 | removeEventListener('click', onclick, false);
111 | removeEventListener('popstate', onpopstate, false);
112 | };
113 |
114 | /**
115 | * Show `path` with optional `state` object.
116 | *
117 | * @param {String} path
118 | * @param {Object} state
119 | * @param {Boolean} dispatch
120 | * @return {Context}
121 | * @api public
122 | */
123 |
124 | page.show = function(path, state, dispatch){
125 | var ctx = new Context(path, state);
126 | if (false !== dispatch) page.dispatch(ctx);
127 | if (!ctx.unhandled) ctx.pushState();
128 | return ctx;
129 | };
130 |
131 | /**
132 | * Replace `path` with optional `state` object.
133 | *
134 | * @param {String} path
135 | * @param {Object} state
136 | * @return {Context}
137 | * @api public
138 | */
139 |
140 | page.replace = function(path, state, init, dispatch){
141 | var ctx = new Context(path, state);
142 | ctx.init = init;
143 | if (null == dispatch) dispatch = true;
144 | if (dispatch) page.dispatch(ctx);
145 | ctx.save();
146 | return ctx;
147 | };
148 |
149 | /**
150 | * Dispatch the given `ctx`.
151 | *
152 | * @param {Object} ctx
153 | * @api private
154 | */
155 |
156 | page.dispatch = function(ctx){
157 | var i = 0;
158 |
159 | function next() {
160 | var fn = page.callbacks[i++];
161 | if (!fn) return unhandled(ctx);
162 | fn(ctx, next);
163 | }
164 |
165 | next();
166 | };
167 |
168 | /**
169 | * Unhandled `ctx`. When it's not the initial
170 | * popstate then redirect. If you wish to handle
171 | * 404s on your own use `page('*', callback)`.
172 | *
173 | * @param {Context} ctx
174 | * @api private
175 | */
176 |
177 | function unhandled(ctx) {
178 | var current = window.location.pathname + window.location.search;
179 | if (current == ctx.canonicalPath) return;
180 | page.stop();
181 | ctx.unhandled = true;
182 | window.location = ctx.canonicalPath;
183 | }
184 |
185 | /**
186 | * Initialize a new "request" `Context`
187 | * with the given `path` and optional initial `state`.
188 | *
189 | * @param {String} path
190 | * @param {Object} state
191 | * @api public
192 | */
193 |
194 | function Context(path, state) {
195 | if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path;
196 | var i = path.indexOf('?');
197 |
198 | this.canonicalPath = path;
199 | this.path = path.replace(base, '') || '/';
200 |
201 | this.title = document.title;
202 | this.state = state || {};
203 | this.state.path = path;
204 | this.querystring = ~i ? path.slice(i + 1) : '';
205 | this.pathname = ~i ? path.slice(0, i) : path;
206 | this.params = [];
207 |
208 | // fragment
209 | this.hash = '';
210 | if (!~this.path.indexOf('#')) return;
211 | var parts = this.path.split('#');
212 | this.path = parts[0];
213 | this.hash = parts[1] || '';
214 | this.querystring = this.querystring.split('#')[0];
215 | }
216 |
217 | /**
218 | * Expose `Context`.
219 | */
220 |
221 | page.Context = Context;
222 |
223 | /**
224 | * Push state.
225 | *
226 | * @api private
227 | */
228 |
229 | Context.prototype.pushState = function(){
230 | history.pushState(this.state, this.title, this.canonicalPath);
231 | };
232 |
233 | /**
234 | * Save the context state.
235 | *
236 | * @api public
237 | */
238 |
239 | Context.prototype.save = function(){
240 | history.replaceState(this.state, this.title, this.canonicalPath);
241 | };
242 |
243 | /**
244 | * Initialize `Route` with the given HTTP `path`,
245 | * and an array of `callbacks` and `options`.
246 | *
247 | * Options:
248 | *
249 | * - `sensitive` enable case-sensitive routes
250 | * - `strict` enable strict matching for trailing slashes
251 | *
252 | * @param {String} path
253 | * @param {Object} options.
254 | * @api private
255 | */
256 |
257 | function Route(path, options) {
258 | options = options || {};
259 | this.path = path;
260 | this.method = 'GET';
261 | this.regexp = pathtoRegexp(path
262 | , this.keys = []
263 | , options.sensitive
264 | , options.strict);
265 | }
266 |
267 | /**
268 | * Expose `Route`.
269 | */
270 |
271 | page.Route = Route;
272 |
273 | /**
274 | * Return route middleware with
275 | * the given callback `fn()`.
276 | *
277 | * @param {Function} fn
278 | * @return {Function}
279 | * @api public
280 | */
281 |
282 | Route.prototype.middleware = function(fn){
283 | var self = this;
284 | return function(ctx, next){
285 | if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
286 | next();
287 | };
288 | };
289 |
290 | /**
291 | * Check if this route matches `path`, if so
292 | * populate `params`.
293 | *
294 | * @param {String} path
295 | * @param {Array} params
296 | * @return {Boolean}
297 | * @api private
298 | */
299 |
300 | Route.prototype.match = function(path, params){
301 | var keys = this.keys
302 | , qsIndex = path.indexOf('?')
303 | , pathname = ~qsIndex ? path.slice(0, qsIndex) : path
304 | , m = this.regexp.exec(decodeURIComponent(pathname));
305 |
306 | if (!m) return false;
307 |
308 | for (var i = 1, len = m.length; i < len; ++i) {
309 | var key = keys[i - 1];
310 |
311 | var val = 'string' == typeof m[i]
312 | ? decodeURIComponent(m[i])
313 | : m[i];
314 |
315 | if (key) {
316 | params[key.name] = undefined !== params[key.name]
317 | ? params[key.name]
318 | : val;
319 | } else {
320 | params.push(val);
321 | }
322 | }
323 |
324 | return true;
325 | };
326 |
327 | /**
328 | * Normalize the given path string,
329 | * returning a regular expression.
330 | *
331 | * An empty array should be passed,
332 | * which will contain the placeholder
333 | * key names. For example "/user/:id" will
334 | * then contain ["id"].
335 | *
336 | * @param {String|RegExp|Array} path
337 | * @param {Array} keys
338 | * @param {Boolean} sensitive
339 | * @param {Boolean} strict
340 | * @return {RegExp}
341 | * @api private
342 | */
343 |
344 | function pathtoRegexp(path, keys, sensitive, strict) {
345 | if (path instanceof RegExp) return path;
346 | if (path instanceof Array) path = '(' + path.join('|') + ')';
347 | path = path
348 | .concat(strict ? '' : '/?')
349 | .replace(/\/\(/g, '(?:/')
350 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
351 | keys.push({ name: key, optional: !! optional });
352 | slash = slash || '';
353 | return ''
354 | + (optional ? '' : slash)
355 | + '(?:'
356 | + (optional ? slash : '')
357 | + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
358 | + (optional || '');
359 | })
360 | .replace(/([\/.])/g, '\\$1')
361 | .replace(/\*/g, '(.*)');
362 | return new RegExp('^' + path + '$', sensitive ? '' : 'i');
363 | }
364 |
365 | /**
366 | * Handle "populate" events.
367 | */
368 |
369 | function onpopstate(e) {
370 | if (e.state) {
371 | var path = e.state.path;
372 | page.replace(path, e.state);
373 | }
374 | }
375 |
376 | /**
377 | * Handle "click" events.
378 | */
379 |
380 | function onclick(e) {
381 | if (1 != which(e)) return;
382 | if (e.metaKey || e.ctrlKey || e.shiftKey) return;
383 | if (e.defaultPrevented) return;
384 |
385 | // ensure link
386 | var el = e.target;
387 | while (el && 'A' != el.nodeName) el = el.parentNode;
388 | if (!el || 'A' != el.nodeName) return;
389 |
390 | // ensure non-hash for the same path
391 | var link = el.getAttribute('href');
392 | if (el.pathname == location.pathname && (el.hash || '#' == link)) return;
393 |
394 | // Check for mailto: in the href
395 | if (link.indexOf("mailto:") > -1) return;
396 |
397 | // check target
398 | if (el.target) return;
399 |
400 | // x-origin
401 | if (!sameOrigin(el.href)) return;
402 |
403 | // rebuild path
404 | var path = el.pathname + el.search + (el.hash || '');
405 |
406 | // same page
407 | var orig = path + el.hash;
408 |
409 | path = path.replace(base, '');
410 | if (base && orig == path) return;
411 |
412 | e.preventDefault();
413 | page.show(orig);
414 | }
415 |
416 | /**
417 | * Event button.
418 | */
419 |
420 | function which(e) {
421 | e = e || window.event;
422 | return null == e.which
423 | ? e.button
424 | : e.which;
425 | }
426 |
427 | /**
428 | * Check if `href` is the same origin.
429 | */
430 |
431 | function sameOrigin(href) {
432 | var origin = location.protocol + '//' + location.hostname;
433 | if (location.port) origin += ':' + location.port;
434 | return 0 == href.indexOf(origin);
435 | }
436 |
437 | /**
438 | * Expose `page`.
439 | */
440 |
441 | if ('undefined' == typeof module) {
442 | window.page = page;
443 | } else {
444 | module.exports = page;
445 | }
446 |
447 | })();
448 |
--------------------------------------------------------------------------------
/examples/file-explorer/components/visionmedia-page.js/index.js:
--------------------------------------------------------------------------------
1 |
2 | ;(function(){
3 |
4 | /**
5 | * Perform initial dispatch.
6 | */
7 |
8 | var dispatch = true;
9 |
10 | /**
11 | * Base path.
12 | */
13 |
14 | var base = '';
15 |
16 | /**
17 | * Running flag.
18 | */
19 |
20 | var running;
21 |
22 | /**
23 | * Register `path` with callback `fn()`,
24 | * or route `path`, or `page.start()`.
25 | *
26 | * page(fn);
27 | * page('*', fn);
28 | * page('/user/:id', load, user);
29 | * page('/user/' + user.id, { some: 'thing' });
30 | * page('/user/' + user.id);
31 | * page();
32 | *
33 | * @param {String|Function} path
34 | * @param {Function} fn...
35 | * @api public
36 | */
37 |
38 | function page(path, fn) {
39 | //
40 | if ('function' == typeof path) {
41 | return page('*', path);
42 | }
43 |
44 | // route to
45 | if ('function' == typeof fn) {
46 | var route = new Route(path);
47 | for (var i = 1; i < arguments.length; ++i) {
48 | page.callbacks.push(route.middleware(arguments[i]));
49 | }
50 | // show with [state]
51 | } else if ('string' == typeof path) {
52 | page.show(path, fn);
53 | // start [options]
54 | } else {
55 | page.start(path);
56 | }
57 | }
58 |
59 | /**
60 | * Callback functions.
61 | */
62 |
63 | page.callbacks = [];
64 |
65 | /**
66 | * Get or set basepath to `path`.
67 | *
68 | * @param {String} path
69 | * @api public
70 | */
71 |
72 | page.base = function(path){
73 | if (0 == arguments.length) return base;
74 | base = path;
75 | };
76 |
77 | /**
78 | * Bind with the given `options`.
79 | *
80 | * Options:
81 | *
82 | * - `click` bind to click events [true]
83 | * - `popstate` bind to popstate [true]
84 | * - `dispatch` perform initial dispatch [true]
85 | *
86 | * @param {Object} options
87 | * @api public
88 | */
89 |
90 | page.start = function(options){
91 | options = options || {};
92 | if (running) return;
93 | running = true;
94 | if (false === options.dispatch) dispatch = false;
95 | if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false);
96 | if (false !== options.click) window.addEventListener('click', onclick, false);
97 | if (!dispatch) return;
98 | var url = location.pathname + location.search + location.hash;
99 | page.replace(url, null, true, dispatch);
100 | };
101 |
102 | /**
103 | * Unbind click and popstate event handlers.
104 | *
105 | * @api public
106 | */
107 |
108 | page.stop = function(){
109 | running = false;
110 | removeEventListener('click', onclick, false);
111 | removeEventListener('popstate', onpopstate, false);
112 | };
113 |
114 | /**
115 | * Show `path` with optional `state` object.
116 | *
117 | * @param {String} path
118 | * @param {Object} state
119 | * @param {Boolean} dispatch
120 | * @return {Context}
121 | * @api public
122 | */
123 |
124 | page.show = function(path, state, dispatch){
125 | var ctx = new Context(path, state);
126 | if (false !== dispatch) page.dispatch(ctx);
127 | if (!ctx.unhandled) ctx.pushState();
128 | return ctx;
129 | };
130 |
131 | /**
132 | * Replace `path` with optional `state` object.
133 | *
134 | * @param {String} path
135 | * @param {Object} state
136 | * @return {Context}
137 | * @api public
138 | */
139 |
140 | page.replace = function(path, state, init, dispatch){
141 | var ctx = new Context(path, state);
142 | ctx.init = init;
143 | if (null == dispatch) dispatch = true;
144 | if (dispatch) page.dispatch(ctx);
145 | ctx.save();
146 | return ctx;
147 | };
148 |
149 | /**
150 | * Dispatch the given `ctx`.
151 | *
152 | * @param {Object} ctx
153 | * @api private
154 | */
155 |
156 | page.dispatch = function(ctx){
157 | var i = 0;
158 |
159 | function next() {
160 | var fn = page.callbacks[i++];
161 | if (!fn) return unhandled(ctx);
162 | fn(ctx, next);
163 | }
164 |
165 | next();
166 | };
167 |
168 | /**
169 | * Unhandled `ctx`. When it's not the initial
170 | * popstate then redirect. If you wish to handle
171 | * 404s on your own use `page('*', callback)`.
172 | *
173 | * @param {Context} ctx
174 | * @api private
175 | */
176 |
177 | function unhandled(ctx) {
178 | var current = window.location.pathname + window.location.search;
179 | if (current == ctx.canonicalPath) return;
180 | page.stop();
181 | ctx.unhandled = true;
182 | window.location = ctx.canonicalPath;
183 | }
184 |
185 | /**
186 | * Initialize a new "request" `Context`
187 | * with the given `path` and optional initial `state`.
188 | *
189 | * @param {String} path
190 | * @param {Object} state
191 | * @api public
192 | */
193 |
194 | function Context(path, state) {
195 | if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path;
196 | var i = path.indexOf('?');
197 |
198 | this.canonicalPath = path;
199 | this.path = path.replace(base, '') || '/';
200 |
201 | this.title = document.title;
202 | this.state = state || {};
203 | this.state.path = path;
204 | this.querystring = ~i ? path.slice(i + 1) : '';
205 | this.pathname = ~i ? path.slice(0, i) : path;
206 | this.params = [];
207 |
208 | // fragment
209 | this.hash = '';
210 | if (!~this.path.indexOf('#')) return;
211 | var parts = this.path.split('#');
212 | this.path = parts[0];
213 | this.hash = parts[1] || '';
214 | this.querystring = this.querystring.split('#')[0];
215 | }
216 |
217 | /**
218 | * Expose `Context`.
219 | */
220 |
221 | page.Context = Context;
222 |
223 | /**
224 | * Push state.
225 | *
226 | * @api private
227 | */
228 |
229 | Context.prototype.pushState = function(){
230 | history.pushState(this.state, this.title, this.canonicalPath);
231 | };
232 |
233 | /**
234 | * Save the context state.
235 | *
236 | * @api public
237 | */
238 |
239 | Context.prototype.save = function(){
240 | history.replaceState(this.state, this.title, this.canonicalPath);
241 | };
242 |
243 | /**
244 | * Initialize `Route` with the given HTTP `path`,
245 | * and an array of `callbacks` and `options`.
246 | *
247 | * Options:
248 | *
249 | * - `sensitive` enable case-sensitive routes
250 | * - `strict` enable strict matching for trailing slashes
251 | *
252 | * @param {String} path
253 | * @param {Object} options.
254 | * @api private
255 | */
256 |
257 | function Route(path, options) {
258 | options = options || {};
259 | this.path = path;
260 | this.method = 'GET';
261 | this.regexp = pathtoRegexp(path
262 | , this.keys = []
263 | , options.sensitive
264 | , options.strict);
265 | }
266 |
267 | /**
268 | * Expose `Route`.
269 | */
270 |
271 | page.Route = Route;
272 |
273 | /**
274 | * Return route middleware with
275 | * the given callback `fn()`.
276 | *
277 | * @param {Function} fn
278 | * @return {Function}
279 | * @api public
280 | */
281 |
282 | Route.prototype.middleware = function(fn){
283 | var self = this;
284 | return function(ctx, next){
285 | if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
286 | next();
287 | };
288 | };
289 |
290 | /**
291 | * Check if this route matches `path`, if so
292 | * populate `params`.
293 | *
294 | * @param {String} path
295 | * @param {Array} params
296 | * @return {Boolean}
297 | * @api private
298 | */
299 |
300 | Route.prototype.match = function(path, params){
301 | var keys = this.keys
302 | , qsIndex = path.indexOf('?')
303 | , pathname = ~qsIndex ? path.slice(0, qsIndex) : path
304 | , m = this.regexp.exec(decodeURIComponent(pathname));
305 |
306 | if (!m) return false;
307 |
308 | for (var i = 1, len = m.length; i < len; ++i) {
309 | var key = keys[i - 1];
310 |
311 | var val = 'string' == typeof m[i]
312 | ? decodeURIComponent(m[i])
313 | : m[i];
314 |
315 | if (key) {
316 | params[key.name] = undefined !== params[key.name]
317 | ? params[key.name]
318 | : val;
319 | } else {
320 | params.push(val);
321 | }
322 | }
323 |
324 | return true;
325 | };
326 |
327 | /**
328 | * Normalize the given path string,
329 | * returning a regular expression.
330 | *
331 | * An empty array should be passed,
332 | * which will contain the placeholder
333 | * key names. For example "/user/:id" will
334 | * then contain ["id"].
335 | *
336 | * @param {String|RegExp|Array} path
337 | * @param {Array} keys
338 | * @param {Boolean} sensitive
339 | * @param {Boolean} strict
340 | * @return {RegExp}
341 | * @api private
342 | */
343 |
344 | function pathtoRegexp(path, keys, sensitive, strict) {
345 | if (path instanceof RegExp) return path;
346 | if (path instanceof Array) path = '(' + path.join('|') + ')';
347 | path = path
348 | .concat(strict ? '' : '/?')
349 | .replace(/\/\(/g, '(?:/')
350 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
351 | keys.push({ name: key, optional: !! optional });
352 | slash = slash || '';
353 | return ''
354 | + (optional ? '' : slash)
355 | + '(?:'
356 | + (optional ? slash : '')
357 | + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
358 | + (optional || '');
359 | })
360 | .replace(/([\/.])/g, '\\$1')
361 | .replace(/\*/g, '(.*)');
362 | return new RegExp('^' + path + '$', sensitive ? '' : 'i');
363 | }
364 |
365 | /**
366 | * Handle "populate" events.
367 | */
368 |
369 | function onpopstate(e) {
370 | if (e.state) {
371 | var path = e.state.path;
372 | page.replace(path, e.state);
373 | }
374 | }
375 |
376 | /**
377 | * Handle "click" events.
378 | */
379 |
380 | function onclick(e) {
381 | if (1 != which(e)) return;
382 | if (e.metaKey || e.ctrlKey || e.shiftKey) return;
383 | if (e.defaultPrevented) return;
384 |
385 | // ensure link
386 | var el = e.target;
387 | while (el && 'A' != el.nodeName) el = el.parentNode;
388 | if (!el || 'A' != el.nodeName) return;
389 |
390 | // ensure non-hash for the same path
391 | var link = el.getAttribute('href');
392 | if (el.pathname == location.pathname && (el.hash || '#' == link)) return;
393 |
394 | // Check for mailto: in the href
395 | if (link.indexOf("mailto:") > -1) return;
396 |
397 | // check target
398 | if (el.target) return;
399 |
400 | // x-origin
401 | if (!sameOrigin(el.href)) return;
402 |
403 | // rebuild path
404 | var path = el.pathname + el.search + (el.hash || '');
405 |
406 | // same page
407 | var orig = path + el.hash;
408 |
409 | path = path.replace(base, '');
410 | if (base && orig == path) return;
411 |
412 | e.preventDefault();
413 | page.show(orig);
414 | }
415 |
416 | /**
417 | * Event button.
418 | */
419 |
420 | function which(e) {
421 | e = e || window.event;
422 | return null == e.which
423 | ? e.button
424 | : e.which;
425 | }
426 |
427 | /**
428 | * Check if `href` is the same origin.
429 | */
430 |
431 | function sameOrigin(href) {
432 | var origin = location.protocol + '//' + location.hostname;
433 | if (location.port) origin += ':' + location.port;
434 | return 0 == href.indexOf(origin);
435 | }
436 |
437 | /**
438 | * Expose `page`.
439 | */
440 |
441 | if ('undefined' == typeof module) {
442 | window.page = page;
443 | } else {
444 | module.exports = page;
445 | }
446 |
447 | })();
448 |
--------------------------------------------------------------------------------
/examples/file-explorer/_build/components/visionmedia-page.js/index.js:
--------------------------------------------------------------------------------
1 |
2 | ;(function(){
3 |
4 | /**
5 | * Perform initial dispatch.
6 | */
7 |
8 | var dispatch = true;
9 |
10 | /**
11 | * Base path.
12 | */
13 |
14 | var base = '';
15 |
16 | /**
17 | * Running flag.
18 | */
19 |
20 | var running;
21 |
22 | /**
23 | * Register `path` with callback `fn()`,
24 | * or route `path`, or `page.start()`.
25 | *
26 | * page(fn);
27 | * page('*', fn);
28 | * page('/user/:id', load, user);
29 | * page('/user/' + user.id, { some: 'thing' });
30 | * page('/user/' + user.id);
31 | * page();
32 | *
33 | * @param {String|Function} path
34 | * @param {Function} fn...
35 | * @api public
36 | */
37 |
38 | function page(path, fn) {
39 | //
40 | if ('function' == typeof path) {
41 | return page('*', path);
42 | }
43 |
44 | // route to
45 | if ('function' == typeof fn) {
46 | var route = new Route(path);
47 | for (var i = 1; i < arguments.length; ++i) {
48 | page.callbacks.push(route.middleware(arguments[i]));
49 | }
50 | // show with [state]
51 | } else if ('string' == typeof path) {
52 | page.show(path, fn);
53 | // start [options]
54 | } else {
55 | page.start(path);
56 | }
57 | }
58 |
59 | /**
60 | * Callback functions.
61 | */
62 |
63 | page.callbacks = [];
64 |
65 | /**
66 | * Get or set basepath to `path`.
67 | *
68 | * @param {String} path
69 | * @api public
70 | */
71 |
72 | page.base = function(path){
73 | if (0 == arguments.length) return base;
74 | base = path;
75 | };
76 |
77 | /**
78 | * Bind with the given `options`.
79 | *
80 | * Options:
81 | *
82 | * - `click` bind to click events [true]
83 | * - `popstate` bind to popstate [true]
84 | * - `dispatch` perform initial dispatch [true]
85 | *
86 | * @param {Object} options
87 | * @api public
88 | */
89 |
90 | page.start = function(options){
91 | options = options || {};
92 | if (running) return;
93 | running = true;
94 | if (false === options.dispatch) dispatch = false;
95 | if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false);
96 | if (false !== options.click) window.addEventListener('click', onclick, false);
97 | if (!dispatch) return;
98 | var url = location.pathname + location.search + location.hash;
99 | page.replace(url, null, true, dispatch);
100 | };
101 |
102 | /**
103 | * Unbind click and popstate event handlers.
104 | *
105 | * @api public
106 | */
107 |
108 | page.stop = function(){
109 | running = false;
110 | removeEventListener('click', onclick, false);
111 | removeEventListener('popstate', onpopstate, false);
112 | };
113 |
114 | /**
115 | * Show `path` with optional `state` object.
116 | *
117 | * @param {String} path
118 | * @param {Object} state
119 | * @param {Boolean} dispatch
120 | * @return {Context}
121 | * @api public
122 | */
123 |
124 | page.show = function(path, state, dispatch){
125 | var ctx = new Context(path, state);
126 | if (false !== dispatch) page.dispatch(ctx);
127 | if (!ctx.unhandled) ctx.pushState();
128 | return ctx;
129 | };
130 |
131 | /**
132 | * Replace `path` with optional `state` object.
133 | *
134 | * @param {String} path
135 | * @param {Object} state
136 | * @return {Context}
137 | * @api public
138 | */
139 |
140 | page.replace = function(path, state, init, dispatch){
141 | var ctx = new Context(path, state);
142 | ctx.init = init;
143 | if (null == dispatch) dispatch = true;
144 | if (dispatch) page.dispatch(ctx);
145 | ctx.save();
146 | return ctx;
147 | };
148 |
149 | /**
150 | * Dispatch the given `ctx`.
151 | *
152 | * @param {Object} ctx
153 | * @api private
154 | */
155 |
156 | page.dispatch = function(ctx){
157 | var i = 0;
158 |
159 | function next() {
160 | var fn = page.callbacks[i++];
161 | if (!fn) return unhandled(ctx);
162 | fn(ctx, next);
163 | }
164 |
165 | next();
166 | };
167 |
168 | /**
169 | * Unhandled `ctx`. When it's not the initial
170 | * popstate then redirect. If you wish to handle
171 | * 404s on your own use `page('*', callback)`.
172 | *
173 | * @param {Context} ctx
174 | * @api private
175 | */
176 |
177 | function unhandled(ctx) {
178 | var current = window.location.pathname + window.location.search;
179 | if (current == ctx.canonicalPath) return;
180 | page.stop();
181 | ctx.unhandled = true;
182 | window.location = ctx.canonicalPath;
183 | }
184 |
185 | /**
186 | * Initialize a new "request" `Context`
187 | * with the given `path` and optional initial `state`.
188 | *
189 | * @param {String} path
190 | * @param {Object} state
191 | * @api public
192 | */
193 |
194 | function Context(path, state) {
195 | if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path;
196 | var i = path.indexOf('?');
197 |
198 | this.canonicalPath = path;
199 | this.path = path.replace(base, '') || '/';
200 |
201 | this.title = document.title;
202 | this.state = state || {};
203 | this.state.path = path;
204 | this.querystring = ~i ? path.slice(i + 1) : '';
205 | this.pathname = ~i ? path.slice(0, i) : path;
206 | this.params = [];
207 |
208 | // fragment
209 | this.hash = '';
210 | if (!~this.path.indexOf('#')) return;
211 | var parts = this.path.split('#');
212 | this.path = parts[0];
213 | this.hash = parts[1] || '';
214 | this.querystring = this.querystring.split('#')[0];
215 | }
216 |
217 | /**
218 | * Expose `Context`.
219 | */
220 |
221 | page.Context = Context;
222 |
223 | /**
224 | * Push state.
225 | *
226 | * @api private
227 | */
228 |
229 | Context.prototype.pushState = function(){
230 | history.pushState(this.state, this.title, this.canonicalPath);
231 | };
232 |
233 | /**
234 | * Save the context state.
235 | *
236 | * @api public
237 | */
238 |
239 | Context.prototype.save = function(){
240 | history.replaceState(this.state, this.title, this.canonicalPath);
241 | };
242 |
243 | /**
244 | * Initialize `Route` with the given HTTP `path`,
245 | * and an array of `callbacks` and `options`.
246 | *
247 | * Options:
248 | *
249 | * - `sensitive` enable case-sensitive routes
250 | * - `strict` enable strict matching for trailing slashes
251 | *
252 | * @param {String} path
253 | * @param {Object} options.
254 | * @api private
255 | */
256 |
257 | function Route(path, options) {
258 | options = options || {};
259 | this.path = path;
260 | this.method = 'GET';
261 | this.regexp = pathtoRegexp(path
262 | , this.keys = []
263 | , options.sensitive
264 | , options.strict);
265 | }
266 |
267 | /**
268 | * Expose `Route`.
269 | */
270 |
271 | page.Route = Route;
272 |
273 | /**
274 | * Return route middleware with
275 | * the given callback `fn()`.
276 | *
277 | * @param {Function} fn
278 | * @return {Function}
279 | * @api public
280 | */
281 |
282 | Route.prototype.middleware = function(fn){
283 | var self = this;
284 | return function(ctx, next){
285 | if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
286 | next();
287 | };
288 | };
289 |
290 | /**
291 | * Check if this route matches `path`, if so
292 | * populate `params`.
293 | *
294 | * @param {String} path
295 | * @param {Array} params
296 | * @return {Boolean}
297 | * @api private
298 | */
299 |
300 | Route.prototype.match = function(path, params){
301 | var keys = this.keys
302 | , qsIndex = path.indexOf('?')
303 | , pathname = ~qsIndex ? path.slice(0, qsIndex) : path
304 | , m = this.regexp.exec(decodeURIComponent(pathname));
305 |
306 | if (!m) return false;
307 |
308 | for (var i = 1, len = m.length; i < len; ++i) {
309 | var key = keys[i - 1];
310 |
311 | var val = 'string' == typeof m[i]
312 | ? decodeURIComponent(m[i])
313 | : m[i];
314 |
315 | if (key) {
316 | params[key.name] = undefined !== params[key.name]
317 | ? params[key.name]
318 | : val;
319 | } else {
320 | params.push(val);
321 | }
322 | }
323 |
324 | return true;
325 | };
326 |
327 | /**
328 | * Normalize the given path string,
329 | * returning a regular expression.
330 | *
331 | * An empty array should be passed,
332 | * which will contain the placeholder
333 | * key names. For example "/user/:id" will
334 | * then contain ["id"].
335 | *
336 | * @param {String|RegExp|Array} path
337 | * @param {Array} keys
338 | * @param {Boolean} sensitive
339 | * @param {Boolean} strict
340 | * @return {RegExp}
341 | * @api private
342 | */
343 |
344 | function pathtoRegexp(path, keys, sensitive, strict) {
345 | if (path instanceof RegExp) return path;
346 | if (path instanceof Array) path = '(' + path.join('|') + ')';
347 | path = path
348 | .concat(strict ? '' : '/?')
349 | .replace(/\/\(/g, '(?:/')
350 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
351 | keys.push({ name: key, optional: !! optional });
352 | slash = slash || '';
353 | return ''
354 | + (optional ? '' : slash)
355 | + '(?:'
356 | + (optional ? slash : '')
357 | + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
358 | + (optional || '');
359 | })
360 | .replace(/([\/.])/g, '\\$1')
361 | .replace(/\*/g, '(.*)');
362 | return new RegExp('^' + path + '$', sensitive ? '' : 'i');
363 | }
364 |
365 | /**
366 | * Handle "populate" events.
367 | */
368 |
369 | function onpopstate(e) {
370 | if (e.state) {
371 | var path = e.state.path;
372 | page.replace(path, e.state);
373 | }
374 | }
375 |
376 | /**
377 | * Handle "click" events.
378 | */
379 |
380 | function onclick(e) {
381 | if (1 != which(e)) return;
382 | if (e.metaKey || e.ctrlKey || e.shiftKey) return;
383 | if (e.defaultPrevented) return;
384 |
385 | // ensure link
386 | var el = e.target;
387 | while (el && 'A' != el.nodeName) el = el.parentNode;
388 | if (!el || 'A' != el.nodeName) return;
389 |
390 | // ensure non-hash for the same path
391 | var link = el.getAttribute('href');
392 | if (el.pathname == location.pathname && (el.hash || '#' == link)) return;
393 |
394 | // Check for mailto: in the href
395 | if (link.indexOf("mailto:") > -1) return;
396 |
397 | // check target
398 | if (el.target) return;
399 |
400 | // x-origin
401 | if (!sameOrigin(el.href)) return;
402 |
403 | // rebuild path
404 | var path = el.pathname + el.search + (el.hash || '');
405 |
406 | // same page
407 | var orig = path + el.hash;
408 |
409 | path = path.replace(base, '');
410 | if (base && orig == path) return;
411 |
412 | e.preventDefault();
413 | page.show(orig);
414 | }
415 |
416 | /**
417 | * Event button.
418 | */
419 |
420 | function which(e) {
421 | e = e || window.event;
422 | return null == e.which
423 | ? e.button
424 | : e.which;
425 | }
426 |
427 | /**
428 | * Check if `href` is the same origin.
429 | */
430 |
431 | function sameOrigin(href) {
432 | var origin = location.protocol + '//' + location.hostname;
433 | if (location.port) origin += ':' + location.port;
434 | return 0 == href.indexOf(origin);
435 | }
436 |
437 | /**
438 | * Expose `page`.
439 | */
440 |
441 | if ('undefined' == typeof module) {
442 | window.page = page;
443 | } else {
444 | module.exports = page;
445 | }
446 |
447 | })();
448 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/components/visionmedia-page.js/index.js:
--------------------------------------------------------------------------------
1 |
2 | ;(function(){
3 |
4 | /**
5 | * Perform initial dispatch.
6 | */
7 |
8 | var dispatch = true;
9 |
10 | /**
11 | * Base path.
12 | */
13 |
14 | var base = '';
15 |
16 | /**
17 | * Running flag.
18 | */
19 |
20 | var running;
21 |
22 | /**
23 | * Register `path` with callback `fn()`,
24 | * or route `path`, or `page.start()`.
25 | *
26 | * page(fn);
27 | * page('*', fn);
28 | * page('/user/:id', load, user);
29 | * page('/user/' + user.id, { some: 'thing' });
30 | * page('/user/' + user.id);
31 | * page();
32 | *
33 | * @param {String|Function} path
34 | * @param {Function} fn...
35 | * @api public
36 | */
37 |
38 | function page(path, fn) {
39 | //
40 | if ('function' == typeof path) {
41 | return page('*', path);
42 | }
43 |
44 | // route to
45 | if ('function' == typeof fn) {
46 | var route = new Route(path);
47 | for (var i = 1; i < arguments.length; ++i) {
48 | page.callbacks.push(route.middleware(arguments[i]));
49 | }
50 | // show with [state]
51 | } else if ('string' == typeof path) {
52 | page.show(path, fn);
53 | // start [options]
54 | } else {
55 | page.start(path);
56 | }
57 | }
58 |
59 | /**
60 | * Callback functions.
61 | */
62 |
63 | page.callbacks = [];
64 |
65 | /**
66 | * Get or set basepath to `path`.
67 | *
68 | * @param {String} path
69 | * @api public
70 | */
71 |
72 | page.base = function(path){
73 | if (0 == arguments.length) return base;
74 | base = path;
75 | };
76 |
77 | /**
78 | * Bind with the given `options`.
79 | *
80 | * Options:
81 | *
82 | * - `click` bind to click events [true]
83 | * - `popstate` bind to popstate [true]
84 | * - `dispatch` perform initial dispatch [true]
85 | *
86 | * @param {Object} options
87 | * @api public
88 | */
89 |
90 | page.start = function(options){
91 | options = options || {};
92 | if (running) return;
93 | running = true;
94 | if (false === options.dispatch) dispatch = false;
95 | if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false);
96 | if (false !== options.click) window.addEventListener('click', onclick, false);
97 | if (!dispatch) return;
98 | var url = location.pathname + location.search + location.hash;
99 | page.replace(url, null, true, dispatch);
100 | };
101 |
102 | /**
103 | * Unbind click and popstate event handlers.
104 | *
105 | * @api public
106 | */
107 |
108 | page.stop = function(){
109 | running = false;
110 | removeEventListener('click', onclick, false);
111 | removeEventListener('popstate', onpopstate, false);
112 | };
113 |
114 | /**
115 | * Show `path` with optional `state` object.
116 | *
117 | * @param {String} path
118 | * @param {Object} state
119 | * @param {Boolean} dispatch
120 | * @return {Context}
121 | * @api public
122 | */
123 |
124 | page.show = function(path, state, dispatch){
125 | var ctx = new Context(path, state);
126 | if (false !== dispatch) page.dispatch(ctx);
127 | if (!ctx.unhandled) ctx.pushState();
128 | return ctx;
129 | };
130 |
131 | /**
132 | * Replace `path` with optional `state` object.
133 | *
134 | * @param {String} path
135 | * @param {Object} state
136 | * @return {Context}
137 | * @api public
138 | */
139 |
140 | page.replace = function(path, state, init, dispatch){
141 | var ctx = new Context(path, state);
142 | ctx.init = init;
143 | if (null == dispatch) dispatch = true;
144 | if (dispatch) page.dispatch(ctx);
145 | ctx.save();
146 | return ctx;
147 | };
148 |
149 | /**
150 | * Dispatch the given `ctx`.
151 | *
152 | * @param {Object} ctx
153 | * @api private
154 | */
155 |
156 | page.dispatch = function(ctx){
157 | var i = 0;
158 |
159 | function next() {
160 | var fn = page.callbacks[i++];
161 | if (!fn) return unhandled(ctx);
162 | fn(ctx, next);
163 | }
164 |
165 | next();
166 | };
167 |
168 | /**
169 | * Unhandled `ctx`. When it's not the initial
170 | * popstate then redirect. If you wish to handle
171 | * 404s on your own use `page('*', callback)`.
172 | *
173 | * @param {Context} ctx
174 | * @api private
175 | */
176 |
177 | function unhandled(ctx) {
178 | var current = window.location.pathname + window.location.search;
179 | if (current == ctx.canonicalPath) return;
180 | page.stop();
181 | ctx.unhandled = true;
182 | window.location = ctx.canonicalPath;
183 | }
184 |
185 | /**
186 | * Initialize a new "request" `Context`
187 | * with the given `path` and optional initial `state`.
188 | *
189 | * @param {String} path
190 | * @param {Object} state
191 | * @api public
192 | */
193 |
194 | function Context(path, state) {
195 | if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path;
196 | var i = path.indexOf('?');
197 |
198 | this.canonicalPath = path;
199 | this.path = path.replace(base, '') || '/';
200 |
201 | this.title = document.title;
202 | this.state = state || {};
203 | this.state.path = path;
204 | this.querystring = ~i ? path.slice(i + 1) : '';
205 | this.pathname = ~i ? path.slice(0, i) : path;
206 | this.params = [];
207 |
208 | // fragment
209 | this.hash = '';
210 | if (!~this.path.indexOf('#')) return;
211 | var parts = this.path.split('#');
212 | this.path = parts[0];
213 | this.hash = parts[1] || '';
214 | this.querystring = this.querystring.split('#')[0];
215 | }
216 |
217 | /**
218 | * Expose `Context`.
219 | */
220 |
221 | page.Context = Context;
222 |
223 | /**
224 | * Push state.
225 | *
226 | * @api private
227 | */
228 |
229 | Context.prototype.pushState = function(){
230 | history.pushState(this.state, this.title, this.canonicalPath);
231 | };
232 |
233 | /**
234 | * Save the context state.
235 | *
236 | * @api public
237 | */
238 |
239 | Context.prototype.save = function(){
240 | history.replaceState(this.state, this.title, this.canonicalPath);
241 | };
242 |
243 | /**
244 | * Initialize `Route` with the given HTTP `path`,
245 | * and an array of `callbacks` and `options`.
246 | *
247 | * Options:
248 | *
249 | * - `sensitive` enable case-sensitive routes
250 | * - `strict` enable strict matching for trailing slashes
251 | *
252 | * @param {String} path
253 | * @param {Object} options.
254 | * @api private
255 | */
256 |
257 | function Route(path, options) {
258 | options = options || {};
259 | this.path = path;
260 | this.method = 'GET';
261 | this.regexp = pathtoRegexp(path
262 | , this.keys = []
263 | , options.sensitive
264 | , options.strict);
265 | }
266 |
267 | /**
268 | * Expose `Route`.
269 | */
270 |
271 | page.Route = Route;
272 |
273 | /**
274 | * Return route middleware with
275 | * the given callback `fn()`.
276 | *
277 | * @param {Function} fn
278 | * @return {Function}
279 | * @api public
280 | */
281 |
282 | Route.prototype.middleware = function(fn){
283 | var self = this;
284 | return function(ctx, next){
285 | if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
286 | next();
287 | };
288 | };
289 |
290 | /**
291 | * Check if this route matches `path`, if so
292 | * populate `params`.
293 | *
294 | * @param {String} path
295 | * @param {Array} params
296 | * @return {Boolean}
297 | * @api private
298 | */
299 |
300 | Route.prototype.match = function(path, params){
301 | var keys = this.keys
302 | , qsIndex = path.indexOf('?')
303 | , pathname = ~qsIndex ? path.slice(0, qsIndex) : path
304 | , m = this.regexp.exec(decodeURIComponent(pathname));
305 |
306 | if (!m) return false;
307 |
308 | for (var i = 1, len = m.length; i < len; ++i) {
309 | var key = keys[i - 1];
310 |
311 | var val = 'string' == typeof m[i]
312 | ? decodeURIComponent(m[i])
313 | : m[i];
314 |
315 | if (key) {
316 | params[key.name] = undefined !== params[key.name]
317 | ? params[key.name]
318 | : val;
319 | } else {
320 | params.push(val);
321 | }
322 | }
323 |
324 | return true;
325 | };
326 |
327 | /**
328 | * Normalize the given path string,
329 | * returning a regular expression.
330 | *
331 | * An empty array should be passed,
332 | * which will contain the placeholder
333 | * key names. For example "/user/:id" will
334 | * then contain ["id"].
335 | *
336 | * @param {String|RegExp|Array} path
337 | * @param {Array} keys
338 | * @param {Boolean} sensitive
339 | * @param {Boolean} strict
340 | * @return {RegExp}
341 | * @api private
342 | */
343 |
344 | function pathtoRegexp(path, keys, sensitive, strict) {
345 | if (path instanceof RegExp) return path;
346 | if (path instanceof Array) path = '(' + path.join('|') + ')';
347 | path = path
348 | .concat(strict ? '' : '/?')
349 | .replace(/\/\(/g, '(?:/')
350 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
351 | keys.push({ name: key, optional: !! optional });
352 | slash = slash || '';
353 | return ''
354 | + (optional ? '' : slash)
355 | + '(?:'
356 | + (optional ? slash : '')
357 | + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
358 | + (optional || '');
359 | })
360 | .replace(/([\/.])/g, '\\$1')
361 | .replace(/\*/g, '(.*)');
362 | return new RegExp('^' + path + '$', sensitive ? '' : 'i');
363 | }
364 |
365 | /**
366 | * Handle "populate" events.
367 | */
368 |
369 | function onpopstate(e) {
370 | if (e.state) {
371 | var path = e.state.path;
372 | page.replace(path, e.state);
373 | }
374 | }
375 |
376 | /**
377 | * Handle "click" events.
378 | */
379 |
380 | function onclick(e) {
381 | if (1 != which(e)) return;
382 | if (e.metaKey || e.ctrlKey || e.shiftKey) return;
383 | if (e.defaultPrevented) return;
384 |
385 | // ensure link
386 | var el = e.target;
387 | while (el && 'A' != el.nodeName) el = el.parentNode;
388 | if (!el || 'A' != el.nodeName) return;
389 |
390 | // ensure non-hash for the same path
391 | var link = el.getAttribute('href');
392 | if (el.pathname == location.pathname && (el.hash || '#' == link)) return;
393 |
394 | // Check for mailto: in the href
395 | if (link.indexOf("mailto:") > -1) return;
396 |
397 | // check target
398 | if (el.target) return;
399 |
400 | // x-origin
401 | if (!sameOrigin(el.href)) return;
402 |
403 | // rebuild path
404 | var path = el.pathname + el.search + (el.hash || '');
405 |
406 | // same page
407 | var orig = path + el.hash;
408 |
409 | path = path.replace(base, '');
410 | if (base && orig == path) return;
411 |
412 | e.preventDefault();
413 | page.show(orig);
414 | }
415 |
416 | /**
417 | * Event button.
418 | */
419 |
420 | function which(e) {
421 | e = e || window.event;
422 | return null == e.which
423 | ? e.button
424 | : e.which;
425 | }
426 |
427 | /**
428 | * Check if `href` is the same origin.
429 | */
430 |
431 | function sameOrigin(href) {
432 | var origin = location.protocol + '//' + location.hostname;
433 | if (location.port) origin += ':' + location.port;
434 | return 0 == href.indexOf(origin);
435 | }
436 |
437 | /**
438 | * Expose `page`.
439 | */
440 |
441 | if ('undefined' == typeof module) {
442 | window.page = page;
443 | } else {
444 | module.exports = page;
445 | }
446 |
447 | })();
448 |
--------------------------------------------------------------------------------
/lib/elem.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Elem
3 | */
4 |
5 | module.exports = Elem;
6 |
7 | var fs = require('fs');
8 | var path = require('path');
9 | var mkdirp = require('mkdirp');
10 | var rmdir = require('rimraf');
11 | var basename = path.basename;
12 | var dirname = path.dirname;
13 | var resolve = path.resolve;
14 | var exists = fs.existsSync || path.existsSync;
15 | var serveStatic = require('serve-static')
16 | var util = require('./util');
17 | var uglify = require('uglify-js');
18 | var glob = require('glob');
19 | var uid = require('uid');
20 |
21 |
22 | /**
23 | * Autoloaded file extensions
24 | */
25 | var autoload = ['.css', '.html', '.js'];
26 |
27 | /**
28 | * Elem
29 | */
30 | function Elem(sourceDir, options) {
31 | if (!(this instanceof Elem)) {
32 | return new Elem(sourceDir, options);
33 | }
34 |
35 | // Default options
36 | options = options || {};
37 |
38 | // Allow either Elem(opts) or Elem(src,opts)
39 | if (typeof sourceDir === 'object') {
40 | options = sourceDir;
41 | }
42 | else {
43 | options.sourceDir = sourceDir;
44 | }
45 |
46 | this.sourceDir = options.sourceDir || '.';
47 | this.buildDir = options.buildDir || path.join(this.sourceDir, '_build');
48 | this.converters = require('./converters');
49 | this.production = options.production;
50 | this.tagName = options.tagName || path.basename(this.sourceDir);
51 |
52 | /**
53 | * Information about the last build, per source file.
54 | *
55 | * Particularly, the last-modified file of the source file
56 | * at the time we last built it.
57 | *
58 | * This allows us to determine if the source needs to be
59 | * rebuilt by checking if the last-modified is different.
60 | */
61 | this.lastBuild = {};
62 |
63 |
64 | /**
65 | * A filename for persisting lastBuild to disk.
66 | */
67 | this.lastBuildFilename = path.join(this.buildDir + "/last_build.json");
68 |
69 | /**
70 | * Try to load lastBuild from disk.
71 | */
72 | try {
73 | this.lastBuild = JSON.parse( fs.readFileSync( this.lastBuildFilename ) );
74 | } catch (e) {
75 | if (e.code !== 'ENOENT') {
76 | throw e;
77 | }
78 | this.lastBuild = {};
79 | }
80 |
81 | }
82 |
83 | /**
84 | * Middleware that returns an empty page
85 | * with nothing but the loader.js and an
86 | * the elem on it.
87 | *
88 | * Example:
89 | * var app = Elem('./app');
90 | * server.use('/app', app.loader());
91 | * server.get('*', app.boot('/app'));
92 | *
93 | * @param {String} elemuri The URI of the elem to boot
94 | */
95 |
96 | Elem.prototype.boot = function(elemuri) {
97 | var self = this;
98 | var src = path.join(elemuri, 'loader.js');
99 |
100 | return function(req, res, next) {
101 | var html = '<'+self.tagName+'>'+self.tagName+'>';
102 |
103 | res.setHeader('Content-Type', 'text/html');
104 | res.end(html);
105 | }
106 | }
107 |
108 | /**
109 | * Generate a loader.js
110 | *
111 | * @param {String} doman domain
112 | * @param {String} basepath Base URL that serves _build/
113 | * @returns {String}
114 | */
115 | Elem.prototype.generateLoaderJS = function(domain, basePath, mode) {
116 | var srcfile = path.join(__dirname,'../boot/loader.js')
117 | var src = ''+fs.readFileSync(srcfile);
118 |
119 | if(this.production) {
120 | src = uglify.minify(src, {fromString:true}).code;
121 | }
122 |
123 | mode = mode || (this.production ? 'production' : 'development');
124 | basePath = basePath || '/';
125 |
126 | // Make sure the basePath ends in a slash
127 | if(basePath[basePath.length-1] != '/') {
128 | basePath += '/';
129 | }
130 |
131 | var config = {
132 | domain: domain,
133 | basePath: basePath,
134 | mode: mode,
135 | index: this.lastBuild.index,
136 | buildId: this.lastBuild.id,
137 | };
138 |
139 | var starter = '\n\nelem.start('+JSON.stringify(config)+');\n';
140 |
141 | src += starter;
142 |
143 | // console.log(src);
144 | return src;
145 | }
146 |
147 | /**
148 | * Generate and write loader.js
149 | */
150 | Elem.prototype.buildLoader = function(domain, basepath) {
151 | src = this.generateLoaderJS(domain, basepath)
152 | out = path.join(this.buildDir, 'loader.js');
153 | fs.writeFileSync(out, src);
154 | }
155 |
156 | /**
157 | * Generates a simple index.html
158 | * so elem can be used as a static site generator.
159 | *
160 | * Every build gets this put in for convenience.
161 | */
162 | Elem.prototype.buildStaticSiteBootstrap = function(basepath) {
163 | var src, out;
164 |
165 | // index.html
166 | src = ''
167 | out = path.join(this.buildDir, 'index.html');
168 | fs.writeFileSync(out, src);
169 | }
170 |
171 |
172 | /**
173 | * Middleware for serving elements
174 | */
175 |
176 | Elem.prototype.loader = function(opts) {
177 | var self = this;
178 | opts = opts || {};
179 |
180 | this.production = !!opts.production;
181 |
182 | self.build();
183 | this.built = true;
184 |
185 | return function(req,res,next) {
186 | if(req.path == '/loader.js') {
187 |
188 | // In development mode
189 | // rebuild every request to loader
190 | if(!opts.production) {
191 | self.build();
192 | this.loaderValid = false;
193 | }
194 |
195 | if(!this.loaderValid) {
196 | var domain = opts.domain ? opts.domain : req.protocol + '://' + req.get('host');
197 | var basepath = path.dirname(req.originalUrl);
198 | self.buildLoader(domain, basepath);
199 | this.loaderValid = true;
200 | }
201 | }
202 |
203 | handleFiles();
204 |
205 | function handleFiles() {
206 | function setHeaders(res) {
207 | if (opts.S_MaxAge) {
208 | res.setHeader('Control-Cache', 's-maxage=' + opts.S_MaxAge);
209 | }
210 | };
211 | serveStatic(self.buildDir, {setHeaders: setHeaders})(req, res, next);
212 | }
213 | };
214 | }
215 |
216 | /**
217 | * Create an assets.json pack from a collection of built files
218 | *
219 | * @param {Array} files (relative t
220 | * o buildDir)
221 | */
222 |
223 | Elem.prototype.pack = function(files) {
224 | var result = {};
225 |
226 | files.forEach(function(file) {
227 | result[file] = ''+fs.readFileSync(path.join(this.buildDir, file));
228 | }, this);
229 |
230 | return JSON.stringify(result);
231 | }
232 |
233 | /**
234 | * Check if a source file is outdated
235 | *
236 | * @param {String} filei Input file path
237 | * @returns {Boolean}
238 | */
239 |
240 | Elem.prototype.isOutdated = function(filei) {
241 |
242 | var mtime = ''+fs.statSync(filei).mtime;
243 |
244 | this.lastBuild.mtimes = this.lastBuild.mtimes || {};
245 |
246 | if(this.lastBuild.mtimes[filei] == mtime) {
247 | return false;
248 | }
249 |
250 |
251 | return true;
252 | }
253 |
254 |
255 | /**
256 | * Add a pre-processor to the build process
257 | * for a particular set of double file extensions.
258 | *
259 | * @param {String} exts ".to.from" extension matcher
260 | */
261 |
262 | Elem.prototype.prep = function(exts, fn) {
263 | this.converters[exts] = fn;
264 | }
265 |
266 |
267 | /**
268 | * Generate a build path
269 | * from a source file path.
270 | *
271 | * Examples:
272 | * a/b.js.html
273 | * _build/a/b.js
274 | *
275 | * @param {String} filei Input file path
276 | * @returns {String} The build file path
277 | */
278 |
279 | Elem.prototype.getBuildPath = function(file, trimExt) {
280 | var sourceDir = this.sourceDir;
281 |
282 | file = path.normalize(file);
283 |
284 | if(trimExt && path.basename(file).split('.').length > 2) {
285 | file = file.split('.').slice(0,-1).join('.');
286 | }
287 |
288 | file = path.join(this.buildDir, path.relative(sourceDir,file));
289 |
290 | return path.normalize(file);
291 | }
292 |
293 | /**
294 | * Check if a file is outdated and rebuild it.
295 | *
296 | * It first attempts to convert anything
297 | * that has a converter. Otherwise it creates a
298 | * symlink to the original.
299 | *
300 | * Only rebuilds when the source has been modified
301 | * since the last file it was built.
302 | *
303 | * @param {String} filei Input file path
304 | * @returns {String} The built file or symlink path
305 | */
306 |
307 | Elem.prototype.buildFile = function(filei) {
308 | var sourceDir = this.sourceDir;
309 |
310 | var stat = fs.statSync(filei)
311 |
312 | if(!filei || stat.isDirectory())
313 | return false;
314 |
315 | // Extract preprocessor directive from file extensions
316 | var ext = util.last2ext(filei);
317 |
318 | // Find converter for extension
319 | var converter = this.converters[ext];
320 |
321 | // Get build path, trim extension if converter available
322 | var fileo = this.getBuildPath(filei, !!converter);
323 |
324 | // If not outdated do nothing
325 | if(!this.isOutdated(filei)) {
326 | return fileo;
327 | }
328 |
329 | // Ensure dirs exist
330 | var dir = path.dirname(fileo);
331 | mkdirp.sync(dir);
332 |
333 | // Read in data from source
334 | var data = fs.readFileSync(filei);
335 |
336 | // If converter, apply it to data
337 | if (converter) {
338 | data = converter.call(this, data, filei);
339 | }
340 |
341 | // Write the output
342 | fs.writeFileSync(fileo, data);
343 |
344 | // Remember the last-modify-time associated with this build
345 | this.lastBuild.mtimes = this.lastBuild.mtimes || {};
346 | var mtime = ''+fs.statSync(filei).mtime;
347 | this.lastBuild.mtimes[filei] = mtime;
348 |
349 | console.log('built', path.relative(this.buildDir, fileo));
350 |
351 | return fileo;
352 | }
353 |
354 | Elem.prototype.parseComponent = function(fname) {
355 |
356 | var json = JSON.parse(fs.readFileSync(fname));
357 | var dir = path.dirname(fname);
358 |
359 | var main = json.main || 'index.js';
360 | var name = json.name;
361 |
362 | main = path.join(dir,main);
363 | main = path.relative(this.sourceDir, main);
364 |
365 | return {
366 | name: name,
367 | main: main
368 | };
369 | }
370 |
371 | /**
372 | * Deletes the build dir
373 | */
374 | Elem.prototype.clean = function() {
375 | rmdir.sync(this.buildDir);
376 | this.lastBuild = {};
377 | }
378 |
379 |
380 | /**
381 | * Builds everything
382 | */
383 | Elem.prototype.build = function() {
384 | var sourceDir = this.sourceDir;
385 | var self = this;
386 |
387 | // Only build once in production mode.
388 | if (this.production && fs.existsSync(this.lastBuildFilename) ) {
389 | console.log('Existing elem build detected. Using it.');
390 | return;
391 | }
392 |
393 | var result = [];
394 | var modules = {};
395 | var buildDir = this.buildDir;
396 |
397 | // Recursively find all files
398 | var files = glob.sync(sourceDir+"/**");
399 |
400 | if(this.production && !this.cleaned) {
401 | this.clean();
402 | this.cleaned = true;
403 | }
404 |
405 | // Filter out files we don't want
406 | // 1. Directories
407 | // 2. Anything that begins with `_` or '.'
408 | // 3. Anything empty
409 | files = files.filter(function(fname) {
410 | if(fname.match(/\/_/)) return false;
411 |
412 | if(!fname.trim()) return false;
413 |
414 | if(fs.statSync(fname).isDirectory()) {
415 | return false;
416 | }
417 |
418 | return true;
419 | });
420 |
421 |
422 | files = files.filter(function(fname) {
423 | // If a component.json file
424 | if(fname.match(/component\.json$/)) {
425 | // If within a components/ folder
426 | if(fname.match(/components\//)) {
427 | var component = self.parseComponent(fname);
428 | modules[component.name] = component.main;
429 | }
430 |
431 | // Never serve component.json
432 | return false;
433 | }
434 |
435 | return true;
436 | });
437 |
438 | files = files.map( function(fname) {
439 | return self.buildFile(fname)
440 | });
441 |
442 |
443 | // Remove any non-autoload extention
444 | files = files.filter( function(fname) {
445 | var ext = path.extname(fname);
446 | return autoload.indexOf(ext) != -1;
447 | });
448 |
449 | // Remove empties
450 | files = files.filter(function(fname) {
451 | return fname;
452 | });
453 |
454 | // Remove duplicates
455 | files = files.filter(function(item, i) {
456 | return files.indexOf(item) == i;
457 | })
458 |
459 | // Convert to relative paths
460 | files = files.map(function(fname) {
461 | return path.relative(self.buildDir, fname);
462 | });
463 |
464 | // Sort lexicographically
465 | files = files.sort(function(a,b) {
466 | return a.localeCompare(b);
467 | });
468 |
469 | var index = {
470 | files: files,
471 | modules: modules,
472 | packages: {}
473 | };
474 |
475 |
476 | if(this.production) {
477 | // Build asset packages
478 | var dir = this.buildDir;
479 |
480 | if(!dir) return;
481 |
482 | // The resulting asset file we are
483 | // trying to build
484 | var assetfile = dir + '/assets.json';
485 |
486 | mkdirp.sync(dir);
487 |
488 | // Find all files under this directory
489 | // AND any files w/ the directory name.
490 | // i.e. sourceDir/body.html
491 | // sourceDir/body/index.js
492 | var files = glob.sync(dir+"{/**,*}")
493 | files = files.filter(function(f) {
494 | if(fs.statSync(f).isDirectory()) {
495 | return false;
496 |
497 | }
498 | return true;
499 | });
500 |
501 | // Make all paths relative to _build
502 | files = files.map(function(fname) {
503 | return path.relative(self.buildDir, fname);
504 | });
505 |
506 | // Filter out any assets which
507 | // are not in the index
508 | files = files.filter(function(fname) {
509 | return index.files.indexOf(fname) !== -1
510 | });
511 |
512 | var json = self.pack(files);
513 | fs.writeFileSync(assetfile, json);
514 |
515 | var relassetfile = path.relative(self.buildDir, assetfile);
516 |
517 | index.files.push(relassetfile);
518 |
519 | files.forEach(function(fname) {
520 | index.packages[fname] = relassetfile;
521 | });
522 | }
523 |
524 | this.lastBuild.id = uid();
525 | this.lastBuild.index = index;
526 |
527 | // Ensure buildDir exists
528 | mkdirp.sync(this.buildDir);
529 |
530 | // Write last_build.json
531 | fs.writeFileSync(this.lastBuildFilename,
532 | JSON.stringify(this.lastBuild));
533 |
534 | this.buildStaticSiteBootstrap();
535 | }
536 |
537 | /**
538 | * Simulate using JSDOM
539 | */
540 | Elem.prototype.simulate = function(html, globals) {
541 | var self = this;
542 | var result = {};
543 | var jsdom = require('jsdom');
544 |
545 | html = html || '<'+self.tagName+'>'+self.tagName+'>';
546 |
547 | var js = this.generateLoaderJS(null, this.buildDir, 'test');
548 | var doc = jsdom.jsdom(html);
549 | var el = doc.body.firstElementChild;
550 |
551 | var window = doc.defaultView;
552 | window.nodeRequire = require;
553 | window.env = 'test'
554 | window.console = console;
555 |
556 | for (var k in globals) {
557 | window[k] = globals[k];
558 | }
559 |
560 | var vm = require('vm');
561 | var script = new vm.Script(js, {filename: 'boot/loader.js'});
562 |
563 | jsdom.evalVMScript(window, script);
564 |
565 | return el;
566 | }
567 |
--------------------------------------------------------------------------------
/examples/helloworld/_build/loader.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var elem = window.elem = {};
3 |
4 | /**
5 | * Root directory of all elements
6 | */
7 | var root = new Dir('/');
8 |
9 | elem.root = root;
10 |
11 | /**
12 | * Environment - "production" or "development"
13 | */
14 | var env = 'development';
15 |
16 | function basedir(filename) {
17 | return filename.split('/').slice(0,-1).join('/') + '/'
18 | }
19 |
20 | function ppcss(css, filename) {
21 | if(env == 'development') {
22 | var link = document.createElement('link');
23 | link.rel = 'stylesheet';
24 | link.href = filename;
25 | document.head.appendChild(link);
26 | }
27 | else {
28 | var dir = basedir(filename);
29 | css = css.replace(/(@import\s*['?"?])([^\/|.*?\/\/])/g, '$1'+dir+'$2')
30 | css = css.replace(/(url\(['?"?])([^\/|.*?\/\/|#|data:])/g, '$1'+dir+'$2')
31 |
32 | var style = document.createElement('style');
33 | style.innerHTML = css;
34 | document.head.appendChild(style);
35 | }
36 |
37 | return css;
38 | }
39 |
40 | function pphtml(html) {
41 |
42 | var dir = html.parent;
43 |
44 | html.data = '\n' + html.data;
45 |
46 | if(!(dir instanceof Dir)) return html.data;
47 |
48 | return html.data.replace(/\.\//m, dir.path)
49 |
50 |
51 | return html.data;
52 | }
53 |
54 | function ppjade(name, src) {
55 | all[name].jade = src;
56 | }
57 |
58 | function attr2json(el) {
59 | var result = {};
60 | // var nodes=[], values=[];
61 | for (var attr, i=0, attrs=el.attributes, l=attrs.length; i\n' + html;
100 | elem.innerHTML = html;
101 | }
102 | else if(typeof html === 'object'
103 | && html.length) {
104 | elem.innerHTML = '';
105 | for(var i=0,l=html.length; i\n' + html.data;
70 |
71 | if(!(dir instanceof Dir)) return html.data;
72 |
73 | return html.data.replace(/\.\//m, dir.path)
74 |
75 | return html.data;
76 | }
77 |
78 | /**
79 | * TODO: This needs to be able to handle conflicting
80 | * names in different directories.
81 | */
82 | function findAssociatedDir(node) {
83 | return elem.allTags[node.tagName];
84 | }
85 |
86 | /**
87 | * Apply self-named resources to an element.
88 | *
89 | * e.g. Within directory page/, applies page.js if present
90 | *
91 | * If Javascript is found it is used to fill out an "exports" object on the directory.
92 | * If HTML is found it is used as the innerHTML ONLY if there is there are no existing children.
93 | * If CSS is found it is linked to the document head.
94 | *
95 | * @param {DOMElement} The target element to enhance
96 | * @param {Dir} dir Custom element directory
97 | * @param {Function} done Callback
98 | */
99 | function enhance(elem, dir) {
100 | if (!elem) {
101 | return false;
102 | }
103 |
104 | // Always enhance once
105 | if (elem.__elem_enhanced) {
106 | return true;
107 | }
108 |
109 | dir = dir || findAssociatedDir(elem);
110 | if (!dir) return false;
111 |
112 | // If removed from the dom after we
113 | // scheduled it for enhancement, cancel
114 | if (!elem.ownerDocument.contains(elem)) {
115 | return;
116 | }
117 |
118 | elem.__elem_enhanced = dir;
119 |
120 | function rescan(node) {
121 | // Re-scan this element against
122 | // ancestor directories
123 | // The impl could have introduced
124 | // new matchable elements.
125 | var node = elem;
126 | while(node) {
127 | var pdir = node.__elem_enhanced;
128 | if(pdir) {
129 | scan(elem, pdir);
130 | }
131 | node = node.parentElement;
132 | }
133 |
134 | // And root
135 | scan(elem, root);
136 | }
137 |
138 | function render(html) {
139 | if(html) {
140 | if(html instanceof Element) {
141 | elem.innerHTML = '';
142 | elem.appendChild( html );
143 | }
144 | else if(typeof html === 'string') {
145 | html = '\n' + html;
146 | elem.innerHTML = html;
147 | }
148 | else if(typeof html === 'object'
149 | && html.length) {
150 | elem.innerHTML = '';
151 | for(var i=0,l=html.length; i