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:
parent
009e988a38
commit
d0cb74bea8
5
actions/readMD.ts
Normal file
5
actions/readMD.ts
Normal file
@ -0,0 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
export const readMD = async (path: string) => await readFile(path, "utf-8");
|
@ -1,5 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
export const readMD = async () => await readFile("./test.md", "utf-8");
|
@ -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 {
|
||||
|
38
app/help/[article]/client.tsx
Normal file
38
app/help/[article]/client.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
5
app/help/[article]/loading.tsx
Normal file
5
app/help/[article]/loading.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Loader } from "@/components/loader";
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader />;
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
30
components/loader.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user