define('JsPackage', function (require, module, exports) { var $ = require('$'); var File = require('File'); var FileRefs = require('FileRefs'); var Path = require('Path'); var Patterns = require('Patterns'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var MD5 = require('MD5'); var Emitter = $.require('Emitter'); var mapper = new Map(); function JsPackage(dir, config) { config = Defaults.clone(module.id, config); var meta = { 'dir': dir, 'patterns': [], //模式列表。 'list': [], //真实 js 文件列表及其它信息。 'content': '', //编译后的 js 内容。 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建。 }; mapper.set(this, meta); } JsPackage.prototype = { constructor: JsPackage, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); //删除之前的文件引用计数 meta.list.forEach(function (item) { FileRefs.delete(item.file); }); $.Object.extend(meta, { 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块,即缓存 toHtml() 方法中的返回结果。 'outer': '', //包括开始标记和结束标记在内的原始的整一块 html。 'patterns': [], //模式列表。 'list': [], //真实 js 文件列表及其它信息。 'content': '', //编译后的 js 内容。 }); }, /** * 根据当前模式获取对应真实的 js 文件列表。 */ get: function (patterns) { var meta = mapper.get(this); var dir = meta.dir; patterns = meta.patterns = Patterns.combine(dir, patterns); var list = Patterns.getFiles(patterns); list = list.map(function (file, index) { file = Path.format(file); FileRefs.add(file); return file; }); meta.list = list; }, /** * 合并对应的 js 文件列表。 */ concat: function (options) { var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { meta.content = ''; return; } //加上文件头部和尾部,形成闭包 var header = options.header; if (header) { header = Path.format(header); FileRefs.add(header); list = [header].concat(list); } var footer = options.footer; if (footer) { footer = Path.format(footer); FileRefs.add(footer); list = list.concat(footer); } var JS = require('JS'); var content = meta.content = JS.concat(list, options); var md5 = MD5.get(content); return md5; }, /** * 压缩合并后的 js 文件。 */ minify: function (dest) { var meta = mapper.get(this); var content = meta.content; var JS = require('JS'); //直接从内容压缩,不读取文件 content = meta.content = JS.minify(content); if (typeof dest == 'object') { var name = dest.name; if (typeof name == 'number') { name = MD5.get(content, name); name += '.js'; } dest = dest.dir + name; File.write(dest, content); //写入合并后的 js 文件 } return dest; }, /** * 监控当前模式下 js 文件的变化。 */ watch: function () { var meta = mapper.get(this); var patterns = meta.patterns; if (patterns.length == 0) { //列表为空,不需要监控 return; } var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); var self = this; var emitter = meta.emitter; function add(files) { //增加到列表 var list = files.map(function (file, index) { file = Path.format(file); FileRefs.add(file); return file; }); meta.list = meta.list.concat(list); } watcher.on({ 'added': function (files) { add(files); emitter.fire('change'); }, 'deleted': function (files) { //从列表中删除 var obj = {}; files.forEach(function (file) { FileRefs.delete(file, true); obj[file] = true; }); meta.list = meta.list.filter(function (file) { return !obj[file]; }); emitter.fire('change'); }, //重命名的,会先后触发 deleted 和 renamed 'renamed': function (files) { add(files); emitter.fire('change'); }, 'changed': function (files) { emitter.fire('change'); }, }); } watcher.set(patterns); }, /** * 取消监控。 */ unwatch: function () { var meta = mapper.get(this); var watcher = meta.watcher; if (watcher) { watcher.close(); } }, /** * 删除模式列表中所对应的 js 物理文件。 */ delete: function () { var meta = mapper.get(this); meta.list.forEach(function (item) { FileRefs.delete(item.file); }); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return $.Object.extend(JsPackage, { }); });
define('LessPackage', function (require, module, exports) { var $ = require('$'); var File = require('File'); var FileRefs = require('FileRefs'); var Watcher = require('Watcher'); var MD5 = require('MD5'); var Path = require('Path'); var Patterns = require('Patterns'); var Defaults = require('Defaults'); var MD5 = require('MD5'); var Mapper = $.require('Mapper'); var Emitter = $.require('Emitter'); var mapper = new Mapper(); function LessPackage(dir, config) { Mapper.setGuid(this); config = Defaults.clone(module.id, config); var meta = { 'dir': dir, //母版页所在的目录。 'patterns': [], //模式列表。 'list': [], //真实 less 文件列表。 'less$item': {}, //less 文件所对应的信息 'content': '', //合并后或压缩后的内容。 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 }; mapper.set(this, meta); } LessPackage.prototype = { constructor: LessPackage, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); var less$item = meta.less$item; meta.list.forEach(function (less) { var item = less$item[less]; FileRefs.delete(less); }); $.Object.extend(meta, { 'patterns': [], //模式列表。 'list': [], //真实 less 文件列表。 'less$item': {}, //less 文件所对应的信息 }); }, /** * 根据当前模式获取对应真实的 less 文件列表和将要产生 css 文件列表。 */ get: function (patterns) { var meta = mapper.get(this); var less$item = meta.less$item; patterns = meta.patterns = Patterns.combine(meta.dir, patterns); var list = Patterns.getFiles(patterns); list.forEach(function (less) { //如 less = '../htdocs/html/test/style/less/index.less'; if (less$item[less]) { //已处理过该项,针对 watch() 中的频繁调用。 return; } less$item[less] = { 'content': '', //编译后的 css 内容。 'md5': '', //编译后的 css 内容对应的 md5 值,需要用到时再去计算。 }; FileRefs.add(less); }); meta.list = list; }, /** * 编译 less 文件列表(异步模式)。 * 如果指定了要编译的列表,则无条件进行编译。 * 否则,从原有的列表中过滤出尚未编译过的文件进行编译。 * 已重载: compile(list, fn); compile(list, options); compile(list); compile(fn); compile(options); compile(options, fn); * @param {Array} [list] 经编译的 less 文件列表。 如果指定了具体的 less 文件列表,则必须为当前文件引用模式下的子集。 如果不指定,则使用原来已经解析出来的文件列表。 提供了参数 list,主要是在 watch() 中用到。 */ compile: function (list, options) { var fn = null; if (list instanceof Array) { if (typeof options == 'function') { //重载 compile(list, fn); fn = options; options = null; } else if (typeof options == 'object') { //重载 compile(list, options); fn = options.done; } else { //重载 compile(list); options = null; } } else if (typeof list == 'function') { //重载 compile(fn); fn = list; list = null; } else if (typeof list == 'object') { //重载 compile(options); 或 compile(options, fn) fn = options; options = list; list = null; fn = fn || options.done; } options = options || { //这个默认值不能删除,供开发时 watch 使用。 'delete': false, //删除 less,仅提供给上层业务 build 时使用。 }; var Less = require('Less'); var meta = mapper.get(this); var less$item = meta.less$item; var force = !!list; //是否强制编译 list = list || meta.list; if (list.length == 0) { //没有 less 文件 fn && fn(); return; } //并行地发起异步的 less 编译 var Tasks = require('Tasks'); Tasks.parallel({ data: list, each: function (less, index, done) { var item = less$item[less]; //没有指定强制编译,并且该文件已经编译过了,则跳过。 if (!force && item.content) { done(); return; } Less.compile({ 'src': less, 'delete': options.delete, 'compress': false, 'done': function (css) { item.content = css; done(); }, }); }, all: function () { //已全部完成 fn && fn(); }, }); }, /** * 合并对应的 css 文件列表。 */ concat: function (dest) { var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { //没有 less 文件 return; } var less$item = meta.less$item; var contents = []; list.forEach(function (less) { var item = less$item[less]; contents.push(item.content); }); var content = meta.content = contents.join(''); if (dest) { File.write(dest, content); //写入合并后的 css 文件 } var md5 = MD5.get(content); return md5; }, /** * 压缩合并后的 css 文件。 */ minify: function (dest, done) { var meta = mapper.get(this); var content = meta.content; var Less = require('Less'); Less.minify(content, function (css) { if (typeof dest == 'object') { var name = dest.name; if (typeof name == 'number') { name = MD5.get(css, name); name += '.css'; } dest = dest.dir + name; } File.write(dest, css); done && done(dest, css); }); }, /** * 监控当前模式下的所有 less 文件。 */ watch: function () { var meta = mapper.get(this); var patterns = meta.patterns; if (patterns.length == 0) { //列表为空,不需要监控 return; } var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); var self = this; var less$item = meta.less$item; var emitter = meta.emitter; watcher.on({ 'added': function (files) { self.get(); self.compile(files, function () { emitter.fire('change'); }); }, 'deleted': function (files) { //删除对应的记录 files.forEach(function (less) { var item = less$item[less]; delete less$item[less]; FileRefs.delete(less, true); }); self.get(); emitter.fire('change'); }, //重命名的,会分别触发:deleted 和 renamed 'renamed': function (files) { self.get(); self.compile(files, function () { emitter.fire('change'); }); }, 'changed': function (files) { //让对应的记录作废 files.forEach(function (less) { var item = less$item[less]; item.md5 = ''; item.content = ''; }); self.compile(files, function () { emitter.fire('change'); }); }, }); } watcher.set(patterns); }, /** * 取消监控。 */ unwatch: function () { var meta = mapper.get(this); var watcher = meta.watcher; if (watcher) { watcher.close(); } }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return LessPackage; });
define('LessList', function (require, module, exports) { var $ = require('$'); var path = require('path'); var File = require('File'); var FileRefs = require('FileRefs'); var Watcher = require('Watcher'); var MD5 = require('MD5'); var Path = require('Path'); var Patterns = require('Patterns'); var Defaults = require('Defaults'); var Log = require('Log'); var Emitter = $.require('Emitter'); var Url = $.require('Url'); var mapper = new Map(); function LessList(dir, config) { config = Defaults.clone(module.id, config); var meta = { 'dir': dir, //母版页所在的目录。 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块。 'outer': '', //包括开始标记和结束标记在内的原始的整一块 html。 'patterns': [], //全部模式列表。 'list': [], //真实 less 文件列表。 'less$item': {}, //less 文件所对应的信息 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 'extraPatterns': config.extraPatterns, //额外附加的模式。 'md5': config.md5, //填充模板所使用的 md5 的长度 'sample': config.sample, //使用的模板 'tags': config.tags, 'htdocsDir': config.htdocsDir, 'cssDir': config.cssDir, 'concat': config.concat, 'minify': config.minify, //记录 concat, minify 的输出结果 'build': { file: '', //完整物理路径 href: '', //用于 link 标签中的 href 属性 content: '', //合并和压缩后的内容 }, }; mapper.set(this, meta); } LessList.prototype = { constructor: LessList, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); var less$item = meta.less$item; meta.list.forEach(function (less) { var item = less$item[less]; FileRefs.delete(less); FileRefs.delete(item.file); }); $.Object.extend(meta, { 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块。 'outer': '', //包括开始标记和结束标记在内的原始的整一块 html。 'patterns': [], //模式列表。 'list': [], //真实 less 文件列表。 'less$item': {}, //less 文件所对应的信息 //记录 concat, minify 的输出结果 'build': { file: '', //完整物理路径 href: '', //用于 link 标签中的 href 属性 content: '', //合并和压缩后的内容 }, }); }, /** * 从当前或指定的母版页 html 内容中提出 less 文件列表信息。 * @param {string} master 要提取的母版页 html 内容字符串。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; var tags = meta.tags; var html = $.String.between(master, tags.begin, tags.end); if (!html) { return; } var patterns = $.String.between(html, '<script>', '</script>'); if (!patterns) { return; } var dir = meta.dir; //母版页中可能会用到的上下文。 var context = { 'dir': dir, 'master': master, 'tags': meta.tags, 'htdocsDir': meta.htdocsDir, 'cssDir': meta.cssDir, }; var fn = new Function('require', 'context', //包装多一层匿名立即执行函数 'return (function () { ' + 'var a = ' + patterns + '; \r\n' + 'return a;' + '})();' ); //执行母版页的 js 代码,并注入变量。 patterns = fn(require, context); if (!Array.isArray(patterns)) { throw new Error('引入文件的模式必须返回一个数组!'); } patterns = patterns.concat(meta.extraPatterns); //跟配置中的模式合并 patterns = Patterns.fill(dir, patterns); patterns = Patterns.combine(dir, patterns); console.log('匹配到'.bgGreen, patterns.length.toString().cyan, '个 less 模式:'); Log.logArray(patterns); meta.patterns = patterns; meta.outer = tags.begin + html + tags.end; }, /** * 根据当前模式获取对应真实的 less 文件列表和将要产生 css 文件列表。 */ get: function () { var meta = mapper.get(this); var patterns = meta.patterns; var htdocsDir = meta.htdocsDir; var cssDir = meta.cssDir; var less$item = meta.less$item; var list = Patterns.getFiles(patterns); list.forEach(function (less) { //如 less = '../htdocs/html/test/style/less/index.less'; if (less$item[less]) { //已处理过该项,针对 watch() 中的频繁调用。 return; } var name = path.relative(htdocsDir, less); //如 'html/test/style/less/index.less' var ext = path.extname(name); //如 '.less' var basename = path.basename(name, ext); //如 'index' name = path.dirname(name); //如 'html/test/style/less' name = name.split('\\').join('.'); //如 'html.test.style.less' name = name + '.' + basename + '.css'; //如 'html.test.style.less.index.css' var file = path.join(cssDir, name); file = Path.format(file); var href = path.relative(meta.dir, file); href = Path.format(href); less$item[less] = { 'file': file, //完整的 css 物理路径。 'href': href, //用于 link 标签中的 href 属性(css) 'content': '', //编译后的 css 内容。 'md5': '', //编译后的 css 内容对应的 md5 值,需要用到时再去计算。 }; FileRefs.add(less); FileRefs.add(file); }); meta.list = list; }, /** * 获取 less 文件列表所对应的 md5 值和引用计数信息。 */ md5: function () { var meta = mapper.get(this); var list = meta.list; var file$stat = {}; list.forEach(function (file) { var stat = file$stat[file]; if (stat) { stat['count']++; return; } var md5 = MD5.read(file); file$stat[file] = { 'count': 1, 'md5': md5, }; }); return file$stat; }, /** * 编译 less 文件列表(异步模式)。 * 如果指定了要编译的列表,则无条件进行编译。 * 否则,从原有的列表中过滤出尚未编译过的文件进行编译。 * 已重载: compile(list, fn); compile(list, options); compile(list); compile(fn); compile(options); compile(options, fn); * @param {Array} [list] 经编译的 less 文件列表。 如果指定了具体的 less 文件列表,则必须为当前文件引用模式下的子集。 如果不指定,则使用原来已经解析出来的文件列表。 提供了参数 list,主要是在 watch() 中用到。 */ compile: function (list, options) { var fn = null; if (list instanceof Array) { if (typeof options == 'function') { //重载 compile(list, fn); fn = options; options = null; } else if (typeof options == 'object') { //重载 compile(list, options); fn = options.done; } else { //重载 compile(list); options = null; } } else if (typeof list == 'function') { //重载 compile(fn); fn = list; list = null; } else if (typeof list == 'object') { //重载 compile(options); 或 compile(options, fn) fn = options; options = list; list = null; fn = fn || options.done; } options = options || { //这个默认值不能删除,供开发时 watch 使用。 'write': true, //写入 css 'delete': false, //删除 less,仅提供给上层业务 build 时使用。 }; var Less = require('Less'); var meta = mapper.get(this); var less$item = meta.less$item; var force = !!list; //是否强制编译 list = list || meta.list; if (list.length == 0) { //没有 less 文件 fn && fn(); return; } //并行地发起异步的 less 编译 var Tasks = require('Tasks'); Tasks.parallel({ data: list, each: function (less, index, done) { var item = less$item[less]; //没有指定强制编译,并且该文件已经编译过了,则跳过。 if (!force && item.content) { done(); return; } Less.compile({ 'src': less, 'dest': options.write ? item.file : '', 'delete': options.delete, 'compress': false, 'done': function (css) { item.content = css; done(); }, }); }, all: function () { //已全部完成 fn && fn(); }, }); }, /** * 把当前的动态 less 引用模式块转成真实的静态 css 引用所对应的 html。 */ toHtml: function () { var meta = mapper.get(this); var sample = meta.sample; var list = meta.list; if (list.length == 0) { meta.html = ''; return; } var tags = meta.tags; var less$item = meta.less$item; //todo: 检查重复的文件 list = $.Array.keep(list, function (less, index) { var item = less$item[less]; var href = item.href; var len = meta.md5; if (len > 0) { var md5 = item.md5; if (!md5) { //动态去获取 md5 值。 md5 = item.md5 = MD5.get(item.content, len); } href = href + '?' + md5; } return $.String.format(sample, { 'href': href, }); }); meta.html = tags.begin + '\r\n ' + list.join('\r\n ') + '\r\n ' + tags.end + '\r\n '; }, /** * 把整一块动态 less 引用模式替换成真实的静态 css 引用。 * @param {string} [master] 要替换的母版 html。 如果不指定,则使用原来的。 * 注意,如果使用新的模板,则该模板中的模式不能变。 */ mix: function (master) { var meta = mapper.get(this); var outer = meta.outer; master = master || meta.master; //实现安全替换 var beginIndex = master.indexOf(outer); var endIndex = beginIndex + outer.length; master = master.slice(0, beginIndex) + meta.html + master.slice(endIndex); return master; }, /** * 合并对应的 css 文件列表。 */ concat: function (options) { var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { //没有 less 文件 meta.html = ''; return; } if (options === true) { //直接指定了为 true,则使用默认配置。 options = meta.concat; } var build = meta.build; var cssDir = meta.cssDir; var less$item = meta.less$item; var contents = []; list.forEach(function (less) { var item = less$item[less]; contents.push(item.content); if (options.delete) { //删除源分 css 文件 FileRefs.delete(item.file); } }); var content = contents.join(''); var name = options.name || 32; var isMd5Name = typeof name == 'number'; //为数字时,则表示使用 md5 作为名称。 var md5 = MD5.get(content); if (isMd5Name) { name = md5.slice(0, name) + '.css'; } var file = cssDir + name; var href = path.relative(meta.dir, file); href = Path.format(href); if (options.write) { //写入合并后的 css 文件 File.write(file, content); } $.Object.extend(build, { 'file': file, 'href': href, 'content': content, }); //更新 html //当不是以 md5 作为名称时,即当成使用固定的名称,如 index.all.debug.css, //为了确保能刷新缓存,这里还是强行加进了 md5 值作为 query 部分。 var len = meta.md5; if (len > 0 && !isMd5Name) { href = href + '?' + md5.slice(0, len); } meta.html = $.String.format(meta.sample, { 'href': href, }); }, /** * 压缩合并后的 css 文件。 */ minify: function (options, fn) { if (!options) { fn && fn(); return; } var meta = mapper.get(this); if (meta.list.length == 0) { //没有 less 文件 meta.html = ''; fn && fn(); return; } if (options === true) { //直接指定了为 true,则使用默认配置。 options = meta.minify; } var cssDir = meta.cssDir; var build = meta.build; var content = build.content; var Less = require('Less'); Less.render(content, { compress: true, }, function (error, output) { var content = output.css; var file = MD5.get(content); file = cssDir + file + '.css'; var href = path.relative(meta.dir, file); href = Path.format(href); //删除 concat() 产生的文件 if (options.delete) { File.delete(build.file); } File.write(file, content); $.Object.extend(build, { 'file': file, 'href': href, 'content': content, }); //更新 html meta.html = $.String.format(meta.sample, { 'href': href, }); fn && fn(); }); }, /** * 删除模式列表中所对应的 less 物理文件。 */ delete: function () { var meta = mapper.get(this); var list = meta.list; list.forEach(function (less) { FileRefs.delete(less); }); }, /** * 监控当前模式下的所有 less 文件。 */ watch: function () { var meta = mapper.get(this); var patterns = meta.patterns; if (patterns.length == 0) { //列表为空,不需要监控 return; } var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); var self = this; var less$item = meta.less$item; var emitter = meta.emitter; watcher.on({ 'added': function (files) { self.get(); self.compile(files, function () { self.toHtml(); emitter.fire('change'); }); }, 'deleted': function (files) { //删除对应的记录 files.forEach(function (less) { var item = less$item[less]; delete less$item[less]; FileRefs.delete(less, true); FileRefs.delete(item.file, true); //实时删除对应的 css 文件。 }); self.get(); self.toHtml(); emitter.fire('change'); }, //重命名的,会分别触发:deleted 和 renamed 'renamed': function (files) { self.get(); self.compile(files, function () { self.toHtml(); emitter.fire('change'); }); }, 'changed': function (files) { //让对应的记录作废 files.forEach(function (less) { var item = less$item[less]; item.md5 = ''; item.content = ''; }); //有一种情况:less 虽然发生了变化,但生成的 css 文件内容却不变。 //比如在 less 里加了些无用的空格、空行等。 var html = meta.html; self.compile(files, function () { self.toHtml(); //生成后的内容确实发生了变化 if (meta.html != html) { emitter.fire('change'); } }); }, }); } watcher.set(patterns); }, copy: function () { }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, }; return LessList; });
define('JsList', function (require, module, exports) { var $ = require('$'); var path = require('path'); var File = require('File'); var FileRefs = require('FileRefs'); var Path = require('Path'); var Patterns = require('Patterns'); var MD5 = require('MD5'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var Log = require('Log'); var Attribute = require('Attribute'); var Lines = require('Lines'); var Url = require('Url'); var Emitter = $.require('Emitter'); var mapper = new Map(); function JsList(dir, config) { config = Defaults.clone(module.id, config); var rid = $.String.random(4); //随机 id var meta = { 'dir': dir, //母版页所在的目录。 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块,即缓存 toHtml() 方法中的返回结果。 'outer': '', //包括开始标记和结束标记在内的原始的整一块 html。 'patterns': [], //模式列表。 'list': [], //真实 js 文件列表及其它信息。 'file$stat': {}, //记录文件内容中的最大行数和最大列数信息。 'file$md5': {}, 'scriptType': $.String.random(64), //用于 script 的 type 值。 在页面压缩 js 时防止重复压缩。 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建。 'extraPatterns': config.extraPatterns, //额外附加的模式。 'regexp': config.regexp, 'md5': config.md5, 'sample': config.sample, 'tags': config.tags, 'concat': config.concat, 'minify': config.minify, 'inline': config.inline, 'max': config.max, //允许的最大行数和列数。 'htdocsDir': config.htdocsDir, //记录 concat, minify 的输出结果 'build': { file: '', //完整物理路径 content: '', //合并和压缩后的内容 }, }; mapper.set(this, meta); } JsList.prototype = { constructor: JsList, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); //删除之前的文件引用计数 meta.list.forEach(function (item) { FileRefs.delete(item.file); }); $.Object.extend(meta, { 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块,即缓存 toHtml() 方法中的返回结果。 'outer': '', //包括开始标记和结束标记在内的原始的整一块 html。 'patterns': [], //模式列表。 'list': [], //真实 js 文件列表及其它信息。 'file$stat': {}, //文件所对应的最大行数和最大列数等统计信息。 'file$md5': {}, // 'build': { file: '', //完整物理路径 content: '', //合并和压缩后的内容 }, }); }, /** * 从当前或指定的母版页 html 内容中提出 js 文件列表信息。 * @param {string} master 要提取的母版页 html 内容字符串。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; var tags = meta.tags; var dir = meta.dir; var html = $.String.between(master, tags.begin, tags.end); if (!html) { return; } var patterns = $.String.between(html, '<script>', '</script>'); if (!patterns) { var list = html.match(meta.regexp); if (!list) { return; } var lines = Lines.get(html); var startIndex = 0; patterns = $.Array.map(list, function (item, index) { var src = Attribute.get(item, 'src'); if (!src) { console.log('JsList 块里的 script 标签必须含有 src 属性:'.bgRed, item); throw new Error(); } var index = Lines.getIndex(lines, item, startIndex); var line = lines[index]; //整一行的 html。 //所在的行给注释掉了,忽略 if (Lines.commented(line, item)) { return null; } startIndex = index + 1; //下次搜索的起始行号 if (Url.checkFull(src)) { //是绝对(外部)地址 console.log('JsList 块里的 script 标签 src 属性不能引用外部地址:'.bgRed, item); throw new Error(); } src = Path.format(src); return src; }); patterns = JSON.stringify(patterns, null, 4); } if (!patterns) { return; } //母版页中可能会用到的上下文。 var context = { 'dir': dir, 'master': master, 'tags': meta.tags, 'htdocsDir': meta.htdocsDir, }; var fn = new Function('require', 'context', //包装多一层匿名立即执行函数 'return (function () { ' + 'var a = ' + patterns + '; \r\n' + 'return a;' + '})();' ); //执行母版页的 js 代码,并注入变量。 patterns = fn(require, context); if (!Array.isArray(patterns)) { throw new Error('引入文件的模式必须返回一个数组!'); } patterns = patterns.concat(meta.extraPatterns); //跟配置中的模式合并 patterns = Patterns.fill(dir, patterns); patterns = Patterns.combine(dir, patterns); console.log('匹配到'.bgGreen, patterns.length.toString().cyan, '个 js 模式:'); Log.logArray(patterns); meta.patterns = patterns; meta.outer = tags.begin + html + tags.end; }, /** * 根据当前模式获取对应真实的 js 文件列表和其它信息。 */ get: function () { var meta = mapper.get(this); //删除之前的文件引用计数 meta.list.forEach(function (item) { FileRefs.delete(item.file); }); var patterns = meta.patterns; var list = Patterns.getFiles(patterns); list = $.Array.keep(list, function (file, index) { file = Path.format(file); var href = path.relative(meta.dir, file); href = Path.format(href); FileRefs.add(file); return { 'file': file, 'href': href, }; }); meta.list = list; }, /** * 获取 js 文件列表所对应的 md5 值和引用计数信息。 */ md5: function () { var meta = mapper.get(this); var file$md5 = meta.file$md5; var list = meta.list; var file$stat = {}; list.forEach(function (item) { var file = item.file; var stat = file$stat[file]; if (stat) { stat['count']++; return; } var md5 = file$md5[file]; if (!md5) { md5 = file$md5[file] = MD5.read(file); } file$stat[file] = { 'count': 1, 'md5': md5, }; }); return file$stat; }, /** * 把当前的动态 js 引用模式块转成真实的静态 js 引用所对应的 html。 */ toHtml: function () { var meta = mapper.get(this); var sample = meta.sample; var list = meta.list; if (list.length == 0) { meta.html = ''; return; } var tags = meta.tags; var file$stat = meta.file$stat; var file$md5 = meta.file$md5; var max = meta.max; //需要排除的文件列表,即不作检查的文件列表。 var excludes = max.excludes; if (excludes) { excludes = Patterns.combine(meta.htdocsDir, excludes); } //todo: 检查重复的文件 list = $.Array.keep(list, function (item, index) { var href = item.href; var file = item.file; var stat = file$stat[file]; if (!stat) { var content = File.read(file); stat = file$stat[file] = Lines.stat(content); } //在排除列表中的文件,不作检查。 //具体为: 如果未指定排除列表,或者不在排除列表中。 if (!excludes || !Patterns.matchedIn(excludes, file)) { if (stat.y > max.y) { console.log('超出所允许的最大行数'.bgRed, JSON.stringify({ '所在文件': file, '当前原始行数': stat.y0, '当前有效行数': stat.y, '允许最大行数': max.y, '超过行数': stat.y - max.y, }, null, 4).yellow); throw new Error(); } if (stat.x > max.x) { console.log('代码行超出所允许的最大长度'.bgRed, JSON.stringify({ '所在文件': file, '所在行号': stat.no, '当前行长度': stat.x, '允许最大长度': max.x, '超过长度': stat.x - max.x, }, null, 4).yellow); throw new Error(); } } var len = meta.md5; if (len > 0) { var md5 = file$md5[file]; if (!md5) { //动态去获取 md5 值。 md5 = file$md5[file] = MD5.read(file); } md5 = md5.slice(0, len); href = href + '?' + md5; } return $.String.format(sample, { 'href': href, }); }); meta.html = tags.begin + '\r\n ' + list.join('\r\n ') + '\r\n ' + tags.end + '\r\n'; }, /** * 把整一块动态 js 引用模式替换成真实的静态 js 引用。 * @param {string} [master] 要替换的母版 html。 如果不指定,则使用原来的。 * 注意,如果使用新的模板,则该模板中的模式不能变。 */ mix: function (master) { var meta = mapper.get(this); var outer = meta.outer; master = master || meta.master; //实现安全替换 var beginIndex = master.indexOf(outer); var endIndex = beginIndex + outer.length; master = master.slice(0, beginIndex) + meta.html + master.slice(endIndex); return master; }, /** * 监控当前模式下 js 文件的变化。 */ watch: function () { var meta = mapper.get(this); var patterns = meta.patterns; if (patterns.length == 0) { //列表为空,不需要监控 return; } var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); var self = this; var file$stat = meta.file$stat; var file$md5 = meta.file$md5; var emitter = meta.emitter; watcher.on({ 'added': function (files) { self.get(); self.toHtml(); emitter.fire('change'); }, 'deleted': function (files) { //删除对应的记录 files.forEach(function (file) { delete file$stat[file]; delete file$md5[file]; FileRefs.delete(file, true); }); self.get(); self.toHtml(); emitter.fire('change'); }, //重命名的,会先后触发:deleted 和 renamed 'renamed': function (files) { self.get(); self.toHtml(); emitter.fire('change'); }, 'changed': function (files) { //让对应的记录作废 files.forEach(function (file) { file$stat[file] = null; file$md5[file] = null; }); self.toHtml(); emitter.fire('change'); }, }); } watcher.set(patterns); }, /** * 合并对应的 js 文件列表。 */ concat: function (options) { var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { meta.html = ''; return; } if (options === true) { //直接指定了为 true,则使用默认配置。 options = meta.concat; } list = $.Array.keep(list, function (item) { return item.file; }); //加上文件头部和尾部,形成闭包 var header = options.header; if (header) { header = Path.format(header); FileRefs.add(header); list = [header].concat(list); } var footer = options.footer; if (footer) { footer = Path.format(footer); FileRefs.add(footer); list = list.concat(footer); } var JS = require('JS'); var content = JS.concat(list, { 'addPath': options.addPath, 'delete': options.delete, }); var name = options.name || 32; var isMd5Name = typeof name == 'number'; //为数字时,则表示使用 md5 作为名称。 var md5 = MD5.get(content); if (isMd5Name) { name = md5.slice(0, name) + '.js'; } var file = meta.dir + name; if (options.write) { //写入合并后的 js 文件 File.write(file, content); } $.Object.extend(meta.build, { 'file': file, 'content': content, }); //更新 html var href = name; //当不是以 md5 作为名称时,即当成使用固定的名称,如 index.all.debug.js, //为了确保能刷新缓存,这里还是强行加进了 md5 值作为 query 部分。 var len = meta.md5; if (len > 0 && !isMd5Name) { href = href + '?' + md5.slice(0, len); } meta.html = $.String.format(meta.sample, { 'href': href, }); }, /** * 压缩合并后的 js 文件。 */ minify: function (options) { var meta = mapper.get(this); if (meta.list.length == 0) { meta.html = ''; return; } if (options === true) { //直接指定了为 true,则使用默认配置。 options = meta.minify; } var build = meta.build; var content = build.content; if (options.delete) { //删除 concat() 产生的文件 File.delete(build.file); } var JS = require('JS'); content = JS.minify(content); //直接从内容压缩,不读取文件 var name = options.name || 32; var isMd5Name = typeof name == 'number'; //为数字时,则表示使用 md5 作为名称。 var md5 = MD5.get(content); if (isMd5Name) { name = md5.slice(0, name) + '.js'; } var file = meta.dir + name; if (options.write) { File.write(file, content); } $.Object.extend(build, { 'file': file, 'content': content, }); //更新 html var href = name; //当不是以 md5 作为名称时,即当成使用固定的名称,如 index.all.debug.js, //为了确保能刷新缓存,这里还是强行加进了 md5 值作为 query 部分。 var len = meta.md5; if (len > 0 && !isMd5Name) { href = href + '?' + md5.slice(0, len); } meta.html = $.String.format(meta.sample, { 'href': href, }); }, /** * 把 js 文件的内容内联到 html 中。 */ inline: function (options) { var meta = mapper.get(this); if (meta.list.length == 0) { meta.html = ''; return; } if (options === true) {//直接指定了为 true,则使用默认配置。 options = meta.inline; } var build = meta.build; var content = build.content; //删除 concat() 或 minify() 产生的文件 if (options.delete) { File.delete(build.file); } //添加一个随机的 type 值,变成不可执行的 js 代码, //可以防止在压缩页面时重复压缩本 js 代码。 var sample = '<script type="{type}">{content}</script>' meta.html = $.String.format(sample, { 'type': meta.scriptType, 'content': content, }); }, /** * 移除临时添加进去的 script type,恢复成可执行的 script 代码。 */ removeType: function (master) { var meta = mapper.get(this); var tag = $.String.format('<script type="{type}">', { 'type': meta.scriptType, }); master = master.split(tag).join('<script>'); //replaceAll return master; }, /** * 删除模式列表中所对应的 js 物理文件。 */ delete: function () { var meta = mapper.get(this); meta.list.forEach(function (item) { FileRefs.delete(item.file); }); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, }; return $.Object.extend(JsList, { //子类,用于提供实例方法: //检查 JsList 块里是否包含指定的 script 标签。 Checker: (function () { var tags = null; function Checker(master) { tags = tags || Defaults.get(module.id).tags; this.html = $.String.between(master, tags.begin, tags.end); } Checker.prototype = { constructor: Checker, /** * 检查 JsList 块里是否包含指定的 script 标签。 * 该方法主要是给 JsScripts 模块使用。 * @param {string} 要检查的 html 文本内容。 * @param {string} script 要检查的 script 标签内容。 * @return {boolean} 返回一个布尔值,该值指示指定的 script 标签是否出现在 JsList 块里。 */ has: function (script) { return this.html.indexOf(script) >= 0; }, }; return Checker; })(), }); });
define('/Favorites', function (require, module, exports) { var $ = require('$'); var Directory = require('Directory'); var File = require('File'); var Config = require('Config'); var API = require('API'); var Image = require('Image'); var Emitter = $.require('Emitter'); var Parser = module.require('Parser'); /** * 构造器。 */ function Favorites(config) { //重载 Favorites(userId) if (typeof config == 'string') { config = { 'userId': config }; } config = Config.get(module.id, config); var dir = Directory.root(); var userId = config.userId; var data = { 'userId': userId, 'dir': dir.slice(0, -1), }; var url = $.String.format(config.url, data); var html = config.html; var json = config.json; var meta = { 'userId': userId, 'url': url, 'host': url.split('/').slice(0, 3).join('/'), 'cache': config.cache, 'html': { 'file': $.String.format(html.file, data), 'write': html.write, }, 'json': { 'file': $.String.format(json.file, data), 'write': json.write, }, 'emitter': new Emitter(this), }; this.meta = meta; } //实例方法 Favorites.prototype = { constructor: Favorites, /** * 发起 GET 网络请求以获取信息。 */ get: function (fn) { fn && this.on('get', fn); var meta = this.meta; var emitter = meta.emitter; var api = new API({ 'cache': meta.cache, 'file': meta.html.file, 'url': meta.url, 'parser': Parser, }); api.on({ 'success': function (data) { //增加些字段。 data = $.Object.extend(data, { 'userId': meta.userId, 'url': meta.url, 'host': meta.host, }); if (meta.json.write) { File.writeJSON(meta.json.file, data); } emitter.fire('get', [data]); }, }); api.get(); }, /** * 绑定事件。 * 已重载 on({...},因此支持批量绑定。 * @param {string} name 事件名称。 * @param {function} fn 回调函数。 */ on: function (name, fn) { var meta = this.meta; var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return Favorites; });
define('SerialTasks', function (require, module, exports) { var $ = require('$'); var Emitter = $.require('Emitter'); function SerialTasks(list) { var meta = { 'list': list, 'emitter': new Emitter(this), }; this.meta = meta; } SerialTasks.prototype = { constructor: SerialTasks, run: function () { var meta = this.meta; var list = meta.list; var emitter = meta.emitter; var index = 0; var len = list.length; var values = []; function process() { var item = list[index]; emitter.fire('each', [item, index, function (value) { index++; values.push(value); //需要收集的值,由调用者传入。 if (index < len) { process(); } else { emitter.fire('all', [values]); } }]); } process(); }, /** * 绑定事件。 * 已重载 on({...},因此支持批量绑定。 * @param {string} name 事件名称。 * @param {function} fn 回调函数。 */ on: function (name, fn) { var meta = this.meta; var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return SerialTasks; });
define('LessLinks', function (require, module, exports) { var $ = require('$'); var path = require('path'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var MD5 = require('MD5'); var File = require('File'); var FileRefs = require('FileRefs'); var Lines = require('Lines'); var Path = require('Path'); var Url = require('Url'); var Attribute = require('Attribute'); var Mapper = $.require('Mapper'); var Emitter = $.require('Emitter'); var $Url = $.require('Url'); var mapper = new Mapper(); function LessLinks(dir, config) { Mapper.setGuid(this); config = Defaults.clone(module.id, config); var meta = { 'dir': dir, 'master': '', 'list': [], 'lines': [], //html 换行拆分的列表 'less$item': {}, //less 文件所对应的信息 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 'regexp': config.regexp, 'md5': config.md5, //填充模板所使用的 md5 的长度 'sample': config.sample, //使用的模板 'tags': config.tags, 'htdocsDir': config.htdocsDir, 'cssDir': config.cssDir, 'minify': config.minify, }; mapper.set(this, meta); } LessLinks.prototype = { constructor: LessLinks, /** * 重置为初始状态,即创建时的状态。 * @param {boolean} keep 是否保留之前编译过的信息。 * 如果需要保留,请指定为 true;否则指定为 false 或不指定。 */ reset: function (keep) { var meta = mapper.get(this); var less$item = meta.less$item; meta.list.forEach(function (obj) { var less = obj.file; var item = less$item[less]; FileRefs.delete(less); FileRefs.delete(item.file); }); $.Object.extend(meta, { 'master': '', 'list': [], 'lines': [], //html 换行拆分的列表 'less$item': keep ? less$item : {}, //less 文件所对应的信息 }); }, /** * 从当前或指定的母版页 html 内容中提出 less 文件列表信息。 * @param {string} master 要提取的母版页 html 内容字符串。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; //这里必须要有,不管下面的 list 是否有数据。 var lines = Lines.get(master); meta.lines = lines; //提取出 link 标签 var list = master.match(meta.regexp); if (!list) { return; } var startIndex = 0; //搜索的起始行号 list = $.Array.map(list, function (item, index) { var href = Attribute.get(item, 'href'); if (!href) { return null; } var index = Lines.getIndex(lines, item, startIndex); startIndex = index + 1; //下次搜索的起始行号。 这里要先加 var line = lines[index]; //整一行的 html。 lines[index] = null; //先清空,后续会在 mix() 中重新计算而填进去。 //所在的行给注释掉了,忽略 if (Lines.commented(line, item)) { return null; } var file = Path.join(meta.dir, href); return { 'file': file, 'index': index, //行号,从 0 开始。 'html': item, //标签的 html 内容。 'line': line, //整一行的 html 内容。 }; }); meta.list = list; }, /** * 根据当前真实的 less 文件列表获取对应将要产生 css 文件列表。 */ get: function () { var meta = mapper.get(this); var htdocsDir = meta.htdocsDir; var cssDir = meta.cssDir; var less$item = meta.less$item; meta.list.forEach(function (item) { var less = item.file; //如 less = '../htdocs/html/test/style/less/index.less'; if (less$item[less]) { //已处理过该项,针对 watch() 中的频繁调用。 return; } var name = path.relative(htdocsDir, less); //如 'html/test/style/less/index.less' var ext = path.extname(name); //如 '.less' var basename = path.basename(name, ext); //如 'index' name = path.dirname(name); //如 'html/test/style/less' name = name.split('\\').join('.'); //如 'html.test.style.less' name = name + '.' + basename + '.css'; //如 'html.test.style.less.index.css' var file = path.join(cssDir, name); file = Path.format(file); var href = path.relative(meta.dir, file); href = Path.format(href); less$item[less] = { 'file': file, //完整的 css 物理路径。 'href': href, //用于 link 标签中的 href 属性(css) 'content': '', //编译后的 css 内容。 'md5': '', //编译后的 css 内容对应的 md5 值,需要用到时再去计算。 }; FileRefs.add(less); FileRefs.add(file); }); }, /** * 获取 less 文件列表所对应的 md5 值和引用计数信息。 */ md5: function () { var meta = mapper.get(this); var list = meta.list; var file$stat = {}; list.forEach(function (item) { var file = item.file; var stat = file$stat[file]; if (stat) { stat['count']++; return; } var md5 = MD5.read(file); file$stat[file] = { 'count': 1, 'md5': md5, }; }); return file$stat; }, /** * 编译 less 文件列表(异步模式)。 * 如果指定了要编译的列表,则无条件进行编译。 * 否则,从原有的列表中过滤出尚未编译过的文件进行编译。 * 已重载: compile(list, fn); compile(list, options); compile(list); compile(fn); compile(options); compile(options, fn); * @param {Array} [list] 经编译的 less 文件列表。 如果指定了具体的 less 文件列表,则必须为当前文件引用模式下的子集。 如果不指定,则使用原来已经解析出来的文件列表。 提供了参数 list,主要是在 watch() 中用到。 */ compile: function (list, options) { var fn = null; if (list instanceof Array) { if (typeof options == 'function') { //重载 compile(list, fn); fn = options; options = null; } else if (typeof options == 'object') { //重载 compile(list, options); fn = options.done; } else { //重载 compile(list); options = null; } } else if (typeof list == 'function') { //重载 compile(fn); fn = list; list = null; } else if (typeof list == 'object') { //重载 compile(options); 或 compile(options, fn) fn = options; options = list; list = null; fn = fn || options.done; } options = options || { //这个默认值不能删除,供开发时 watch 使用。 'write': true, //写入 css 'minify': false, //使用压缩版。 'delete': false, //删除 less,仅提供给上层业务 build 时使用。 }; var Less = require('Less'); var meta = mapper.get(this); var less$item = meta.less$item; var force = !!list; //是否强制编译 list = list || meta.list.map(function (item) { return item.file; }); if (list.length == 0) { //没有 less 文件 fn && fn(); return; } //并行地发起异步的 less 编译 var Tasks = require('Tasks'); Tasks.parallel({ data: list, each: function (less, index, done) { var item = less$item[less]; //没有指定强制编译,并且该文件已经编译过了,则跳过。 if (!force && item.content) { done(); return; } Less.compile({ 'src': less, 'dest': options.write ? item.file : '', 'delete': options.delete, 'compress': options.minify, 'done': function (css) { item.content = css; done(); }, }); }, all: function () { //已全部完成 fn && fn(); }, }); }, /** * 监控 css 文件的变化。 */ watch: function () { var meta = mapper.get(this); //这里不要缓存起来,因为可能在 parse() 中给重设为新的对象。 //var list = meta.list; var watcher = meta.watcher; if (!watcher) { //首次创建。 watcher = meta.watcher = new Watcher(); var self = this; var emitter = meta.emitter; var less$item = meta.less$item; watcher.on({ 'deleted': function (files) { console.log('文件已给删除'.yellow, files); }, 'changed': function (files) { //让对应的记录作废 files.forEach(function (less) { var item = less$item[less]; item.md5 = ''; item.content = ''; //根据当前文件名,找到具有相同文件名的节点集合。 //让对应的 html 作废。 meta.list.forEach(function (item) { if (item.file != less) { return; } meta.lines[item.index] = null; }); }); self.compile(files, function () { emitter.fire('change'); }); }, }); } var files = meta.list.map(function (item) { return item.file; }); watcher.set(files); }, /** * */ mix: function () { var meta = mapper.get(this); var list = meta.list; var lines = meta.lines; var replace = $.String.replaceAll; var len = meta.md5; var less$item = meta.less$item; var sample = meta.sample; list.forEach(function (obj) { var index = obj.index; if (lines[index]) { //之前已经生成过了 return; } var less = obj.file; var item = less$item[less]; var href = item.href; if (len > 0) { var md5 = item.md5; if (!md5) { //动态去获取 md5 值。 md5 = item.md5 = MD5.get(item.content, len); } href = href + '?' + md5; } var html = $.String.format(sample, { 'href': href, }); var line = replace(obj.line, obj.html, html); lines[index] = line; }); return Lines.join(lines); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, }; return LessLinks; });
define('/Page', function (require, module, exports) { var $ = require('$'); var Directory = require('Directory'); var File = require('File'); var Config = require('Config'); var API = require('API'); var Emitter = $.require('Emitter'); var Parser = module.require('Parser'); /** * 构造器。 */ function Page(config) { config = Config.get(module.id, config); var dir = Directory.root(); var userId = config.userId; var no = config.no; //当前页码,从 1 开始。 var total = config.total; //总页数。 var sn = no - total; //编程页码,最后一页为 0,倒数第二页为 -1,倒数第三页为 -2,依次类推。 var data = { 'dir': dir.slice(0, -1), 'userId': userId, 'no': no, 'sn': sn, }; var url = $.String.format(config.url, data); var html = config.html; var json = config.json; var meta = { 'userId': userId, 'url': url, 'no': no, 'total': total, 'cache': config.cache, 'html': { 'file': $.String.format(html.file, data), 'write': html.write, }, 'json': { 'file': $.String.format(json.file, data), 'write': json.write, }, 'emitter': new Emitter(this), }; this.meta = meta; } Page.prototype = { //实例方法 constructor: Page, /** * 发起 GET 网络请求以获取信息。 */ get: function (fn) { fn && this.on('get', fn); var meta = this.meta; var emitter = meta.emitter; var api = new API({ 'cache': meta.cache, 'file': meta.html.file, 'url': meta.url, 'parser': Parser, }); api.on({ 'success': function (data) { $.Object.extend(data, { 'userId': meta.userId, 'url': meta.url, 'no': meta.no, 'total': meta.total, }); if (meta.json.write) { File.writeJSON(meta.json.file, data); } emitter.fire('get', [data]); }, 'fail': function () { emitter.fire('get', [null]); }, 'error': function () { emitter.fire('get', []); }, }); api.get(); }, /** * 绑定事件。 * 已重载 on({...},因此支持批量绑定。 * @param {string} name 事件名称。 * @param {function} fn 回调函数。 */ on: function (name, fn) { var meta = this.meta; var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return Page; });
define('Package', function (require, module, exports) { var $ = require('$'); var path = require('path'); var File = require('File'); var FileRefs = require('FileRefs'); var Path = require('Path'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var Lines = require('Lines'); var Url = require('Url'); var Patterns = require('Patterns'); var Log = require('Log'); var Emitter = $.require('Emitter'); var HtmlPackage = require('HtmlPackage'); var JsPackage = require('JsPackage'); var LessPackage = require('LessPackage'); var mapper = new Map(); var name$file = {}; //记录包的名称与文件名的对应关系,防止出现重名的包。 function Package(file, config) { config = Defaults.clone(module.id, config); var htdocsDir = config.htdocsDir; file = Path.join(htdocsDir, file); var dir = Path.dirname(file); //分包 package.json 文件所在的目录 var meta = { 'dir': dir, 'file': file, 'htdocsDir': htdocsDir, 'packageDir': config.packageDir, 'cssDir': config.cssDir, 'compile': config.compile, 'minify': config.minify, 'md5': config.md5, 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 'old': {}, //用来存放旧的 HtmlPackage、JsPackage 和 LessPackage。 'HtmlPackage': null, 'JsPackage': null, 'LessPackage': null, 'css': '', 'html': '', 'js': '', }; mapper.set(this, meta); } Package.prototype = { constructor: Package, /** * 重置上一次可能存在的结果。 */ reset: function () { var meta = mapper.get(this); var old = meta.old; //先备份。 old 中的一旦有值,将再也不会变为 null。 old.HtmlPackage = meta.HtmlPackage; old.JsPackage = meta.JsPackage; old.LessPackage = meta.LessPackage; //再清空。 $.Object.extend(meta, { 'HtmlPackage': null, 'JsPackage': null, 'LessPackage': null, 'css': '', 'html': '', 'js': '', }); }, /** * */ parse: function () { var meta = mapper.get(this); var file = meta.file; var dir = meta.dir; var htdocsDir = meta.htdocsDir; var json = File.readJSON(file); var name = json.name; //如果未指定 name,则以包文件所在的目录的第一个 js 文件名作为 name。 if (!name) { var files = Patterns.getFiles(dir, '*.js'); name = files[0]; if (!name) { console.log('包文件'.bgRed, file.yellow, '中未指定 name 字段,且未在其的所在目录找到任何 js 文件。'.bgRed); throw new Error(); } name = Path.relative(dir, name); name = name.slice(0, -3); //去掉 `.js` 后缀。 } else if (name == '*') { name = Path.relative(htdocsDir, dir); name = name.split('/').join('.'); } var oldFile = name$file[name]; if (oldFile && oldFile != file) { console.log('存在同名'.bgRed, name.green, '的包文件:'.bgRed); Log.logArray([oldFile, file], 'yellow'); throw new Error(); } name$file[name] = file; meta.name = name; var old = meta.old; var packageDir = htdocsDir + meta.packageDir; if (json.html) { meta.HtmlPackage = old.HtmlPackage || new HtmlPackage(dir); meta.html = { 'src': json.html, 'dir': packageDir, 'dest': packageDir + name + '.html', 'md5': '', }; } if (json.js) { meta.JsPackage = old.JsPackage || new JsPackage(dir); meta.js = { 'src': json.js, 'dir': packageDir, 'dest': packageDir + name + '.js', 'md5': '', }; } if (json.css) { var cssDir = htdocsDir + meta.cssDir; meta.LessPackage = old.LessPackage || new LessPackage(dir); meta.css = { 'src': json.css, 'dir': cssDir, 'dest': cssDir + name + '.css', 'md5': '', }; } }, /** * 编译当前包文件。 */ compile: function (options, done) { //重载 compile(done) if (typeof options == 'function') { done = options; options = null; } var meta = mapper.get(this); var HtmlPackage = meta.HtmlPackage; var JsPackage = meta.JsPackage; var LessPackage = meta.LessPackage; options = options || meta.compile; if (HtmlPackage) { var file = options.html.write ? meta.html.dest : ''; HtmlPackage.reset(); HtmlPackage.get(meta.html.src); meta.html.md5 = HtmlPackage.compile(file); if (options.html.delete) { HtmlPackage.delete(); } } if (JsPackage) { var js = options.js; js.dest = js.write ? meta.js.dest : ''; JsPackage.reset(); JsPackage.get(meta.js.src); meta.js.md5 = JsPackage.concat(js); } if (LessPackage) { var less = options.less; var opt = { delete: less.delete }; LessPackage.reset(); LessPackage.get(meta.css.src); LessPackage.compile(opt, function () { var css = less.write ? meta.css.dest : ''; meta.css.md5 = LessPackage.concat(css); done && done(); }); } else { done && done(); } }, /** * 压缩。 */ minify: function (options, done) { //重载 minify(done) if (typeof options == 'function') { done = options; options = null; } var meta = mapper.get(this); var dest = meta.dest; var HtmlPackage = meta.HtmlPackage; var JsPackage = meta.JsPackage; var LessPackage = meta.LessPackage; options = options || meta.minify; if (HtmlPackage) { var opt = options.html; if (opt) { if (opt === true) { //当指定为 true 时,则使用默认的压缩选项。 opt = meta.minify.html; } var html = meta.html; html.dest = HtmlPackage.minify(opt, { 'dir': html.dir, 'name': 32, //md5 的长度。 }); html.md5 = ''; } } if (JsPackage) { var opt = options.js; if (opt && opt.write) { var js = meta.js; js.dest = JsPackage.minify({ 'dir': js.dir, 'name': 32, }); js.md5 = ''; } } if (LessPackage) { var opt = options.less; if (opt && opt.write) { var css = meta.css; var dest = { 'dir': css.dir, 'name': 32, //md5 的长度。 }; LessPackage.minify(dest, function (dest, content) { css.dest = dest; css.md5 = ''; done && done(); }); } else { done && done(); } } else { done && done(); } }, /** * 监控当前包文件及各个资源引用模块。 */ watch: function () { var meta = mapper.get(this); var HtmlPackage = meta.HtmlPackage; var JsPackage = meta.JsPackage; var LessPackage = meta.LessPackage; var emitter = meta.emitter; var old = meta.old; if (HtmlPackage) { HtmlPackage.watch(); if (!old.HtmlPackage) { HtmlPackage.on('change', function () { var html = meta.html; html.md5 = HtmlPackage.compile(html.dest); emitter.fire('change'); }); } } else if(old.HtmlPackage) { old.HtmlPackage.unwatch(); } if (JsPackage) { JsPackage.watch(); if (!old.JsPackage) { JsPackage.on('change', function () { var js = meta.js; js.md5 = JsPackage.concat({ 'dest': js.dest, }); emitter.fire('change'); }); } } else if (old.JsPackage) { old.JsPackage.unwatch(); } if (LessPackage) { LessPackage.watch(); if (!old.LessPackage) { LessPackage.on('change', function () { var css = meta.css; css.md5 = LessPackage.concat(css.dest); emitter.fire('change'); }); } } else if (old.LessPackage) { old.LessPackage.unwatch(); } var watcher = meta.watcher; if (!watcher) { var self = this; watcher = meta.watcher = new Watcher(); watcher.set(meta.file); //这里只需要添加一次 watcher.on('changed', function () { self.reset(); self.parse(); //json 文件发生变化,重新解析。 self.compile(); //根节点发生变化,需要重新编译。 self.watch(); emitter.fire('change'); }); } }, /** * 构建。 */ build: function (options, done) { var pkg = this; pkg.parse(); pkg.compile(options.compile, function () { pkg.minify(options.minify, function () { done && done(); }); }); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, clean: function () { var meta = mapper.get(this); FileRefs.delete(meta.file); }, /** * 获取输出目标包的信息。 * 该方法由静态方法 write 调用。 */ get: function () { var meta = mapper.get(this); var name = meta.name; var htdocsDir = meta.htdocsDir; var data = {}; ['js', 'html', 'css'].forEach(function (type) { var item = meta[type]; if (!item) { return; } var href = Path.relative(htdocsDir, item.dest); var md5 = item.md5.slice(0, meta.md5); if (md5) { href = href + '?' + md5; } data[type] = href; }); var obj = {}; obj[name] = data; return obj; }, }; //静态方法。 return $.Object.extend(Package, { /** * 写入到指定的总包。 */ write: function (dest, pkgs, minify) { var json = File.readJSON(dest) || {}; pkgs.forEach(function (pkg) { var obj = pkg.get(); $.Object.extend(json, obj); }); File.writeJSON(dest, json, minify); }, }); });
define('/Avatar', function (require, module, exports) { var $ = require('$'); var Directory = require('Directory'); var Config = require('Config'); var Image = require('Image'); var Emitter = $.require('Emitter'); /** * 构造器。 */ function Avatar(config) { config = Config.get(module.id, config); var dir = Directory.root(); var userId = config.userId; //用于 config 中的模板填充。 var data = { 'userId': userId, 'dir': dir.slice(0, -1), }; var url = config.url; if (!url.startsWith('http')) { // 有些以 `//` 开头 url = config.host.split('//')[0] + url; } var meta = { 'userId': userId, 'url': url, 'cache': config.cache, 'file': $.String.format(config.file, data), 'write': config.write, 'emitter': new Emitter(this), }; this.meta = meta; } Avatar.prototype = { //实例方法 constructor: Avatar, /** * 发起 GET 网络请求以获取信息。 */ get: function (fn) { fn && this.on('get', fn); var meta = this.meta; var emitter = meta.emitter; Image.get({ 'cache': meta.cache, 'url': meta.url, 'file': meta.file, 'done': function (error) { emitter.fire('get', []); }, }); }, /** * 绑定事件。 * 已重载 on({...},因此支持批量绑定。 * @param {string} name 事件名称。 * @param {function} fn 回调函数。 */ on: function (name, fn) { var meta = this.meta; var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return Avatar; });
define('/FavUsers', function (require, module, exports) { var $ = require('$'); var Url = $.require('Url'); var Directory = require('Directory'); var File = require('File'); var Config = require('Config'); var API = require('API'); var Emitter = $.require('Emitter'); var Parser = module.require('Parser'); /** * 构造器。 */ function FavUsers(config) { config = Config.get(module.id, config); var dir = Directory.root(); var userId = config.userId; var data = { 'userId': userId, 'dir': dir.slice(0, -1), }; var html = config.html; var json = config.json; var host = config.host; var url = host + config.url; url = Url.addQueryString(url, { 'size': 100000, }); //增大每页的记录数,以便一次性全取回来。 var meta = { 'userId': userId, 'url': url, 'host': host, 'cache': config.cache, 'html': { 'file': $.String.format(html.file, data), 'write': html.write, }, 'json': { 'file': $.String.format(json.file, data), 'write': json.write, }, 'emitter': new Emitter(this), }; this.meta = meta; } FavUsers.prototype = { //实例方法 constructor: FavUsers, /** * 发起 GET 网络请求以获取信息。 */ get: function (fn) { fn && this.on('get', fn); var meta = this.meta; var emitter = meta.emitter; var api = new API({ 'cache': meta.cache, 'file': meta.html.file, 'url': meta.url, 'parser': Parser, }); api.on({ 'success': function (data) { data = $.Object.extend(data, { 'userId': meta.userId, 'url': meta.url, 'host': meta.host, }); if (meta.json.write) { File.writeJSON(meta.json.file, data); } emitter.fire('get', [data]); }, 'fail': function () { emitter.fire('get', [null]); }, 'error': function () { emitter.fire('get', []); }, }); api.get(); }, /** * 绑定事件。 * 已重载 on({...},因此支持批量绑定。 * @param {string} name 事件名称。 * @param {function} fn 回调函数。 */ on: function (name, fn) { var meta = this.meta; var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); }, }; return FavUsers; });
define('JsScripts', function (require, module, exports) { var $ = require('$'); var path = require('path'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var MD5 = require('MD5'); var File = require('File'); var FileRefs = require('FileRefs'); var Lines = require('Lines'); var Path = require('Path'); var Url = require('Url'); var Attribute = require('Attribute'); var Mapper = $.require('Mapper'); var Emitter = $.require('Emitter'); var $Url = $.require('Url'); var mapper = new Mapper(); function JsScripts(dir, config) { Mapper.setGuid(this); config = Defaults.clone(module.id, config); var meta = { 'dir': dir, 'master': '', 'list': [], //js 文件列表及其它信息。 'lines': [], //html 换行拆分的列表 'file$md5': {}, 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建。 'regexp': config.regexp, 'md5': config.md5, 'exts': config.exts, 'minify': config.minify, }; mapper.set(this, meta); } JsScripts.prototype = { constructor: JsScripts, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); meta.list.forEach(function (item) { FileRefs.delete(item.file); //删除之前的文件引用计数 FileRefs.delete(item.build.file); //删除之前的文件引用计数 }); $.Object.extend(meta, { 'master': '', 'list': [], 'lines': [], 'file$md5': {}, }); }, /** * 从当前或指定的母版页 html 内容中提出 js 文件列表信息。 * @param {string} master 要提取的母版页 html 内容字符串。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; //这个不能少,不管下面的 list 是否为空。 在 mix() 中用到。 var lines = Lines.get(master); meta.lines = lines; //<script src="f/jquery/jquery-2.1.1.debug.js"></script> //提取出含有 src 属性的 script 标签 //var reg = /<script\s+.*src\s*=\s*["\'][\s\S]*?["\'].*>[\s\S]*?<\/script>/ig; //var reg = /<script[^>]*?>[\s\S]*?<\/script>/gi; //var reg = /<script\s+.*src\s*=\s*[^>]*?>[\s\S]*?<\/script>/gi; var list = master.match(meta.regexp); if (!list) { return; } var debug = meta.exts.debug; var min = meta.exts.min; var Checker = require('JsList').Checker; var JsList = new Checker(master); var startIndex = 0; list = $.Array.map(list, function (item, index) { //不含有 src 属性,忽略掉。 var src = Attribute.get(item, 'src'); if (!src) { return null; } //该 script 标签出现在 JsList 块里,忽略掉。 if (JsList.has(item)) { return null; } var index = Lines.getIndex(lines, item, startIndex); var line = lines[index]; //整一行的 html。 lines[index] = null; //先清空,后续会在 mix() 中重新计算而填进去。 //所在的行给注释掉了,忽略掉。 if (Lines.commented(line, item)) { return null; } startIndex = index + 1; //下次搜索的起始行号 var suffix = Url.suffix(src); var prefix = suffix ? src.slice(0, -suffix.length) : src; var ext = $.String.endsWith(prefix, debug) ? debug : $.String.endsWith(prefix, min) ? min : path.extname(prefix); var name = ext ? prefix.slice(0, -ext.length) : prefix; var file = ''; if (!Url.checkFull(src)) { //不是绝对(外部)地址 file = Path.format(src); file = Path.join(meta.dir, file); FileRefs.add(file); } return { 'file': file, //完整的物理路径。 如果是外部地址,则为空字符串。 'src': src, //原始地址,带 query 和 hash 部分。 'suffix': suffix, //扩展名之后的部分,包括 '?' 在内的 query 和 hash 一整体。 'name': name, //扩展名之前的部分。 'ext': ext, //路径中的后缀名,如 '.debug.js'|'.min.js'|'.js'。 'index': index, //行号,从 0 开始。 'html': item, //标签的 html 内容。 'line': line, //整一行的 html 内容。 'build': {}, //记录 build() 的输出结果。 }; }); meta.list = list; }, /** * 获取 js 文件列表所对应的 md5 值和引用计数信息。 */ md5: function () { var meta = mapper.get(this); var file$md5 = meta.file$md5; var list = meta.list; var file$stat = {}; list.forEach(function (item) { var file = item.file; if (!file) { return; } var stat = file$stat[file]; if (stat) { stat['count']++; return; } var md5 = file$md5[file]; if (!md5) { md5 = file$md5[file] = MD5.read(file); } file$stat[file] = { 'count': 1, 'md5': md5, }; }); return file$stat; }, /** * 监控 js 文件的变化。 */ watch: function () { var meta = mapper.get(this); //这里不要缓存起来,因为可能在 parse() 中给重设为新的对象。 //var list = meta.list; //var file$md5 = meta.file$md5; var watcher = meta.watcher; if (!watcher) { //首次创建。 watcher = meta.watcher = new Watcher(); var self = this; var emitter = meta.emitter; watcher.on({ 'added': function (files) { }, 'deleted': function (files) { console.log('文件已给删除'.yellow, files); }, //重命名的,会先后触发:deleted 和 renamed 'renamed': function (files) { //emitter.fire('change'); }, 'changed': function (files) { files.forEach(function (file) { //让对应的 md5 记录作废。 meta.file$md5[file] = ''; //根据当前文件名,找到具有相同文件名的节点集合。 var items = $.Array.grep(meta.list, function (item, index) { return item.file == file; }); //对应的 html 作废。 items.forEach(function (item) { meta.lines[item.index] = null; }); }); emitter.fire('change'); }, }); } var files = $.Array.map(meta.list, function (item) { return item.file || null; }); //watcher.set(files); }, /** * */ mix: function () { var meta = mapper.get(this); var list = meta.list; var lines = meta.lines; var file$md5 = meta.file$md5; var replace = $.String.replaceAll; var len = meta.md5; var rid = $.String.random(32); list.forEach(function (item) { var index = item.index; if (lines[index]) { //之前已经生成过了 return; } var build = item.build; var ext = build.ext || item.ext; var dest = item.name + ext + item.suffix; var file = build.file || item.file; if (file) {//引用的是本地文件 var md5 = file$md5[file]; if (!md5) { //动态去获取 md5 值。 md5 = file$md5[file] = MD5.read(file); } md5 = md5.slice(0, len); dest = $Url.addQueryString(dest, md5, rid); dest = replace(dest, md5 + '=' + rid, md5); //为了把类似 'MD5=XXX' 换成 'MD5'。 } var html = replace(item.html, item.src, dest); var line = replace(item.line, item.html, html); lines[index] = line; }); return Lines.join(lines); }, /** * 压缩对应的 js 文件。 */ minify: function (options) { var meta = mapper.get(this); if (options === true) { //直接指定了为 true,则使用默认配置。 options = meta.minify; } //https://github.com/mishoo/UglifyJS2 var UglifyJS = require('uglify-js'); var list = meta.list; list.forEach(function (item) { var ext = item.ext; var opts = options[ext]; if (!opts) { return; } var file = item.file; if (!file) { //外部地址 if (opts.outer) { //指定了替换外部地址为压缩版 item.build.ext = opts.ext; } return; } var result = UglifyJS.minify(file); var content = result.code; if (opts.delete) { //删除源 js文件 FileRefs.delete(file); } var dest = item.name + opts.ext; dest = Path.join(meta.dir, dest); if (opts.write) { if (File.exists(dest)) { if (opts.overwrite) { File.write(dest, content); } } else { File.write(dest, content); } } $.Object.extend(item.build, { 'file': dest, 'ext': opts.ext, 'content': content, }); }); }, /** * 把 js 文件的内容内联到 html 中。 */ inline: function (items) { var meta = mapper.get(this); var list = meta.list; var lines = meta.lines; //重载 inline(); if (!items) { items = $.Array.map(list, function (item) { var file = item.file; if (!file) { return null; } return { 'file': item.file, 'delete': false, //是否删除源 js 文件。 }; }); } items.forEach(function (item) { var file = Path.format(item.file); var content = File.read(file); var items = $.Array.grep(list, function (item) { return item.file == file; }); items.forEach(function (item) { var index = item.index; lines[index] = ' <script>' + content + '</script>'; }); //删除 if (item.delete) { FileRefs.delete(file); } }); return Lines.join(lines); }, /** * 删除列表中所对应的 js 物理文件。 */ 'delete': function () { var meta = mapper.get(this); var list = meta.list; list.forEach(function (item) { FileRefs.delete(item.file); }); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, }; return JsScripts; });
define('HtmlLinks', function (require, module, exports) { var $ = require('$'); var path = require('path'); var File = require('File'); var FileRefs = require('FileRefs'); var Path = require('Path'); var Watcher = require('Watcher'); var Defaults = require('Defaults'); var Lines = require('Lines'); var Attribute = require('Attribute'); var Emitter = $.require('Emitter'); var Url = $.require('Url'); var LogicFile = module.require('LogicFile'); var mapper = new Map(); function HtmlLinks(dir, config) { config = Defaults.clone(module.id, config); //base 为下级页面的基目录。 //假如 base='Detail',而引入下级页面的 href='/panel.html', //则 href='Detai/panel.html',即提供了一种短名称引入下级页面的方式。 var meta = { 'dir': dir, //当前分母版页所在的目录。 'master': '', //当前分母版页的内容。 'lines': [], //html 换行拆分的列表 'list': [], //html 片段文件列表及其它信息。 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 'regexp': config.regexp, // 'base': config.base, //下级页面的基目录。 }; mapper.set(this, meta); } HtmlLinks.prototype = { constructor: HtmlLinks, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); meta.list.forEach(function (item) { item.links.destroy(); //移除之前的子节点实例 FileRefs.delete(item.file); //删除之前的文件引用计数 }); $.Object.extend(meta, { 'master': '', //当前分母版页的内容。 'lines': [], //html 换行拆分的列表 'list': [], //html 片段文件列表及其它信息。 }); }, /** * 从当前或指定的母版页 html 内容中提出 html 标签列表信息 * @param {string} master 要提取的母版页 html 内容字符串。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; //提取出如引用了 html 分文件的 link 标签 var list = master.match(meta.regexp); if (!list) { return; } var lines = Lines.get(master); meta.lines = lines; var startIndex = 0; list = $.Array.map(list, function (item, index) { var index = Lines.getIndex(lines, item, startIndex); var line = lines[index]; //整一行的 html startIndex = index + 1; //下次搜索的起始行号 //所在的行给注释掉了,忽略 if (Lines.commented(line, item)) { return null; } var href = Attribute.get(item, 'href'); if (!href) { return null; } var file = ''; var prefix = Attribute.get(item, 'prefix'); if (prefix) { var selector = ' ' + prefix + '="' + href + '"'; //如 ` data-panel="/User/List" ` var matches = LogicFile.get(selector); file = matches[0]; if (!file) { console.log('无法找到内容中含有 '.bgRed, selector.bgYellow, ' 的 html 文件'.bgRed); throw new Error(); } if (matches.length > 1) { console.log('找到多个内容中含有 '.bgRed, selector.bgYellow, ' 的 html 文件'.bgRed); Log.logArray(matches, 'yellow'); throw new Error(); } href = Path.relative(meta.dir, file); } else { //以 '/' 开头,如 '/panel.html',则补充完名称。 if (href.slice(0, 1) == '/') { href = meta.base + href; } file = path.join(meta.dir, href); } href = Path.format(href); file = Path.format(file); FileRefs.add(file); //添加文件引用计数。 var pad = line.indexOf(item); //前导空格数 pad = new Array(pad + 1).join(' '); //产生指定数目的空格 var dir = Path.dirname(file); //递归下级页面 //下级节点的基目录,根据当前页面的文件名得到 var ext = path.extname(file); var base = path.basename(file, ext); var master = File.read(file); var links = new HtmlLinks(dir, { 'base': base }); var list = links.parse(master); if (list && list.length > 0) { console.log('复合片段'.bgMagenta, file.bgMagenta); } return { 'href': href, //原始地址 'file': file, //完整的物理路径。 'index': index, //行号,从 0 开始 'html': item, //标签的 html 内容 'line': line, //整一行的 html 内容 'pad': pad, //前导空格 'dir': dir, //所在的目录 'name': base, //基本名称,如 'CardList' 'links': links, //下级页面 }; }); meta.list = list; return list; }, /** * 混入(递归)。 * 即把对 html 分文件的引用用所对应的内容替换掉。 */ mix: function (options) { options = options || { 'delete': false, //是否删除源 master 文件,仅提供给上层业务 build 时使用。 }; var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { //没有下级页面 return meta.master; //原样返回 } var lines = meta.lines; list.forEach(function (item, index) { var html = item.links.mix(options); //递归 console.log('混入'.yellow, item.file.green); //在所有行的前面加上空格串,以保持原有的缩进 var pad = item.pad; html = pad + Lines.get(html).join(Lines.seperator + pad); lines[item.index] = html; if (options.delete) { FileRefs.delete(item.file); } }); var html = Lines.join(lines); return html; }, /** * 监控当前 html 文件列表的变化。 */ watch: function () { var meta = mapper.get(this); var emitter = meta.emitter; var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); //因为是静态文件列表,所以只监控文件内容是否发生变化即可。 watcher.on('changed', function (files) { //{ 文件名: [节点, 节点, ..., 节点] },一对多的关系。 files.forEach(function (file) { //根据当前文件名,找到具有相同文件名的节点集合。 //闭包的关系,这里必须用 meta.list,且不能缓存起来。 var items = $.Array.grep(meta.list, function (item, index) { return item.file == file; }); items.forEach(function (item) { var file = item.file; var html = File.read(file); var links = item.links; links.reset(); // links.parse(html); //可能添加或移除了下级子节点 links.watch(); //更新监控列表 }); }); emitter.fire('change'); }); } //监控下级节点所对应的文件列表。 var files = []; meta.list.forEach(function (item) { files.push(item.file); var links = item.links; links.on('change', function () { emitter.fire('change'); }); links.watch(); }); watcher.set(files); }, /** * 删除引用列表中所对应的 html 物理文件。 */ delete: function () { var meta = mapper.get(this); var list = meta.list; list.forEach(function (item) { FileRefs.delete(item.file); item.links.delete(); //递归删除下级的 }); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, /** * 销毁当前对象。 */ destroy: function () { var meta = mapper.get(this); meta.emitter.destroy(); var watcher = meta.watcher; watcher && watcher.destroy(); meta.list.forEach(function (item) { item.links.destroy(); }); mapper.delete(this); }, }; return HtmlLinks; });
define('HtmlList', function (require, module, exports) { var $ = require('$'); var path = require('path'); var Watcher = require('Watcher'); var Patterns = require('Patterns'); var Path = require('Path'); var Defaults = require('Defaults'); var Log = require('Log'); var Mapper = $.require('Mapper'); var Emitter = $.require('Emitter'); var Url = $.require('Url'); var mapper = new Mapper(); //该模块不需要进行资源文件引用计数,交给 HtmlLinks 计数即可。 function HtmlList(dir, config) { Mapper.setGuid(this); config = Defaults.clone(module.id, config); var rid = $.String.random(4); //随机 id var meta = { 'dir': dir, //母版页所在的目录。 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块,即缓存 toHtml() 方法中的返回结果。 'outer': '', //包括开始标记和结束标记在内的原始的整一块的 html。 'patterns': [], //模式列表。 'list': [], //真实 html 文件列表及其它信息。 'emitter': new Emitter(this), 'watcher': null, //监控器,首次用到时再创建 'extraPatterns': config.extraPatterns, //额外附加的模式。 'sample': config.sample, //使用的模板 'tags': config.tags, }; mapper.set(this, meta); } HtmlList.prototype = { constructor: HtmlList, /** * 重置为初始状态,即创建时的状态。 */ reset: function () { var meta = mapper.get(this); $.Object.extend(meta, { 'master': '', //母版页的内容,在 parse() 中用到。 'html': '', //模式所生成的 html 块,即缓存 toHtml() 方法中的返回结果。 'outer': '', //包括开始标记和结束标记在内的原始的整一块的 html。 'patterns': [], //模式列表。 'list': [], //真实 html 文件列表及其它信息。 }); }, /** * 从当前或指定的母版页 html 内容中提出 html 文件列表信息。 * @param {string} master 要提取的母版页 html 内容字符串。 * @return {Array} 返回一个模式数组。 */ parse: function (master) { var meta = mapper.get(this); master = meta.master = master || meta.master; var tags = meta.tags; var dir = meta.dir; var html = $.String.between(master, tags.begin, tags.end); if (!html) { return; } var patterns = $.String.between(html, '<script>', '</script>'); if (!patterns) { return; } //母版页中可能会用到的上下文。 var context = { 'dir': dir, 'master': master, 'tags': tags, }; var fn = new Function('require', 'context', //包装多一层匿名立即执行函数 'return (function () { ' + 'var a = ' + patterns + '; \r\n' + 'return a;' + '})();' ); //执行母版页的 js 代码,并注入变量。 patterns = fn(require, context); if (!Array.isArray(patterns)) { throw new Error('引入文件的模式必须返回一个数组!'); } patterns = patterns.concat(meta.extraPatterns); //跟配置中的模式合并 patterns = Patterns.fill(dir, patterns); patterns = Patterns.combine(dir, patterns); console.log('匹配到'.bgGreen, patterns.length.toString().cyan, '个 html 模式:'); Log.logArray(patterns); meta.patterns = patterns; meta.outer = tags.begin + html + tags.end; }, /** * 根据当前模式获取对应真实的 html 文件列表和其它信息。 */ get: function () { var meta = mapper.get(this); var patterns = meta.patterns; var list = Patterns.getFiles(patterns); meta.list = list = list.map(function (file, index) { file = Path.format(file); var href = path.relative(meta.dir, file); href = Path.format(href); return { 'file': file, 'href': href, }; }); return list; }, /** * 把当前的动态 html 引用模式块转成真实的静态 html 引用所对应的 html。 */ toHtml: function () { var meta = mapper.get(this); var list = meta.list; if (list.length == 0) { meta.html = ''; return; } var tags = meta.tags; var sample = meta.sample; //todo: 检查重复的文件 list = list.map(function (item, index) { return $.String.format(sample, { 'href': item.href, }); }); var Lines = require('Lines'); var seperator = Lines.seperator + ' '; meta.html = list.join(seperator) + seperator; }, /** * 把整一块动态 html 引用模式替换成真实的静态 html 引用。 * @param {string} [master] 要替换的母版 html。 如果不指定,则使用原来的。 * 注意,如果使用新的模板,则该模板中的模式不能变。 */ mix: function (master) { var meta = mapper.get(this); var outer = meta.outer; master = master || meta.master; //实现安全替换 var beginIndex = master.indexOf(outer); var endIndex = beginIndex + outer.length; master = master.slice(0, beginIndex) + meta.html + master.slice(endIndex); return master; }, /** * 监控当前模式下 html 文件的变化。 */ watch: function () { var meta = mapper.get(this); var patterns = meta.patterns; if (patterns.length == 0) { //列表为空,不需要监控 return; } var watcher = meta.watcher; if (!watcher) { //首次创建 watcher = meta.watcher = new Watcher(); var emitter = meta.emitter; var self = this; watcher.on({ 'added': function (files) { self.get(); self.toHtml(); emitter.fire('change'); }, 'deleted': function (files) { self.get(); self.toHtml(); emitter.fire('change'); }, //重命名的,会先后触发:deleted 和 renamed 'renamed': function (files) { self.get(); self.toHtml(); emitter.fire('change'); }, }); } watcher.set(patterns); }, /** * 绑定事件。 */ on: function (name, fn) { var meta = mapper.get(this); var emitter = meta.emitter; var args = [].slice.call(arguments, 0); emitter.on.apply(emitter, args); return this; }, }; return HtmlList; });