Commit 66234ae

HPCesia <me@hpcesia.com>
2025-02-12 07:43:39
feat: github blockquote icons
1 parent 16eb26d
src/plugins/remark-github-blockquote.ts
@@ -0,0 +1,153 @@
+import type { RemarkPlugin } from '@astrojs/markdown-remark';
+import type { BlockContent, Data, DefinitionContent } from 'mdast';
+import type { Node } from 'unist';
+import { visit } from 'unist-util-visit';
+
+type TextFilter = string | RegExp | ((title: string) => string);
+type ClassNames = string | string[];
+type ClassNameMap = ClassNames | ((title: string) => ClassNames);
+
+export interface Config {
+  titleFilter: TextFilter[];
+  titleTextMap: (title: string) => {
+    displayTitle: (string | Node)[];
+    checkedTitle: string;
+  };
+  dataMaps: {
+    title: (data: Data) => Data;
+    block: (data: Data) => Data;
+  };
+  classNameMaps: {
+    title: ClassNameMap;
+    block: ClassNameMap;
+  };
+}
+
+const defaultConfig: Config = {
+  titleFilter: [
+    /\[!NOTE(:.*?)?\]/,
+    /\[!TIP(:.*?)?\]/,
+    /\[!IMPORTANT(:.*?)?\]/,
+    /\[!CAUTION(:.*?)?\]/,
+    /\[!WARNING(:.*?)?\]/,
+  ],
+  titleTextMap: (title) => {
+    const [type, text] = title.substring(2, title.length - 1).split(':');
+    let icon: string;
+    switch (type) {
+      case 'NOTE':
+        icon = 'material-symbols--info-outline-rounded';
+        break;
+      case 'TIP':
+        icon = 'ic--outline-tips-and-updates';
+        break;
+      case 'IMPORTANT':
+        icon = 'material-symbols--chat-info-outline-rounded';
+        break;
+      case 'CAUTION':
+        icon = 'mdi--alert-octagon-outline';
+        break;
+      case 'WARNING':
+        icon = 'material-symbols--warning-rounded';
+        break;
+      default:
+        icon = 'material-symbols--info-outline-rounded';
+        break;
+    }
+    return {
+      displayTitle: [
+        {
+          type: 'element',
+          data: { hName: 'span', hProperties: { className: `icon-[${icon}]` } },
+        } as Node,
+        text || type,
+      ],
+      checkedTitle: type,
+    };
+  },
+  dataMaps: {
+    title: (data) => data,
+    block: (data) => data,
+  },
+  classNameMaps: {
+    title: 'admonition-title',
+    block: (title) => `admonition admonition-${title.toLowerCase()}`,
+  },
+};
+
+function nameFilter(fliters: TextFilter[]): (title: string) => boolean {
+  return (title) => {
+    for (const filter of fliters) {
+      if (typeof filter == 'string') {
+        if (title.startsWith(filter)) return true;
+      } else if (filter instanceof RegExp) {
+        if (filter.test(title)) return true;
+      } else if (typeof filter == 'function') {
+        if (filter(title)) return true;
+      }
+    }
+    return false;
+  };
+}
+
+function classNameMap(gen: ClassNameMap): (title: string) => string {
+  return (title) => {
+    const classNames = typeof gen == 'function' ? gen(title) : gen;
+    return typeof classNames == 'object' ? classNames.join(' ') : classNames;
+  };
+}
+
+export const remarkGithubBlockquote: RemarkPlugin<Partial<Config>[]> = function (...params) {
+  const providedConfig = params.reduce((a, b) => ({ ...a, ...b }), {});
+  const config = { ...defaultConfig, ...providedConfig };
+  return (tree) => {
+    visit(tree, (node) => {
+      if (node.type != 'blockquote') return;
+      const blockquote = node;
+      if (blockquote.children[0]?.type != 'paragraph') return;
+      const paragraph = blockquote.children[0];
+      if (paragraph.children[0]?.type != 'text') return;
+      const text = paragraph.children[0];
+      let title;
+      const titleEnd = text.value.indexOf('\n');
+      if (titleEnd < 0) {
+        if (paragraph.children.length > 1) {
+          if (paragraph.children.at(1)?.type == 'break') {
+            paragraph.children.splice(1, 1);
+          } else {
+            return;
+          }
+        }
+        title = text.value;
+        if (!nameFilter(config.titleFilter)(title)) return;
+        paragraph.children.shift();
+      } else {
+        const textBody = text.value.substring(titleEnd + 1);
+        title = text.value.substring(0, titleEnd);
+        const m = /[ \t\v\f\r]+$/.exec(title);
+        if (m) {
+          title = title.substring(0, title.length - m[0].length);
+        }
+        if (!nameFilter(config.titleFilter)(title)) return;
+        text.value = textBody;
+      }
+      const { displayTitle, checkedTitle } = config.titleTextMap(title);
+      const paragraphTitleTexts = displayTitle.map((text) =>
+        typeof text == 'string' ? { type: 'text', value: text } : text
+      );
+      const paragraphTitle = {
+        type: 'paragraph',
+        children: [...paragraphTitleTexts],
+        data: config.dataMaps.title({
+          hProperties: { className: classNameMap(config.classNameMaps.title)(checkedTitle) },
+        }),
+      } as BlockContent | DefinitionContent;
+      blockquote.children.unshift(paragraphTitle);
+      blockquote.data = config.dataMaps.block({
+        ...blockquote.data,
+        hProperties: { className: classNameMap(config.classNameMaps.block)(checkedTitle) },
+        hName: 'div',
+      });
+    });
+  };
+};
src/styles/global.css
@@ -6,6 +6,8 @@
     dark --prefersdark;
 }
 
+@plugin "@iconify/tailwind4";
+
 @plugin 'daisyui/theme' {
   name: 'light';
 
src/styles/markdown.css
@@ -240,7 +240,7 @@ article {
     @apply my-4 rounded-sm border-l-4;
 
     .admonition-title {
-      font-weight: bold;
+      @apply inline-flex items-center gap-1 text-lg font-bold;
     }
 
     &.admonition-note {
astro.config.mjs
@@ -2,6 +2,7 @@
 import { CDN } from './src/constants/cdn.mjs';
 import { rehypeWrapTables } from './src/plugins/rehype-wrap-tables.ts';
 import { remarkExcerpt } from './src/plugins/remark-excerpt.ts';
+import { remarkGithubBlockquote } from './src/plugins/remark-github-blockquote.ts';
 // import { remarkHeadingShift } from './src/plugins/remark-heading-shift.ts';
 import { remarkImageProcess } from './src/plugins/remark-image-process.ts';
 import { remarkReadingTime } from './src/plugins/remark-reading-time.ts';
@@ -14,7 +15,6 @@ import pagefind from 'astro-pagefind';
 import { defineConfig } from 'astro/config';
 import rehypeAutolinkHeadings from 'rehype-autolink-headings';
 import rehypeMathJaxCHtml from 'rehype-mathjax/chtml';
-import remarkGithubBlockQuote from 'remark-github-beta-blockquote-admonitions';
 import remarkMath from 'remark-math';
 
 // https://astro.build/config
@@ -43,17 +43,7 @@ export default defineConfig({
       remarkReadingTime,
       remarkExcerpt,
       remarkImageProcess,
-      // @ts-expect-error - types are not up to date
-      [
-        remarkGithubBlockQuote,
-        {
-          classNameMaps: {
-            block: (/** @type {string} */ title) =>
-              `admonition admonition-${title.toLowerCase()}`,
-            title: 'admonition-title',
-          },
-        },
-      ],
+      remarkGithubBlockquote,
     ],
     rehypePlugins: [
       rehypeHeadingIds,
package.json
@@ -17,6 +17,7 @@
     "@astrojs/mdx": "^4.0.8",
     "@astrojs/rss": "^4.0.11",
     "@astrojs/sitemap": "^3.2.1",
+    "@iconify-json/ic": "^1.2.2",
     "@iconify-json/material-symbols": "^1.2.14",
     "@iconify-json/mdi": "^1.2.3",
     "@tailwindcss/vite": "^4.0.6",
@@ -48,11 +49,13 @@
     "@astrojs/check": "^0.9.4",
     "@astrojs/ts-plugin": "^1.10.4",
     "@eslint/js": "^9.20.0",
+    "@iconify/tailwind4": "^1.0.3",
     "@trivago/prettier-plugin-sort-imports": "^5.2.2",
     "@types/hast": "^3.0.4",
     "@types/markdown-it": "^14.1.2",
     "@types/mdast": "^4.0.4",
     "@types/sanitize-html": "^2.13.0",
+    "@types/unist": "^3.0.3",
     "@typescript-eslint/parser": "^8.24.0",
     "astro-eslint-parser": "^1.2.1",
     "eslint": "^9.20.0",