UNPKG

@bbob/parser

Version:

A BBCode to AST Parser part of @bbob

976 lines (965 loc) 34.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.BbobParser = {})); })(this, (function (exports) { 'use strict'; const N = '\n'; const TAB = '\t'; const EQ = '='; const QUOTEMARK = '"'; const SPACE = ' '; const OPEN_BRAKET = '['; const CLOSE_BRAKET = ']'; const SLASH = '/'; const BACKSLASH = '\\'; function isTagNode(el) { return typeof el === 'object' && el !== null && 'tag' in el; } function isStringNode(el) { return typeof el === 'string'; } function keysReduce(obj, reduce, def) { const keys = Object.keys(obj); return keys.reduce((acc, key)=>reduce(acc, key, obj), def); } function getNodeLength(node) { if (isTagNode(node) && Array.isArray(node.content)) { return node.content.reduce((count, contentNode)=>{ return count + getNodeLength(contentNode); }, 0); } if (isStringNode(node)) { return String(node).length; } return 0; } function appendToNode(node, value) { if (Array.isArray(node.content)) { node.content.push(value); } } /** * Replaces " to &qquot; * @param {string} value */ function escapeAttrValue(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')// eslint-disable-next-line no-script-url .replace(/(javascript|data|vbscript):/gi, '$1%3A'); } /** * Accept name and value and return valid html5 attribute string */ function attrValue(name, value) { // in case of performance switch(typeof value){ case 'boolean': return value ? `${name}` : ''; case 'number': return `${name}="${value}"`; case 'string': return `${name}="${escapeAttrValue(value)}"`; case 'object': return `${name}="${escapeAttrValue(JSON.stringify(value))}"`; default: return ''; } } /** * Transforms attrs to html params string * @example * attrsToString({ 'foo': true, 'bar': bar' }) => 'foo="true" bar="bar"' */ function attrsToString(values) { // To avoid some malformed attributes if (values == null) { return ''; } return keysReduce(values, (arr, key, obj)=>[ ...arr, attrValue(key, obj[key]) ], [ '' ]).join(' '); } /** * Gets value from * @example * getUniqAttr({ 'foo': true, 'bar': bar' }) => 'bar' */ function getUniqAttr(attrs) { return keysReduce(attrs || {}, (res, key, obj)=>obj[key] === key ? obj[key] : null, null); } const getTagAttrs = (tag, params)=>{ const uniqAttr = getUniqAttr(params); if (uniqAttr) { const tagAttr = attrValue(tag, uniqAttr); const attrs = { ...params }; delete attrs[String(uniqAttr)]; const attrsStr = attrsToString(attrs); return `${tagAttr}${attrsStr}`; } return `${tag}${attrsToString(params)}`; }; const renderContent = (content, openTag, closeTag)=>{ const toString = (node)=>{ if (isTagNode(node)) { return node.toString({ openTag, closeTag }); } return String(node); }; if (Array.isArray(content)) { return content.reduce((r, node)=>{ if (node !== null) { return r + toString(node); } return r; }, ''); } if (content) { return toString(content); } return null; }; class TagNode { attr(name, value) { if (typeof value !== 'undefined') { this.attrs[name] = value; } return this.attrs[name]; } append(value) { return appendToNode(this, value); } setStart(value) { this.start = value; } setEnd(value) { this.end = value; } get length() { return getNodeLength(this); } toTagStart({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) { const tagAttrs = getTagAttrs(String(this.tag), this.attrs); return `${openTag}${tagAttrs}${closeTag}`; } toTagEnd({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) { return `${openTag}${SLASH}${this.tag}${closeTag}`; } toTagNode() { const newNode = new TagNode(String(this.tag).toLowerCase(), this.attrs, this.content); if (this.start) { newNode.setStart(this.start); } if (this.end) { newNode.setEnd(this.end); } return newNode; } toString({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) { const content = this.content ? renderContent(this.content, openTag, closeTag) : ''; const tagStart = this.toTagStart({ openTag, closeTag }); if (this.content === null || Array.isArray(this.content) && this.content.length === 0) { return tagStart; } return `${tagStart}${content}${this.toTagEnd({ openTag, closeTag })}`; } static create(tag, attrs = {}, content = null, start) { const node = new TagNode(tag, attrs, content); if (start) { node.setStart(start); } return node; } static isOf(node, type) { return node.tag === type; } constructor(tag, attrs, content){ this.tag = tag; this.attrs = attrs; this.content = content; } } // type, value, line, row, start pos, end pos const TOKEN_TYPE_ID = 't'; // 0; const TOKEN_VALUE_ID = 'v'; // 1; const TOKEN_COLUMN_ID = 'r'; // 2; const TOKEN_LINE_ID = 'l'; // 3; const TOKEN_START_POS_ID = 's'; // 4; const TOKEN_END_POS_ID = 'e'; // 5; const TOKEN_TYPE_WORD = 1; // 'word'; const TOKEN_TYPE_TAG = 2; // 'tag'; const TOKEN_TYPE_ATTR_NAME = 3; // 'attr-name'; const TOKEN_TYPE_ATTR_VALUE = 4; // 'attr-value'; const TOKEN_TYPE_SPACE = 5; // 'space'; const TOKEN_TYPE_NEW_LINE = 6; // 'new-line'; const getTokenValue = (token)=>{ if (token && typeof token[TOKEN_VALUE_ID] !== 'undefined') { return token[TOKEN_VALUE_ID]; } return ''; }; const getTokenLine = (token)=>token && token[TOKEN_LINE_ID] || 0; const getTokenColumn = (token)=>token && token[TOKEN_COLUMN_ID] || 0; const getStartPosition = (token)=>token && token[TOKEN_START_POS_ID] || 0; const getEndPosition = (token)=>token && token[TOKEN_END_POS_ID] || 0; const isTextToken = (token)=>{ if (token && typeof token[TOKEN_TYPE_ID] !== 'undefined') { return token[TOKEN_TYPE_ID] === TOKEN_TYPE_SPACE || token[TOKEN_TYPE_ID] === TOKEN_TYPE_NEW_LINE || token[TOKEN_TYPE_ID] === TOKEN_TYPE_WORD; } return false; }; const isTagToken = (token)=>{ if (token && typeof token[TOKEN_TYPE_ID] !== 'undefined') { return token[TOKEN_TYPE_ID] === TOKEN_TYPE_TAG; } return false; }; const isTagEnd = (token)=>getTokenValue(token).charCodeAt(0) === SLASH.charCodeAt(0); const isTagStart = (token)=>!isTagEnd(token); const isAttrNameToken = (token)=>{ if (token && typeof token[TOKEN_TYPE_ID] !== 'undefined') { return token[TOKEN_TYPE_ID] === TOKEN_TYPE_ATTR_NAME; } return false; }; const isAttrValueToken = (token)=>{ if (token && typeof token[TOKEN_TYPE_ID] !== 'undefined') { return token[TOKEN_TYPE_ID] === TOKEN_TYPE_ATTR_VALUE; } return false; }; const getTagName = (token)=>{ const value = getTokenValue(token); return isTagEnd(token) ? value.slice(1) : value; }; const tokenToText = (token, openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET)=>{ let text = openTag; text += getTokenValue(token); text += closeTag; return text; }; /** * @export * @class Token */ class Token { get type() { return this[TOKEN_TYPE_ID]; } isEmpty() { return this[TOKEN_TYPE_ID] === 0 || isNaN(this[TOKEN_TYPE_ID]); } isText() { return isTextToken(this); } isTag() { return isTagToken(this); } isAttrName() { return isAttrNameToken(this); } isAttrValue() { return isAttrValueToken(this); } isStart() { return isTagStart(this); } isEnd() { return isTagEnd(this); } getName() { return getTagName(this); } getValue() { return getTokenValue(this); } getLine() { return getTokenLine(this); } getColumn() { return getTokenColumn(this); } getStart() { return getStartPosition(this); } getEnd() { return getEndPosition(this); } toString({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) { return tokenToText(this, openTag, closeTag); } constructor(type, value, row = 0, col = 0, start = 0, end = 0){ this[TOKEN_LINE_ID] = row; this[TOKEN_COLUMN_ID] = col; this[TOKEN_TYPE_ID] = type || 0; this[TOKEN_VALUE_ID] = String(value); this[TOKEN_START_POS_ID] = start; this[TOKEN_END_POS_ID] = end; } } const TYPE_WORD = TOKEN_TYPE_WORD; const TYPE_TAG = TOKEN_TYPE_TAG; const TYPE_ATTR_NAME = TOKEN_TYPE_ATTR_NAME; const TYPE_ATTR_VALUE = TOKEN_TYPE_ATTR_VALUE; const TYPE_SPACE = TOKEN_TYPE_SPACE; const TYPE_NEW_LINE = TOKEN_TYPE_NEW_LINE; class CharGrabber { skip(num = 1, silent) { this.c.pos += num; if (this.o && this.o.onSkip && !silent) { this.o.onSkip(); } } hasNext() { return this.c.len > this.c.pos; } getCurr() { if (typeof this.s[this.c.pos] === 'undefined') { return ''; } return this.s[this.c.pos]; } getPos() { return this.c.pos; } getLength() { return this.c.len; } getRest() { return this.s.substring(this.c.pos); } getNext() { const nextPos = this.c.pos + 1; return nextPos <= this.s.length - 1 ? this.s[nextPos] : null; } getPrev() { const prevPos = this.c.pos - 1; if (typeof this.s[prevPos] === 'undefined') { return null; } return this.s[prevPos]; } isLast() { return this.c.pos === this.c.len; } includes(val) { return this.s.indexOf(val, this.c.pos) >= 0; } grabWhile(condition, silent) { let start = 0; if (this.hasNext()) { start = this.c.pos; while(this.hasNext() && condition(this.getCurr())){ this.skip(1, silent); } } return this.s.substring(start, this.c.pos); } grabN(num = 0) { return this.s.substring(this.c.pos, this.c.pos + num); } /** * Grabs rest of string until it find a char */ substrUntilChar(char) { const { pos } = this.c; const idx = this.s.indexOf(char, pos); return idx >= 0 ? this.s.substring(pos, idx) : ''; } constructor(source, options = {}){ this.s = source; this.c = { pos: 0, len: source.length }; this.o = options; } } /** * Creates a grabber wrapper for source string, that helps to iterate over string char by char */ const createCharGrabber = (source, options)=>new CharGrabber(source, options); /** * Trims string from start and end by char * @example * trimChar('*hello*', '*') ==> 'hello' */ const trimChar = (str, charToRemove)=>{ while(str.charAt(0) === charToRemove){ // eslint-disable-next-line no-param-reassign str = str.substring(1); } while(str.charAt(str.length - 1) === charToRemove){ // eslint-disable-next-line no-param-reassign str = str.substring(0, str.length - 1); } return str; }; /** * Unquotes \" to " */ const unquote = (str)=>str.replace(BACKSLASH + QUOTEMARK, QUOTEMARK); // for cases <!-- --> const EM = '!'; function createTokenOfType(type, value, r = 0, cl = 0, p = 0, e = 0) { return new Token(type, value, r, cl, p, e); } const STATE_WORD = 0; const STATE_TAG = 1; const STATE_TAG_ATTRS = 2; const TAG_STATE_NAME = 0; const TAG_STATE_ATTR = 1; const TAG_STATE_VALUE = 2; const WHITESPACES = [ SPACE, TAB ]; const SPECIAL_CHARS = [ EQ, SPACE, TAB ]; const END_POS_OFFSET = 2; // length + start position offset const isWhiteSpace = (char)=>WHITESPACES.indexOf(char) >= 0; const isEscapeChar = (char)=>char === BACKSLASH; const isSpecialChar = (char)=>SPECIAL_CHARS.indexOf(char) >= 0; const isNewLine = (char)=>char === N; const unq = (val)=>unquote(trimChar(val, QUOTEMARK)); function createLexer(buffer, options = {}) { let row = 0; let prevCol = 0; let col = 0; let tokenIndex = -1; let stateMode = STATE_WORD; let tagMode = TAG_STATE_NAME; let contextFreeTag = ''; const tokens = new Array(Math.floor(buffer.length)); const openTag = options.openTag || OPEN_BRAKET; const closeTag = options.closeTag || CLOSE_BRAKET; const escapeTags = !!options.enableEscapeTags; const contextFreeTags = (options.contextFreeTags || []).filter(Boolean).map((tag)=>tag.toLowerCase()); const caseFreeTags = options.caseFreeTags || false; const nestedMap = new Map(); const onToken = options.onToken || (()=>{}); const RESERVED_CHARS = [ closeTag, openTag, QUOTEMARK, BACKSLASH, SPACE, TAB, EQ, N, EM ]; const NOT_CHAR_TOKENS = [ openTag, SPACE, TAB, N ]; const isCharReserved = (char)=>RESERVED_CHARS.indexOf(char) >= 0; const isCharToken = (char)=>NOT_CHAR_TOKENS.indexOf(char) === -1; const isEscapableChar = (char)=>char === openTag || char === closeTag || char === BACKSLASH; const onSkip = ()=>{ col++; }; const checkContextFreeMode = (name, isClosingTag)=>{ if (contextFreeTag !== '' && isClosingTag) { contextFreeTag = ''; } if (contextFreeTag === '' && contextFreeTags.includes(name.toLowerCase())) { contextFreeTag = name; } }; const chars = createCharGrabber(buffer, { onSkip }); /** * Emits newly created token to subscriber */ function emitToken(type, value, startPos, endPos) { const token = createTokenOfType(type, value, row, prevCol, startPos, endPos); onToken(token); prevCol = col; tokenIndex += 1; tokens[tokenIndex] = token; } function nextTagState(tagChars, isSingleValueTag, masterStartPos) { if (tagMode === TAG_STATE_ATTR) { const validAttrName = (char)=>!(char === EQ || isWhiteSpace(char)); const name = tagChars.grabWhile(validAttrName); const isEnd = tagChars.isLast(); const isValue = tagChars.getCurr() !== EQ; tagChars.skip(); if (isEnd || isValue) { emitToken(TYPE_ATTR_VALUE, unq(name)); } else { emitToken(TYPE_ATTR_NAME, name); } if (isEnd) { return TAG_STATE_NAME; } if (isValue) { return TAG_STATE_ATTR; } return TAG_STATE_VALUE; } if (tagMode === TAG_STATE_VALUE) { let stateSpecial = false; const validAttrValue = (char)=>{ // const isEQ = char === EQ; const isQM = char === QUOTEMARK; const prevChar = tagChars.getPrev(); const nextChar = tagChars.getNext(); const isPrevSLASH = prevChar === BACKSLASH; const isNextEQ = nextChar === EQ; const isWS = isWhiteSpace(char); // const isPrevWS = isWhiteSpace(prevChar); const isNextWS = nextChar && isWhiteSpace(nextChar); if (stateSpecial && isSpecialChar(char)) { return true; } if (isQM && !isPrevSLASH) { stateSpecial = !stateSpecial; if (!stateSpecial && !(isNextEQ || isNextWS)) { return false; } } if (!isSingleValueTag) { return !isWS; // return (isEQ || isWS) === false; } return true; }; const name = tagChars.grabWhile(validAttrValue); tagChars.skip(); emitToken(TYPE_ATTR_VALUE, unq(name)); if (tagChars.getPrev() === QUOTEMARK) { prevCol++; } if (tagChars.isLast()) { return TAG_STATE_NAME; } return TAG_STATE_ATTR; } const start = masterStartPos + tagChars.getPos() - 1; const validName = (char)=>!(char === EQ || isWhiteSpace(char) || tagChars.isLast()); const name = tagChars.grabWhile(validName); emitToken(TYPE_TAG, name, start, masterStartPos + tagChars.getLength() + 1); checkContextFreeMode(name); tagChars.skip(); prevCol++; // in cases when we has [url=someval]GET[/url] and we dont need to parse all if (isSingleValueTag) { return TAG_STATE_VALUE; } const hasEQ = tagChars.includes(EQ); return hasEQ ? TAG_STATE_ATTR : TAG_STATE_VALUE; } function stateTag() { const currChar = chars.getCurr(); const nextChar = chars.getNext(); chars.skip(); // detect case where we have '[My word [tag][/tag]' or we have '[My last line word' const substr = chars.substrUntilChar(closeTag); const hasInvalidChars = substr.length === 0 || substr.indexOf(openTag) >= 0; if (nextChar && isCharReserved(nextChar) || hasInvalidChars || chars.isLast()) { emitToken(TYPE_WORD, currChar); return STATE_WORD; } // [myTag ] const isNoAttrsInTag = substr.indexOf(EQ) === -1; // [/myTag] const isClosingTag = substr[0] === SLASH; if (isNoAttrsInTag || isClosingTag) { const startPos = chars.getPos() - 1; const name = chars.grabWhile((char)=>char !== closeTag); const endPos = startPos + name.length + END_POS_OFFSET; chars.skip(); // skip closeTag emitToken(TYPE_TAG, name, startPos, endPos); checkContextFreeMode(name, isClosingTag); return STATE_WORD; } return STATE_TAG_ATTRS; } function stateAttrs() { const startPos = chars.getPos(); const silent = true; const tagStr = chars.grabWhile((char)=>char !== closeTag, silent); const tagGrabber = createCharGrabber(tagStr, { onSkip }); const hasSpace = tagGrabber.includes(SPACE); tagMode = TAG_STATE_NAME; while(tagGrabber.hasNext()){ tagMode = nextTagState(tagGrabber, !hasSpace, startPos); } chars.skip(); // skip closeTag return STATE_WORD; } function stateWord() { if (isNewLine(chars.getCurr())) { emitToken(TYPE_NEW_LINE, chars.getCurr()); chars.skip(); col = 0; prevCol = 0; row++; return STATE_WORD; } if (isWhiteSpace(chars.getCurr())) { const word = chars.grabWhile(isWhiteSpace); emitToken(TYPE_SPACE, word); return STATE_WORD; } if (chars.getCurr() === openTag) { if (contextFreeTag) { const fullTagLen = openTag.length + SLASH.length + contextFreeTag.length; const fullTagName = `${openTag}${SLASH}${contextFreeTag}`; const foundTag = chars.grabN(fullTagLen); const isEndContextFreeMode = foundTag === fullTagName; if (isEndContextFreeMode) { return STATE_TAG; } } else if (chars.includes(closeTag)) { return STATE_TAG; } emitToken(TYPE_WORD, chars.getCurr()); chars.skip(); prevCol++; return STATE_WORD; } if (escapeTags) { if (isEscapeChar(chars.getCurr())) { const currChar = chars.getCurr(); const nextChar = chars.getNext(); chars.skip(); // skip the \ without emitting anything if (nextChar && isEscapableChar(nextChar)) { chars.skip(); // skip past the [, ] or \ as well emitToken(TYPE_WORD, nextChar); return STATE_WORD; } emitToken(TYPE_WORD, currChar); return STATE_WORD; } const isChar = (char)=>isCharToken(char) && !isEscapeChar(char); const word = chars.grabWhile(isChar); emitToken(TYPE_WORD, word); return STATE_WORD; } const word = chars.grabWhile(isCharToken); emitToken(TYPE_WORD, word); return STATE_WORD; } function tokenize() { stateMode = STATE_WORD; while(chars.hasNext()){ switch(stateMode){ case STATE_TAG: stateMode = stateTag(); break; case STATE_TAG_ATTRS: stateMode = stateAttrs(); break; case STATE_WORD: default: stateMode = stateWord(); break; } } tokens.length = tokenIndex + 1; return tokens; } function isTokenNested(tokenValue) { const value = openTag + SLASH + tokenValue; if (nestedMap.has(value)) { return !!nestedMap.get(value); } else { const status = caseFreeTags ? buffer.toLowerCase().indexOf(value.toLowerCase()) > -1 : buffer.indexOf(value) > -1; nestedMap.set(value, status); return status; } } return { tokenize, isTokenNested }; } 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 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(); } exports.TagNode = TagNode; exports.createLexer = createLexer; exports.createTokenOfType = createTokenOfType; exports.default = parse; exports.parse = parse; Object.defineProperty(exports, '__esModule', { value: true }); }));