mirror of
https://github.com/tenrok/BBob.git
synced 2026-06-08 17:22:26 +03:00
feat: typescript support (#185)
* feat: initial typescript support * feat: typescript support * feat(plugin-helper): move files to typescript * chore: update lock files * feat: preset types * fix: build * fix: benchmark * fix: remove pnpm cache * fix: bench action * fix: pnpm recursive install * fix: nx cache * fix: lock file * fix: workflows * fix: lerna support in pnpm * fix: pnpm workspace * fix: remove unused files * fix: pnpm lock file * fix: update lerna for support pnpm * fix: lerna bootstrap * fix: rollup build * fix: update nx * fix: build * fix: add nx dep target * fix: remove nx cache * fix: workflow run on push only for master * fix: test workflow run on push only for master * fix: remove parallel for gen types * fix: benchmark * fix: benchmark imports * fix: pnpm * fix: types errors and pnpm * fix: types * fix: types * refactor: parser * fix(parser): tests * fix: preset tests * fix: react types * fix: react type declarations * fix: pnpm lock file * fix: react preset types * fix: lock file * fix: vue2 types * feat: dev container support * fix: types * fix: types * refactor: rewrite pkg-task, add nx gen-types deps, fix react/render.ts * refactor: types * fix: types * fix: rename gen-types to types * fix: nx build order * fix: nx reset * fix: define nx deps explicit * fix: build * fix: nx * fix: nx order build * fix: nx deps * fix: bbob cli tests * fix: tests * fix: cli tests and import * fix: test cover * fix: cli cover
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import {
|
||||
CLOSE_BRAKET,
|
||||
OPEN_BRAKET,
|
||||
TagNode,
|
||||
isTagNode,
|
||||
} from "@bbob/plugin-helper";
|
||||
|
||||
import { createLexer } from "./lexer";
|
||||
|
||||
import type { NodeContent, TagNodeTree } from "@bbob/plugin-helper";
|
||||
import type { LexerTokenizer, LexerOptions } from "./types";
|
||||
import type { Token } from "./Token";
|
||||
|
||||
type ParseError = {
|
||||
tagName: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
};
|
||||
|
||||
export interface ParseOptions {
|
||||
createTokenizer?: (input: string, options?: LexerOptions) => LexerTokenizer;
|
||||
openTag?: string;
|
||||
closeTag?: string;
|
||||
onlyAllowTags?: string[];
|
||||
contextFreeTags?: string[];
|
||||
enableEscapeTags?: boolean;
|
||||
onError?: (error: ParseError) => void;
|
||||
}
|
||||
|
||||
class NodeList<Value> {
|
||||
private n: Value[];
|
||||
|
||||
constructor() {
|
||||
this.n = [];
|
||||
}
|
||||
|
||||
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: Value) {
|
||||
this.n.push(value);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return this.n;
|
||||
}
|
||||
}
|
||||
|
||||
const createList = <Type>() => new NodeList<Type>();
|
||||
|
||||
function parse(input: string, opts: ParseOptions = {}) {
|
||||
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());
|
||||
|
||||
let tokenizer: LexerTokenizer | null = null;
|
||||
|
||||
/**
|
||||
* Result AST of nodes
|
||||
* @private
|
||||
* @type {NodeList}
|
||||
*/
|
||||
const nodes = createList<TagNode>();
|
||||
/**
|
||||
* Temp buffer of nodes that's nested to another node
|
||||
* @private
|
||||
*/
|
||||
const nestedNodes = createList<NodeContent>();
|
||||
/**
|
||||
* Temp buffer of nodes [tag..]...[/tag]
|
||||
* @private
|
||||
* @type {NodeList}
|
||||
*/
|
||||
const tagNodes = createList<TagNode>();
|
||||
/**
|
||||
* Temp buffer of tag attributes
|
||||
* @private
|
||||
* @type {NodeList}
|
||||
*/
|
||||
const tagNodesAttrName = createList<string>();
|
||||
|
||||
/**
|
||||
* Cache for nested tags checks
|
||||
*/
|
||||
const nestedTagsMap = new Set<string>();
|
||||
|
||||
function isTokenNested(token: Token) {
|
||||
const value = token.getValue();
|
||||
const { isTokenNested } = tokenizer || {};
|
||||
|
||||
if (!nestedTagsMap.has(value) && isTokenNested && isTokenNested(token)) {
|
||||
nestedTagsMap.add(value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return nestedTagsMap.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function isTagNested(tagName: string) {
|
||||
return Boolean(nestedTagsMap.has(tagName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function isAllowedTag(value: string) {
|
||||
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?: TagNodeTree,
|
||||
node?: TagNode,
|
||||
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?: TagNodeTree, node?: NodeContent) {
|
||||
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: Token) {
|
||||
flushTagNodes();
|
||||
|
||||
const tagNode = TagNode.create(token.getValue());
|
||||
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: Token) {
|
||||
flushTagNodes();
|
||||
|
||||
const lastNestedNode = nestedNodes.flush();
|
||||
|
||||
if (lastNestedNode) {
|
||||
const nodes = getNodes();
|
||||
appendNodes(nodes, lastNestedNode);
|
||||
} 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: Token) {
|
||||
// [tag]
|
||||
if (token.isStart()) {
|
||||
handleTagStart(token);
|
||||
}
|
||||
|
||||
// [/tag]
|
||||
if (token.isEnd()) {
|
||||
handleTagEnd(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
function handleNode(token: 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());
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Token} token
|
||||
*/
|
||||
function onToken(token: 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,
|
||||
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;
|
||||
Reference in New Issue
Block a user