yamlover 0.3.4 → 0.3.6

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-CFkcuQ1f.js +1 -0
  63. package/dist/client/assets/djvu-B3wuFWwI.js +1 -0
  64. package/dist/client/assets/docx-putHYXzO.js +230 -0
  65. package/dist/client/assets/heic-oIxhIbIW.js +66 -0
  66. package/dist/client/assets/imagemap-CUKUIcGe.js +1 -0
  67. package/dist/client/assets/index-CQ3TGVSx.js +617 -0
  68. package/dist/client/assets/index-wTjOzmSY.css +1 -0
  69. package/dist/client/assets/map-CZS083CS.js +1 -0
  70. package/dist/client/assets/paged-CjxSuuik.js +1 -0
  71. package/dist/client/assets/panzoom-C7OuIIda.js +4 -0
  72. package/dist/client/assets/panzoom-CIGW-MKW.css +1 -0
  73. package/dist/client/assets/pdf-Dz3gUx0B.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-C8yNwHFw.js +11 -0
  77. package/dist/client/assets/spreadsheet-DBv7aqoN.js +38 -0
  78. package/dist/client/assets/tiff-73RKtIux.js +1 -0
  79. package/{index.html → dist/client/index.html} +2 -1
  80. package/package.json +18 -21
  81. package/src/client/App.tsx +0 -424
  82. package/src/client/NodeView.tsx +0 -422
  83. package/src/client/TaskStrip.tsx +0 -34
  84. package/src/client/Tree.tsx +0 -97
  85. package/src/client/api.ts +0 -213
  86. package/src/client/icons.ts +0 -91
  87. package/src/client/links.tsx +0 -108
  88. package/src/client/live.ts +0 -42
  89. package/src/client/main.tsx +0 -10
  90. package/src/client/paste-html.ts +0 -228
  91. package/src/client/paste-links.ts +0 -42
  92. package/src/client/paths.ts +0 -139
  93. package/src/client/render.tsx +0 -339
  94. package/src/client/renderers/annotate.tsx +0 -641
  95. package/src/client/renderers/asciidoc.tsx +0 -36
  96. package/src/client/renderers/chapter.tsx +0 -142
  97. package/src/client/renderers/csv.tsx +0 -234
  98. package/src/client/renderers/decoded.tsx +0 -72
  99. package/src/client/renderers/djvu.tsx +0 -271
  100. package/src/client/renderers/djvuWorker.ts +0 -99
  101. package/src/client/renderers/doc.tsx +0 -40
  102. package/src/client/renderers/docx.tsx +0 -49
  103. package/src/client/renderers/epub.tsx +0 -147
  104. package/src/client/renderers/explorer.tsx +0 -294
  105. package/src/client/renderers/fb2.tsx +0 -149
  106. package/src/client/renderers/headings.ts +0 -69
  107. package/src/client/renderers/heic.tsx +0 -23
  108. package/src/client/renderers/imagemap.tsx +0 -170
  109. package/src/client/renderers/kml.ts +0 -46
  110. package/src/client/renderers/latex.tsx +0 -37
  111. package/src/client/renderers/map.tsx +0 -205
  112. package/src/client/renderers/marklower.tsx +0 -120
  113. package/src/client/renderers/markup.tsx +0 -64
  114. package/src/client/renderers/media.tsx +0 -19
  115. package/src/client/renderers/paged.ts +0 -137
  116. package/src/client/renderers/panzoom.ts +0 -101
  117. package/src/client/renderers/pdf.tsx +0 -316
  118. package/src/client/renderers/plaintext.tsx +0 -120
  119. package/src/client/renderers/plantuml.tsx +0 -83
  120. package/src/client/renderers/psd.tsx +0 -25
  121. package/src/client/renderers/registry.tsx +0 -411
  122. package/src/client/renderers/rtf.tsx +0 -210
  123. package/src/client/renderers/spreadsheet.tsx +0 -105
  124. package/src/client/renderers/tag.tsx +0 -113
  125. package/src/client/renderers/text.tsx +0 -42
  126. package/src/client/renderers/tiff.tsx +0 -33
  127. package/src/client/styles.css +0 -1212
  128. package/src/client/vendor/README.md +0 -30
  129. package/src/client/vite-env.d.ts +0 -31
  130. package/src/server/api.ts +0 -147
  131. package/src/server/embed.ts +0 -187
  132. package/src/server/engine-api.ts +0 -1563
  133. package/src/server/gitignore.ts +0 -81
  134. package/src/server/node-kind.ts +0 -61
  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,424 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { fetchInfo, fetchTasks, fetchTree, PasteResult, TaskInfo, TreeNode } from "./api";
3
- import { Tree } from "./Tree";
4
- import { TaskStrip } from "./TaskStrip";
5
- import { NodeView, Format, FORMATS, DEFAULT_FORMAT } from "./NodeView";
6
- import { rendererName, tocView } from "./renderers/registry";
7
-
8
- const isStandardFormat = (f: Format) => (FORMATS as string[]).includes(f);
9
- import { crumbs, formatFromUrl, isAncestorPath, pathFromUrl, segsToStr, strToSegs, writeUrl } from "./paths";
10
- import { broadcastDiff } from "./live";
11
-
12
- // Levels of the TOC fetched at once — initially and on each lazy expand. One
13
- // level keeps every fetch cheap on a huge/slow tree (a fetch only reads the
14
- // directories it actually shows); deeper levels load instantly on expand.
15
- const INITIAL_DEPTH = 1;
16
-
17
- /** Return a copy of `tree` with the children of the node at `path` replaced. */
18
- function replaceChildren(tree: TreeNode, path: string, children: TreeNode[]): TreeNode {
19
- if (tree.path === path) return { ...tree, children };
20
- if (!tree.children.length) return tree;
21
- return { ...tree, children: tree.children.map((c) => replaceChildren(c, path, children)) };
22
- }
23
-
24
- /** Merge a freshly fetched branch over the old one at the same path: the fresh rows win
25
- * (labels, flags, order, additions/removals), but a row that already had its children
26
- * loaded keeps them (recursively) when the fresh fetch didn't reach that deep — so a live
27
- * refresh never collapses what the user has expanded. */
28
- function mergeBranch(old: TreeNode | undefined, fresh: TreeNode): TreeNode {
29
- if (!old) return fresh;
30
- const byPath = new Map(old.children.map((c) => [c.path, c] as const));
31
- const children = fresh.children.length
32
- ? fresh.children.map((c) => mergeBranch(byPath.get(c.path), c))
33
- : fresh.hasChildren
34
- ? old.children // past the fetch depth — keep the loaded subtree
35
- : [];
36
- return { ...fresh, children };
37
- }
38
-
39
- /** Return a copy of `tree` with the fresh subtree merged in at `path` (see mergeBranch). */
40
- function mergeAt(tree: TreeNode, path: string, fresh: TreeNode): TreeNode {
41
- if (tree.path === path) return mergeBranch(tree, fresh);
42
- if (!tree.children.length) return tree;
43
- return { ...tree, children: tree.children.map((c) => mergeAt(c, path, fresh)) };
44
- }
45
-
46
- /** Find the node at `path` in the (partially loaded) tree. */
47
- function findNode(tree: TreeNode, path: string): TreeNode | null {
48
- if (tree.path === path) return tree;
49
- for (const c of tree.children) {
50
- const f = findNode(c, path);
51
- if (f) return f;
52
- }
53
- return null;
54
- }
55
-
56
- /** The shallowest ancestor of `current` that is loaded but whose children are
57
- * not yet fetched — the next branch to load while revealing `current`. */
58
- function nextToLoad(tree: TreeNode, current: string): string | null {
59
- const segs = strToSegs(current);
60
- for (let k = 0; k < segs.length; k++) {
61
- const ancestor = segsToStr(segs.slice(0, k));
62
- const node = findNode(tree, ancestor);
63
- if (!node) return null; // the path diverges from the tree (e.g. invalid) — stop
64
- if (node.hasChildren && node.children.length === 0) return ancestor;
65
- }
66
- return null;
67
- }
68
-
69
- /** The TOC rows in document (pre-order) order, mirroring exactly what `Tree`
70
- * shows — `tocView` applies the same per-renderer unwrap/filter (chapters
71
- * surface subchapters, dirs show children). Used by Ctrl-PgDn/PgUp to step the
72
- * selection to the neighbouring entry. Covers only the LOADED tree: per-branch
73
- * collapse state lives in each `Tree`'s local `open`, not here — but a branch
74
- * starts open once its children are loaded, so loaded ≈ visible in practice;
75
- * deep unloaded branches simply aren't reachable until expanded (lazy load). */
76
- function flattenToc(tree: TreeNode | null): string[] {
77
- if (!tree) return [];
78
- const out: string[] = [];
79
- const walk = (n: TreeNode) => {
80
- out.push(n.path);
81
- for (const c of tocView(n).children) walk(c);
82
- };
83
- walk(tree);
84
- return out;
85
- }
86
-
87
- export function App() {
88
- const [tree, setTree] = useState<TreeNode | null>(null);
89
- const [error, setError] = useState<string | null>(null);
90
- const [current, setCurrent] = useState<string>(pathFromUrl());
91
- const [format, setFormat] = useState<Format>(formatFromUrl(DEFAULT_FORMAT) as Format);
92
- const [rootLabel, setRootLabel] = useState<string>(""); // CLI ROOT (breadcrumb head)
93
- const [leftWidth, setLeftWidth] = useState<number>(320);
94
- const mainRef = useRef<HTMLElement>(null); // RHS pane — focused on TOC click so the keyboard drives the viewer
95
-
96
- // The breadcrumb head is the ROOT given on the command line (blank if omitted).
97
- useEffect(() => {
98
- fetchInfo().then((i) => setRootLabel(i.root)).catch(() => {});
99
- }, []);
100
-
101
- // Long-running server tasks (indexing, hashing, …): seeded from /api/tasks (a page loaded
102
- // mid-task), updated by `{type:"task"}` SSE frames; finished ones linger ~3s then drop.
103
- const [tasks, setTasks] = useState<TaskInfo[]>([]);
104
- useEffect(() => {
105
- fetchTasks().then(setTasks).catch(() => {});
106
- }, []);
107
- const upsertTask = useCallback((t: TaskInfo) => {
108
- setTasks((prev) => [...prev.filter((x) => x.id !== t.id), t].sort((a, b) => a.startedAt - b.startedAt));
109
- if (t.state !== "running") {
110
- setTimeout(() => setTasks((prev) => prev.filter((x) => x.id !== t.id)), 3000);
111
- }
112
- }, []);
113
-
114
- // Whether the initial URL pinned a representation; if not, a landing node that
115
- // has a renderer opens in its rendered view (decided once the TOC loads).
116
- const explicitFormat = useRef(new URLSearchParams(window.location.search).has("format"));
117
-
118
- // Reflect the initial path+format back into the URL (so ?format= is present).
119
- useEffect(() => {
120
- writeUrl(current, format, true);
121
- // eslint-disable-next-line react-hooks/exhaustive-deps
122
- }, []);
123
-
124
- // Load the TOC's first few levels; deeper branches load on expand.
125
- useEffect(() => {
126
- fetchTree(":", INITIAL_DEPTH)
127
- .then(setTree)
128
- .catch((e) => setError(e.message));
129
- // eslint-disable-next-line react-hooks/exhaustive-deps
130
- }, []);
131
-
132
- // First time the landing node appears in the TOC (immediately when shallow,
133
- // after lazy-loading the path when deep-linked), open it in its renderer's
134
- // view — unless the URL pinned a representation. Runs once; later tab switches
135
- // and in-app navigation make their own choice.
136
- const resolvedLanding = useRef(false);
137
- useEffect(() => {
138
- if (explicitFormat.current || resolvedLanding.current || !tree) return;
139
- const n = findNode(tree, current);
140
- if (!n) return; // not loaded along the path yet — wait for the next pass
141
- resolvedLanding.current = true;
142
- const rn = rendererName(n, n.concrete);
143
- if (rn) {
144
- setFormat(rn);
145
- writeUrl(current, rn, true);
146
- }
147
- }, [tree, current]);
148
-
149
- // Fetch a collapsed branch's children and splice them into the tree in place.
150
- // `levels` is how deep to pull (default one): a renderer whose TOC rows sit
151
- // deeper than its direct children (a chapter, surfacing subchapters from under
152
- // its `children` wrapper) asks for more, so one expand reveals them.
153
- const loadChildren = useCallback(async (path: string, levels = INITIAL_DEPTH) => {
154
- const sub = await fetchTree(path, levels);
155
- setTree((t) => (t ? replaceChildren(t, path, sub.children) : t));
156
- }, []);
157
-
158
- // Reveal the current node: lazily load the branches along its path (one level
159
- // per pass; each load re-runs this until the whole path is present), so a
160
- // deep-linked / pasted URL gets its TOC entry expanded and selected.
161
- useEffect(() => {
162
- if (!tree) return;
163
- const next = nextToLoad(tree, current);
164
- if (next) loadChildren(next).catch(() => {});
165
- }, [tree, current, loadChildren]);
166
-
167
- // Live refresh: the server watches the filesystem and pushes each reindex diff over SSE
168
- // (/api/events). For every changed path we re-fetch its deepest LOADED tree branch (merged
169
- // in, so expanded subtrees stay open), and re-fetch the node pane when the current node is
170
- // touched. Unloaded branches need nothing — they fetch fresh on expand anyway.
171
- const treeRef = useRef(tree);
172
- treeRef.current = tree;
173
- const currentRef = useRef(current);
174
- currentRef.current = current;
175
- const [refreshSignal, setRefreshSignal] = useState(0);
176
-
177
- const refreshBranches = useCallback(async (paths: string[]) => {
178
- const t = treeRef.current;
179
- if (!t) return;
180
- const targets = new Set<string>();
181
- for (const p of paths) {
182
- const segs = strToSegs(p);
183
- let best = ":";
184
- for (let k = 0; k < segs.length; k++) {
185
- const anc = segsToStr(segs.slice(0, k));
186
- const node = findNode(t, anc);
187
- if (!node) break;
188
- if (anc === ":" || node.children.length > 0) best = anc;
189
- }
190
- targets.add(best);
191
- }
192
- const list = [...targets].filter((a) => ![...targets].some((b) => b !== a && isAncestorPath(b, a)));
193
- await Promise.all(
194
- list.map(async (p) => {
195
- const sub = await fetchTree(p, INITIAL_DEPTH);
196
- setTree((prev) => (prev ? mergeAt(prev, p, sub) : prev));
197
- }),
198
- );
199
- }, []);
200
-
201
- // Cold-start recovery: a page opened before the FIRST index landed has no tree (the initial
202
- // fetch 404'd on an empty store). When the index task finishes — or any diff arrives —
203
- // retry the initial fetches and clear the stale error.
204
- const retryInitial = useCallback(() => {
205
- if (treeRef.current) return;
206
- fetchTree(":", INITIAL_DEPTH)
207
- .then((t) => {
208
- setTree(t);
209
- setError(null);
210
- })
211
- .catch(() => {});
212
- fetchInfo().then((i) => setRootLabel(i.root)).catch(() => {});
213
- }, []);
214
-
215
- useEffect(() => {
216
- if (typeof EventSource === "undefined") return; // test envs (jsdom) have no SSE
217
- const es = new EventSource("/api/events");
218
- es.onmessage = (ev) => {
219
- try {
220
- const msg = JSON.parse(ev.data) as { type?: string };
221
- if (msg.type === "task") {
222
- const t = (msg as unknown as { task: TaskInfo }).task;
223
- upsertTask(t);
224
- if (t.state === "done") retryInitial(); // no-op once the tree is loaded
225
- return;
226
- }
227
- const diff = msg as unknown as {
228
- added: string[]; changed: string[]; removed: string[];
229
- moved?: { from: string; to: string }[]; // inferred moves (frame-compatible: optional)
230
- };
231
- retryInitial(); // no-op once the tree is loaded
232
- const paths = [...diff.added, ...diff.changed, ...diff.removed,
233
- ...(diff.moved ?? []).flatMap((m) => [m.from, m.to])];
234
- if (!paths.length) return;
235
- // Re-broadcast for the useDiffBump subscribers (live.ts — the unified change flow):
236
- // hooks outside this component's prop reach refetch from the same diff currency.
237
- broadcastDiff({ paths, removed: diff.removed });
238
- refreshBranches(paths).catch(() => {});
239
- const cur = currentRef.current;
240
- if (paths.some((p) => p === cur || isAncestorPath(p, cur) || isAncestorPath(cur, p))) {
241
- setRefreshSignal((s) => s + 1);
242
- }
243
- } catch {
244
- // a malformed frame — ignore
245
- }
246
- };
247
- return () => es.close();
248
- }, [refreshBranches, upsertTask, retryInitial]);
249
-
250
- // Keep state in sync with the browser's back/forward buttons.
251
- useEffect(() => {
252
- const onPop = () => {
253
- setCurrent(pathFromUrl());
254
- setFormat(formatFromUrl(DEFAULT_FORMAT) as Format);
255
- };
256
- window.addEventListener("popstate", onPop);
257
- return () => window.removeEventListener("popstate", onPop);
258
- }, []);
259
-
260
- // Navigating changes the path. The format carries over, except: a target that
261
- // has a renderer opens in that renderer's view (so e.g. a chapter reads as a
262
- // page, with its tab active), and leaving a renderer node drops a stale renderer
263
- // format back to the default. A target absent from the TOC keeps the current
264
- // format — reading deeper into a rendered tree stays rendered. NodeView makes
265
- // the final call from the fetched node's (type, format).
266
- const navigate = useCallback(
267
- (p: string) => {
268
- const target = tree ? findNode(tree, p) : null;
269
- let f: Format = format;
270
- if (target) {
271
- const rn = rendererName(target, target.concrete);
272
- f = rn ?? (isStandardFormat(format) ? format : DEFAULT_FORMAT);
273
- }
274
- writeUrl(p, f, false);
275
- setCurrent(p);
276
- if (f !== format) setFormat(f);
277
- },
278
- [format, tree],
279
- );
280
-
281
- // Selecting a TOC row navigates AND hands keyboard focus to the RHS pane, so
282
- // Ctrl-PgDn/PgUp (and plain scroll keys) drive the viewer right after a click.
283
- // Scoped to the tree — crumbs and in-content links keep plain `navigate`.
284
- const selectFromToc = useCallback(
285
- (p: string) => {
286
- navigate(p);
287
- mainRef.current?.focus();
288
- },
289
- [navigate],
290
- );
291
-
292
- // Ctrl/Alt + Down / Up step the selection to the next / previous TOC entry in
293
- // document order (Alt as well as Ctrl because Ctrl+Up/Down is taken by macOS
294
- // Mission Control). Attached once; reads live state through refs so the listener
295
- // stays stable. `navigate` reveals + scrolls the new row (Tree's selected effect).
296
- const navigateRef = useRef(navigate);
297
- navigateRef.current = navigate;
298
- useEffect(() => {
299
- const onKey = (e: KeyboardEvent) => {
300
- if (!(e.ctrlKey || e.altKey) || (e.key !== "ArrowDown" && e.key !== "ArrowUp")) return;
301
- const t = e.target as HTMLElement | null;
302
- if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
303
- const order = flattenToc(treeRef.current);
304
- const i = order.indexOf(currentRef.current);
305
- if (i < 0) return; // current not in the loaded TOC yet — nothing to step from
306
- const next = Math.min(Math.max(i + (e.key === "ArrowDown" ? 1 : -1), 0), order.length - 1);
307
- e.preventDefault();
308
- if (next !== i) navigateRef.current(order[next]);
309
- };
310
- window.addEventListener("keydown", onKey);
311
- return () => window.removeEventListener("keydown", onKey);
312
- }, []);
313
-
314
- const changeFormat = useCallback(
315
- (f: Format) => {
316
- writeUrl(current, f, true);
317
- setFormat(f);
318
- },
319
- [current],
320
- );
321
-
322
- // A paste/upload added a file (and maybe a chapter chunk) at `p`. Reload that branch of the TOC
323
- // so a new directory child shows up; fall back to the root when the branch isn't loaded yet.
324
- const onContentChanged = useCallback(
325
- (p: string) => {
326
- const target = tree ? findNode(tree, p) : null;
327
- loadChildren(target ? p : ":").catch(() => {});
328
- },
329
- [tree, loadChildren],
330
- );
331
-
332
- // A file pasted/dropped onto a directory MEMBER landed in the enclosing dir — open it. We fetch
333
- // the dir's branch first (so the TOC shows the file AND we learn its type/format), then navigate
334
- // to it in its renderer's view (an image opens as an image, not as data). Falls back to a plain
335
- // navigate if the branch fetch fails.
336
- const onOpenUploaded = useCallback(
337
- async (result: PasteResult) => {
338
- const dir = result.dir ?? ":";
339
- try {
340
- const sub = await fetchTree(dir, INITIAL_DEPTH);
341
- setTree((t) => (t ? replaceChildren(t, dir, sub.children) : t));
342
- const fileNode = sub.children.find((c) => c.path === result.path);
343
- const f: Format = (fileNode ? rendererName(fileNode, fileNode.concrete) : null) ?? DEFAULT_FORMAT;
344
- writeUrl(result.path, f, false);
345
- setCurrent(result.path);
346
- setFormat(f);
347
- } catch {
348
- navigate(result.path);
349
- }
350
- },
351
- [navigate],
352
- );
353
-
354
- // --- draggable splitter -------------------------------------------------- //
355
- const dragging = useRef(false);
356
- useEffect(() => {
357
- const onMove = (e: MouseEvent) => {
358
- if (!dragging.current) return;
359
- setLeftWidth(Math.min(Math.max(e.clientX, 160), window.innerWidth - 240));
360
- };
361
- const onUp = () => {
362
- dragging.current = false;
363
- document.body.style.userSelect = "";
364
- };
365
- window.addEventListener("mousemove", onMove);
366
- window.addEventListener("mouseup", onUp);
367
- return () => {
368
- window.removeEventListener("mousemove", onMove);
369
- window.removeEventListener("mouseup", onUp);
370
- };
371
- }, []);
372
-
373
- return (
374
- <div className="app">
375
- <header className="topbar">
376
- <nav className="crumbs">
377
- {crumbs(current, rootLabel).map((c, i) => (
378
- <span key={c.path}>
379
- {i > 0 && <span className="crumb-sep">:</span>}
380
- <a
381
- className="crumb"
382
- href={c.path}
383
- onClick={(e) => {
384
- e.preventDefault();
385
- navigate(c.path);
386
- }}
387
- >
388
- {c.label}
389
- </a>
390
- </span>
391
- ))}
392
- </nav>
393
- <TaskStrip tasks={tasks} />
394
- </header>
395
-
396
- <div className="body">
397
- <aside className="pane left" style={{ width: leftWidth }}>
398
- {(() => {
399
- // no tree yet + a server task running ⇒ the index is still being built — show
400
- // its progress instead of a stale fetch error / a bare "loading…"
401
- const running = !tree ? tasks.find((t) => t.state === "running") : undefined;
402
- if (running) {
403
- const { done, total } = running.progress;
404
- return <div className="loading">{running.label}… {total ? `${done}/${total}` : ""}</div>;
405
- }
406
- if (error) return <div className="error">{error}</div>;
407
- if (!tree) return <div className="loading">loading…</div>;
408
- return <Tree node={tree} current={current} onSelect={selectFromToc} onLoadChildren={loadChildren} />;
409
- })()}
410
- </aside>
411
- <div
412
- className="splitter"
413
- onMouseDown={() => {
414
- dragging.current = true;
415
- document.body.style.userSelect = "none";
416
- }}
417
- />
418
- <main className="pane right" ref={mainRef} tabIndex={-1}>
419
- <NodeView path={current} format={format} refreshSignal={refreshSignal} onFormat={changeFormat} onNavigate={navigate} onContentChanged={onContentChanged} onOpenUploaded={onOpenUploaded} />
420
- </main>
421
- </div>
422
- </div>
423
- );
424
- }