yamlover 0.3.3 → 0.3.5

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 (137) hide show
  1. package/bin/yamlover.js +199 -112
  2. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/client/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  62. package/dist/client/assets/decoded-DRWSdzgb.js +1 -0
  63. package/dist/client/assets/djvu-CD0wXtTp.js +1 -0
  64. package/dist/client/assets/docx-BP_KsH_m.js +230 -0
  65. package/dist/client/assets/heic-B_4kLge_.js +66 -0
  66. package/dist/client/assets/imagemap-j0tV2wtS.js +1 -0
  67. package/dist/client/assets/index-BiwY33f2.js +617 -0
  68. package/dist/client/assets/index-wTjOzmSY.css +1 -0
  69. package/dist/client/assets/map-Cb9TTMM9.js +1 -0
  70. package/dist/client/assets/paged-DK_n72xc.js +1 -0
  71. package/dist/client/assets/panzoom-CIGW-MKW.css +1 -0
  72. package/dist/client/assets/panzoom-tORQ67Sx.js +4 -0
  73. package/dist/client/assets/pdf-DfMLV_Qu.js +12 -0
  74. package/dist/client/assets/pdf-m5e3jsV4.css +1 -0
  75. package/dist/client/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
  76. package/dist/client/assets/psd-CVyrW016.js +11 -0
  77. package/dist/client/assets/spreadsheet-QnPEhgjy.js +38 -0
  78. package/dist/client/assets/tiff-BswbwivW.js +1 -0
  79. package/{index.html → dist/client/index.html} +2 -1
  80. package/dist/server.js +370 -109
  81. package/package.json +18 -21
  82. package/src/client/App.tsx +0 -424
  83. package/src/client/NodeView.tsx +0 -422
  84. package/src/client/TaskStrip.tsx +0 -34
  85. package/src/client/Tree.tsx +0 -97
  86. package/src/client/api.ts +0 -186
  87. package/src/client/icons.ts +0 -91
  88. package/src/client/links.tsx +0 -108
  89. package/src/client/live.ts +0 -42
  90. package/src/client/main.tsx +0 -10
  91. package/src/client/paste-html.ts +0 -228
  92. package/src/client/paste-links.ts +0 -42
  93. package/src/client/paths.ts +0 -139
  94. package/src/client/render.tsx +0 -326
  95. package/src/client/renderers/annotate.tsx +0 -521
  96. package/src/client/renderers/asciidoc.tsx +0 -35
  97. package/src/client/renderers/chapter.tsx +0 -138
  98. package/src/client/renderers/csv.tsx +0 -233
  99. package/src/client/renderers/decoded.tsx +0 -72
  100. package/src/client/renderers/djvu.tsx +0 -271
  101. package/src/client/renderers/djvuWorker.ts +0 -99
  102. package/src/client/renderers/doc.tsx +0 -40
  103. package/src/client/renderers/docx.tsx +0 -49
  104. package/src/client/renderers/epub.tsx +0 -147
  105. package/src/client/renderers/explorer.tsx +0 -294
  106. package/src/client/renderers/fb2.tsx +0 -149
  107. package/src/client/renderers/headings.ts +0 -69
  108. package/src/client/renderers/heic.tsx +0 -23
  109. package/src/client/renderers/imagemap.tsx +0 -157
  110. package/src/client/renderers/kml.ts +0 -46
  111. package/src/client/renderers/latex.tsx +0 -36
  112. package/src/client/renderers/map.tsx +0 -205
  113. package/src/client/renderers/marklower.tsx +0 -119
  114. package/src/client/renderers/markup.tsx +0 -64
  115. package/src/client/renderers/media.tsx +0 -19
  116. package/src/client/renderers/paged.ts +0 -137
  117. package/src/client/renderers/panzoom.ts +0 -101
  118. package/src/client/renderers/pdf.tsx +0 -316
  119. package/src/client/renderers/plaintext.tsx +0 -120
  120. package/src/client/renderers/plantuml.tsx +0 -82
  121. package/src/client/renderers/psd.tsx +0 -25
  122. package/src/client/renderers/registry.tsx +0 -389
  123. package/src/client/renderers/rtf.tsx +0 -210
  124. package/src/client/renderers/spreadsheet.tsx +0 -105
  125. package/src/client/renderers/tag.tsx +0 -113
  126. package/src/client/renderers/text.tsx +0 -41
  127. package/src/client/renderers/tiff.tsx +0 -33
  128. package/src/client/styles.css +0 -1190
  129. package/src/client/vendor/README.md +0 -30
  130. package/src/client/vite-env.d.ts +0 -31
  131. package/src/server/api.ts +0 -147
  132. package/src/server/engine-api.ts +0 -1442
  133. package/src/server/gitignore.ts +0 -81
  134. package/src/server/node-kind.ts +0 -48
  135. package/src/server/tasks.ts +0 -83
  136. package/src/server/yamlover.ts +0 -1133
  137. /package/{src/client/vendor/djvu.js → dist/client/assets/djvu-Ci0lFKxE.js} +0 -0
@@ -1,42 +0,0 @@
1
- // Pasted-LINK handlers: a paste that is EXACTLY one well-known URL means the linked CONTENT is
2
- // wanted, not the URL text. Each recognizer turns the link into something the normal paste flows
3
- // accept — a file to upload (arXiv) or a text to chunk (a tweet). A link inside longer text is
4
- // left alone (the prose is the paste). Used by NodeView's paste listener.
5
-
6
- /** The arXiv paper behind a pasted link: a lone `arxiv.org/{abs|pdf|html}/<id>` URL — fetch its
7
- * PDF (arXiv sends `access-control-allow-origin: *`) and run the normal file-paste flow. New
8
- * (`2605.00615v2`) and old (`math/0211159`) id styles. */
9
- export function arxivPdf(text: string): { url: string; name: string } | null {
10
- const m = /^(?:https?:\/\/)?(?:www\.)?arxiv\.org\/(?:abs|pdf|html)\/(.+?)(?:\.pdf)?(?:[?#][^\s]*)?$/i.exec(text.trim());
11
- if (!m) return null;
12
- const id = m[1];
13
- if (!/^(?:\d{4}\.\d{4,5}|[a-z-]+(?:\.[A-Z]{2})?\/\d{7})(?:v\d+)?$/.test(id)) return null;
14
- return { url: `https://arxiv.org/pdf/${id}`, name: `arxiv-${id.replace(/\//g, "-")}.pdf` };
15
- }
16
-
17
- /** The tweet behind a pasted `x.com`/`twitter.com` status link, canonicalized; null if the text
18
- * is not exactly one such link. */
19
- export function tweetUrl(text: string): string | null {
20
- const m = /^(?:https?:\/\/)?(?:www\.|mobile\.)?(?:x\.com|twitter\.com)\/(\w{1,15})\/status(?:es)?\/(\d+)(?:[/?#][^\s]*)?$/i.exec(text.trim());
21
- return m ? `https://twitter.com/${m[1]}/status/${m[2]}` : null;
22
- }
23
-
24
- /** Fetch a tweet's FULL content via X's public oEmbed endpoint (`publish.x.com/oembed` — the
25
- * embedded-tweet API: no auth, CORS-open) and compose it as pasteable text — the whole message,
26
- * then an author/date attribution line, then the link. */
27
- export async function fetchTweetText(statusUrl: string): Promise<string> {
28
- const res = await fetch(`https://publish.x.com/oembed?url=${encodeURIComponent(statusUrl)}&omit_script=true&dnt=true`);
29
- if (!res.ok) throw new Error(`oEmbed HTTP ${res.status}`);
30
- const o = (await res.json()) as { html?: string; author_name?: string; author_url?: string; url?: string };
31
- // the payload's `html` is a <blockquote><p>tweet…</p>— Author (@handle) <a>date</a></blockquote>
32
- const doc = new DOMParser().parseFromString((o.html ?? "").replace(/<br\s*\/?>/gi, "\n"), "text/html");
33
- const quote = doc.querySelector("blockquote");
34
- const body = quote?.querySelector("p")?.textContent?.trim();
35
- if (!body) throw new Error("no tweet text in the oEmbed payload");
36
- const handle = (o.author_url ?? "").split("/").filter(Boolean).pop();
37
- const dateLinks = quote ? Array.from(quote.querySelectorAll(":scope > a")) : [];
38
- const date = dateLinks[dateLinks.length - 1]?.textContent?.trim();
39
- const who = [o.author_name, handle && `@${handle}`].filter(Boolean).join(" ");
40
- const attribution = [who && `— ${who}`, date].filter(Boolean).join(", ");
41
- return [body, "", [attribution, o.url ?? statusUrl].filter(Boolean).join("\n")].join("\n");
42
- }
@@ -1,139 +0,0 @@
1
- // JSON-space path helpers. The CANONICAL client path is COLON-form (SEPARATOR.md M4):
2
- // `:key[0]:sub`, root `:` — what the API speaks and the UI displays. The BROWSER URL
3
- // stays SLASH-transported (`/key[0]/sub` — ruling: "the URL should be slashed, of
4
- // course"), converted at this boundary only.
5
- //
6
- // A key may itself contain `:`, `/`, `[`, or `]` (e.g. `@vitejs/plugin-react`), so each
7
- // key is percent-encoded (encodeURIComponent) — the structural separators then
8
- // unambiguously tokenize, and the URL spelling is address-bar-safe as is.
9
-
10
- export type Seg = string | number;
11
-
12
- /** Canonical client path: `:key[0]:sub` (keys percent-encoded), root `:`. */
13
- export function segsToStr(segs: Seg[]): string {
14
- return segs.map((s) => (typeof s === "number" ? `[${s}]` : `:${encodeURIComponent(s)}`)).join("") || ":";
15
- }
16
-
17
- const PATH_TOKEN = /\[\d+\]|[^:\[\]]+/g; // canonical (colon) form
18
- const URL_TOKEN = /\[\d+\]|[^/\[\]]+/g; // URL (slash) transport form
19
-
20
- export function strToSegs(str: string): Seg[] {
21
- const out: Seg[] = [];
22
- for (const tok of str.match(PATH_TOKEN) || []) {
23
- out.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : safeDecode(tok));
24
- }
25
- return out;
26
- }
27
-
28
- /** Canonical key for cross-SCOPE comparison: collapse the colon scope ladder so a
29
- * project ref (`::yamlover:…`), a document ref (`:yamlover:…`), and a server-echoed
30
- * `:`-form path all compare equal. The ladder colons are not tokens (PATH_TOKEN), so
31
- * re-emitting drops them; keys are normalized through one decode→encode pass. */
32
- export function canonPath(p: string): string {
33
- return segsToStr(strToSegs(p));
34
- }
35
-
36
- function safeDecode(s: string): string {
37
- try {
38
- return decodeURIComponent(s);
39
- } catch {
40
- return s;
41
- }
42
- }
43
-
44
- /** The current JSON path taken from the browser URL (slash transport), in canonical
45
- * COLON form. The pathname is already per-key-encoded, so it is tokenized *before*
46
- * decoding — segments ride through encoded into the canonical string. */
47
- export function pathFromUrl(): string {
48
- const segs: Seg[] = [];
49
- for (const tok of window.location.pathname.match(URL_TOKEN) || []) {
50
- segs.push(/^\[\d+\]$/.test(tok) ? Number(tok.slice(1, -1)) : safeDecode(tok));
51
- }
52
- return segsToStr(segs);
53
- }
54
-
55
- /** The slash-transport URL spelling of a canonical path (`:a[0]:b` → `/a[0]/b`). */
56
- export function urlOfPath(path: string): string {
57
- const segs = strToSegs(path);
58
- return segs.map((s) => (typeof s === "number" ? `[${s}]` : `/${encodeURIComponent(s)}`)).join("") || "/";
59
- }
60
-
61
- /** A human-readable form of a canonical path: each key decoded (so a percent-encoded
62
- * segment like `%D0%9F…` shows as its actual characters), colon-separated, indices as
63
- * `[i]`. For display only — tooltips, labels — never for URLs or navigation. */
64
- export function displayPath(path: string): string {
65
- const segs = strToSegs(path);
66
- if (!segs.length) return ":";
67
- return segs.map((s) => (typeof s === "number" ? `[${s}]` : `:${s}`)).join("");
68
- }
69
-
70
- /** A human-readable form of a path-LIKE key whose structure must survive verbatim —
71
- * a relations key (`..`, `:eve`, `::a:b`), where {@link displayPath} would mangle the
72
- * leading `::` or a bare `..`. Each key token is decoded in place; display only. */
73
- export function displayKey(key: string): string {
74
- return key.replace(/[^:/\[\]]+/g, (tok) => safeDecode(tok));
75
- }
76
-
77
- /** Whether canonical path `a` is a (strict) ancestor of `p`. Root `:` is an ancestor
78
- * of everything; otherwise `p` must continue past `a` at a `:` or `[`. */
79
- export function isAncestorPath(a: string, p: string): boolean {
80
- if (a === p) return false;
81
- if (a === ":") return true;
82
- return p.startsWith(a + ":") || p.startsWith(a + "[");
83
- }
84
-
85
- /** The current representation taken from the URL's `?format=` (or `fallback`). */
86
- export function formatFromUrl(fallback: string): string {
87
- return new URLSearchParams(window.location.search).get("format") || fallback;
88
- }
89
-
90
- /** The current page from the URL's `?page=` (1-based). 1 when absent, ≤1, or not an integer —
91
- * used by paged viewers (PDF/DjVu) to restore where the reader left off. */
92
- export function pageFromUrl(): number {
93
- const n = Number(new URLSearchParams(window.location.search).get("page"));
94
- return Number.isInteger(n) && n > 1 ? n : 1;
95
- }
96
-
97
- /** Record the current page in `?page=` via replaceState — preserving every other param, and
98
- * DROPPING the param at page 1 (the implicit default). This does NOT remount the renderer (only
99
- * path/format/refreshSignal do), so a paged viewer can update it freely while scrolling. */
100
- export function writePageToUrl(n: number): void {
101
- const params = new URLSearchParams(window.location.search);
102
- if (n > 1) params.set("page", String(n));
103
- else params.delete("page");
104
- const qs = params.toString();
105
- const url = window.location.pathname + (qs ? "?" + qs : "");
106
- if (url !== window.location.pathname + window.location.search) window.history.replaceState({}, "", url);
107
- }
108
-
109
- /** Write the JSON path (canonical colon form, converted to the slash-transport URL)
110
- * plus `?format=` into the URL. Path navigation pushes a history entry; switching
111
- * format replaces. Any other query params already present are kept (e.g. a renderer's
112
- * own options such as the CSV `sep`/`header`), so only `format` is overwritten here. */
113
- export function writeUrl(path: string, format: string, replace = false): void {
114
- const params = new URLSearchParams(window.location.search);
115
- params.set("format", format);
116
- // `?page=` is node-specific: a format switch (replace) keeps it, but navigating to another node
117
- // (push) must not carry the old node's page over — drop it there.
118
- if (!replace) params.delete("page");
119
- const url = `${urlOfPath(path || ":")}?${params.toString()}`;
120
- if (url === window.location.pathname + window.location.search) return;
121
- if (replace) window.history.replaceState({}, "", url);
122
- else window.history.pushState({}, "", url);
123
- }
124
-
125
- /** Breadcrumb crumbs for a path. The head is the CLI ROOT (`rootLabel`); it is
126
- * omitted when blank, so a default (cwd) root shows just the in-tree segments.
127
- * e.g. (":a:b", "examples") → [examples, a, b] (each decoded, linking to its path). */
128
- export function crumbs(p: string, rootLabel: string): { label: string; path: string }[] {
129
- const segs = strToSegs(p);
130
- const out: { label: string; path: string }[] = [];
131
- if (rootLabel) out.push({ label: rootLabel, path: ":" });
132
- segs.forEach((s, i) => {
133
- out.push({
134
- label: typeof s === "number" ? `[${s}]` : s,
135
- path: segsToStr(segs.slice(0, i + 1)),
136
- });
137
- });
138
- return out;
139
- }
@@ -1,326 +0,0 @@
1
- import { ReactNode } from "react";
2
-
3
- // Keep in sync with LINK_KEY / BINARY_KEY in src/server/yamlover.ts.
4
- //
5
- // A node shown only as a link (a container past the one-level view, or any binary
6
- // leaf) arrives as `{ [LINK_KEY]: {kind, path, count|size} }` — the same marker in
7
- // the value and the schema — so every representation renders the same hyperlink.
8
- //
9
- // The bytes of a selected binary leaf arrive as `{ [BINARY_KEY]: {format,size,
10
- // base64} }`, rendered as `!!binary` (YAML) or the metadata object (JSON).
11
- //
12
- // An `x-yamlover.rel` pointer arrives as `{ [REF_KEY]: {text, path} }` — the
13
- // pointer string rendered as a hyperlink that navigates to the resolved `path`
14
- // (or as plain text when `path` is null, i.e. the pointer does not resolve).
15
- const LINK_KEY = "$yamloverLink";
16
- const BINARY_KEY = "$yamloverBinary";
17
- const REF_KEY = "$yamloverRef";
18
- // An omni/mix node (a `!!omni` self-value + fields, or a `!!mix` of items + fields) arrives as
19
- // `{ [MIXED_KEY]: {kind, value?, entries:[{key,value}]} }`, rendered in yamlover as a leading
20
- // scalar (omni) then each entry positional (`- v`, key=null) or keyed (`k: v`).
21
- const MIXED_KEY = "$yamloverMixed";
22
-
23
- export interface Link {
24
- kind: "object" | "array" | "scalar" | "binary" | "omni" | "mix";
25
- type?: string; // the target's JSON-Schema type; with `format`, the routing key
26
- path: string;
27
- title?: string; // the target's schema title, when set (used as a link label)
28
- count?: number;
29
- size?: number;
30
- format?: string | null;
31
- value?: unknown; // for a link to a scalar: its value, shown as the label
32
- color?: string | null; // for a link to a pure color tag: its explicit color (badges)
33
- concrete?: string | null; // how the target is stored; `dir`/`yamlover` → a folder icon
34
- }
35
-
36
- interface Ref {
37
- text: string;
38
- path: string | null;
39
- }
40
-
41
- interface Mixed {
42
- kind: "omni" | "mix";
43
- value?: unknown; // omni: the node's own scalar self-value
44
- entries: { key: string | null; value: unknown }[]; // key=null ⇒ positional item, else keyed field
45
- }
46
-
47
- interface BinaryPayload {
48
- format: string | null;
49
- size: number;
50
- base64: string;
51
- }
52
-
53
- function asSingle<T>(v: unknown, key: string): T | null {
54
- if (v && typeof v === "object" && !Array.isArray(v)) {
55
- const keys = Object.keys(v as object);
56
- if (keys.length === 1 && keys[0] === key) return (v as any)[key] as T;
57
- }
58
- return null;
59
- }
60
-
61
- /** Read a one-level link marker (a nested container or binary shown as a
62
- * hyperlink), or null when `v` is not one. Exported for custom renderers that
63
- * need to treat a child's link specially (e.g. the chapter renderer). */
64
- export const asLink = (v: unknown) => asSingle<Link>(v, LINK_KEY);
65
- const asBinary = (v: unknown) => asSingle<BinaryPayload>(v, BINARY_KEY);
66
- const asRef = (v: unknown) => asSingle<Ref>(v, REF_KEY);
67
- const asMixed = (v: unknown) => asSingle<Mixed>(v, MIXED_KEY);
68
-
69
- type Syntax = "yaml" | "json";
70
-
71
- /**
72
- * Render a one-level value or schema in YAML or JSON syntax, syntax-highlighted,
73
- * with nested containers as hyperlinks. Used for every RHS representation so they
74
- * behave identically: scalars shown, nested objects/arrays clicked to descend.
75
- */
76
- export function Render({
77
- value,
78
- syntax,
79
- onNavigate,
80
- }: {
81
- value: unknown;
82
- syntax: Syntax;
83
- onNavigate: (path: string) => void;
84
- }) {
85
- const bin = asBinary(value);
86
- if (bin && syntax === "yaml") return <BinaryYaml bin={bin} />;
87
- const v = bin ?? value; // JSON shows the {format,size,base64} metadata object
88
-
89
- const out: ReactNode[] = [];
90
- const kc = { n: 0 };
91
- if (syntax === "yaml") emitYaml(v, 0, out, kc, onNavigate);
92
- else emitJson(v, 0, out, kc, onNavigate);
93
- return <>{out}</>;
94
- }
95
-
96
- /** A selected binary leaf as a YAML `!!binary` block (a comment carries the
97
- * format/size; the base64 is the block scalar's content, canonically wrapped at
98
- * 76 columns and indented two spaces). */
99
- function BinaryYaml({ bin }: { bin: BinaryPayload }) {
100
- const lines: string[] = [];
101
- for (let i = 0; i < bin.base64.length; i += 76) lines.push(" " + bin.base64.slice(i, i + 76));
102
- return (
103
- <>
104
- <span className="b">!!binary</span> <span className="punct">|</span>{" "}
105
- <span className="c">{`# ${bin.format ?? "binary"}, ${bin.size} bytes`}</span>
106
- {"\n"}
107
- <span className="s">{lines.join("\n")}</span>
108
- </>
109
- );
110
- }
111
-
112
- interface KC {
113
- n: number;
114
- }
115
-
116
- function linkNode(link: Link, syntax: Syntax, kc: KC, onNavigate: (p: string) => void): ReactNode {
117
- // a scalar child links by its rendered value (`~`/`null`, quoted/bare per syntax);
118
- // a container by its `{ … }`/`[ … ]` summary
119
- const label = link.kind === "scalar" ? scalarLabel(link.value, syntax) : linkLabel(link);
120
- return (
121
- <a
122
- key={kc.n++}
123
- className="descend"
124
- href={link.path}
125
- onClick={(e) => {
126
- e.preventDefault();
127
- onNavigate(link.path);
128
- }}
129
- >
130
- {label}
131
- </a>
132
- );
133
- }
134
-
135
- /** A scalar value as it would render in `syntax` — used as a scalar link's label. */
136
- function scalarLabel(v: unknown, syntax: Syntax): string {
137
- if (v === null || v === undefined) return syntax === "json" ? "null" : "~";
138
- if (typeof v === "boolean" || typeof v === "number") return String(v);
139
- return syntax === "json" ? JSON.stringify(v) : String(v);
140
- }
141
-
142
- /** A `rel` pointer: its text as a hyperlink to the resolved `path`, or — when the
143
- * pointer does not resolve — plain string text (no link). */
144
- function refNode(ref: Ref, syntax: Syntax, kc: KC, onNavigate: (p: string) => void): ReactNode {
145
- const text = syntax === "json" ? JSON.stringify(ref.text) : ref.text;
146
- if (!ref.path) return <span className="s" key={kc.n++}>{text}</span>;
147
- const target = ref.path;
148
- return (
149
- <a
150
- key={kc.n++}
151
- className="descend"
152
- href={target}
153
- onClick={(e) => {
154
- e.preventDefault();
155
- onNavigate(target);
156
- }}
157
- >
158
- {text}
159
- </a>
160
- );
161
- }
162
-
163
- function linkLabel(link: Link): string {
164
- const n = link.count ?? 0;
165
- if (link.kind === "array") return `[ array with ${n} ${n === 1 ? "item" : "items"} ]`;
166
- if (link.kind === "binary") return `< binary of ${link.size ?? 0} bytes >`;
167
- if (link.kind === "mix") return `{ mixed with ${n} ${n === 1 ? "entry" : "entries"} }`;
168
- if (link.kind === "omni") return `{ variant ${scalarLabel(link.value, "yaml")} + ${n} ${n === 1 ? "field" : "fields"} }`;
169
- return `{ object with ${n} ${n === 1 ? "property" : "properties"} }`;
170
- }
171
-
172
- function scalarNode(v: unknown, syntax: Syntax, kc: KC): ReactNode {
173
- if (v === null) return <span className="null" key={kc.n++}>{syntax === "json" ? "null" : "~"}</span>;
174
- if (typeof v === "boolean") return <span className="b" key={kc.n++}>{String(v)}</span>;
175
- if (typeof v === "number") return <span className="n" key={kc.n++}>{String(v)}</span>;
176
- const text = syntax === "json" ? JSON.stringify(v) : String(v);
177
- return <span className="s" key={kc.n++}>{text}</span>;
178
- }
179
-
180
- const isObj = (v: unknown): v is Record<string, unknown> =>
181
- typeof v === "object" && v !== null && !Array.isArray(v);
182
-
183
- // --------------------------------------------------------------------------- //
184
- // YAML
185
- // --------------------------------------------------------------------------- //
186
- function emitYaml(value: unknown, indent: number, out: ReactNode[], kc: KC, nav: (p: string) => void): void {
187
- const link = asLink(value);
188
- if (link) {
189
- out.push(linkNode(link, "yaml", kc, nav), "\n");
190
- return;
191
- }
192
- const ref = asRef(value);
193
- if (ref) {
194
- out.push(refNode(ref, "yaml", kc, nav), "\n");
195
- return;
196
- }
197
- const mixed = asMixed(value);
198
- if (mixed) {
199
- const pad = " ".repeat(indent);
200
- // omni: the node's own scalar value on its own line first (`!!omni 5` → `5`)
201
- if (mixed.kind === "omni") out.push(pad, scalarNode(mixed.value, "yaml", kc), "\n");
202
- for (const e of mixed.entries) {
203
- if (e.key === null) {
204
- out.push(pad, <span className="punct" key={kc.n++}>{"- "}</span>);
205
- emitYamlChild(e.value, indent, out, kc, nav, true);
206
- } else {
207
- out.push(pad, <span className="k" key={kc.n++}>{e.key}</span>, <span className="punct" key={kc.n++}>:</span>);
208
- emitYamlChild(e.value, indent, out, kc, nav);
209
- }
210
- }
211
- return;
212
- }
213
- if (isObj(value)) {
214
- const entries = Object.entries(value);
215
- if (!entries.length) {
216
- out.push(<span className="punct" key={kc.n++}>{"{}"}</span>, "\n");
217
- return;
218
- }
219
- const pad = " ".repeat(indent);
220
- for (const [k, v] of entries) {
221
- out.push(pad, <span className="k" key={kc.n++}>{k}</span>, <span className="punct" key={kc.n++}>:</span>);
222
- emitYamlChild(v, indent, out, kc, nav);
223
- }
224
- return;
225
- }
226
- if (Array.isArray(value)) {
227
- if (!value.length) {
228
- out.push(<span className="punct" key={kc.n++}>{"[]"}</span>, "\n");
229
- return;
230
- }
231
- const pad = " ".repeat(indent);
232
- for (const item of value) {
233
- out.push(pad, <span className="punct" key={kc.n++}>{"- "}</span>);
234
- emitYamlChild(item, indent, out, kc, nav, true);
235
- }
236
- return;
237
- }
238
- out.push(scalarNode(value, "yaml", kc), "\n");
239
- }
240
-
241
- // Render a value that follows a `key:` or `- ` — inline for scalars/links/empties,
242
- // or a newline then a nested block.
243
- function emitYamlChild(
244
- v: unknown,
245
- indent: number,
246
- out: ReactNode[],
247
- kc: KC,
248
- nav: (p: string) => void,
249
- inArray = false,
250
- ): void {
251
- const link = asLink(v);
252
- const ref = asRef(v);
253
- if (link) {
254
- out.push(" ", linkNode(link, "yaml", kc, nav), "\n");
255
- } else if (ref) {
256
- out.push(" ", refNode(ref, "yaml", kc, nav), "\n");
257
- } else if (isObj(v) && Object.keys(v).length === 0) {
258
- out.push(" ", <span className="punct" key={kc.n++}>{"{}"}</span>, "\n");
259
- } else if (Array.isArray(v) && v.length === 0) {
260
- out.push(" ", <span className="punct" key={kc.n++}>{"[]"}</span>, "\n");
261
- } else if (isObj(v) || Array.isArray(v)) {
262
- out.push("\n");
263
- emitYaml(v, indent + 2, out, kc, nav);
264
- } else {
265
- out.push(" ", scalarNode(v, "yaml", kc), "\n");
266
- }
267
- void inArray;
268
- }
269
-
270
- // --------------------------------------------------------------------------- //
271
- // JSON (nested containers become links, so the view is not strictly valid JSON —
272
- // it is the same one-level representation, in JSON syntax)
273
- // --------------------------------------------------------------------------- //
274
- function emitJson(value: unknown, indent: number, out: ReactNode[], kc: KC, nav: (p: string) => void): void {
275
- const link = asLink(value);
276
- if (link) {
277
- out.push(linkNode(link, "json", kc, nav));
278
- return;
279
- }
280
- const ref = asRef(value);
281
- if (ref) {
282
- out.push(refNode(ref, "json", kc, nav));
283
- return;
284
- }
285
- const mixed = asMixed(value);
286
- const objEntries: [string, unknown][] | null = mixed
287
- ? [
288
- ...(mixed.kind === "omni" ? ([["$value", mixed.value]] as [string, unknown][]) : []),
289
- ...mixed.entries.map((e, i): [string, unknown] => [e.key ?? String(i), e.value]),
290
- ]
291
- : isObj(value)
292
- ? Object.entries(value)
293
- : null;
294
- if (objEntries) {
295
- const entries = objEntries;
296
- if (!entries.length) {
297
- out.push(<span className="punct" key={kc.n++}>{"{}"}</span>);
298
- return;
299
- }
300
- const pad = " ".repeat(indent + 2);
301
- out.push(<span className="punct" key={kc.n++}>{"{"}</span>, "\n");
302
- entries.forEach(([k, v], i) => {
303
- out.push(pad, <span className="k" key={kc.n++}>{`"${k}"`}</span>, <span className="punct" key={kc.n++}>{": "}</span>);
304
- emitJson(v, indent + 2, out, kc, nav);
305
- out.push(i < entries.length - 1 ? "," : "", "\n");
306
- });
307
- out.push(" ".repeat(indent), <span className="punct" key={kc.n++}>{"}"}</span>);
308
- return;
309
- }
310
- if (Array.isArray(value)) {
311
- if (!value.length) {
312
- out.push(<span className="punct" key={kc.n++}>{"[]"}</span>);
313
- return;
314
- }
315
- const pad = " ".repeat(indent + 2);
316
- out.push(<span className="punct" key={kc.n++}>{"["}</span>, "\n");
317
- value.forEach((item, i) => {
318
- out.push(pad);
319
- emitJson(item, indent + 2, out, kc, nav);
320
- out.push(i < value.length - 1 ? "," : "", "\n");
321
- });
322
- out.push(" ".repeat(indent), <span className="punct" key={kc.n++}>{"]"}</span>);
323
- return;
324
- }
325
- out.push(scalarNode(value, "json", kc));
326
- }