Skip to content

Commit 068bf4e

Browse files
committed
Add :::steps and :::note markdown directives
1 parent 5002cd4 commit 068bf4e

10 files changed

Lines changed: 325 additions & 11 deletions

File tree

docs/mdsx.config.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ import { defineConfig } from 'mdsx';
22

33
import rehypeSlug from 'rehype-slug';
44
import remarkGfm from 'remark-gfm';
5+
import remarkDirective from 'remark-directive';
56
import rehypePrettyCode from 'rehype-pretty-code';
67

78
import {
89
prettyCodeOptions,
910
rehypeCodeBlockTitle,
1011
rehypeHandleCodeBlocks,
11-
remarkLiveCode
12+
remarkLiveCode,
13+
remarkDirectives
1214
} from './src/lib/markdown/config/index.js';
1315

1416
export const mdsxConfig = defineConfig({
1517
extensions: ['.md'],
16-
remarkPlugins: [remarkGfm, remarkLiveCode],
18+
remarkPlugins: [
19+
remarkGfm,
20+
remarkDirective, // Parse directive syntax (:::directive)
21+
remarkDirectives, // Transform directives to components
22+
remarkLiveCode
23+
],
1724
rehypePlugins: [
1825
rehypeSlug,
1926
// rehypeComponentExample,

docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"prettier-plugin-svelte": "^3.4.0",
9494
"rehype-pretty-code": "^0.14.1",
9595
"rehype-slug": "^6.0.0",
96+
"remark-directive": "^3.0.0",
9697
"remark-gfm": "^4.0.1",
9798
"runed": "^0.37.0",
9899
"shiki": "^3.20.0",

docs/src/app.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,24 @@ pre {
137137
*::-webkit-scrollbar-thumb:active {
138138
@apply bg-surface-content/40;
139139
}
140+
141+
/* Steps component styling - inspired by Docus */
142+
.steps {
143+
@apply ms-4 ps-8 border-l border-surface-content/10;
144+
counter-reset: step;
145+
146+
/* Headings (h2, h3, h4) in steps */
147+
& :is(h2, h3, h4) {
148+
counter-increment: step;
149+
@apply relative font-semibold text-base mb-2 mt-6 first:mt-0;
150+
151+
/* Counter circle */
152+
&::before {
153+
content: counter(step);
154+
@apply absolute size-6 -start-[45px] bg-surface-100 rounded-full;
155+
@apply font-semibold text-sm tabular-nums;
156+
@apply inline-flex items-center justify-center;
157+
@apply ring-1 ring-surface-content/20;
158+
}
159+
}
160+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from 'svelte/elements';
3+
import { cls } from '@layerstack/tailwind';
4+
5+
import LucideInfo from '~icons/lucide/info';
6+
import LucideLightbulb from '~icons/lucide/lightbulb';
7+
import LucideTriangleAlert from '~icons/lucide/triangle-alert';
8+
import LucideOctagonAlert from '~icons/lucide/octagon-alert';
9+
10+
interface Props extends HTMLAttributes<HTMLDivElement> {
11+
variant?: 'note' | 'tip' | 'warning' | 'caution';
12+
}
13+
14+
const { children, class: className, variant = 'note', ...restProps }: Props = $props();
15+
16+
const variants = {
17+
note: { color: 'var(--color-blue-500)', Icon: LucideInfo },
18+
tip: { color: 'var(--color-green-500)', Icon: LucideLightbulb },
19+
warning: { color: 'var(--color-amber-500)', Icon: LucideTriangleAlert },
20+
caution: { color: 'var(--color-red-500)', Icon: LucideOctagonAlert }
21+
};
22+
23+
const { color, Icon } = $derived(variants[variant]);
24+
</script>
25+
26+
<div
27+
class={cls(
28+
'border border-l-[6px] px-4 py-2 my-4 rounded-sm flex items-center gap-2 text-sm',
29+
'bg-(--color)/10 border-(--color)/50',
30+
className
31+
)}
32+
style:--color={color}
33+
{...restProps}
34+
>
35+
<Icon class="shrink-0 text-(--color)" />
36+
{@render children?.()}
37+
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
let { children }: { children: Snippet } = $props();
5+
</script>
6+
7+
<div class="steps mt-6">
8+
{@render children?.()}
9+
</div>

docs/src/lib/markdown/components/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export { default as td } from './td.svelte';
1515
export { default as th } from './th.svelte';
1616
export { default as tr } from './tr.svelte';
1717
export { default as ul } from './ul.svelte';
18+
19+
// Directive components
20+
export { default as Note } from './Note.svelte';
21+
export { default as Steps } from './Steps.svelte';

docs/src/lib/markdown/config/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Remark plugins
22
export { remarkLiveCode } from '../rehype/live-code.js';
3+
export { remarkDirectives } from '../remark/directives.js';
34

45
// Rehype plugins
56
export { rehypeCodeBlockTitle } from '../rehype/code-block-title.js';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { visit } from 'unist-util-visit';
2+
3+
/**
4+
* Remark plugin to transform directives (:::tip, :::note, :::steps, etc.) into custom components
5+
* Works with remark-directive to convert container directives into Svelte components
6+
*
7+
* Supported directives:
8+
* - :::tip - renders as Note component with variant="tip"
9+
* - :::note - renders as Note component with variant="note"
10+
* - :::warning - renders as Note component with variant="warning"
11+
* - :::caution - renders as Note component with variant="caution"
12+
* - :::steps - renders as Steps component
13+
*
14+
* @returns {(tree: any) => void} A remark transformer function
15+
*/
16+
export function remarkDirectives() {
17+
return (tree) => {
18+
const componentsToImport = new Set();
19+
20+
visit(tree, (node) => {
21+
// Handle container directives (:::directive)
22+
if (node.type === 'containerDirective') {
23+
const directiveName = node.name;
24+
25+
// Alert variants all use the Note component
26+
const alertVariants = ['tip', 'note', 'warning', 'caution'];
27+
28+
// Map directive names to component names and variants
29+
let componentName;
30+
let variant;
31+
32+
if (alertVariants.includes(directiveName)) {
33+
componentName = 'Note';
34+
variant = directiveName;
35+
} else if (directiveName === 'steps') {
36+
componentName = 'Steps';
37+
} else {
38+
// Unknown directive, skip
39+
return;
40+
}
41+
42+
// Track which components we need to import
43+
componentsToImport.add(componentName);
44+
45+
// Get directive attributes
46+
const attributes = node.attributes || {};
47+
48+
// Convert the directive into a custom component in the HTML tree
49+
// We set data.hName to tell rehype to convert this to the component
50+
const data = node.data || (node.data = {});
51+
data.hName = componentName;
52+
data.hProperties = {
53+
...attributes,
54+
// Pass variant for alert components
55+
...(variant && { variant }),
56+
// Pass any directive label as a prop
57+
...(node.attributes?.label && { label: node.attributes.label })
58+
};
59+
}
60+
61+
// Handle text directives (:directive[text]) if needed
62+
if (node.type === 'textDirective') {
63+
// Can be used for inline directives if needed in the future
64+
}
65+
66+
// Handle leaf directives (::directive) if needed
67+
if (node.type === 'leafDirective') {
68+
// Can be used for self-closing directives if needed in the future
69+
}
70+
});
71+
72+
// Inject component imports at the beginning of the file
73+
if (componentsToImport.size > 0) {
74+
const componentArray = Array.from(componentsToImport);
75+
const importStatements = componentArray
76+
.map((comp) => `import ${comp} from '$lib/markdown/components/${comp}.svelte';`)
77+
.join('\n');
78+
79+
// Check if there's already a script tag
80+
let hasScript = false;
81+
visit(tree, 'html', (node) => {
82+
if (node.value.startsWith('<script') && !hasScript) {
83+
hasScript = true;
84+
node.value = node.value.replace(
85+
/<script[^>]*>/,
86+
(match) => `${match}\n${importStatements}`
87+
);
88+
return visit.EXIT;
89+
}
90+
});
91+
92+
if (!hasScript) {
93+
// Create new script tag at the beginning
94+
tree.children.unshift({
95+
type: 'html',
96+
value: `<script>\n${importStatements}\n</script>`
97+
});
98+
}
99+
}
100+
};
101+
}

docs/src/routes/docs/markdown/+page.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,50 @@
127127
| ----- | ------ | ----- |
128128
| 1 | 2 | 3 |
129129
| 4 | 5 | 6 |
130+
131+
## Directives
132+
133+
### Note
134+
135+
:::note
136+
Here's some additional information. This uses the new `:::note` directive syntax!
137+
:::
138+
139+
### Tip
140+
141+
:::tip
142+
Here's a helpful suggestion using the `:::tip` directive.
143+
:::
144+
145+
### Warning
146+
147+
:::warning
148+
Be careful with this action as it might have unexpected results. This uses `:::warning`.
149+
:::
150+
151+
### Caution
152+
153+
:::caution
154+
This action cannot be undone. This uses `:::caution`.
155+
:::
156+
157+
### Steps
158+
159+
:::steps
160+
161+
## Step 1: Install dependencies
162+
163+
First, install the required packages:
164+
165+
```bash
166+
npm install remark-directive
167+
```
168+
169+
## Step 2: Configure mdsx
170+
171+
Add the directive plugins to your mdsx configuration.
172+
173+
## Step 3: Use directives
174+
175+
Start using `:::directive` syntax in your markdown files!
176+
:::

0 commit comments

Comments
 (0)