mirror of
https://github.com/tenrok/BBob.git
synced 2026-06-05 16:42:27 +03:00
refactor(parser): better jsdoc, some behavior fixes, more tests
— all operations on nodes moved to `createList` function - fixed problem with single tags with value only like `[url=value]` fixes #6 - write tests for `Token` class - moved all node arrays to parse func, now parser supports many instances - add jsdoc to critical parts of the parser to better understanding how it works
This commit is contained in:
@@ -41,10 +41,6 @@ const getTagName = (token) => {
|
||||
const convertTagToText = (token) => {
|
||||
let text = OPEN_BRAKET;
|
||||
|
||||
if (isTagEnd(token)) {
|
||||
text += SLASH;
|
||||
}
|
||||
|
||||
text += getTokenValue(token);
|
||||
text += CLOSE_BRAKET;
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@ function createLexer(buffer, options = {}) {
|
||||
if (isCharReserved(nextChar) || hasInvalidChars || bufferGrabber.isLast()) {
|
||||
emitToken(createToken(TYPE_WORD, currChar, row, col));
|
||||
} else {
|
||||
//
|
||||
const str = bufferGrabber.grabWhile(val => val !== closeTag);
|
||||
|
||||
bufferGrabber.skip(); // skip closeTag
|
||||
|
||||
+183
-201
@@ -1,151 +1,135 @@
|
||||
import TagNode from '@bbob/plugin-helper/lib/TagNode';
|
||||
import { createLexer } from './lexer';
|
||||
import { createList } from './utils';
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
let nodes;
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
let nestedNodes;
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
let tagNodes;
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
let tagNodesAttrName;
|
||||
|
||||
let options = {};
|
||||
let tokenizer = null;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let tokens = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param token
|
||||
* @return {*}
|
||||
*/
|
||||
const isTagNested = token => tokenizer.isTokenNested(token);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {TagNode}
|
||||
*/
|
||||
const getLastTagNode = () => (tagNodes.length ? tagNodes[tagNodes.length - 1] : null);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
* @public
|
||||
* @param {String} input
|
||||
* @param {Object} opts
|
||||
* @param {Function} opts.createTokenizer
|
||||
* @param {Array<string>} opts.onlyAllowTags
|
||||
* @param {String} opts.openTag
|
||||
* @param {String} opts.closeTag
|
||||
* @return {Array}
|
||||
*/
|
||||
const createTagNode = token => tagNodes.push(TagNode.create(token.getValue()));
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
* @return {Array}
|
||||
*/
|
||||
const createTagNodeAttrName = token => tagNodesAttrName.push(token.getValue());
|
||||
const parse = (input, opts = {}) => {
|
||||
const options = opts;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const getTagNodeAttrName = () =>
|
||||
(tagNodesAttrName.length ? tagNodesAttrName[tagNodesAttrName.length - 1] : null);
|
||||
let tokenizer = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const clearTagNodeAttrName = () => {
|
||||
if (tagNodesAttrName.length) {
|
||||
tagNodesAttrName.pop();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Result AST of nodes
|
||||
* @private
|
||||
* @type {ItemList}
|
||||
*/
|
||||
const nodes = createList();
|
||||
/**
|
||||
* Temp buffer of nodes that's nested to another node
|
||||
* @private
|
||||
* @type {ItemList}
|
||||
*/
|
||||
const nestedNodes = createList();
|
||||
/**
|
||||
* Temp buffer of nodes [tag..]...[/tag]
|
||||
* @private
|
||||
* @type {ItemList}
|
||||
*/
|
||||
const tagNodes = createList();
|
||||
/**
|
||||
* Temp buffer of tag attributes
|
||||
* @private
|
||||
* @type {ItemList}
|
||||
*/
|
||||
const tagNodesAttrName = createList();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const clearTagNode = () => {
|
||||
if (tagNodes.length) {
|
||||
tagNodes.pop();
|
||||
/**
|
||||
* Cache for nested tags checks
|
||||
* @type {{}}
|
||||
*/
|
||||
const nestedTagsMap = {};
|
||||
|
||||
clearTagNodeAttrName();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const getNodes = () => {
|
||||
if (nestedNodes.length) {
|
||||
const nestedNode = nestedNodes[nestedNodes.length - 1];
|
||||
|
||||
return nestedNode.content;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param tag
|
||||
*/
|
||||
const appendNode = (tag) => {
|
||||
getNodes().push(tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const isAllowedTag = (value) => {
|
||||
if (options.onlyAllowTags && options.onlyAllowTags.length) {
|
||||
return options.onlyAllowTags.indexOf(value) >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagStart = (token) => {
|
||||
if (token.isStart()) {
|
||||
createTagNode(token);
|
||||
|
||||
if (isTagNested(token)) {
|
||||
nestedNodes.push(getLastTagNode());
|
||||
} else {
|
||||
appendNode(getLastTagNode());
|
||||
clearTagNode();
|
||||
const isTokenNested = (token) => {
|
||||
if (typeof nestedTagsMap[token.getValue()] === 'undefined') {
|
||||
nestedTagsMap[token.getValue()] = tokenizer.isTokenNested(token);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagEnd = (token) => {
|
||||
if (token.isEnd()) {
|
||||
clearTagNode();
|
||||
return nestedTagsMap[token.getValue()];
|
||||
};
|
||||
|
||||
const lastNestedNode = nestedNodes.pop();
|
||||
const isTagNested = tagName => !!nestedTagsMap[tagName];
|
||||
|
||||
/**
|
||||
* Flushes temp tag nodes and its attributes buffers
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const flushTagNodes = () => {
|
||||
if (tagNodes.flushLast()) {
|
||||
tagNodesAttrName.flushLast();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
const getNodes = () => {
|
||||
const lastNestedNode = nestedNodes.getLast();
|
||||
|
||||
return lastNestedNode ? lastNestedNode.content : nodes.toArray();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {TagNode} tag
|
||||
*/
|
||||
const appendNodes = (tag) => {
|
||||
getNodes().push(tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {String} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const isAllowedTag = (value) => {
|
||||
if (options.onlyAllowTags && options.onlyAllowTags.length) {
|
||||
return options.onlyAllowTags.indexOf(value) >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagStart = (token) => {
|
||||
flushTagNodes();
|
||||
|
||||
const tagNode = TagNode.create(token.getValue());
|
||||
const isNested = isTokenNested(token);
|
||||
|
||||
tagNodes.push(tagNode);
|
||||
|
||||
if (isNested) {
|
||||
nestedNodes.push(tagNode);
|
||||
} else {
|
||||
appendNodes(tagNode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagEnd = (token) => {
|
||||
flushTagNodes();
|
||||
|
||||
const lastNestedNode = nestedNodes.flushLast();
|
||||
|
||||
if (lastNestedNode) {
|
||||
appendNode(lastNestedNode);
|
||||
appendNodes(lastNestedNode);
|
||||
} else if (options.onError) {
|
||||
const tag = token.getValue();
|
||||
const line = token.getLine();
|
||||
@@ -158,92 +142,90 @@ const handleTagEnd = (token) => {
|
||||
columnNumber: column,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagToken = (token) => {
|
||||
if (token.isTag()) {
|
||||
if (isAllowedTag(token.getName())) {
|
||||
// [tag]
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTag = (token) => {
|
||||
// [tag]
|
||||
if (token.isStart()) {
|
||||
handleTagStart(token);
|
||||
|
||||
// [/tag]
|
||||
handleTagEnd(token);
|
||||
} else {
|
||||
appendNode(token.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleTagNode = (token) => {
|
||||
const tagNode = getLastTagNode();
|
||||
// [/tag]
|
||||
if (token.isEnd()) {
|
||||
handleTagEnd(token);
|
||||
}
|
||||
};
|
||||
|
||||
if (tagNode) {
|
||||
if (token.isAttrName()) {
|
||||
createTagNodeAttrName(token);
|
||||
tagNode.attr(getTagNodeAttrName(), '');
|
||||
} else if (token.isAttrValue()) {
|
||||
const attrName = getTagNodeAttrName();
|
||||
const attrValue = token.getValue();
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const handleNode = (token) => {
|
||||
/**
|
||||
* @type {TagNode}
|
||||
*/
|
||||
const lastTagNode = tagNodes.getLast();
|
||||
const tokenValue = token.getValue();
|
||||
const isNested = isTagNested(token);
|
||||
|
||||
if (attrName) {
|
||||
tagNode.attr(getTagNodeAttrName(), attrValue);
|
||||
clearTagNodeAttrName();
|
||||
} else {
|
||||
tagNode.attr(attrValue, attrValue);
|
||||
if (lastTagNode) {
|
||||
if (token.isAttrName()) {
|
||||
tagNodesAttrName.push(tokenValue);
|
||||
lastTagNode.attr(tagNodesAttrName.getLast(), '');
|
||||
} else if (token.isAttrValue()) {
|
||||
const attrName = tagNodesAttrName.getLast();
|
||||
|
||||
if (attrName) {
|
||||
lastTagNode.attr(attrName, tokenValue);
|
||||
tagNodesAttrName.flushLast();
|
||||
} else {
|
||||
lastTagNode.attr(tokenValue, tokenValue);
|
||||
}
|
||||
} else if (token.isText()) {
|
||||
if (isNested) {
|
||||
lastTagNode.append(tokenValue);
|
||||
} else {
|
||||
appendNodes(tokenValue);
|
||||
}
|
||||
} else if (token.isTag()) {
|
||||
// if tag is not allowed, just past it as is
|
||||
appendNodes(token.toString());
|
||||
}
|
||||
} else if (token.isText()) {
|
||||
tagNode.append(token.getValue());
|
||||
appendNodes(tokenValue);
|
||||
} else if (token.isTag()) {
|
||||
// if tag is not allowed, just past it as is
|
||||
appendNodes(token.toString());
|
||||
}
|
||||
} else if (token.isText()) {
|
||||
appendNode(token.getValue());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param token
|
||||
*/
|
||||
const parseToken = (token) => {
|
||||
handleTagToken(token);
|
||||
handleTagNode(token);
|
||||
};
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
const onToken = (token) => {
|
||||
if (token.isTag() && isAllowedTag(token.getName())) {
|
||||
handleTag(token);
|
||||
} else {
|
||||
handleNode(token);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param input
|
||||
* @param opts
|
||||
* @param {Function} opts.createTokenizer
|
||||
* @param {Array<string>} opts.onlyAllowTags
|
||||
* @param {String} opts.openTag
|
||||
* @param {String} opts.closeTag
|
||||
* @return {Array}
|
||||
*/
|
||||
const parse = (input, opts = {}) => {
|
||||
options = opts;
|
||||
tokenizer = (opts.createTokenizer ? opts.createTokenizer : createLexer)(input, {
|
||||
onToken: parseToken,
|
||||
onToken,
|
||||
onlyAllowTags: options.onlyAllowTags,
|
||||
openTag: options.openTag,
|
||||
closeTag: options.closeTag,
|
||||
});
|
||||
|
||||
nodes = [];
|
||||
nestedNodes = [];
|
||||
tagNodes = [];
|
||||
tagNodesAttrName = [];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const tokens = tokenizer.tokenize();
|
||||
|
||||
tokens = tokenizer.tokenize();
|
||||
|
||||
return nodes;
|
||||
return nodes.toArray();
|
||||
};
|
||||
|
||||
export { parse };
|
||||
|
||||
@@ -76,10 +76,12 @@ export const createCharGrabber = (source, { onSkip } = {}) => {
|
||||
*/
|
||||
export 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);
|
||||
}
|
||||
|
||||
@@ -92,3 +94,53 @@ export const trimChar = (str, charToRemove) => {
|
||||
* @return {String}
|
||||
*/
|
||||
export const unquote = str => str.replace(BACKSLASH + QUOTEMARK, QUOTEMARK);
|
||||
|
||||
/**
|
||||
* @typedef {Object} ItemList
|
||||
* @type {Object}
|
||||
* @property {getLastCb} getLast
|
||||
* @property {flushLastCb} flushLast
|
||||
* @property {pushCb} push
|
||||
* @property {toArrayCb} toArray
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param values
|
||||
* @return {ItemList}
|
||||
*/
|
||||
export const createList = (values = []) => {
|
||||
const nodes = values;
|
||||
/**
|
||||
* @callback getLastCb
|
||||
*/
|
||||
const getLast = () => (nodes.length ? nodes[nodes.length - 1] : null);
|
||||
/**
|
||||
* @callback flushLastCb
|
||||
* @return {*}
|
||||
*/
|
||||
const flushLast = () => {
|
||||
if (nodes.length) {
|
||||
return nodes.pop();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* @callback pushCb
|
||||
* @param value
|
||||
*/
|
||||
const push = value => nodes.push(value);
|
||||
|
||||
/**
|
||||
* @callback toArrayCb
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
return {
|
||||
getLast,
|
||||
flushLast,
|
||||
push,
|
||||
toArray: () => nodes,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user