zoo-kernel
Version:
Based on fis-kernel for fis-zoo
503 lines (478 loc) • 18.9 kB
JavaScript
/*
* fis
* http://fis.baidu.com/
*/
;
var CACHE_DIR;
var exports = module.exports = function(file){
if(!CACHE_DIR){
fis.log.error('uninitialized compile cache directory.');
}
file = fis.file.wrap(file);
if(!file.realpath){
error('unable to compile [' + file.subpath + ']: Invalid file realpath.');
}
fis.log.debug('compile [' + file.realpath + '] start');
fis.emitter.emit('compile:start', file);
if(file.isFile()){
if(file.useCompile && file.ext && file.ext !== '.'){
var cache = file.cache = fis.cache(file.realpath, CACHE_DIR),
revertObj = {};
if(file.useCache && cache.revert(revertObj)){
exports.settings.beforeCacheRevert(file);
file.requires = revertObj.info.requires;
file.extras = revertObj.info.extras;
if(file.isText()){
revertObj.content = revertObj.content.toString('utf8');
}
file.setContent(revertObj.content);
exports.settings.afterCacheRevert(file);
} else {
exports.settings.beforeCompile(file);
file.setContent(fis.util.read(file.realpath));
process(file);
exports.settings.afterCompile(file);
revertObj = {
requires : file.requires,
extras : file.extras
};
cache.save(file.getContent(), revertObj);
}
} else {
file.setContent(file.isText() ? fis.util.read(file.realpath) : fis.util.fs.readFileSync(file.realpath));
}
} else if(file.useCompile && file.ext && file.ext !== '.'){
process(file);
}
if(exports.settings.hash && file.useHash){
file.getHash();
}
file.compiled = true;
fis.log.debug('compile [' + file.realpath + '] end');
fis.emitter.emit('compile:end', file);
embeddedUnlock(file);
return file;
};
exports.settings = {
unique : false,
debug : false,
optimize : false,
lint : false,
test : false,
hash : false,
domain : false,
beforeCacheRevert : function(){},
afterCacheRevert : function(){},
beforeCompile : function(){},
afterCompile : function(){}
};
exports.setup = function(opt){
var settings = exports.settings;
if(opt){
fis.util.map(settings, function(key){
if(typeof opt[key] !== 'undefined'){
settings[key] = opt[key];
}
});
}
CACHE_DIR = 'compile/';
if(settings.unique){
CACHE_DIR += Date.now() + '-' + Math.random();
} else {
CACHE_DIR += ''
+ (settings.debug ? 'debug' : 'release')
+ (settings.optimize ? '-optimize' : '')
+ (settings.hash ? '-hash' : '')
+ (settings.domain ? '-domain' : '');
}
return CACHE_DIR;
};
exports.clean = function(name){
if(name){
fis.cache.clean('compile/' + name);
} else if(CACHE_DIR) {
fis.cache.clean(CACHE_DIR);
} else {
fis.cache.clean('compile');
}
};
var map = exports.lang = (function(){
var keywords = ['require', 'embed', 'uri', 'dep', 'jsEmbed'],
LD = '<<<', RD = '>>>',
qLd = fis.util.escapeReg(LD),
qRd = fis.util.escapeReg(RD),
map = {
reg : new RegExp(
qLd + '(' + keywords.join('|') + '):([\\s\\S]+?)' + qRd,
'g'
)
};
keywords.forEach(function(key){
map[key] = {};
map[key]['ld'] = LD + key + ':';
map[key]['rd'] = RD;
});
return map;
})();
//"abc?__inline" return true
//"abc?__inlinee" return false
//"abc?a=1&__inline"" return true
function isInline(info){
return /[?&]__inline(?:[=&'"]|$)/.test(info.query);
}
//analyse [@require id] syntax in comment
function analyseComment(comment, callback){
var reg = /(@require\s+)('[^']+'|"[^"]+"|[^\s;!@#%^&*()]+)/g;
callback = callback || function(m, prefix, value){
return prefix + map.require.ld + value + map.require.rd;
};
return comment.replace(reg, callback);
}
//expand javascript
//[@require id] in comment to require resource
//__inline(path) to embedd resource content or base64 encodings
//__uri(path) to locate resource
//require(path) to require resource
function extJs(content, callback){
var reg = /"(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|(\/\/[^\r\n\f]+|\/\*[\s\S]*?(?:\*\/|$))|\b(__inline|__uri|require)\s*\(\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*')\s*\)/g;
callback = callback || function(m, comment, type, value){
if(type){
switch (type){
case '__inline':
m = map.jsEmbed.ld + value + map.jsEmbed.rd;
break;
case '__uri':
m = map.uri.ld + value + map.uri.rd;
break;
case 'require':
m = 'require(' + map.require.ld + value + map.require.rd + ')';
break;
}
} else if(comment){
m = analyseComment(comment);
}
return m;
};
return content.replace(reg, callback);
}
//expand css
//[@require id] in comment to require resource
//[@import url(path?__inline)] to embed resource content
//url(path) to locate resource
//url(path?__inline) to embed resource content or base64 encodings
//src=path to locate resource
function extCss(content, callback){
var reg = /(\/\*[\s\S]*?(?:\*\/|$))|(?:@import\s+)?\burl\s*\(\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|[^)}\s]+)\s*\)(\s*;?)|\bsrc\s*=\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|[^\s}]+)/g;
callback = callback || function(m, comment, url, last, filter){
if(url){
var key = isInline(fis.util.query(url)) ? 'embed' : 'uri';
if(m.indexOf('@') === 0){
if(key === 'embed'){
m = map.embed.ld + url + map.embed.rd + last.replace(/;$/, '');
} else {
m = '@import url(' + map.uri.ld + url + map.uri.rd + ')' + last;
}
} else {
m = 'url(' + map[key].ld + url + map[key].rd + ')' + last;
}
} else if(filter) {
m = 'src=' + map.uri.ld + filter + map.uri.rd;
} else if(comment) {
m = analyseComment(comment);
}
return m;
};
return content.replace(reg, callback);
}
//expand html
//[@require id] in comment to require resource
//<!--inline[path]--> to embed resource content
//<img|embed|audio|video|link|object ... (data-)?src="path"/> to locate resource
//<img|embed|audio|video|link|object ... (data-)?src="path?__inline"/> to embed resource content
//<script|style ... src="path"></script|style> to locate js|css resource
//<script|style ... src="path?__inline"></script|style> to embed js|css resource
//<script|style ...>...</script|style> to analyse as js|css
function extHtml(content, callback){
var reg = /(<script(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/script\s*>|$)|(<style(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/style\s*>|$)|<(img|embed|audio|video|link|object|source)\s+[\s\S]*?["'\s\w\/\-](?:>|$)|<!--inline\[([^\]]+)\]-->|<!--(?!\[)([\s\S]*?)(-->|$)/ig;
callback = callback || function(m, $1, $2, $3, $4, $5, $6, $7, $8){
if($1){//<script>
var embed = '';
$1 = $1.replace(/(\s(?:data-)?src\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value){
if(isInline(fis.util.query(value))){
embed += map.embed.ld + value + map.embed.rd;
return '';
} else {
return prefix + map.uri.ld + value + map.uri.rd;
}
});
if(embed){
//embed file
m = $1 + embed;
} else if(!/\s+type\s*=/i.test($1) || /\s+type\s*=\s*(['"]?)text\/javascript\1/i.test($1)) {
//without attrubite [type] or must be [text/javascript]
m = $1 + extJs($2);
} else {
//other type as html
m = $1 + extHtml($2);
}
} else if($3){//<style>
m = $3 + extCss($4);
} else if($5){//<img|embed|audio|video|link|object|source>
var tag = $5.toLowerCase();
if(tag === 'link'){
var inline = '', isCssLink = false, isImportLink = false;
var result = m.match(/\srel\s*=\s*('[^']+'|"[^"]+"|[^\s\/>]+)/i);
if(result && result[1]){
var rel = result[1].replace(/^['"]|['"]$/g, '').toLowerCase();
isCssLink = rel === 'stylesheet';
isImportLink = rel === 'import';
}
m = m.replace(/(\s(?:data-)?href\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(_, prefix, value){
if((isCssLink || isImportLink) && isInline(fis.util.query(value))){
if(isCssLink) {
inline += '<style' + m.substring(5).replace(/\/(?=>$)/, '').replace(/\s+(?:charset|href|data-href|hreflang|rel|rev|sizes|target)\s*=\s*(?:'[^']+'|"[^"]+"|[^\s\/>]+)/ig, '');
}
inline += map.embed.ld + value + map.embed.rd;
if(isCssLink) {
inline += '</style>';
}
return '';
} else {
return prefix + map.uri.ld + value + map.uri.rd;
}
});
m = inline || m;
} else if(tag === 'object'){
m = m.replace(/(\sdata\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value){
return prefix + map.uri.ld + value + map.uri.rd;
});
} else {
m = m.replace(/(\s(?:data-)?src\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value){
var key = isInline(fis.util.query(value)) ? 'embed' : 'uri';
return prefix + map[key]['ld'] + value + map[key]['rd'];
});
}
} else if($6){
m = map.embed.ld + $6 + map.embed.rd;
} else if($7){
m = '<!--' + analyseComment($7) + $8;
}
return m;
};
return content.replace(reg, callback);
}
function process(file){
if(file.useParser !== false){
pipe(file, 'parser', file.ext);
}
if(file.rExt){
if(file.usePreprocessor !== false){
pipe(file, 'preprocessor', file.rExt);
}
if(file.useStandard !== false){
standard(file);
}
if(file.usePostprocessor !== false){
pipe(file, 'postprocessor', file.rExt);
}
if(exports.settings.lint && file.useLint !== false){
pipe(file, 'lint', file.rExt, true);
}
if(exports.settings.test && file.useTest !== false){
pipe(file, 'test', file.rExt, true);
}
if(exports.settings.optimize && file.useOptimizer !== false){
pipe(file, 'optimizer', file.rExt);
}
}
}
function pipe(file, type, ext, keep){
var key = type + ext;
fis.util.pipe(key, function(processor, settings, key){
settings.filename = file.realpath;
var content = file.getContent();
try {
fis.log.debug('pipe [' + key + '] start');
var result = processor(content, file, settings);
fis.log.debug('pipe [' + key + '] end');
if(keep){
file.setContent(content);
} else if(typeof result === 'undefined'){
fis.log.warning('invalid content return of pipe [' + key + ']');
} else {
file.setContent(result);
}
} catch(e) {
//log error
fis.log.debug('pipe [' + key + '] fail');
var msg = key + ': ' + String(e.message || e.msg || e).trim() + ' [' + (e.filename || file.realpath);
if(e.hasOwnProperty('line')){
msg += ':' + e.line;
if(e.hasOwnProperty('col')){
msg += ':' + e.col;
} else if(e.hasOwnProperty('column')) {
msg += ':' + e.column;
}
}
msg += ']';
e.message = msg;
error(e);
}
});
}
var embeddedMap = {};
function error(msg){
//for watching, unable to exit
embeddedMap = {};
fis.log.error(msg);
}
function embeddedCheck(main, embedded){
main = fis.file.wrap(main).realpath;
embedded = fis.file.wrap(embedded).realpath;
if(main === embedded){
error('unable to embed file[' + main + '] into itself.');
} else if(embeddedMap[embedded]) {
var next = embeddedMap[embedded],
msg = [embedded];
while(next && next !== embedded){
msg.push(next);
next = embeddedMap[next];
}
msg.push(embedded);
error('circular dependency on [' + msg.join('] -> [') + '].');
}
embeddedMap[embedded] = main;
return true;
}
/**
* 获取 embed 根源文件,也就是根源的宿主文件。
* @author rsk
* @param embedded 当前被嵌入的文件
* @return source 根宿主文件(成功) 或者 false(失败)
*/
function embeddedSource(embedded){
var source, next, projectPath;
embedded = fis.file.wrap(embedded).realpath;
source = embedded;
next = embeddedMap[embedded];
if(!next){
return false;
}
while(next && next !== source){
source = next;
next = embeddedMap[next];
}
projectPath = fis.project.getProjectPath();
return source.replace(projectPath, projectPath.substring(projectPath.lastIndexOf('/')));
}
function embeddedUnlock(file){
delete embeddedMap[file.realpath];
}
function addDeps(a, b){
if(a && a.cache && b){
if(b.cache){
a.cache.mergeDeps(b.cache);
}
a.cache.addDeps(b.realpath || b);
}
}
function standard(file){
var path = file.realpath,
content = file.getContent();
if(typeof content === 'string'){
fis.log.debug('standard start');
//expand language ability
if(file.isHtmlLike){
content = extHtml(content);
} else if(file.isJsLike){
content = extJs(content);
} else if(file.isCssLike){
content = extCss(content);
}
content = content.replace(map.reg, function(all, type, value){
var ret = '', info;
try {
switch(type){
case 'require':
info = fis.uri.getId(value, file.dirname);
file.addRequire(info.id);
ret = info.quote + info.id + info.quote;
break;
case 'uri':
info = fis.uri(value, file.dirname);
if(info.file && info.file.isFile()){
if(info.file.useHash && exports.settings.hash){
if(embeddedCheck(file, info.file)){
exports(info.file);
addDeps(file, info.file);
}
}
var query = (info.file.query && info.query) ? '&' + info.query.substring(1) : info.query;
var url = info.file.getUrl(exports.settings.hash, exports.settings.domain);
var hash = info.hash || info.file.hash;
//compiled to relative path added by rsk
if(fis.config.get('roadmap.relative')){
url = fis.util.getRelativePath(url, embeddedSource(file) || file.release);
}
//end
ret = info.quote + url + query + hash + info.quote;
} else {
ret = value;
}
break;
case 'dep':
if(file.cache){
info = fis.uri(value, file.dirname);
addDeps(file, info.file);
} else {
fis.log.warning('unable to add deps to file [' + path + ']');
}
break;
case 'embed':
case 'jsEmbed':
info = fis.uri(value, file.dirname);
var f;
if(info.file){
f = info.file;
} else if(fis.util.isAbsolute(info.rest)){
f = fis.file(info.rest);
}
if(f && f.isFile()){
if(embeddedCheck(file, f)){
exports(f);
addDeps(file, f);
f.requires.forEach(function(id){
file.addRequire(id);
});
if(f.isText()){
ret = f.getContent();
if(type === 'jsEmbed' && !f.isJsLike && !f.isJsonLike){
ret = JSON.stringify(ret);
}
} else {
ret = info.quote + f.getBase64() + info.quote;
}
}
} else {
fis.log.error('unable to embed non-existent file [' + value + ']');
}
break;
default :
fis.log.error('unsupported fis language tag [' + type + ']');
}
} catch (e) {
embeddedMap = {};
e.message = e.message + ' in [' + file.subpath + ']';
throw e;
}
return ret;
});
file.setContent(content);
fis.log.debug('standard end');
}
}
exports.extJs = extJs;
exports.extCss = extCss;
exports.extHtml = extHtml;
exports.isInline = isInline;
exports.analyseComment = analyseComment;