@markii/host 0.0.0-stage → 0.17.0-next.2

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/LICENSE +21 -0
  2. package/README.md +19 -2
  3. package/dist/browser-worker.js +639 -0
  4. package/dist/browser.d.ts +57 -0
  5. package/dist/browser.js +167 -0
  6. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  7. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  8. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  11. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  14. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  17. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  20. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  21. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  23. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  24. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  25. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  26. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  27. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  28. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  29. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  31. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  32. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  33. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  34. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  35. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  36. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  37. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  38. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  40. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  41. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  43. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  46. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  47. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  48. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  49. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  51. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  52. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  53. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  58. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  59. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  61. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  63. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  64. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  65. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  66. package/dist/conformance/host/README.md +221 -0
  67. package/dist/conformance.d.ts +2 -0
  68. package/dist/conformance.js +5 -0
  69. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  70. package/dist/diagnostics/render-diagnostics.js +46 -0
  71. package/dist/export/cascade.d.ts +98 -0
  72. package/dist/export/cascade.js +159 -0
  73. package/dist/export/export-zip.d.ts +19 -0
  74. package/dist/export/export-zip.js +38 -0
  75. package/dist/export/image-embed.d.ts +126 -0
  76. package/dist/export/image-embed.js +274 -0
  77. package/dist/export/note-export.d.ts +332 -0
  78. package/dist/export/note-export.js +366 -0
  79. package/dist/export/note-links.d.ts +83 -0
  80. package/dist/export/note-links.js +241 -0
  81. package/dist/glue.wasm +0 -0
  82. package/dist/host/adapter.d.ts +223 -0
  83. package/dist/host/adapter.js +9 -0
  84. package/dist/host/cascade-export-behavior.d.ts +120 -0
  85. package/dist/host/cascade-export-behavior.js +151 -0
  86. package/dist/host/conformance-runner.d.ts +156 -0
  87. package/dist/host/conformance-runner.js +634 -0
  88. package/dist/host/create-host.d.ts +253 -0
  89. package/dist/host/create-host.js +329 -0
  90. package/dist/host/editor-behavior.d.ts +157 -0
  91. package/dist/host/editor-behavior.js +227 -0
  92. package/dist/host/export/md-plain.d.ts +15 -0
  93. package/dist/host/export/md-plain.js +125 -0
  94. package/dist/host/export-behavior.d.ts +113 -0
  95. package/dist/host/export-behavior.js +257 -0
  96. package/dist/host/labels.d.ts +24 -0
  97. package/dist/host/labels.js +32 -0
  98. package/dist/host/pack-archive.d.ts +30 -0
  99. package/dist/host/pack-archive.js +68 -0
  100. package/dist/host/pack-install.d.ts +88 -0
  101. package/dist/host/pack-install.js +189 -0
  102. package/dist/host/pack-load.d.ts +72 -0
  103. package/dist/host/pack-load.js +57 -0
  104. package/dist/host/pack-remove.d.ts +21 -0
  105. package/dist/host/pack-remove.js +65 -0
  106. package/dist/host/pack-trust-behavior.d.ts +79 -0
  107. package/dist/host/pack-trust-behavior.js +262 -0
  108. package/dist/host/refresh-interval.d.ts +39 -0
  109. package/dist/host/refresh-interval.js +61 -0
  110. package/dist/host/run-behavior.d.ts +89 -0
  111. package/dist/host/run-behavior.js +168 -0
  112. package/dist/host/run-diagnostics.d.ts +26 -0
  113. package/dist/host/run-diagnostics.js +43 -0
  114. package/dist/host/script-execution.d.ts +72 -0
  115. package/dist/host/script-execution.js +68 -0
  116. package/dist/host/terminal-packs.d.ts +39 -0
  117. package/dist/host/terminal-packs.js +51 -0
  118. package/dist/html-engine.d.ts +2 -0
  119. package/dist/html-engine.js +29 -0
  120. package/dist/index.d.ts +95 -0
  121. package/dist/index.js +70 -0
  122. package/dist/insert/component-catalog.d.ts +23 -0
  123. package/dist/insert/component-catalog.js +96 -0
  124. package/dist/node-fs.d.ts +14 -0
  125. package/dist/node-fs.js +70 -0
  126. package/dist/node.d.ts +5 -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 +81 -0
  132. package/dist/packs/pack-diagnostics.js +105 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +106 -0
  135. package/dist/packs/pack-render-registry.d.ts +83 -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 +81 -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 +57 -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 +70 -0
  166. package/dist/run/net-provider.js +289 -0
  167. package/dist/run/node-isolate.d.ts +30 -0
  168. package/dist/run/node-isolate.js +117 -0
  169. package/dist/run/run-flow.d.ts +187 -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 +42 -0
  186. package/dist/run/worker-entry-browser.js +77 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +96 -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 +84 -4
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Static-export plumbing shared by every host (GitHub issue #28, slice 1):
3
+ * turning one note's text plus its last-run values into a self-contained
4
+ * HTML document, and working out what the resulting file is called.
5
+ *
6
+ * This builds NO renderer of its own. `@markii/html` is the static engine
7
+ * (`packages/platforms/markii-html`): `renderMarkToHtml` walks
8
+ * `@markii/core`'s sanitized hast to an HTML string, and
9
+ * `exportHtmlDocument` wraps that string in the full-page shell with the
10
+ * shared `doc.css` embedded. Everything here is the thin host-neutral layer
11
+ * around those two calls, so the VS Code extension and the Obsidian plugin
12
+ * cannot drift on file naming, on the page CSS an exported file carries, or
13
+ * on how a value store is handed to the engine.
14
+ *
15
+ * WHAT AN EXPORT CONTAINS. The standard component set, with the last run's
16
+ * values baked in wherever a note binds one (`data=`, `:value[...]`), plus
17
+ * every pack component the exporting host has loaded (issue #28 slice 2 —
18
+ * see `buildNoteExport` and `composeNoteHtmlExport` at the bottom of this
19
+ * file). A host with React and a merged registry in front of it renders the
20
+ * body itself and hands the string here; a host with neither falls back to
21
+ * the static engine, where a pack directive comes out as that engine's
22
+ * ordinary unknown-component fallback: a labeled box with the inner
23
+ * markdown still rendered inside it. Nothing crashes and no content is
24
+ * dropped either way; see `@markii/html`'s `render.ts`.
25
+ */
26
+ import { createValueStore } from '@markii/runtime';
27
+ import { EMPTY_IMAGE_REPORT, embedImagesInHtml } from './image-embed.js';
28
+ /** The last path segment of a `/`-separated path, or the string itself when it has no separator. */
29
+ function lastSegment(pathOrName) {
30
+ const separator = pathOrName.lastIndexOf('/');
31
+ return separator === -1 ? pathOrName : pathOrName.slice(separator + 1);
32
+ }
33
+ /** The canonical Markii file extension, matching `apps/vscode/src/mark-document.ts`'s `MARK_EXTENSION`. */
34
+ export const MARK_EXTENSION = '.mk.md';
35
+ /**
36
+ * The name used when a note has no usable base name at all — a file called
37
+ * exactly `.mk.md`, or an empty path segment. Rare enough that neither host
38
+ * offers it as a normal path, but a file name is not optional here, so
39
+ * there has to be one rather than an empty string.
40
+ */
41
+ export const FALLBACK_EXPORT_BASE_NAME = 'markii-note';
42
+ /**
43
+ * The base name an export is built from: the file name with a recognized
44
+ * Markii/markdown extension removed, case-insensitively. `notes.mk.md` and
45
+ * `notes.md` both give `notes`; a name with any other extension keeps it
46
+ * (`notes.txt` gives `notes.txt`), because guessing at an unknown extension
47
+ * would be more surprising than leaving it alone. A `/`-separated path is
48
+ * accepted too: only its last segment is read.
49
+ */
50
+ export function exportBaseName(pathOrName) {
51
+ const fileName = lastSegment(pathOrName);
52
+ const lower = fileName.toLowerCase();
53
+ if (lower.endsWith(MARK_EXTENSION)) {
54
+ const base = fileName.slice(0, fileName.length - MARK_EXTENSION.length);
55
+ return base || FALLBACK_EXPORT_BASE_NAME;
56
+ }
57
+ if (lower.endsWith('.md')) {
58
+ const base = fileName.slice(0, fileName.length - '.md'.length);
59
+ return base || FALLBACK_EXPORT_BASE_NAME;
60
+ }
61
+ return fileName || FALLBACK_EXPORT_BASE_NAME;
62
+ }
63
+ /** The exported file's name for `fileName`: `notes.mk.md` + `.html` gives `notes.html`. */
64
+ export function exportedFileName(fileName, extension) {
65
+ return `${exportBaseName(fileName)}${extension}`;
66
+ }
67
+ /**
68
+ * The path an export is written to when it lands beside its source note:
69
+ * the same folder, the same base name, the new extension. `notePath` is a
70
+ * `/`-separated path — a `vscode.Uri.path` or an Obsidian vault-relative
71
+ * path, never a platform `fsPath` — so no `node:path` import and no
72
+ * platform branching is needed, and this module stays environment-free.
73
+ */
74
+ export function exportedSiblingPath(notePath, extension) {
75
+ const separator = notePath.lastIndexOf('/');
76
+ const folder = separator === -1 ? '' : notePath.slice(0, separator + 1);
77
+ const fileName = notePath.slice(separator + 1);
78
+ return `${folder}${exportedFileName(fileName, extension)}`;
79
+ }
80
+ /** 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. */
81
+ export function exportDocumentTitle(fileName) {
82
+ return exportBaseName(fileName);
83
+ }
84
+ /**
85
+ * The page-level CSS an exported file carries in addition to `doc.css`.
86
+ *
87
+ * `doc.css` is deliberately a LIBRARY stylesheet: it styles `.doc` and
88
+ * never claims `:root` or `body`, because hosts embed it into pages they
89
+ * do not own. A standalone exported file has no host to supply the page
90
+ * around it, so this is that page: a ground color, a readable measure, and
91
+ * print rules, all of them host-authored and trusted (they are passed to
92
+ * the engine's `composeDocument` as `extraCss`, which is inserted verbatim).
93
+ *
94
+ * The print block matters more than it looks: the Obsidian PDF command
95
+ * prints exactly this document, so the paper layout is decided here rather
96
+ * than in a host-specific branch. `@page` sets the sheet's own margins
97
+ * (the PDF command has no separate page-setup step of its own), the
98
+ * `break-inside: avoid` list keeps a card/callout/table from splitting
99
+ * across two pages wherever it fits on one, and the collapsed script
100
+ * marker and the VS Code webview's run marker are hidden on paper: both
101
+ * are live-preview affordances (a disclosure toggle, a status line about
102
+ * the last run) that mean nothing on a printed or archived page.
103
+ */
104
+ /**
105
+ * The class `composeNoteHtmlExport`/`buildNoteHtmlExport` put on the `.doc`
106
+ * wrapper when `hideScriptBlocks` is on, and the class `EXPORT_PAGE_CSS`'s
107
+ * rule below is scoped to.
108
+ *
109
+ * Both hosts already have this preference (VS Code's
110
+ * `markii.hideScriptBlocks`, Obsidian's "Hide script blocks" setting) for
111
+ * their live preview, hiding only the collapsed `⚙ name` marker and
112
+ * nothing else — a failure a reader needs to see is never hidden along
113
+ * with it. An export carries whichever value the exporting host's own
114
+ * preview was showing, so a note exported with scripts hidden in the
115
+ * editor stays that way in the file.
116
+ */
117
+ export const EXPORT_HIDE_SCRIPT_BLOCKS_CLASS = 'mk-export--hide-scripts';
118
+ export const EXPORT_PAGE_CSS = `
119
+ body {
120
+ margin: 0;
121
+ background: #fff;
122
+ }
123
+ .doc {
124
+ box-sizing: border-box;
125
+ max-width: 46rem;
126
+ margin: 0 auto;
127
+ padding: 2.5rem 1.5rem 4rem;
128
+ }
129
+ .doc.${EXPORT_HIDE_SCRIPT_BLOCKS_CLASS} .mk-script {
130
+ display: none;
131
+ }
132
+ @page {
133
+ margin: 2cm;
134
+ }
135
+ @media print {
136
+ .doc {
137
+ max-width: none;
138
+ margin: 0;
139
+ padding: 0;
140
+ }
141
+ .doc .mk-card,
142
+ .doc .mk-callout,
143
+ .doc .mk-stat,
144
+ .doc .mk-figure,
145
+ .doc .mk-unknown,
146
+ .doc table {
147
+ break-inside: avoid;
148
+ }
149
+ /* A collapsed \`details\` prints as its summary alone, which loses the
150
+ content the author wrote. On paper there is nothing to expand, so
151
+ every disclosure prints open. */
152
+ .doc details > *:not(summary) {
153
+ display: block;
154
+ }
155
+ .doc .mk-script,
156
+ .doc .mk-preview__run-marker {
157
+ display: none;
158
+ }
159
+ }
160
+ `;
161
+ /**
162
+ * Renders one note to a complete, self-contained HTML document string.
163
+ *
164
+ * Never throws: `renderMarkToHtml` already returns its own failure fallback
165
+ * rather than propagating, so a caller always gets a document it can write.
166
+ */
167
+ export function buildNoteHtmlExport(options) {
168
+ return composeNoteHtmlExport({
169
+ bodyHtml: renderStaticBody(options.engine, options.text, options.values ?? {}, options),
170
+ engine: options.engine,
171
+ fileName: options.fileName,
172
+ ...(options.extraCss !== undefined ? { extraCss: options.extraCss } : {}),
173
+ ...(options.hideScriptBlocks !== undefined
174
+ ? { hideScriptBlocks: options.hideScriptBlocks }
175
+ : {}),
176
+ });
177
+ }
178
+ /**
179
+ * The static engine's body for one note: `@markii/html` walking
180
+ * `@markii/core`'s sanitized hast to a string, with the last run's values
181
+ * baked in. The one place `renderMarkToHtml` is called, so the static path
182
+ * and every React path's fallback produce the identical body.
183
+ */
184
+ function renderStaticBody(engine, text, values, diagnostics = {}) {
185
+ const store = Object.keys(values).length > 0 ? createValueStore(values) : undefined;
186
+ return engine.renderBody(text, store, {
187
+ ...(diagnostics.unavailableComponents !== undefined
188
+ ? { unavailableComponents: diagnostics.unavailableComponents }
189
+ : {}),
190
+ ...(diagnostics.onDiagnostic !== undefined
191
+ ? { onDiagnostic: diagnostics.onDiagnostic }
192
+ : {}),
193
+ });
194
+ }
195
+ /**
196
+ * Neutralizes the one sequence that can end a `<style>` element early.
197
+ *
198
+ * Pack CSS is trusted in the security sense (docs/security.md: packs are
199
+ * user-installed), but trusted does not mean well-formed, and a stray
200
+ * `</style` in a pack's stylesheet would close the block and spill the rest
201
+ * of the sheet into the page as text. `<\/style` is a valid CSS escape for
202
+ * the same `/` inside a string, and outside a string a bare `</style` was
203
+ * never valid CSS anyway, so nothing legitimate changes meaning here.
204
+ */
205
+ function neutralizeStyleClose(cssText) {
206
+ return cssText.replace(/<\/(style)/gi, '<\\/$1');
207
+ }
208
+ /** A namespace reduced to what is safe inside a CSS comment label. Pack names are validated kebab-case upstream; this only guarantees the comment cannot be escaped. */
209
+ function commentSafeNamespace(namespace) {
210
+ const cleaned = namespace.replace(/[^a-zA-Z0-9._-]/g, '');
211
+ return cleaned.length > 0 ? cleaned : 'pack';
212
+ }
213
+ /**
214
+ * The pack stylesheets as one CSS block, in the order the host loaded them
215
+ * — the same order its preview injects them, which is what decides the
216
+ * cascade between two packs that style the same thing. Each block is
217
+ * labeled with its pack's namespace so a reader of the exported file can
218
+ * tell whose rules these are.
219
+ *
220
+ * Placed AFTER `doc.css` and `EXPORT_PAGE_CSS` by `composeNoteHtmlExport`,
221
+ * matching docs/packs.md's load order: a pack sees resolved `--mk-*` token
222
+ * values and is not overridden by the document stylesheet's broader rules.
223
+ */
224
+ export function packStylesheetsCss(sheets) {
225
+ return sheets
226
+ .map((sheet) => `/* pack: ${commentSafeNamespace(sheet.namespace)} */\n${neutralizeStyleClose(sheet.cssText)}`)
227
+ .join('\n');
228
+ }
229
+ /**
230
+ * Wraps an already-rendered body in the standalone export page: the same
231
+ * shell, `doc.css`, and print rules `buildNoteHtmlExport` produces, plus
232
+ * every loaded pack's stylesheet so pack components in the body are styled.
233
+ *
234
+ * Host-neutral and renderer-neutral on purpose: it takes a STRING, so the
235
+ * React path and the static path compose the identical page and can never
236
+ * drift on what an exported file carries.
237
+ */
238
+ export function composeNoteHtmlExport(options) {
239
+ const packCss = packStylesheetsCss(options.packStylesheets ?? []);
240
+ const parts = [EXPORT_PAGE_CSS];
241
+ if (options.extraCss)
242
+ parts.push(options.extraCss);
243
+ if (packCss)
244
+ parts.push(packCss);
245
+ return options.engine.composeDocument(options.bodyHtml, {
246
+ title: exportDocumentTitle(options.fileName),
247
+ extraCss: parts.join('\n'),
248
+ ...(options.hideScriptBlocks
249
+ ? { docClassName: EXPORT_HIDE_SCRIPT_BLOCKS_CLASS }
250
+ : {}),
251
+ });
252
+ }
253
+ /** The verbatim reason for a thrown value. Diagnostics only, never a notice. */
254
+ function detailOf(error) {
255
+ return error instanceof Error ? error.message : String(error);
256
+ }
257
+ /**
258
+ * Builds one note's standalone export document, through the host's React
259
+ * engine when it offered one and through `@markii/html` otherwise.
260
+ *
261
+ * Never throws and never leaves a caller without a file: a renderer that
262
+ * fails, times out, or throws is classified, recorded in `render`, and
263
+ * replaced by the static engine's output, which is the same document slice
264
+ * 1 produced.
265
+ */
266
+ export async function buildNoteExport(request) {
267
+ const values = request.values ?? {};
268
+ const valueCount = Object.keys(values).length;
269
+ /**
270
+ * The one place a body becomes a finished document, so image embedding
271
+ * and page composition cannot end up applied on one path and not the
272
+ * other. Images are embedded BEFORE composition, since the page shell is
273
+ * not a place an `<img>` can appear.
274
+ */
275
+ const finish = async (bodyHtml, render, packStylesheets) => {
276
+ const embedded = request.embedImages
277
+ ? await embedImagesInHtml(bodyHtml, request.embedImages)
278
+ : { html: bodyHtml, report: EMPTY_IMAGE_REPORT };
279
+ return {
280
+ html: composeNoteHtmlExport({
281
+ engine: request.engine,
282
+ bodyHtml: embedded.html,
283
+ fileName: request.fileName,
284
+ packStylesheets,
285
+ ...(request.extraCss !== undefined
286
+ ? { extraCss: request.extraCss }
287
+ : {}),
288
+ ...(request.hideScriptBlocks !== undefined
289
+ ? { hideScriptBlocks: request.hideScriptBlocks }
290
+ : {}),
291
+ }),
292
+ render,
293
+ valueCount,
294
+ images: embedded.report,
295
+ };
296
+ };
297
+ /** The static engine's document. Pack stylesheets are never embedded here: the static body has nothing that would use them. */
298
+ const staticDocument = (render) => finish(renderStaticBody(request.engine, request.text, values, request), render, []);
299
+ if (!request.renderBody) {
300
+ return staticDocument({
301
+ engine: 'static',
302
+ reason: request.staticReason ?? 'no-packs',
303
+ });
304
+ }
305
+ let result;
306
+ try {
307
+ result = await request.renderBody(request.text, values);
308
+ }
309
+ catch (error) {
310
+ return staticDocument({
311
+ engine: 'static',
312
+ reason: 'render-failed',
313
+ detail: detailOf(error),
314
+ });
315
+ }
316
+ if (!result.ok) {
317
+ return staticDocument({
318
+ engine: 'static',
319
+ reason: result.reason,
320
+ ...(result.detail !== undefined ? { detail: result.detail } : {}),
321
+ });
322
+ }
323
+ const packStylesheets = request.packStylesheets ?? [];
324
+ return finish(result.html, {
325
+ engine: 'react',
326
+ packCount: request.packCount ?? packStylesheets.length,
327
+ stylesheetCount: packStylesheets.length,
328
+ }, packStylesheets);
329
+ }
330
+ /**
331
+ * Whether `text` contains at least one Markii script fence, i.e. a code
332
+ * fence whose info string names `lua`. Both hosts' export notices use this
333
+ * so a note with no scripts is never told to run itself first.
334
+ */
335
+ export function noteHasScripts(text) {
336
+ return /^ {0,3}(?:`{3,}|~{3,})[ \t]*lua\b/m.test(text);
337
+ }
338
+ /** The title `exportHtmlDocument` gives the index page itself, distinct from any exported note's own title. */
339
+ const CASCADE_INDEX_TITLE = 'Markii export';
340
+ /**
341
+ * Builds a cascade archive's `index.html`: a self-contained document
342
+ * listing every exported note by title, each linking to its own file.
343
+ *
344
+ * A cascade archive has no single note to open first once it is
345
+ * unzipped, and without this a reader has to guess a file name or open the
346
+ * archive's contents blind. `entries` is expected in the same order the
347
+ * cascade walked (root note first, breadth first), which is what makes the
348
+ * list read as a sensible table of contents rather than an alphabetized
349
+ * jumble.
350
+ *
351
+ * Uses the same page shell every exported note gets (`doc.css`,
352
+ * `EXPORT_PAGE_CSS`), so the index reads as part of the same export rather
353
+ * than a bare, unstyled file dropped in beside it.
354
+ */
355
+ export function buildCascadeIndexHtml(entries, engine) {
356
+ const { escapeHtml } = engine;
357
+ const items = entries
358
+ .map((entry) => `<li><a href="${escapeHtml(entry.fileName)}">${escapeHtml(entry.title)}</a></li>`)
359
+ .join('\n');
360
+ const body = `<ul class="mk-cascade-index">\n${items}\n</ul>`;
361
+ return composeNoteHtmlExport({
362
+ engine,
363
+ bodyHtml: body,
364
+ fileName: CASCADE_INDEX_TITLE,
365
+ });
366
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Reading and rewriting the links one note makes to another (GitHub issue
3
+ * #28 slice 3, part 2: the cascade export).
4
+ *
5
+ * PURE AND HOST-NEUTRAL. Nothing here touches a vault, a workspace, or a
6
+ * filesystem: it takes a note's text and gives back the links it contains,
7
+ * or a rewritten copy of that text. Resolving a link target to an actual
8
+ * note is the host's job, injected by the caller. That is what lets the
9
+ * Obsidian cascade command and a future VS Code one share this code
10
+ * unchanged, and it keeps every rule about what counts as a link testable
11
+ * without a vault.
12
+ *
13
+ * WHAT COUNTS AS A LINK. Two forms, both of which a note author uses: a
14
+ * wikilink (`[[Other note]]`, `[[Other note|shown text]]`,
15
+ * `[[Other note#a heading]]`) and an ordinary markdown link
16
+ * (`[shown text](other-note.md)`). An embed or image (`![[...]]`,
17
+ * `![](...)`) is deliberately NOT a note link: it is a picture or a
18
+ * transclusion, not a page to walk to. A target with a scheme
19
+ * (`https:`, `mailto:`), a protocol-relative `//host/path`, or a bare
20
+ * fragment (`#section`) is not a note link either.
21
+ *
22
+ * CODE IS NOT CONTENT. A fenced code block or an inline code span that
23
+ * happens to contain `[[...]]` is showing link syntax, not using it, so
24
+ * both are masked out before anything is matched. The mask replaces
25
+ * characters one for one, so every offset this reports still indexes the
26
+ * ORIGINAL text.
27
+ */
28
+ /** One link from a note to something else, as written. */
29
+ export interface NoteLink {
30
+ /** Which syntax the author used. */
31
+ readonly kind: 'wikilink' | 'markdown';
32
+ /**
33
+ * The link target with any alias and any `#heading` / `^block` fragment
34
+ * removed: the part a host resolves against its vault or workspace.
35
+ * Percent escapes in a markdown target are decoded, so `My%20Note.md`
36
+ * resolves as `My Note.md`.
37
+ */
38
+ readonly path: string;
39
+ /** The `#heading` or `^block` fragment including its leading character, or an empty string. */
40
+ readonly fragment: string;
41
+ /** The text shown to a reader: a wikilink's alias or its target, or a markdown link's label. */
42
+ readonly label: string;
43
+ /** Offset of the link's first character in the original text. */
44
+ readonly start: number;
45
+ /** Offset one past the link's last character in the original text. */
46
+ readonly end: number;
47
+ }
48
+ /** True for a target that could name a note in this vault or workspace: a non-empty, local, relative path rather than a URL or a bare fragment. */
49
+ export declare function isLocalNoteTarget(target: string): boolean;
50
+ /**
51
+ * A copy of `text` with every fenced code block and inline code span
52
+ * replaced by mask characters, so link syntax a note is merely DISPLAYING
53
+ * is never mistaken for a link the note makes. Length and offsets are
54
+ * unchanged, and newlines are preserved so line-anchored scanning still
55
+ * works over the masked copy.
56
+ */
57
+ export declare function maskCodeRegions(text: string): string;
58
+ /**
59
+ * Every note link in `text`, in document order. Embeds, images, URLs and
60
+ * bare fragments are all excluded, and anything inside code is invisible
61
+ * here.
62
+ */
63
+ export declare function extractNoteLinks(text: string): NoteLink[];
64
+ /**
65
+ * Decides one link's replacement destination, or `undefined` to leave the
66
+ * link exactly as the author wrote it. A cascade passes a resolver that
67
+ * answers with the sibling `.html` file name for a note that is in the
68
+ * export, and `undefined` for everything else.
69
+ */
70
+ export type NoteLinkTargetResolver = (link: NoteLink) => string | undefined;
71
+ /**
72
+ * Rewrites every link `resolve` claims, leaving every other byte of `text`
73
+ * untouched.
74
+ *
75
+ * A markdown link keeps its shape and only changes destination. A WIKILINK
76
+ * BECOMES A MARKDOWN LINK, which is a deliberate change and the only way a
77
+ * cascade is navigable: Markii renders CommonMark, where `[[Other note]]`
78
+ * is literal text rather than a link, so leaving wikilinks as written would
79
+ * produce a set of exported pages that cannot reach each other. A wikilink
80
+ * whose target is NOT in the export is left exactly as written, so nothing
81
+ * outside the exported set is ever invented.
82
+ */
83
+ export declare function rewriteNoteLinks(text: string, resolve: NoteLinkTargetResolver): string;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Reading and rewriting the links one note makes to another (GitHub issue
3
+ * #28 slice 3, part 2: the cascade export).
4
+ *
5
+ * PURE AND HOST-NEUTRAL. Nothing here touches a vault, a workspace, or a
6
+ * filesystem: it takes a note's text and gives back the links it contains,
7
+ * or a rewritten copy of that text. Resolving a link target to an actual
8
+ * note is the host's job, injected by the caller. That is what lets the
9
+ * Obsidian cascade command and a future VS Code one share this code
10
+ * unchanged, and it keeps every rule about what counts as a link testable
11
+ * without a vault.
12
+ *
13
+ * WHAT COUNTS AS A LINK. Two forms, both of which a note author uses: a
14
+ * wikilink (`[[Other note]]`, `[[Other note|shown text]]`,
15
+ * `[[Other note#a heading]]`) and an ordinary markdown link
16
+ * (`[shown text](other-note.md)`). An embed or image (`![[...]]`,
17
+ * `![](...)`) is deliberately NOT a note link: it is a picture or a
18
+ * transclusion, not a page to walk to. A target with a scheme
19
+ * (`https:`, `mailto:`), a protocol-relative `//host/path`, or a bare
20
+ * fragment (`#section`) is not a note link either.
21
+ *
22
+ * CODE IS NOT CONTENT. A fenced code block or an inline code span that
23
+ * happens to contain `[[...]]` is showing link syntax, not using it, so
24
+ * both are masked out before anything is matched. The mask replaces
25
+ * characters one for one, so every offset this reports still indexes the
26
+ * ORIGINAL text.
27
+ */
28
+ /** True when `value` begins with a URL scheme, by the same rule `@markii/core`'s `isSafeUrl` uses: text before the first `:`, but only when that `:` precedes any `/`, `?` or `#`. */
29
+ function hasScheme(value) {
30
+ const colon = value.indexOf(':');
31
+ if (colon === -1)
32
+ return false;
33
+ const slash = value.indexOf('/');
34
+ const questionMark = value.indexOf('?');
35
+ const numberSign = value.indexOf('#');
36
+ return ((slash === -1 || colon < slash) &&
37
+ (questionMark === -1 || colon < questionMark) &&
38
+ (numberSign === -1 || colon < numberSign));
39
+ }
40
+ /** True for a target that could name a note in this vault or workspace: a non-empty, local, relative path rather than a URL or a bare fragment. */
41
+ export function isLocalNoteTarget(target) {
42
+ if (target.length === 0)
43
+ return false;
44
+ if (target.startsWith('#'))
45
+ return false;
46
+ if (target.startsWith('//'))
47
+ return false;
48
+ return !hasScheme(target);
49
+ }
50
+ /**
51
+ * The mask character. A space cannot begin or continue any link syntax, and
52
+ * replacing one character for one keeps every offset aligned with the
53
+ * original text.
54
+ */
55
+ const MASK = ' ';
56
+ /**
57
+ * A copy of `text` with every fenced code block and inline code span
58
+ * replaced by mask characters, so link syntax a note is merely DISPLAYING
59
+ * is never mistaken for a link the note makes. Length and offsets are
60
+ * unchanged, and newlines are preserved so line-anchored scanning still
61
+ * works over the masked copy.
62
+ */
63
+ export function maskCodeRegions(text) {
64
+ const chars = [...text];
65
+ const mask = (from, to) => {
66
+ for (let index = from; index < to && index < chars.length; index += 1) {
67
+ if (chars[index] !== '\n')
68
+ chars[index] = MASK;
69
+ }
70
+ };
71
+ // Fenced blocks first: a fence's contents are code even when they contain
72
+ // unbalanced backticks that inline scanning would misread.
73
+ const fence = /^[ \t]{0,3}(`{3,}|~{3,})[^\n]*$/gm;
74
+ const fenced = [];
75
+ let openMatch;
76
+ while ((openMatch = fence.exec(text)) !== null) {
77
+ const marker = openMatch[1] ?? '';
78
+ const markerChar = marker.startsWith('`') ? '`' : '~';
79
+ const closer = new RegExp(`^[ \\t]{0,3}${markerChar}{${String(marker.length)},}[ \\t]*$`, 'gm');
80
+ closer.lastIndex = openMatch.index + openMatch[0].length;
81
+ const closeMatch = closer.exec(text);
82
+ const bodyEnd = closeMatch
83
+ ? closeMatch.index + closeMatch[0].length
84
+ : text.length;
85
+ fenced.push([openMatch.index, bodyEnd]);
86
+ fence.lastIndex = bodyEnd;
87
+ }
88
+ for (const [from, to] of fenced)
89
+ mask(from, to);
90
+ // Inline spans, over what is left: a run of N backticks closes at the
91
+ // next run of exactly N.
92
+ let index = 0;
93
+ while (index < chars.length) {
94
+ if (chars[index] !== '`') {
95
+ index += 1;
96
+ continue;
97
+ }
98
+ let runEnd = index;
99
+ while (runEnd < chars.length && chars[runEnd] === '`')
100
+ runEnd += 1;
101
+ const runLength = runEnd - index;
102
+ let search = runEnd;
103
+ let closeEnd = -1;
104
+ while (search < chars.length) {
105
+ if (chars[search] !== '`') {
106
+ search += 1;
107
+ continue;
108
+ }
109
+ let candidateEnd = search;
110
+ while (candidateEnd < chars.length && chars[candidateEnd] === '`') {
111
+ candidateEnd += 1;
112
+ }
113
+ if (candidateEnd - search === runLength) {
114
+ closeEnd = candidateEnd;
115
+ break;
116
+ }
117
+ search = candidateEnd;
118
+ }
119
+ if (closeEnd === -1) {
120
+ index = runEnd;
121
+ continue;
122
+ }
123
+ mask(index, closeEnd);
124
+ index = closeEnd;
125
+ }
126
+ return chars.join('');
127
+ }
128
+ /** Splits a target into its path and its `#heading` / `^block` fragment. */
129
+ function splitFragment(target) {
130
+ const hash = target.indexOf('#');
131
+ const caret = target.indexOf('^');
132
+ const cut = hash === -1 ? caret : caret === -1 ? hash : Math.min(hash, caret);
133
+ if (cut === -1)
134
+ return { path: target, fragment: '' };
135
+ return { path: target.slice(0, cut), fragment: target.slice(cut) };
136
+ }
137
+ /** Percent-decodes a markdown target, leaving it untouched when it is not valid escaping rather than throwing. */
138
+ function decodeTarget(target) {
139
+ try {
140
+ return decodeURIComponent(target);
141
+ }
142
+ catch {
143
+ return target;
144
+ }
145
+ }
146
+ /** `[[target]]` and `[[target|alias]]`, with a leading `!` captured so an embed can be rejected. */
147
+ const WIKILINK_PATTERN = /(!?)\[\[([^\]\n]*)\]\]/g;
148
+ /**
149
+ * `[label](target)` and `[label](<target>)`, with an optional `"title"`,
150
+ * and a leading `!` captured so an image can be rejected. The label
151
+ * deliberately allows no nested `]`: a label with brackets in it is left
152
+ * alone rather than guessed at.
153
+ */
154
+ const MARKDOWN_LINK_PATTERN = /(!?)\[([^\]\n]*)\]\((?:<([^>\n]*)>|([^)\s]*))(?:[ \t]+"[^"\n]*")?\)/g;
155
+ /**
156
+ * Every note link in `text`, in document order. Embeds, images, URLs and
157
+ * bare fragments are all excluded, and anything inside code is invisible
158
+ * here.
159
+ */
160
+ export function extractNoteLinks(text) {
161
+ const masked = maskCodeRegions(text);
162
+ const links = [];
163
+ let match;
164
+ WIKILINK_PATTERN.lastIndex = 0;
165
+ while ((match = WIKILINK_PATTERN.exec(masked)) !== null) {
166
+ if (match[1] === '!')
167
+ continue;
168
+ const inner = match[2] ?? '';
169
+ const pipe = inner.indexOf('|');
170
+ const rawTarget = (pipe === -1 ? inner : inner.slice(0, pipe)).trim();
171
+ const alias = pipe === -1 ? undefined : inner.slice(pipe + 1).trim();
172
+ if (!isLocalNoteTarget(rawTarget))
173
+ continue;
174
+ const { path, fragment } = splitFragment(rawTarget);
175
+ if (path.length === 0)
176
+ continue;
177
+ links.push({
178
+ kind: 'wikilink',
179
+ path,
180
+ fragment,
181
+ label: alias !== undefined && alias.length > 0 ? alias : rawTarget,
182
+ start: match.index,
183
+ end: match.index + match[0].length,
184
+ });
185
+ }
186
+ MARKDOWN_LINK_PATTERN.lastIndex = 0;
187
+ while ((match = MARKDOWN_LINK_PATTERN.exec(masked)) !== null) {
188
+ if (match[1] === '!')
189
+ continue;
190
+ const rawTarget = match[3] ?? match[4] ?? '';
191
+ if (!isLocalNoteTarget(rawTarget))
192
+ continue;
193
+ const { path, fragment } = splitFragment(decodeTarget(rawTarget));
194
+ if (path.length === 0)
195
+ continue;
196
+ links.push({
197
+ kind: 'markdown',
198
+ path,
199
+ fragment,
200
+ label: match[2] ?? '',
201
+ start: match.index,
202
+ end: match.index + match[0].length,
203
+ });
204
+ }
205
+ return links.sort((left, right) => left.start - right.start);
206
+ }
207
+ /** Escapes the characters that would break out of a markdown link label. */
208
+ function escapeLinkLabel(label) {
209
+ return label.replace(/([[\]])/g, '\\$1');
210
+ }
211
+ /** Escapes the characters that would break out of a markdown link destination, and percent-encodes spaces so the href survives. */
212
+ function escapeLinkDestination(destination) {
213
+ return destination.replace(/[()]/g, encodeURIComponent).replace(/ /g, '%20');
214
+ }
215
+ /**
216
+ * Rewrites every link `resolve` claims, leaving every other byte of `text`
217
+ * untouched.
218
+ *
219
+ * A markdown link keeps its shape and only changes destination. A WIKILINK
220
+ * BECOMES A MARKDOWN LINK, which is a deliberate change and the only way a
221
+ * cascade is navigable: Markii renders CommonMark, where `[[Other note]]`
222
+ * is literal text rather than a link, so leaving wikilinks as written would
223
+ * produce a set of exported pages that cannot reach each other. A wikilink
224
+ * whose target is NOT in the export is left exactly as written, so nothing
225
+ * outside the exported set is ever invented.
226
+ */
227
+ export function rewriteNoteLinks(text, resolve) {
228
+ const links = extractNoteLinks(text);
229
+ let result = text;
230
+ // Right to left, so an earlier link's offsets stay valid as we splice.
231
+ for (let index = links.length - 1; index >= 0; index -= 1) {
232
+ const link = links[index];
233
+ const destination = resolve(link);
234
+ if (destination === undefined)
235
+ continue;
236
+ const href = escapeLinkDestination(destination) + link.fragment;
237
+ const replacement = `[${escapeLinkLabel(link.label)}](${href})`;
238
+ result = result.slice(0, link.start) + replacement + result.slice(link.end);
239
+ }
240
+ return result;
241
+ }
package/dist/glue.wasm ADDED
Binary file