mirror of
https://codeberg.org/HPCesia/AstralHalo.git
synced 2025-04-08 17:34:27 +08:00
feat: use MDX instead of directives
This commit is contained in:
parent
b5bc27a7a3
commit
f218cc88a2
@ -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,
|
||||
{
|
||||
|
@ -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",
|
||||
|
19
src/components/user/TabItem.astro
Normal file
19
src/components/user/TabItem.astro
Normal 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>
|
62
src/components/user/Tabs.astro
Normal file
62
src/components/user/Tabs.astro
Normal 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>
|
@ -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({
|
||||
|
@ -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[].
|
||||
|
||||
::tab[Code of Examples]
|
||||
|
||||
```md
|
||||
This is an inline img example: :inline[].
|
||||
```
|
||||
|
||||
:::
|
110
src/content/posts/markdown-components.mdx
Normal file
110
src/content/posts/markdown-components.mdx
Normal 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>
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
7
src/scripts/stores.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { map } from 'nanostores';
|
||||
|
||||
export interface SyncTabs {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export const syncTabs = map<SyncTabs>();
|
Loading…
Reference in New Issue
Block a user