├── 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+'>'; 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+'>'; 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