diff --git a/app/globals.css b/app/globals.css index 7a61697..eff61e1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -31,7 +31,7 @@ } .heading { - @apply pb-8 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; @@ -68,7 +68,7 @@ @apply rounded-t-md; } .accordion:has(+ .accordion) { - @apply border-b border-black/20; + @apply border-b border-black/20 dark:border-white/20; } .accordion:not(:has(+ .accordion)) { @apply rounded-b-md; diff --git a/app/help/[article]/client.tsx b/app/help/[article]/client.tsx index 3ca480b..20ed9ba 100644 --- a/app/help/[article]/client.tsx +++ b/app/help/[article]/client.tsx @@ -1,7 +1,8 @@ "use client"; import { TTCMD, TTCMDRenderer } from "@/components/ttcmd"; -import { FC, use, useCallback, useState } from "react"; +import { extractFrontMatter } from "@/lib/tcmd"; +import { FC, use, useCallback, useEffect, useState } from "react"; export const HelpClient: FC<{ body: Promise; title: string }> = ({ body: bodyPromise, @@ -14,21 +15,47 @@ export const HelpClient: FC<{ body: Promise; title: string }> = ({ return true; }, []); + const [frontMatter, setFrontMatter] = useState({}); + const [cleanBody, setBody] = useState(""); + + useEffect(() => { + if (!body) return; + + const [frontmatter, clean] = extractFrontMatter(body); + setFrontMatter(frontmatter); + setBody(clean); + }, [body]); + return ( <>

Help

-

{decodeURIComponent(title)}

+

{frontMatter.title || decodeURIComponent(title)}

+
+ {!!frontMatter.date && ( +
+ Published: {frontMatter.date} + {!!frontMatter.updated && ( + + , Last updated: {frontMatter.updated} + + )} +
+ )} + {!!frontMatter.author && ( +
By: {frontMatter.author}
+ )} +
{toc && ( -
+
)} diff --git a/lib/tcmd/TokenIdentifiers.ts b/lib/tcmd/TokenIdentifiers.ts index 9a3d16a..c4b025a 100644 --- a/lib/tcmd/TokenIdentifiers.ts +++ b/lib/tcmd/TokenIdentifiers.ts @@ -1,3 +1,5 @@ +import { parse } from "path"; + type TokenIdentifier = { rx: RegExp; parse: (s: string) => Token; @@ -63,8 +65,13 @@ TokenIdentifiers.set("card", { return search(s, start, end, rx, crx); }, parse(s) { - const rx = /\[{2}(!?)\n+([\s\S]*)\n+\]{2}/; - const [_, isBlock, content] = s.match(rx) || ["", "Unable to parse card"]; + const rx = /\[{2}(!?)\s*?\n+([\s\S]*)\n+\]{2}/; + const match = s.match(rx); + if (!match) debugger; + const [_, isBlock, content] = match || + ["", "", s]; + + // if (!_) debugger; return { content: content.trim(), @@ -74,6 +81,7 @@ TokenIdentifiers.set("card", { }, type: "card", uuid: crypto.randomUUID(), + rendersChildrenOnly, }; }, }); @@ -187,7 +195,7 @@ TokenIdentifiers.set("anchor", { }, }); TokenIdentifiers.set("inline-code", { - rx: /\s?`(.{3,}?|[a-z0-9]*?)`[^`a-z0-9\n]/gi, + rx: /(?<=\s|^)`(.*?)`(?=[\s,.!?)]|$)/gi, parse(s) { return { // content: inline, @@ -307,6 +315,21 @@ TokenIdentifiers.set("comment", { }, }); +TokenIdentifiers.set("frontmatter", { + rx: /^---([\s\S]*?)---/g, + parse(s) { + return { + content: "", + metadata: { + frontmatterString: s.match(this.rx)?.at(0) || "", + }, + raw: "", + type: "frontmatter", + uuid: "frontmatter", + }; + }, +}); + function findMatchingClosedParenthesis( str: string, openRegex: RegExp, diff --git a/lib/tcmd/index.ts b/lib/tcmd/index.ts index a22c938..794df84 100644 --- a/lib/tcmd/index.ts +++ b/lib/tcmd/index.ts @@ -155,9 +155,20 @@ const contentToChildren = (token: Token) => { content: c.replaceAll("\n", ""), metadata: {}, raw: c, - type: "text", + type: token.rendersChildrenOnly ? "p" : "text", uuid: crypto.randomUUID(), rendersContentOnly: true, + children: token.rendersChildrenOnly && c.replaceAll("\n", "") + ? [ + { + content: c.replaceAll("\n", ""), + metadata: {}, + raw: c, + type: "text", + uuid: crypto.randomUUID(), + }, + ] + : undefined, })), token.children || [], ).filter((c) => c.children?.length || (c.rendersContentOnly && c.content)); @@ -239,3 +250,26 @@ function processChunks(chunks: Token[][]) { // Filter out chunks that were merged into others return chunks.filter((c) => !mergedChunks.find((c2) => c === c2)); } + +/** + * @description Extracts the frontmatter of a markdown document and returns it as an object and with the body with the frontmatter removed from it + * + * @returns a tuple containing the body and the frontmatter object + */ +export function extractFrontMatter(body: string): [FrontMatter, string] { + const rx = /^---([\s\S]*?)---/; + const [_, frontmatterString] = body.match(rx) || ["", ""]; + + body = body.replace(rx, ""); + + const frontMatter: FrontMatter = {}; + + for (const line of frontmatterString.split("\n")) { + if (!line) continue; + + const [key, value] = line.split(": "); + frontMatter[key] = value; + } + + return [frontMatter, body]; +} diff --git a/types.d.ts b/types.d.ts index 87707b5..a04a730 100644 --- a/types.d.ts +++ b/types.d.ts @@ -58,6 +58,8 @@ type TokenMarker = { token: Token; }; +type FrontMatter = Record; + type MultilineCfg = { rx: RegExp; closeRx?: RegExp;