"use client"; import { zipArrays } from "../zip"; import { TokenIdentifiers } from "./TokenIdentifiers"; export const createElements = (body: string): [TokenMarker[], number] => { const tabOptions = [ /^\s{2}(?!\s|\t)/m, /^\s{4}(?!\s|\t)/m, /^\t(?!\s|\t)]/m, ]; let tabSpacing = 0; for (const [i, tabOption] of tabOptions.entries()) { if (body.match(tabOption)) { tabSpacing = i; break; } } const tokens = tokenize(body); return [buildAbstractSyntaxTree(tokens), tabSpacing]; }; const tokenize = (body: string) => { const tokenizedBody: TokenMarker[] = []; const addToken = (thing: TokenMarker) => { tokenizedBody.push(thing); }; for (const [type, token] of TokenIdentifiers.entries()) { const rx = new RegExp(token.rx); let match; while ((match = rx.exec(body)) !== null) { const start = match.index; const end = rx.lastIndex; addToken({ start, end, type, token: token.parse(match[0]), }); } } return tokenizedBody; }; function buildAbstractSyntaxTree(markers: TokenMarker[]) { markers.sort((a, b) => a.start - b.start); markers = filterOverlappingPBlocks(markers); establishClosestParent(markers); for (const marker of markers) { if (marker.parent) { marker.parent.token.children = marker.parent.token.children || []; marker.parent.token.children.push(marker.token); } } // By starting at the end, we can always assure that we are not filtering out children that haven't been processed yet for (const marker of [...markers].reverse()) { contentToChildren(marker.token); } return markers.filter((m) => !m.parent); // return markers; } function establishClosestParent(blocks: TokenMarker[]): void { blocks.sort((a, b) => a.start - b.start); // Sort blocks by start position for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; if (block.parent) continue; // Skip blocks that already have a parent let closestParent: TokenMarker | undefined = undefined; let minDistance = Number.MAX_SAFE_INTEGER; // Find the closest parent block for each block for (let j = 0; j < i; j++) { const otherBlock = blocks[j]; if (otherBlock.end >= block.start && otherBlock.start <= block.start) { const distance = block.start - otherBlock.start; if (distance < minDistance) { minDistance = distance; closestParent = otherBlock; } } } if (closestParent) { block.parent = closestParent; // Assign the closest parent block } } } function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] { return blocks.filter((block) => { if (block.type !== "p") { return true; // Keep blocks that are not 'p' type } // Filter out 'p' blocks that overlap with any other block for (const otherBlock of blocks) { if ( otherBlock !== block && ( otherBlock.start === block.start || otherBlock.end === block.end ) ) { return false; // Overlapping 'p' block found, filter it out } } return true; // Keep 'p' block if it doesn't overlap with any other block }); } const contentToChildren = (token: Token) => { let content = token.content; const splitMarker = "{{^^}}"; for (const child of token.children || []) { content = content.replace(child.raw, splitMarker); } token.children = zipArrays( content.split(splitMarker).map((c): Token => ({ content: c.replaceAll("\n", ""), metadata: {}, raw: c, type: "text", uuid: crypto.randomUUID(), rendersContentOnly: true, })), token.children || [], ).filter((c) => c.children?.length || (c.rendersContentOnly && c.content)); };