ttcMD: finished migrating to abstracted identifier registration, inlines token renderers to reduce memory overhead
This commit is contained in:
parent
447f9f1dc1
commit
a2f50b1fe9
@ -16,6 +16,7 @@ import React, {
|
||||
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { MDSkeletonLoader } from "../loader";
|
||||
import { Token } from "@/types";
|
||||
|
||||
export const TTCMD: FC<
|
||||
{ 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(
|
||||
() => (
|
||||
<>
|
||||
@ -90,9 +93,7 @@ export const TTCMDRenderer: FC<{ tokens: Token[] }> = ({ tokens }) => {
|
||||
|
||||
function renderer(tokens: Token[]) {
|
||||
const usedIds: string[] = [];
|
||||
return tokens.map((t) => (
|
||||
<div className="p" key={t.uuid}>{render(t, usedIds)}</div>
|
||||
));
|
||||
return tokens.map((t) => <div className="p" key={t.uuid}>{t.render(t)}</div>);
|
||||
}
|
||||
|
||||
function render(token: Token, usedIds: string[]) {
|
||||
@ -175,7 +176,7 @@ function render(token: Token, usedIds: string[]) {
|
||||
}
|
||||
case "inline-code":
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -4,6 +4,11 @@ import {
|
||||
TokenAttributes,
|
||||
TokenRenderer,
|
||||
} 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) => {
|
||||
start: number;
|
||||
@ -23,9 +28,9 @@ type TokenIdentifierMap = Map<
|
||||
TokenIdentifier
|
||||
>;
|
||||
|
||||
export const TokenIdentifiers = new Map<
|
||||
export const TokenRenderers = new Map<
|
||||
string,
|
||||
TokenIdentifier
|
||||
TokenRenderer
|
||||
>();
|
||||
|
||||
type IdentifierRegistration = (
|
||||
@ -91,6 +96,7 @@ export function buildIdentifierMap(): [
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
TokenRenderers.set(type, renderFunction);
|
||||
}
|
||||
|
||||
return [TokenIdentifiers, registerIdentifier];
|
||||
@ -99,6 +105,15 @@ export function buildIdentifierMap(): [
|
||||
export const buildOnlyDefaultElements = () => {
|
||||
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 rendersChildrenOnly = true;
|
||||
|
||||
@ -120,8 +135,22 @@ export const buildOnlyDefaultElements = () => {
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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,
|
||||
@ -134,9 +163,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
(s) => {
|
||||
const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/;
|
||||
const match = s.match(rx);
|
||||
if (!match) debugger;
|
||||
const [_, isBlock, content] = match ||
|
||||
["", "", s];
|
||||
const [_, isBlock, content] = match || ["", "", s];
|
||||
|
||||
return {
|
||||
content: content.trim(),
|
||||
@ -148,8 +175,20 @@ export const buildOnlyDefaultElements = () => {
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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,
|
||||
@ -165,8 +204,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap bg-black/20 p-2 rounded-md">
|
||||
{token.content}
|
||||
</pre>
|
||||
);
|
||||
});
|
||||
|
||||
// list
|
||||
@ -187,8 +230,29 @@ export const buildOnlyDefaultElements = () => {
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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(),
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
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
|
||||
@ -244,8 +333,23 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
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
|
||||
@ -271,8 +375,17 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
const { metadata } = token;
|
||||
return (
|
||||
<Link
|
||||
className={metadata.classes ||
|
||||
"dark:text-primary-600 underline dark:no-underline"}
|
||||
href={metadata.href}
|
||||
>
|
||||
{token.content}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
// inline-code
|
||||
@ -289,8 +402,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return (
|
||||
<span className="font-bold">
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// italic
|
||||
@ -318,8 +439,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return (
|
||||
<span className="italic">
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// popover
|
||||
@ -333,8 +458,23 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
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(
|
||||
@ -350,8 +490,23 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
return <>{t.raw}</>;
|
||||
(token) => {
|
||||
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: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
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) => {
|
||||
@ -374,8 +544,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
||||
});
|
||||
|
||||
registerIdentifier("comment", /<!--[\s\S]+?-->/g, (s, rx) => {
|
||||
@ -386,8 +556,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
registerIdentifier("frontmatter", /^---([\s\S]*?)---/g, (s, rx) => {
|
||||
@ -399,8 +569,8 @@ export const buildOnlyDefaultElements = () => {
|
||||
raw: "",
|
||||
uuid: "frontmatter",
|
||||
};
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
}, (token) => {
|
||||
return <>{token.raw}</>;
|
||||
});
|
||||
|
||||
registerIdentifier("table", /^\|\s[\s\S]*?\|(?=(\n\n)|$)/g, (s, rx) => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { FrontMatter, Token, TokenMarker } from "@/types";
|
||||
import { zipArrays } from "../zip";
|
||||
import { TokenIdentifiers } from "./TokenIdentifiers";
|
||||
import { buildOnlyDefaultElements, TokenRenderers } from "./TokenIdentifiers";
|
||||
|
||||
export const createElements = (body: string): Token[] => {
|
||||
const tokens = tokenize(body);
|
||||
@ -15,7 +16,9 @@ const tokenize = (body: string) => {
|
||||
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);
|
||||
let match;
|
||||
while ((match = rx.exec(body)) !== null) {
|
||||
@ -114,13 +117,13 @@ function isAcceptableChild(parentType: string, childType: string): boolean {
|
||||
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[] {
|
||||
return blocks.filter((block) => {
|
||||
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) {
|
||||
if (
|
||||
otherBlock !== block &&
|
||||
@ -129,11 +132,11 @@ function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] {
|
||||
(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,
|
||||
type: token.rendersChildrenOnly ? "p" : "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly: true,
|
||||
rendersContentOnly: token.rendersChildrenOnly ? false : true,
|
||||
render: TokenRenderers.get(token.rendersChildrenOnly ? "p" : "text")!,
|
||||
children: token.rendersChildrenOnly && c.replaceAll("\n", "")
|
||||
? [
|
||||
{
|
||||
@ -166,6 +170,8 @@ const contentToChildren = (token: Token) => {
|
||||
raw: c,
|
||||
type: "text",
|
||||
uuid: crypto.randomUUID(),
|
||||
render: TokenRenderers.get("text")!,
|
||||
rendersContentOnly: true,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
@ -238,6 +244,7 @@ function processChunks(chunks: Token[][]) {
|
||||
metadata: { initialDepth: currentChunk[0].metadata.initialDepth },
|
||||
uuid: crypto.randomUUID(),
|
||||
children: currentChunk,
|
||||
render: TokenRenderers.get("list")!,
|
||||
});
|
||||
mergedChunks.push(currentChunk);
|
||||
break;
|
||||
|
@ -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(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1,40 +1,4 @@
|
||||
[][][]
|
||||
[[
|
||||
|
||||
```
|
||||
[][][]
|
||||
|
||||
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.
|
||||
]]
|
||||
|
||||
/[]
|
||||
| test | Table | header |
|
||||
-------------------------
|
||||
| test | table | row |
|
||||
| look | another |
|
14
types.d.ts
vendored
14
types.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type InlineToken = {
|
||||
type:
|
||||
| "text"
|
||||
@ -39,8 +41,7 @@ type SingleLineToken = {
|
||||
cfg?: SingleLineCfg;
|
||||
uuid: string;
|
||||
};
|
||||
type Token = {
|
||||
type: string;
|
||||
type IdentifiedToken = {
|
||||
metadata: Record<string, string>;
|
||||
children?: Token[];
|
||||
uuid: string;
|
||||
@ -50,6 +51,15 @@ type Token = {
|
||||
rendersContentOnly?: boolean;
|
||||
};
|
||||
|
||||
type TokenRenderer = (t: Token) => ReactNode;
|
||||
|
||||
type TokenAttributes = {
|
||||
type: string;
|
||||
render: TokenRenderer;
|
||||
};
|
||||
|
||||
type Token = IdentifiedToken & TokenAttributes;
|
||||
|
||||
type TokenMarker = {
|
||||
start: number;
|
||||
end: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user