From 26db5329776cadd9b01731ced2e0229896b52e47 Mon Sep 17 00:00:00 2001 From: Rizzo Date: Thu, 6 Aug 2026 00:59:19 +0800 Subject: [PATCH] fix: render YAML frontmatter as a metadata card in markdown preview react-markdown has no frontmatter support, so a leading `---` block was parsed as CommonMark instead: the opening `---` became an
and the closing `---` turned the YAML lines into a setext heading, leaking raw metadata like `title: My Doc` into the rendered preview between two horizontal rules (and letting remark-math mangle any $ inside it). - Add remark-frontmatter (yaml only) to both render pipelines so the block parses as a `yaml` node and disappears from the rendered output. - Add parseFrontmatter() (lib/markdown.ts) backed by js-yaml, and a FrontmatterCard that renders the metadata above the preview body: `title` as a heading, `tags`/`categories`/`keywords` as chips, remaining keys as a key/value table with http(s)/mailto values as links. - Malformed YAML shows no card but is still hidden by the remark plugin; documents without frontmatter render exactly as before. Verified with tsc --noEmit and eslint; the parse pipeline was also checked with unified directly (frontmatter yields no output, and the setext-trap / mid-document `---` cases still parse as CommonMark). --- app/globals.css | 58 +++++++++++ components/FileViewer.tsx | 9 +- components/FrontmatterCard.tsx | 85 ++++++++++++++++ lib/markdown.ts | 38 ++++++- package-lock.json | 177 +++++++++++++++++++++++---------- package.json | 3 + 6 files changed, 314 insertions(+), 56 deletions(-) create mode 100644 components/FrontmatterCard.tsx diff --git a/app/globals.css b/app/globals.css index 1c0e917bd..805bea01d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -757,6 +757,64 @@ pre, code { padding: 0; } +/* YAML frontmatter metadata card (file preview) */ +.markdown-frontmatter { + margin-bottom: 20px; + padding: 14px 16px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-panel); +} +.markdown-frontmatter-title { + font-size: 1.3em; + font-weight: 600; + line-height: 1.35; + color: var(--text); +} +.markdown-frontmatter-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.markdown-frontmatter-tag { + font-size: 12px; + line-height: 1.6; + color: var(--text-muted); + background: var(--bg-subtle); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0 10px; +} +.markdown-frontmatter-rows { + display: grid; + grid-template-columns: auto 1fr; + gap: 5px 14px; + margin: 10px 0 0; + font-size: 13px; + line-height: 1.55; +} +.markdown-frontmatter-row { + display: contents; +} +.markdown-frontmatter-row dt { + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 0.92em; + white-space: nowrap; +} +.markdown-frontmatter-row dd { + margin: 0; + color: var(--text); + min-width: 0; + word-break: break-word; +} +.markdown-frontmatter-row dd a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; +} + /* syntax highlighter line numbers */ .markdown-body span.linenumber, span.linenumber { diff --git a/components/FileViewer.tsx b/components/FileViewer.tsx index 9635b4c6d..1912648c4 100644 --- a/components/FileViewer.tsx +++ b/components/FileViewer.tsx @@ -19,8 +19,9 @@ import { } from "@/lib/file-types"; import { encodeFilePathForApi, getFileDirectory, getFileName, getRelativeFilePath } from "@/lib/file-paths"; import { resolveLocalFileHref } from "@/lib/file-links"; -import { markdownPreviewRehypePlugins, markdownPreviewRemarkPlugins, normalizeDisplayMath } from "@/lib/markdown"; +import { markdownPreviewRehypePlugins, markdownPreviewRemarkPlugins, normalizeDisplayMath, parseFrontmatter } from "@/lib/markdown"; import { CodeBlock, MermaidBlock } from "./MermaidBlock"; +import { FrontmatterCard } from "./FrontmatterCard"; import { parseUnifiedPatch } from "@/lib/patch"; import type { GitFileDiffResponse } from "@/lib/git-types"; import { useI18n } from "@/hooks/useI18n"; @@ -939,6 +940,11 @@ function TextFileViewer({ filePath, cwd, sourceSessionId, onOpenFile, onMentionL [data], ); + const frontmatter = useMemo( + () => (data?.language === "markdown" ? parseFrontmatter(data.content) : null), + [data], + ); + useEffect(() => { const updateSelectedLineRange = () => { const root = contentRef.current; @@ -1142,6 +1148,7 @@ function TextFileViewer({ filePath, cwd, sourceSessionId, onOpenFile, onMentionL className="markdown-body markdown-file-preview" style={{ padding: "24px 32px" }} > + {frontmatter?.data && } | null; +} + +const TAG_KEYS = ["tags", "categories", "keywords", "tag", "category"]; + +function isUrl(value: string): boolean { + return /^(https?:\/\/|mailto:)/i.test(value); +} + +function formatValue(value: unknown): string { + if (value === null || value === undefined) return ""; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + if (Array.isArray(value)) return value.map(formatValue).filter(Boolean).join(", "); + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + return String(value); +} + +function renderValue(value: unknown): ReactNode { + const text = formatValue(value); + if (!text) return null; + if (typeof value === "string" && isUrl(value)) { + // Only safe schemes — values come from the user's own file but stay escaped + // by React regardless; this just prevents javascript: hrefs. + return ( + + {text} + + ); + } + // Arrays are rendered as inline text; anything else keeps its plain text form. + return text; +} + +export function FrontmatterCard({ data }: FrontmatterCardProps) { + if (!data) return null; + const entries = Object.entries(data); + if (entries.length === 0) return null; + + const title = + typeof data.title === "string" && data.title.trim() ? data.title.trim() : null; + + const tagKey = TAG_KEYS.find((key) => Array.isArray(data[key])); + const tags = tagKey + ? (data[tagKey] as unknown[]).map(formatValue).filter(Boolean) + : []; + + const rows = entries.filter(([key]) => key !== "title" && key !== tagKey); + + return ( +
+ {title &&
{title}
} + {tags.length > 0 && ( +
+ {tags.map((tag, index) => ( + + {tag} + + ))} +
+ )} + {rows.length > 0 && ( +
+ {rows.map(([key, value]) => ( +
+
{key}
+
{renderValue(value)}
+
+ ))} +
+ )} +
+ ); +} diff --git a/lib/markdown.ts b/lib/markdown.ts index d984268cb..fc06bddd1 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -1,7 +1,9 @@ import type { Options as ReactMarkdownOptions } from "react-markdown"; +import { load as parseYaml } from "js-yaml"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import remarkFrontmatter from "remark-frontmatter"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; @@ -160,8 +162,40 @@ function normalizeInlineLatexMath(line: string): string { ); } -export const markdownRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath]; -export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath]; +// Parse YAML frontmatter into a `yaml` node before the math/GFM plugins run, so +// the raw metadata never leaks into the rendered output (without it, the opening +// `---` becomes an
and the closing `---` turns the YAML into a setext heading). +export const markdownRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [[remarkFrontmatter, ["yaml"]], remarkGfm, remarkMath]; +export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [[remarkFrontmatter, ["yaml"]], remarkGfm, remarkMath]; + +/** + * Leading `---`-fenced YAML block. Mirrors what `remarkFrontmatter(["yaml"])` + * recognizes so the metadata card and the hidden-in-body behavior stay in sync. + */ +const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; + +export interface FrontmatterResult { + /** Parsed metadata, or null when the document has no valid frontmatter. */ + data: Record | null; + /** Markdown with the frontmatter block removed. */ + rest: string; +} + +export function parseFrontmatter(markdown: string): FrontmatterResult { + const match = markdown.match(FRONTMATTER_RE); + if (!match) return { data: null, rest: markdown }; + + try { + const parsed = parseYaml(match[1]); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return { data: parsed as Record, rest: markdown.slice(match[0].length) }; + } + } catch { + // Malformed YAML: leave the text untouched, the frontmatter plugin still hides it. + } + + return { data: null, rest: markdown }; +} export const markdownRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [ rehypeRaw, diff --git a/package-lock.json b/package-lock.json index 99ca9d023..40ea9085a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@earendil-works/pi-ai": "0.83.0", "@earendil-works/pi-coding-agent": "0.83.0", "@earendil-works/pi-tui": "0.83.0", + "js-yaml": "^5.2.3", "next": "16.2.12", "proper-lockfile": "4.1.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "remark-frontmatter": "^5.0.0", "undici": "8.5.0" }, "bin": { @@ -25,6 +27,7 @@ "devDependencies": { "@lobehub/icons": "^5.6.0", "@tailwindcss/postcss": "^4.2.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^25", "@types/proper-lockfile": "4.1.4", "@types/react": "^19", @@ -3378,6 +3381,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7177,7 +7203,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -7224,6 +7249,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7249,7 +7281,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -7266,7 +7297,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -7351,7 +7381,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -8078,7 +8107,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -8354,7 +8382,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -8604,7 +8631,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -9491,7 +9517,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "dev": true, "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -9568,7 +9593,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9588,7 +9612,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dev": true, "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -9602,6 +9625,7 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -10869,7 +10893,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true, "engines": { "node": ">=0.4.x" } @@ -11722,6 +11745,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -12168,7 +12192,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12402,16 +12425,25 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz", + "integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/jsesc": { @@ -13000,7 +13032,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -13189,7 +13220,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -13210,6 +13240,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-gfm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", @@ -13437,7 +13497,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -13474,7 +13533,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -13496,7 +13554,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -13593,7 +13650,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13630,7 +13686,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13707,6 +13762,35 @@ } } }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", @@ -13966,7 +14050,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13988,7 +14071,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14039,7 +14121,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14060,7 +14141,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14083,7 +14163,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14106,7 +14185,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14127,7 +14205,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14147,7 +14224,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14169,7 +14245,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14190,7 +14265,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14210,7 +14284,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14233,7 +14306,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14276,7 +14348,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14293,7 +14364,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14313,7 +14383,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14333,7 +14402,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14355,7 +14423,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14378,7 +14445,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14395,7 +14461,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -16181,6 +16246,22 @@ } } }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -17373,7 +17454,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -17626,7 +17706,6 @@ "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -17662,7 +17741,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -17719,7 +17797,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -17733,7 +17810,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17749,7 +17825,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17884,7 +17959,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17914,7 +17988,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18096,7 +18169,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -18212,7 +18284,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 03966a46f..c219969a7 100644 --- a/package.json +++ b/package.json @@ -41,15 +41,18 @@ "@earendil-works/pi-ai": "0.83.0", "@earendil-works/pi-coding-agent": "0.83.0", "@earendil-works/pi-tui": "0.83.0", + "js-yaml": "^5.2.3", "next": "16.2.12", "proper-lockfile": "4.1.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "remark-frontmatter": "^5.0.0", "undici": "8.5.0" }, "devDependencies": { "@lobehub/icons": "^5.6.0", "@tailwindcss/postcss": "^4.2.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^25", "@types/proper-lockfile": "4.1.4", "@types/react": "^19",