UNPKG

@bbob/parser

Version:

A BBCode to AST Parser part of @bbob

274 lines (273 loc) 8.55 kB
import { CLOSE_BRAKET, OPEN_BRAKET, TagNode, isTagNode } from "@bbob/plugin-helper"; import { createLexer } from "./lexer"; class NodeList { last() { if (Array.isArray(this.n) && this.n.length > 0 && typeof this.n[this.n.length - 1] !== "undefined") { return this.n[this.n.length - 1]; } return null; } flush() { return this.n.length ? this.n.pop() : false; } push(value) { this.n.push(value); } toArray() { return this.n; } constructor(){ this.n = []; } } const createList = ()=>new NodeList(); function parse(input, opts = {}) { const options = opts; const openTag = options.openTag || OPEN_BRAKET; const closeTag = options.closeTag || CLOSE_BRAKET; const onlyAllowTags = (options.onlyAllowTags || []).filter(Boolean).map((tag)=>tag.toLowerCase()); const caseFreeTags = options.caseFreeTags || false; let tokenizer = null; /** * Result AST of nodes * @private * @type {NodeList} */ const nodes = createList(); /** * Temp buffer of nodes that's nested to another node * @private */ const nestedNodes = createList(); /** * Temp buffer of nodes [tag..]...[/tag] * @private * @type {NodeList} */ const tagNodes = createList(); /** * Temp buffer of tag attributes * @private * @type {NodeList} */ const tagNodesAttrName = createList(); /** * Cache for nested tags checks */ const nestedTagsMap = new Set(); function isTokenNested(token) { const tokenValue = token.getValue(); const value = caseFreeTags ? tokenValue.toLowerCase() : tokenValue; const { isTokenNested } = tokenizer || {}; if (!nestedTagsMap.has(value) && isTokenNested && isTokenNested(value)) { nestedTagsMap.add(value); return true; } return nestedTagsMap.has(value); } /** * @private */ function isTagNested(tagName) { return Boolean(nestedTagsMap.has(caseFreeTags ? tagName.toLowerCase() : tagName)); } /** * @private */ function isAllowedTag(value) { if (onlyAllowTags.length) { return onlyAllowTags.indexOf(value.toLowerCase()) >= 0; } return true; } /** * Flushes temp tag nodes and its attributes buffers * @private */ function flushTagNodes() { if (tagNodes.flush()) { tagNodesAttrName.flush(); } } /** * @private */ function getNodes() { const lastNestedNode = nestedNodes.last(); if (lastNestedNode && isTagNode(lastNestedNode)) { return lastNestedNode.content; } return nodes.toArray(); } /** * @private */ function appendNodeAsString(nodes, node, isNested = true) { if (Array.isArray(nodes) && typeof node !== "undefined") { nodes.push(node.toTagStart({ openTag, closeTag })); if (Array.isArray(node.content) && node.content.length) { node.content.forEach((item)=>{ nodes.push(item); }); if (isNested) { nodes.push(node.toTagEnd({ openTag, closeTag })); } } } } /** * @private */ function appendNodes(nodes, node) { if (Array.isArray(nodes) && typeof node !== "undefined") { if (isTagNode(node)) { if (isAllowedTag(node.tag)) { nodes.push(node.toTagNode()); } else { appendNodeAsString(nodes, node); } } else { nodes.push(node); } } } /** * @private * @param {Token} token */ function handleTagStart(token) { flushTagNodes(); const tagNode = TagNode.create(token.getValue(), {}, [], { from: token.getStart(), to: token.getEnd() }); const isNested = isTokenNested(token); tagNodes.push(tagNode); if (isNested) { nestedNodes.push(tagNode); } else { const nodes = getNodes(); appendNodes(nodes, tagNode); } } /** * @private * @param {Token} token */ function handleTagEnd(token) { const tagName = token.getValue().slice(1); const lastNestedNode = nestedNodes.flush(); flushTagNodes(); if (lastNestedNode) { const nodes = getNodes(); if (isTagNode(lastNestedNode)) { lastNestedNode.setEnd({ from: token.getStart(), to: token.getEnd() }); } appendNodes(nodes, lastNestedNode); } else if (!isTagNested(tagName)) { const nodes = getNodes(); appendNodes(nodes, token.toString({ openTag, closeTag })); } else if (typeof options.onError === "function") { const tag = token.getValue(); const line = token.getLine(); const column = token.getColumn(); options.onError({ tagName: tag, lineNumber: line, columnNumber: column }); } } /** * @private * @param {Token} token */ function handleTag(token) { // [tag] if (token.isStart()) { handleTagStart(token); } // [/tag] if (token.isEnd()) { handleTagEnd(token); } } /** * @private * @param {Token} token */ function handleNode(token) { /** * @type {TagNode} */ const activeTagNode = tagNodes.last(); const tokenValue = token.getValue(); const isNested = isTagNested(token.toString()); const nodes = getNodes(); if (activeTagNode !== null) { if (token.isAttrName()) { tagNodesAttrName.push(tokenValue); const attrName = tagNodesAttrName.last(); if (attrName) { activeTagNode.attr(attrName, ""); } } else if (token.isAttrValue()) { const attrName = tagNodesAttrName.last(); if (attrName) { activeTagNode.attr(attrName, tokenValue); tagNodesAttrName.flush(); } else { activeTagNode.attr(tokenValue, tokenValue); } } else if (token.isText()) { if (isNested) { activeTagNode.append(tokenValue); } else { appendNodes(nodes, tokenValue); } } else if (token.isTag()) { // if tag is not allowed, just pass it as is appendNodes(nodes, token.toString({ openTag, closeTag })); } } else if (token.isText()) { appendNodes(nodes, tokenValue); } else if (token.isTag()) { // if tag is not allowed, just pass it as is appendNodes(nodes, token.toString({ openTag, closeTag })); } } /** * @private * @param {Token} token */ function onToken(token) { if (token.isTag()) { handleTag(token); } else { handleNode(token); } } const lexer = opts.createTokenizer ? opts.createTokenizer : createLexer; tokenizer = lexer(input, { onToken, openTag, closeTag, onlyAllowTags: options.onlyAllowTags, contextFreeTags: options.contextFreeTags, caseFreeTags: options.caseFreeTags, enableEscapeTags: options.enableEscapeTags }); // eslint-disable-next-line no-unused-vars const tokens = tokenizer.tokenize(); // handles situations where we open tag, but forgot close them // for ex [q]test[/q][u]some[/u][q]some [u]some[/u] // forgot to close [/q] // so we need to flush nested content to nodes array const lastNestedNode = nestedNodes.flush(); if (lastNestedNode !== null && lastNestedNode && isTagNode(lastNestedNode) && isTagNested(lastNestedNode.tag)) { appendNodeAsString(getNodes(), lastNestedNode, false); } return nodes.toArray(); } export { parse }; export default parse;