elem
Version:
An asset manager based on custom elements
858 lines (676 loc) • 18.6 kB
JavaScript
(function() {
var elem = window.elem = {};
/**
* Root directory of all elements
*/
var root = new Dir('/');
elem.root = root;
/**
* Environment - "production" or "development"
*/
var env = 'development';
function basedir(filename) {
return filename.split('/').slice(0,-1).join('/') + '/'
}
function ppcss(css, filename) {
if(env == 'development') {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = filename;
document.head.appendChild(link);
}
else {
var dir = basedir(filename);
css = css.replace(/(@import\s*['?"?])([^\/|.*?\/\/])/g, '$1'+dir+'$2')
css = css.replace(/(url\(['?"?])([^\/|.*?\/\/|#|data:])/g, '$1'+dir+'$2')
var style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
}
return css;
}
function pphtml(html) {
var dir = html.parent;
html.data = '<!-- '+html.path+' -->\n' + html.data;
if(!(dir instanceof Dir)) return html.data;
return html.data.replace(/\.\//m, dir.path)
return html.data;
}
function ppjade(name, src) {
all[name].jade = src;
}
function attr2json(el) {
var result = {};
// var nodes=[], values=[];
for (var attr, i=0, attrs=el.attributes, l=attrs.length; i<l; i++){
attr = attrs.item(i)
result[attr.nodeName] = attr.nodeValue;
// nodes.push(attr.nodeName);
// values.push(attr.nodeValue);
}
return result;
}
/**
* Apply self-named resources to an element.
*
* e.g. Within directory page/, applies page.js if present
*
* If Javascript is found it is used to fill out an "exports" object on the directory.
* If HTML is found it is used as the innerHTML ONLY if there is there are no existing children.
* If CSS is found it is linked to the document head.
*
* @param {DOMElement} The target element to enhance
* @param {Dir} dir Custom element directory
* @param {Function} done Callback
*/
function enhance(elem, dir, done) {
if(elem.__elem_enhanced) {
return;
}
elem.__elem_enhanced = dir;
function implDone(html) {
if(html) {
if(html instanceof Element) {
elem.innerHTML = '';
elem.appendChild( html );
}
else if(typeof html === 'string') {
html = '<!-- generated by '+dir.path+' -->\n' + html;
elem.innerHTML = html;
}
else if(typeof html === 'object'
&& html.length) {
elem.innerHTML = '';
for(var i=0,l=html.length; i<l; ++i) {
elem.appendChild( html[i] );
}
}
}
// Re-scan this element against
// ancestor directories
// The impl could have introduced
// new matchable elements.
var node = elem;
while(node) {
var pdir = node.__elem_enhanced;
if(pdir) {
scan(elem, pdir);
}
node = node.parentElement;
}
// And root
scan(elem, root);
}
var html = require(dir.path,'html');
if(html) {
elem.innerHTML = html;
}
var impl = require(dir.path,'js');
if(impl) {
if(typeof impl === 'function') {
if(impl.length == 0) {
var html = impl.call(elem);
implDone(html);
}
else if(impl.length == 2) {
impl.call(elem, {deprecated: 'deprecated'}, function(err,html) {
implDone(err, html);
});
}
else {
impl.call(elem, function(err,html) {
implDone(err, html);
});
}
}
else {
implDone();
}
}
else {
implDone();
}
}
/**
* Searches a base element for instances of custom elements,
* loads the resources, and then calls enhance().
*
* @param {DOMElement} base The root element to search within
* @param {Dir} dir Custom element directory
* @param {Function} done Callback
*/
function scan(base, dir) {
var uses = dir.findAll(base);
// uses = uses.sort(function(a,b) {
// return b.compareDocumentPosition(a)
// });
each(uses, function(elem) {
var tagName = elem.tagName.toLowerCase();
var path = [];
var tmp = dir;
while(tmp) {
if(tmp[tagName]) {
path.unshift(tmp[tagName]);
}
tmp = tmp.parent;
}
// Must load all first an enhance in order
parallel(path, function() {
each(path, function(dir) {
enhance(elem, dir);
});
});
});
}
/**
* A simple XMLHttpRequest GET.
*
* @param {String} url URL to fetch
* @param {Function} done Callback
*/
function ajax(url,done) {
var xmlhttp;
if(window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest(); // Browsers
}
else {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); // IE
}
xmlhttp.onreadystatechange = function() {
if(xmlhttp.readyState == 4
&& xmlhttp.status == 200) {
done(null, xmlhttp.responseText)
}
}
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
/**
*
*/
function get(path,done) {
return ajax(root.path+path, done);
}
/**
* Load the index with AJAX.
*
* @param {Function} done Callback
*/
function loadIndex(done) {
get('index.json', function(err, data) {
if(err) {
console.error('Elem build index not found. Did you build it?');
return;
}
data = JSON.parse(data);
parseIndex(data);
done();
});
}
function select(base, tags) {
var elems = [];
each(tags, function(tag) {
var all = base.getElementsByTagName(tag);
for(var i=0,l=all.length; i < l; ++i) {
elems.push(all[i]);
}
});
return elems;
}
/**
* Simple parallel processor.
*/
function parallel(objs, done) {
var count = objs.length;
if(objs.length === 0) done();
each(objs, function(obj) {
obj.load(function() {
if(--count == 0) {
done();
}
});
});
}
/**
* Dir
*
* A local mapping of a server-side directory.
*
* @param {Dir} parent Parent directory object
* @param {String} path path of the directory
*/
function Dir(path, parent) {
this.parent = parent;
this.path = path;
this.availTags = [];
this.observers = [];
}
Dir.prototype.findAll = function(base) {
var tags = this.availTags;
if(this.parent) {
tags = tags.concat(this.parent.availTags);
}
return select(base, tags);
};
Dir.prototype.getSelector = function() {
var tags = this.availTags;
if(this.parent) {
tags = tags.concat(this.parent.availTags);
}
return tags.join(',');
};
Dir.prototype.observe = function(done) {
this.observers.push(done);
}
Dir.prototype.complete = function() {
if(this.loaded) return;
this.loaded = true;
this.loading = false;
this.observers.forEach(function(fn) {
fn();
});
this.observers = [];
}
Dir.prototype.children = function(recursive) {
var files = [];
for(var filename in this) {
if(filename == 'parent') continue;
var f = this[filename];
if(f instanceof File) {
files.push(this[filename]);
}
if(f instanceof Dir) {
if(recursive || filename == this.tagName) {
[].push.apply(files, f.children(recursive));
}
}
}
return files;
}
Dir.prototype.isLoaded = function(recursive) {
if(this.loaded) {
return true;
}
if(this.loading) {
this.loaded = this.children().reduce(function(a,b) {
return a && b.isLoaded()
}, true);
}
return this.loaded;
}
Dir.prototype.load = function(done, recursive) {
var self = this;
if(this.isLoaded()) {
done();
return;
}
this.observe(done);
if(this.loading) {
return;
}
this.loading = true;
var resources = [];
// FIXME
// We should not need to sort client-side
// Just do things in order of the index...
// This whole thing is a huge waste of bytes
if(this.window) {
[].push.apply(resources, this.window.children(true));
var self = this;
this.window.load(function() {
function runAll(dir) {
var globals = Object.keys(dir);
globals = globals.sort(function(a,b) {
return b.length < a.length ? 1 : -1;
});
each(globals, function(name) {
if(dir[name] instanceof File) {
var path = dir[name].path;
require(path,'js');
}
});
each(globals, function(name) {
if(name == 'parent') return;
if(dir[name] instanceof Dir) {
runAll(dir[name]);
}
});
}
runAll(self.window);
}, true);
}
if(this.components) {
[].push.apply(resources, this.components.children(true));
}
if(this.lib) {
[].push.apply(resources, this.lib.children(true));
}
[].push.apply(resources, this.children(true));
// if(this.parent && env === 'production') {
// if(this.path[this.path.length-1] == '/') debugger;
// // // if(this.path == "_build/site/heading") debugger;
// // // console.log(this.path);
// // get(this.path + '/assets.json', function(err, json) {
// // var assets = json.parse(json);
// // for(var k in assets) {
// // var file = file.map[k];
// // if(!file) {
// // console.warn('unexpected file in asset package', k);
// // continue;
// // }
// // file.data = assets[k];
// // file.complete();
// // // var parent = file.parent;
// // // while(parent) {
// // // parent.complete();
// // // parent = parent.parent;
// // // }
// // }
// // self.complete();
// // // self.loaded = true;
// // // self.loading = false;
// // // done();
// // });
// // }
// else {
parallel(resources, function() {
self.complete()
});
// }
};
function jsfn(txt, global) {
var fn;
if(global) {
fn = new Function(txt);
}
else {
fn = new Function('module','exports','require', txt);
}
return fn;
}
function normalize(path) {
var result = [];
var parts;
var token;
parts = path.split('/');
for(var i=0, l=parts.length; i < l; ++i) {
token = parts[i];
if (token === '..') {
result.pop();
} else if (token && token !== '.') {
result.push(token);
}
}
return result.join('/').replace(/[\/]{2,}/g, '/');
}
function resolve(base,rel) {
var basedir = base.split('/').slice(0,-1).join('/');
var pathname = [basedir,rel].join('/');
return normalize(pathname);
}
function isGlobal(dir) {
while(dir) {
if(dir.tagName === 'window')
return true;
dir = dir.parent;
}
return false;
}
/**
* Finds dependencies given a base just like node requires
* but with an abstract extension.
*
* Examples:
*
* require('../body','js','/elements/header/header.js')
* require('../body','html','/elements/header/header.js')
*
* @param {String} filename Module path
* @param {String} ext File extension to look for
* @param {String} basename Reference point for relative paths
*/
function require(filename, ext, basename) {
basename = basename || ".";
var relpath = resolve(basename, filename);
var lastname = relpath.split('/').filter(function(n){return n;}).slice(-1)[0] || '';
var file;
filename = filename.toLowerCase();
lastname = lastname.toLowerCase();
relpath = relpath.toLowerCase();
// Make a list of possible paths
var possible = [
relpath,
relpath+'.'+ext,
relpath+'/'+lastname+'.'+ext,
relpath+'/index.'+ext,
globalModules[filename]
];
// Try all of them in order
while(possible.length) {
var attempt = File.map[possible.pop()];
if(attempt) {
file = attempt;
break;
}
}
if(!file) {
return false;
}
if(file.tagName != 'js') {
// if(file.tagName == 'jquery') debugger;
return file.data;
}
file.data += '\n//@ sourceURL=elem::' + file.path;
var global = isGlobal(file);
var fn = jsfn(file.data, global);
if(global) {
fn();
return false;
}
// If we already executed return exports
if(file.module) return file.module.exports;
// Run for the first time and save exports
function localRequire(name, ext) {
ext = ext || 'js';
var dep = require(name, ext, file.path);
if(!dep) {
throw new Error("failed to require "+name+" from "+file.path);
}
return dep;
}
var module = {exports: {}};
file.module = module;
fn.call(window, module, module.exports, localRequire);
return module.exports;
}
/**
* File
*
* Local tracker object for a single remote file.
*
* @param {String} path The path of the file relative to global `base`
* @param {Dir} parent Parent dir object of the file
*/
function File(path, parent) {
this.observers = [];
this.loading = false;
this.loaded = false;
this.path = path;
this.parent = parent;
File.map[path.toLowerCase()] = this;
}
File.map = {};
File.prototype = {
handle: function() {
var file = this.path;
var ext = file.split('.').slice(-1)[0];
var handlers = {
html: pphtml,
jade: ppjade,
css: ppcss,
json: ppjson,
js: ppjs
};
if(handlers[ext]) {
handlers[ext](name, data, file);
}
}
, isLoaded: function() {
return this.loaded;
}
, complete: function() {
this.loading = false;
this.loaded = true;
var ext = this.path.split('.').slice(-1)[0];
if(ext == 'js') {
this.data = '\n// ' + this.path + '\n\n' + this.data;
}
if(ext == 'json') {
this.data = JSON.parse(this.data);
}
if(ext == 'css') {
ppcss(this.data, root.path+this.path);
}
if(ext == 'html') {
this.data = pphtml(this);
}
each(this.observers, function(done) {
done();
});
this.observers = [];
}
, observe: function(done) {
this.observers.push(done);
}
, load: function(done) {
var self = this;
if(this.loaded) {
done();
return;
}
// No need to ajax load
// css since we link it
if(env == 'development'
&& this.path.match(/\.css$/)) {
this.data = '';
self.complete();
done();
return;
}
this.observe(done);
if(this.loading) {
return;
}
this.loading = true;
var pkgpath = packages[this.path];
// css since we link it
if(pkgpath) {
var pkg = File.map[pkgpath];
pkg.load(function() {
self.data = pkg.data[self.path];
self.complete();
});
}
else {
get(self.path, function(err, data) {
self.data = data;
self.complete();
if(err) {
console.error('Problem loading ' + self.path);
return;
}
});
}
}
};
var globalModules = {};
var packages = {};
function parseIndex(json) {
var files = json.files;
var modules = json.modules;
globalModules = json.modules;
packages = json.packages;
var base = '/';
each(files, function(file) {
var nodes = file.split(/[\/|\.]/);
var numDirs = file.split('/').length+1;
var parent = root;
var parentName = null;
var dirpath = base;
each(nodes, function(node,i) {
// Space not allowed
// TODO make actual accepted symbols
if(node.match(/\s/)) return;
// Merge _ prefixed directories in
// to the parent directory
if(node && node[0] != '_') {
if(--numDirs) {
dirpath += node + '/';
}
if(i == nodes.length-1) {
var resource = new File(file, parent);
parent[node] = resource;
parent[node].tagName = node;
}
else {
var dir = parent[node] = parent[node] || new Dir(dirpath, parent);
dir.tagName = node;
// Old IEs needs this.
// It's a classic way of getting HTML5
// elements recognized.
// document.createElement(node);
if(parent.availTags.indexOf(node) == -1) {
parent.availTags.push(node);
}
}
parent = parent[node];
parentName = node;
}
});
});
}
var started = false;
elem.start = function(basepath, setenv, index) {
if(started) {
throw 'elem.start() called twice!';
}
started = true;
// Make sure the basepath ends in a slash
if(basepath[basepath.length-1] != '/')
basepath += '/';
root.path = basepath || '/';
env = setenv || 'development';
function loadRoot() {
root.load(function() {
domReady(function() {
scan(document, root);
});
});
}
if(index) {
parseIndex(index);
loadRoot();
}
else {
// Load index immediately
loadIndex(loadRoot);
}
}
// We don't support IE6 or 7. We can do a much simpler document ready check.
function domReady(callback) {
if (document.readyState !== "loading") return callback();
var addListener = document.addEventListener || document.attachEvent,
removeListener = document.removeEventListener || document.detachEvent
eventName = document.addEventListener ? "DOMContentLoaded" : "onreadystatechange"
addListener.call(document, eventName, function(){
removeListener.call(document, eventName, arguments.callee, false )
callback()
}, false )
}
function each(arr,fn) {
for(var i=0,l=arr.length;i<l;++i) {
fn(arr[i],i);
}
}
})();
elem.start("/","development",{"files":["body/body.html","hello/hello.js","components/visionmedia-page.js/index.js"],"modules":{"page":"components/visionmedia-page.js/index.js"},"packages":{}});