import { ElementNode } from './ElementNode' import { TextNode } from './TextNode' import { DocNode, SyntaxKind, Token } from './types' const addToken = ( stack: Token[], line: string, isTextNode: boolean, patternString: string ): void => { if (!line) { return } if (isTextNode) { stack.push({ kind: SyntaxKind.text, text: line, }) return } let match: RegExpMatchArray | null // if ((match = line.match(/<([^\s<>]+) ?([^<>]*)\/>/))) { stack.push({ kind: SyntaxKind.self, tagName: match[1], attributes: match[2], text: line, }) return } // if ((match = line.match(/<\/([^\s<>]+)>/))) { stack.push({ kind: SyntaxKind.close, tagName: match[1], text: line, }) return } // if ((match = line.match(/<([^\s<>]+) ?([^<>]*)>/))) { stack.push({ kind: SyntaxKind.open, tagName: match[1], attributes: match[2], text: line, }) return } // {text} if ((match = line.match(new RegExp(patternString)))) { stack.push({ kind: SyntaxKind.self, tagName: match[1], attributes: match[1], text: line, }) return } return } const tokenize = (str: string, patternString: string): Token[] => { const pattern = new RegExp( `(<[^<>]+>${patternString ? `|${patternString}` : ''})`, 'g' ) const stack: Token[] = [] let match: RegExpExecArray | null = null let pointer: number = 0 while ((match = pattern.exec(str))) { addToken(stack, str.slice(pointer, match.index), true, patternString) addToken(stack, match[1], false, patternString) pointer = match.index + match[0].length } const hasMoreText = pointer < str.length if (hasMoreText) { addToken(stack, str.slice(pointer), true, patternString) } return stack } const lex = (tokens: Token[]): DocNode[] => { const children: DocNode[] = [] if (tokens.length === 0) { return children } let currentNode: DocNode | null = null for (const node of tokens) { if (node.kind === SyntaxKind.text) { if (currentNode instanceof ElementNode) { currentNode.children.push(new TextNode(node.text, currentNode)) } else { children.push(new TextNode(node.text, null)) } continue } if (node.kind === SyntaxKind.close) { if (!(currentNode instanceof ElementNode)) { throw new Error(`missing open node: '${node.tagName}'`) } if (node.tagName !== currentNode.tagName) { throw new Error(`missing close node: '${currentNode.tagName}'`) } currentNode = currentNode.parent continue } if (node.kind === SyntaxKind.open) { const newNode = new ElementNode(node, currentNode) if (currentNode instanceof ElementNode) { currentNode.children.push(newNode) } else { children.push(newNode) } currentNode = newNode continue } if (node.kind === SyntaxKind.self) { if (currentNode instanceof ElementNode) { currentNode.children.push(new ElementNode(node, currentNode)) } else { children.push(new ElementNode(node, null)) } continue } } if ( currentNode instanceof ElementNode && currentNode.kind === SyntaxKind.open ) { throw new Error(`missing close node: '${currentNode.tagName}'`) } return children } const parse = ( str: string, patternString: string = '{([\\w-]+)}' ): DocNode[] => { // Break our string into tokens const lines = tokenize(str, patternString) // Add extra context to our tokens and validate the syntax const ast = lex(lines) // Return our syntax tree return ast } export { parse }