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"; import { OnDemandResolver, Resolver } from "./Resolver"; export const TokenRenderers = new Map>(); export function buildIdentifierMap(): [ TokenIdentifierMap, IdentifierRegistration ] { const TokenIdentifiers = new Map>(); function registerIdentifier( type: string, match: RegExp, parseFunction: (s: string, rx: RegExp) => IdentifiedToken, renderFunction: TokenRenderer ): void; function registerIdentifier( type: string, match: RegExp, parseFunction: (s: string, rx: RegExp) => IdentifiedToken, renderFunction: TokenRenderer, openTagRx: RegExp, closeTagRx: RegExp ): void; function registerIdentifier>( type: string, match: RegExp, parseFunction: (s: string, rx: RegExp) => IdentifiedToken, renderFunction: TokenRenderer, 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; }, 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) => { return ( {t.content.replaceAll(/\\n ?/g, "\n")} ); }); const usedIds: string[] = []; const rendersContentOnly = true; const rendersChildrenOnly = true; // grid registerIdentifier<{ columns: string }>( "grid", /(? { 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 (
{children?.map((c) => { const Comp = c.metadata.span ? Fragment : "div"; return ( {c.render(c)} ); })}
); }, /(? { 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 (
{children?.map((e) => ( {e.render(e)} ))}
); }, /\[\[/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 (
          {token.content}
        
); } ); // 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 ( <>
    {children?.map((c) => { return (
  • {c.children?.map((c: Token) => ( {c.render(c)} ))}
  • ); })}
); } ); // ordered-list registerIdentifier( "ordered-list", /(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g, (s, rx) => { 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; return ( <>
    {children?.map((c) => { return (
  1. {c.children?.map((c: Token) => ( {c.render(c)} ))}
  2. ); })}
); } ); // 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 (
  • {children?.map((c) => ( {c.render(c)} ))}
  • ); } ); // 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 (
    {token.content}
    ); } ); // 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(" ); } // eslint-disable-next-line @next/next/no-img-element return {token.content}; } ); // anchor registerIdentifier( "anchor", /(? { 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 ( {token.content} ); } ); // 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 ( {token.content} ); } ); // 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 {token.content}; } ); // italic registerIdentifier( "italic", /(? { return { content: s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse italic", raw: s, metadata: {}, uuid: crypto.randomUUID(), rendersContentOnly, }; }, (token) => { return {token.content}; } ); // 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 ( ( {c.render(c)} )) || token.content } preferredAlign="centered" preferredEdge="bottom" className="cursor-pointer mx-2" > {metadata.title} ); } ); // accordion 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 (
    {children?.map((e) => ( {e.render(e)} ))}
    ); } ); // paragraph registerIdentifier( "p", // /(?<=\n\n|^)([\s\S]*?)(?=\n\n|$)/g, /(?<=\n\n)([\s\S]*?)(?=\n\n)/g, (s) => { return { content: s.replace("\n", " "), raw: s, metadata: {}, uuid: crypto.randomUUID(), }; }, (token) => { const { children } = token; return (
    {children?.map((e) => { return {e.render(e)}; })}
    ); } ); // horizontal rule registerIdentifier( "hr", /^-{3,}$/gm, (s) => { return { content: s, raw: s, metadata: {}, uuid: crypto.randomUUID(), rendersContentOnly, }; }, () => { return
    ; } ); // comment registerIdentifier( "comment", //g, (s) => { return { content: "", metadata: { comment: s }, raw: "", uuid: crypto.randomUUID(), rendersContentOnly, }; }, () => { return <>; } ); // frontmatter registerIdentifier( "frontmatter", /^---([\s\S]*?)---/g, (s, rx) => { return { content: "", metadata: { frontmatterString: s.match(rx)?.at(0) || "", }, raw: "", uuid: "frontmatter", }; }, (token) => { return <>{token.raw}; } ); // table 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 ( {!!headerRows && ( {headerRows.map((r, i) => ( {r.concat(Array(columns - r.length).fill("")).map((c) => { const child = t.children?.find((child) => child.raw === c); return ( ); })} ))} )} {!!bodyRows && ( {bodyRows.map((r, i) => ( {r.concat(Array(columns - r.length).fill("")).map((c, i) => { const child = t.children?.find((child) => child.raw === c); return ( ); })} ))} )} {!!footerRows && ( {footerRows.map((r, i) => ( {r.concat(Array(columns - r.length).fill("")).map((c) => { const child = t.children?.find((child) => child.raw === c); return ( ); })} ))} )}
    {child?.render(child) || c}
    {child?.render(child) || c}
    {child?.render(child) || c}
    ); } ); // resolver registerIdentifier( "resolver", /\?\?<<(.*?)>>/g, (s) => { const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0]; if (inp == undefined) return { content: "Error parsing resolver: " + s, metadata: {}, raw: "ERROR", uuid: crypto.randomUUID(), }; return { content: inp, metadata: {}, raw: s, uuid: crypto.randomUUID(), }; }, (t) => { if (t.content.startsWith("Error")) return {t.content}; return ; } ); // on-demand resolver registerIdentifier( "on-demand resolver", /\?\?\[.*?\](\(.*?\))<<(.*?)>>/g, (s) => { const inp = s.match(/(?<=<<)(.*?)(?=>>)/)![0]; const title = s.match(/(?<=\?\?\[)(.*?)(?=\])/)![0]; const template = s.match(/(?<=\]\()(.*?)(?=\))/)![0]; if (inp == undefined) return { content: "Error parsing resolver: " + s, metadata: { title: "", template: "", }, raw: "ERROR", uuid: crypto.randomUUID(), }; return { content: inp, metadata: { title, template, }, raw: s, uuid: crypto.randomUUID(), }; }, (t) => { if (t.content.startsWith("Error")) return {t.content}; return ( ); } ); 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 for "); 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-\d]/gi, "") .trim() .replaceAll(" ", "-"); let idNum = 1; while (usedIds.includes(id)) { id = id.replace(/-[0-9]+$/g, ""); id += "-" + idNum; idNum++; } return id; }