@markii/host 0.0.1 → 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.
- package/README.md +18 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +167 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +223 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +156 -0
- package/dist/host/conformance-runner.js +634 -0
- package/dist/host/create-host.d.ts +253 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +157 -0
- package/dist/host/editor-behavior.js +227 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +113 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +24 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +68 -0
- package/dist/host/pack-install.d.ts +88 -0
- package/dist/host/pack-install.js +189 -0
- package/dist/host/pack-load.d.ts +72 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +39 -0
- package/dist/host/refresh-interval.js +61 -0
- package/dist/host/run-behavior.d.ts +89 -0
- package/dist/host/run-behavior.js +168 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +70 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +81 -0
- package/dist/packs/pack-diagnostics.js +105 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +106 -0
- package/dist/packs/pack-render-registry.d.ts +83 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +81 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +57 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +70 -0
- package/dist/run/net-provider.js +289 -0
- package/dist/run/node-isolate.d.ts +30 -0
- package/dist/run/node-isolate.js +117 -0
- package/dist/run/run-flow.d.ts +187 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +42 -0
- package/dist/run/worker-entry-browser.js +77 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +96 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +77 -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
|