"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; if (token.search) { const found = token.search(body.substring(start), start, end); rx.lastIndex = found.lastIndex; addToken({ start: found.start, end: found.end, type, token: token.parse(found.text), }); continue; } 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 && isAcceptableChild(otherBlock.type, block.type) ) { minDistance = distance; closestParent = otherBlock; } } } if (closestParent) { block.parent = closestParent; // Assign the closest parent block } } } type ParentChildMap = { [parentType: string]: string[]; // Map parent types to an array of acceptable child types }; const parentChildMap: ParentChildMap = { "list": ["list-item"], // Add more mappings as needed... }; function isAcceptableChild(parentType: string, childType: string): boolean { const acceptableChildren = parentChildMap[parentType]; return acceptableChildren ? acceptableChildren.includes(childType) : true; } 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; if (!content) return; const wasSpecialCase = handleSpecial(token); if (wasSpecialCase) return; 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)); }; function handleSpecial(token: Token) { switch (token.type) { case "list": { const chunks = splitByDepth(token.children!); const items = processChunks(chunks); token.children = items.flat(); return token.children; } default: return; } } function splitByDepth(items: Token[]) { const chunks: Token[][] = []; let currentDepth = -1; let chunk: Token[] = []; if (!items) return chunks; for (const item of items) { const depth = Number(item.metadata.initialDepth); if (depth === currentDepth) { chunk.push(item); } else { if (chunk.length > 0) { chunks.push(chunk); } chunk = [item]; currentDepth = depth; } } if (chunk.length > 0) { chunks.push(chunk); } return chunks; } function processChunks(chunks: Token[][]) { const mergedChunks: Token[][] = []; for (let i = 1; i < chunks.length; i++) { const currentChunk = chunks[i]; let j = i - 1; // Find the first chunk with a lower depth while (j >= 0) { const prevChunk = chunks[j]; const prevDepth = Number(prevChunk[0].metadata.initialDepth); if (prevDepth < Number(currentChunk[0].metadata.initialDepth)) { // Append the current chunk to the children of the found chunk const lastPrev = prevChunk[prevChunk.length - 1]; lastPrev.children = lastPrev.children || []; lastPrev.children.push({ type: "list", content: "", raw: "", metadata: { initialDepth: currentChunk[0].metadata.initialDepth }, uuid: crypto.randomUUID(), children: currentChunk, }); mergedChunks.push(currentChunk); break; } j--; } } // Filter out chunks that were merged into others return chunks.filter((c) => !mergedChunks.find((c2) => c === c2)); }