UNPKG

cbml

Version:
556 lines (555 loc) 19.9 kB
(function (root, factory) { /* istanbul ignore next */ if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports"], factory); } else { factory(null, root["cbml"] = {}); } })(this, function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // #endregion /** * @file cbml * * CBML Parser * @author * zswang (http://weibo.com/zswang) * @version 1.0.0-alpha.33 * @date 2017-10-27 */ var htmlDecodeDict = { 'quot': '"', 'lt': '<', 'gt': '>', 'amp': '&', 'nbsp': '\xa0', }; function decodeHTML(html) { return html.replace(/&((quot|lt|gt|amp|nbsp)|#x([a-f\d]+)|#(\d+));/ig, function (all, group, key, hex, dec) { return key ? htmlDecodeDict[key.toLowerCase()] : hex ? String.fromCharCode(parseInt(hex, 16)) : String.fromCharCode(dec); }); } function calcPosition(code, pos) { var buffer = code.slice(0, pos).split('\n'); return { line: buffer.length, column: buffer[buffer.length - 1].length + 1, }; } function calcLocation(code, start, end, source) { return { source: source ? code.slice(start, end) : null, start: calcPosition(code, start), end: calcPosition(code, end), }; } /** * 支持的语言形式 */ var LanguageMap = { xml: { pattern: ['<!--', '-->'], closed: /^\s*(\/?-->)/, closed2: /^\s*(\/?-->|>)/, }, c: { pattern: ['/*<', '>*/'], closed: /^\s*(\/?>\*\/)/, closed2: /^\s*(\/?>\*\/|>)/, }, pascal: { pattern: ['(*<', '>*)'], closed: /^\s*(\/?>\*\))/, closed2: /^\s*(\/?>\*\)|>)/, }, python: { pattern: ["'''<", ">'''"], closed: /^\s*(\/?>''')/, closed2: /^\s*(\/?>'''|>)/, }, lua: { pattern: ['--[[<', '>]]'], closed: /^\s*(\/?>\]\])/, closed2: /^\s*(\/?>\]\]|>)/, } }; /** * 计算代码块属于的语言类型 * @param text 文本 * @return 返回代码块所属语言类型 */ function calcLanguage(text) { var result; Object.keys(LanguageMap).some(function (key) { var language = LanguageMap[key]; if (language.pattern.indexOf(text) >= 0) { result = key; return true; } }); return result; } /** * 扫描文本段包含 CBML 的语法元素 * * @param code 代码段 * @param options 配置项 * @param scan 扫描函数 */ function tokenizer(code, options, scan) { var start = 0; var end = 0; /** * 记录代码位置信息,并添加 CBML 节点 * @param node CBML 节点 */ function append(node) { if (!options.loc) { node.loc = null; } else { node.loc = calcLocation(code, start, end, options.source); } node.range = [start, end]; scan(node); start = end; } var _loop_1 = function () { var match = code.slice(start).match(/(<!--|\/\*<|\(\*<|'''<|--\[\[<)(\/?)([\w_]+[\w_-]*[\w_]|[\w_]+)\s*|(<\/)([\w_]+[\w_-]*[\w_]|[\w_]+)(>\*\/|>\*\)|>'''|>\]\]|-->)/); // 没有 CBML 元素 end = match ? start + match.index : code.length; if (end > start) { append({ type: 'TextNode', content: code.slice(start, end), loc: null, }); } if (!match) { return "break"; } var tag = match[3]; end += match[0].length; if (!tag) { // 右边闭合 tag = match[5]; append({ type: 'RightCommentTokenizer', attributes: null, tag: tag, language: calcLanguage(match[6]), }); return "continue"; } var language = calcLanguage(match[1]); if (match[2] === '/') { var find_1 = LanguageMap[language].closed; match = code.slice(end).match(find_1); // 《!--/jdists「--》」 if (!match) { var pos = calcPosition(code, end); throw "Uncaught SyntaxError: Can't match " + find_1 + ". (line: " + pos.line + ", col: " + pos.column + ")"; } end += match[0].length; append({ type: 'RightBlockTokenizer', attributes: null, tag: tag, language: language, }); return "continue"; } // 属性 // 《!--/jdists「file="1.js" clean」--》」 // find attrs var attributes = {}; while (true) { // find attrName match = code.slice(end).match(/^\s*([\w_]+[\w_-]*[\w_]|[\w_]+)\s*/); if (!match) { break; } end += match[0].length; var name_1 = match[1]; var attribute = { value: '', quoted: '', }; var value = ''; // find attrValue match = code.slice(end).match(/^\s*=\s*('([^']*)'|"([^"]*)"|([^'"\s\/>]+))\s*/); if (match) { end += match[0].length; value = match[1]; } switch (value[0]) { case '"': case "'": attribute.quoted = value[0]; value = value.slice(1, -1); break; } attribute.value = decodeHTML(value); if (attributes[name_1] === undefined) { attributes[name_1] = attribute; } } var find = LanguageMap[language].closed2; match = code.slice(end).match(find); if (!match) { if (language === 'xml') { match = code.slice(end).match(/^[^]*?-->/); if (match) { end += match[0].length; } else { end = code.length; } append({ type: 'TextNode', content: code.slice(start, end), loc: null, }); return "continue"; } if (language === 'c') { if (Object.keys(attributes).some(function (name) { var attribute = attributes[name]; return attribute.quoted === '' && /^\s*\{/.test(attribute.value); })) { match = code.slice(end).match(/^[^]*?>\*\//); if (match) { end += match[0].length; } else { end = code.length; } append({ type: 'TextNode', content: code.slice(start, end), loc: null, }); return "continue"; } } var pos = calcPosition(code, end); throw "Uncaught SyntaxError: Can't match " + find + ". (line: " + pos.line + ", col: " + pos.column + ")"; } end += match[0].length; var element = void 0; if (match[1] === '>') { element = { type: 'LeftCommentTokenizer', attributes: attributes, tag: tag, language: language, body: [], }; } else if (match[1][0] === '/') { element = { type: 'VoidElement', attributes: attributes, tag: tag, language: language, }; } else { element = { type: 'LeftBlockTokenizer', attributes: attributes, tag: tag, language: language, body: [], }; } append(element); }; while (start < code.length) { var state_1 = _loop_1(); if (state_1 === "break") break; } } /** * 将没有闭合的左侧标签转换成文本节点,并合并相邻的文本节点 * * @param code 当前代码 * @param body 父节点 */ function merge(code, parent) { if (!parent || !parent.body) { return; } for (var i = parent.body.length - 1; i >= 0; i--) { var node = parent.body[i]; var container = node; merge(code, container); if (['LeftBlockTokenizer', 'LeftCommentTokenizer'].indexOf(node.type) >= 0) { var token = node; var TextNode = { type: 'TextNode', loc: node.loc, content: code.slice(token.range[0], token.range[1]), range: token.range, }; node = parent.body[i] = TextNode; var firstChild = token.body[0]; if (firstChild) { var lastChild = token.body[token.body.length - 1]; if (node.loc) { node.loc.end = { line: firstChild.loc.start.line, column: firstChild.loc.start.column, }; } var params = [i + 1, 0]; params.push.apply(params, token.body); params.splice.apply(parent.body, params); } } var nextNode = parent.body[i + 1]; if (nextNode && node.type === 'TextNode' && nextNode.type === 'TextNode') { var t = node; nextNode.range[0] = t.range[0]; if (nextNode.loc) { nextNode.loc.start = { line: node.loc.start.line, column: node.loc.start.column, }; if (nextNode.loc.source) { nextNode.loc.source = code.slice(nextNode.range[0], nextNode.range[1]); } } nextNode.content = code.slice(nextNode.range[0], nextNode.range[1]); parent.body.splice(i, 1); } } } /** * 清除仅解析期使用的字段 * * @param parent 容器节点 */ function clean(parent) { delete parent.range; if (!parent.body) { return; } parent.body.forEach(function (node) { clean(node); }); } /** * 解析代码片段包含的 CBML 元素 * * @param code 代码片段 * @param options 配置项 * @return 返回 CBML 元素 */ function parse(code, options) { if (code === null || code === undefined) { return null; } options = options || {}; options.loc = options.loc === undefined ? true : options.loc; options.source = options.source === undefined ? true : options.source; options.range = options.range === undefined ? false : options.range; code = String(code); var loc = options.loc ? calcLocation(code, 0, code.length, options.source) : null; var result = { language: 'cbml', tag: '#cbml', attributes: {}, type: 'CBMLElement', body: [], loc: loc, range: [0, code.length], }; if (!code) { return result; } var lefts = []; // 左边标签集合,用于寻找配对的右边标签 var current = result; // 当前容器 tokenizer(code, options, function (token) { switch (token.type) { case 'VoidElement': case 'TextNode': current.body.push(token); current.range[1] = token.range[1]; break; case 'LeftBlockTokenizer': case 'LeftCommentTokenizer': current.body.push(token); var container = token; lefts.push(container); current = container; break; case 'RightBlockTokenizer': case 'RightCommentTokenizer': var tokenizer_1 = token; for (var i = lefts.length - 1;; i--) { var curr = lefts[i]; var prev = lefts[i - 1]; if (!curr) { throw "No start tag. (" + tokenizer_1.tag + ")"; } if (curr.tag == tokenizer_1.tag && curr.language == tokenizer_1.language && curr.type.slice('Left'.length) === token.type.slice('Right'.length)) { // 匹配 if (token.type === 'RightBlockTokenizer') { curr.type = 'BlockElement'; } else { curr.type = 'CommentElement'; } curr.range[1] = tokenizer_1.range[1]; if (curr.loc) { curr.loc.end = { line: tokenizer_1.loc.end.line, column: tokenizer_1.loc.end.column, }; if (curr.loc.source) { curr.loc.source = code.slice(curr.range[0], curr.range[1]); } } if (prev) { current = prev; } else { current = result; } lefts = lefts.slice(0, i); break; } if (prev) { var tokenizer_2 = curr; // replace tokenizer lefts[i] = { type: 'TextNode', content: code.slice(tokenizer_2.range[0], tokenizer_2.range[1]), loc: tokenizer_2.loc, range: tokenizer_2.range, }; } } break; } }); merge(code, result); if (!options.range) { clean(result); } return result; } exports.parse = parse; /** * 搜索满足表达式的节点 * * @param root 根节点 * @param selector 选择器表达式 * @example querySelector():base ```js var root = cbml.parse(` <!~remove trigger="release">remove1</remove~> <!~remove trigger="debug">remove2</remove~> <!~remove trigger>remove3</remove~> <!~remove>remove4</remove~> `) console.log(cbml.querySelector(root, 'remove').loc.source) // > <!~remove trigger="release">remove1</remove~> console.log(cbml.querySelector(root, 'remove[trigger]').loc.source) // > <!~remove trigger="release">remove1</remove~> console.log(cbml.querySelector(root, 'remove[trigger="debug"]').loc.source) // > <!~remove trigger="debug">remove2</remove~> console.log(cbml.querySelector(root, 'remove*').length) // > 4 console.log(cbml.querySelector(root, 'remove[trigger]*').length) // > 3 console.log(cbml.querySelector(root, 'remove[trigger="debug"]*').length) // > 1 ``` * @example querySelector():coverage ```js var root = cbml.parse(` <!~remove trigger="release">remove1</remove~> <!~remove trigger="debug">remove2</remove~> <!~remove trigger>remove3</remove~> <!~remove>remove4</remove~> `) console.log(cbml.querySelector(root, 'dev')) // > null console.log(cbml.querySelector(root)) // > null console.log(cbml.querySelector()) // > null ``` * @example querySelector():throw ```js var root = cbml.parse(` <!~remove trigger="release">remove1</remove~> `) cbml.querySelector(root, 'dev//1') // * throw ``` */ function querySelector(root, selector) { if (!root || !selector) { return null; } var match = selector.match(/^\s*([\w_-]*)((\s*\[[\w_-]+\s*(=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*))?\])*)\s*(\*?)$/); if (!match) { throw JSON.stringify(selector) + " is not a valid selector."; } var tag = match[1]; var all = match[10] === '*'; var attributes = {}; match[2].replace(/\s*\[([\w_-]+)\s*(=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*))?\]/g, function (match, name, expr, value) { if (/^['"]/.test(value)) { /*jslint evil: true */ value = new Function('return (' + value + ');')(); } attributes[name] = value; return ''; }); function check(element) { var result = false; if (!tag || element.tag === tag) { result = Object.keys(attributes).every(function (name) { if (!element.attributes[name]) { return false; } if (!attributes[name] && attributes[name] !== '') { return true; } return element.attributes[name].value === attributes[name]; }); } return result; } var items = []; function scan(element) { if (check(element)) { items.push(element); return true; } var parent = element; if (parent.body) { parent.body.some(function (item) { return scan(item) && !all; }); } } scan(root); if (all) { return items; } else { return items[0] || null; } } exports.querySelector = querySelector; });