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 8384bf0..76d7e5f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/ttcmd/index.tsx b/components/ttcmd/index.tsx index a6848f1..8075743 100644 --- a/components/ttcmd/index.tsx +++ b/components/ttcmd/index.tsx @@ -2,31 +2,185 @@ import { Accordion, AccordionContent } from "@/lib/accordion"; import { Poppable } from "@/lib/poppables/components/poppable"; -import { createElements } from "@/lib/tcmd"; +import { buildAbstractSyntaxTree, createElements } from "@/lib/tcmd"; import Link from "next/link"; import React, { FC, Fragment, ReactNode, use, useMemo } from "react"; import { sanitize } from "isomorphic-dompurify"; +import StaticGenerationSearchParamsBailoutProvider from "next/dist/client/components/static-generation-searchparams-bailout-provider"; export const TTCMD: FC<{ body: Promise }> = ({ 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;