@bbob/parser
Version:
A BBCode to AST Parser part of @bbob
976 lines (965 loc) • 34.7 kB
JavaScript
(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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')// 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 });
}));