cbml
Version:
CBML Parser
589 lines (588 loc) • 16.4 kB
text/typescript
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
}
}