diff --git a/website/bun.lockb b/website/bun.lockb index 1a31e23..25a0afb 100755 Binary files a/website/bun.lockb and b/website/bun.lockb differ diff --git a/website/package.json b/website/package.json index a42fa36..816e031 100644 --- a/website/package.json +++ b/website/package.json @@ -24,6 +24,9 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.46.0", "globals": "^15.11.0", + "mdast": "^3.0.0", + "mdast-util-directive": "^3.0.0", + "mdast-util-to-hast": "^13.2.0", "mdsvex": "^0.12.3", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.7", @@ -48,15 +51,19 @@ "hast-util-heading": "^3.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-text": "^4.0.2", + "hastscript": "^9.0.0", + "lucide-static": "^0.462.0", "lucide-svelte": "^0.446.0", "rehype-infer-description-meta": "^2.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", + "remark-directive": "^3.0.0", "remark-frontmatter": "^5.0.0", "remark-gemoji": "^8.0.0", "remark-gfm": "^4.0.0", + "remark-github-admonitions-to-directives": "^2.1.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "shiki": "^1.22.2", diff --git a/website/src/admonitions.css b/website/src/admonitions.css new file mode 100644 index 0000000..7826766 --- /dev/null +++ b/website/src/admonitions.css @@ -0,0 +1,108 @@ +.admonition { + @apply my-4 rounded-sm px-4 py-3 text-[--tw-prose-body] prose-p:my-2 prose-pre:my-4; + @apply border-l-4 border-[--admonition-border]; + @apply bg-[--admonition-bg]; + + @apply [--shiki-background:theme(colors.white/0.2)]; + @apply dark:[--shiki-background:theme(colors.black/0.2)]; + + --tw-prose-body: theme(colors.light); + --tw-prose-headings: theme(colors.light); + --tw-prose-lead: theme(colors.light); + --tw-prose-links: var(--admonition-text); + --tw-prose-bold: theme(colors.light); + --tw-prose-counters: theme(colors.light); + --tw-prose-bullets: var(--admonition-border); + --tw-prose-hr: var(--admonition-border); + --tw-prose-quotes: theme(colors.light); + --tw-prose-quote-borders: var(--admonition-border); + --tw-prose-code: theme(colors.light); + --tw-prose-pre-code: theme(colors.light); + --tw-prose-pre-bg: var(--shiki-background); + --tw-prose-th-borders: var(--admonition-border); + --tw-prose-td-borders: var(--admonition-border); +} + +.admonition pre { + @apply border border-[--admonition-border] bg-[--shiki-background]; +} + +.admonition-title { + @apply flex items-center space-x-2 text-lg font-semibold; +} + +.admonition-title * { + color: var(--admonition-text); +} + +.admonition-icon { + @apply inline-block size-6 bg-current; + mask-image: var(--admonition-icon); +} + +.admonition-note { + --admonition-bg: theme(colors.blue.600 / 0.1); + --admonition-border: theme(colors.blue.600 / 0.4); + --admonition-text: theme(colors.blue.950); + --admonition-icon: url(lucide-static/icons/info.svg); +} + +.admonition-tip { + --admonition-bg: theme(colors.green.600 / 0.1); + --admonition-border: theme(colors.green.600 / 0.4); + --admonition-text: theme(colors.green.950); + --admonition-icon: url(lucide-static/icons/lightbulb.svg); +} + +.admonition-info { + --admonition-bg: theme(colors.purple.600 / 0.1); + --admonition-border: theme(colors.purple.600 / 0.4); + --admonition-text: theme(colors.purple.950); + --admonition-icon: url(lucide-static/icons/message-square-warning.svg); +} + +.admonition-warning { + --admonition-bg: theme(colors.yellow.600 / 0.1); + --admonition-border: theme(colors.yellow.600 / 0.4); + --admonition-text: theme(colors.yellow.950); + --admonition-icon: url(lucide-static/icons/triangle-alert.svg); +} + +.admonition-danger { + --admonition-bg: theme(colors.red.600 / 0.1); + --admonition-border: theme(colors.red.600 / 0.4); + --admonition-text: theme(colors.red.950); + --admonition-icon: url(lucide-static/icons/octagon-alert.svg); +} + +@media (prefers-color-scheme: dark) { + .admonition-note { + --admonition-bg: theme(colors.blue.500 / 0.1); + --admonition-border: theme(colors.blue.500 / 0.6); + --admonition-text: theme(colors.blue.100); + } + + .admonition-tip { + --admonition-bg: theme(colors.green.500 / 0.1); + --admonition-border: theme(colors.green.500 / 0.6); + --admonition-text: theme(colors.green.100); + } + + .admonition-info { + --admonition-bg: theme(colors.purple.500 / 0.1); + --admonition-border: theme(colors.purple.500 / 0.6); + --admonition-text: theme(colors.purple.100); + } + + .admonition-warning { + --admonition-bg: theme(colors.yellow.500 / 0.1); + --admonition-border: theme(colors.yellow.500 / 0.6); + --admonition-text: theme(colors.yellow.100); + } + + .admonition-danger { + --admonition-bg: theme(colors.red.500 / 0.1); + --admonition-border: theme(colors.red.500 / 0.6); + --admonition-text: theme(colors.red.100); + } +} diff --git a/website/src/app.css b/website/src/app.css index 0883f6a..35cb953 100644 --- a/website/src/app.css +++ b/website/src/app.css @@ -2,6 +2,8 @@ @import "tailwindcss/components"; @import "tailwindcss/utilities"; +@import "admonitions.css"; + :root { --color-background: 255 245 230; --color-card: 245 230 210; diff --git a/website/src/app.d.ts b/website/src/app.d.ts index f70d0e1..6463062 100644 --- a/website/src/app.d.ts +++ b/website/src/app.d.ts @@ -1,3 +1,5 @@ +/// + // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { diff --git a/website/src/lib/markdown.ts b/website/src/lib/markdown.ts index 2cab9b2..044a35d 100644 --- a/website/src/lib/markdown.ts +++ b/website/src/lib/markdown.ts @@ -3,14 +3,19 @@ import type { Nodes } from "hast" import { heading } from "hast-util-heading" import { headingRank } from "hast-util-heading-rank" import { toText } from "hast-util-to-text" +import { h, type Child } from "hastscript" +import type { ContainerDirective } from "mdast-util-directive" +import type { Handler } from "mdast-util-to-hast" import rehypeInferDescriptionMeta from "rehype-infer-description-meta" import rehypeRaw from "rehype-raw" -import rehypeSanitize from "rehype-sanitize" +import rehypeSanitize, { defaultSchema } from "rehype-sanitize" import rehypeSlug from "rehype-slug" import rehypeStringify from "rehype-stringify" +import remarkDirective from "remark-directive" import remarkFrontmatter from "remark-frontmatter" import remarkGemoji from "remark-gemoji" import remarkGfm from "remark-gfm" +import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives" import remarkParse from "remark-parse" import remarkRehype from "remark-rehype" import { createCssVariablesTheme, createHighlighter } from "shiki" @@ -23,15 +28,92 @@ const highlighter = createHighlighter({ langs: [], }) +const ADMONITION_TYPES = { + note: { + label: "Note", + }, + tip: { + label: "Tip", + }, + info: { + label: "Info", + }, + warning: { + label: "Warning", + }, + danger: { + label: "Danger", + }, +} + +const containerDirectiveHandler: Handler = (state, node: ContainerDirective) => { + const type = node.name as keyof typeof ADMONITION_TYPES + if (!type || !(type in ADMONITION_TYPES)) { + return + } + + const typeInfo = ADMONITION_TYPES[type] + + let label: Child = typeInfo.label + + const firstChild = node.children[0] + if (firstChild?.type === "paragraph" && firstChild.data?.directiveLabel) { + node.children.shift() + label = state.all(firstChild) + } + + return h( + "div", + { + class: `admonition admonition-${type}`, + }, + [ + h( + "p", + { + class: "admonition-title", + }, + [ + h("span", { + class: "admonition-icon", + }), + h( + "span", + { + class: "admonition-label", + }, + label, + ), + ], + ), + state.all(node), + ], + ) +} + +const sanitizeSchema: typeof defaultSchema = { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + "*": [...(defaultSchema.attributes?.["*"] ?? []), ["className", "admonition", /^admonition-/]], + }, +} + +const remarkRehypeHandlers = { + containerDirective: containerDirectiveHandler, +} + export const markdown = (async () => { return unified() .use(remarkParse) .use(remarkFrontmatter) .use(remarkGfm) .use(remarkGemoji) - .use(remarkRehype, { allowDangerousHtml: true }) + .use(remarkGithubAdmonitionsToDirectives) + .use(remarkDirective) + .use(remarkRehype, { allowDangerousHtml: true, handlers: remarkRehypeHandlers }) .use(rehypeRaw) - .use(rehypeSanitize) + .use(rehypeSanitize, sanitizeSchema) .use(rehypeShikiFromHighlighter, await highlighter, { lazy: true, theme: createCssVariablesTheme({ @@ -58,7 +140,13 @@ export const docsMarkdown = (async () => { .use(remarkFrontmatter) .use(remarkGfm) .use(remarkGemoji) - .use(remarkRehype, { allowDangerousHtml: true, clobberPrefix: "" }) + .use(remarkGithubAdmonitionsToDirectives) + .use(remarkDirective) + .use(remarkRehype, { + allowDangerousHtml: true, + clobberPrefix: "", + handlers: remarkRehypeHandlers, + }) .use(rehypeSlug) .use(() => (node, file) => { const toc: TocItem[] = [] @@ -97,7 +185,7 @@ export const docsMarkdown = (async () => { }) as Node }) .use(rehypeRaw) - .use(rehypeSanitize) + .use(rehypeSanitize, sanitizeSchema) .use(rehypeShikiFromHighlighter, await highlighter, { lazy: true, theme: createCssVariablesTheme({