@bbob/parser
Version:
A BBCode to AST Parser part of @bbob
283 lines (282 loc) • 9.02 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
default: function() {
return _default;
},
parse: function() {
return parse;
}
});
var _pluginhelper = require("@bbob/plugin-helper");
var _lexer = require("./lexer.js");
var _Token = require("./Token.js");
var _NodeList = require("./NodeList.js");
var createList = function() {
return new _NodeList.NodeList();
};
function parse(input, opts) {
if (opts === void 0) opts = {};
var options = opts;
var openTag = options.openTag || _pluginhelper.OPEN_BRAKET;
var closeTag = options.closeTag || _pluginhelper.CLOSE_BRAKET;
var onlyAllowTags = (options.onlyAllowTags || []).filter(Boolean).map(function(tag) {
return tag.toLowerCase();
});
var caseFreeTags = options.caseFreeTags || false;
var tokenizer = null;
/**
* Result AST of nodes
* @private
* @type {NodeList}
*/ var nodes = createList();
/**
* Temp buffer of nodes that's nested to another node
* @private
*/ var nestedNodes = createList();
/**
* Temp buffer of nodes [tag..]...[/tag]
* @private
* @type {NodeList}
*/ var activeTagNode = null;
/**
* Temp buffer of tag attributes
* @private
* @type {NodeList}
*/ var activeTagNodesAttrName = null;
/**
* Cache for nested tags checks
*/ var nestedTagsMap = new Set();
function getValue(tokenValue) {
return caseFreeTags ? tokenValue.toLowerCase() : tokenValue;
}
function isTokenNested(token) {
var tokenValue = token.getValue();
var value = getValue(tokenValue);
var isTokenNested = (tokenizer || {}).isTokenNested;
if (!nestedTagsMap.has(value) && typeof isTokenNested === "function") {
if (isTokenNested(value)) {
nestedTagsMap.add(value);
return true;
}
}
return nestedTagsMap.has(value);
}
/**
* @private
*/ function isTagNested(tagName) {
return Boolean(nestedTagsMap.has(getValue(tagName)));
}
/**
* @private
*/ function isTagAllowed(value) {
if (onlyAllowTags.length) {
return onlyAllowTags.indexOf(value.toLowerCase()) >= 0;
}
return true;
}
/**
* Flushes temp tag nodes and its attributes buffers
* @private
*/ function activeTagNodeFlush() {
if (activeTagNode) {
activeTagNode = null;
activeTagNodesAttrName = null;
}
}
/**
* @private
*/ function getNodesContent() {
var lastNestedNode = nestedNodes.last();
if (lastNestedNode && (0, _pluginhelper.isTagNode)(lastNestedNode)) {
return lastNestedNode.content;
}
return nodes.ref();
}
/**
* @private
*/ function nodesAppendAsString(nodes, node, isNested) {
if (isNested === void 0) isNested = true;
if (Array.isArray(nodes) && typeof node !== "undefined") {
nodes.push(node.toTagStart({
openTag: openTag,
closeTag: closeTag
}));
if (Array.isArray(node.content) && node.content.length) {
node.content.forEach(function(item) {
nodes.push(item);
});
if (isNested) {
nodes.push(node.toTagEnd({
openTag: openTag,
closeTag: closeTag
}));
}
}
}
}
/**
* @private
*/ function nodesAppend(node) {
var nodes = getNodesContent();
if (Array.isArray(nodes) && typeof node !== "undefined") {
if ((0, _pluginhelper.isTagNode)(node)) {
if (isTagAllowed(node.tag)) {
nodes.push(node.toTagNode());
} else {
nodesAppendAsString(nodes, node);
}
} else {
nodes.push(node);
}
}
}
/**
* @private
* @param {Token} token
*/ function tagHandleStart(token) {
activeTagNodeFlush();
var tagNode = _pluginhelper.TagNode.create(token.getValue(), {}, [], {
from: token.getStart(),
to: token.getEnd()
});
var isNested = isTokenNested(token);
activeTagNode = tagNode;
if (isNested) {
nestedNodes.push(tagNode);
} else {
nodesAppend(tagNode);
}
}
/**
* @private
* @param {Token} token
*/ function tagHandleEnd(token) {
var tagName = token.getValue().slice(1);
var lastNestedNode = nestedNodes.flush();
activeTagNodeFlush();
if (lastNestedNode) {
if ((0, _pluginhelper.isTagNode)(lastNestedNode)) {
lastNestedNode.setEnd({
from: token.getStart(),
to: token.getEnd()
});
}
nodesAppend(lastNestedNode);
} else if (!isTagNested(tagName)) {
nodesAppend(token.toString({
openTag: openTag,
closeTag: closeTag
}));
} else if (typeof options.onError === "function") {
var tag = token.getValue();
var line = token.getLine();
var column = token.getColumn();
options.onError({
tagName: tag,
lineNumber: line,
columnNumber: column
});
}
}
/**
* @private
* @param {Token} token
*/ function nodeHandle(token) {
var tokenValue = token.getValue();
var isNested = isTagNested(token.toString());
if (activeTagNode) {
switch(token.type){
case _Token.TYPE_ATTR_NAME:
activeTagNodesAttrName = tokenValue;
if (tokenValue) {
activeTagNode.attr(tokenValue, "");
}
break;
case _Token.TYPE_ATTR_VALUE:
if (activeTagNodesAttrName) {
activeTagNode.attr(activeTagNodesAttrName, tokenValue);
activeTagNodesAttrName = null;
} else {
activeTagNode.attr(tokenValue, tokenValue);
}
break;
case _Token.TYPE_SPACE:
case _Token.TYPE_NEW_LINE:
case _Token.TYPE_WORD:
if (isNested) {
activeTagNode.append(tokenValue);
} else {
nodesAppend(tokenValue);
}
break;
case _Token.TYPE_TAG:
// if tag is not allowed, just pass it as is
nodesAppend(token.toString({
openTag: openTag,
closeTag: closeTag
}));
break;
}
} else if (token.isText()) {
nodesAppend(tokenValue);
} else if (token.isTag()) {
// if tag is not allowed, just pass it as is
nodesAppend(token.toString({
openTag: openTag,
closeTag: closeTag
}));
}
}
/**
* @private
* @param {Token} token
*/ function onToken(token) {
if (token.isTag()) {
// [tag]
if (token.isStart()) {
tagHandleStart(token);
}
// [/tag]
if (token.isEnd()) {
tagHandleEnd(token);
}
} else {
nodeHandle(token);
}
}
var lexer = opts.createTokenizer ? opts.createTokenizer : _lexer.createLexer;
tokenizer = lexer(input, {
onToken: onToken,
openTag: openTag,
closeTag: closeTag,
onlyAllowTags: options.onlyAllowTags,
contextFreeTags: options.contextFreeTags,
caseFreeTags: options.caseFreeTags,
enableEscapeTags: options.enableEscapeTags,
whitespaceInTags: options.whitespaceInTags
});
// eslint-disable-next-line no-unused-vars
var tokens = tokenizer.tokenize();
// handles situations where we opened tag, but forget to 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
do {
var node = nestedNodes.flush();
if ((0, _pluginhelper.isTagNode)(node) && isTagNested(node.tag)) {
nodesAppendAsString(getNodesContent(), node, false);
} else if (typeof node !== "undefined") {
nodesAppend(node);
}
}while (nestedNodes.has());
return nodes.ref();
}
var _default = parse;