- {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
- {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;