UNPKG

20.9 kBJavaScriptView Raw
1/**
2 * @file jdists scope
3 *
4 * Code block processing tools
5 * @author
6 * zswang (http://weibo.com/zswang)
7 * @version 2.2.3
8 * @date 2018-05-30
9 */
10var colors = require('colors');
11var util = require('util');
12var path = require('path');
13var mkdirp = require('mkdirp');
14var jsets = require('jsets');
15var cbml = require('cbml');
16var fs = require('fs');
17var url = require('url');
18var minimatch = require('minimatch');
19/**
20 * 是否无效的文件名
21 *
22 * @param {string} name 文件名
23 * @return {boolean} 如果是无效文件名返回 true 否则返回 false
24 */
25function invalidFilename(name) {
26 if (!name) {
27 return;
28 }
29 return /[<>^|"'?*\t\r\f\v\n\x00-\x06\x0e-\x0f]/.test(name);
30}
31/**
32 * 唯一标识,用来区分缓存内容是否修改
33 *
34 * @type {number}
35 */
36var guid = 0;
37/**
38 * 通过代码获取处理器
39 *
40 * @param {string} body 处理器代码
41 * @return {Funciton} 返回处理器函数
42 */
43function buildProcessor(body) {
44 if (/\bmodule\.exports\s*=/.test(body)) { // module 模式
45 var module = {
46 exports: {}
47 };
48 /*jslint evil: true */
49 new Function('require', 'module', 'exports', body)(
50 require, module, module.exports
51 );
52 return module.exports;
53 }
54 else { // 纯函数
55 /*jslint evil: true */
56 return new Function('require', 'return (' + body + ');')(require);
57 }
58}
59exports.buildProcessor = buildProcessor;
60/**
61 * 创建作用域,一个文件对应一个作用域
62 *
63 * @param {Object} options 配置项
64 * @param {string} options.filename 文件名
65 * @param {Object} options.variants 变量集合
66 * @param {Object} options.tags 被定义的 tags
67 * @param {Object} options.processors 处理器集合
68 * @param {Object} options.argv 控制台参数集合
69 * @param {Object} options.scopes 作用域集合
70 * @param {Array} options.excludeList 排除的文件名
71 * @param {jdistsScope} options.rootScope 顶级作用域
72 * @return {jdistsScope} 返回 jdists 作用域对象
73 */
74function create(options) {
75 options = options || {};
76 var filename = path.resolve('', options.filename || '');
77 var tags = options.tags || {};
78 var clean = options.clean;
79 var removeList = options.removeList || [];
80 var fromString = options.fromString;
81 var contentString = options.content;
82 var instance = {};
83 var argv = options.argv || {};
84 var env = options.env || global.process.env;
85 var rootScope = options.rootScope || instance;
86 var scopes = options.scopes || {};
87 var processors = options.processors || {};
88 var variants = options.variants || {};
89 var cacheKeys = options.cacheKeys || {};
90 var tokens = options.tokens;
91 var excludeList = options.excludeList || [];
92 /**
93 * 清除空行
94 *
95 * @param {string} text 输入文本
96 * @return {string} 返回处理后的结果
97 */
98 function cleanContent(content) {
99 if (!clean) {
100 return content;
101 }
102 return String(content).replace(/^[^\n\S]*$/gm, '') // 清除空行
103 .replace(/\n{2,}/gm, '\n'); // 清除连接的空行
104 }
105 /**
106 * 清除定义
107 *
108 * @param {string} text 输入文本
109 * @return {string} 返回处理后的结果
110 */
111 function cleanDefine(content) {
112 return String(content).replace(/^[^\S\n]*\n|\n[^\S\n]*$/g, '');
113 }
114 /**
115 * 编译 jdists 文件,初始化语法树
116 */
117 function init() {
118 if (tokens) {
119 return;
120 }
121 if (fromString) {
122 tokens = cbml.parse(contentString);
123 }
124 else {
125 tokens = cbml.parse(fs.readFileSync(filename));
126 }
127 /*<debug>
128 console.log(JSON.stringify(tokens, null, ' '));
129 //</debug>*/
130 }
131 scopes[filename] = instance;
132 /**
133 * 编译完整内容,一般用于模板引擎二次处理 jdists
134 *
135 * @param {string} content 可能包含 jdists 代码块的内容
136 * @return {string} 返回编译的结果
137 *
138 * @example
139 * ```js
140 * function processor(content) {
141 * if (!content) {
142 * return content;
143 * }
144 * return scope.compile(
145 * content.replace(/<~/g, '(*<').replae(/~>/g, '>*)')
146 * );
147 * }
148 * ```
149 */
150 function compile(content) {
151 return buildBlock(cbml.parse(content));
152 }
153 instance.compile = compile;
154 /**
155 * 获取控制台参数
156 *
157 * @param {string} name 参数名
158 * @return {string} 返回控制台参数
159 */
160 var getArgument = jsets.createGetter(instance, function (name) {
161 return argv[name];
162 }, false);
163 instance.getArgument = getArgument;
164 /**
165 * 获取环境变量
166 *
167 * @param {string} name 变量名
168 * @return {string} 返回环境变量值
169 */
170 var getEnvironment = jsets.createGetter(instance, function (name) {
171 return env[name];
172 }, false);
173 instance.getEnvironment = getEnvironment;
174 /**
175 * 获取变量值
176 *
177 * @param {string} name 变量名
178 * @return {*} 返回变量值
179 */
180 var getVariant = jsets.createGetter(instance, function (name) {
181 if (!(name in variants)) {
182 console.warn(
183 colors.blue(
184 'Variant "%s" is not set.'
185 ), name
186 );
187 }
188 return variants[name];
189 }, true);
190 instance.getVariant = getVariant;
191 /**
192 * 设置变量值
193 *
194 * @param {string} name 变量名
195 * @param {*} value 变量值
196 * @return {jdistsScope} 返回当前作用域
197 */
198 var setVariant = jsets.createSetter(instance, function (name, value) {
199 if (variants[name] !== value) {
200 variants[name] = value;
201 }
202 }, true);
203 instance.setVariant = setVariant;
204 /**
205 * 获取顶级作用域
206 *
207 * @return {jdistsScope} 返回顶级作用域
208 */
209 instance.getRootScope = function () {
210 return rootScope;
211 };
212 /**
213 * 获取当前文件所在目录
214 *
215 * @return {string} 返回当前文件所在目录
216 */
217 function getDirname() {
218 return path.dirname(filename);
219 }
220 instance.getDirname = getDirname;
221 /**
222 * 获取当前文件名,相对工作目录
223 *
224 * @return {string} 返回当前文件所在目录
225 */
226 function getFilename() {
227 return url.format(path.relative('', filename));
228 }
229 instance.getFilename = getFilename;
230 /**
231 * 获取一个文件的作用域
232 *
233 * @param {string} filename 对应文件名
234 * @return {jdistsScope} 返回文件对应的作用域
235 */
236 function fileScope(filename) {
237 filename = path.resolve('', filename || '');
238 var result = scopes[filename];
239 if (!result) {
240 result = create({
241 clean: clean,
242 removeList: removeList,
243 excludeList: excludeList,
244 rootScope: rootScope,
245 filename: filename,
246 argv: argv,
247 scopes: scopes,
248 processors: processors,
249 variants: variants,
250 cacheKeys: cacheKeys,
251 tags: tags
252 });
253 }
254 return result;
255 }
256 instance.fileScope = fileScope;
257 instance.getScope = fileScope; // forward compatbility
258 /**
259 * 获取内容的作用域
260 *
261 * @param {string} content 对应文件名
262 * @param {string=} file 对应文件名
263 * @return {jdistsScope} 返回文件对应的作用域
264 */
265 function contentScope(content, file) {
266 if (file) { // 已指定
267 file = path.resolve(getDirname(), file);
268 } else { // 未指定
269 file = filename;
270 }
271 return create({
272 fromString: true,
273 content: content,
274 clean: clean,
275 removeList: removeList,
276 excludeList: excludeList,
277 rootScope: rootScope,
278 filename: file,
279 argv: argv,
280 scopes: scopes,
281 processors: processors,
282 variants: variants,
283 cacheKeys: cacheKeys,
284 tags: tags
285 });
286 }
287 instance.contentScope = contentScope;
288 /**
289 * 将内容进行编码
290 *
291 * @param {string} content 内容
292 * @param {string} encoding 编码名称
293 * @param {Object} attrs 属性集合
294 * @return {Function} 返回编码后的结果
295 */
296 function process(content, encoding, attrs, node) {
297 if (typeof content === 'undefined') {
298 console.error(
299 colors.red('process() : Undefined "content" parameters.')
300 );
301 return '';
302 }
303 if (!encoding || encoding === 'original') {
304 return content;
305 }
306 var items = encoding.split(/\s*,\s*/);
307 if (items.length > 1) {
308 items.forEach(function (item) {
309 content = process(content, item, attrs, node);
310 });
311 return content;
312 }
313 var processor = getProcessor(encoding);
314 if (!processor) {
315 console.error(
316 colors.red('The "%s" processor does not exist.'), encoding
317 );
318 return content;
319 }
320 return processor(content, attrs, instance, node);
321 }
322 instance.process = process;
323 /**
324 * 获取处理器
325 *
326 * @inner
327 * @param {string} encoding 编码名称
328 * @return {Function} 返回名称对应的处理器,如果没有找到则返回 undefined
329 */
330 function getProcessor(encoding) {
331 var result;
332 if (/^[\w-_]+$/.test(encoding)) { // 标准编码器
333 result = processors[encoding];
334 if (result) {
335 return result;
336 }
337 var file = path.join(__dirname, '../processor-extend', 'processor-' + encoding + '.js');
338 if (fs.existsSync(file)) {
339 processors[encoding] = require(file);
340 return processors[encoding];
341 }
342 console.warn(
343 colors.blue(
344 'Processor "%s" is not registered.'
345 ), encoding
346 );
347 return;
348 }
349 var item = processors[encoding];
350 if (item) {
351 if (item.cacheKey === cacheKeys[encoding]) { // 处理缓存
352 return item.processor;
353 }
354 else {
355 processors[encoding] = null;
356 }
357 }
358 var body = execImport(encoding);
359 if (!body || body.indexOf('function') < 0) {
360 console.error(colors.red('Invalid encoding %j.'), encoding);
361 return function (content) {
362 return content;
363 };
364 }
365 result = buildProcessor(body);
366 if (/^[#@]/.test(encoding)) { // 缓存编码
367 cacheKeys[encoding] = guid++;
368 processors[encoding] = {
369 cacheKey: cacheKeys[encoding],
370 processor: result
371 };
372 }
373 return result;
374 }
375 /**
376 * 根据搜索表达式查询节点
377 *
378 * @param {string} selector 搜索表达式 "tagName[attrName=attrValue]\*",如果表达式最后一个字符是 '*' 则返回数组
379 * @return {jdistsNode|Array} 返回第一个匹配的节点
380 */
381 function querySelector(selector) {
382 init();
383 if (!selector) {
384 return tokens;
385 }
386 var match = selector.match(
387 /^\s*([\w_-]*)((\s*\[[\w_-]+\s*=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*)\])*)\s*(\*?)$/
388 );
389 if (!match) {
390 console.warn(colors.blue('Invalid selector expressions %j.'), selector);
391 return;
392 }
393 var tag = match[1];
394 var attributes = [];
395 var all = match[9] === '*';
396 match[2].replace(/\s*\[([\w_-]+)\s*=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*)\]/g,
397 function (match, name, value) {
398 if (/^['"]/.test(value)) {
399 /*jslint evil: true */
400 value = new Function('return (' + value + ');')();
401 }
402 attributes.push({
403 name: name,
404 value: value
405 });
406 }
407 );
408 function check(node) {
409 if (!node || !node.attrs) {
410 return;
411 }
412 var flag;
413 if (!tag || node.tag === tag) {
414 flag = true;
415 attributes.every(function (item) {
416 if (item.value !== node.attrs[item.name]) {
417 flag = false;
418 }
419 return flag;
420 });
421 }
422 return flag;
423 }
424 var items;
425 function scan(node) {
426 if (check(node)) {
427 if (items) {
428 items.push(node);
429 }
430 return node;
431 }
432 var result;
433 if (node.nodes) {
434 node.nodes.every(function (item) {
435 result = scan(item);
436 return items || !result;
437 });
438 }
439 return result;
440 }
441 if (all) {
442 items = [];
443 scan(tokens);
444 return items;
445 }
446 else {
447 return scan(tokens);
448 }
449 }
450 instance.querySelector = querySelector;
451 /**
452 * 执行触发器
453 *
454 * @param {string} trigger 触发器表达式
455 * @return {boolean} 返回触发器是否生效
456 */
457 function execTrigger(trigger) {
458 if (!trigger) {
459 return true;
460 }
461 // "trigger1[,trigger2]*"
462 if (/^([\w-_]+)(,[\w-_]+)*$/.test(trigger)) {
463 var a1 = String(getArgument('trigger')).split(',');
464 var a2 = trigger.split(',');
465 var flag = false;
466 a1.every(function (item) {
467 if (a2.indexOf(item) >= 0) {
468 flag = true;
469 }
470 return !flag;
471 });
472 return flag;
473 }
474 // "@trigger === 'debug'"
475 // "#variant === 'debug'"
476 // ":environment === 'debug'"
477 /*jslint evil: true */
478 return new Function('return (' +
479 trigger.replace(/(@|#|:)([\w-_]+)/g, function (all, flag, name) {
480 if (flag === '@') {
481 return JSON.stringify(getArgument(name));
482 }
483 else if (flag === ':') {
484 return JSON.stringify(getEnvironment(name));
485 }
486 else {
487 return JSON.stringify(getVariant(name));
488 }
489 }) +
490 ')'
491 )();
492 }
493 instance.execTrigger = execTrigger;
494 /**
495 * 执行文件排除
496 *
497 * @param {string} file 文件名
498 * @return {boolean} 返回文件是否被排除
499 */
500 function execExclude(file) {
501 var result = false;
502 excludeList.every(function (item) {
503 if (minimatch(file, item)) {
504 result = true;
505 }
506 return !result;
507 });
508 return result;
509 }
510 /**
511 * 执行数据导入
512 *
513 * @param {string} importation 导入项表达式 : "#variant" 内存, "@argv" 属性, "filename[?selector]" 文件和代码块
514 * @param {Array=} froms 来源结婚,默认全部
515 * @return {string} 返回导入的内容
516 */
517 function execImport(importation, froms) {
518 if (!importation) {
519 return importation;
520 }
521 if (importation.indexOf('#') === 0) { // variants
522 if (froms instanceof Array && froms.indexOf('variant') < 0) {
523 return importation;
524 }
525 return getVariant(importation.slice(1));
526 }
527 if (importation.indexOf('@') === 0) { // argv
528 if (froms instanceof Array && froms.indexOf('argv') < 0) {
529 return importation;
530 }
531 return getArgument(importation.slice(1));
532 }
533 if (importation.indexOf(':') === 0) { // env
534 if (froms instanceof Array && froms.indexOf('env') < 0) {
535 return importation;
536 }
537 return getEnvironment(importation.slice(1));
538 }
539 if (/^'([^\\']*(\\.)*)*'$/.test(importation)) { // 字符串输出
540 /*jslint evil: true */
541 return new Function('return (' + importation + ');')();
542 }
543 if (/^[\[\{"]/.test(importation)) { // 可能是 JSON
544 return importation;
545 }
546 if (froms instanceof Array && froms.indexOf('file') < 0) {
547 return importation;
548 }
549 // file
550 var items = importation.split('?');
551 var name = items[0];
552 if (invalidFilename(name)) { // 无效文件名
553 return importation;
554 }
555 var selector = items[1];
556 var scope;
557 if (!name) {
558 scope = instance;
559 }
560 else {
561 var file = path.resolve(getDirname(), name);
562 if (!fs.existsSync(file)) { // 文件不存在原样
563 return importation;
564 }
565 if (execExclude(file)) {
566 if (!selector) {
567 return fs.readFileSync(file);
568 }
569 else {
570 console.error(
571 colors.red('File "%s" has been ruled exclude, unable to code block import.'), file
572 );
573 return '';
574 }
575 }
576 scope = fileScope(file);
577 }
578 if (selector) {
579 var node = scope.querySelector(selector);
580 if (!node) {
581 console.error(
582 colors.red('Selector "%s" is no matching nodes.'), selector
583 );
584 return '';
585 }
586 if (node instanceof Array) {
587 return node.map(function (item) {
588 return scope.buildBlock(item, true);
589 }).join('\n');
590 }
591 return scope.buildBlock(node, true);
592 }
593 else {
594 if (instance === scope) { // 不能引用自己
595 console.error(
596 colors.red('Cannot reference himself.')
597 );
598 return '';
599 }
600 return scope.build(instance);
601 }
602 }
603 instance.execImport = execImport;
604 /**
605 * 执行数据导出
606 *
607 * @param {string} exportation 导出项表达式 : "#variant" 内存, "filename" 文件
608 * @return {boolean} 返回导出是否成功
609 */
610 function execExport(exportation, content) {
611 if (!exportation) {
612 return;
613 }
614 if (exportation.indexOf('@') === 0) { // argv
615 console.error(colors.red('Argv is readonly.'));
616 return;
617 }
618 if (exportation.indexOf(':') === 0) { // env
619 console.error(colors.red('Env is readonly.'));
620 return;
621 }
622 if (exportation.indexOf('#') === 0) { // variants
623 setVariant(exportation.slice(1), content);
624 cacheKeys[exportation] = null;
625 return true;
626 }
627 else if (!invalidFilename(exportation)) {
628 var name = path.resolve(getDirname(), exportation);
629 cacheKeys[name] = null;
630 if (fs.existsSync(name)) {
631 console.warn(
632 colors.blue('File "%s" overwrite.'), name
633 );
634 }
635 else {
636 mkdirp.sync(path.dirname(name));
637 }
638 fs.writeFileSync(name, content);
639 scopes[name] = null;
640 return true;
641 }
642 else {
643 console.error(colors.red('Export %j invalid.'), exportation);
644 return;
645 }
646 }
647 instance.execExport = execExport;
648 function isYes(text) {
649 return /^(true|on|yes|ok)$/i.test(text);
650 }
651 instance.isYes = isYes;
652 function isNo(text) {
653 return /^(false|off|no)$/i.test(text);
654 }
655 instance.isNo = isNo;
656 /**
657 * 编译 CBML 标签节点
658 *
659 * @param {jdistsNode} node 该节点
660 * @param {boolean} isImport 是否为导入方式
661 * @return {string} 返回编译后的内容,如果 isImport 为 true 时,不返回前后缀
662 */
663 function buildBlock(node, isImport) {
664 if (!node) {
665 return '';
666 }
667 init();
668 if (node.pending) {
669 var error = util.format('A circular reference. (%s:%d:%d)',
670 filename, node.line || 0, node.col || 0
671 );
672 console.error(colors.red(error));
673 throw error;
674 }
675 if (node.fixed) { // 已经编译过
676 if (isImport) { // 未触发的 tag
677 return node.content;
678 }
679 return node.value;
680 }
681 if (node.type === 'text') { // 文本节点直接返回
682 return node.value;
683 }
684 node.pending = true;
685 var value = '';
686 var fixed = true;
687 if (node.attrs && isYes(node.attrs.important)) {
688 value = node.content;
689 } else if (node.nodes) {
690 node.nodes.forEach(function (item) {
691 var text = buildBlock(item);
692 if (!item.fixed && item.type !== 'text') {
693 fixed = false;
694 }
695 if (item.altered) {
696 value = value.replace(/[^\S\n]*$/, '');
697 }
698 value += text;
699 });
700 }
701 var tagInfo = tags[node.tag];
702 var isTrigger = true;
703 node.altered = false;
704 if (node.attrs && node.attrs.trigger) {
705 isTrigger = execTrigger(node.attrs.trigger);
706 }
707 if (tagInfo && isTrigger) { // 已注册 tag
708 if (node.attrs.import && node.attrs.import !== '&') {
709 value = execImport(node.attrs.import);
710 if (!/^[@:]/.test(node.attrs.import)) {
711 fixed = false;
712 }
713 }
714 else {
715 value = cleanDefine(value);
716 }
717 value = process(value, node.attrs.encoding || tagInfo.encoding,
718 node.attrs, node);
719 if (/^\s+/.test(value)) {
720 node.altered = true;
721 }
722 if (typeof value === 'object') {
723 value = JSON.stringify(value);
724 } else {
725 value = String(value);
726 }
727 node.content = value;
728 if (node.attrs.export && node.attrs.export !== '&') {
729 execExport(node.attrs.export, value);
730 value = '';
731 fixed = false;
732 }
733 else if (removeList.indexOf(node.tag) >= 0) { // 这是被移除的节点
734 value = '';
735 }
736 }
737 else if (node.tag) { // 并不是根目录
738 node.content = cleanDefine(value);
739 if (isTrigger && removeList.indexOf(node.tag) >= 0) { // 这是被移除的节点
740 node.altered = false;
741 value = '';
742 }
743 else if (node.type !== 'single') { // 非空内容标签
744 value = node.prefix + value + node.suffix;
745 }
746 else {
747 value = node.value;
748 }
749 }
750 else {
751 value = cleanContent(value);
752 }
753 node.pending = false;
754 node.value = value;
755 node.fixed = fixed;
756 if (isImport) {
757 return node.content;
758 }
759 return value;
760 }
761 instance.buildBlock = buildBlock;
762 /**
763 * 编译当前作用域
764 *
765 * @return {string} 返回编译后的结果
766 */
767 function build() {
768 init();
769 if (tokens.fixed) { // 已经被编译过
770 return tokens.value;
771 }
772 return buildBlock(tokens);
773 }
774 instance.build = build;
775 return instance;
776}
777exports.create = create;
\No newline at end of file