@fusion.dev/helix 0.106.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112209,6 +112209,21 @@
112209
112209
  }
112210
112210
  ]
112211
112211
  },
112212
+ {
112213
+ "kind": "javascript-module",
112214
+ "path": "src/rehype.ts",
112215
+ "declarations": [],
112216
+ "exports": [
112217
+ {
112218
+ "kind": "js",
112219
+ "name": "rehypeHelix",
112220
+ "declaration": {
112221
+ "name": "rehypeHelix",
112222
+ "module": "src/rehype.ts"
112223
+ }
112224
+ }
112225
+ ]
112226
+ },
112212
112227
  {
112213
112228
  "kind": "javascript-module",
112214
112229
  "path": "src/sandbox/index.ts",
@@ -0,0 +1,54 @@
1
+ /**
2
+ * **A rehype plugin that gives Markdown's two missing constructs their Helix
3
+ * elements, and parses nothing.** Your Markdown library turns `###` into
4
+ * `<h3>`, and `data-prose` styles the HTML it writes. Two constructs carry
5
+ * what plain HTML cannot: a code fence, which a reader copies and which has a
6
+ * language, and a GitHub alert, which says how much a passage matters. This
7
+ * rewrites those two in the tree your parser already built, after your own
8
+ * sanitizing, and leaves everything else as it is.
9
+ *
10
+ * - A fence becomes `helix-code-block`, labeled with its language. What is
11
+ * inside the code is kept, so a highlighter that ran before this has its
12
+ * colors kept too.
13
+ * - An alert, `> [!NOTE]` through `> [!CAUTION]`, becomes `helix-callout`: a
14
+ * note is `info`, a tip `success`, a warning `warning`, and a caution
15
+ * `danger`. An important passage has no tone, since none of the four says
16
+ * it. Each keeps its kind as its heading, as GitHub shows it.
17
+ *
18
+ * Any pipeline that reaches rehype takes it: `remark` with `remark-rehype`,
19
+ * `react-markdown`, and MDX.
20
+ *
21
+ * ```js
22
+ * import rehypeHelix from '@fusion.dev/helix/rehype'
23
+ *
24
+ * unified().use(remarkParse).use(remarkRehype).use(rehypeHelix).use(rehypeStringify)
25
+ * ```
26
+ */
27
+ /** A node of the HTML tree, as far as this plugin reads one. */
28
+ interface HastNode {
29
+ children?: HastNode[];
30
+ properties?: Record<string, unknown>;
31
+ tagName?: string;
32
+ type: string;
33
+ value?: string;
34
+ }
35
+ /** The five kinds of GitHub alert. */
36
+ export type AlertKind = 'caution' | 'important' | 'note' | 'tip' | 'warning';
37
+ /** What the plugin rewrites, and what it calls each alert. */
38
+ export interface RehypeHelixOptions {
39
+ /** Whether alerts become callouts. `true` by default. */
40
+ alerts?: boolean | undefined;
41
+ /**
42
+ * Whether fences become code blocks. `true` by default; `false` where the
43
+ * page draws its own fences, as a site that highlights them itself does.
44
+ */
45
+ fences?: boolean | undefined;
46
+ /** An alert's heading by its kind, for a page in another language. English by default. */
47
+ headings?: Partial<Record<AlertKind, string>> | undefined;
48
+ }
49
+ /**
50
+ * The plugin: rewrites the tree in place, from the top down, so a fence
51
+ * inside an alert becomes a code block inside a callout.
52
+ */
53
+ export default function rehypeHelix(options?: RehypeHelixOptions): (tree: HastNode) => void;
54
+ export { rehypeHelix };
package/dist/rehype.js ADDED
@@ -0,0 +1,100 @@
1
+ const TONES = {
2
+ caution: 'danger',
3
+ important: undefined,
4
+ note: 'info',
5
+ tip: 'success',
6
+ warning: 'warning',
7
+ };
8
+ const HEADINGS = {
9
+ caution: 'Caution',
10
+ important: 'Important',
11
+ note: 'Note',
12
+ tip: 'Tip',
13
+ warning: 'Warning',
14
+ };
15
+ const MARKER = /^\s*\[!(caution|important|note|tip|warning)\][ \t]*(?:\r?\n|$)/i;
16
+ const blank = (node) => node.type === 'text' && !node.value?.trim();
17
+ const elements = (node) => (node.children ?? []).filter((child) => !blank(child));
18
+ const languageOf = (code) => {
19
+ const names = code.properties?.className;
20
+ const list = Array.isArray(names) ? names : typeof names === 'string' ? names.split(/\s+/) : [];
21
+ const found = list.find((name) => typeof name === 'string' && name.startsWith('language-'));
22
+ return typeof found === 'string' ? found.slice('language-'.length) : undefined;
23
+ };
24
+ const codeBlockOf = (node) => {
25
+ const inside = elements(node);
26
+ const code = inside[0];
27
+ if (node.tagName !== 'pre' || inside.length !== 1 || code?.tagName !== 'code') {
28
+ return undefined;
29
+ }
30
+ const children = [...(code.children ?? [])];
31
+ const last = children.at(-1);
32
+ if (last?.type === 'text' && last.value?.endsWith('\n')) {
33
+ children[children.length - 1] = { ...last, value: last.value.slice(0, -1) };
34
+ }
35
+ const language = languageOf(code);
36
+ return {
37
+ children,
38
+ properties: language ? { label: language } : {},
39
+ tagName: 'helix-code-block',
40
+ type: 'element',
41
+ };
42
+ };
43
+ const calloutOf = (node, headings) => {
44
+ if (node.tagName !== 'blockquote') {
45
+ return undefined;
46
+ }
47
+ const inside = elements(node);
48
+ const first = inside[0];
49
+ const text = first?.tagName === 'p' ? first.children?.[0] : undefined;
50
+ const match = text?.type === 'text' ? MARKER.exec(text.value ?? '') : null;
51
+ if (!first || !text || !match) {
52
+ return undefined;
53
+ }
54
+ const kind = (match[1] ?? '').toLowerCase();
55
+ const rest = (text.value ?? '').slice(match[0].length);
56
+ const paragraph = {
57
+ ...first,
58
+ children: [...(rest ? [{ ...text, value: rest }] : []), ...(first.children ?? []).slice(1)],
59
+ };
60
+ if (paragraph.children?.[0]?.tagName === 'br') {
61
+ paragraph.children = paragraph.children.slice(1);
62
+ }
63
+ const body = [...(elements(paragraph).length > 0 ? [paragraph] : []), ...inside.slice(1)];
64
+ const tone = TONES[kind];
65
+ return {
66
+ children: [
67
+ {
68
+ children: [{ type: 'text', value: headings[kind] }],
69
+ properties: { slot: 'heading' },
70
+ tagName: 'span',
71
+ type: 'element',
72
+ },
73
+ ...body,
74
+ ],
75
+ properties: tone ? { tone } : {},
76
+ tagName: 'helix-callout',
77
+ type: 'element',
78
+ };
79
+ };
80
+ export default function rehypeHelix(options = {}) {
81
+ const headings = { ...HEADINGS, ...options.headings };
82
+ const rewrite = (node) => {
83
+ const children = node.children;
84
+ if (!children) {
85
+ return;
86
+ }
87
+ children.forEach((child, index) => {
88
+ const replaced = (options.fences !== false ? codeBlockOf(child) : undefined) ??
89
+ (options.alerts !== false ? calloutOf(child, headings) : undefined);
90
+ if (replaced) {
91
+ children[index] = replaced;
92
+ }
93
+ rewrite(children[index]);
94
+ });
95
+ };
96
+ return (tree) => {
97
+ rewrite(tree);
98
+ };
99
+ }
100
+ export { rehypeHelix };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix",
3
3
  "description": "Styled component tier. Extends the headless tier with token-driven presentation.",
4
- "version": "0.106.0",
4
+ "version": "0.108.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -23,6 +23,10 @@
23
23
  },
24
24
  "./custom-elements.json": "./custom-elements.json",
25
25
  "./jsx": "./types/jsx.d.ts",
26
+ "./rehype": {
27
+ "types": "./dist/rehype.d.ts",
28
+ "default": "./dist/rehype.js"
29
+ },
26
30
  "./svelte": "./types/svelte.d.ts",
27
31
  "./vue": "./types/vue.d.ts"
28
32
  },
@@ -47,18 +51,21 @@
47
51
  },
48
52
  "bugs": "https://github.com/project-fusion/fusion/issues",
49
53
  "dependencies": {
50
- "@fusion.dev/helix-core": "0.106.0",
51
- "@fusion.dev/helix-headless": "0.106.0",
52
- "@fusion.dev/helix-styles": "0.106.0"
54
+ "@fusion.dev/helix-core": "0.108.0",
55
+ "@fusion.dev/helix-headless": "0.108.0",
56
+ "@fusion.dev/helix-styles": "0.108.0"
53
57
  },
54
58
  "devDependencies": {
55
- "@fusion.dev/helix-cem": "0.106.0",
56
- "@fusion.dev/helix-testing": "0.106.0",
59
+ "@fusion.dev/helix-cem": "0.108.0",
60
+ "@fusion.dev/helix-testing": "0.108.0",
57
61
  "@fusion.dev/scripts": "0.0.0-dev",
58
62
  "@happy-dom/global-registrator": "^20.0.0",
59
63
  "@lit-labs/ssr": "^4.1.0",
60
64
  "@types/bun": "^1.3.0",
61
- "lit": "^3.2.1"
65
+ "lit": "^3.2.1",
66
+ "remark-parse": "^11.0.0",
67
+ "remark-rehype": "^11.1.2",
68
+ "unified": "^11.0.5"
62
69
  },
63
70
  "homepage": "https://fusion.dev",
64
71
  "keywords": [