diff --git a/package.json b/package.json index 3f703252..5f44c795 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@lezer/highlight": "1.1.6", "@mantine/core": "7.0.2", "@mantine/dates": "7.0.2", + "@microflash/remark-callout-directives": "1.6.0", "@monaco-editor/react": "4.5.2", "@popperjs/core": "2.11.8", "@prisma/client": "5.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9641df6..38d765e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: '@mantine/dates': specifier: 7.0.2 version: 7.0.2(@mantine/core@7.0.2)(@mantine/hooks@7.0.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + '@microflash/remark-callout-directives': + specifier: 1.6.0 + version: 1.6.0 '@monaco-editor/react': specifier: 4.5.2 version: 4.5.2(monaco-editor@0.43.0)(react-dom@18.2.0)(react@18.2.0) @@ -2368,6 +2371,17 @@ packages: - supports-color dev: false + /@microflash/remark-callout-directives@1.6.0: + resolution: {integrity: sha512-W2ovtw9JdSpO/gjQb3wlf3o5AW4h+w6mewXybsrtSvifb/30P7ahBELDmFSCQBuXiofMbN8xO0/0GqVYoZrXmQ==} + dependencies: + defu: 6.1.2 + hastscript: 7.2.0 + mdast-util-from-markdown: 1.3.1 + unist-util-visit: 4.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /@monaco-editor/loader@1.3.3(monaco-editor@0.43.0): resolution: {integrity: sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==} peerDependencies: @@ -5492,6 +5506,10 @@ packages: object-keys: 1.1.1 dev: true + /defu@6.1.2: + resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} + dev: false + /delaunator@5.0.0: resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==} dependencies: diff --git a/src/css/prose.css b/src/css/prose.css index f25faccf..ee973f1c 100644 --- a/src/css/prose.css +++ b/src/css/prose.css @@ -136,20 +136,94 @@ } .prose .callout { - @apply border border-zinc-100 bg-zinc-50 text-accent rounded-xl p-5 md:p-8 font-normal; - @apply -mx-5 md:-mx-8 my-10; + --callout-color: hsl(207, 14%, 62%); + --callout-background: ; + --callout-border: hsl(205, 15%, 33%); + --callout-radius: 8px; + --callout-padding: 2ch; + border-width: 1px; + border-style: solid; + border-color: var(--callout-border); + border-radius: var(--callout-radius); + background: var(--callout-background); + margin-top: 0.8em; + margin-bottom: 0.8em; +} + +.prose .callout code, +.prose .callout pre { + background-color: hsl(0, 0%, 100%, 0.1); + border: none; +} + +.prose .callout-content { + padding-left: var(--callout-padding); + padding-right: var(--callout-padding); +} + +.prose .callout-content:first-child, +.prose .callout-content:only-child { + margin-block-start: 0; +} + +.prose .callout-content:last-child, +.prose .callout-content:only-child { + margin-block-end: 0; +} + +.prose .callout-indicator { + display: flex; + align-items: center; + padding: var(--callout-padding) var(--callout-padding) 0; +} + +.prose .callout-hint { + margin-inline-end: calc(var(--callout-padding) / 4); + color: var(--callout-color); +} + +.prose .callout-title { + margin-inline-start: calc(var(--callout-padding) / 4); + color: var(--callout-color); + font-size: 0.9em; + font-weight: bold; + letter-spacing: 0.04em; +} + +.prose .callout-note { + --callout-background: hsla(0, 0%, 0%, 0.05); + --callout-color: hsl(0, 0%, 43%); + --callout-border: hsl(0, 0%, 0%, 0.25); +} + +:root.dark .prose .callout-note { + --callout-background: hsl(0, 0%, 100%, 0.05); + --callout-color: hsl(0, 0%, 43%); + --callout-border: hsl(0, 0%, 100%, 0.25); +} + +.prose .callout-commend { + --callout-background: hsl(153, 47%, 49%, 0.05); + --callout-color: hsl(153, 62%, 54%); + --callout-border: hsl(153, 47%, 49%, 0.25); } .prose .callout-warn { - @apply border-orange-200 bg-orange-100 text-orange-700; + --callout-background: hsl(45, 100%, 55%, 0.05); + --callout-color: hsl(45, 97%, 66%); + --callout-border: hsl(45, 100%, 55%, 0.25); } -.prose .callout-success { - @apply border-green-200 bg-green-100 text-green-700; +.prose .callout-deter { + --callout-background: hsl(353, 83%, 58%, 0.05); + --callout-color: hsl(341, 89%, 63%); + --callout-border: hsl(353, 83%, 58%, 0.25); } -.prose .callout-danger { - @apply border-red-200 bg-red-100 text-red-700; +.prose .callout-assert { + --callout-background: rgba(47, 131, 157, 0.05); + --callout-color: hsl(201, 89%, 63%); + --callout-border: hsl(195, 61%, 56%, 0.25); } .prose .code-wrapper { diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 8a94c038..da8372e6 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -29,6 +29,9 @@ import remarkParse from "remark-parse" import remarkRehype from "remark-rehype" import { unified } from "unified" +// @ts-expect-error +import remarkCalloutDirectives from "@microflash/remark-callout-directives" + import AdvancedImage from "~/components/ui/AdvancedImage" import { transformers } from "./embed-transformers" @@ -44,7 +47,6 @@ import { rehypeTable } from "./rehype-table" import { rehypeVideo } from "./rehype-video" import { rehypeWrapCode } from "./rehype-wrap-code" import { rehypeExternalLink } from "./rehyper-external-link" -import { remarkCallout } from "./remark-callout" import { remarkMermaid } from "./remark-mermaid" import { remarkPangu } from "./remark-pangu" import { remarkYoutube } from "./remark-youtube" @@ -121,9 +123,9 @@ export const renderPageContent = ( .use(remarkGfm, { singleTilde: false, }) - .use(remarkCallout) .use(remarkDirective) .use(remarkDirectiveRehype) + .use(remarkCalloutDirectives) .use(remarkYoutube) .use(remarkMermaid) .use(remarkMath, { diff --git a/src/markdown/remark-callout.ts b/src/markdown/remark-callout.ts deleted file mode 100644 index 17d38a90..00000000 --- a/src/markdown/remark-callout.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Root } from "mdast" -import type { Plugin } from "unified" -import { visit } from "unist-util-visit" - -const CALLOUT_RE = /^(TIP|WARN|SUCCESS|DANGER):(\n|$)/ - -export const remarkCallout: Plugin<[], Root> = () => (tree: Root) => { - visit(tree, { type: "blockquote" }, (node, index, parent) => { - if (node.type !== "blockquote") return - - const firstChild = node.children[0] - if (firstChild?.type === "paragraph") { - const paragraphFirstChild = firstChild.children[0] - if ( - paragraphFirstChild.type === "text" && - CALLOUT_RE.test(paragraphFirstChild.value) - ) { - const type = paragraphFirstChild.value.match(CALLOUT_RE)![1] - const newValue = paragraphFirstChild.value.replace(CALLOUT_RE, "") - // In the following case `newValue` will be empty empty - // We should remove the paragraph - // > TIP: - // > - // > some tip - if (!newValue) { - node.children.splice(0, 1) - } - paragraphFirstChild.value = newValue - const className = `callout callout-${type.toLowerCase()}` - node.data = { - ...node.data, - hProperties: { - class: className, - }, - } - } - } - }) -} - -export const allowedBlockquoteAttrs: Array<[string, ...string[]]> = [ - [ - "className", - "callout", - "callout-tip", - "callout-warn", - "callout-success", - "callout-danger", - ], -] diff --git a/src/markdown/sanitize-schema.ts b/src/markdown/sanitize-schema.ts index 2ac13587..44ea6c2f 100644 --- a/src/markdown/sanitize-schema.ts +++ b/src/markdown/sanitize-schema.ts @@ -1,7 +1,5 @@ import { defaultSchema } from "rehype-sanitize" -import { allowedBlockquoteAttrs } from "./remark-callout" - const scheme = { ...defaultSchema, tagNames: [ @@ -16,11 +14,12 @@ const scheme = { "audio", "source", "mermaid", + "aside", + ...["svg", "path", "circle"], ], attributes: { ...defaultSchema.attributes, "*": [...(defaultSchema.attributes?.["*"] || []), "className", "style"], - blockquote: allowedBlockquoteAttrs, video: ["src", "controls", "loop", "muted", "autoPlay", "playsInline"], audio: [ "src", @@ -35,6 +34,19 @@ const scheme = { ], source: ["src", "type"], iframe: ["src", "allowFullScreen", "frameborder", "allow"], + svg: [ + "xmlns", + "width", + "height", + "viewBox", + "fill", + "stroke", + "strokeLineCap", + "strokeLineJoin", + "strokeWidth", + ], + path: ["d", "fill"], + circle: ["cx", "cy", "r", "fill"], }, }