UNPKG

cbml

Version:
589 lines (588 loc) 16.4 kB
import * as cbml from './ast' export interface IParserOptions { /** * 是否获取位置信息,默认:true */ loc?: boolean /** * 是否获取源代码信息,默认:true */ source?: boolean /** * 是否获取代码范围信息,默认:false */ range?: boolean } // #region 临时语法元素 /** * 文本节点 */ interface TextNodeTokenizer extends cbml.TextNode { type: 'TextNode' } /** * 启始普通块标记 */ interface LeftBlockTokenizer extends cbml.ContainerElement { type: 'LeftBlockTokenizer' } /** * 启始注释块标记 */ interface LeftCommentTokenizer extends cbml.ContainerElement { type: 'LeftCommentTokenizer' } /** * 结束普通块标记 */ interface RightBlockTokenizer extends cbml.ContainerElement { type: 'RightBlockTokenizer' } /** * 结束注释块标记 */ interface RightCommentTokenizer extends cbml.ContainerElement { type: 'RightCommentTokenizer' } // #endregion /** * @file cbml * * CBML Parser * @author * zswang (http://weibo.com/zswang) * @version 1.0.0-alpha.33 * @date 2017-10-27 */ const htmlDecodeDict = { 'quot': '"', 'lt': '<', 'gt': '>', 'amp': '&', 'nbsp': '\xa0', } function decodeHTML(html: string): string { return html.replace( /&((quot|lt|gt|amp|nbsp)|#x([a-f\d]+)|#(\d+));/ig, (all, group, key, hex, dec) => { return key ? htmlDecodeDict[key.toLowerCase()] : hex ? String.fromCharCode(parseInt(hex, 16)) : String.fromCharCode(dec) } ) } function calcPosition(code: string, pos: number): cbml.Position { let buffer = code.slice(0, pos).split('\n') return { line: buffer.length, column: buffer[buffer.length - 1].length + 1, } } function calcLocation(code: string, start: number, end: number, source: boolean): cbml.SourceLocation { return { source: source ? code.slice(start, end) : null, start: calcPosition(code, start), end: calcPosition(code, end), } } /** * 支持的语言形式 */ const 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: string): cbml.Language { let result Object.keys(LanguageMap).some((key) => { let language = LanguageMap[key] if (language.pattern.indexOf(text) >= 0) { result = key return true } }) return result } /** * 扫描函数类型 */ interface IScanNodeFunction { (token: cbml.Node) } /** * 扫描文本段包含 CBML 的语法元素 * * @param code 代码段 * @param options 配置项 * @param scan 扫描函数 */ function tokenizer(code: string, options: IParserOptions, scan: IScanNodeFunction) { let start = 0 let end = 0 /** * 记录代码位置信息,并添加 CBML 节点 * @param node CBML 节点 */ function append(node: cbml.Node) { if (!options.loc) { node.loc = null } else { node.loc = calcLocation(code, start, end, options.source) } node.range = [start, end] scan(node) start = end } while (start < code.length) { let 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, } as TextNodeTokenizer) } if (!match) { break } let tag = match[3] end += match[0].length if (!tag) { // 右边闭合 tag = match[5] append({ type: 'RightCommentTokenizer', attributes: null, tag: tag, language: calcLanguage(match[6]), } as RightCommentTokenizer) continue } let language = calcLanguage(match[1]) if (match[2] === '/') { // 闭合便签 // 《!--「/」jdists--》 let find = LanguageMap[language].closed match = code.slice(end).match(find) // 《!--/jdists「--》」 if (!match) { let pos = calcPosition(code, end) throw `Uncaught SyntaxError: Can't match ${find}. (line: ${pos.line}, col: ${pos.column})` } end += match[0].length append({ type: 'RightBlockTokenizer', attributes: null, tag: tag, language: language, } as RightBlockTokenizer) continue } // 属性 // 《!--/jdists「file="1.js" clean」--》」 // find attrs let attributes: cbml.Attributes = {} while (true) { // find attrName match = code.slice(end).match(/^\s*([\w_]+[\w_-]*[\w_]|[\w_]+)\s*/) if (!match) { break } end += match[0].length let name = match[1] let attribute: cbml.Attribute = { value: '', quoted: '', } let 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] === undefined) { attributes[name] = attribute } } let find = LanguageMap[language].closed2 match = code.slice(end).match(find) if (!match) { if (language === 'xml') { // xml 则宽松一些 // <!~1. line<br>~> 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, } as TextNodeTokenizer) continue } if (language === 'c') { // jsx if (Object.keys(attributes).some((name) => { let 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, } as TextNodeTokenizer) continue } } let pos = calcPosition(code, end) throw `Uncaught SyntaxError: Can't match ${find}. (line: ${pos.line}, col: ${pos.column})` } end += match[0].length let element: cbml.Element if (match[1] === '>') { // 需要闭合 // 《!--/jdists》...《/jdists--》」 element = { type: 'LeftCommentTokenizer', attributes: attributes, tag: tag, language: language, body: [], } as LeftCommentTokenizer } else if (match[1][0] === '/') { element = { type: 'VoidElement', attributes: attributes, tag: tag, language: language, } as cbml.VoidElement } else { element = { type: 'LeftBlockTokenizer', attributes: attributes, tag: tag, language: language, body: [], } as LeftBlockTokenizer } append(element) } } /** * 将没有闭合的左侧标签转换成文本节点,并合并相邻的文本节点 * * @param code 当前代码 * @param body 父节点 */ function merge(code: string, parent: cbml.ContainerElement) { if (!parent || !parent.body) { return } for (let i = parent.body.length - 1; i >= 0; i--) { let node = parent.body[i] let container = node as cbml.ContainerElement merge(code, container) if (['LeftBlockTokenizer', 'LeftCommentTokenizer'].indexOf(node.type) >= 0) { let token = node as LeftBlockTokenizer let TextNode: TextNodeTokenizer = { type: 'TextNode', loc: node.loc, content: code.slice(token.range[0], token.range[1]), range: token.range, } node = parent.body[i] = TextNode let firstChild = token.body[0] if (firstChild) { let lastChild = token.body[token.body.length - 1] if (node.loc) { node.loc.end = { line: firstChild.loc.start.line, column: firstChild.loc.start.column, } } let params = [i + 1, 0] params.push.apply(params, token.body) params.splice.apply(parent.body, params) } } let nextNode = parent.body[i + 1] as TextNodeTokenizer if (nextNode && node.type === 'TextNode' && nextNode.type === 'TextNode') { // 合并文本 let t = node as TextNodeTokenizer 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: cbml.ContainerElement) { delete parent.range if (!parent.body) { return } parent.body.forEach((node) => { clean(node as cbml.ContainerElement) }) } /** * 解析代码片段包含的 CBML 元素 * * @param code 代码片段 * @param options 配置项 * @return 返回 CBML 元素 */ export function parse(code: string, options?: IParserOptions): cbml.CBMLElement { 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) let loc: cbml.SourceLocation = options.loc ? calcLocation(code, 0, code.length, options.source) : null let result: cbml.CBMLElement = { language: 'cbml', tag: '#cbml', attributes: {}, type: 'CBMLElement', body: [], loc: loc, range: [0, code.length], } if (!code) { return result } let lefts: cbml.Node[] = [] // 左边标签集合,用于寻找配对的右边标签 let current: cbml.ContainerElement = result // 当前容器 tokenizer(code, options, (token: cbml.Node) => { 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) let container = token as LeftBlockTokenizer lefts.push(container) current = container break case 'RightBlockTokenizer': case 'RightCommentTokenizer': let tokenizer = token as RightBlockTokenizer for (let i = lefts.length - 1; ; i--) { let curr = lefts[i] as cbml.ContainerElement let prev = lefts[i - 1] if (!curr) { throw `No start tag. (${tokenizer.tag})` } if (curr.tag == tokenizer.tag && curr.language == tokenizer.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.range[1] if (curr.loc) { curr.loc.end = { line: tokenizer.loc.end.line, column: tokenizer.loc.end.column, } if (curr.loc.source) { curr.loc.source = code.slice(curr.range[0], curr.range[1]) } } if (prev) { current = prev as cbml.ContainerElement } else { current = result } lefts = lefts.slice(0, i) break } if (prev) { let tokenizer = curr as LeftBlockTokenizer // replace tokenizer lefts[i] = { type: 'TextNode', content: code.slice(tokenizer.range[0], tokenizer.range[1]), loc: tokenizer.loc, range: tokenizer.range, } as TextNodeTokenizer } } break } }) merge(code, result) if (!options.range) { clean(result) } return result } /** * 搜索满足表达式的节点 * * @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 ``` */ export function querySelector(root: cbml.ContainerElement, selector: string): cbml.Node | cbml.Node[] { if (!root || !selector) { return null } let match = selector.match(/^\s*([\w_-]*)((\s*\[[\w_-]+\s*(=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*))?\])*)\s*(\*?)$/) if (!match) { throw `${JSON.stringify(selector)} is not a valid selector.` } let tag = match[1] let all = match[10] === '*'; let attributes: { [name: string]: string } = {} match[2].replace(/\s*\[([\w_-]+)\s*(=\s*("([^\\"]*(\\.)*)*"|'([^\\']*(\\.)*)*'|[^\[\]]*))?\]/g, (match, name, expr, value) => { if (/^['"]/.test(value)) { /*jslint evil: true */ value = new Function('return (' + value + ');')() } attributes[name] = value; return '' } ) function check(element: cbml.Element): boolean { let result = false if (!tag || element.tag === tag) { result = Object.keys(attributes).every((name) => { if (!element.attributes[name]) { return false } if (!attributes[name] && attributes[name] !== '') { return true } return element.attributes[name].value === attributes[name] }) } return result } let items: cbml.Element[] = [] function scan(element: cbml.Element): boolean { if (check(element)) { items.push(element) return true } let parent = element as cbml.ContainerElement if (parent.body) { parent.body.some((item) => { return scan(item as cbml.Element) && !all }) } } scan(root) if (all) { return items } else { return items[0] || null } }