void-plugin-hono-jsx 0.1.0-rc.1 → 0.2.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +29 -23
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/plugins/hono-islands.d.ts.map +1 -1
  5. package/dist/plugins/hono-islands.js +11 -3
  6. package/dist/plugins/hono-islands.js.map +1 -1
  7. package/dist/plugins/hono-markdown.d.ts +3 -1
  8. package/dist/plugins/hono-markdown.d.ts.map +1 -1
  9. package/dist/plugins/hono-markdown.js +13 -6
  10. package/dist/plugins/hono-markdown.js.map +1 -1
  11. package/dist/plugins/markdown-compiler.d.ts +65 -2
  12. package/dist/plugins/markdown-compiler.d.ts.map +1 -1
  13. package/dist/plugins/markdown-compiler.js +54 -40
  14. package/dist/plugins/markdown-compiler.js.map +1 -1
  15. package/dist/plugins/markdown-copy.d.ts +3 -0
  16. package/dist/plugins/markdown-copy.d.ts.map +1 -0
  17. package/dist/plugins/markdown-copy.js +31 -0
  18. package/dist/plugins/markdown-copy.js.map +1 -0
  19. package/dist/plugins/markdown-features.d.ts +12 -0
  20. package/dist/plugins/markdown-features.d.ts.map +1 -0
  21. package/dist/plugins/markdown-features.js +56 -0
  22. package/dist/plugins/markdown-features.js.map +1 -0
  23. package/dist/theme/code.css +202 -0
  24. package/dist/theme/containers.css +125 -0
  25. package/dist/theme/content.css +4 -0
  26. package/dist/theme/index.css +7 -0
  27. package/dist/theme/prose.css +294 -0
  28. package/dist/theme/reset.css +100 -0
  29. package/package.json +20 -2
  30. package/src/plugins/hono-islands.ts +11 -3
  31. package/src/plugins/hono-markdown.ts +15 -8
  32. package/src/plugins/markdown-compiler.ts +46 -30
  33. package/src/plugins/markdown-copy.ts +30 -0
  34. package/src/plugins/markdown-features.ts +57 -0
  35. package/src/theme/code.css +202 -0
  36. package/src/theme/containers.css +125 -0
  37. package/src/theme/content.css +4 -0
  38. package/src/theme/index.css +7 -0
  39. package/src/theme/prose.css +294 -0
  40. package/src/theme/reset.css +100 -0
@@ -0,0 +1,125 @@
1
+ /* Void Markdown — Containers & GitHub Alerts */
2
+
3
+ .void-md .custom-block {
4
+ margin: 1em 0;
5
+ padding: 1em 1.25em;
6
+ border-radius: 8px;
7
+ border-left: 4px solid;
8
+ }
9
+
10
+ .void-md .custom-block-title {
11
+ font-weight: 600;
12
+ margin-bottom: 0.5em;
13
+ }
14
+
15
+ .void-md .custom-block > :last-child {
16
+ margin-bottom: 0;
17
+ }
18
+
19
+ /* Tip */
20
+ .void-md .tip {
21
+ border-color: #10b981;
22
+ background: rgba(16, 185, 129, 0.08);
23
+ }
24
+
25
+ .void-md .tip .custom-block-title {
26
+ color: #10b981;
27
+ }
28
+
29
+ /* Info / Note */
30
+ .void-md .info,
31
+ .void-md .note {
32
+ border-color: #3b82f6;
33
+ background: rgba(59, 130, 246, 0.08);
34
+ }
35
+
36
+ .void-md .info .custom-block-title,
37
+ .void-md .note .custom-block-title {
38
+ color: #3b82f6;
39
+ }
40
+
41
+ /* Warning / Important */
42
+ .void-md .warning,
43
+ .void-md .important {
44
+ border-color: #eab308;
45
+ background: rgba(234, 179, 8, 0.08);
46
+ }
47
+
48
+ .void-md .warning .custom-block-title,
49
+ .void-md .important .custom-block-title {
50
+ color: #ca8a04;
51
+ }
52
+
53
+ /* Danger / Caution */
54
+ .void-md .danger,
55
+ .void-md .caution {
56
+ border-color: #ef4444;
57
+ background: rgba(239, 68, 68, 0.08);
58
+ }
59
+
60
+ .void-md .danger .custom-block-title,
61
+ .void-md .caution .custom-block-title {
62
+ color: #ef4444;
63
+ }
64
+
65
+ /* Details (collapsible) */
66
+ .void-md details.custom-block {
67
+ cursor: pointer;
68
+ }
69
+
70
+ .void-md details.custom-block summary {
71
+ font-weight: 600;
72
+ cursor: pointer;
73
+ }
74
+
75
+ .void-md details.custom-block[open] summary {
76
+ margin-bottom: 0.5em;
77
+ }
78
+
79
+ /* Dark mode adjustments */
80
+ @media (prefers-color-scheme: dark) {
81
+ :root:not([data-theme='light']) .void-md .tip {
82
+ background: rgba(16, 185, 129, 0.12);
83
+ }
84
+ :root:not([data-theme='light']) .void-md .info,
85
+ :root:not([data-theme='light']) .void-md .note {
86
+ background: rgba(59, 130, 246, 0.12);
87
+ }
88
+ :root:not([data-theme='light']) .void-md .warning,
89
+ :root:not([data-theme='light']) .void-md .important {
90
+ background: rgba(234, 179, 8, 0.12);
91
+ }
92
+ :root:not([data-theme='light']) .void-md .warning .custom-block-title,
93
+ :root:not([data-theme='light']) .void-md .important .custom-block-title {
94
+ color: #eab308;
95
+ }
96
+ :root:not([data-theme='light']) .void-md .danger,
97
+ :root:not([data-theme='light']) .void-md .caution {
98
+ background: rgba(239, 68, 68, 0.12);
99
+ }
100
+ }
101
+
102
+ :root[data-theme='dark'] .void-md .tip {
103
+ background: rgba(16, 185, 129, 0.12);
104
+ }
105
+ :root[data-theme='dark'] .void-md .info,
106
+ :root[data-theme='dark'] .void-md .note {
107
+ background: rgba(59, 130, 246, 0.12);
108
+ }
109
+ :root[data-theme='dark'] .void-md .warning,
110
+ :root[data-theme='dark'] .void-md .important {
111
+ background: rgba(234, 179, 8, 0.12);
112
+ }
113
+ :root[data-theme='dark'] .void-md .warning .custom-block-title,
114
+ :root[data-theme='dark'] .void-md .important .custom-block-title {
115
+ color: #eab308;
116
+ }
117
+ :root[data-theme='dark'] .void-md .danger,
118
+ :root[data-theme='dark'] .void-md .caution {
119
+ background: rgba(239, 68, 68, 0.12);
120
+ }
121
+
122
+ /* GitHub Alerts */
123
+ .void-md .github-alert {
124
+ border-left-width: 4px;
125
+ }
@@ -0,0 +1,4 @@
1
+ /* Void Markdown — Content Styles (scoped to .void-md) */
2
+ @import './prose.css';
3
+ @import './code.css';
4
+ @import './containers.css';
@@ -0,0 +1,7 @@
1
+ /* Void Markdown — Full Theme (reset + baseline + content)
2
+ *
3
+ * Use this entry for standalone markdown sites that need a complete stylesheet.
4
+ * For embedding markdown in an existing app, use content.css instead.
5
+ */
6
+ @import './reset.css';
7
+ @import './content.css';
@@ -0,0 +1,294 @@
1
+ /* Void Markdown — Prose Typography */
2
+
3
+ .void-md {
4
+ --vmd-font-body: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
5
+ --vmd-font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
6
+ --vmd-text: #1a1a2e;
7
+ --vmd-text-muted: #64748b;
8
+ --vmd-link: #2563eb;
9
+ --vmd-link-hover: #1d4ed8;
10
+ --vmd-border: #e2e8f0;
11
+ --vmd-bg-soft: #f8fafc;
12
+ --vmd-line-height: 1.75;
13
+
14
+ font-family: var(--vmd-font-body);
15
+ color: var(--vmd-text);
16
+ line-height: var(--vmd-line-height);
17
+ word-wrap: break-word;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme='light']) .void-md {
22
+ --vmd-text: #e2e8f0;
23
+ --vmd-text-muted: #94a3b8;
24
+ --vmd-link: #60a5fa;
25
+ --vmd-link-hover: #93bbfd;
26
+ --vmd-border: #334155;
27
+ --vmd-bg-soft: #1e293b;
28
+ }
29
+ }
30
+
31
+ :root[data-theme='dark'] .void-md {
32
+ --vmd-text: #e2e8f0;
33
+ --vmd-text-muted: #94a3b8;
34
+ --vmd-link: #60a5fa;
35
+ --vmd-link-hover: #93bbfd;
36
+ --vmd-border: #334155;
37
+ --vmd-bg-soft: #1e293b;
38
+ }
39
+
40
+ /* Headings */
41
+ .void-md h1,
42
+ .void-md h2,
43
+ .void-md h3,
44
+ .void-md h4,
45
+ .void-md h5,
46
+ .void-md h6 {
47
+ font-weight: 600;
48
+ line-height: 1.3;
49
+ margin-top: 1.5em;
50
+ margin-bottom: 0.5em;
51
+ }
52
+
53
+ .void-md h1 {
54
+ font-size: 2em;
55
+ margin-top: 0;
56
+ }
57
+ .void-md h2 {
58
+ font-size: 1.5em;
59
+ padding-bottom: 0.3em;
60
+ border-bottom: 1px solid var(--vmd-border);
61
+ }
62
+ .void-md h3 {
63
+ font-size: 1.25em;
64
+ }
65
+ .void-md h4 {
66
+ font-size: 1em;
67
+ }
68
+
69
+ /* Paragraphs */
70
+ .void-md p {
71
+ margin-top: 0;
72
+ margin-bottom: 1em;
73
+ }
74
+
75
+ /* Links */
76
+ .void-md a {
77
+ color: var(--vmd-link);
78
+ text-decoration: none;
79
+ font-weight: 500;
80
+ }
81
+
82
+ .void-md a:hover {
83
+ color: var(--vmd-link-hover);
84
+ text-decoration: underline;
85
+ }
86
+
87
+ /* Lists */
88
+ .void-md ul,
89
+ .void-md ol {
90
+ padding-left: 1.5em;
91
+ margin-top: 0;
92
+ margin-bottom: 1em;
93
+ }
94
+
95
+ .void-md li {
96
+ margin-bottom: 0.25em;
97
+ }
98
+
99
+ .void-md li > p {
100
+ margin-bottom: 0.5em;
101
+ }
102
+
103
+ /* Blockquotes */
104
+ .void-md blockquote {
105
+ margin: 0 0 1em;
106
+ padding: 0.5em 1em;
107
+ border-left: 4px solid var(--vmd-border);
108
+ color: var(--vmd-text-muted);
109
+ }
110
+
111
+ .void-md blockquote > :last-child {
112
+ margin-bottom: 0;
113
+ }
114
+
115
+ /* Tables */
116
+ .void-md table {
117
+ width: 100%;
118
+ border-collapse: collapse;
119
+ margin-bottom: 1em;
120
+ display: block;
121
+ overflow-x: auto;
122
+ }
123
+
124
+ .void-md th,
125
+ .void-md td {
126
+ padding: 0.5em 1em;
127
+ border: 1px solid var(--vmd-border);
128
+ text-align: left;
129
+ }
130
+
131
+ .void-md th {
132
+ font-weight: 600;
133
+ background: var(--vmd-bg-soft);
134
+ }
135
+
136
+ /* Inline code */
137
+ .void-md :not(pre) > code {
138
+ font-family: var(--vmd-font-mono);
139
+ font-size: 0.875em;
140
+ padding: 0.15em 0.4em;
141
+ border-radius: 4px;
142
+ background: var(--vmd-bg-soft);
143
+ border: 1px solid var(--vmd-border);
144
+ }
145
+
146
+ /* Horizontal rule */
147
+ .void-md hr {
148
+ border: none;
149
+ border-top: 1px solid var(--vmd-border);
150
+ margin: 2em 0;
151
+ }
152
+
153
+ /* Images */
154
+ .void-md img {
155
+ max-width: 100%;
156
+ height: auto;
157
+ }
158
+
159
+ /* Strong / emphasis */
160
+ .void-md strong {
161
+ font-weight: 600;
162
+ }
163
+
164
+ /* Anchor links */
165
+ .void-md .header-anchor {
166
+ float: left;
167
+ margin-left: -0.87em;
168
+ padding-right: 0.23em;
169
+ font-weight: 500;
170
+ opacity: 0;
171
+ transition: opacity 0.15s;
172
+ }
173
+
174
+ .void-md h1:hover .header-anchor,
175
+ .void-md h2:hover .header-anchor,
176
+ .void-md h3:hover .header-anchor,
177
+ .void-md h4:hover .header-anchor,
178
+ .void-md h5:hover .header-anchor,
179
+ .void-md h6:hover .header-anchor {
180
+ opacity: 1;
181
+ }
182
+
183
+ /* Task lists */
184
+ .void-md ul.contains-task-list {
185
+ list-style: none;
186
+ padding-left: 0;
187
+ }
188
+
189
+ .void-md .task-list-item {
190
+ display: flex;
191
+ align-items: baseline;
192
+ gap: 0.5em;
193
+ }
194
+
195
+ .void-md .task-list-item input[type='checkbox'] {
196
+ margin: 0;
197
+ flex-shrink: 0;
198
+ }
199
+
200
+ /* Nested task lists */
201
+ .void-md .task-list-item ul.contains-task-list {
202
+ margin-top: 0.25em;
203
+ }
204
+
205
+ /* Keyboard input */
206
+ .void-md kbd {
207
+ font-family: var(--vmd-font-mono);
208
+ font-size: 0.875em;
209
+ padding: 0.1em 0.4em;
210
+ border-radius: 4px;
211
+ border: 1px solid var(--vmd-border);
212
+ box-shadow: 0 1px 0 var(--vmd-border);
213
+ background: var(--vmd-bg-soft);
214
+ }
215
+
216
+ /* Highlighted text */
217
+ .void-md mark {
218
+ background: rgba(234, 179, 8, 0.3);
219
+ padding: 0.1em 0.2em;
220
+ border-radius: 2px;
221
+ color: inherit;
222
+ }
223
+
224
+ /* Definition lists */
225
+ .void-md dl {
226
+ margin-bottom: 1em;
227
+ }
228
+
229
+ .void-md dt {
230
+ font-weight: 600;
231
+ margin-top: 0.5em;
232
+ }
233
+
234
+ .void-md dd {
235
+ margin-left: 1.5em;
236
+ margin-bottom: 0.5em;
237
+ }
238
+
239
+ /* Abbreviations */
240
+ .void-md abbr[title] {
241
+ text-decoration: underline dotted;
242
+ cursor: help;
243
+ }
244
+
245
+ /* Subscript / Superscript */
246
+ .void-md sub,
247
+ .void-md sup {
248
+ font-size: 0.75em;
249
+ line-height: 0;
250
+ position: relative;
251
+ vertical-align: baseline;
252
+ }
253
+
254
+ .void-md sup {
255
+ top: -0.5em;
256
+ }
257
+
258
+ .void-md sub {
259
+ bottom: -0.25em;
260
+ }
261
+
262
+ /* Footnotes */
263
+ .void-md section.footnotes {
264
+ margin-top: 2em;
265
+ padding-top: 1em;
266
+ border-top: 1px solid var(--vmd-border);
267
+ font-size: 0.875em;
268
+ color: var(--vmd-text-muted);
269
+ }
270
+
271
+ .void-md .footnote-ref a {
272
+ font-size: 0.75em;
273
+ vertical-align: super;
274
+ line-height: 0;
275
+ margin-left: 0.1em;
276
+ }
277
+
278
+ .void-md .footnote-backref {
279
+ font-family: initial;
280
+ }
281
+
282
+ /* Table of contents */
283
+ .void-md .table-of-contents {
284
+ margin-bottom: 1em;
285
+ }
286
+
287
+ .void-md .table-of-contents ul {
288
+ list-style: none;
289
+ padding-left: 1em;
290
+ }
291
+
292
+ .void-md .table-of-contents > ul {
293
+ padding-left: 0;
294
+ }
@@ -0,0 +1,100 @@
1
+ /* Void Markdown — Reset & Baseline
2
+ *
3
+ * Opinionated modern reset for markdown-driven sites.
4
+ * Imported by index.css — not intended for standalone use.
5
+ */
6
+
7
+ /* Box sizing */
8
+ *,
9
+ *::before,
10
+ *::after {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ /* Remove default margins */
15
+ * {
16
+ margin: 0;
17
+ }
18
+
19
+ /* Document */
20
+ html {
21
+ line-height: 1.5;
22
+ -webkit-text-size-adjust: 100%;
23
+ -moz-tab-size: 4;
24
+ tab-size: 4;
25
+ scroll-behavior: smooth;
26
+ }
27
+
28
+ /* Body */
29
+ body {
30
+ min-height: 100dvh;
31
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
32
+ -webkit-font-smoothing: antialiased;
33
+ -moz-osx-font-smoothing: grayscale;
34
+ text-rendering: optimizeLegibility;
35
+ }
36
+
37
+ /* Typography */
38
+ h1,
39
+ h2,
40
+ h3,
41
+ h4,
42
+ h5,
43
+ h6 {
44
+ overflow-wrap: break-word;
45
+ text-wrap: balance;
46
+ }
47
+
48
+ p {
49
+ overflow-wrap: break-word;
50
+ text-wrap: pretty;
51
+ }
52
+
53
+ /* Media */
54
+ img,
55
+ picture,
56
+ video,
57
+ canvas,
58
+ svg {
59
+ display: block;
60
+ max-width: 100%;
61
+ }
62
+
63
+ img,
64
+ video {
65
+ height: auto;
66
+ }
67
+
68
+ /* Forms */
69
+ input,
70
+ button,
71
+ textarea,
72
+ select {
73
+ font: inherit;
74
+ }
75
+
76
+ /* Monospace */
77
+ code,
78
+ kbd,
79
+ samp,
80
+ pre {
81
+ font-family:
82
+ ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
83
+ font-size: 1em;
84
+ }
85
+
86
+ /* Links */
87
+ a {
88
+ color: inherit;
89
+ text-decoration: inherit;
90
+ }
91
+
92
+ /* Tables */
93
+ table {
94
+ border-collapse: collapse;
95
+ }
96
+
97
+ /* Horizontal rule */
98
+ hr {
99
+ border: none;
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void-plugin-hono-jsx",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Hono JSX Pages adapter for Void",
6
6
  "type": "module",
@@ -36,6 +36,12 @@
36
36
  "./markdown": {
37
37
  "types": "./dist/server/markdown.d.ts",
38
38
  "default": "./dist/server/markdown.js"
39
+ },
40
+ "./theme.css": {
41
+ "default": "./dist/theme/index.css"
42
+ },
43
+ "./theme-content.css": {
44
+ "default": "./dist/theme/content.css"
39
45
  }
40
46
  },
41
47
  "peerDependencies": {
@@ -43,7 +49,10 @@
43
49
  "void": ">=1.1.11 <1.2.0",
44
50
  "vite": ">=8.0.16 <9.0.0",
45
51
  "markdown-exit": ">=1.3.0 <2.0.0",
46
- "yaml": ">=2.9.1 <3.0.0"
52
+ "yaml": ">=2.9.1 <3.0.0",
53
+ "markdown-it-container": ">=4.0.0 <5.0.0",
54
+ "shiki": ">=4.5.0 <5.0.0",
55
+ "@shikijs/transformers": ">=4.5.0 <5.0.0"
47
56
  },
48
57
  "repository": {
49
58
  "type": "git",
@@ -72,6 +81,15 @@
72
81
  },
73
82
  "yaml": {
74
83
  "optional": true
84
+ },
85
+ "markdown-it-container": {
86
+ "optional": true
87
+ },
88
+ "shiki": {
89
+ "optional": true
90
+ },
91
+ "@shikijs/transformers": {
92
+ "optional": true
75
93
  }
76
94
  }
77
95
  }
@@ -2,7 +2,7 @@ import { readFileSync, realpathSync } from 'node:fs';
2
2
  import { join, relative, resolve, dirname, basename } from 'node:path';
3
3
  import { normalizePath, parseSync, type Plugin } from 'vite';
4
4
  import { scanPages, resolveEffectiveLayoutChain, resolvePagesDir, type PageScanResult } from 'void/pages';
5
- import { findIslandImports, editIslandSource, computePagesNeedingClientJS, ISLANDS_RENDERER_ID, ISLANDS_CLIENT_ENTRY_ID } from 'void/pages-islands-plugin';
5
+ import { findIslandImports, findStaticCssImports, rewriteCssImportForVirtualModule, editIslandSource, computePagesNeedingClientJS, ISLANDS_RENDERER_ID, ISLANDS_CLIENT_ENTRY_ID } from 'void/pages-islands-plugin';
6
6
 
7
7
  import { resolveComponent } from './resolve-component.ts';
8
8
 
@@ -15,6 +15,7 @@ export function honoIslands(): Plugin {
15
15
  const islands = new Map<string, string>();
16
16
  const pageFiles = new Set<string>();
17
17
  let dependencyFiles = new Set<string>();
18
+ let cssImports: string[] = [];
18
19
  const graph = new Map<string, Set<string>>();
19
20
  const directlyInteractive = new Set<string>();
20
21
  const interactivePages = new Set<string>();
@@ -123,6 +124,10 @@ export function honoIslands(): Plugin {
123
124
  for (const page of scan.pages.filter((page) => !page.island)) {
124
125
  for (const file of [page.componentPath, ...resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts).map((layout) => layout.filePath)]) validateInteractive(join(pagesDir, file));
125
126
  }
127
+ // Match the official adapters: retain static-layout CSS in the client build
128
+ // even when the document itself needs no client-side JavaScript. Reuse the
129
+ // analysis snapshot rather than rereading modules during entry generation.
130
+ cssImports = [...new Set([...pageFiles].flatMap(file => findStaticCssImports(analyses.get(file)!.code, file).map(item => rewriteCssImportForVirtualModule(item.statement, item.specifier, file))))];
126
131
  dependencyFiles = watched;
127
132
  const needsJS = new Set(directlyInteractive);
128
133
  let changed = true;
@@ -210,9 +215,12 @@ export function renderIslandPage(page, assets) {
210
215
  }`;
211
216
  }
212
217
  if (id === `\0${ISLANDS_CLIENT_ENTRY_ID}`) {
213
- if (!islands.size || !interactivePages.size) return 'export {};';
218
+ // Void owns dev first-paint CSS. Production needs the static imports
219
+ // so its shared manifest asset collector can emit stylesheet links.
220
+ const styles = this.environment?.mode === 'dev' ? '' : cssImports.join('\n');
221
+ if (!islands.size || !interactivePages.size) return styles || 'export {};';
214
222
  const loaders = [...islands].map(([name, path]) => `${JSON.stringify(name)}: () => import(${JSON.stringify(path)})`).join(',\n');
215
- return `${markdown ? "import { MarkdownContext } from 'void-plugin-hono-jsx/markdown';" : ''}
223
+ return `${styles}\n${markdown ? "import { MarkdownContext } from 'void-plugin-hono-jsx/markdown';" : ''}
216
224
  import { initIslands } from 'void/pages-client';
217
225
  import { jsx } from 'hono/jsx/dom/jsx-runtime';
218
226
  import { render } from 'hono/jsx/dom';
@@ -3,7 +3,10 @@ import { join, relative, dirname } from 'node:path';
3
3
  import { normalizePath, type Plugin } from 'vite';
4
4
  import { resolvePagesDir } from 'void/pages';
5
5
  import { resolveComponent } from './resolve-component.ts';
6
- import { compileMarkdown, extractMarkdown, renderChunks } from './markdown-compiler.ts';
6
+ import { createMarkdownCompiler, renderChunks } from './markdown-compiler.ts';
7
+ import type { HonoMarkdownOptions } from './markdown-features.ts';
8
+ import { markdownCopyId, markdownCopyCode } from './markdown-copy.ts';
9
+ export type { HonoMarkdownOptions } from './markdown-features.ts';
7
10
  import type { HonoMarkdownApi } from './markdown-api.ts';
8
11
 
9
12
  const clientId = 'virtual:void-hono-markdown-client';
@@ -11,22 +14,24 @@ const scriptQuery = '?hono-markdown-script';
11
14
  const literal = (value: unknown) => `JSON.parse(${JSON.stringify(JSON.stringify(value))})`;
12
15
 
13
16
  /** Optional authored Markdown compiler; independent of @void/md private files. */
14
- export function voidHonoMarkdown(): Plugin {
17
+ export function voidHonoMarkdown(options: HonoMarkdownOptions = {}): Plugin {
18
+ const compiler = createMarkdownCompiler(options);
15
19
  let root = process.cwd();
16
20
  let pagesDir = resolvePagesDir(root);
17
21
  let scripts = new Map<string, string>();
18
22
  let files = new Map<string, string>();
19
- let sources = new Map<string, ReturnType<typeof extractMarkdown>>();
23
+ let sources = new Map<string, ReturnType<typeof compiler.parse>>();
20
24
  function rescan() {
21
25
  const nextScripts = new Map<string, string>();
22
26
  const nextFiles = new Map<string, string>();
23
- const nextSources = new Map<string, ReturnType<typeof extractMarkdown>>();
27
+ const nextSources = new Map<string, ReturnType<typeof compiler.parse>>();
24
28
  for (const entry of existsSync(pagesDir) ? readdirSync(pagesDir, { recursive: true, withFileTypes: true }) : []) {
25
29
  if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
26
30
  const file = join(entry.parentPath, entry.name);
27
31
  const id = normalizePath(relative(pagesDir, file)).replace(/\.md$/, '').replace(/\.island$/, '').replace(/\/index$/, '').replace(/^index$/, '') || 'index';
28
32
  const canonical = realpathSync(file);
29
- const extracted = nextSources.get(canonical) ?? extractMarkdown(readFileSync(canonical, 'utf8'));
33
+ const extracted = nextSources.get(canonical) ?? compiler.parse(readFileSync(canonical, 'utf8'));
34
+ if (!nextSources.has(canonical) && extracted.hasCode) extracted.clientCode = `import ${JSON.stringify(markdownCopyId)};\n` + extracted.clientCode;
30
35
  nextSources.set(canonical, extracted);
31
36
  if (extracted.clientCode) { nextScripts.set(id, extracted.clientCode); nextFiles.set(id, file); }
32
37
  }
@@ -51,8 +56,9 @@ export function voidHonoMarkdown(): Plugin {
51
56
  const entry = this.environment.moduleGraph.getModuleById(`\0${clientId}`);
52
57
  if (entry) this.environment.moduleGraph.invalidateModule(entry, new Set(), timestamp, true);
53
58
  },
54
- resolveId(id) { if (id === clientId) return `\0${id}`; },
59
+ resolveId(id) { if (id === clientId || id === markdownCopyId) return `\0${id}`; },
55
60
  load(id) {
61
+ if (id === `\0${markdownCopyId}`) return markdownCopyCode;
56
62
  if (id.endsWith(scriptQuery)) return source(id.slice(0, -scriptQuery.length)).clientCode || 'export {};';
57
63
  if (id !== `\0${clientId}`) return;
58
64
  if (!files.size) return 'export {};';
@@ -62,8 +68,9 @@ if (id && scripts[id]) await scripts[id]();`;
62
68
  },
63
69
  transform: {
64
70
  filter: { id: /\.md$/ },
65
- handler(source, id) {
66
- const result = compileMarkdown(source);
71
+ async handler(source, id) {
72
+ const cached = sources.get(realpathSync(id));
73
+ const result = await compiler.render(cached?.source === source ? cached : compiler.parse(source));
67
74
  const imports = result.imports.map((entry, index) => {
68
75
  const target = resolveComponent(entry.specifier, dirname(id), ['.tsx', '.jsx']);
69
76
  if (!target || !entry.specifier.startsWith('.')) throw new Error(`Hono Markdown: cannot resolve relative TSX/JSX island ${entry.specifier} in ${id}.`);