ttcmd: adds hr element

help pages: adds a way to extract the table of contents to the parent,
smoothes out the rough loading on help pages
This commit is contained in:
Emmaline Autumn 2024-03-13 02:52:59 -06:00
parent 009e988a38
commit d0cb74bea8
12 changed files with 199 additions and 70 deletions

5
actions/readMD.ts Normal file
View File

@ -0,0 +1,5 @@
"use server";
import { readFile } from "fs/promises";
export const readMD = async (path: string) => await readFile(path, "utf-8");

View File

@ -1,5 +0,0 @@
"use server";
import { readFile } from "fs/promises";
export const readMD = async () => await readFile("./test.md", "utf-8");

View File

@ -56,6 +56,13 @@
.poppable {
@apply card dark:bg-mixed-300 bg-primary-600 p-2 rounded-lg transition-opacity data-[visible=true]:z-10 data-[visible=true]:opacity-100 data-[visible=false]:opacity-0 -z-10 max-w-[400px] absolute;
}
.skeleton {
@apply animate-pulse bg-black/20 rounded-md text-white/0;
}
}
@keyframes identifier {
}
@layer utilities {

View File

@ -0,0 +1,38 @@
"use client";
import { TTCMD, TTCMDRenderer } from "@/components/ttcmd";
import { FC, use, useCallback, useState } from "react";
export const HelpClient: FC<{ body: Promise<string>; title: string }> = ({
body: bodyPromise,
title,
}) => {
const body = use(bodyPromise);
const [toc, setToc] = useState<Token[]>();
const escapeTOC = useCallback((t: Token[]) => {
setToc(t);
return true;
}, []);
return (
<>
<section className="heading">
<h2 className="strapline">Help</h2>
<h1>{decodeURIComponent(title)}</h1>
</section>
<section className="grid grid-cols-3 gap-x-8 gap-y-6 my-6">
<div className="col-span-2">
<TTCMD
body={body}
escapeTOC={escapeTOC}
/>
</div>
{toc && (
<div className="sticky top-8 h-min">
<TTCMDRenderer tokens={toc} />
</div>
)}
</section>
</>
);
};

View File

@ -0,0 +1,5 @@
import { Loader } from "@/components/loader";
export default function Loading() {
return <Loader />;
}

View File

@ -1,31 +1,25 @@
import { TTCMD } from "@/components/ttcmd";
import { ArrowLeftCircleIcon } from "@heroicons/react/24/solid";
import { readFile } from "fs/promises";
import { readMD } from "@/actions/readMD";
import { HelpClient } from "./client";
import { Suspense } from "react";
import { Loader } from "@/components/loader";
// export const
export default async function Help({
params,
}: {
params: { article: string };
}) {
if (!params.article.endsWith(".md")) return <></>;
const body = readFile(
if (!params.article.endsWith(".md")) return;
const body = readMD(
"./md/help articles/" + decodeURIComponent(params.article),
"utf-8",
);
return (
<>
<section className="heading">
<h2 className="strapline">Help</h2>
<h1>{decodeURIComponent(params.article).replace(".md", "")}</h1>
</section>
<section className="grid grid-cols-3 gap-x-8 gap-y-6 my-6">
<div className="col-span-2">
<Suspense>
<TTCMD body={body} />
<Suspense fallback={<Loader />}>
<HelpClient
body={body}
title={decodeURIComponent(params.article).replace(".md", "")}
/>
</Suspense>
</div>
</section>
</>
);
}

View File

@ -53,7 +53,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className + " flex min-h-[100vh]"}>
<nav className="h-[100vh] fixed top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
<nav className="h-[100vh] sticky top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
<Link href="/">Tabletop Commander</Link>
</h1>
@ -71,7 +71,7 @@ export default function RootLayout({
))}
</ul>
</nav>
<main className="p-8 w-full ml-64">
<main className="p-8 w-full overflow-visible">
{children}
</main>
<div id="root-portal"></div>

30
components/loader.tsx Normal file
View File

@ -0,0 +1,30 @@
import { FC } from "react";
export const Loader: FC = () => {
const tragedy = [
"Did you ever hear the tragedy of Darth Plagueis the Wise?",
"No.",
"I thought not. It's not a story the Jedi would tell you. It's a Sith legend. Darth Plagueis... was a Dark Lord of the Sith so powerful and so wise, he could use the Force to influence the midi-chlorians... to create... life. He had such a knowledge of the dark side, he could even keep the ones he cared about... from dying.",
"He could actually... save people from death?",
"The dark side of the Force is a pathway to many abilities... some consider to be unnatural.",
"Wh What happened to him?",
"He became so powerful, the only thing he was afraid of was... losing his power. Which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew. Then his apprentice killed him in his sleep. It's ironic. He could save others from death, but not himself.",
"Is it possible to learn this power?",
"Not from a Jedi.",
];
return (
<>
<section className="heading">
<h2 className="strapline skeleton">Do It</h2>
<h1 className="skeleton">Kill him. Kill him now.</h1>
</section>
<section>
{tragedy.map((t) => <p key={t} className=".skeleton">{t}</p>)}
</section>
</>
// <div className="mx-auto text-9xl flex justify-center py-24">
// Wrangling Cats...
// </div>
);
};

View File

@ -4,36 +4,89 @@ import { Accordion, AccordionContent } from "@/lib/accordion";
import { Poppable } from "@/lib/poppables/components/poppable";
import { createElements } from "@/lib/tcmd";
import Link from "next/link";
import React, { FC, Fragment, ReactNode, use, useMemo } from "react";
import React, {
FC,
Fragment,
Suspense,
use,
useEffect,
useMemo,
useState,
} from "react";
import { sanitize } from "isomorphic-dompurify";
import StaticGenerationSearchParamsBailoutProvider from "next/dist/client/components/static-generation-searchparams-bailout-provider";
import { Loader } from "../loader";
export const TTCMD: FC<{ body: Promise<string> }> = ({ body }) => {
"use client";
const text = use(body);
const [elements, tabSpacing] = useMemo(() => createElements(text), [text]);
const tada = useMemo(
() => <>{renderer(elements.map((e) => e.token))}</>,
[elements],
export const TTCMD: FC<
{ body: string; escapeTOC?: (tokens: Token[]) => boolean }
> = ({ body, escapeTOC = () => false }) => {
const elements = useMemo(() => createElements(body), [body]);
const [toc, start, end] = useMemo(() => {
const tocHead = elements.findIndex((t) =>
t.content.includes("Table of Contents")
);
if (tocHead > -1) {
const hr = elements.slice(tocHead).findIndex((t) => t.type === "hr");
if (hr > -1) {
const end = hr + 1;
return [elements.slice(tocHead, end), tocHead, end - tocHead];
}
}
return [[], 0, 0];
}, [elements]);
// const hasEscapedTOC = useMemo(() => toc && escapeTOC(toc), [escapeTOC, toc])
const [hasEscapedTOC, setHasEscapedTOC] = useState<boolean>();
useEffect(() => {
setHasEscapedTOC(escapeTOC(toc));
}, [escapeTOC, toc]);
return (
<div className="text-lg col-span-2">
<div>
<button
<Suspense fallback={<Loader />}>
{
/* <button
className="btn-primary"
onClick={() =>
navigator.clipboard.writeText(JSON.stringify(elements, null, 2))}
>
copy ast
</button>
</div>
{/* {elements.map((e, i) => <Fragment key={e.uuid}>{render(e)}</Fragment>)} */}
</button> */
}
{hasEscapedTOC !== undefined &&
(
<TTCMDRenderer
tokens={hasEscapedTOC
? [...elements].toSpliced(start, end)
: elements}
/>
)}
</Suspense>
);
};
export const TTCMDRenderer: FC<{ tokens: Token[] }> = ({ tokens }) => {
const tada = useMemo(
() => (
<>
{renderer(tokens)}
</>
),
[tokens],
);
if (!tokens.length) {
const audio = new Audio(
"https://assets.mixkit.co/active_storage/sfx/221/221-preview.mp3",
);
audio.onload = () => {
audio.play();
};
}
return (
<div className="text-lg">
{tada}
</div>
// <div className="grid grid-cols-3">
// {/* <pre suppressHydrationWarning>{JSON.stringify(elements,null,2)}</pre> */}
// </div>
);
};
@ -214,6 +267,8 @@ function render(token: Token, usedIds: string[]) {
{token.children?.map((c) => render(c, usedIds))}
</li>
);
case "hr":
return <div className="w-full border-b border-mixed-500 my-3"></div>;
default:
return (
<div className="p bg-red-600 text-white">

View File

@ -63,8 +63,6 @@ TokenIdentifiers.set("card", {
return search(s, start, end, rx, crx);
},
parse(s) {
console.log(s);
const rx = /\[{2}\n+([\s\S]*)\n+\]{2}/;
const [_, content] = s.match(rx) || ["", "Unable to parse card"];
@ -279,6 +277,20 @@ TokenIdentifiers.set("p", {
},
});
TokenIdentifiers.set("hr", {
rx: /^-{3,}$/gm,
parse(s) {
return {
content: s,
raw: s,
metadata: {},
type: "hr",
uuid: crypto.randomUUID(),
rendersContentOnly,
};
},
});
// const p = TokenIdentifiers.get("p");
// TokenIdentifiers.clear();
// p && TokenIdentifiers.set("p", p);

View File

@ -3,22 +3,9 @@
import { zipArrays } from "../zip";
import { TokenIdentifiers } from "./TokenIdentifiers";
export const createElements = (body: string): [TokenMarker[], 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;
}
}
export const createElements = (body: string): Token[] => {
const tokens = tokenize(body);
return [buildAbstractSyntaxTree(tokens), tabSpacing];
return buildAbstractSyntaxTree(tokens).map((t) => t.token);
};
const tokenize = (body: string) => {
@ -159,7 +146,6 @@ const contentToChildren = (token: Token) => {
const splitMarker = "{{^^}}";
for (const child of token.children || []) {
if (token.type === "card" && child.type === "code") debugger;
content = content.replace(child.raw, splitMarker);
}

View File

@ -1,6 +1,6 @@
# Table of Contents
- [Table of Contents](#table-of-contents)
- [How do you use ttcMD?](#how-do-you-use-ttcmd)
- [What even is ttcMD?](#what-even-is-ttcmd)
- [Enhanced Standard Elements](#enhanced-standard-elements)
- [Links](#links)
- [Custom Elements](#custom-elements)
@ -10,7 +10,9 @@
- [Card](#card)
- [Grid](#grid)
# How do you use ttcMD?
---
# What even is 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.