870 lines
21 KiB
TypeScript
870 lines
21 KiB
TypeScript
import { sanitize } from "isomorphic-dompurify";
|
|
import Link from "next/link";
|
|
import React, { Fragment } from "react";
|
|
import { Poppable } from "../poppables/components/poppable";
|
|
import { Accordion, AccordionContent } from "../accordion";
|
|
|
|
export const TokenRenderers = new Map<string, TokenRenderer<any>>();
|
|
|
|
export function buildIdentifierMap(): [
|
|
TokenIdentifierMap,
|
|
IdentifierRegistration
|
|
] {
|
|
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
|
|
|
|
function registerIdentifier<M>(
|
|
type: string,
|
|
match: RegExp,
|
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
|
renderFunction: TokenRenderer<M>
|
|
): void;
|
|
function registerIdentifier<M>(
|
|
type: string,
|
|
match: RegExp,
|
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
|
renderFunction: TokenRenderer<M>,
|
|
openTagRx: RegExp,
|
|
closeTagRx: RegExp
|
|
): void;
|
|
function registerIdentifier<M = Record<string, string>>(
|
|
type: string,
|
|
match: RegExp,
|
|
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
|
renderFunction: TokenRenderer<M>,
|
|
openTagRx?: RegExp,
|
|
closeTagRx?: RegExp
|
|
) {
|
|
TokenIdentifiers.set(type, {
|
|
rx: match,
|
|
parse(s) {
|
|
const identifiedToken = parseFunction(s, this.rx);
|
|
const token: TokenAttributes = {
|
|
render: renderFunction,
|
|
type,
|
|
};
|
|
|
|
return { ...token, ...identifiedToken } as Token<M>;
|
|
},
|
|
search:
|
|
openTagRx && closeTagRx
|
|
? (s, start, end) => {
|
|
return search(
|
|
s,
|
|
start,
|
|
end,
|
|
new RegExp(openTagRx, "g"),
|
|
new RegExp(closeTagRx, "g")
|
|
);
|
|
}
|
|
: undefined,
|
|
});
|
|
TokenRenderers.set(type, renderFunction);
|
|
}
|
|
|
|
return [TokenIdentifiers, registerIdentifier];
|
|
}
|
|
|
|
export const buildOnlyDefaultElements = () => {
|
|
const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap();
|
|
|
|
TokenRenderers.set("text", (t: Token<any>) => {
|
|
return (
|
|
<span className="whitespace-pre-wrap">
|
|
{t.content.replaceAll(/\\n ?/g, "\n")}
|
|
</span>
|
|
);
|
|
});
|
|
|
|
const usedIds: string[] = [];
|
|
|
|
const rendersContentOnly = true;
|
|
const rendersChildrenOnly = true;
|
|
|
|
// grid
|
|
registerIdentifier<{ columns: string }>(
|
|
"grid",
|
|
/(?<!\/)(?:\[\])+\n+((?:.|\n)*?)\n+\/\[\]/g,
|
|
(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(),
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersChildrenOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata } = token;
|
|
return (
|
|
<div
|
|
style={
|
|
{
|
|
"--grid-cols": metadata.columns,
|
|
} as React.CSSProperties
|
|
}
|
|
className="grid grid-cols-dynamic gap-x-8 mb-6"
|
|
>
|
|
{children?.map((c) => {
|
|
const Comp = c.metadata.span ? Fragment : "div";
|
|
return (
|
|
<Comp className="p" key={c.uuid}>
|
|
{c.render(c)}
|
|
</Comp>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
},
|
|
/(?<!\/)(?:\[\])+/g,
|
|
/\/\[\]/g
|
|
);
|
|
|
|
// card
|
|
registerIdentifier(
|
|
"card",
|
|
/\[{2}[\s\S]*?\n+\]{2}/g,
|
|
(s) => {
|
|
const rx = /\[{2}((?:!?)(?:[0-9]?))\s*?\n+([\s\S]*)\n+\]{2}/;
|
|
const match = s.match(rx);
|
|
const [_, isBlockOrSpan, content] = match || ["", "", s];
|
|
|
|
const isBlock = isBlockOrSpan.includes("!");
|
|
const span = Number(isBlockOrSpan.replace("!", ""));
|
|
|
|
return {
|
|
content: content.trim(),
|
|
raw: s,
|
|
metadata: {
|
|
isBlock,
|
|
span,
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersChildrenOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata } = token;
|
|
return (
|
|
<div
|
|
data-block={!!metadata.isBlock}
|
|
style={
|
|
{
|
|
"--v-span": metadata.span || 1,
|
|
} as React.CSSProperties
|
|
}
|
|
className="data-[block=false]:card data-[block=false]:mb-6 col-span-2"
|
|
>
|
|
{children?.map((e) => (
|
|
<Fragment key={e.uuid}>{e.render(e)}</Fragment>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
/\[\[/g,
|
|
/\]\]/g
|
|
);
|
|
|
|
// fenced code block
|
|
registerIdentifier(
|
|
"code",
|
|
/`{3}\n+((?:.|\n)*?)\n+`{3}/g,
|
|
(s, rx) => {
|
|
return {
|
|
content: s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse code",
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
return (
|
|
<pre className="whitespace-pre-wrap bg-black/20 p-2 rounded-md">
|
|
{token.content}
|
|
</pre>
|
|
);
|
|
}
|
|
);
|
|
|
|
// list
|
|
registerIdentifier(
|
|
"list",
|
|
/(?<=\n\n|^) *-\s([\s\S]*?)(?=\n\n|$)/g,
|
|
(s, rx) => {
|
|
return {
|
|
content: s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse list",
|
|
raw: s,
|
|
metadata: {
|
|
initialDepth:
|
|
s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersChildrenOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata } = 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: Token<any>) => (
|
|
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
|
))}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|
|
);
|
|
|
|
// ordered-list
|
|
registerIdentifier(
|
|
"ordered-list",
|
|
/(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g,
|
|
(s, rx) => {
|
|
// debugger;
|
|
return {
|
|
content:
|
|
s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list",
|
|
raw: s,
|
|
metadata: {
|
|
// initialDepth:
|
|
// s.replace("\n", "").split(/\d+\./).at(0)?.length.toString() || "1",
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersChildrenOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children } = token;
|
|
debugger;
|
|
return (
|
|
<>
|
|
<ol
|
|
// data-depth={(Number(metadata.initialDepth) / 2) % 3}
|
|
className="ml-6 list-decimal"
|
|
>
|
|
{children?.map((c) => {
|
|
return (
|
|
<li key={c.uuid}>
|
|
{c.children?.map((c: Token<any>) => (
|
|
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
|
))}
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</>
|
|
);
|
|
}
|
|
);
|
|
|
|
// ordered list-item
|
|
// list-item
|
|
registerIdentifier(
|
|
"list-item",
|
|
/(?<=^|\n) *(?:-|\d+\.)\s(.*?)(?=\n|$)/g,
|
|
(s, rx) => {
|
|
return {
|
|
content:
|
|
s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse list-item",
|
|
raw: s,
|
|
metadata: {
|
|
initialDepth:
|
|
s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata } = token;
|
|
return (
|
|
<li data-depth={metadata.initialDepth} className="ml-2">
|
|
{children?.map((c) => (
|
|
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
|
))}
|
|
</li>
|
|
);
|
|
}
|
|
);
|
|
|
|
// heading
|
|
registerIdentifier(
|
|
"heading",
|
|
/^#+\s(.*?)$/gm,
|
|
(s, rx) => {
|
|
const content =
|
|
s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse heading";
|
|
return {
|
|
content: content,
|
|
raw: s,
|
|
metadata: {
|
|
strength: s.match(/#/g)?.length.toString() || "1",
|
|
id: generateId(content, usedIds),
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
return (
|
|
<div
|
|
id={token.metadata.id}
|
|
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
|
|
registerIdentifier(
|
|
"image",
|
|
/\!\[(.*?)\]\((.*?)\)/g,
|
|
(s, rx) => {
|
|
const [_, title, src] = s.match(new RegExp(rx, ""))!;
|
|
|
|
return {
|
|
// content: inline,
|
|
content: title.trim(),
|
|
raw: s,
|
|
metadata: {
|
|
src,
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(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
|
|
registerIdentifier(
|
|
"anchor",
|
|
/(?<![\!^])\[(.*?)\]\((.*?)\)/g,
|
|
(s, rx) => {
|
|
let preset,
|
|
[_, title, href] = s.match(new RegExp(rx, ""))!;
|
|
const match = title.match(/~{2}(cta|button)?(.*)/);
|
|
|
|
if (match) {
|
|
[_, preset, title] = match;
|
|
}
|
|
|
|
const classes = {
|
|
button: "btn-primary inline-block",
|
|
cta: "btn-secondary inline-block uppercase",
|
|
};
|
|
return {
|
|
content: title.trim(),
|
|
raw: s,
|
|
metadata: {
|
|
href,
|
|
classes: classes[preset as keyof typeof classes],
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(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
|
|
registerIdentifier(
|
|
"inline-code",
|
|
/(?<=\s|^)`(.*?)`(?=[\s,.!?)]|$)/gi,
|
|
(s, rx) => {
|
|
return {
|
|
content:
|
|
s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse inline-code",
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
return (
|
|
<span className="p-1 rounded-md font-mono bg-black/20 border border-mixed-100/20 mx-1">
|
|
{token.content}
|
|
</span>
|
|
);
|
|
}
|
|
);
|
|
|
|
// bold
|
|
registerIdentifier(
|
|
"bold",
|
|
/\*{2}(.*?)\*{2}/g,
|
|
(s, rx) => {
|
|
return {
|
|
content: s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse bold",
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
return <span className="font-bold">{token.content}</span>;
|
|
}
|
|
);
|
|
|
|
// italic
|
|
registerIdentifier(
|
|
"italic",
|
|
/(?<!\*)\*([^\*]+?)\*(?!\*)/g,
|
|
(s, rx) => {
|
|
return {
|
|
content:
|
|
s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse italic",
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
return <span className="italic">{token.content}</span>;
|
|
}
|
|
);
|
|
|
|
// popover
|
|
registerIdentifier(
|
|
"popover",
|
|
/\^\[(.*?)\]\<<(.*?)\>>/g,
|
|
(s, rx) => {
|
|
const [_, title, content] = s.match(new RegExp(rx, ""))!;
|
|
|
|
return {
|
|
content,
|
|
raw: s,
|
|
metadata: { title },
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata, uuid } = token;
|
|
return (
|
|
<Poppable
|
|
content={
|
|
children?.map((c) => (
|
|
<Fragment key={uuid}>{c.render(c)}</Fragment>
|
|
)) || token.content
|
|
}
|
|
preferredAlign="centered"
|
|
preferredEdge="bottom"
|
|
className="cursor-pointer mx-2"
|
|
>
|
|
<span className="border-b-2 border-dotted border-white">
|
|
{metadata.title}
|
|
</span>
|
|
</Poppable>
|
|
);
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"accordion",
|
|
/\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
|
|
(s, rx) => {
|
|
const [_, title, content] = s.match(new RegExp(rx, ""))!;
|
|
|
|
return {
|
|
content,
|
|
raw: s,
|
|
metadata: { title },
|
|
uuid: crypto.randomUUID(),
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children, metadata } = token;
|
|
return (
|
|
<div className="bg-black/20 p-1 accordion">
|
|
<Accordion title={metadata.title || "Expand"}>
|
|
<AccordionContent>
|
|
{children?.map((e) => (
|
|
<Fragment key={e.uuid}>{e.render(e)}</Fragment>
|
|
))}
|
|
</AccordionContent>
|
|
</Accordion>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"p",
|
|
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
|
|
(s) => {
|
|
return {
|
|
content: s.replace("\n", " "),
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
};
|
|
},
|
|
(token) => {
|
|
const { children } = token;
|
|
|
|
return (
|
|
<div className="p">
|
|
{children?.map((e) => {
|
|
return <Fragment key={e.uuid}>{e.render(e)}</Fragment>;
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"hr",
|
|
/^-{3,}$/gm,
|
|
(s) => {
|
|
return {
|
|
content: s,
|
|
raw: s,
|
|
metadata: {},
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
() => {
|
|
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"comment",
|
|
/<!--[\s\S]+?-->/g,
|
|
(s) => {
|
|
return {
|
|
content: "",
|
|
metadata: { comment: s },
|
|
raw: "",
|
|
uuid: crypto.randomUUID(),
|
|
rendersContentOnly,
|
|
};
|
|
},
|
|
() => {
|
|
return <></>;
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"frontmatter",
|
|
/^---([\s\S]*?)---/g,
|
|
(s, rx) => {
|
|
return {
|
|
content: "",
|
|
metadata: {
|
|
frontmatterString: s.match(rx)?.at(0) || "",
|
|
},
|
|
raw: "",
|
|
uuid: "frontmatter",
|
|
};
|
|
},
|
|
(token) => {
|
|
return <>{token.raw}</>;
|
|
}
|
|
);
|
|
|
|
registerIdentifier(
|
|
"table",
|
|
/(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
|
|
(s) => {
|
|
const splitMarker = "{{^^}}";
|
|
const original = s;
|
|
|
|
let columnPattern: string[] = [];
|
|
let fullWidth = false;
|
|
s = s.replace(/^\| (<-)?[-|\s^]+(->)? \|$/gm, (e) => {
|
|
if (!columnPattern.length) {
|
|
columnPattern = e.split("|").filter((e) => e);
|
|
}
|
|
if (e.match(/^\| <-.*?-> \|/gm)) {
|
|
fullWidth = true;
|
|
}
|
|
return splitMarker;
|
|
});
|
|
|
|
const rowSections = s.split(splitMarker).map((s) =>
|
|
s
|
|
.split("\n")
|
|
.filter((r) => !!r)
|
|
.map((r) =>
|
|
r
|
|
.split("|")
|
|
.map((c) => c.trim())
|
|
.filter((c) => !!c)
|
|
)
|
|
);
|
|
|
|
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: original,
|
|
raw: original,
|
|
metadata: {
|
|
headerRows: headerRows,
|
|
bodyRows: bodyRows,
|
|
footerRows: footerRows,
|
|
columns: maxColumns,
|
|
columnPattern,
|
|
fullWidth,
|
|
},
|
|
uuid: crypto.randomUUID(),
|
|
};
|
|
},
|
|
(t) => {
|
|
const {
|
|
headerRows,
|
|
bodyRows,
|
|
footerRows,
|
|
columns,
|
|
columnPattern,
|
|
fullWidth,
|
|
} = t.metadata;
|
|
|
|
return (
|
|
<table
|
|
data-full-width={fullWidth}
|
|
className="md-table data-[full-width=true]:w-full"
|
|
>
|
|
{!!headerRows && (
|
|
<thead>
|
|
{headerRows.map((r, i) => (
|
|
<tr key={r.join() + i}>
|
|
{r.concat(Array(columns - r.length).fill("")).map((c) => {
|
|
const child = t.children?.find((child) => child.raw === c);
|
|
return (
|
|
<th key={r.join() + i + c}>
|
|
{child?.render(child) || c}
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
)}
|
|
{!!bodyRows && (
|
|
<tbody>
|
|
{bodyRows.map((r, i) => (
|
|
<tr key={r.join() + i}>
|
|
{r.concat(Array(columns - r.length).fill("")).map((c, i) => {
|
|
const child = t.children?.find((child) => child.raw === c);
|
|
return (
|
|
<td
|
|
key={r.join() + i + c}
|
|
className="data-[center=true]:text-center"
|
|
data-center={
|
|
!!(
|
|
columnPattern?.at(i) &&
|
|
columnPattern.at(i)?.includes("^")
|
|
)
|
|
}
|
|
>
|
|
{child?.render(child) || c}
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
)}
|
|
{!!footerRows && (
|
|
<tfoot>
|
|
{footerRows.map((r, i) => (
|
|
<tr key={r.join() + i}>
|
|
{r.concat(Array(columns - r.length).fill("")).map((c) => {
|
|
const child = t.children?.find((child) => child.raw === c);
|
|
return (
|
|
<td key={r.join() + i + c}>
|
|
{child?.render(child) || c}
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tfoot>
|
|
)}
|
|
</table>
|
|
);
|
|
}
|
|
);
|
|
|
|
return TokenIdentifiers;
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
// Finds a unique id for things like headings
|
|
function generateId(t: string, usedIds: string[]) {
|
|
let id = t
|
|
.toLowerCase()
|
|
.replace(/[^a-z\s]/gi, "")
|
|
.trim()
|
|
.replaceAll(" ", "-");
|
|
let idNum = 1;
|
|
while (usedIds.includes(id)) {
|
|
id = id.replace(/-[0-9]+$/g, "");
|
|
id += "-" + idNum;
|
|
idNum++;
|
|
}
|
|
return id;
|
|
}
|