Commit f218cc8
Changed files (11)
src
components
user
content
plugins
components
scripts
src/components/user/TabItem.astro
@@ -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>
src/components/user/Tabs.astro
@@ -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>
src/content/posts/markdown-components.mdx
@@ -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>
src/content/posts/Markdown-Extensions.md
@@ -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[].
-```
-
-:::
src/plugins/components/inline.mjs
@@ -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);
-}
src/plugins/components/tabs.mjs
@@ -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);
-}
src/plugins/remark-prase-directive.mjs
@@ -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;
- }
- });
- };
-}
src/scripts/stores.ts
@@ -0,0 +1,7 @@
+import { map } from 'nanostores';
+
+export interface SyncTabs {
+ [key: string]: number;
+}
+
+export const syncTabs = map<SyncTabs>();
src/content.config.ts
@@ -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({
astro.config.mjs
@@ -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,
{
package.json
@@ -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",