Commit d6dd2a7

HPCesia <me@hpcesia.com>
2025-02-11 08:54:49
feat: markdown component
- use `remark-directive` and `rehype-components` to render markdown components. - add `tabs` component.
1 parent 544f2b1
src/plugins/components/tabs.mjs
@@ -0,0 +1,70 @@
+/// <reference types="mdast" />
+import { h } from 'hastscript';
+
+/**
+ * Create a Tabs component.
+ *
+ * @param {Object} props - The properties of the component.
+ * @param {import('mdast').RootContent[]} children - The children elements of the component.
+ * @returns {import('mdast').Parent} The created Tabs component.
+ */
+export function componentTabs(props, children) {
+  if (!Array.isArray(children) || children.length === 0) {
+    return h(
+      'div',
+      { class: 'hidden' },
+      'Invalid directive. ("tabs" directive must container type and have at least one tab.)'
+    );
+  }
+  if (children[0].tagName !== 'tab') {
+    return h(
+      'div',
+      { class: 'hidden' },
+      'Invalid directive. ("tabs" directive must container type with "tab" leaf directive as first child.)'
+    );
+  }
+
+  const { result: tabs } = children.reduce(
+    (acc, child, index) => {
+      if (child.tagName === 'tab') {
+        if (acc.temp.title !== '') {
+          acc.result.push(acc.temp);
+          acc.temp = { title: '', content: [] };
+        }
+        acc.temp.title =
+          child.children.length > 0 ? child.children[0].value : `Tab ${acc.result.length + 1}`;
+        if ('active' in child.properties) {
+          acc.temp.active = true;
+        }
+        return acc;
+      }
+      acc.temp.content.push(child);
+      if (index === children.length - 1) acc.result.push(acc.temp);
+
+      return acc;
+    },
+    {
+      temp: {
+        title: '',
+        active: false,
+        content: [],
+      },
+      result: [],
+    }
+  );
+
+  const tabsId = `tabs-${Math.random().toString(36).substring(2, 15)}`;
+  const tabsContent = tabs.flatMap((tab) => {
+    const tabTitle = h('input', {
+      class: 'tab',
+      type: 'radio',
+      name: tabsId,
+      'aria-label': tab.title,
+      checked: tab.active ? 'checked' : undefined,
+    });
+    const tabContent = h('div', { class: 'tab-content p-4' }, ...tab.content);
+    return [tabTitle, tabContent];
+  });
+
+  return h('div', { class: 'tabs tabs-box bg-base-200/15 my-4' }, tabsContent);
+}
src/plugins/remark-prase-directive.mjs
@@ -0,0 +1,37 @@
+/**
+ * @import {} from 'mdast-util-directive'
+ * @import {Root} from 'mdast'
+ */
+import { h } from 'hastscript';
+import { visit } from 'unist-util-visit';
+
+export function parseDirectiveNodes() {
+  /**
+   * @param {Root} tree
+   *   Tree.
+   * @returns {undefined}
+   *   Nothing.
+   */
+  return (tree) => {
+    visit(tree, (node) => {
+      if (
+        node.type === 'containerDirective' ||
+        node.type === 'leafDirective' ||
+        node.type === 'textDirective'
+      ) {
+        const data = node.data || (node.data = {});
+        node.attributes = node.attributes || {};
+        if (
+          node.children.length > 0 &&
+          node.children[0].data &&
+          node.children[0].data.directiveLabel
+        ) {
+          node.attributes['has-directive-label'] = true;
+        }
+        const hast = h(node.name, node.attributes);
+        data.hName = hast.tagName;
+        data.hProperties = hast.properties;
+      }
+    });
+  };
+}
astro.config.mjs
@@ -1,7 +1,9 @@
 // @ts-check
 import { CDN } from './src/constants/cdn.mjs';
+import { componentTabs } from './src/plugins/components/tabs.mjs';
 import { rehypeWrapTables } from './src/plugins/rehype-wrap-tables.mjs';
 import { remarkExcerpt } from './src/plugins/remark-excerpt';
+import { parseDirectiveNodes } from './src/plugins/remark-prase-directive.mjs';
 import { remarkReadingTime } from './src/plugins/remark-reading-time.mjs';
 import { rehypeHeadingIds } from '@astrojs/markdown-remark';
 import sitemap from '@astrojs/sitemap';
@@ -10,7 +12,9 @@ import icon from 'astro-icon';
 import pagefind from 'astro-pagefind';
 import { defineConfig } from 'astro/config';
 import rehypeAutolinkHeadings from 'rehype-autolink-headings';
+import rehypeComponents from 'rehype-components';
 import rehypeMathJaxCHtml from 'rehype-mathjax/chtml';
+import remarkDirective from 'remark-directive';
 import remarkGithubBlockQuote from 'remark-github-beta-blockquote-admonitions';
 import remarkMath from 'remark-math';
 
@@ -49,9 +53,17 @@ export default defineConfig({
           },
         },
       ],
+      remarkDirective,
+      parseDirectiveNodes,
     ],
     rehypePlugins: [
       rehypeHeadingIds,
+      [
+        rehypeComponents,
+        {
+          components: { tabs: componentTabs },
+        },
+      ],
       [
         rehypeAutolinkHeadings,
         {
package.json
@@ -26,12 +26,15 @@
     "autoprefixer": "^10.4.20",
     "daisyui": "5.0.0-beta.7",
     "dayjs": "^1.11.13",
+    "hastscript": "^9.0.0",
     "markdown-it": "^14.1.0",
     "mdast-util-to-string": "^4.0.0",
     "postcss-load-config": "^6.0.1",
     "reading-time": "^1.5.0",
     "rehype-autolink-headings": "^7.1.0",
+    "rehype-components": "^0.3.0",
     "rehype-mathjax": "^6.0.0",
+    "remark-directive": "^3.0.1",
     "remark-github-beta-blockquote-admonitions": "^3.1.1",
     "remark-math": "^6.0.0",
     "sanitize-html": "^2.14.0",
@@ -46,6 +49,7 @@
     "@eslint/js": "^9.20.0",
     "@trivago/prettier-plugin-sort-imports": "^5.2.2",
     "@types/markdown-it": "^14.1.2",
+    "@types/mdast": "^4.0.4",
     "@types/sanitize-html": "^2.13.0",
     "@typescript-eslint/parser": "^8.23.0",
     "astro-eslint-parser": "^1.2.1",