ttcMD: finished migrating to abstracted identifier registration, inlines token renderers to reduce memory overhead

This commit is contained in:
Emmaline Autumn 2024-03-15 08:22:43 -06:00
parent 447f9f1dc1
commit a2f50b1fe9
7 changed files with 290 additions and 504 deletions

View File

@ -16,6 +16,7 @@ import React, {
import { sanitize } from "isomorphic-dompurify"; import { sanitize } from "isomorphic-dompurify";
import { MDSkeletonLoader } from "../loader"; import { MDSkeletonLoader } from "../loader";
import { Token } from "@/types";
export const TTCMD: FC< export const TTCMD: FC<
{ body: string; escapeTOC?: (tokens: Token[]) => boolean } { body: string; escapeTOC?: (tokens: Token[]) => boolean }
@ -64,7 +65,9 @@ export const TTCMD: FC<
); );
}; };
export const TTCMDRenderer: FC<{ tokens: Token[] }> = ({ tokens }) => { export const TTCMDRenderer: FC<{ tokens: Token[] }> = (
{ tokens },
) => {
const tada = useMemo( const tada = useMemo(
() => ( () => (
<> <>
@ -90,9 +93,7 @@ export const TTCMDRenderer: FC<{ tokens: Token[] }> = ({ tokens }) => {
function renderer(tokens: Token[]) { function renderer(tokens: Token[]) {
const usedIds: string[] = []; const usedIds: string[] = [];
return tokens.map((t) => ( return tokens.map((t) => <div className="p" key={t.uuid}>{t.render(t)}</div>);
<div className="p" key={t.uuid}>{render(t, usedIds)}</div>
));
} }
function render(token: Token, usedIds: string[]) { function render(token: Token, usedIds: string[]) {
@ -175,7 +176,7 @@ function render(token: Token, usedIds: string[]) {
} }
case "inline-code": case "inline-code":
return ( return (
<span className="p-1 rounded-md font-mono bg-black/20 border border-mixed-100/20 mx-2"> <span className="p-1 rounded-md font-mono bg-black/20 border border-mixed-100/20 mx-1">
{token.content} {token.content}
</span> </span>
); );

View File

@ -1,411 +0,0 @@
import { parse } from "path";
type TokenIdentifier = {
rx: RegExp;
parse: (s: string) => Token;
search?: (s: string, start: number, end: number) => {
start: number;
end: number;
text: string;
lastIndex: number;
};
};
export const TokenIdentifiers = new Map<
string,
TokenIdentifier
>();
// TokenIdentifiers.set("p", {
// rx: /\n{2,}((?:.|\n)*?)\n{2,}/g,
// parse(s) {
// const [_, content] = s.match(new RegExp(this.rx, ""))!;
// return {
// // content,
// content,
// raw: s,
// metadata: {},
// type: "p",
// uuid: crypto.randomUUID(),
// };
// },
// });
const rendersContentOnly = true;
const rendersChildrenOnly = true;
TokenIdentifiers.set("grid", {
search(s, start, end) {
const rx = /(?<!\/)(?:\[\])+/g;
const closeRx = /\/\[\]/g;
return search(s, start, end, rx, closeRx);
},
rx: /(?<!\/)(?:\[\])+\n+((?:.|\n)*?)\n+\/\[\]/g,
parse(s) {
const rx = /((?:\[\])+)\n+([\s\S]*)\n+\/\[\]/;
const [_, columns, content] = s.match(rx) ||
["", "..", "Unable to parse grid"];
return {
content,
raw: s,
metadata: {
columns: (columns.length / 2).toString(),
},
type: "grid",
uuid: crypto.randomUUID(),
rendersChildrenOnly,
};
},
});
TokenIdentifiers.set("card", {
rx: /\[{2}([\s\S]*?)\n+\]{2}/g,
search(s, start, end) {
const rx = /\[\[/g;
const crx = /\]\]/g;
return search(s, start, end, rx, crx);
},
parse(s) {
const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/;
const match = s.match(rx);
if (!match) debugger;
const [_, isBlock, content] = match ||
["", "", s];
// if (!_) debugger;
return {
content: content.trim(),
raw: s,
metadata: {
isBlock,
},
type: "card",
uuid: crypto.randomUUID(),
rendersChildrenOnly,
};
},
});
TokenIdentifiers.set("code", {
rx: /`{3}\n+((?:.|\n)*?)\n+`{3}/g,
parse(s) {
return {
content: s.match(new RegExp(this.rx, ""))?.at(1) ||
"Unable to parse code",
raw: s,
metadata: {},
type: "code",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("list", {
rx: /^\s*-\s([\s\S]*?)\n\n/gm,
parse(s) {
return {
content: s.match(new RegExp(this.rx, ""))?.at(1) ||
"Unable to parse list",
raw: s,
metadata: {
initialDepth: s.replace("\n", "").split("-").at(0)?.length.toString() ||
"1",
},
type: "list",
uuid: crypto.randomUUID(),
rendersChildrenOnly,
};
},
});
TokenIdentifiers.set("list-item", {
rx: /^\s*-\s(.*?)$/gm,
parse(s) {
return {
content: s.match(new RegExp(this.rx, ""))?.at(1) ||
"Unable to parse list-item",
raw: s,
metadata: {
initialDepth: s.replace("\n", "").split("-").at(0)?.length.toString() ||
"1",
},
type: "list-item",
uuid: crypto.randomUUID(),
};
},
});
TokenIdentifiers.set("heading", {
rx: /^#+\s(.*?)$/gm,
parse(s) {
return {
content: s.match(new RegExp(this.rx, ""))?.at(1) ||
"Unable to parse heading",
raw: s,
metadata: {
strength: s.match(/#/g)?.length.toString() || "1",
},
type: "heading",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("image", {
rx: /\!\[(.*?)\]\((.*?)\)/g,
parse(s) {
const [_, title, src] = s.match(new RegExp(this.rx, ""))!;
return {
// content: inline,
content: title.trim(),
raw: s,
metadata: {
src,
},
type: "image",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("anchor", {
rx: /(?<![\!^])\[(.*?)\]\((.*?)\)/g,
parse(s) {
let preset, [_, title, href] = s.match(new RegExp(this.rx, ""))!;
const match = title.match(/`{3}(cta|button)?(.*)/);
if (match) {
[_, preset, title] = match;
}
const classes = {
button: "btn-primary inline-block",
cta: "btn-secondary inline-block uppercase",
};
return {
// content: inline,
content: title.trim(),
raw: s,
metadata: {
href,
classes: classes[preset as keyof typeof classes],
},
type: "anchor",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("inline-code", {
rx: /(?<=\s|^)`(.*?)`(?=[\s,.!?)]|$)/gi,
parse(s) {
return {
// content: inline,
content: s.match(new RegExp(this.rx, "i"))?.at(1) ||
"Unable to parse inline-code",
raw: s,
metadata: {},
type: "inline-code",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("bold", {
rx: /\*{2}(.*?)\*{2}/g,
parse(s) {
return {
// content: inline,
content: s.match(new RegExp(this.rx, "i"))?.at(1) ||
"Unable to parse bold",
raw: s,
metadata: {},
type: "bold",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("italic", {
rx: /(?<!\*)\*([^\*]+?)\*(?!\*)/g,
parse(s) {
return {
// content: inline,
content: s.match(new RegExp(this.rx, "i"))?.at(1) ||
"Unable to parse italic",
raw: s,
metadata: {},
type: "italic",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("popover", {
rx: /\^\[(.*?)\]\<<(.*?)\>>/g,
parse(s) {
const [_, title, content] = s.match(new RegExp(this.rx, ""))!;
return {
// content,
content,
raw: s,
metadata: { title },
type: "popover",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("accordion", {
rx: /\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
parse(s) {
const [_, title, content] = s.match(new RegExp(this.rx, ""))!;
return {
// content,
content,
raw: s,
metadata: { title },
type: "accordion",
uuid: crypto.randomUUID(),
};
},
});
TokenIdentifiers.set("p", {
// rx: /(?<=\n)\n?([\s\S]*?)\n(?=\n)/g,
rx: /(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
parse(s) {
// const [_, content] = s.match(new RegExp(this.rx, ""))!;
return {
// content,
content: s,
raw: s,
metadata: {},
type: "p",
uuid: crypto.randomUUID(),
};
},
});
TokenIdentifiers.set("hr", {
rx: /^-{3,}$/gm,
parse(s) {
return {
content: s,
raw: s,
metadata: {},
type: "hr",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("comment", {
rx: /<!--[\s\S]+?-->/g,
parse(s) {
return {
content: "",
metadata: { comment: s },
raw: "",
type: "comment",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
TokenIdentifiers.set("frontmatter", {
rx: /^---([\s\S]*?)---/g,
parse(s) {
return {
content: "",
metadata: {
frontmatterString: s.match(this.rx)?.at(0) || "",
},
raw: "",
type: "frontmatter",
uuid: "frontmatter",
};
},
});
function findMatchingClosedParenthesis(
str: string,
openRegex: RegExp,
closedRegex: RegExp,
): number | null {
let openings = 0;
let closings = 0;
openRegex = new RegExp(openRegex, "g");
closedRegex = new RegExp(closedRegex, "g");
let lastOpeningSuccessIndex = 0;
let lastClosingSuccessIndex = 0;
do {
const openingMatch = openRegex.exec(str);
const closingMatch = closedRegex.exec(str);
if ((openingMatch && !closingMatch)) {
throw Error("Things have gone horribly wrong");
}
// if ((!openingMatch && closingMatch) || (!openingMatch && !closingMatch)) break;
if (
openingMatch && closingMatch && openingMatch.index < closingMatch.index
) {
openings++;
lastOpeningSuccessIndex = openingMatch.index + openingMatch[0].length;
closedRegex.lastIndex = lastClosingSuccessIndex;
} else if (
(!openingMatch && closingMatch) ||
(openingMatch && closingMatch && openingMatch.index > closingMatch.index)
) {
closings++;
lastClosingSuccessIndex = closingMatch.index + closingMatch[0].length;
openRegex.lastIndex = lastOpeningSuccessIndex;
} else {
return closingMatch?.index ?? null;
}
} while (openings > closings);
return closedRegex.lastIndex;
}
interface SearchResult {
start: number;
end: number;
text: string;
lastIndex: number;
}
function search(
s: string,
start: number,
end: number,
openRx: RegExp,
closeRx: RegExp,
): SearchResult {
const oldEnd = end;
const newEnd = findMatchingClosedParenthesis(
s,
// s.substring(0, end - start),
openRx,
closeRx,
);
if (newEnd === null) throw Error("There was an issue finding a closing tag");
end = newEnd + start;
return {
start,
end,
text: s.substring(0, newEnd),
lastIndex: oldEnd === end ? end : start + s.match(openRx)![0].length,
};
}

View File

@ -4,6 +4,11 @@ import {
TokenAttributes, TokenAttributes,
TokenRenderer, TokenRenderer,
} from "@/types"; } from "@/types";
import { sanitize } from "isomorphic-dompurify";
import Link from "next/link";
import { Fragment } from "react";
import { Poppable } from "../poppables/components/poppable";
import { Accordion, AccordionContent } from "../accordion";
type SearchFunction = (s: string, start: number, end: number) => { type SearchFunction = (s: string, start: number, end: number) => {
start: number; start: number;
@ -23,9 +28,9 @@ type TokenIdentifierMap = Map<
TokenIdentifier TokenIdentifier
>; >;
export const TokenIdentifiers = new Map< export const TokenRenderers = new Map<
string, string,
TokenIdentifier TokenRenderer
>(); >();
type IdentifierRegistration = ( type IdentifierRegistration = (
@ -91,6 +96,7 @@ export function buildIdentifierMap(): [
} }
: undefined, : undefined,
}); });
TokenRenderers.set(type, renderFunction);
} }
return [TokenIdentifiers, registerIdentifier]; return [TokenIdentifiers, registerIdentifier];
@ -99,6 +105,15 @@ export function buildIdentifierMap(): [
export const buildOnlyDefaultElements = () => { export const buildOnlyDefaultElements = () => {
const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap(); const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap();
TokenRenderers.set("text", (t) => {
debugger;
return (
<span className="whitespace-pre-wrap">
{t.content.replaceAll("\\n", "\n")}
</span>
);
});
const rendersContentOnly = true; const rendersContentOnly = true;
const rendersChildrenOnly = true; const rendersChildrenOnly = true;
@ -120,8 +135,22 @@ export const buildOnlyDefaultElements = () => {
rendersChildrenOnly, rendersChildrenOnly,
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; const { content, children, metadata, uuid } = token;
return (
<div
style={{
"--grid-cols": metadata.columns,
} as React.CSSProperties}
className="grid grid-cols-dynamic gap-x-8 gap-y-6 mb-6"
>
{children?.map((c, i) => (
<div key={c.uuid}>
{c.render(c)}
</div>
))}
</div>
);
}, },
/(?<!\/)(?:\[\])+/g, /(?<!\/)(?:\[\])+/g,
/\/\[\]/g, /\/\[\]/g,
@ -134,9 +163,7 @@ export const buildOnlyDefaultElements = () => {
(s) => { (s) => {
const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/; const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/;
const match = s.match(rx); const match = s.match(rx);
if (!match) debugger; const [_, isBlock, content] = match || ["", "", s];
const [_, isBlock, content] = match ||
["", "", s];
return { return {
content: content.trim(), content: content.trim(),
@ -148,8 +175,20 @@ export const buildOnlyDefaultElements = () => {
rendersChildrenOnly, rendersChildrenOnly,
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; const { children, metadata, uuid } = token;
return (
<div
data-block={!!metadata.isBlock}
className="data-[block=false]:card mb-6"
>
{children?.map((e) => (
<Fragment key={e.uuid}>
{e.render(e)}
</Fragment>
))}
</div>
);
}, },
/\[\[/g, /\[\[/g,
/\]\]/g, /\]\]/g,
@ -165,8 +204,12 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return (
<pre className="whitespace-pre-wrap bg-black/20 p-2 rounded-md">
{token.content}
</pre>
);
}); });
// list // list
@ -187,8 +230,29 @@ export const buildOnlyDefaultElements = () => {
rendersChildrenOnly, rendersChildrenOnly,
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; const { children, metadata, uuid } = token;
return (
<>
<ul
data-depth={(Number(metadata.initialDepth) / 2) % 3}
className="data-[depth='2']:list-[circle] data-[depth='1']:list-[square] list-disc ml-6"
>
{children?.map((c) => {
return (
<li
key={c.uuid}
data-depth={metadata.initialDepth}
>
{c.children?.map((c) => (
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
))}
</li>
);
})}
</ul>
</>
);
}, },
); );
@ -209,8 +273,20 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; const { children, metadata, uuid } = token;
return (
<li
data-depth={metadata.initialDepth}
className="ml-2"
>
{children?.map((c) => (
<Fragment key={c.uuid}>
(c.render(c))
</Fragment>
))}
</li>
);
}, },
); );
@ -226,8 +302,21 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return (
<div
// id={generateId(token.raw, usedIds)}
data-strength={token.metadata.strength}
className={`
font-bold
data-[strength="1"]:text-4xl
data-[strength="2"]:text-3xl
data-[strength="3"]:text-2xl
`}
>
{token.content}
</div>
);
}); });
// image // image
@ -244,8 +333,23 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; const { metadata } = token;
metadata.src = metadata.src as string;
if (metadata.src.startsWith("<svg")) {
return (
<div
dangerouslySetInnerHTML={{
__html: sanitize(metadata.src, {
USE_PROFILES: { svg: true },
}),
}}
>
</div>
);
}
// eslint-disable-next-line @next/next/no-img-element
return <img src={metadata.src} alt={token.content} />;
}); });
// anchor // anchor
@ -271,8 +375,17 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; const { metadata } = token;
return (
<Link
className={metadata.classes ||
"dark:text-primary-600 underline dark:no-underline"}
href={metadata.href}
>
{token.content}
</Link>
);
}); });
// inline-code // inline-code
@ -289,8 +402,12 @@ export const buildOnlyDefaultElements = () => {
rendersContentOnly, rendersContentOnly,
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; return (
<span className="p-1 rounded-md font-mono bg-black/20 border border-mixed-100/20 mx-1">
{token.content}
</span>
);
}, },
); );
@ -304,8 +421,12 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return (
<span className="font-bold">
{token.content}
</span>
);
}); });
// italic // italic
@ -318,8 +439,12 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return (
<span className="italic">
{token.content}
</span>
);
}); });
// popover // popover
@ -333,8 +458,23 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; const { children, metadata, uuid } = token;
return (
<Poppable
content={children?.map((c) => (
<Fragment key={uuid}>{c.render(c)}</Fragment>
)) ||
metadata.content}
preferredAlign="centered"
preferredEdge="bottom"
className="cursor-pointer mx-2"
>
<span className="border-b-2 border-dotted border-white">
{metadata.title}
</span>
</Poppable>
);
}); });
registerIdentifier( registerIdentifier(
@ -350,8 +490,23 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
}; };
}, },
(t) => { (token) => {
return <>{t.raw}</>; const { children, metadata, uuid } = token;
return (
<div className="bg-black/20 p-1 accordion">
<Accordion
title={metadata.title || "Expand"}
>
<AccordionContent>
{children?.map((e, i) => (
<Fragment key={e.uuid}>
{e.render(e)}
</Fragment>
))}
</AccordionContent>
</Accordion>
</div>
);
}, },
); );
@ -362,8 +517,23 @@ export const buildOnlyDefaultElements = () => {
metadata: {}, metadata: {},
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; const { children, uuid } = token;
debugger;
return (
<div className="p">
{children?.map((e) => {
console.log(e);
return (
<Fragment key={e.uuid}>
{e.render(e)}
</Fragment>
);
})}
</div>
);
}); });
registerIdentifier("hr", /^-{3,}$/gm, (s, rx) => { registerIdentifier("hr", /^-{3,}$/gm, (s, rx) => {
@ -374,8 +544,8 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return <div className="w-full border-b border-mixed-500 my-3"></div>;
}); });
registerIdentifier("comment", /<!--[\s\S]+?-->/g, (s, rx) => { registerIdentifier("comment", /<!--[\s\S]+?-->/g, (s, rx) => {
@ -386,8 +556,8 @@ export const buildOnlyDefaultElements = () => {
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly, rendersContentOnly,
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return <></>;
}); });
registerIdentifier("frontmatter", /^---([\s\S]*?)---/g, (s, rx) => { registerIdentifier("frontmatter", /^---([\s\S]*?)---/g, (s, rx) => {
@ -399,8 +569,8 @@ export const buildOnlyDefaultElements = () => {
raw: "", raw: "",
uuid: "frontmatter", uuid: "frontmatter",
}; };
}, (t) => { }, (token) => {
return <>{t.raw}</>; return <>{token.raw}</>;
}); });
registerIdentifier("table", /^\|\s[\s\S]*?\|(?=(\n\n)|$)/g, (s, rx) => { registerIdentifier("table", /^\|\s[\s\S]*?\|(?=(\n\n)|$)/g, (s, rx) => {

View File

@ -1,7 +1,8 @@
"use client"; "use client";
import { FrontMatter, Token, TokenMarker } from "@/types";
import { zipArrays } from "../zip"; import { zipArrays } from "../zip";
import { TokenIdentifiers } from "./TokenIdentifiers"; import { buildOnlyDefaultElements, TokenRenderers } from "./TokenIdentifiers";
export const createElements = (body: string): Token[] => { export const createElements = (body: string): Token[] => {
const tokens = tokenize(body); const tokens = tokenize(body);
@ -15,7 +16,9 @@ const tokenize = (body: string) => {
tokenizedBody.push(thing); tokenizedBody.push(thing);
}; };
for (const [type, token] of TokenIdentifiers.entries()) { const ti = buildOnlyDefaultElements();
for (const [type, token] of ti.entries()) {
const rx = new RegExp(token.rx); const rx = new RegExp(token.rx);
let match; let match;
while ((match = rx.exec(body)) !== null) { while ((match = rx.exec(body)) !== null) {
@ -114,13 +117,13 @@ function isAcceptableChild(parentType: string, childType: string): boolean {
return acceptableChildren ? acceptableChildren.includes(childType) : true; return acceptableChildren ? acceptableChildren.includes(childType) : true;
} }
// Occasionally, some P blocks start exactly at the same point as another block (a side effect of needing to exclude preceding linebreaks from the regex while also having the only clear delineation being those linebreaks) so we just remove those P blocks so that when searching for a parent, it doesn't need to figure out if the P block is valid or not. This doesn't cause issues during rendering since each block handles its own container element
function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] { function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] {
return blocks.filter((block) => { return blocks.filter((block) => {
if (block.type !== "p") { if (block.type !== "p") {
return true; // Keep blocks that are not 'p' type return true;
} }
// Filter out 'p' blocks that overlap with any other block
for (const otherBlock of blocks) { for (const otherBlock of blocks) {
if ( if (
otherBlock !== block && otherBlock !== block &&
@ -129,11 +132,11 @@ function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] {
(otherBlock.end === block.end && otherBlock.start < block.start) (otherBlock.end === block.end && otherBlock.start < block.start)
) )
) { ) {
return false; // Overlapping 'p' block found, filter it out return false;
} }
} }
return true; // Keep 'p' block if it doesn't overlap with any other block return true;
}); });
} }
@ -157,7 +160,8 @@ const contentToChildren = (token: Token) => {
raw: c, raw: c,
type: token.rendersChildrenOnly ? "p" : "text", type: token.rendersChildrenOnly ? "p" : "text",
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
rendersContentOnly: true, rendersContentOnly: token.rendersChildrenOnly ? false : true,
render: TokenRenderers.get(token.rendersChildrenOnly ? "p" : "text")!,
children: token.rendersChildrenOnly && c.replaceAll("\n", "") children: token.rendersChildrenOnly && c.replaceAll("\n", "")
? [ ? [
{ {
@ -166,6 +170,8 @@ const contentToChildren = (token: Token) => {
raw: c, raw: c,
type: "text", type: "text",
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
render: TokenRenderers.get("text")!,
rendersContentOnly: true,
}, },
] ]
: undefined, : undefined,
@ -238,6 +244,7 @@ function processChunks(chunks: Token[][]) {
metadata: { initialDepth: currentChunk[0].metadata.initialDepth }, metadata: { initialDepth: currentChunk[0].metadata.initialDepth },
uuid: crypto.randomUUID(), uuid: crypto.randomUUID(),
children: currentChunk, children: currentChunk,
render: TokenRenderers.get("list")!,
}); });
mergedChunks.push(currentChunk); mergedChunks.push(currentChunk);
break; break;

View File

@ -42,3 +42,48 @@ const paragraphTokens: {
}, },
}, },
]; ];
TokenIdentifiers.set("table", {
rx: /^\|\s[\s\S]*?\|(?=(\n\n)|$)/g,
parse(s) {
const rowSections = s.split(/-/gm).map((s) =>
s.split("\n").map((r) => r.split(/\s?\|\s?/g))
);
let headerRows: string[][] = [],
bodyRows: string[][] = [],
footerRows: string[][] = [];
switch (rowSections.length) {
case 1:
bodyRows = rowSections[0];
break;
case 2:
headerRows = rowSections[0];
bodyRows = rowSections[1];
break;
case 3:
headerRows = rowSections[0];
bodyRows = rowSections[1];
footerRows = rowSections[3];
break;
}
const maxColumns = Math.max(
...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length),
);
return {
content: s,
raw: s,
metadata: {
headerRows: headerRows.join(" | "),
bodyRows: bodyRows.join(" | "),
footerRows: footerRows.join(" | "),
columns: maxColumns.toString(),
},
type: "table",
uuid: crypto.randomUUID(),
};
},
});

View File

@ -1,40 +1,4 @@
[][][] | test | Table | header |
[[ -------------------------
| test | table | row |
``` | look | another |
[][][]
This will make three columns, just like how this is laid out right now.
Each element will get its own cell in the grid.
So each of these paragraphs will end up in a separate column.
/[]
```
]]
[[
```
[][]
This will make two columns
[[
Each column can use a different element
]]
/[]
```
]]
[[
This card will end up in the third column...
]]
[[
... but since there isn't enough for this one, it will automatically get moved to the next row.
]]
/[]

14
types.d.ts vendored
View File

@ -1,3 +1,5 @@
import { ReactNode } from "react";
type InlineToken = { type InlineToken = {
type: type:
| "text" | "text"
@ -39,8 +41,7 @@ type SingleLineToken = {
cfg?: SingleLineCfg; cfg?: SingleLineCfg;
uuid: string; uuid: string;
}; };
type Token = { type IdentifiedToken = {
type: string;
metadata: Record<string, string>; metadata: Record<string, string>;
children?: Token[]; children?: Token[];
uuid: string; uuid: string;
@ -50,6 +51,15 @@ type Token = {
rendersContentOnly?: boolean; rendersContentOnly?: boolean;
}; };
type TokenRenderer = (t: Token) => ReactNode;
type TokenAttributes = {
type: string;
render: TokenRenderer;
};
type Token = IdentifiedToken & TokenAttributes;
type TokenMarker = { type TokenMarker = {
start: number; start: number;
end: number; end: number;