@markii/host 0.0.1 → 0.17.0-next.3

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 (193) hide show
  1. package/README.md +20 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +61 -0
  4. package/dist/browser.js +172 -0
  5. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  6. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  7. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  8. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  11. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  14. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  17. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  20. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  21. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  23. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  24. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  25. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  26. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  27. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  28. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  29. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  31. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  32. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  33. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  34. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  35. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  36. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  37. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  38. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  40. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  41. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  43. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  46. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  47. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  48. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  49. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  51. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  52. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  53. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  58. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  59. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  61. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  63. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  64. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  65. package/dist/conformance/host/README.md +221 -0
  66. package/dist/conformance.d.ts +2 -0
  67. package/dist/conformance.js +5 -0
  68. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  69. package/dist/diagnostics/render-diagnostics.js +46 -0
  70. package/dist/export/cascade.d.ts +98 -0
  71. package/dist/export/cascade.js +159 -0
  72. package/dist/export/export-zip.d.ts +19 -0
  73. package/dist/export/export-zip.js +38 -0
  74. package/dist/export/image-embed.d.ts +126 -0
  75. package/dist/export/image-embed.js +274 -0
  76. package/dist/export/note-export.d.ts +332 -0
  77. package/dist/export/note-export.js +366 -0
  78. package/dist/export/note-links.d.ts +83 -0
  79. package/dist/export/note-links.js +241 -0
  80. package/dist/glue.wasm +0 -0
  81. package/dist/host/adapter.d.ts +218 -0
  82. package/dist/host/adapter.js +9 -0
  83. package/dist/host/cascade-export-behavior.d.ts +120 -0
  84. package/dist/host/cascade-export-behavior.js +151 -0
  85. package/dist/host/conformance-runner.d.ts +159 -0
  86. package/dist/host/conformance-runner.js +632 -0
  87. package/dist/host/create-host.d.ts +251 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +153 -0
  90. package/dist/host/editor-behavior.js +223 -0
  91. package/dist/host/export/md-plain.d.ts +15 -0
  92. package/dist/host/export/md-plain.js +125 -0
  93. package/dist/host/export-behavior.d.ts +111 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +23 -0
  96. package/dist/host/labels.js +32 -0
  97. package/dist/host/pack-archive.d.ts +30 -0
  98. package/dist/host/pack-archive.js +62 -0
  99. package/dist/host/pack-install.d.ts +87 -0
  100. package/dist/host/pack-install.js +186 -0
  101. package/dist/host/pack-load.d.ts +71 -0
  102. package/dist/host/pack-load.js +57 -0
  103. package/dist/host/pack-remove.d.ts +21 -0
  104. package/dist/host/pack-remove.js +65 -0
  105. package/dist/host/pack-trust-behavior.d.ts +79 -0
  106. package/dist/host/pack-trust-behavior.js +262 -0
  107. package/dist/host/refresh-interval.d.ts +37 -0
  108. package/dist/host/refresh-interval.js +59 -0
  109. package/dist/host/run-behavior.d.ts +88 -0
  110. package/dist/host/run-behavior.js +189 -0
  111. package/dist/host/run-diagnostics.d.ts +26 -0
  112. package/dist/host/run-diagnostics.js +43 -0
  113. package/dist/host/script-execution.d.ts +72 -0
  114. package/dist/host/script-execution.js +68 -0
  115. package/dist/host/terminal-packs.d.ts +39 -0
  116. package/dist/host/terminal-packs.js +51 -0
  117. package/dist/html-engine.d.ts +2 -0
  118. package/dist/html-engine.js +29 -0
  119. package/dist/index.d.ts +95 -0
  120. package/dist/index.js +69 -0
  121. package/dist/insert/component-catalog.d.ts +23 -0
  122. package/dist/insert/component-catalog.js +96 -0
  123. package/dist/node-fs.d.ts +14 -0
  124. package/dist/node-fs.js +70 -0
  125. package/dist/node-worker.js +20657 -0
  126. package/dist/node.d.ts +6 -0
  127. package/dist/node.js +13 -0
  128. package/dist/package.json +3 -0
  129. package/dist/packs/discover.d.ts +119 -0
  130. package/dist/packs/discover.js +266 -0
  131. package/dist/packs/pack-diagnostics.d.ts +79 -0
  132. package/dist/packs/pack-diagnostics.js +103 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +105 -0
  135. package/dist/packs/pack-render-registry.d.ts +82 -0
  136. package/dist/packs/pack-render-registry.js +182 -0
  137. package/dist/packs/pack-scripts.d.ts +20 -0
  138. package/dist/packs/pack-scripts.js +96 -0
  139. package/dist/packs/pack-trust.d.ts +57 -0
  140. package/dist/packs/pack-trust.js +111 -0
  141. package/dist/packs/prebuilt.d.ts +49 -0
  142. package/dist/packs/prebuilt.js +97 -0
  143. package/dist/path-util.d.ts +24 -0
  144. package/dist/path-util.js +82 -0
  145. package/dist/run/browser-isolate.d.ts +45 -0
  146. package/dist/run/browser-isolate.js +85 -0
  147. package/dist/run/bundle-document.d.ts +29 -0
  148. package/dist/run/bundle-document.js +40 -0
  149. package/dist/run/bundle-run.d.ts +188 -0
  150. package/dist/run/bundle-run.js +232 -0
  151. package/dist/run/grant-flow.d.ts +232 -0
  152. package/dist/run/grant-flow.js +390 -0
  153. package/dist/run/ip-address.d.ts +41 -0
  154. package/dist/run/ip-address.js +255 -0
  155. package/dist/run/isolate.d.ts +59 -0
  156. package/dist/run/isolate.js +22 -0
  157. package/dist/run/lua-resolver.d.ts +21 -0
  158. package/dist/run/lua-resolver.js +63 -0
  159. package/dist/run/net-bridge-worker.d.ts +26 -0
  160. package/dist/run/net-bridge-worker.js +66 -0
  161. package/dist/run/net-bridge.d.ts +56 -0
  162. package/dist/run/net-bridge.js +59 -0
  163. package/dist/run/net-pinning.d.ts +82 -0
  164. package/dist/run/net-pinning.js +137 -0
  165. package/dist/run/net-provider.d.ts +72 -0
  166. package/dist/run/net-provider.js +303 -0
  167. package/dist/run/node-isolate.d.ts +22 -0
  168. package/dist/run/node-isolate.js +68 -0
  169. package/dist/run/run-flow.d.ts +186 -0
  170. package/dist/run/run-flow.js +323 -0
  171. package/dist/run/run-host.d.ts +107 -0
  172. package/dist/run/run-host.js +241 -0
  173. package/dist/run/run-job.d.ts +130 -0
  174. package/dist/run/run-job.js +279 -0
  175. package/dist/run/run-progress.d.ts +55 -0
  176. package/dist/run/run-progress.js +42 -0
  177. package/dist/run/run-trace.d.ts +44 -0
  178. package/dist/run/run-trace.js +36 -0
  179. package/dist/run/script-requirements.d.ts +98 -0
  180. package/dist/run/script-requirements.js +212 -0
  181. package/dist/run/snapshot-storage.d.ts +25 -0
  182. package/dist/run/snapshot-storage.js +31 -0
  183. package/dist/run/stale-values.d.ts +25 -0
  184. package/dist/run/stale-values.js +32 -0
  185. package/dist/run/worker-entry-browser.d.ts +62 -0
  186. package/dist/run/worker-entry-browser.js +172 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +98 -0
  189. package/dist/values-failure.d.ts +17 -0
  190. package/dist/values-failure.js +1 -0
  191. package/dist/values-merge.d.ts +39 -0
  192. package/dist/values-merge.js +33 -0
  193. package/package.json +80 -4
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Embedding a note's local images into its exported HTML (GitHub issue #28
3
+ * slice 3, part 1), so an exported file is genuinely self-contained: one
4
+ * document a user can mail, archive, or open on a machine that has never
5
+ * seen the vault, with its pictures still in it.
6
+ *
7
+ * WHERE THIS SITS. `./note-export.ts` composes an already-rendered body
8
+ * string into the export page. This module rewrites that body BEFORE the
9
+ * composition, turning every embeddable `<img src>` into a `data:` URI. It
10
+ * works on the rendered string rather than on the note's AST on purpose:
11
+ * the body is whatever engine rendered it, React or `@markii/html`, and a
12
+ * pack component's own images have to be embedded too. A string rewrite is
13
+ * the one place both engines and every component meet.
14
+ *
15
+ * WHAT STAYS A URL. Anything with a scheme, so `http:` and `https:` sources
16
+ * stay live URLs exactly as authored, and a `data:` URI is already
17
+ * embedded. A protocol-relative `//host/x.png` is a remote source too. Only
18
+ * a genuinely local, relative path is a candidate.
19
+ *
20
+ * THE HOST SEAM. This package knows nothing about vaults, workspace
21
+ * folders, or filesystems, so reading the bytes is injected: a host hands
22
+ * in an `ExportImageReader` that resolves a source the way its own preview
23
+ * resolves one and reads it, or reports that it could not. That keeps
24
+ * `@markii/host` free of host APIs while the size cap, the MIME rules, the
25
+ * base64 encoding, and the reporting stay in ONE place, shared by every
26
+ * host.
27
+ *
28
+ * NOTHING HERE EVER FAILS AN EXPORT. An image that is too large, of an
29
+ * unknown type, missing, or unreadable keeps the source the author wrote
30
+ * and is recorded in the returned report. The host turns that report into
31
+ * diagnostics lines: per AGENTS.md, a skipped image is a quiet fact on the
32
+ * diagnostics surface, never a notice and never a failed export.
33
+ */
34
+ /**
35
+ * The per-image ceiling for embedding, 2 MiB. An image above it keeps its
36
+ * original path rather than inflating the exported file by a third of its
37
+ * own size in base64. Exported so a host with a cheap size check can skip
38
+ * reading such a file at all and report `oversize` straight away.
39
+ */
40
+ export const MAX_EMBEDDED_IMAGE_BYTES = 2 * 1024 * 1024;
41
+ /**
42
+ * The extensions this embeds, and the MIME type each becomes. Hand-rolled
43
+ * and deliberately small, in the same spirit as this repo's other
44
+ * hand-rolled validation: an extension that is not on this list is not
45
+ * embedded at all, rather than guessed at with a generic type that a
46
+ * browser would then have to sniff.
47
+ */
48
+ const IMAGE_MIME_TYPES = {
49
+ png: 'image/png',
50
+ jpg: 'image/jpeg',
51
+ jpeg: 'image/jpeg',
52
+ gif: 'image/gif',
53
+ webp: 'image/webp',
54
+ avif: 'image/avif',
55
+ bmp: 'image/bmp',
56
+ ico: 'image/x-icon',
57
+ svg: 'image/svg+xml',
58
+ };
59
+ /** Every extension `mimeTypeForImageSrc` recognizes, lowercase and without the dot. Exported for a host that wants to name them in its own documentation or tests. */
60
+ export const EMBEDDABLE_IMAGE_EXTENSIONS = Object.keys(IMAGE_MIME_TYPES);
61
+ /**
62
+ * The MIME type for one image source, read from its extension, or
63
+ * `undefined` when the extension is missing or unrecognized. Any `?query`
64
+ * or `#fragment` is ignored first, so `logo.png?v=2` still reads as a PNG.
65
+ */
66
+ export function mimeTypeForImageSrc(src) {
67
+ const withoutQuery = src.split(/[?#]/, 1)[0] ?? '';
68
+ const lastDot = withoutQuery.lastIndexOf('.');
69
+ if (lastDot === -1)
70
+ return undefined;
71
+ const extension = withoutQuery.slice(lastDot + 1).toLowerCase();
72
+ if (extension.length === 0)
73
+ return undefined;
74
+ return Object.prototype.hasOwnProperty.call(IMAGE_MIME_TYPES, extension)
75
+ ? IMAGE_MIME_TYPES[extension]
76
+ : undefined;
77
+ }
78
+ /**
79
+ * True when `value` begins with a URL scheme, using the same rule as
80
+ * `@markii/core`'s `isSafeUrl` and `apps/vscode/src/webview/
81
+ * document-images.ts`: text before the first `:`, but only when that `:`
82
+ * comes before any `/`, `?` or `#`. So `notes/a:b.png` is correctly a
83
+ * relative path, while `https://x/y.png` and `data:image/png;base64,...`
84
+ * are not.
85
+ */
86
+ function hasScheme(value) {
87
+ const colon = value.indexOf(':');
88
+ if (colon === -1)
89
+ return false;
90
+ const slash = value.indexOf('/');
91
+ const questionMark = value.indexOf('?');
92
+ const numberSign = value.indexOf('#');
93
+ return ((slash === -1 || colon < slash) &&
94
+ (questionMark === -1 || colon < questionMark) &&
95
+ (numberSign === -1 || colon < numberSign));
96
+ }
97
+ /**
98
+ * True for a source this will try to embed: a non-empty, local, relative
99
+ * path. A scheme of any kind, a protocol-relative `//host/...`, and an
100
+ * empty or fragment-only value are all left exactly as authored.
101
+ */
102
+ export function isEmbeddableImageSrc(src) {
103
+ if (src.length === 0)
104
+ return false;
105
+ if (src.startsWith('#'))
106
+ return false;
107
+ if (src.startsWith('//'))
108
+ return false;
109
+ return !hasScheme(src);
110
+ }
111
+ /** An empty report, for an export that embedded nothing because no host reader was offered. */
112
+ export const EMPTY_IMAGE_REPORT = {
113
+ embedded: [],
114
+ embeddedBytes: 0,
115
+ skipped: [],
116
+ remote: 0,
117
+ };
118
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
119
+ /**
120
+ * Base64 for a byte array, hand-rolled so this module needs neither
121
+ * `Buffer` (Node only) nor `btoa` (which cannot take bytes without a
122
+ * lossy string round trip) and stays environment-free, matching how this
123
+ * repo hand-rolls its manifest validation and `uses:` reader rather than
124
+ * taking a dependency for something this small.
125
+ */
126
+ export function encodeBase64(bytes) {
127
+ let result = '';
128
+ const full = bytes.length - (bytes.length % 3);
129
+ for (let index = 0; index < full; index += 3) {
130
+ const chunk = ((bytes[index] ?? 0) << 16) |
131
+ ((bytes[index + 1] ?? 0) << 8) |
132
+ (bytes[index + 2] ?? 0);
133
+ result +=
134
+ BASE64_ALPHABET[(chunk >> 18) & 63] +
135
+ BASE64_ALPHABET[(chunk >> 12) & 63] +
136
+ BASE64_ALPHABET[(chunk >> 6) & 63] +
137
+ BASE64_ALPHABET[chunk & 63];
138
+ }
139
+ const remaining = bytes.length - full;
140
+ if (remaining === 1) {
141
+ const chunk = (bytes[full] ?? 0) << 16;
142
+ result +=
143
+ BASE64_ALPHABET[(chunk >> 18) & 63] +
144
+ BASE64_ALPHABET[(chunk >> 12) & 63] +
145
+ '==';
146
+ }
147
+ else if (remaining === 2) {
148
+ const chunk = ((bytes[full] ?? 0) << 16) | ((bytes[full + 1] ?? 0) << 8);
149
+ result +=
150
+ BASE64_ALPHABET[(chunk >> 18) & 63] +
151
+ BASE64_ALPHABET[(chunk >> 12) & 63] +
152
+ BASE64_ALPHABET[(chunk >> 6) & 63] +
153
+ '=';
154
+ }
155
+ return result;
156
+ }
157
+ /** One image's bytes as a `data:` URI. Base64 output is alphanumeric plus `+/=`, so it is always safe inside a double-quoted HTML attribute with no further escaping. */
158
+ export function toImageDataUri(bytes, mimeType) {
159
+ return `data:${mimeType};base64,${encodeBase64(bytes)}`;
160
+ }
161
+ /** The five entities a renderer escapes in an attribute value, decoded back to the path the author actually wrote. `&amp;` is decoded last so `&amp;lt;` does not become `<`. */
162
+ function decodeAttributeValue(value) {
163
+ return value
164
+ .replace(/&lt;/g, '<')
165
+ .replace(/&gt;/g, '>')
166
+ .replace(/&quot;/g, '"')
167
+ .replace(/&#(?:39|x27);/g, "'")
168
+ .replace(/&amp;/g, '&');
169
+ }
170
+ /**
171
+ * Matches one `<img ...>` start tag. Both renderers emit attribute values
172
+ * in double quotes, and every `<` inside rendered text or code is escaped
173
+ * to `&lt;` before it reaches here, so a `>`-terminated scan cannot run
174
+ * past a tag or match a literal `<img` a note merely talks about.
175
+ */
176
+ const IMG_TAG_PATTERN = /<img\b[^>]*>/gi;
177
+ /** Matches the `src="..."` attribute inside one already-matched `<img>` tag. */
178
+ const SRC_ATTRIBUTE_PATTERN = /(\ssrc=")([^"]*)(")/i;
179
+ /**
180
+ * Rewrites every embeddable local `<img src>` in `html` into a `data:` URI,
181
+ * using `readImage` to fetch each source's bytes, and reports what happened
182
+ * to every image it saw.
183
+ *
184
+ * Each distinct source is read at most once, so a note that shows the same
185
+ * picture five times reads and encodes it once and embeds the same URI five
186
+ * times. Never throws: a reader that throws is recorded as `unreadable` and
187
+ * that one image keeps its original source.
188
+ */
189
+ export async function embedImagesInHtml(html, readImage) {
190
+ const embedded = [];
191
+ const skipped = [];
192
+ let embeddedBytes = 0;
193
+ let remote = 0;
194
+ // Resolved once per distinct source: `undefined` means "keep the original".
195
+ const dataUris = new Map();
196
+ const tags = html.match(IMG_TAG_PATTERN) ?? [];
197
+ for (const tag of tags) {
198
+ const attribute = SRC_ATTRIBUTE_PATTERN.exec(tag);
199
+ if (!attribute)
200
+ continue;
201
+ const rawSrc = attribute[2] ?? '';
202
+ const src = decodeAttributeValue(rawSrc);
203
+ if (!isEmbeddableImageSrc(src)) {
204
+ if (src.length > 0 && !src.startsWith('#'))
205
+ remote += 1;
206
+ continue;
207
+ }
208
+ if (dataUris.has(src))
209
+ continue;
210
+ const mimeType = mimeTypeForImageSrc(src);
211
+ if (mimeType === undefined) {
212
+ dataUris.set(src, undefined);
213
+ skipped.push({ src, reason: 'unsupported-type' });
214
+ continue;
215
+ }
216
+ let result;
217
+ try {
218
+ result = await readImage(src);
219
+ }
220
+ catch (error) {
221
+ result = {
222
+ kind: 'unreadable',
223
+ detail: error instanceof Error ? error.message : String(error),
224
+ };
225
+ }
226
+ if (result.kind === 'oversize') {
227
+ dataUris.set(src, undefined);
228
+ skipped.push({
229
+ src,
230
+ reason: 'too-large',
231
+ byteLength: result.byteLength,
232
+ });
233
+ continue;
234
+ }
235
+ if (result.kind === 'unreadable') {
236
+ dataUris.set(src, undefined);
237
+ skipped.push({
238
+ src,
239
+ reason: 'unreadable',
240
+ ...(result.detail !== undefined ? { detail: result.detail } : {}),
241
+ });
242
+ continue;
243
+ }
244
+ // Re-checked here even though a host may have checked already: the cap
245
+ // is this module's rule, and a host that cannot cheaply stat a file
246
+ // legitimately reads first and lets this decide.
247
+ if (result.bytes.byteLength > MAX_EMBEDDED_IMAGE_BYTES) {
248
+ dataUris.set(src, undefined);
249
+ skipped.push({
250
+ src,
251
+ reason: 'too-large',
252
+ byteLength: result.bytes.byteLength,
253
+ });
254
+ continue;
255
+ }
256
+ dataUris.set(src, toImageDataUri(result.bytes, mimeType));
257
+ embedded.push(src);
258
+ embeddedBytes += result.bytes.byteLength;
259
+ }
260
+ const rewritten = html.replace(IMG_TAG_PATTERN, (tag) => {
261
+ const attribute = SRC_ATTRIBUTE_PATTERN.exec(tag);
262
+ if (!attribute)
263
+ return tag;
264
+ const src = decodeAttributeValue(attribute[2] ?? '');
265
+ const dataUri = dataUris.get(src);
266
+ if (dataUri === undefined)
267
+ return tag;
268
+ return tag.replace(SRC_ATTRIBUTE_PATTERN, (_match, before, _value, after) => `${before}${dataUri}${after}`);
269
+ });
270
+ return {
271
+ html: rewritten,
272
+ report: { embedded, embeddedBytes, skipped, remote },
273
+ };
274
+ }
@@ -0,0 +1,332 @@
1
+ import type { ValueStore } from '@markii/runtime';
2
+ import type { EmbeddedImageReport, ExportImageReader } from './image-embed.js';
3
+ import type { StoredValue } from '@markii/runtime';
4
+ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
5
+ /** The canonical Markii file extension, matching `apps/vscode/src/mark-document.ts`'s `MARK_EXTENSION`. */
6
+ export declare const MARK_EXTENSION = ".mk.md";
7
+ /** The extensions an export can be written as. */
8
+ export type ExportExtension = '.html' | '.pdf' | '.txt' | '.md';
9
+ /**
10
+ * The name used when a note has no usable base name at all — a file called
11
+ * exactly `.mk.md`, or an empty path segment. Rare enough that neither host
12
+ * offers it as a normal path, but a file name is not optional here, so
13
+ * there has to be one rather than an empty string.
14
+ */
15
+ export declare const FALLBACK_EXPORT_BASE_NAME = "markii-note";
16
+ /**
17
+ * The base name an export is built from: the file name with a recognized
18
+ * Markii/markdown extension removed, case-insensitively. `notes.mk.md` and
19
+ * `notes.md` both give `notes`; a name with any other extension keeps it
20
+ * (`notes.txt` gives `notes.txt`), because guessing at an unknown extension
21
+ * would be more surprising than leaving it alone. A `/`-separated path is
22
+ * accepted too: only its last segment is read.
23
+ */
24
+ export declare function exportBaseName(pathOrName: string): string;
25
+ /** The exported file's name for `fileName`: `notes.mk.md` + `.html` gives `notes.html`. */
26
+ export declare function exportedFileName(fileName: string, extension: ExportExtension): string;
27
+ /**
28
+ * The path an export is written to when it lands beside its source note:
29
+ * the same folder, the same base name, the new extension. `notePath` is a
30
+ * `/`-separated path — a `vscode.Uri.path` or an Obsidian vault-relative
31
+ * path, never a platform `fsPath` — so no `node:path` import and no
32
+ * platform branching is needed, and this module stays environment-free.
33
+ */
34
+ export declare function exportedSiblingPath(notePath: string, extension: ExportExtension): string;
35
+ /** The exported document's `<title>`: the note's own base name, so a browser tab and a PDF's metadata name the note rather than the format. */
36
+ export declare function exportDocumentTitle(fileName: string): string;
37
+ /**
38
+ * The page-level CSS an exported file carries in addition to `doc.css`.
39
+ *
40
+ * `doc.css` is deliberately a LIBRARY stylesheet: it styles `.doc` and
41
+ * never claims `:root` or `body`, because hosts embed it into pages they
42
+ * do not own. A standalone exported file has no host to supply the page
43
+ * around it, so this is that page: a ground color, a readable measure, and
44
+ * print rules, all of them host-authored and trusted (they are passed to
45
+ * the engine's `composeDocument` as `extraCss`, which is inserted verbatim).
46
+ *
47
+ * The print block matters more than it looks: the Obsidian PDF command
48
+ * prints exactly this document, so the paper layout is decided here rather
49
+ * than in a host-specific branch. `@page` sets the sheet's own margins
50
+ * (the PDF command has no separate page-setup step of its own), the
51
+ * `break-inside: avoid` list keeps a card/callout/table from splitting
52
+ * across two pages wherever it fits on one, and the collapsed script
53
+ * marker and the VS Code webview's run marker are hidden on paper: both
54
+ * are live-preview affordances (a disclosure toggle, a status line about
55
+ * the last run) that mean nothing on a printed or archived page.
56
+ */
57
+ /**
58
+ * The class `composeNoteHtmlExport`/`buildNoteHtmlExport` put on the `.doc`
59
+ * wrapper when `hideScriptBlocks` is on, and the class `EXPORT_PAGE_CSS`'s
60
+ * rule below is scoped to.
61
+ *
62
+ * Both hosts already have this preference (VS Code's
63
+ * `markii.hideScriptBlocks`, Obsidian's "Hide script blocks" setting) for
64
+ * their live preview, hiding only the collapsed `⚙ name` marker and
65
+ * nothing else — a failure a reader needs to see is never hidden along
66
+ * with it. An export carries whichever value the exporting host's own
67
+ * preview was showing, so a note exported with scripts hidden in the
68
+ * editor stays that way in the file.
69
+ */
70
+ export declare const EXPORT_HIDE_SCRIPT_BLOCKS_CLASS = "mk-export--hide-scripts";
71
+ export declare const EXPORT_PAGE_CSS = "\nbody {\n margin: 0;\n background: #fff;\n}\n.doc {\n box-sizing: border-box;\n max-width: 46rem;\n margin: 0 auto;\n padding: 2.5rem 1.5rem 4rem;\n}\n.doc.mk-export--hide-scripts .mk-script {\n display: none;\n}\n@page {\n margin: 2cm;\n}\n@media print {\n .doc {\n max-width: none;\n margin: 0;\n padding: 0;\n }\n .doc .mk-card,\n .doc .mk-callout,\n .doc .mk-stat,\n .doc .mk-figure,\n .doc .mk-unknown,\n .doc table {\n break-inside: avoid;\n }\n /* A collapsed `details` prints as its summary alone, which loses the\n content the author wrote. On paper there is nothing to expand, so\n every disclosure prints open. */\n .doc details > *:not(summary) {\n display: block;\n }\n .doc .mk-script,\n .doc .mk-preview__run-marker {\n display: none;\n }\n}\n";
72
+ /**
73
+ * The static HTML engine, injected so this module imports no renderer.
74
+ * A host passes `createHtmlExportEngine()` from `@markii/host/node`, which
75
+ * wraps `@markii/html`; a test may pass a stub.
76
+ */
77
+ export interface HtmlExportEngine {
78
+ /** Renders one note's source to a body string with the standard component set, last-run values bound through `store`. Never throws. */
79
+ renderBody(text: string, store: ValueStore | undefined, diagnostics: {
80
+ readonly unavailableComponents?: readonly UnavailableComponent[];
81
+ readonly onDiagnostic?: OnDiagnostic;
82
+ }): string;
83
+ /** Wraps a body in the full-page shell (`doc.css` embedded). */
84
+ composeDocument(bodyHtml: string, options: {
85
+ readonly title: string;
86
+ readonly extraCss: string;
87
+ readonly docClassName?: string;
88
+ }): string;
89
+ /** The one HTML-escaping primitive. */
90
+ escapeHtml(value: string): string;
91
+ }
92
+ /** What `buildNoteHtmlExport` needs to turn one note into a standalone file. */
93
+ export interface NoteHtmlExportOptions {
94
+ /** The static HTML engine. */
95
+ readonly engine: HtmlExportEngine;
96
+ /** The note's full source text. */
97
+ readonly text: string;
98
+ /** The note's file name, used for the document title. A path is accepted; only the last segment is read. */
99
+ readonly fileName: string;
100
+ /**
101
+ * The note's last-run values, exactly as the host persisted them
102
+ * (`readPersistedValues`). Bound components and `:value[...]` markers
103
+ * bake these in. Omitted or empty means the note exports with its
104
+ * standard empty states, which is what a note that has never been run
105
+ * should show.
106
+ */
107
+ readonly values?: Record<string, StoredValue>;
108
+ /** Extra host CSS appended after `doc.css` and `EXPORT_PAGE_CSS`. Trusted, inserted verbatim. */
109
+ readonly extraCss?: string;
110
+ /**
111
+ * Hides the collapsed script marker in the exported file, mirroring the
112
+ * exporting host's own `hideScriptBlocks` preview preference. Defaults to
113
+ * `false`, which exports every script marker exactly as the preview
114
+ * would show it with the preference off.
115
+ */
116
+ readonly hideScriptBlocks?: boolean;
117
+ /** Installed pack components the static engine cannot draw; see `NoteExportBuildRequest`. */
118
+ readonly unavailableComponents?: readonly UnavailableComponent[];
119
+ /** Receives the static engine's render diagnostics. */
120
+ readonly onDiagnostic?: OnDiagnostic;
121
+ }
122
+ /**
123
+ * Renders one note to a complete, self-contained HTML document string.
124
+ *
125
+ * Never throws: `renderMarkToHtml` already returns its own failure fallback
126
+ * rather than propagating, so a caller always gets a document it can write.
127
+ */
128
+ export declare function buildNoteHtmlExport(options: NoteHtmlExportOptions): string;
129
+ /**
130
+ * ISSUE #28 SLICE 2: rendering an export through a host's REACT engine.
131
+ *
132
+ * Slice 1 rendered every export with `@markii/html`, so a pack directive
133
+ * came out as that engine's unknown-component box. The static engine cannot
134
+ * load a pack, because a pack component is a React module. The fix is not a
135
+ * second component system: it is to let a host that ALREADY has React and a
136
+ * merged registry in front of it (both do, for the preview) render the
137
+ * export body with exactly what the preview renders, and hand the resulting
138
+ * STRING back here.
139
+ *
140
+ * That keeps this package React-free. Composition is the host-neutral part
141
+ * (the page shell, `doc.css`, the print rules, the pack stylesheets, the
142
+ * file name), and it needs no renderer at all once it has a body string.
143
+ * The React render itself happens where React already lives: the VS Code
144
+ * webview, and the Obsidian plugin's own renderer process.
145
+ */
146
+ /** One loaded pack's emitted stylesheet, as a host already holds it for its preview. */
147
+ export interface ExportPackStylesheet {
148
+ /** The pack's namespace, used only to label the block in the exported file. */
149
+ readonly namespace: string;
150
+ /** The stylesheet's text, exactly as the host injects it into its preview. */
151
+ readonly cssText: string;
152
+ }
153
+ /**
154
+ * Why an export fell back to the static engine. Diagnostics-facing: each
155
+ * host words these itself, since the thing a user would do about it differs
156
+ * per host.
157
+ *
158
+ * - `no-packs`: no pack components were loaded, so the static engine
159
+ * renders precisely what the React engine would have. Not a degradation.
160
+ * - `no-renderer`: the host had no React surface to render through at all
161
+ * (VS Code with no preview panel open).
162
+ * - `timeout`: the host's renderer was asked and did not answer in time.
163
+ * - `render-failed`: the renderer answered with a failure, or threw.
164
+ */
165
+ export type StaticExportReason = 'no-packs' | 'no-renderer' | 'timeout' | 'render-failed';
166
+ /** Which engine actually rendered an export's body, and what that means. */
167
+ export type ExportRenderInfo = {
168
+ readonly engine: 'react';
169
+ /** How many loaded packs contributed components to the registry that rendered this file. */
170
+ readonly packCount: number;
171
+ /** How many of those packs contributed a stylesheet embedded in the file. */
172
+ readonly stylesheetCount: number;
173
+ } | {
174
+ readonly engine: 'static';
175
+ readonly reason: StaticExportReason;
176
+ /** The verbatim detail behind a `timeout`/`render-failed`, for the diagnostics surface only. */
177
+ readonly detail?: string;
178
+ };
179
+ /** What a host's React renderer returns: the body markup, or why it could not produce one. */
180
+ export type ExportBodyResult = {
181
+ readonly ok: true;
182
+ readonly html: string;
183
+ } | {
184
+ readonly ok: false;
185
+ readonly reason: Extract<StaticExportReason, 'timeout' | 'render-failed'>;
186
+ readonly detail?: string;
187
+ };
188
+ /**
189
+ * A host's React render of one note's body, as a string. Given the note's
190
+ * text and the values to bake in, it returns the markup that goes INSIDE
191
+ * the exported document's `.doc` wrapper. It is expected not to throw; one
192
+ * that does is caught here and treated as `render-failed`, so a caller
193
+ * always gets a document it can write.
194
+ */
195
+ export type ExportBodyRenderer = (text: string, values: Record<string, StoredValue>) => ExportBodyResult | Promise<ExportBodyResult>;
196
+ /**
197
+ * The pack stylesheets as one CSS block, in the order the host loaded them
198
+ * — the same order its preview injects them, which is what decides the
199
+ * cascade between two packs that style the same thing. Each block is
200
+ * labeled with its pack's namespace so a reader of the exported file can
201
+ * tell whose rules these are.
202
+ *
203
+ * Placed AFTER `doc.css` and `EXPORT_PAGE_CSS` by `composeNoteHtmlExport`,
204
+ * matching docs/packs.md's load order: a pack sees resolved `--mk-*` token
205
+ * values and is not overridden by the document stylesheet's broader rules.
206
+ */
207
+ export declare function packStylesheetsCss(sheets: readonly ExportPackStylesheet[]): string;
208
+ /** What `composeNoteHtmlExport` needs to wrap an already-rendered body in the export shell. */
209
+ export interface ComposeNoteHtmlExportOptions {
210
+ /** The static HTML engine. */
211
+ readonly engine: HtmlExportEngine;
212
+ /** The rendered body markup that goes inside the `.doc` wrapper. Inserted verbatim; it is expected to be renderer output, already escaped. */
213
+ readonly bodyHtml: string;
214
+ /** The note's file name, used for the document title. A path is accepted; only the last segment is read. */
215
+ readonly fileName: string;
216
+ /** The loaded packs' stylesheets, appended after `doc.css` and `EXPORT_PAGE_CSS`. */
217
+ readonly packStylesheets?: readonly ExportPackStylesheet[];
218
+ /** Extra host CSS, appended after `EXPORT_PAGE_CSS` and before the pack stylesheets. Trusted, inserted verbatim. */
219
+ readonly extraCss?: string;
220
+ /** Hides the collapsed script marker in the exported file. See `NoteHtmlExportOptions.hideScriptBlocks`. Defaults to `false`. */
221
+ readonly hideScriptBlocks?: boolean;
222
+ }
223
+ /**
224
+ * Wraps an already-rendered body in the standalone export page: the same
225
+ * shell, `doc.css`, and print rules `buildNoteHtmlExport` produces, plus
226
+ * every loaded pack's stylesheet so pack components in the body are styled.
227
+ *
228
+ * Host-neutral and renderer-neutral on purpose: it takes a STRING, so the
229
+ * React path and the static path compose the identical page and can never
230
+ * drift on what an exported file carries.
231
+ */
232
+ export declare function composeNoteHtmlExport(options: ComposeNoteHtmlExportOptions): string;
233
+ /** What `buildNoteExport` needs. Everything except `text`/`fileName` is optional; omitting `renderBody` is the plain static export. */
234
+ export interface NoteExportBuildRequest {
235
+ /** The static HTML engine. */
236
+ readonly engine: HtmlExportEngine;
237
+ /** The note's full source text. */
238
+ readonly text: string;
239
+ /** The note's file name, used for the document title. A path is accepted; only the last segment is read. */
240
+ readonly fileName: string;
241
+ /** The note's last-run values, baked in wherever the note binds one. Omitted or empty exports the note's standard empty states. */
242
+ readonly values?: Record<string, StoredValue>;
243
+ /**
244
+ * The host's React render of the body. Omitted when the host has nothing
245
+ * to render through, in which case `staticReason` says which case that
246
+ * is and the static engine renders instead.
247
+ */
248
+ readonly renderBody?: ExportBodyRenderer;
249
+ /** Why the static engine is being used, when `renderBody` is omitted. Defaults to `no-packs`. */
250
+ readonly staticReason?: StaticExportReason;
251
+ /** The loaded packs' stylesheets. Embedded only when the React path actually rendered the body; the static engine's output never references them. */
252
+ readonly packStylesheets?: readonly ExportPackStylesheet[];
253
+ /** How many loaded packs contributed components to the registry that rendered this file. Diagnostics only. */
254
+ readonly packCount?: number;
255
+ /** Extra host CSS appended after `EXPORT_PAGE_CSS`. Trusted, inserted verbatim. */
256
+ readonly extraCss?: string;
257
+ /**
258
+ * Reads one of the note's local images, so the export can embed it as a
259
+ * `data:` URI and stand on its own (`./image-embed.ts`, issue #28 slice
260
+ * 3). Omitted leaves every image source exactly as the author wrote it,
261
+ * which is slice 2's behavior.
262
+ */
263
+ readonly embedImages?: ExportImageReader;
264
+ /** Hides the collapsed script marker in the exported file. See `NoteHtmlExportOptions.hideScriptBlocks`. Defaults to `false`. */
265
+ readonly hideScriptBlocks?: boolean;
266
+ /**
267
+ * Installed pack components the static engine cannot draw. A directive
268
+ * naming one reads "no HTML view for" instead of "unknown component" in
269
+ * the exported body. Ignored on the React path, whose host passes the
270
+ * same list to its own renderer inside `renderBody`.
271
+ */
272
+ readonly unavailableComponents?: readonly UnavailableComponent[];
273
+ /** Receives the static engine's render diagnostics (the `no-engine-view` event). Same scope as `unavailableComponents`. */
274
+ readonly onDiagnostic?: OnDiagnostic;
275
+ }
276
+ /** One built export: the complete document, how it was rendered, and how many values it baked in. */
277
+ export interface NoteExportDocument {
278
+ /** The complete, self-contained HTML document. */
279
+ readonly html: string;
280
+ /** Which engine rendered the body, and why when it was the static one. */
281
+ readonly render: ExportRenderInfo;
282
+ /** How many last-run values were baked in. */
283
+ readonly valueCount: number;
284
+ /** What image embedding did, for the host's diagnostics surface. Empty when no `embedImages` reader was offered. */
285
+ readonly images: EmbeddedImageReport;
286
+ }
287
+ /**
288
+ * Builds one note's standalone export document, through the host's React
289
+ * engine when it offered one and through `@markii/html` otherwise.
290
+ *
291
+ * Never throws and never leaves a caller without a file: a renderer that
292
+ * fails, times out, or throws is classified, recorded in `render`, and
293
+ * replaced by the static engine's output, which is the same document slice
294
+ * 1 produced.
295
+ */
296
+ export declare function buildNoteExport(request: NoteExportBuildRequest): Promise<NoteExportDocument>;
297
+ /**
298
+ * Whether `text` contains at least one Markii script fence, i.e. a code
299
+ * fence whose info string names `lua`. Both hosts' export notices use this
300
+ * so a note with no scripts is never told to run itself first.
301
+ */
302
+ export declare function noteHasScripts(text: string): boolean;
303
+ /**
304
+ * One note a cascade index page links to (GitHub issue #28 slice 3, part
305
+ * 2's `index.html`).
306
+ */
307
+ export interface CascadeIndexEntry {
308
+ /**
309
+ * The note's title as shown on the index page — the same base name
310
+ * `exportDocumentTitle` gives that note's own `<title>` tag, so the two
311
+ * pages never name a note two different ways.
312
+ */
313
+ readonly title: string;
314
+ /** The note's exported file name inside the archive, e.g. `week-2.html`. Used as the link's `href`, relative to `index.html`. */
315
+ readonly fileName: string;
316
+ }
317
+ /**
318
+ * Builds a cascade archive's `index.html`: a self-contained document
319
+ * listing every exported note by title, each linking to its own file.
320
+ *
321
+ * A cascade archive has no single note to open first once it is
322
+ * unzipped, and without this a reader has to guess a file name or open the
323
+ * archive's contents blind. `entries` is expected in the same order the
324
+ * cascade walked (root note first, breadth first), which is what makes the
325
+ * list read as a sensible table of contents rather than an alphabetized
326
+ * jumble.
327
+ *
328
+ * Uses the same page shell every exported note gets (`doc.css`,
329
+ * `EXPORT_PAGE_CSS`), so the index reads as part of the same export rather
330
+ * than a bare, unstyled file dropped in beside it.
331
+ */
332
+ export declare function buildCascadeIndexHtml(entries: readonly CascadeIndexEntry[], engine: HtmlExportEngine): string;