Compare commits
No commits in common. "df20a472533bd9bd518f232315f6125472217d68" and "1b0d39f7a3f1a1515ad0e4b70ff07ab21bd4438f" have entirely different histories.
df20a47253
...
1b0d39f7a3
@ -1,23 +1,3 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"args": "all",
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "all",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
@ -9,8 +9,6 @@ export const HomeClient: FC<{ body: Promise<string> }> = ({ body }) => {
|
||||
return (
|
||||
<TTCMD
|
||||
body={text}
|
||||
parserId="home"
|
||||
title="home"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.heading {
|
||||
@apply pb-6 mb-6 border-b border-b-primary-500 dark:border-b-dark-500 min-w-full;
|
||||
@apply pb-6 border-b border-b-primary-500 dark:border-b-dark-500 min-w-full;
|
||||
}
|
||||
.heading h1 {
|
||||
@apply text-5xl font-bold;
|
||||
@ -50,7 +50,7 @@
|
||||
@apply dark:text-primary-500 text-primary-100 py-4 px-6 font-bold text-lg;
|
||||
}
|
||||
.p {
|
||||
@apply pb-1;
|
||||
@apply py-1;
|
||||
}
|
||||
|
||||
.poppable {
|
||||
@ -73,14 +73,6 @@
|
||||
.accordion:not(:has(+ .accordion)) {
|
||||
@apply rounded-b-md;
|
||||
}
|
||||
|
||||
.md-table {
|
||||
@apply bg-black/20 rounded-md overflow-clip;
|
||||
}
|
||||
.md-table td,
|
||||
.md-table th {
|
||||
@apply px-4 border border-black/20 dark:border-white/20;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes identifier {
|
||||
|
@ -52,8 +52,6 @@ export const HelpClient: FC<{ body: Promise<string>; title: string }> = ({
|
||||
<TTCMD
|
||||
body={cleanBody}
|
||||
escapeTOC={escapeTOC}
|
||||
parserId={title}
|
||||
title={title}
|
||||
/>
|
||||
</div>
|
||||
{toc && (
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
QuestionMarkCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import { DevToolboxContextProvider } from "@/components/devtools/context";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@ -51,8 +50,6 @@ export default function RootLayout({
|
||||
},
|
||||
];
|
||||
|
||||
console.log(process.env.NODE_ENV);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className + " flex min-h-[100vh]"}>
|
||||
@ -74,13 +71,9 @@ export default function RootLayout({
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<DevToolboxContextProvider
|
||||
isDev={process.env.NODE_ENV !== "production"}
|
||||
>
|
||||
<main className="p-8 w-full overflow-visible">
|
||||
{children}
|
||||
</main>
|
||||
</DevToolboxContextProvider>
|
||||
<div id="root-portal"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { readMD } from "@/actions/readMD";
|
||||
import { TTCMD } from "@/components/ttcmd";
|
||||
import { readFile } from "fs/promises";
|
||||
import { Suspense } from "react";
|
||||
import { HomeClient } from "./client";
|
||||
import { MDSkeletonLoader } from "@/components/loader";
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Portal } from "@/lib/portal/components";
|
||||
import { FC, PropsWithChildren, use, useEffect, useState } from "react";
|
||||
import { DevToolboxContext } from "./context";
|
||||
import { WrenchScrewdriverIcon } from "@heroicons/react/24/solid";
|
||||
import { XMarkIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
export const DevToolbox: FC = () => {
|
||||
const { tools, shouldShowDevTools } = use(DevToolboxContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
return shouldShowDevTools
|
||||
? (
|
||||
<Portal>
|
||||
<div className="dev-portal flex flex-col gap-2 fixed bottom-2 right-2 border rounded-lg bg-black/50">
|
||||
{open
|
||||
? (
|
||||
<div className="relative p-2">
|
||||
<button
|
||||
className="p-1 absolute top-2 right-2"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<XMarkIcon className="w-3 h-3">
|
||||
</XMarkIcon>
|
||||
</button>
|
||||
<p className="mb-4 mr-8">Dev Toolbox</p>
|
||||
{Object.values(tools)}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<button className="p-4" onClick={() => setOpen(!open)}>
|
||||
<WrenchScrewdriverIcon className="w-4 h-4">
|
||||
</WrenchScrewdriverIcon>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
: <></>;
|
||||
};
|
||||
|
||||
export const DevTool: FC<PropsWithChildren<{ id: string }>> = (
|
||||
{ children, id },
|
||||
) => {
|
||||
const { addTool, removeTool } = use(DevToolboxContext);
|
||||
useEffect(() => {
|
||||
addTool(id, children);
|
||||
(() => removeTool(id));
|
||||
}, [addTool, children, id, removeTool]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { DevToolbox } from "./Toolbox";
|
||||
|
||||
interface ContextProps {
|
||||
tools: Record<string, ReactNode>;
|
||||
addTool: (key: string, r: ReactNode) => void;
|
||||
removeTool: (key: string) => void;
|
||||
shouldShowDevTools: boolean;
|
||||
setShouldShowDevTools: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const DevToolboxContext = createContext<ContextProps>({
|
||||
tools: {},
|
||||
addTool: () => {},
|
||||
removeTool: () => {},
|
||||
shouldShowDevTools: false,
|
||||
setShouldShowDevTools: () => {},
|
||||
});
|
||||
|
||||
export const DevToolboxContextProvider: FC<
|
||||
PropsWithChildren<{ isDev: boolean }>
|
||||
> = (
|
||||
{ children, isDev },
|
||||
) => {
|
||||
console.log(isDev);
|
||||
const [tools, setTools] = useState<Record<string, ReactNode>>({});
|
||||
const [shouldShowDevTools, setShouldShowDevTools] = useState(isDev);
|
||||
|
||||
const addTool = useCallback((key: string, r: ReactNode) => {
|
||||
setTools((t) => ({ ...t, [key]: r }));
|
||||
}, []);
|
||||
const removeTool = useCallback((key: string) => {
|
||||
setTools((t) => ({ ...t, [key]: undefined }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("dev")) setShouldShowDevTools(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DevToolboxContext.Provider
|
||||
value={{
|
||||
tools,
|
||||
addTool,
|
||||
removeTool,
|
||||
shouldShowDevTools,
|
||||
setShouldShowDevTools,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<DevToolbox></DevToolbox>
|
||||
</DevToolboxContext.Provider>
|
||||
);
|
||||
};
|
@ -72,7 +72,7 @@ export const MDSkeletonLoader: FC = () => {
|
||||
i,
|
||||
) => (
|
||||
<li
|
||||
key={t + i}
|
||||
key={t}
|
||||
className={"my-2 leading-8 text-black/20 " + indentation[
|
||||
i === 0 ? 0 : Math.floor(Math.random() * indentation.length)
|
||||
]}
|
||||
|
@ -1,19 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { createElements } from "@/lib/tcmd";
|
||||
import React, { FC, Suspense, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { MDSkeletonLoader } from "../loader";
|
||||
import { DevTool } from "../devtools/Toolbox";
|
||||
|
||||
interface Props {
|
||||
body: string;
|
||||
escapeTOC?: (tokens: Token[]) => boolean;
|
||||
parserId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const TTCMD: FC<Props> = (
|
||||
{ body, parserId, escapeTOC = () => false, title },
|
||||
) => {
|
||||
export const TTCMD: FC<
|
||||
{ body: string; escapeTOC?: (tokens: Token[]) => boolean }
|
||||
> = ({ body, escapeTOC = () => false }) => {
|
||||
const elements = useMemo(() => createElements(body), [body]);
|
||||
|
||||
const [toc, start, end] = useMemo(() => {
|
||||
@ -37,19 +31,15 @@ export const TTCMD: FC<Props> = (
|
||||
setHasEscapedTOC(escapeTOC(toc));
|
||||
}, [escapeTOC, toc]);
|
||||
|
||||
console.log("mdId", parserId);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<MDSkeletonLoader />}>
|
||||
<DevTool id={parserId}>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(JSON.stringify(elements, null, 2))}
|
||||
>
|
||||
Copy AST for {title}
|
||||
copy ast
|
||||
</button>
|
||||
</DevTool>
|
||||
{hasEscapedTOC !== undefined &&
|
||||
(
|
||||
<TTCMDRenderer
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { ReactNode, useCallback, useRef } from "react";
|
||||
import { ReactNode, useCallback, useRef } from 'react'
|
||||
|
||||
export const useRefCallback = <T = ReactNode>(): [
|
||||
T | null,
|
||||
(arg: T) => void,
|
||||
] => {
|
||||
export const useRefCallback = <T = ReactNode>() : [T | null, (arg: T) => void] => {
|
||||
const ref = useRef<T | null>(null);
|
||||
const setRef = useCallback((val: T) => {
|
||||
console.log(val);
|
||||
if (ref.current) {
|
||||
// does something?
|
||||
}
|
||||
@ -14,8 +12,8 @@ export const useRefCallback = <T = ReactNode>(): [
|
||||
// also does something?
|
||||
}
|
||||
|
||||
ref.current = val;
|
||||
}, []);
|
||||
ref.current = val
|
||||
}, [])
|
||||
|
||||
return [ref.current, setRef];
|
||||
};
|
||||
return [ref.current, setRef]
|
||||
}
|
@ -4,6 +4,7 @@ import {
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { bulkRound } from "../../utils/bulkRound";
|
||||
@ -23,7 +24,7 @@ interface IProps {
|
||||
setHover: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
type position = { top: number; left: number; width?: number; };
|
||||
type position = { top: number; left: number; width?: number };
|
||||
|
||||
export const PoppableContent: FC<PropsWithChildren<IProps>> = (
|
||||
{
|
||||
@ -34,6 +35,7 @@ export const PoppableContent: FC<PropsWithChildren<IProps>> = (
|
||||
spacing = 10,
|
||||
setHover,
|
||||
isClosing,
|
||||
isClosed,
|
||||
},
|
||||
) => {
|
||||
const [popRef, setPopRef] = useState<HTMLDivElement>();
|
||||
@ -50,7 +52,7 @@ export const PoppableContent: FC<PropsWithChildren<IProps>> = (
|
||||
relHeight: number,
|
||||
popWidth: number,
|
||||
popHeight: number,
|
||||
_edge: edge,
|
||||
edge: edge,
|
||||
align: alignment,
|
||||
): position => {
|
||||
const pos = {
|
||||
|
@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren, ReactNode, useCallback, useState } from "react";
|
||||
import {
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { PoppableContent } from "./poppable-content";
|
||||
import { useDebounce } from "../../../hooks/useDebounce";
|
||||
|
||||
|
@ -1,36 +1,69 @@
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import Link from "next/link";
|
||||
import React, { Fragment } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { Poppable } from "../poppables/components/poppable";
|
||||
import { Accordion, AccordionContent } from "../accordion";
|
||||
|
||||
export const TokenRenderers = new Map<string, TokenRenderer<any>>();
|
||||
type SearchFunction = (s: string, start: number, end: number) => {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
lastIndex: number;
|
||||
};
|
||||
|
||||
type TokenIdentifier = {
|
||||
rx: RegExp;
|
||||
parse: (s: string) => Token;
|
||||
search?: SearchFunction;
|
||||
};
|
||||
|
||||
type TokenIdentifierMap = Map<
|
||||
string,
|
||||
TokenIdentifier
|
||||
>;
|
||||
|
||||
export const TokenRenderers = new Map<
|
||||
string,
|
||||
TokenRenderer
|
||||
>();
|
||||
|
||||
type IdentifierRegistration = (
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken,
|
||||
renderFunction: TokenRenderer,
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: RegExp,
|
||||
) => void;
|
||||
|
||||
export function buildIdentifierMap(): [
|
||||
TokenIdentifierMap,
|
||||
IdentifierRegistration,
|
||||
] {
|
||||
const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();
|
||||
const TokenIdentifiers = new Map<
|
||||
string,
|
||||
TokenIdentifier
|
||||
>();
|
||||
|
||||
function registerIdentifier<M>(
|
||||
function registerIdentifier(
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken,
|
||||
renderFunction: TokenRenderer,
|
||||
): void;
|
||||
function registerIdentifier<M>(
|
||||
function registerIdentifier(
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken,
|
||||
renderFunction: TokenRenderer,
|
||||
openTagRx: RegExp,
|
||||
closeTagRx: RegExp,
|
||||
): void;
|
||||
function registerIdentifier<M = Record<string, string>>(
|
||||
function registerIdentifier(
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
|
||||
renderFunction: TokenRenderer<M>,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken,
|
||||
renderFunction: TokenRenderer,
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: RegExp,
|
||||
) {
|
||||
@ -43,9 +76,9 @@ export function buildIdentifierMap(): [
|
||||
type,
|
||||
};
|
||||
|
||||
return { ...token, ...identifiedToken } as Token<M>;
|
||||
return { ...token, ...identifiedToken };
|
||||
},
|
||||
search: openTagRx && closeTagRx
|
||||
search: (openTagRx && closeTagRx)
|
||||
? (s, start, end) => {
|
||||
return search(
|
||||
s,
|
||||
@ -66,11 +99,11 @@ export function buildIdentifierMap(): [
|
||||
export const buildOnlyDefaultElements = () => {
|
||||
const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap();
|
||||
|
||||
TokenRenderers.set("text", (t: Token<any>) => {
|
||||
TokenRenderers.set("text", (t) => {
|
||||
debugger;
|
||||
return (
|
||||
<span className="whitespace-pre-wrap">
|
||||
{t.content.replaceAll(/\\n ?/g, "\n")}
|
||||
{t.content.replaceAll("\\n", "\n")}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
@ -81,16 +114,13 @@ export const buildOnlyDefaultElements = () => {
|
||||
const rendersChildrenOnly = true;
|
||||
|
||||
// grid
|
||||
registerIdentifier<{ columns: string }>(
|
||||
registerIdentifier(
|
||||
"grid",
|
||||
/(?<!\/)(?:\[\])+\n+((?:.|\n)*?)\n+\/\[\]/g,
|
||||
(s) => {
|
||||
const rx = /((?:\[\])+)\n+([\s\S]*)\n+\/\[\]/;
|
||||
const [_, columns, content] = s.match(rx) || [
|
||||
"",
|
||||
"..",
|
||||
"Unable to parse grid",
|
||||
];
|
||||
const [_, columns, content] = s.match(rx) ||
|
||||
["", "..", "Unable to parse grid"];
|
||||
return {
|
||||
content,
|
||||
raw: s,
|
||||
@ -102,18 +132,19 @@ export const buildOnlyDefaultElements = () => {
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children, metadata } = token;
|
||||
const { content, children, metadata, uuid } = token;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
"--grid-cols": metadata.columns,
|
||||
} as React.CSSProperties}
|
||||
className="grid grid-cols-dynamic gap-x-8 mb-6"
|
||||
className="grid grid-cols-dynamic gap-x-8 gap-y-6 mb-6"
|
||||
>
|
||||
{children?.map((c) => {
|
||||
const Comp = c.metadata.span ? Fragment : "div";
|
||||
return <Comp className="p" key={c.uuid}>{c.render(c)}</Comp>;
|
||||
})}
|
||||
{children?.map((c, i) => (
|
||||
<div key={c.uuid}>
|
||||
{c.render(c)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -124,38 +155,34 @@ export const buildOnlyDefaultElements = () => {
|
||||
// card
|
||||
registerIdentifier(
|
||||
"card",
|
||||
/\[{2}[\s\S]*?\n+\]{2}/g,
|
||||
/\[{2}([\s\S]*?)\n+\]{2}/g,
|
||||
(s) => {
|
||||
const rx = /\[{2}((?:!?)(?:[0-9]?))\s*?\n+([\s\S]*)\n+\]{2}/;
|
||||
const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/;
|
||||
const match = s.match(rx);
|
||||
const [_, isBlockOrSpan, content] = match || ["", "", s];
|
||||
|
||||
const isBlock = isBlockOrSpan.includes("!");
|
||||
const span = Number(isBlockOrSpan.replace("!", ""));
|
||||
const [_, isBlock, content] = match || ["", "", s];
|
||||
|
||||
return {
|
||||
content: content.trim(),
|
||||
raw: s,
|
||||
metadata: {
|
||||
isBlock,
|
||||
span,
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children, metadata } = token;
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<div
|
||||
data-block={!!metadata.isBlock}
|
||||
style={{
|
||||
"--v-span": metadata.span || 1,
|
||||
} as React.CSSProperties}
|
||||
className="data-[block=false]:card data-[block=false]:mb-6 col-span-2"
|
||||
className="data-[block=false]:card mb-6"
|
||||
>
|
||||
{children?.map((e) => <Fragment key={e.uuid}>{e.render(e)}
|
||||
</Fragment>)}
|
||||
{children?.map((e) => (
|
||||
<Fragment key={e.uuid}>
|
||||
{e.render(e)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -164,26 +191,22 @@ export const buildOnlyDefaultElements = () => {
|
||||
);
|
||||
|
||||
// fenced code block
|
||||
registerIdentifier(
|
||||
"code",
|
||||
/`{3}\n+((?:.|\n)*?)\n+`{3}/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("code", /`{3}\n+((?:.|\n)*?)\n+`{3}/g, (s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse code",
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) ||
|
||||
"Unable to parse code",
|
||||
raw: s,
|
||||
metadata: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap bg-black/20 p-2 rounded-md">
|
||||
{token.content}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// list
|
||||
registerIdentifier(
|
||||
@ -191,18 +214,20 @@ export const buildOnlyDefaultElements = () => {
|
||||
/^\s*-\s([\s\S]*?)\n\n/gm,
|
||||
(s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse list",
|
||||
content: s.match(new RegExp(rx, ""))?.at(1) ||
|
||||
"Unable to parse list",
|
||||
raw: s,
|
||||
metadata: {
|
||||
initialDepth:
|
||||
s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
|
||||
s.replace("\n", "").split("-").at(0)?.length.toString() ||
|
||||
"1",
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersChildrenOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children, metadata } = token;
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<>
|
||||
<ul
|
||||
@ -211,8 +236,11 @@ export const buildOnlyDefaultElements = () => {
|
||||
>
|
||||
{children?.map((c) => {
|
||||
return (
|
||||
<li key={c.uuid} data-depth={metadata.initialDepth}>
|
||||
{c.children?.map((c: Token<any>) => (
|
||||
<li
|
||||
key={c.uuid}
|
||||
data-depth={metadata.initialDepth}
|
||||
>
|
||||
{c.children?.map((c) => (
|
||||
<Fragment key={c.uuid}>{c.render(c)}</Fragment>
|
||||
))}
|
||||
</li>
|
||||
@ -235,15 +263,19 @@ export const buildOnlyDefaultElements = () => {
|
||||
raw: s,
|
||||
metadata: {
|
||||
initialDepth:
|
||||
s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
|
||||
s.replace("\n", "").split("-").at(0)?.length.toString() ||
|
||||
"1",
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children, metadata } = token;
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<li data-depth={metadata.initialDepth} className="ml-2">
|
||||
<li
|
||||
data-depth={metadata.initialDepth}
|
||||
className="ml-2"
|
||||
>
|
||||
{children?.map((c) => (
|
||||
<Fragment key={c.uuid}>
|
||||
(c.render(c))
|
||||
@ -255,10 +287,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
);
|
||||
|
||||
// heading
|
||||
registerIdentifier(
|
||||
"heading",
|
||||
/^#+\s(.*?)$/gm,
|
||||
(s, rx) => {
|
||||
registerIdentifier("heading", /^#+\s(.*?)$/gm, (s, rx) => {
|
||||
const content = s.match(new RegExp(rx, ""))?.at(1) ||
|
||||
"Unable to parse heading";
|
||||
return {
|
||||
@ -271,11 +300,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
return (
|
||||
<div
|
||||
id={token.metadata.id}
|
||||
// id={generateId(token.raw, usedIds)}
|
||||
data-strength={token.metadata.strength}
|
||||
className={`
|
||||
font-bold
|
||||
@ -287,14 +315,10 @@ export const buildOnlyDefaultElements = () => {
|
||||
{token.content}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// image
|
||||
registerIdentifier(
|
||||
"image",
|
||||
/\!\[(.*?)\]\((.*?)\)/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("image", /\!\[(.*?)\]\((.*?)\)/g, (s, rx) => {
|
||||
const [_, title, src] = s.match(new RegExp(rx, ""))!;
|
||||
|
||||
return {
|
||||
@ -307,8 +331,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
const { metadata } = token;
|
||||
metadata.src = metadata.src as string;
|
||||
if (metadata.src.startsWith("<svg")) {
|
||||
@ -325,17 +348,12 @@ export const buildOnlyDefaultElements = () => {
|
||||
}
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={metadata.src} alt={token.content} />;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// anchor
|
||||
registerIdentifier(
|
||||
"anchor",
|
||||
/(?<![\!^])\[(.*?)\]\((.*?)\)/g,
|
||||
(s, rx) => {
|
||||
let preset,
|
||||
[_, title, href] = s.match(new RegExp(rx, ""))!;
|
||||
const match = title.match(/~{2}(cta|button)?(.*)/);
|
||||
registerIdentifier("anchor", /(?<![\!^])\[(.*?)\]\((.*?)\)/g, (s, rx) => {
|
||||
let preset, [_, title, href] = s.match(new RegExp(rx, ""))!;
|
||||
const match = title.match(/`{3}(cta|button)?(.*)/);
|
||||
|
||||
if (match) {
|
||||
[_, preset, title] = match;
|
||||
@ -355,8 +373,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
const { metadata } = token;
|
||||
return (
|
||||
<Link
|
||||
@ -367,8 +384,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
{token.content}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// inline-code
|
||||
registerIdentifier(
|
||||
@ -394,28 +410,25 @@ export const buildOnlyDefaultElements = () => {
|
||||
);
|
||||
|
||||
// bold
|
||||
registerIdentifier(
|
||||
"bold",
|
||||
/\*{2}(.*?)\*{2}/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("bold", /\*{2}(.*?)\*{2}/g, (s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse bold",
|
||||
content: s.match(new RegExp(rx, "i"))?.at(1) ||
|
||||
"Unable to parse bold",
|
||||
raw: s,
|
||||
metadata: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
return <span className="font-bold">{token.content}</span>;
|
||||
},
|
||||
}, (token) => {
|
||||
return (
|
||||
<span className="font-bold">
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// italic
|
||||
registerIdentifier(
|
||||
"italic",
|
||||
/(?<!\*)\*([^\*]+?)\*(?!\*)/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("italic", /(?<!\*)\*([^\*]+?)\*(?!\*)/g, (s, rx) => {
|
||||
return {
|
||||
content: s.match(new RegExp(rx, "i"))?.at(1) ||
|
||||
"Unable to parse italic",
|
||||
@ -424,17 +437,16 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
return <span className="italic">{token.content}</span>;
|
||||
},
|
||||
}, (token) => {
|
||||
return (
|
||||
<span className="italic">
|
||||
{token.content}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// popover
|
||||
registerIdentifier(
|
||||
"popover",
|
||||
/\^\[(.*?)\]\<<(.*?)\>>/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("popover", /\^\[(.*?)\]\<<(.*?)\>>/g, (s, rx) => {
|
||||
const [_, title, content] = s.match(new RegExp(rx, ""))!;
|
||||
|
||||
return {
|
||||
@ -444,14 +456,14 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<Poppable
|
||||
content={children?.map((c) => (
|
||||
<Fragment key={uuid}>{c.render(c)}</Fragment>
|
||||
)) || token.content}
|
||||
)) ||
|
||||
metadata.content}
|
||||
preferredAlign="centered"
|
||||
preferredEdge="bottom"
|
||||
className="cursor-pointer mx-2"
|
||||
@ -461,8 +473,7 @@ export const buildOnlyDefaultElements = () => {
|
||||
</span>
|
||||
</Poppable>
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
registerIdentifier(
|
||||
"accordion",
|
||||
@ -478,13 +489,17 @@ export const buildOnlyDefaultElements = () => {
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children, metadata } = token;
|
||||
const { children, metadata, uuid } = token;
|
||||
return (
|
||||
<div className="bg-black/20 p-1 accordion">
|
||||
<Accordion title={metadata.title || "Expand"}>
|
||||
<Accordion
|
||||
title={metadata.title || "Expand"}
|
||||
>
|
||||
<AccordionContent>
|
||||
{children?.map((e) => (
|
||||
<Fragment key={e.uuid}>{e.render(e)}</Fragment>
|
||||
{children?.map((e, i) => (
|
||||
<Fragment key={e.uuid}>
|
||||
{e.render(e)}
|
||||
</Fragment>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</Accordion>
|
||||
@ -493,36 +508,33 @@ export const buildOnlyDefaultElements = () => {
|
||||
},
|
||||
);
|
||||
|
||||
registerIdentifier(
|
||||
"p",
|
||||
/(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
|
||||
(s) => {
|
||||
registerIdentifier("p", /(?<=\n\n)([\s\S]*?)(?=\n\n)/g, (s, rx) => {
|
||||
return {
|
||||
content: s.replace("\n", " "),
|
||||
content: s,
|
||||
raw: s,
|
||||
metadata: {},
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
const { children } = token;
|
||||
}, (token) => {
|
||||
const { children, uuid } = token;
|
||||
|
||||
debugger;
|
||||
|
||||
return (
|
||||
<div className="p">
|
||||
{children?.map((e) => {
|
||||
return <Fragment key={e.uuid}>{e.render(e)}</Fragment>;
|
||||
console.log(e);
|
||||
return (
|
||||
<Fragment key={e.uuid}>
|
||||
{e.render(e)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
registerIdentifier(
|
||||
"hr",
|
||||
/^-{3,}$/gm,
|
||||
(s) => {
|
||||
registerIdentifier("hr", /^-{3,}$/gm, (s, rx) => {
|
||||
return {
|
||||
content: s,
|
||||
raw: s,
|
||||
@ -530,16 +542,11 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
}, (token) => {
|
||||
return <div className="w-full border-b border-mixed-500 my-3"></div>;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
registerIdentifier(
|
||||
"comment",
|
||||
/<!--[\s\S]+?-->/g,
|
||||
(s) => {
|
||||
registerIdentifier("comment", /<!--[\s\S]+?-->/g, (s, rx) => {
|
||||
return {
|
||||
content: "",
|
||||
metadata: { comment: s },
|
||||
@ -547,16 +554,11 @@ export const buildOnlyDefaultElements = () => {
|
||||
uuid: crypto.randomUUID(),
|
||||
rendersContentOnly,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
}, (token) => {
|
||||
return <></>;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
registerIdentifier(
|
||||
"frontmatter",
|
||||
/^---([\s\S]*?)---/g,
|
||||
(s, rx) => {
|
||||
registerIdentifier("frontmatter", /^---([\s\S]*?)---/g, (s, rx) => {
|
||||
return {
|
||||
content: "",
|
||||
metadata: {
|
||||
@ -565,41 +567,13 @@ export const buildOnlyDefaultElements = () => {
|
||||
raw: "",
|
||||
uuid: "frontmatter",
|
||||
};
|
||||
},
|
||||
(token) => {
|
||||
}, (token) => {
|
||||
return <>{token.raw}</>;
|
||||
},
|
||||
);
|
||||
|
||||
registerIdentifier(
|
||||
"table",
|
||||
/(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
|
||||
(s) => {
|
||||
const splitMarker = "{{^^}}";
|
||||
const original = s;
|
||||
|
||||
let columnPattern: string[] = [];
|
||||
let fullWidth = false;
|
||||
s = s.replace(/^\| (<-)?[-|\s^]+(->)? \|$/gm, (e) => {
|
||||
if (!columnPattern.length) {
|
||||
columnPattern = e.split("|").filter((e) => e);
|
||||
}
|
||||
if (e.match(/^\| <-.*?-> \|/gm)) {
|
||||
fullWidth = true;
|
||||
}
|
||||
return splitMarker;
|
||||
});
|
||||
|
||||
const rowSections = s.split(splitMarker).map((s) =>
|
||||
s
|
||||
.split("\n")
|
||||
.filter((r) => !!r)
|
||||
.map((r) =>
|
||||
r
|
||||
.split("|")
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => !!c)
|
||||
)
|
||||
registerIdentifier("table", /^\|\s[\s\S]*?\|(?=(\n\n)|$)/g, (s, rx) => {
|
||||
const rowSections = s.split(/-/gm).map((s) =>
|
||||
s.split("\n").map((r) => r.split(/\s?\|\s?/g))
|
||||
);
|
||||
|
||||
let headerRows: string[][] = [],
|
||||
@ -626,91 +600,19 @@ export const buildOnlyDefaultElements = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
content: original,
|
||||
raw: original,
|
||||
content: s,
|
||||
raw: s,
|
||||
metadata: {
|
||||
headerRows: headerRows,
|
||||
bodyRows: bodyRows,
|
||||
footerRows: footerRows,
|
||||
columns: maxColumns,
|
||||
columnPattern,
|
||||
fullWidth,
|
||||
headerRows: headerRows.join(" | "),
|
||||
bodyRows: bodyRows.join(" | "),
|
||||
footerRows: footerRows.join(" | "),
|
||||
columns: maxColumns.toString(),
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
(t) => {
|
||||
const {
|
||||
headerRows,
|
||||
bodyRows,
|
||||
footerRows,
|
||||
columns,
|
||||
columnPattern,
|
||||
fullWidth,
|
||||
} = t.metadata;
|
||||
|
||||
return (
|
||||
<table
|
||||
data-full-width={fullWidth}
|
||||
className="md-table data-[full-width=true]:w-full"
|
||||
>
|
||||
{!!headerRows && (
|
||||
<thead>
|
||||
{headerRows.map((r, i) => (
|
||||
<tr key={r.join() + i}>
|
||||
{r.concat(Array(columns - r.length).fill("")).map((c) => {
|
||||
const child = t.children?.find((child) => child.raw === c);
|
||||
return (
|
||||
<th key={r.join() + i + c}>
|
||||
{child?.render(child) || c}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
)}
|
||||
{!!bodyRows && (
|
||||
<tbody>
|
||||
{bodyRows.map((r, i) => (
|
||||
<tr key={r.join() + i}>
|
||||
{r.concat(Array(columns - r.length).fill("")).map((c, i) => {
|
||||
const child = t.children?.find((child) => child.raw === c);
|
||||
return (
|
||||
<td
|
||||
key={r.join() + i + c}
|
||||
className="data-[center=true]:text-center"
|
||||
data-center={!!(columnPattern?.at(i) &&
|
||||
columnPattern.at(i)?.includes("^"))}
|
||||
>
|
||||
{child?.render(child) || c}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
)}
|
||||
{!!footerRows && (
|
||||
<tfoot>
|
||||
{footerRows.map((r, i) => (
|
||||
<tr key={r.join() + i}>
|
||||
{r.concat(Array(columns - r.length).fill("")).map((c) => {
|
||||
const child = t.children?.find((child) => child.raw === c);
|
||||
return (
|
||||
<td key={r.join() + i + c}>
|
||||
{child?.render(child) || c}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
);
|
||||
},
|
||||
);
|
||||
}, (t) => {
|
||||
return <>{t.raw}</>;
|
||||
});
|
||||
|
||||
return TokenIdentifiers;
|
||||
};
|
||||
@ -733,16 +635,14 @@ function findMatchingClosedParenthesis(
|
||||
const openingMatch = openRegex.exec(str);
|
||||
const closingMatch = closedRegex.exec(str);
|
||||
|
||||
if (openingMatch && !closingMatch) {
|
||||
if ((openingMatch && !closingMatch)) {
|
||||
throw Error("Things have gone horribly wrong");
|
||||
}
|
||||
|
||||
// if ((!openingMatch && closingMatch) || (!openingMatch && !closingMatch)) break;
|
||||
|
||||
if (
|
||||
openingMatch &&
|
||||
closingMatch &&
|
||||
openingMatch.index < closingMatch.index
|
||||
openingMatch && closingMatch && openingMatch.index < closingMatch.index
|
||||
) {
|
||||
openings++;
|
||||
lastOpeningSuccessIndex = openingMatch.index + openingMatch[0].length;
|
||||
@ -799,11 +699,10 @@ function search(
|
||||
|
||||
// Finds a unique id for things like headings
|
||||
function generateId(t: string, usedIds: string[]) {
|
||||
let id = t
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z\s]/gi, "")
|
||||
.trim()
|
||||
.replaceAll(" ", "-");
|
||||
let id = t.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
|
||||
" ",
|
||||
"-",
|
||||
);
|
||||
let idNum = 1;
|
||||
while (usedIds.includes(id)) {
|
||||
id = id.replace(/-[0-9]+$/g, "");
|
||||
|
@ -11,8 +11,6 @@ export const createElements = (body: string): Token[] => {
|
||||
const tokenize = (body: string) => {
|
||||
const tokenizedBody: TokenMarker[] = [];
|
||||
|
||||
body = body.replaceAll(/[ \t]+\n/g, "\n").replaceAll(/\n{3,}/g, "\n\n");
|
||||
|
||||
const addToken = (thing: TokenMarker) => {
|
||||
tokenizedBody.push(thing);
|
||||
};
|
||||
@ -118,7 +116,7 @@ function isAcceptableChild(parentType: string, childType: string): boolean {
|
||||
return acceptableChildren ? acceptableChildren.includes(childType) : true;
|
||||
}
|
||||
|
||||
// Occasionally, some P blocks start exactly at the same point as another block (a side effect of needing to exclude preceding line-breaks from the regex while also having the only clear delineation being those line-breaks) so we just remove those P blocks so that when searching for a parent, it doesn't need to figure out if the P block is valid or not. This doesn't cause issues during rendering since each block handles its own container element
|
||||
// Occasionally, some P blocks start exactly at the same point as another block (a side effect of needing to exclude preceding linebreaks from the regex while also having the only clear delineation being those linebreaks) so we just remove those P blocks so that when searching for a parent, it doesn't need to figure out if the P block is valid or not. This doesn't cause issues during rendering since each block handles its own container element
|
||||
function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] {
|
||||
return blocks.filter((block) => {
|
||||
if (block.type !== "p") {
|
||||
|
@ -1,28 +1,14 @@
|
||||
---
|
||||
title: How to use ttcMD
|
||||
author: Emmaline Autumn
|
||||
date: March 14th, 2024
|
||||
updated: March 14th, 2024
|
||||
---
|
||||
|
||||
# Table of Contents
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What even is ttcMD?](#what-even-is-ttcmd)
|
||||
- [Enhanced Standard Elements](#enhanced-standard-elements)
|
||||
- [Links](#links)
|
||||
- [Tables](#tables)
|
||||
- [Custom Elements](#custom-elements)
|
||||
- [Pop-outs](#pop-outs)
|
||||
- [Block-level Elements](#block-level-elements)
|
||||
- [Accordions](#accordions)
|
||||
- [Card](#card)
|
||||
- [Block](#block)
|
||||
- [Grid](#grid)
|
||||
- [Query Elements](#query-elements)
|
||||
- [Resolver](#resolver)
|
||||
- [On-demand Resolver](#on-demand-resolver)
|
||||
- [Query Block Template](#query-block-template)
|
||||
- [Query Block](#query-block)
|
||||
|
||||
---
|
||||
|
||||
@ -30,91 +16,27 @@ updated: March 14th, 2024
|
||||
|
||||
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.
|
||||
|
||||
One thing to note, however, is that ttcMD is *not* standard markdown. It does not generate valid markup for just about any element, and as such it will not be released in any scope wider than inside TTC itself. One could more accurately call it a layout language than actual markdown.
|
||||
|
||||
## 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.
|
||||
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:
|
||||
Prefix the link name with ````button` to create a button.
|
||||
`[```button link name](#links)` produces:
|
||||
|
||||
[~~button link name](#links)
|
||||
[```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:
|
||||
Prefix the link name with ````cta` to create a modestly styled button/call to action.
|
||||
`[```cta link name](#links)` produces:
|
||||
|
||||
[~~cta link name](#links)
|
||||
|
||||
### Tables
|
||||
|
||||
Generally tables will only be as wide as their content needs. To make a table take up the full width, you can use this syntax:
|
||||
|
||||
[][][]
|
||||
|
||||
With a header:
|
||||
```
|
||||
| Table | Header | Row |
|
||||
| <---- | ------ | --> |
|
||||
| Table | Body | Row 1 |
|
||||
| Table | Body | Row 2 |
|
||||
| Table | Body | Row 3 |
|
||||
```
|
||||
|
||||
Without a header:
|
||||
```
|
||||
| <---- | ------ | --> |
|
||||
| Table | Body | Row 1 |
|
||||
| Table | Body | Row 2 |
|
||||
| Table | Body | Row 3 |
|
||||
```
|
||||
|
||||
As you can see, it makes use of the default separator line and uses `<` and `>` to denote that it should be full width. Note that the length of the dashes does not matter, all that matters is that the separator line starts with `| <-` and ends with `-> |`.
|
||||
|
||||
[[!2
|
||||
|
||||
Additionally, you can specify if a column is meant to be centered by using `^` inside the separator. For example, `| <--- | -^- | ---> |` will make the second column of all table rows centered. Its positioning within the column doesn't matter.
|
||||
|
||||
]]
|
||||
|
||||
/[]
|
||||
|
||||
There is also an additional feature of adding a table footer by using a second separator. This second separator does not have any affect on the width or centering of the footer columns.
|
||||
|
||||
**Examples:**
|
||||
|
||||
[][][]
|
||||
|
||||
Normal table:
|
||||
| Table | Header | Row |
|
||||
| ---- | ------ | -- |
|
||||
| Table | Body | Row 1 |
|
||||
| Table | Body | Row 2 |
|
||||
| Table | Body | Row 3 |
|
||||
|
||||
Full width:
|
||||
| <---- | ----- | --> |
|
||||
| Table | Body | Row 1 |
|
||||
| Table | Body | Row 2 |
|
||||
| Table | Body | Row 3 |
|
||||
|
||||
Full width with a centered body column
|
||||
| Table | Header | Row |
|
||||
| <---- | ---^-- | --> |
|
||||
| Table | Body | Row 1 |
|
||||
| Table | Body | Row 2 |
|
||||
| Table | Body | Row 3 |
|
||||
|
||||
|
||||
/[]
|
||||
[```cta link name](#links)
|
||||
|
||||
## Custom Elements
|
||||
|
||||
@ -128,7 +50,7 @@ The syntax is thus: `^[pop-out title]<<pop-out content>>`. The pop-out title wil
|
||||
|
||||
Example:
|
||||
|
||||
This syntax `^[This is my favorite image]<<*Goofy!* >>` will produce this element: ^[This is my favorite image]<<*Goofy!* >>
|
||||
This syntax `^[goofy!]<<This is my *favorite* picture: >>` will produce this element: ^[goofy!]<<This is my *favorite* picture: >>
|
||||
|
||||
Note: currently, only inline elements are available, so formatting is limited
|
||||
|
||||
@ -163,43 +85,6 @@ I can include a [link](#accordions), or *italic* and **bold** text.
|
||||
I can even include a card, like this one
|
||||
]]
|
||||
[/accordion]
|
||||
|
||||
[[!
|
||||
[accordion Accordions look great stacked!]
|
||||
Conveniently group data together in stacked accordions
|
||||
[/accordion]
|
||||
|
||||
[accordion It's great for saving a lot of space]
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt, nesciunt repellat ipsa unde, reiciendis nostrum molestiae necessitatibus tempore ea neque quae nihil veritatis autem recusandae nam eaque facilis rerum quos nisi quam temporibus! Quasi nostrum iste praesentium consequatur a itaque. Eaque temporibus aliquam ex tempore rem iste at in itaque?
|
||||
|
||||
Sint iure nam maiores dolorem dolor officia omnis eos. Odio, corporis sunt totam accusamus, ut necessitatibus molestias molestiae error numquam suscipit quas obcaecati aspernatur qui atque optio unde minima hic autem saepe, nihil debitis cumque vel. Placeat magnam nesciunt sequi suscipit provident dolore commodi corrupti, porro facere assumenda, distinctio magni?
|
||||
|
||||
Suscipit illum temporibus sequi! Nulla excepturi hic tempore nihil deleniti. Hic tempora debitis iure inventore, dolores facilis, sequi dolorem veritatis corrupti dignissimos voluptate consequuntur quaerat quidem totam, odio odit quisquam harum? Consequuntur, doloribus. Necessitatibus rem quidem, saepe eum nobis, tenetur nihil iusto debitis incidunt repudiandae molestiae, corrupti rerum deleniti. Numquam.
|
||||
|
||||
Recusandae iusto distinctio cupiditate quam in aliquid cum ipsa et cumque adipisci voluptatum fuga expedita voluptate assumenda error hic, doloribus beatae libero animi ratione aut modi? Ex reprehenderit facere explicabo quo iusto ad minima nihil. Minus magni quidem architecto at fugiat quo, ab ipsa tenetur facilis, optio iusto deleniti illo.
|
||||
|
||||
Eaque dicta iusto reiciendis natus qui excepturi nesciunt asperiores totam nam, reprehenderit perspiciatis amet beatae libero. Laudantium labore aliquam eveniet ab minima ut sunt laboriosam consequuntur alias impedit magnam voluptatem ea at, soluta nisi tenetur rem officia rerum facilis perferendis voluptas nulla incidunt. Sequi, facere vero doloremque saepe magni consequatur!
|
||||
|
||||
Veritatis quod eveniet expedita quae explicabo, nobis enim reiciendis repellat maiores accusantium id doloribus magni dolorem quis qui illum perferendis repudiandae nisi aut maxime doloremque nulla quam fuga aspernatur? Unde sapiente nisi hic, voluptatibus facilis rem omnis saepe asperiores numquam vitae perferendis eligendi nihil temporibus! Commodi voluptatibus eos perspiciatis quaerat?
|
||||
|
||||
Natus impedit quasi voluptates libero vel dicta totam vitae expedita nulla, saepe doloremque veniam, beatae molestiae quod! Dolores, doloribus est! Iste asperiores fuga, ab esse, aliquid enim laborum delectus rem doloribus earum voluptates? Quibusdam dolorum esse voluptatem quas nisi aspernatur? Accusamus est totam recusandae rerum nisi accusantium dignissimos reprehenderit sapiente!
|
||||
|
||||
Debitis ratione molestiae veniam iure quibusdam quae, eos voluptate adipisci! Tempore blanditiis illum optio ea debitis praesentium, iure pariatur neque nisi facere, unde velit eos expedita dolores fugiat! Accusamus iusto amet perferendis adipisci natus iure sunt assumenda aut, esse tempore provident, repudiandae officiis dolor deserunt cumque aspernatur unde voluptatum suscipit!
|
||||
|
||||
Asperiores maiores expedita qui officia laudantium eos molestiae iusto obcaecati, odio numquam cumque dolores laborum consequatur voluptatem dolore voluptatum corrupti repudiandae doloribus quisquam? Ratione maxime aut molestiae rem suscipit, ab animi corrupti ullam deleniti aliquam, doloremque nemo ducimus quisquam laboriosam nobis debitis! Corporis maxime atque cum nulla consequatur asperiores facere!
|
||||
|
||||
Veniam necessitatibus molestiae explicabo nam vel ipsam non sapiente tempora quo nesciunt ullam, accusamus illo! Voluptatem tempore, quos magnam vitae similique deleniti mollitia. Totam, eligendi corrupti. Commodi ullam tenetur, quo ipsum, quam reiciendis eaque assumenda voluptates consequatur, aliquam perferendis praesentium beatae esse dignissimos iste quidem voluptatibus molestiae qui culpa exercitationem?
|
||||
[/accordion]
|
||||
|
||||
[accordion And can be very useful for secret items in games]
|
||||
|
||||
Let's just pretend that this accordion holds a poker hand instead of a random picture.
|
||||
|
||||
You'll never believe me when I tell you that this legitimately is one of my favorite images of all time.
|
||||

|
||||
[/accordion]
|
||||
|
||||
]]
|
||||
/[]
|
||||
|
||||
### Card
|
||||
@ -218,8 +103,8 @@ super secret! I'll never tell!
|
||||
[/accordion]
|
||||
]]
|
||||
```
|
||||
|
||||
[[
|
||||
|
||||
Card text!
|
||||
|
||||
This is a real wild thing! Look, an accordion!
|
||||
@ -238,43 +123,11 @@ And hurt you.
|
||||
|
||||
[/accordion]
|
||||
]]
|
||||
|
||||
|
||||
[[2
|
||||
Additionally, you can specify a number after the opening brackets (`[[2 ... ]]`) to specify how many grid columns the card should cover, just like this card!
|
||||
]]
|
||||
|
||||
/[]
|
||||
|
||||
### Block
|
||||
|
||||
[][][]
|
||||
An unstyled version of the card is the Block block. This is for grouping items in a grid that should be in the same cell. Same syntax, but you simply add an exclamation point after the opening brackets
|
||||
|
||||
```
|
||||
[[!
|
||||
Any markup :D
|
||||
]]
|
||||
```
|
||||
|
||||
[[!
|
||||
Normally all of these paragraphs would end up in their own cell. However, since I put them in a block, they will be kept together.
|
||||
|
||||
And it's not just paragraphs that you can group together, you can use any other ttcMD element, like this picture!
|
||||
|
||||

|
||||
]]
|
||||
|
||||
[[!2
|
||||
Additionally, you can specify a number after the opening brackets (`[[!2 ... ]]`) to specify how many grid columns the block should cover, just like this block!
|
||||
]]
|
||||
|
||||
/[]
|
||||
|
||||
|
||||
### Grid
|
||||
|
||||
Grid blocks give you access to basic grid layouts. You define the number of columns in the grid by using a number of matching brackets. On smaller screens or containers, grids are ignored, turned into a single column.
|
||||
Grid blocks give you access to basic grid layouts. You define the number of columns in the grid by using a number of matching brackets.
|
||||
|
||||
[][][]
|
||||
[[
|
||||
@ -316,61 +169,3 @@ This card will end up in the third column...
|
||||
]]
|
||||
|
||||
/[]
|
||||
|
||||
## Query Elements
|
||||
|
||||
The following elements are used in combination with ttcQuery. These are definitely more advanced. If you understand generally what "dot notation" is in programming, then it will be a lot easier, but don't let that deter you. Once you understand what it is, you can come back to this and be able to create really cool layouts!
|
||||
|
||||
Query elements (aside for the on-demand resolver) are calculated before parsing the markdown. Will that matter to you? Probably not, but could be necessary as you think about how you are writing your query elements.
|
||||
|
||||
### Resolver
|
||||
|
||||
The resolver is the basic element that allows you to get data, but it has a lot of functionality to it. It has not been fully implemented yet
|
||||
|
||||
Syntax: `??<<List -> QueryValue>>`
|
||||
|
||||
If you've read the ttcQuery docs, you'll know that the `List` type means that you can input any number of `QueryValues` in this element. When the resolver runs, it runs each `QueryValue` from left to right. The last `QueryValue` to run is what gets rendered, but you have access to previous values through their index by using the `$#` variables.
|
||||
|
||||
As `QueryValues` are capable of arithmetic, you can quite easily do simple math inline, though it's unfortunately not quite as simple as just writing an equation. Each `QueryValue` can only do one arithmetic operation. This is to prevent performance issues with parsing arbitrary calculations. So, if you wanted to get the average of the values of 1, 2, 3 and 4, the query would look like this: `??<<1+2,3+4,$0+$1,$2/4>>` which will result in the value 2.5. Arithmetic will fail if a value provided is not a number and will render a message in the markdown.
|
||||
|
||||
If the resolver results in a list of items, it will list all of them together, separated by commas.
|
||||
|
||||
Let's say you want to get the the result of rolling a dice field. You would simply write `$$<<_.path.to.dice,$0.roll>>`. This will roll the dice when the markdown is render, which means it only happens once. If you want to reroll the dice, you either need to reload the markdown by closing the viewer and reopening it, or you need to use an On-demand Resolver.
|
||||
|
||||
### On-demand Resolver
|
||||
|
||||
This works very similarly to the normal resolver, but when it renders it will have a button that you can press. When you press it, it recalculates its value. This is very useful for dice and decks of cards. It has not been fully implemented yet
|
||||
|
||||
Here's the syntax: `??[Template]<<List::QueryValue>>`. Template is a basic string that has access to all values of the resolver using the `$#` variables. If template is left blank, the value that the resolver finally reaches will be rendered. In lieu of a template, you can also have it render the display of a field if it has one.
|
||||
|
||||
To use the dice as an example again, here's how you would do that: `??[Rolling $0, you got: $1]<<_path.to.dice,$0.roll>>`
|
||||
|
||||
For drawing a card and having it show the card's display: `??[]<<_path.to.deck,$0.draw,$1.display>>`
|
||||
|
||||
### Query Block Template
|
||||
|
||||
The Query Block Template is the keystone of the query elements. It allows you to query for an object and use that object as the root for rendering a block of markdown. It has not been fully implemented yet.
|
||||
|
||||
[][][]
|
||||
|
||||
Syntax:
|
||||
```
|
||||
??<<QueryValue>>[[
|
||||
Any markdown.
|
||||
]]
|
||||
```
|
||||
|
||||
/[]
|
||||
|
||||
While in a Query Block Template, all resolvers will use the queried value as the root `_` variable. If the original query returns multiple values, it will render the markdown for each result. This is what makes it so useful. When you want to render your 40k army list, you can do so with very little markdown.
|
||||
|
||||
### Query Block
|
||||
|
||||
Similar to the Query Block Template, the query block passes the values of the query and passes it to the markdown inside of it. However, instead of rendering the whole block, it instead renders the markdown a single time. It has not been fully implemented yet
|
||||
|
||||
Unlike the Query Block Template, it does not change the `_` variable to the queried value. For resolvers to work with the queried data, you need to access it with the `$$` variable. `$$` will be iterated over and will act similar to the Query Block Template, but on a per-line basis, meaning that if a line contains the `$$` variable, it will render that line of markdown for each result of the query. This is primarily useful for something like a List or a Table.
|
||||
|
||||
Syntax:
|
||||
```
|
||||
Still working on the syntax, it's difficult coming up with all of this on the fly when it needs to be unique and easily parsed
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
| test | Table | header |
|
||||
| ---- | ----- | ------ |
|
||||
-------------------------
|
||||
| test | table | row |
|
||||
| shorter | *row* |
|
||||
| look | another |
|
43
md/home.md
43
md/home.md
@ -1,13 +1,10 @@
|
||||
[[
|
||||
|
||||
Tabletop Commander (TTC) is a rules-and-tools app for tabletop games - board, card, war, role-playing, you name it! It is the spiritual successor of Chapter Master by Emmaline Autumn, a Warhammer 40,000 9th Edition rules reference and game helper.
|
||||
Tabletop Commander (TC) is a rules-and-tools app for tabletop games - board, card, war, role-playing, you name it! It is the spiritual successor of Chapter Master by Emmaline Autumn, a Warhammer 40,000 9th Edition rules reference and game helper.
|
||||
|
||||
Emma decided to move on from Chapter Master as her interest in Warhammer 40k waned, replaced by a broader enthusiasm for the wargaming hobby. The release of the 10th edition highlighted Chapter Master's inflexibility and tediousness, prompting her to seek new avenues.
|
||||
|
||||
See, Emma had a vision that anyone could contribute to making rules corrections so that anyone could have all of the rules as they currently exist. This ballooned into the idea that you could have all the rules as they existed at *any point in time.* As soon as she realized that every code change either needed to keep that backward compatibility in mind or would cause the data to no longer be usable, she decided to drop Chapter Master entirely.
|
||||
|
||||
It didn't sit right with her. A big project no longer being worked on and a dead dream. Enter Tabletop Commander. Inspired by the flexibility of Battlescribe and disappointed in its features and lack of updates, Emma started designing a new system, from the ground up, that can be used to build almost anything.
|
||||
Emma decided to move on from Chapter Master as her interest in 40k was supplanted by the greater wargaming hobby after the release of 10th edition made clear that Chapter Master was too inflexible and tedious to work on.
|
||||
|
||||
See, Emma had a vision that anyone could contribute to making rules corrections so that anyone could have all of the rules as they currently exist. This ballooned into the idea that you could have all the rules as they existed at *any time*
|
||||
]]
|
||||
|
||||
[][][]
|
||||
@ -16,7 +13,7 @@ It didn't sit right with her. A big project no longer being worked on and a dead
|
||||
|
||||
### Game Systems
|
||||
|
||||
The basis of TTC is called a Game System. This package
|
||||
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
|
||||
consistently updated content publications, or fork it to
|
||||
@ -35,7 +32,7 @@ your approval and contributions carry.
|
||||
If your score is high enough, and a contribution request has
|
||||
enough approvals, you can even be the one to merge it in!
|
||||
|
||||
[~~cta Learn More](/help/Game%20Systems.md)
|
||||
[```cta Learn More](/help/Game%20Systems.md)
|
||||
|
||||
]]
|
||||
|
||||
@ -44,22 +41,22 @@ enough approvals, you can even be the one to merge it in!
|
||||
### Schemas
|
||||
|
||||
Those who have studied English or databases, you would know that
|
||||
a schema is a structural pattern. TTC aims to provide a simple,
|
||||
a schema is a structural pattern. TC aims to provide a simple,
|
||||
user-edited and maintained schema system for *any* game.
|
||||
|
||||
If that flew over your head, don't worry. Others can share
|
||||
the schemas they've made with everyone, which come as part
|
||||
If that flew over your head, don't worry. Others can share
|
||||
the schemas they've made with everyone, which come as part
|
||||
of a Game System package that you can fork or follow to get both
|
||||
content and schemas ready to use.
|
||||
|
||||
**For the techies:**
|
||||
|
||||
The schema system makes use of a powerful custom query language
|
||||
(ttcQuery) I designed. By writing queries directly into the
|
||||
(tcQuery) I designed. By writing queries directly into the
|
||||
schema, we can reduce the amount of re-written content, while
|
||||
maintaining the presence of data anywhere we need it.
|
||||
|
||||
[~~cta Learn More](/help/Schemas.md)
|
||||
[```cta Learn More](/help/Schemas.md)
|
||||
|
||||
]]
|
||||
|
||||
@ -68,7 +65,7 @@ maintaining the presence of data anywhere we need it.
|
||||
### Publications
|
||||
|
||||
Publications are the actual content of the rules. They
|
||||
don't just contain the content, but also the style in which
|
||||
don't just contain the content, but also the style in which
|
||||
the content is shown.
|
||||
|
||||
Content can include text, images, and even video (through
|
||||
@ -78,23 +75,15 @@ parts of the publication through context based pop-overs.
|
||||
**For the techies (again):**
|
||||
|
||||
Publications use an enhanced markdown syntax (ttcMD) that
|
||||
implements ttcQuery, and adds a bit of custom syntax for things
|
||||
implements tcQuery, and adds a bit of custom syntax for things
|
||||
like pop-overs and styling hints for rendering.
|
||||
|
||||
Though it uses markdown as its base syntax, ttcMD could more accurately be called a templating language as it contains a lot of custom elements to handle different layouts
|
||||
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 ttcMD!
|
||||
|
||||
[~~cta Learn More](/help/Publications.md)
|
||||
[```cta Learn More](/help/Publications.md)
|
||||
|
||||
]]
|
||||
|
||||
[[2
|
||||
|
||||
Want to keep up with TTC? Join us over at the CyborgGrizzly Games Discord server! Come discuss tabletop gaming, stay updated on the latest developments with Tabletop Commander, and collaborate with fellow gamers to create and update game systems. It's the perfect spot to connect with like-minded enthusiasts and be part of shaping the future of tabletop gaming. Don't miss out – hop on the Discord server today!
|
||||
|
||||
Disclaimer: *I'm so sorry, I had ChatGPT write that last paragraph. I tried to save it, but it's just so... corporate*
|
||||
|
||||
[~~cta Join the Discord](https://discord.gg/bePt7MQHQA)
|
||||
|
||||
]]
|
||||
/[]
|
||||
|
||||
|
@ -16,15 +16,14 @@
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^7.2.0"
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,6 @@ const config: Config = {
|
||||
height: {
|
||||
variable: "var(--v-height)",
|
||||
},
|
||||
gridColumn: {
|
||||
variable: "span var(--v-span) / span var(--v-span)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
40
types.d.ts
vendored
40
types.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
type IdentifiedToken<M> = {
|
||||
metadata: M;
|
||||
type IdentifiedToken = {
|
||||
metadata: Record<string, string>;
|
||||
children?: Token[];
|
||||
uuid: string;
|
||||
raw: string;
|
||||
@ -8,49 +8,21 @@ type IdentifiedToken<M> = {
|
||||
rendersContentOnly?: boolean;
|
||||
};
|
||||
|
||||
type TokenRenderer<M> = (t: Token<M>) => ReactNode;
|
||||
type TokenRenderer = (t: Token) => ReactNode;
|
||||
|
||||
type TokenAttributes = {
|
||||
type: string;
|
||||
render: TokenRenderer;
|
||||
};
|
||||
|
||||
type Token<M = Record<string, string>> = IdentifiedToken<M> & TokenAttributes;
|
||||
type Token = IdentifiedToken & TokenAttributes;
|
||||
|
||||
type TokenMarker<M = Record<string, string>> = {
|
||||
type TokenMarker = {
|
||||
start: number;
|
||||
end: number;
|
||||
type: string;
|
||||
parent?: TokenMarker;
|
||||
token: Token<M>;
|
||||
token: Token;
|
||||
};
|
||||
|
||||
type FrontMatter = Record<string, string>;
|
||||
|
||||
type SearchFunction = (
|
||||
s: string,
|
||||
start: number,
|
||||
end: number,
|
||||
) => {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
lastIndex: number;
|
||||
};
|
||||
|
||||
type TokenIdentifier<M> = {
|
||||
rx: RegExp;
|
||||
parse: (s: string) => Token<M>;
|
||||
search?: SearchFunction;
|
||||
};
|
||||
|
||||
type TokenIdentifierMap = Map<string, TokenIdentifier<any>>;
|
||||
|
||||
type IdentifierRegistration = <N = Record<string, string>>(
|
||||
type: string,
|
||||
match: RegExp,
|
||||
parseFunction: (s: string, rx: RegExp) => IdentifiedToken<N>,
|
||||
renderFunction: TokenRenderer<N>,
|
||||
openTagRx?: RegExp,
|
||||
closeTagRx?: RegExp,
|
||||
) => void;
|
||||
|
Loading…
x
Reference in New Issue
Block a user