2024-03-14 00:48:00 -06:00

242 lines
6.3 KiB
TypeScript

"use client";
import { zipArrays } from "../zip";
import { TokenIdentifiers } from "./TokenIdentifiers";
export const createElements = (body: string): Token[] => {
const tokens = tokenize(body);
return buildAbstractSyntaxTree(tokens).map((t) => t.token);
};
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 && otherBlock.start < block.start)
)
) {
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;
}
// case "grid":
// 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));
}