@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,98 @@
|
|
|
1
|
+
import type { NoteLink } from './note-links.js';
|
|
2
|
+
/**
|
|
3
|
+
* How far from the root note a cascade follows links. Four hops covers the
|
|
4
|
+
* shapes people actually build, an index note pointing at sections that
|
|
5
|
+
* point at pages, without turning one command into a whole-vault export.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_CASCADE_MAX_DEPTH = 4;
|
|
8
|
+
/**
|
|
9
|
+
* How many notes one cascade may export. A second, independent guard: a
|
|
10
|
+
* shallow graph can still be enormously wide, and an archive of a hundred
|
|
11
|
+
* pages is already past what anyone meant by "this note and what it links
|
|
12
|
+
* to".
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_CASCADE_MAX_NOTES = 100;
|
|
15
|
+
/** One note the walk reached. */
|
|
16
|
+
export interface CascadeNote {
|
|
17
|
+
/** The note's own path, in whatever form the host resolves and reads. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
/** The note's full source text. */
|
|
20
|
+
readonly text: string;
|
|
21
|
+
/** Hops from the root note. The root itself is 0. */
|
|
22
|
+
readonly depth: number;
|
|
23
|
+
}
|
|
24
|
+
/** Reads one note's text, or reports that it could not be read. A reader that throws is treated as unreadable. */
|
|
25
|
+
export type CascadeNoteReader = (path: string) => Promise<string | undefined> | string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves one link, as written in `fromNotePath`, to the path of a note in
|
|
28
|
+
* this vault or workspace, or `undefined` when it names something that is
|
|
29
|
+
* not a note here. This is where a host puts its own resolution rules: an
|
|
30
|
+
* Obsidian vault resolves a bare note name from anywhere, a folder-relative
|
|
31
|
+
* host resolves against the linking note's folder.
|
|
32
|
+
*/
|
|
33
|
+
export type CascadeLinkResolver = (link: NoteLink, fromNotePath: string) => string | undefined;
|
|
34
|
+
/** Why a walk stopped early. */
|
|
35
|
+
export type CascadeTruncation = 'depth' | 'count';
|
|
36
|
+
export interface CascadeWalkOptions {
|
|
37
|
+
/** The note the cascade starts from. */
|
|
38
|
+
readonly rootPath: string;
|
|
39
|
+
readonly readNote: CascadeNoteReader;
|
|
40
|
+
readonly resolveLink: CascadeLinkResolver;
|
|
41
|
+
/** Defaults to `DEFAULT_CASCADE_MAX_DEPTH`. */
|
|
42
|
+
readonly maxDepth?: number;
|
|
43
|
+
/** Defaults to `DEFAULT_CASCADE_MAX_NOTES`. */
|
|
44
|
+
readonly maxNotes?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface CascadeWalkResult {
|
|
47
|
+
/** Every note reached, breadth first, the root note always first. */
|
|
48
|
+
readonly notes: readonly CascadeNote[];
|
|
49
|
+
/** Links that resolved to a note the reader could not read, for the diagnostics surface. */
|
|
50
|
+
readonly unreadable: readonly {
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly from: string;
|
|
53
|
+
}[];
|
|
54
|
+
/** Set when a limit stopped the walk before it ran out of links to follow. */
|
|
55
|
+
readonly truncated?: CascadeTruncation;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Walks the note graph out from `rootPath`, breadth first, and returns
|
|
59
|
+
* every note it reached.
|
|
60
|
+
*
|
|
61
|
+
* Breadth first rather than depth first on purpose: when a limit cuts the
|
|
62
|
+
* walk short, what survives is everything CLOSE to the root, which is what
|
|
63
|
+
* a reader of the root note actually needs.
|
|
64
|
+
*
|
|
65
|
+
* Never throws. A note that cannot be read is recorded and skipped, and a
|
|
66
|
+
* root note that cannot be read simply produces an empty walk, which the
|
|
67
|
+
* caller reports as having nothing to export.
|
|
68
|
+
*/
|
|
69
|
+
export declare function walkNoteCascade(options: CascadeWalkOptions): Promise<CascadeWalkResult>;
|
|
70
|
+
/**
|
|
71
|
+
* The archive entry name a cascade's own index page uses. Reserved up
|
|
72
|
+
* front with `assignCascadeFileNames`'s `reservedNames`, so a walked note
|
|
73
|
+
* that happens to be called `index` gets a numeric suffix instead of
|
|
74
|
+
* silently colliding with, and overwriting, the index page in the zip.
|
|
75
|
+
*/
|
|
76
|
+
export declare const CASCADE_INDEX_FILE_NAME = "index.html";
|
|
77
|
+
/**
|
|
78
|
+
* The file name each note gets inside the archive: its own base name with
|
|
79
|
+
* an `.html` extension.
|
|
80
|
+
*
|
|
81
|
+
* The archive is flat, so two notes in different folders can want the same
|
|
82
|
+
* name. The first one to be reached keeps it, and each later claimant gets
|
|
83
|
+
* a numeric suffix. The walk is breadth first and deterministic, so the
|
|
84
|
+
* same vault always produces the same names.
|
|
85
|
+
*
|
|
86
|
+
* `reservedNames` are claimed before any note, case-insensitively, exactly
|
|
87
|
+
* like an earlier note's claim — a host builds the same archive's index
|
|
88
|
+
* page under one of these names and needs it to never collide with a
|
|
89
|
+
* walked note.
|
|
90
|
+
*/
|
|
91
|
+
export declare function assignCascadeFileNames(paths: readonly string[], reservedNames?: Iterable<string>): Map<string, string>;
|
|
92
|
+
/**
|
|
93
|
+
* One note's text with every link to another note IN THIS CASCADE pointed
|
|
94
|
+
* at that note's exported file. A link to anything else, a note outside the
|
|
95
|
+
* walked set or a file that is not a note at all, is left exactly as the
|
|
96
|
+
* author wrote it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function rewriteCascadeLinks(note: CascadeNote, fileNames: ReadonlyMap<string, string>, resolveLink: CascadeLinkResolver): string;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cascade export's pure half (GitHub issue #28 slice 3, part 2):
|
|
3
|
+
* walking the notes one note links to, deciding what each exported file is
|
|
4
|
+
* called, and rewriting the links between them so the exported set is
|
|
5
|
+
* navigable on its own.
|
|
6
|
+
*
|
|
7
|
+
* PURE AND HOST-NEUTRAL, like `./note-links.ts` beside it. Reading a note
|
|
8
|
+
* and resolving a link target to a real note are injected, because only a
|
|
9
|
+
* host knows what a vault or a workspace contains. Obsidian is the first
|
|
10
|
+
* caller (issue #28 slice 3); a VS Code cascade command would supply its
|
|
11
|
+
* own two functions and reuse everything here unchanged. Nothing in this
|
|
12
|
+
* file imports `node:*`, so it stays available to a browser bundle if a
|
|
13
|
+
* host ever needs it there.
|
|
14
|
+
*
|
|
15
|
+
* BOUNDED BY CONSTRUCTION. A note graph has cycles, and a vault can be
|
|
16
|
+
* enormous, so the walk carries three guards a caller cannot switch off:
|
|
17
|
+
* a visited set, a depth limit, and a note-count limit. Hitting a limit is
|
|
18
|
+
* not a failure. The walk stops, says which limit stopped it, and the host
|
|
19
|
+
* writes that to its diagnostics surface, per AGENTS.md's rule that a
|
|
20
|
+
* quiet outcome still has to be discoverable.
|
|
21
|
+
*/
|
|
22
|
+
import { extractNoteLinks, rewriteNoteLinks } from './note-links.js';
|
|
23
|
+
import { exportBaseName } from './note-export.js';
|
|
24
|
+
/**
|
|
25
|
+
* How far from the root note a cascade follows links. Four hops covers the
|
|
26
|
+
* shapes people actually build, an index note pointing at sections that
|
|
27
|
+
* point at pages, without turning one command into a whole-vault export.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_CASCADE_MAX_DEPTH = 4;
|
|
30
|
+
/**
|
|
31
|
+
* How many notes one cascade may export. A second, independent guard: a
|
|
32
|
+
* shallow graph can still be enormously wide, and an archive of a hundred
|
|
33
|
+
* pages is already past what anyone meant by "this note and what it links
|
|
34
|
+
* to".
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_CASCADE_MAX_NOTES = 100;
|
|
37
|
+
/**
|
|
38
|
+
* Walks the note graph out from `rootPath`, breadth first, and returns
|
|
39
|
+
* every note it reached.
|
|
40
|
+
*
|
|
41
|
+
* Breadth first rather than depth first on purpose: when a limit cuts the
|
|
42
|
+
* walk short, what survives is everything CLOSE to the root, which is what
|
|
43
|
+
* a reader of the root note actually needs.
|
|
44
|
+
*
|
|
45
|
+
* Never throws. A note that cannot be read is recorded and skipped, and a
|
|
46
|
+
* root note that cannot be read simply produces an empty walk, which the
|
|
47
|
+
* caller reports as having nothing to export.
|
|
48
|
+
*/
|
|
49
|
+
export async function walkNoteCascade(options) {
|
|
50
|
+
const maxDepth = options.maxDepth ?? DEFAULT_CASCADE_MAX_DEPTH;
|
|
51
|
+
const maxNotes = options.maxNotes ?? DEFAULT_CASCADE_MAX_NOTES;
|
|
52
|
+
const notes = [];
|
|
53
|
+
const unreadable = [];
|
|
54
|
+
const visited = new Set();
|
|
55
|
+
let truncated;
|
|
56
|
+
const read = async (path) => {
|
|
57
|
+
try {
|
|
58
|
+
return await options.readNote(path);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const rootText = await read(options.rootPath);
|
|
65
|
+
if (rootText === undefined) {
|
|
66
|
+
return { notes: [], unreadable: [{ path: options.rootPath, from: '' }] };
|
|
67
|
+
}
|
|
68
|
+
visited.add(options.rootPath);
|
|
69
|
+
notes.push({ path: options.rootPath, text: rootText, depth: 0 });
|
|
70
|
+
for (let cursor = 0; cursor < notes.length; cursor += 1) {
|
|
71
|
+
const current = notes[cursor];
|
|
72
|
+
if (current.depth >= maxDepth) {
|
|
73
|
+
// Only a note that actually has an unfollowed link proves the depth
|
|
74
|
+
// limit changed the outcome, so the flag is set below rather than
|
|
75
|
+
// here.
|
|
76
|
+
const hasLink = extractNoteLinks(current.text).some((link) => options.resolveLink(link, current.path) !== undefined);
|
|
77
|
+
if (hasLink)
|
|
78
|
+
truncated ??= 'depth';
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const link of extractNoteLinks(current.text)) {
|
|
82
|
+
const resolved = options.resolveLink(link, current.path);
|
|
83
|
+
if (resolved === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
if (visited.has(resolved))
|
|
86
|
+
continue;
|
|
87
|
+
visited.add(resolved);
|
|
88
|
+
if (notes.length >= maxNotes) {
|
|
89
|
+
truncated ??= 'count';
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const text = await read(resolved);
|
|
93
|
+
if (text === undefined) {
|
|
94
|
+
unreadable.push({ path: resolved, from: current.path });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
notes.push({ path: resolved, text, depth: current.depth + 1 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
notes,
|
|
102
|
+
unreadable,
|
|
103
|
+
...(truncated !== undefined ? { truncated } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The archive entry name a cascade's own index page uses. Reserved up
|
|
108
|
+
* front with `assignCascadeFileNames`'s `reservedNames`, so a walked note
|
|
109
|
+
* that happens to be called `index` gets a numeric suffix instead of
|
|
110
|
+
* silently colliding with, and overwriting, the index page in the zip.
|
|
111
|
+
*/
|
|
112
|
+
export const CASCADE_INDEX_FILE_NAME = 'index.html';
|
|
113
|
+
/**
|
|
114
|
+
* The file name each note gets inside the archive: its own base name with
|
|
115
|
+
* an `.html` extension.
|
|
116
|
+
*
|
|
117
|
+
* The archive is flat, so two notes in different folders can want the same
|
|
118
|
+
* name. The first one to be reached keeps it, and each later claimant gets
|
|
119
|
+
* a numeric suffix. The walk is breadth first and deterministic, so the
|
|
120
|
+
* same vault always produces the same names.
|
|
121
|
+
*
|
|
122
|
+
* `reservedNames` are claimed before any note, case-insensitively, exactly
|
|
123
|
+
* like an earlier note's claim — a host builds the same archive's index
|
|
124
|
+
* page under one of these names and needs it to never collide with a
|
|
125
|
+
* walked note.
|
|
126
|
+
*/
|
|
127
|
+
export function assignCascadeFileNames(paths, reservedNames = []) {
|
|
128
|
+
const names = new Map();
|
|
129
|
+
const taken = new Set();
|
|
130
|
+
for (const reserved of reservedNames) {
|
|
131
|
+
taken.add(reserved.toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
for (const path of paths) {
|
|
134
|
+
const base = exportBaseName(path);
|
|
135
|
+
let candidate = `${base}.html`;
|
|
136
|
+
let counter = 2;
|
|
137
|
+
while (taken.has(candidate.toLowerCase())) {
|
|
138
|
+
candidate = `${base}-${String(counter)}.html`;
|
|
139
|
+
counter += 1;
|
|
140
|
+
}
|
|
141
|
+
taken.add(candidate.toLowerCase());
|
|
142
|
+
names.set(path, candidate);
|
|
143
|
+
}
|
|
144
|
+
return names;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* One note's text with every link to another note IN THIS CASCADE pointed
|
|
148
|
+
* at that note's exported file. A link to anything else, a note outside the
|
|
149
|
+
* walked set or a file that is not a note at all, is left exactly as the
|
|
150
|
+
* author wrote it.
|
|
151
|
+
*/
|
|
152
|
+
export function rewriteCascadeLinks(note, fileNames, resolveLink) {
|
|
153
|
+
return rewriteNoteLinks(note.text, (link) => {
|
|
154
|
+
const resolved = resolveLink(link, note.path);
|
|
155
|
+
if (resolved === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
return fileNames.get(resolved);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** One file in a cascade archive. */
|
|
2
|
+
export interface ExportArchiveEntry {
|
|
3
|
+
/** The file's name inside the archive. Flat: no directories. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The file's UTF-8 text. */
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The archive bytes for `entries`.
|
|
10
|
+
*
|
|
11
|
+
* Deflate level 6, the ordinary default: exported HTML embeds base64 image
|
|
12
|
+
* data, which compresses poorly, while the markup around it compresses
|
|
13
|
+
* well, and level 6 is the balance every archiver picked for the same
|
|
14
|
+
* reason.
|
|
15
|
+
*
|
|
16
|
+
* `mtime` is deliberately not set, so the same set of notes always produces
|
|
17
|
+
* byte-identical output and a re-export is diffable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function zipExportArchive(entries: readonly ExportArchiveEntry[]): Uint8Array;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packing a cascade export into one archive (GitHub issue #28 slice 3,
|
|
3
|
+
* part 2).
|
|
4
|
+
*
|
|
5
|
+
* WHY NOT `@markii/bundle`. That package's zip module is about `.mkz`
|
|
6
|
+
* bundles: reading one safely (entry-size guards, CRC checks, a path jail)
|
|
7
|
+
* and writing one back out with a manifest. A cascade archive is not a
|
|
8
|
+
* bundle. It has no manifest, no scripts, and no bundle semantics at all,
|
|
9
|
+
* so borrowing `exportZipBundle` would mean claiming a format this is not.
|
|
10
|
+
* What both share is `fflate`, an approved dependency, so this writes the
|
|
11
|
+
* archive directly and stays a dozen lines.
|
|
12
|
+
*
|
|
13
|
+
* Host-neutral and free of `node:*`: `fflate` and `TextEncoder` work in
|
|
14
|
+
* every environment either host runs in.
|
|
15
|
+
*/
|
|
16
|
+
import { zipSync } from 'fflate';
|
|
17
|
+
/**
|
|
18
|
+
* The archive bytes for `entries`.
|
|
19
|
+
*
|
|
20
|
+
* Deflate level 6, the ordinary default: exported HTML embeds base64 image
|
|
21
|
+
* data, which compresses poorly, while the markup around it compresses
|
|
22
|
+
* well, and level 6 is the balance every archiver picked for the same
|
|
23
|
+
* reason.
|
|
24
|
+
*
|
|
25
|
+
* `mtime` is deliberately not set, so the same set of notes always produces
|
|
26
|
+
* byte-identical output and a re-export is diffable.
|
|
27
|
+
*/
|
|
28
|
+
export function zipExportArchive(entries) {
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
// A null-prototype object, matching how `@markii/bundle`'s own zip
|
|
31
|
+
// writing guards `zipSync`'s bracket assignment against a file named
|
|
32
|
+
// `__proto__`.
|
|
33
|
+
const files = Object.create(null);
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
files[entry.name] = encoder.encode(entry.text);
|
|
36
|
+
}
|
|
37
|
+
return zipSync(files, { level: 6 });
|
|
38
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 declare const MAX_EMBEDDED_IMAGE_BYTES: number;
|
|
41
|
+
/** Every extension `mimeTypeForImageSrc` recognizes, lowercase and without the dot. Exported for a host that wants to name them in its own documentation or tests. */
|
|
42
|
+
export declare const EMBEDDABLE_IMAGE_EXTENSIONS: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* The MIME type for one image source, read from its extension, or
|
|
45
|
+
* `undefined` when the extension is missing or unrecognized. Any `?query`
|
|
46
|
+
* or `#fragment` is ignored first, so `logo.png?v=2` still reads as a PNG.
|
|
47
|
+
*/
|
|
48
|
+
export declare function mimeTypeForImageSrc(src: string): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* True for a source this will try to embed: a non-empty, local, relative
|
|
51
|
+
* path. A scheme of any kind, a protocol-relative `//host/...`, and an
|
|
52
|
+
* empty or fragment-only value are all left exactly as authored.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isEmbeddableImageSrc(src: string): boolean;
|
|
55
|
+
/** What a host's image reader came back with for one source. */
|
|
56
|
+
export type ExportImageResult = {
|
|
57
|
+
/** The file's bytes, ready to embed. */
|
|
58
|
+
readonly kind: 'bytes';
|
|
59
|
+
readonly bytes: Uint8Array;
|
|
60
|
+
} | {
|
|
61
|
+
/** The host measured the file and it is over `MAX_EMBEDDED_IMAGE_BYTES`, so it never read it. */
|
|
62
|
+
readonly kind: 'oversize';
|
|
63
|
+
readonly byteLength: number;
|
|
64
|
+
} | {
|
|
65
|
+
/** The source did not resolve to a readable file, or reading it failed. */
|
|
66
|
+
readonly kind: 'unreadable';
|
|
67
|
+
/** The verbatim reason, for the diagnostics surface only. */
|
|
68
|
+
readonly detail?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Reads one image source for an export. A host resolves the source the way
|
|
72
|
+
* its own preview resolves one, applies its own jail, and reads the bytes.
|
|
73
|
+
* It is expected not to throw; one that does is caught by
|
|
74
|
+
* `embedImagesInHtml` and treated as `unreadable`, so an export is never
|
|
75
|
+
* lost to a bad path.
|
|
76
|
+
*/
|
|
77
|
+
export type ExportImageReader = (src: string) => ExportImageResult | Promise<ExportImageResult>;
|
|
78
|
+
/** Why one image kept its original source instead of being embedded. Only outcomes worth a diagnostics line appear here; a remote URL is not a skip, it is the documented behavior. */
|
|
79
|
+
export type ImageSkipReason = 'unsupported-type' | 'too-large' | 'unreadable';
|
|
80
|
+
/** One image that was not embedded, and why. */
|
|
81
|
+
export interface SkippedImage {
|
|
82
|
+
/** The source exactly as it appears in the note. */
|
|
83
|
+
readonly src: string;
|
|
84
|
+
readonly reason: ImageSkipReason;
|
|
85
|
+
/** The file's size, for a `too-large` skip. */
|
|
86
|
+
readonly byteLength?: number;
|
|
87
|
+
/** The verbatim reason, for an `unreadable` skip. Diagnostics only. */
|
|
88
|
+
readonly detail?: string;
|
|
89
|
+
}
|
|
90
|
+
/** What one pass of image embedding did, for the host's diagnostics surface. */
|
|
91
|
+
export interface EmbeddedImageReport {
|
|
92
|
+
/** The sources that became `data:` URIs, in document order. */
|
|
93
|
+
readonly embedded: readonly string[];
|
|
94
|
+
/** How many bytes of image data the file gained, before base64 expansion. */
|
|
95
|
+
readonly embeddedBytes: number;
|
|
96
|
+
/** Every local image that kept its original source, and why. */
|
|
97
|
+
readonly skipped: readonly SkippedImage[];
|
|
98
|
+
/** How many sources were left alone because they are remote or already embedded. Never a skip; counted only so a host can say the file still reaches the network. */
|
|
99
|
+
readonly remote: number;
|
|
100
|
+
}
|
|
101
|
+
/** An empty report, for an export that embedded nothing because no host reader was offered. */
|
|
102
|
+
export declare const EMPTY_IMAGE_REPORT: EmbeddedImageReport;
|
|
103
|
+
/**
|
|
104
|
+
* Base64 for a byte array, hand-rolled so this module needs neither
|
|
105
|
+
* `Buffer` (Node only) nor `btoa` (which cannot take bytes without a
|
|
106
|
+
* lossy string round trip) and stays environment-free, matching how this
|
|
107
|
+
* repo hand-rolls its manifest validation and `uses:` reader rather than
|
|
108
|
+
* taking a dependency for something this small.
|
|
109
|
+
*/
|
|
110
|
+
export declare function encodeBase64(bytes: Uint8Array): string;
|
|
111
|
+
/** 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. */
|
|
112
|
+
export declare function toImageDataUri(bytes: Uint8Array, mimeType: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* Rewrites every embeddable local `<img src>` in `html` into a `data:` URI,
|
|
115
|
+
* using `readImage` to fetch each source's bytes, and reports what happened
|
|
116
|
+
* to every image it saw.
|
|
117
|
+
*
|
|
118
|
+
* Each distinct source is read at most once, so a note that shows the same
|
|
119
|
+
* picture five times reads and encodes it once and embeds the same URI five
|
|
120
|
+
* times. Never throws: a reader that throws is recorded as `unreadable` and
|
|
121
|
+
* that one image keeps its original source.
|
|
122
|
+
*/
|
|
123
|
+
export declare function embedImagesInHtml(html: string, readImage: ExportImageReader): Promise<{
|
|
124
|
+
html: string;
|
|
125
|
+
report: EmbeddedImageReport;
|
|
126
|
+
}>;
|