From 3c8f5bb8ba585cdcdaecba75e05bb2bf7682809b Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 12 Mar 2024 04:53:54 -0600 Subject: [PATCH] I'm really sick of not making any progress --- .gitignore | 2 + app/help/[article]/page.tsx | 13 +- app/layout.tsx | 10 +- bun.lockb | Bin 160846 -> 161192 bytes components/ttcmd/index.tsx | 256 +++++++++++++++---- lib/portal/components/index.ts | 5 +- lib/tcmd/TokenIdentifiers.ts | 192 ++++++++++++++ lib/tcmd/index.ts | 363 ++++++++++++++++++++------- lib/tcmd/tokenizeBlock.ts | 4 +- lib/tcmd/tokenizeInline.ts | 15 ++ lib/tcmd/tokenizeParagraph.ts | 20 +- md/help articles/How to use ttcMD.md | 88 ++++++- md/home.md | 6 +- package.json | 1 + tsconfig.json | 26 +- types.d.ts | 29 ++- 16 files changed, 863 insertions(+), 167 deletions(-) create mode 100644 lib/tcmd/TokenIdentifiers.ts diff --git a/.gitignore b/.gitignore index e4d6f72..baa6bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ next-env.d.ts # vscode .vscode + +temp.json \ No newline at end of file diff --git a/app/help/[article]/page.tsx b/app/help/[article]/page.tsx index d4d4b12..ef17903 100644 --- a/app/help/[article]/page.tsx +++ b/app/help/[article]/page.tsx @@ -1,6 +1,6 @@ import { TTCMD } from "@/components/ttcmd"; +import { ArrowLeftCircleIcon } from "@heroicons/react/24/solid"; import { readFile } from "fs/promises"; -import Error from "next/error"; import { Suspense } from "react"; export default async function Help({ @@ -8,6 +8,7 @@ export default async function Help({ }: { params: { article: string }; }) { + if (!params.article.endsWith(".md")) return <>; const body = readFile( "./md/help articles/" + decodeURIComponent(params.article), "utf-8", @@ -18,9 +19,13 @@ export default async function Help({

Help

How to use TTCMD

- - - +
+
+ + + +
+
); } diff --git a/app/layout.tsx b/app/layout.tsx index 9de98d6..26408b6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -23,16 +23,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { const navItems = [ - { - to: "/schemas", - icon: CircleStackIcon, - text: "Schemas", - }, { to: "/game-systems", icon: PuzzlePieceIcon, text: "Game Systems", }, + { + to: "/schemas", + icon: CircleStackIcon, + text: "Schemas", + }, { to: "/publications", icon: BookOpenIcon, diff --git a/bun.lockb b/bun.lockb index 8384bf0b920c02938d4a191273738d28326cb584..76d7e5fd52c7d401c41a4e49750fdc320fd0e205 100755 GIT binary patch delta 31504 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 delta 30676 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 }> = ({ body }) => { const text = use(body); - const elements = useMemo(() => createElements(text), [text]); + const [elements, tabSpacing] = useMemo(() => createElements(text), [text]); return ( - //
- //
{JSON.stringify(elements,null,2)}
- //
-
- {elements.map((e, i) => {renderBlock(e)} - )} +
+
+ +
+ {/* {elements.map((e, i) => {render(e)})} */} + {renderer(elements, tabSpacing)}
+ //
+ // {/*
{JSON.stringify(elements,null,2)}
*/} + //
); }; -const renderBlock = (block: BlockChildren): ReactNode => { +const renderer = (tokens: Token[], tabSpacing: number) => { + const usedIds: string[] = []; + return tokens.map((t) => ( + {render(t, usedIds, tabSpacing)} + )); +}; + +const render = (token: Token, usedIds: string[], tabSpacing: number) => { + switch (token.type) { + case "heading": + return ( +
+ {token.content} +
+ ); + case "grid": + return ( +
+ {token.children?.map((c, i) => ( +
+ {render(c, usedIds, tabSpacing)} +
+ ))} +
+ ); + case "code": + return ( +
+          {token.content}
+        
+ ); + case "card": + return ( +
+ {token.children?.map((e) => ( + + {render(e, usedIds, tabSpacing)} + + ))} +
+ ); + case "anchor": + return ( + + {token.content} + + ); + case "image": { + token.metadata.src = token.metadata.src as string; + if (token.metadata.src.startsWith(" +
+ ); + } + // eslint-disable-next-line @next/next/no-img-element + return {token.content}; + } + case "inline-code": + return ( + + {token.content} + + ); + case "popover": + return ( + render(c, usedIds, tabSpacing)) || + token.content} + preferredAlign="centered" + preferredEdge="bottom" + className="cursor-pointer mx-2" + > + + {token.metadata.title} + + + ); + case "text": + return {token.content}; + case "p": + return ( +
+ {token.children?.map((e, i) => ( + + {render(e, usedIds, tabSpacing)} + + ))} +
+ ); + case "accordion": + return ( +
+ + + {token.children?.map((e, i) => ( + + {render(e, usedIds, tabSpacing)} + + ))} + + +
+ ); + default: + return ( +
+ Block or paragraph missing implementation: {token.type} +
+ ); + } +}; + +const renderBlock = ( + block: BlockChildren, + usedIds: string[] = [], +): ReactNode => { + usedIds = usedIds || []; switch (block.type) { case "block": return block.children.map((e, i) => ( - {renderBlock(e)} + {renderBlock(e, usedIds)} )); case "grid": return ( @@ -38,50 +192,52 @@ const renderBlock = (block: BlockChildren): ReactNode => { > {block.children.map((c, i) => (
- {renderBlock(c)} + {renderBlock(c, usedIds)}
))} ); case "card": return ( -
+
{block.children.map((e, i) => ( - {renderBlock(e)} + {renderBlock(e, usedIds)} ))}
); case "accordion": return ( - - - {block.children.map((e, i) => ( - - {renderBlock(e)} - - ))} - - +
+ + + {block.children.map((e, i) => ( + + {renderBlock(e, usedIds)} + + ))} + + +
); default: return ( - renderParagraph(block as ParagraphToken) + renderParagraph(block as ParagraphToken, usedIds) ); } }; -const renderParagraph = (p: ParagraphToken) => { +const renderParagraph = (p: ParagraphToken, usedIds: string[]) => { switch (p.type) { case "p": return (
{p.content.map((e, i) => ( - {renderToken(e)} + {renderToken(e, usedIds)} ))}
@@ -101,44 +257,52 @@ const renderParagraph = (p: ParagraphToken) => { } }; -const renderToken = (t: Token) => { +const generateId = (t: string, usedIds: string[]) => { + let id = t.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll( + " ", + "-", + ); + let idNum = 1; + while (usedIds.includes(id)) { + id = id.replace(/-[0-9]+$/g, ""); + id += "-" + idNum; + idNum++; + } + return id; +}; + +const renderToken = (t: Token, usedIds: string[]) => { switch (t.type) { - case "h1": + case "h1": { return (
{renderInlineToken(t.line)}
); - case "h2": + } + case "h2": { return (
{renderInlineToken(t.line)}
); - case "h3": + } + case "h3": { return (
{renderInlineToken(t.line)}
); + } case "p": return (
@@ -223,6 +387,12 @@ const renderInlineToken = (l: Line) => { ); + case "inline-code": + return ( + + {token.content} + + ); default: return ( diff --git a/lib/portal/components/index.ts b/lib/portal/components/index.ts index 7a2287e..7b297d8 100644 --- a/lib/portal/components/index.ts +++ b/lib/portal/components/index.ts @@ -1,3 +1,5 @@ +"use client"; + import { FC, PropsWithChildren, useEffect, useState } from "react"; import { createPortal } from "react-dom"; @@ -12,9 +14,10 @@ export const Portal: FC> = ( const [container] = useState(() => { // This will be executed only on the initial render // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state - return document.getElementById("root-portal")!; + return document.getElementById("root-portal") || document.createElement(el); }); + // todo: this smells. appending the same element? useEffect(() => { container.classList.add(className); document.body.appendChild(container); diff --git a/lib/tcmd/TokenIdentifiers.ts b/lib/tcmd/TokenIdentifiers.ts new file mode 100644 index 0000000..51a2740 --- /dev/null +++ b/lib/tcmd/TokenIdentifiers.ts @@ -0,0 +1,192 @@ +export const TokenIdentifiers = new Map Token; +}>(); + +// 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("card", { + rx: /\[{2}\n+(\n|.*?)\n+\]{2}/g, + parse(s) { + return { + content: s.match(new RegExp(this.rx, ""))?.at(1) || + "Unable to parse card", + raw: s, + metadata: {}, + type: "card", + uuid: crypto.randomUUID(), + }; + }, +}); +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("grid", { + rx: /(?:\[\])+\n+((?:.|\n)*?)\n+\/\[\]/g, + parse(s) { + return { + content: s.match(new RegExp(this.rx, ""))?.at(1) || + "Unable to parse grid", + raw: s, + metadata: { + columns: s.split("\n").at(0)?.match(/\[\]/g)?.length.toString() || "1", + }, + type: "grid", + uuid: crypto.randomUUID(), + rendersChildrenOnly, + }; + }, +}); +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) { + 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, + parse(s) { + // const [_, content] = s.match(new RegExp(this.rx, ""))!; + + return { + // content, + content: s, + raw: s, + metadata: {}, + type: "p", + uuid: crypto.randomUUID(), + }; + }, +}); + +// const p = TokenIdentifiers.get("p"); +// TokenIdentifiers.clear(); +// p && TokenIdentifiers.set("p", p); diff --git a/lib/tcmd/index.ts b/lib/tcmd/index.ts index d15b600..7da536f 100644 --- a/lib/tcmd/index.ts +++ b/lib/tcmd/index.ts @@ -1,103 +1,286 @@ +"use client"; + import { zipArrays } from "../zip"; -import { tokenizeLine } from "./tokenizeLine"; -import { tokenizeBlock } from "./tokenizeBlock"; -import { tokenizeParagraph } from "./tokenizeParagraph"; +import { TokenIdentifiers } from "./TokenIdentifiers"; -export const createElements = (body: string) => { +export const createElements = (body: string): [Token[], number] => { + const tabOptions = [ + /^\s{2}(?!\s|\t)/m, + /^\s{4}(?!\s|\t)/m, + /^\t(?!\s|\t)]/m, + ]; + let tabSpacing = 0; + + for (const [i, tabOption] of tabOptions.entries()) { + if (body.match(tabOption)) { + tabSpacing = i; + break; + } + } const tokens = tokenize(body); - - return tokens; + return [buildAbstractSyntaxTree(tokens, body), tabSpacing]; }; const tokenize = (body: string) => { - body = body.replace(/\n?\n?/gs, ""); + const tokenizedBody: tokenMarker[] = []; - const paragraphs = body.split("\n\n"); + const addToken = (thing: tokenMarker) => { + tokenizedBody.push(thing); + }; - const blockTokens: BlockToken[] = []; - const paragraphTokens: ParagraphToken[] = []; + 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; - for (const paragraph of paragraphs) { - const block = tokenizeBlock(paragraph); - let openBT = blockTokens.findLast((bt) => !bt.closed); - if (block) { - if (typeof block === "string") { - if (openBT) { - openBT.closed = true; - } - continue; - } - - if (openBT) { - openBT.children.push(block); - block.parent = openBT.type; - } - blockTokens.push(block); - continue; - } - - if (!openBT) { - openBT = { - children: [], - closed: false, - metadata: {}, - type: "block", - uuid: crypto.randomUUID(), - }; - blockTokens.push(openBT); - } - - const multiline = tokenizeParagraph(paragraph); - let openP = paragraphTokens.findLast((p) => !p.closed); - if (multiline) { - if (Array.isArray(multiline)) { - if (openP) { - openP.closed = true; - openP.content = openP.content.concat(multiline); - } - continue; - } - - openBT.children.push(multiline); - paragraphTokens.push(multiline); - continue; - } else if (openP && !openP?.allowsInline) { - openP.content.push({ - line: paragraph, - raw: paragraph, - type: "text", - uuid: crypto.randomUUID(), - }); - } - - // I don't think the closed check is necessary, but just in case - // if (openP && !openP.closed && !openP.allowsInline) continue; - if (!openP) { - openP = { - allowsInline: true, - closed: true, - content: [], - metadata: {}, - type: "p", - uuid: crypto.randomUUID(), - }; - openBT.children.push(openP); - paragraphTokens.push(openP); - } - - const lines = paragraph.split("\n"); - let previous; - for (const line of lines) { - const singleLine = tokenizeLine(line, previous); - - if (singleLine) { - if (singleLine !== previous) { - openP.content.push(singleLine); - } - previous = singleLine; + if (type !== "p" || !tokenizedBody.find((i) => i.start === start)) { + addToken({ + start, + end, + type, + }); } } } - - return blockTokens.filter((b) => !b.parent); + return tokenizedBody; }; + +export const buildAbstractSyntaxTree = ( + markers: tokenMarker[], + body: string, +): Token[] => { + ensureNoOrphans(markers); + + markers.sort((a, b) => { + if (a.start === b.start) { + console.log(a, b); + if (a.type === "p") return -1; + if (b.type === "p") return 1; + } + // if (a.type === "p" && a.start === b.start) return -1; + // if (b.type === "p" && a.start === b.start) return 1; + return a.start - b.start; + }); + + for (const marker of markers) { + marker.token = TokenIdentifiers.get(marker.type)?.parse( + body.substring(marker.start, marker.end), + ); + // if (marker.type === "p" && marker.parent && marker.parent?.type !== "p") { + // marker.parent = undefined; + // continue; + // } + if (!marker.token) { + throw new Error("Failed to parse token. Token type not found?"); + } + if (!marker.parent) continue; + + if (!marker.parent.token) { + // debugger; + throw new Error("Failed to parse token. Child tokenized before parent"); + } + + marker.parent.token.children = marker.parent.token.children || []; + marker.parent.token.children.push(marker.token); + // marker.token.parent = marker.parent.token; + } + + const tokens = markers.filter((m) => + markers.filter((a) => a !== m && (a.end === m.end || a.start === m.start)) + .length || m.type !== "p" + ).map((t) => t.token!); + + for (const token of tokens) { + contentToChildren(token); + } + + return tokens.filter((t) => !t.parent); +}; + +const ensureNoOrphansOld = (tokens: tokenMarker[]) => { + for (const token of tokens) { + const parentPs = tokens.filter((t) => ( + t.type === "p" && ( + // any p that fully encapsulates the token + (t.start <= token.start && t.end >= token.end) || + // any p that contains the start of the token + (t.start <= token.start && t.end >= token.start) || + // any p that contains the end of the token + (t.start <= token.end && t.end >= token.end) + ) + )).sort((a, b) => (a.start - b.start)); + + if (parentPs.length > 1) { + parentPs[0].end = parentPs.at(-1)!.end; + const remainingParents = parentPs.slice(1); + for (const token of tokens) { + if (token.parent && remainingParents.includes(token.parent)) { + token.parent = parentPs[0]; + } + } + if (parentPs[0] && parentPs[0].end < token.end) { + parentPs[0].end = token.end; + } + tokens = tokens.filter((t) => !remainingParents.includes(t)); + } + + const potentialParents = tokens.filter((t) => + (t.start < token.start && t.end > token.end) || + (t.type === "p" && t.start <= token.start && + t.end >= token.end && t !== token) + ).sort((a, b) => { + if (token.start - a.start < token.start - b.start) return -1; + return 1; + }); + + token.parent = potentialParents.find((p) => p.type !== "p") ?? + potentialParents[0]; + + if (token.type === "grid") { + debugger; + } + } +}; + +const ensureNoOrphans = (tokens: tokenMarker[]) => { + ensureNoOrphansOld(tokens); +}; + +const contentToChildren = (token: Token) => { + const children: Token[] = []; + let part, content = token.content; + + // for (const child of token.children || []) { + // if (!content) continue; + // [part, content] = content.split(child.raw); + // part && children.push({ + // content: part.trim(), + // metadata: {}, + // raw: part, + // type: "text", + // uuid: crypto.randomUUID(), + // }); + // children.push(child); + // } + + // if (content) { + // children.push({ + // content: content.trim(), + // metadata: {}, + // raw: content, + // type: "text", + // uuid: crypto.randomUUID(), + // }); + // } + 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.trim(), + metadata: {}, + raw: c, + type: "text", + uuid: crypto.randomUUID(), + rendersContentOnly: true, + })), + token.children || [], + ).filter((c) => c.children?.length || (c.rendersContentOnly && c.content)); +}; + +// const tokenize = (body: string) => { +// body = body.replace(/\n?\n?/gs, ""); + +// const paragraphs = body.split("\n\n"); + +// const blockTokens: BlockToken[] = []; +// const paragraphTokens: ParagraphToken[] = []; + +// for (const paragraph of paragraphs) { +// const block = tokenizeBlock(paragraph); +// let openBT = blockTokens.findLast((bt) => !bt.closed); +// if (block) { +// if (typeof block === "string") { +// if (openBT) { +// openBT.closed = true; +// } +// continue; +// } + +// if (openBT) { +// openBT.children.push(block); +// block.parent = openBT.type; +// } +// blockTokens.push(block); +// continue; +// } + +// if (!openBT) { +// openBT = { +// children: [], +// closed: false, +// metadata: {}, +// type: "block", +// uuid: crypto.randomUUID(), +// }; +// blockTokens.push(openBT); +// } + +// const multiline = tokenizeParagraph(paragraph); +// let openP = paragraphTokens.findLast((p) => !p.closed); +// if (multiline) { +// if (Array.isArray(multiline)) { +// if (openP) { +// openP.closed = true; +// openP.content = openP.content.concat(multiline); +// } +// continue; +// } + +// openBT.children.push(multiline); +// paragraphTokens.push(multiline); +// continue; +// } else if (openP && !openP?.allowsInline) { +// openP.content.push({ +// line: paragraph, +// raw: paragraph, +// type: "text", +// uuid: crypto.randomUUID(), +// }); +// } + +// // I don't think the closed check is necessary, but just in case +// // if (openP && !openP.closed && !openP.allowsInline) continue; +// if (!openP) { +// openP = { +// allowsInline: true, +// closed: true, +// content: [], +// metadata: {}, +// type: "p", +// uuid: crypto.randomUUID(), +// }; +// openBT.children.push(openP); +// paragraphTokens.push(openP); +// } + +// const lines = paragraph.split("\n"); +// let previous; +// for (const line of lines) { +// const singleLine = tokenizeLine(line, previous); + +// if (singleLine) { +// if (singleLine !== previous) { +// openP.content.push(singleLine); +// } +// previous = singleLine; +// } +// } +// } + +// return blockTokens.filter((b) => !b.parent); +// }; diff --git a/lib/tcmd/tokenizeBlock.ts b/lib/tcmd/tokenizeBlock.ts index 6975abd..6495818 100644 --- a/lib/tcmd/tokenizeBlock.ts +++ b/lib/tcmd/tokenizeBlock.ts @@ -44,8 +44,8 @@ const blockTokens: { }, }, { - rx: /\[accordion\s?([a-z\s]*)\]/, - closeRx: /\[\/accordion\]/, + rx: /^\[accordion\s?([a-z\s]*)\]/i, + closeRx: /^\[\/accordion\]/, create(line) { const title = line.match(this.rx)?.at(1); return { diff --git a/lib/tcmd/tokenizeInline.ts b/lib/tcmd/tokenizeInline.ts index ac53c42..0ee2f38 100644 --- a/lib/tcmd/tokenizeInline.ts +++ b/lib/tcmd/tokenizeInline.ts @@ -61,6 +61,21 @@ export const inlineTokens: { ) => void; replace: (line: string) => string; }[] = [ + { + rx: /\s?`(.*?)`[^a-z0-9`]\s?/gi, + create(content, start, end, tokens) { + tokens.push({ + content: this.replace(content[0]), + type: "inline-code", + end, + start, + uuid: crypto.randomUUID(), + }); + }, + replace(l) { + return l.replace(this.rx, (...all) => all[1]); + }, + }, { rx: /(\*\*)(.*?)(\*\*)/g, create(content, start, end, tokens) { diff --git a/lib/tcmd/tokenizeParagraph.ts b/lib/tcmd/tokenizeParagraph.ts index 01c1537..d0c1b3d 100644 --- a/lib/tcmd/tokenizeParagraph.ts +++ b/lib/tcmd/tokenizeParagraph.ts @@ -1,37 +1,37 @@ export const tokenizeParagraph = (paragraph: string) => { - for (const block of blockTokens) { - const openTest = block.rx.test(paragraph), - closeTest = block.closeRx.test(paragraph); + for (const pgraph of paragraphTokens) { + const openTest = pgraph.rx.test(paragraph), + closeTest = pgraph.closeRx.test(paragraph); if (openTest && closeTest) { - const p = block.create(paragraph); + const p = pgraph.create(paragraph); p.closed = true; return p; } - if (closeTest) return block.create(paragraph).content; + if (closeTest) return pgraph.create(paragraph).content; if (openTest) { - return block.create(paragraph); + return pgraph.create(paragraph); } } }; -const blockTokens: { +const paragraphTokens: { rx: RegExp; closeRx: RegExp; create: (line: string) => ParagraphToken; }[] = [ { - rx: /^```/g, + rx: /\n```/g, closeRx: /\n```/g, create(line) { return { type: "code", metadata: { - language: line.split("\n").at(0)!.replace(this.rx, ""), + // language: line.split("\n").at(0)!.replace(this.rx, ""), }, closed: false, content: [{ - line: line.replace(/```.*?\n/g, "").replace(/\n```/, ""), + line: line.match(/```(.*?)\n```/g)?.at(1) || line, type: "text", raw: line, uuid: crypto.randomUUID(), diff --git a/md/help articles/How to use ttcMD.md b/md/help articles/How to use ttcMD.md index 9385312..d833dba 100644 --- a/md/help articles/How to use ttcMD.md +++ b/md/help articles/How to use ttcMD.md @@ -1 +1,87 @@ -# Henlo, am help \ No newline at end of file +# Table of Contents +- [Table of Contents](#table-of-contents) +- [How do you use ttcMD?](#how-do-you-use-ttcmd) + - [Enhanced Standard Elements](#enhanced-standard-elements) + - [Links](#links) + - [Custom Elements](#custom-elements) + - [Pop-outs](#pop-outs) + - [Block-level Elements](#block-level-elements) + - [Accordions](#accordions) + +# How do you use ttcMD? + +ttcMD is a flavor of markdown that has been specifically designed to use with [ttcQuery](/help/ttcQuery.md). It has all of the basic syntax of [markdown](https://www.markdownguide.org/cheat-sheet/), but also includes Tables, basic Fenced Code Blocks and a slew of custom elements and styling annotations. + +## Enhanced Standard Elements + +This section will cover all of the enhancements that are added for basic markdown elements + +### Links + +You can use the typical link syntax: `[link name](/link/location)`, but there are a few presets that allow you to style them to look a bit nicer. + +**Primary Button:** +Prefix the link name with ````button` to create a button. +`[```button link name](#links)` produces: + +[```button link name](#links) + +**Call to Action:** +Prefix the link name with ````cta` to create a modestly styled button/call to action. +`[```cta link name](#links)` produces: + +[```cta link name](#links) + +## Custom Elements + +This section will cover the specific elements custom built for Tabletop Commander. + +### Pop-outs + +Pop-outs, or popovers, are the little cards that "pop out" when you hover over an item. + +The syntax is thus: `^[pop-out title]<>`. The pop-out title will be rendered inline, just like a link, and the content will be included in the pop-out itself. Content can also include inline markdown elements as well, so you can format the content within as well. + +Example: + +This syntax `^[goofy!]<>` will produce this element: ^[goofy!]<> + +Note: currently, only inline elements are available, so formatting is limited + +## Block-level Elements + +Block-level elements have a slightly different syntax than the single-line and inline elements we've seen so far. In order to use block-level elements, they *must* be formatted correctly, including the empty lines. As a general rule, you cannot nest block-level elements within themselves, but you can nest different block-level elements within it. + +### Accordions + +Accordions are when you can click an item to expand it to show additional information. + +Syntax: + +[][][] + +``` +[accordion title] + +whatever markdown you desire, including non-accordion block elements + +[/accordion] +``` + +[accordion this is what an accordion looks like] + +This is the body of the accordion. + +As you can see, I can do normal markdown in here. + +I can include a [link](#accordions), or *italic* and **bold** text. + +[[ + +I can even include a card, like this one + +]] + +[/accordion] + +/[] \ No newline at end of file diff --git a/md/home.md b/md/home.md index 73bd1c1..413a4e9 100644 --- a/md/home.md +++ b/md/home.md @@ -12,7 +12,7 @@ See, Emma had a vision that anyone could contribute to making rules corrections [[ ### Game Systems - + The basis of TC is called a Game System Package. This package includes everything needed for a game system, including schemas, publications, and tools. Players can follow a Game System to get @@ -74,13 +74,13 @@ parts of the publication through context based pop-overs. **For the techies (again):** -Publications use an enhanced markdown syntax (tcMD) that +Publications use an enhanced markdown syntax (ttcMD) that implements tcQuery, and adds a bit of custom syntax for things like pop-overs and styling hints for rendering. The styling aspect is similar to a very trimmed down CSS, but can accomplish quite a lot. For example, this page is actually -built using tcMD! +built using ttcMD! [```cta Learn More](/help/Publications.md) diff --git a/package.json b/package.json index 439a382..e9800fd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@heroicons/react": "^2.1.1", "isomorphic-dompurify": "^2.4.0", + "marked": "^12.0.1", "next": "14.1.0", "react": "^18", "react-dom": "^18" diff --git a/tsconfig.json b/tsconfig.json index e7ff90f..2112683 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,9 +22,19 @@ } ], "paths": { - "@/*": ["./*"] - } + "@/*": [ + "./*" + ] + }, + "target": "es2022" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 23cab37..918f626 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,5 +1,12 @@ type InlineToken = { - type: "text" | "bold" | "anchor" | "image" | "popover" | "italic"; + type: + | "text" + | "bold" + | "anchor" + | "image" + | "popover" + | "italic" + | "inline-code"; content: string; data?: any; uuid: string; @@ -32,7 +39,25 @@ type SingleLineToken = { cfg?: SingleLineCfg; uuid: string; }; -type Token = SingleLineToken | MultilineToken; +type Token = { + type: string; + metadata: Record; + parent?: Token; + children?: Token[]; + uuid: string; + raw: string; + content: string; + rendersChildrenOnly?: boolean; + rendersContentOnly?: boolean; +}; + +type tokenMarker = { + start: number; + end: number; + type: string; + parent?: tokenMarker; + token?: Token; +}; type MultilineCfg = { rx: RegExp;