feat: use MDX instead of directives

This commit is contained in:
HPCesia 2025-02-12 12:17:50 +08:00
parent b5bc27a7a3
commit f218cc88a2
11 changed files with 204 additions and 303 deletions

View File

@ -1,22 +1,18 @@
// @ts-check
import { CDN } from './src/constants/cdn.mjs';
import { componentInline } from './src/plugins/components/inline.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 { remarkImageProcess } from './src/plugins/remark-image-process.mjs';
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 mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
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';
@ -30,6 +26,7 @@ export default defineConfig({
icon(),
sitemap({ filter: (page) => !page.includes('/archives/') && !page.includes('/about/') }),
pagefind(),
mdx(),
],
markdown: {
shikiConfig: {
@ -56,17 +53,9 @@ export default defineConfig({
},
},
],
remarkDirective,
parseDirectiveNodes,
],
rehypePlugins: [
rehypeHeadingIds,
[
rehypeComponents,
{
components: { tabs: componentTabs, inline: componentInline },
},
],
[
rehypeAutolinkHeadings,
{

View File

@ -14,6 +14,7 @@
},
"dependencies": {
"@astrojs/markdown-remark": "^6.1.0",
"@astrojs/mdx": "^4.0.8",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@iconify-json/material-symbols": "^1.2.14",
@ -30,12 +31,11 @@
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"medium-zoom": "^1.1.0",
"nanostores": "^0.11.3",
"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",

View File

@ -0,0 +1,19 @@
---
interface Props {
label: string;
active?: boolean;
}
const { label, active } = Astro.props;
---
<input
type="radio"
class="tab"
name="{{{tabs-name}}}"
aria-label={label}
{...active ? { checked: 'checked' } : {}}
/>
<div class="tab-content p-4">
<slot />
</div>

View File

@ -0,0 +1,62 @@
---
interface Props {
syncKey?: string;
}
const syncKey = Astro.props.syncKey;
const tabsName = `tabs-${Math.random().toString(36).substring(2, 9)}`;
const html = (await Astro.slots.render('default')).replaceAll(/{{{tabs-name}}}/g, tabsName);
---
<div
class="tabs tabs-box bg-base-200/15 my-4 shadow"
{...syncKey ? { 'data-sync-key': syncKey } : {}}
>
<Fragment set:html={html} />
</div>
<script>
import { syncTabs } from '@scripts/stores';
import { listenKeys } from 'nanostores';
function init() {
const tabsNeedSync = document.querySelectorAll('.tabs[data-sync-key]');
tabsNeedSync.forEach((tab) => {
const syncKey = tab.getAttribute('data-sync-key')!;
const tabItems: NodeListOf<HTMLInputElement> = tab.querySelectorAll(
':scope > input[type="radio"]'
);
if (syncKey in syncTabs.get()) {
const activeTabIndex = syncTabs.get()[syncKey];
if (activeTabIndex === -1) {
tabItems.forEach((tab) => tab.removeAttribute('checked'));
} else {
tabItems[activeTabIndex].setAttribute('checked', 'checked');
}
} else {
const activeTabIndex = Array.from(tabItems).findIndex((tab) => tab.checked);
console.log(syncKey, 'init', activeTabIndex);
syncTabs.setKey(syncKey, activeTabIndex);
}
tabItems.forEach((tab, index) => {
tab.addEventListener('change', () => {
if (tab.checked) {
syncTabs.setKey(syncKey, index);
}
});
});
listenKeys(syncTabs, [syncKey], (curr) => {
const activeTabIndex = curr[syncKey];
if (activeTabIndex === -1) {
tabItems.forEach((tab) => (tab.checked = false));
} else {
tabItems[activeTabIndex].checked = true;
}
});
});
}
document.addEventListener('astro:after-swap', init);
init();
</script>

View File

@ -3,7 +3,7 @@ import { defineCollection, z } from 'astro:content';
const postsCollection = defineCollection({
loader: glob({
pattern: '**/*.md',
pattern: '**/*.{md,mdx}',
base: 'src/content/posts',
}),
schema: z.object({
@ -23,7 +23,7 @@ const postsCollection = defineCollection({
const specCollection = defineCollection({
loader: glob({
pattern: '**/*.md',
pattern: '**/*.{md,mdx}',
base: 'src/content/spec',
}),
schema: z.object({

View File

@ -1,138 +0,0 @@
---
title: Markdown Extensions
slug: q1k423y0
category: Example
tags:
- markdown
- example
published: 2025-02-10T21:23:23+08:00
---
This post demonstrates the use of markdown extensions.
## Directives
With [remark-directive](https://github.com/remarkjs/remark-directive), you can use three types of directives instead of HTML comments in markdown files. For example, you can generate a details element with a summary element as follows:
::::details
::summary[This is a collapsible content]
And to create this:sup[:a[1]{#fake-footnote-here data-footnote-ref href="#fake-footnote-there"}], you can use the following markdown:
````markdown
:::details
::summary[This is a collapsible content]
And to create this:sup[:a[1]{#fake-footnote-here data-footnote-ref href="#fake-footnote-there"}], you can use the following markdown:
```markdown
An infinite loop! So there is no need to write the markdown code again and again here.
```
:a[1: fake footnote here.]{#fake-footnote-there data-footnote-backref href="#fake-footnote-here"}
:::
````
:a[1: fake footnote here.]{#fake-footnote-there data-footnote-backref href="#fake-footnote-here"}
::::
Instead of using HTML comments:
```html
<details>
<summary>This is a collapsible content</summary>
You can't use markdown here. To create code blocks, you need to use HTML like this:
<pre>
<code>
<span class="line">HTML Tags Hell!</span>
</code>
</pre>
</details>
```
## Components
Using directives, you can write HTML with markdown inside. But to generate complex components, it's a bit tedious, not elegant, and not easy to maintain. It's another hell of HTML tags[^1]. Fortunately, with [rehype-components](https://github.com/marekweb/rehype-components), you can use pre-defined components in markdown files.
[^1]: If you want to know how hellish it is, check the [source code](https://github.com/HPCesia/astral-halo/tree/master/src/content/posts/Markdown-Extensions.md) of this post.
### Tabs
::::tabs
::tab[Component Syntax]
```md
:::tabs
::tab[title]{active}
[content]
::tab[title]
[content]
:::
```
::tab[Parameter Description]
- `title`: The title of the tab.
- `active`: The tab is active by default, should only be used once in a tabs group.
- `content`: The content of the tab.
::tab[Component Examples]{active}
:::tabs
::tab[Tab Example 1]{active}
This is the content of tab 1.
::tab[Tab Example 2]
This is the content of tab 2.
:::
:::tabs
::tab
This is the content of tab 1.
::tab{active}
If no title, will automatically use `Tab [index]` as title.
:::
::tab[Code of Examples]
```md
:::tabs
::tab[Tab 1]{active}
This is the content of tab 1.
::tab[Tab 2]
This is the content of tab 2.
:::
```
```md
:::tabs
::tab
This is the content of tab 1.
::tab{active}
If no title, will automatically use `Tab [index]` as title.
:::
```
::::
### Inline
:::tabs
::tab[Component Syntax]
```md
:inline[content]
```
::tab[Parameter Description]
- `content`: The content of the inline component, will add `inline` class to the rendered content.
::tab[Component Examples]{active}
This is an inline img example: :inline[![Inline Image](/favicon/favicon-192x192.png)].
::tab[Code of Examples]
```md
This is an inline img example: :inline[![Inline Image](/favicon/favicon-192x192.png)].
```
:::

View File

@ -0,0 +1,110 @@
---
title: Markdown Components
slug: q1k423y0
category: Example
tags:
- markdown
- example
published: 2025-02-10T21:23:23+08:00
---
import TabItem from '@components/user/TabItem.astro';
import Tabs from '@components/user/Tabs.astro';
## Tabs
<Tabs>
<TabItem label="Component Syntax">
```jsx
<Tabs syncKey={syncKey}>
<TabItem label={tabsTitle} active={isActive}>
{content}
</TabItem>
</Tabs>
```
</TabItem>
<TabItem label="Parameter Description">
- `syncKey`: The key to sync tabs.
- `tabsTitle`: The title of the tab.
- `isActive`: The active tab.
- `content`: The content of the tab.
</TabItem>
<TabItem label="Component Examples" active={true}>
<Tabs>
<TabItem label="Foo">
This is the content of common Tab 1
</TabItem>
<TabItem label="Bar" active={true}>
This is the content of common Tab 2
</TabItem>
</Tabs>
<Tabs>
<TabItem label="Lorem">
This is the content of spoiler Tab 1
</TabItem>
<TabItem label="Ipsum">
This is the content of spoiler Tab 2
</TabItem>
</Tabs>
<Tabs syncKey="test-tabs-sync">
<TabItem label="Tab 1" active={true}>
This is the content of sync Tab 1, sync with next Tabs.
</TabItem>
<TabItem label="Tab 2">
This is the content of sync Tab 2, sync with next Tabs.
</TabItem>
</Tabs>
<Tabs syncKey="test-tabs-sync">
<TabItem label="NPM" active={true}>
```bash
npm install foo bar
```
</TabItem>
<TabItem label="PNPM">
```bash
pnpm add foo bar
```
</TabItem>
</Tabs>
</TabItem>
<TabItem label='Code of Examples'>
````jsx
<Tabs>
<TabItem label="Foo">
This is the content of common Tab 1
</TabItem>
<TabItem label="Bar" active={true}>
This is the content of common Tab 2
</TabItem>
</Tabs>
<Tabs>
<TabItem label="Lorem">
This is the content of spoiler Tab 1
</TabItem>
<TabItem label="Ipsum">
This is the content of spoiler Tab 2
</TabItem>
</Tabs>
<Tabs syncKey="test-tabs-sync">
<TabItem label="Tab 1" active={true}>
This is the content of sync Tab 1, sync with next Tabs.
</TabItem>
<TabItem label="Tab 2">
This is the content of sync Tab 2, sync with next Tabs.
</TabItem>
</Tabs>
<Tabs syncKey="test-tabs-sync">
<TabItem label="NPM" active={true}>
```bash
npm install foo bar
```
</TabItem>
<TabItem label="PNPM">
```bash
pnpm add foo bar
```
</TabItem>
</Tabs>
````
</TabItem>
</Tabs>

View File

@ -1,41 +0,0 @@
/// <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 componentInline(props, children) {
if (!Array.isArray(children)) {
return h(
'span',
{ class: 'hidden' },
'Invalid directive. ("inline" directive must be a text directive with child.)'
);
}
if (children.length !== 1) {
return h(
'span',
{ class: 'hidden' },
'Invalid directive. ("inline" directive must be a text directive with child.)'
);
}
const child = children[0];
if (child.tagName === 'img') {
delete child.properties['data-zoom'];
}
const classes = [
...new Set([
...('class' in child.properties ? child.properties.class : '').split(' '),
'inline',
]),
].join(' ');
child.properties.class = classes;
return h(child.tagName, child.properties, child.children);
}

View File

@ -1,70 +0,0 @@
/// <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);
}

View File

@ -1,37 +0,0 @@
/**
* @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;
}
});
};
}

7
src/scripts/stores.ts Normal file
View File

@ -0,0 +1,7 @@
import { map } from 'nanostores';
export interface SyncTabs {
[key: string]: number;
}
export const syncTabs = map<SyncTabs>();