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,294 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import { NodeJson, fetchTagged } from "../api";
3
- import { asLink, Link } from "../render";
4
- import { typeIcon } from "../icons";
5
- import { TAG_FORMAT, tagLabel, tagBody, resolveTagColor } from "./tag";
6
- import { displayPath, displayKey } from "../paths";
7
- import { touchesYamlover, useDiffBump } from "../live";
8
-
9
- const ANNOTATION_FORMAT = "x-yamlover-annotation";
10
- const MIXED_KEY = "$yamloverMixed";
11
-
12
- // ---- the view mode: a URL parameter (`?view=`), so a view is a shareable link ---- //
13
-
14
- const VIEWS = ["large", "small"] as const;
15
- type ViewMode = (typeof VIEWS)[number];
16
- const DEFAULT_VIEW: ViewMode = "large";
17
- const params = () => new URLSearchParams(window.location.search);
18
-
19
- /** The grid view from the URL's `?view=`, or the default (an unknown value ignored). */
20
- export function explorerViewMode(): ViewMode {
21
- const v = params().get("view");
22
- return (VIEWS as readonly string[]).includes(v ?? "") ? (v as ViewMode) : DEFAULT_VIEW;
23
- }
24
-
25
- function writeViewMode(v: ViewMode): void {
26
- const q = params();
27
- if (v === DEFAULT_VIEW) q.delete("view");
28
- else q.set("view", v);
29
- const qs = q.toString();
30
- window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : ""));
31
- }
32
-
33
- /** The view selector beside the explorer tab (the renderer's `config` hook) — writes
34
- * `?view=` and rerenders, like the plaintext encoding control. */
35
- export function ExplorerViewControl({ rerender }: { rerender: () => void }) {
36
- return (
37
- <label className="enc-control">
38
- view{" "}
39
- <select
40
- value={explorerViewMode()}
41
- onChange={(e) => {
42
- writeViewMode(e.target.value as ViewMode);
43
- rerender();
44
- }}
45
- >
46
- <option value="large">large icons</option>
47
- <option value="small">small icons</option>
48
- </select>
49
- </label>
50
- );
51
- }
52
-
53
- /**
54
- * The EXPLORER renderer — a directory (or a tag) as a desktop file manager:
55
- * every member an icon + label, with the node's reverse UPLINKS (`relations`:
56
- * the `..` parent and each upstream `*`/`~` source) leading the grid as
57
- * visually distinct items. Two views, chosen by the `?view=` URL parameter
58
- * (a renderer param, like the markup width or the CSV options): **large
59
- * icons** (the default — tiles, the icon above the label) and **small icons**
60
- * (rows, the icon beside the label).
61
- *
62
- * It claims two shapes:
63
- * - a node stored as a filesystem directory (`concrete` `dir`/`yamlover`,
64
- * the registry's concrete fallback) — ALL members show, not just files:
65
- * scalar members read `key: value`, containers and binaries link onward;
66
- * - a tag (`x-yamlover-tag`) — the members are the MATERIALS filed under it
67
- * (GET /api/tagged: annotations resolved to their `target`, deduped),
68
- * alongside its owned fields (subtags as colored badges); the mediating
69
- * annotation nodes themselves stay out of the grid.
70
- */
71
-
72
- /** One grid item: a navigable link marker (or a defensive non-link member). */
73
- export interface ExplorerItem {
74
- key: string; // the label source: a member key, `[i]`, or a relation key (`..`, `/eve`, …)
75
- link: Link | null;
76
- raw?: unknown; // the value when it is not a link marker (rendered inert)
77
- up?: boolean; // an uplink (relations) item — shown first, styled distinct
78
- }
79
-
80
- /** The uplink items: the node's `relations` in server order (`..` always first). */
81
- export function uplinkItems(relations?: Record<string, unknown>): ExplorerItem[] {
82
- return Object.entries(relations ?? {})
83
- .map(([key, v]) => ({ key, link: asLink(v), up: true }))
84
- .filter((it) => it.link != null);
85
- }
86
-
87
- /** The member items, from any depth-1 projection shape: a plain object's entries, an array's
88
- * items, or a `$yamloverMixed` marker's entries (the omni self-value is the BODY — shown in
89
- * the header, not the grid). At depth 1 every member arrives as a `$yamloverLink` marker;
90
- * a non-link value is kept defensively as an inert label. */
91
- export function memberItems(node: NodeJson): ExplorerItem[] {
92
- const v = node.value;
93
- if (Array.isArray(v)) return v.map((item, i) => ({ key: `[${i}]`, link: asLink(item), raw: item }));
94
- if (!v || typeof v !== "object") return [];
95
- const mixed = (v as Record<string, unknown>)[MIXED_KEY] as
96
- | { entries?: { key: string | null; value: unknown }[] }
97
- | undefined;
98
- if (Object.keys(v).length === 1 && mixed?.entries) {
99
- return mixed.entries.map((e, i) => ({ key: e.key ?? `[${i}]`, link: asLink(e.value), raw: e.value }));
100
- }
101
- return Object.entries(v as Record<string, unknown>).map(([key, val]) => ({ key, link: asLink(val), raw: val }));
102
- }
103
-
104
- /** A scalar member's value as a short label tail (`key: <this>`) — its first line,
105
- * capped (a long text would bloat the DOM; the CSS ellipsis only hides overflow). */
106
- function scalarText(v: unknown): string {
107
- if (v === null || v === undefined) return "~";
108
- const line = String(v).split("\n", 1)[0];
109
- return line.length > 80 ? line.slice(0, 79) + "…" : line;
110
- }
111
-
112
- /** A scalar whose format marks it file-like CONTENT (a media type such as `text/markdown`,
113
- * or an `x-yamlover-…` shape) — its value is the whole document, not a datum: the grid
114
- * shows just the name (the icon already says what it is). A schema VALUE format (`date`,
115
- * `email`, …) keeps the `key: value` form. */
116
- function isDocFormat(f?: string | null): boolean {
117
- return !!f && (f.includes("/") || f.startsWith("x-yamlover-"));
118
- }
119
-
120
- /** The grid item to move to from `cur` for an arrow key. Left/Right step in reading
121
- * order; Up/Down pick the nearest item in the adjacent row, preferring the same
122
- * column — measured from live geometry, so it's correct for the wrapping flex grid
123
- * (variable columns, a short last row) without knowing the column count. */
124
- function arrowTarget(els: (HTMLElement | null)[], cur: number, key: string, count: number): number {
125
- if (key === "ArrowRight") return Math.min(cur + 1, count - 1);
126
- if (key === "ArrowLeft") return Math.max(cur - 1, 0);
127
- if (key === "Home") return 0;
128
- if (key === "End") return count - 1;
129
- const a = els[cur];
130
- if (!a) return cur;
131
- const r = a.getBoundingClientRect();
132
- const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
133
- const down = key === "ArrowDown";
134
- let best = cur, bestScore = Infinity;
135
- for (let i = 0; i < count; i++) {
136
- const el = els[i];
137
- if (!el || i === cur) continue;
138
- const ri = el.getBoundingClientRect();
139
- const ix = ri.left + ri.width / 2, iy = ri.top + ri.height / 2;
140
- if (down ? iy <= cy + 1 : iy >= cy - 1) continue; // must lie in the arrow's direction
141
- const score = Math.abs(ix - cx) * 2 + Math.abs(iy - cy); // prefer same column, nearest row
142
- if (score < bestScore) { bestScore = score; best = i; }
143
- }
144
- return best;
145
- }
146
-
147
- function Item({ it, active, setRef, onFocus, onNavigate }: {
148
- it: ExplorerItem;
149
- active: boolean;
150
- setRef: (el: HTMLElement | null) => void;
151
- onFocus: () => void;
152
- onNavigate: (path: string) => void;
153
- }) {
154
- const link = it.link;
155
- const tabIndex = active ? 0 : -1; // roving tabindex: only the selected item is in the tab order
156
- if (!link) {
157
- // not a marker (unexpected at depth 1) — an inert label, no navigation
158
- return (
159
- <span className="dirview-item" ref={setRef} tabIndex={tabIndex} onFocus={onFocus}>
160
- <span className="dirview-icon t-bin">•</span>
161
- <span className="dirview-label">{it.key}: {scalarText(it.raw)}</span>
162
- </span>
163
- );
164
- }
165
- const g = typeIcon(link.type ?? link.kind, link.format ?? null, link.concrete);
166
- // an uplink labels by its (decoded) relation key; a member by title, else its key
167
- const name = it.up ? displayKey(it.key) : link.title ?? it.key;
168
- const label =
169
- link.format === TAG_FORMAT ? (
170
- // a tag member (e.g. a subtag) keeps its badge color everywhere
171
- <span className="tagtag" style={{ background: resolveTagColor({ name, color: link.color }) }}>{name}</span>
172
- ) : link.kind === "scalar" && !isDocFormat(link.format) ? (
173
- <>
174
- {name}: <span className="val">{scalarText(link.value)}</span>
175
- </>
176
- ) : (
177
- name
178
- );
179
- return (
180
- <a
181
- className={"dirview-item" + (it.up ? " dirview-up" : "")}
182
- href={link.path}
183
- title={displayPath(link.path)}
184
- ref={setRef}
185
- tabIndex={tabIndex}
186
- onFocus={onFocus}
187
- onClick={(e) => {
188
- e.preventDefault();
189
- onNavigate(link.path);
190
- }}
191
- >
192
- <span className={"dirview-icon " + g.cls} title={g.title}>{g.glyph}</span>
193
- <span className="dirview-label">{label}</span>
194
- </a>
195
- );
196
- }
197
-
198
- export function ExplorerView({ node, onNavigate }: { node: NodeJson; onNavigate: (path: string) => void }) {
199
- const isTag = node.format === TAG_FORMAT;
200
- // a tag's materials (annotations resolved to their targets) — fetched per tag page, refetched
201
- // when a diff (live.ts) touches a `.yamlover` file (an annotation created/deleted anywhere)
202
- const [tagged, setTagged] = useState<Link[]>([]);
203
- const diffBump = useDiffBump(touchesYamlover);
204
- useEffect(() => {
205
- setTagged([]);
206
- if (!isTag) return;
207
- let cancelled = false;
208
- fetchTagged(node.path)
209
- .then((arr) => {
210
- if (!cancelled) setTagged(arr.map(asLink).filter((l): l is Link => l != null));
211
- })
212
- .catch(() => {}); // the owned members still show
213
- return () => {
214
- cancelled = true;
215
- };
216
- }, [node.path, isTag, diffBump]);
217
-
218
- const ups = uplinkItems(node.relations);
219
- let members = memberItems(node);
220
- if (isTag) {
221
- // the raw back-edge members are the mediating ANNOTATION nodes — the grid shows the
222
- // materials from /api/tagged instead (directly-tagged nodes are in both → dedup by path)
223
- members = members.filter((m) => m.link?.format !== ANNOTATION_FORMAT);
224
- const have = new Set(members.map((m) => m.link?.path).filter(Boolean));
225
- for (const l of tagged) if (!have.has(l.path)) members.push({ key: tagLabel(l.path, l.title), link: l });
226
- }
227
- const items = [...ups, ...members];
228
-
229
- // Roving keyboard focus over the grid: plain arrows walk the icons, Enter opens the
230
- // selected one. The item elements are tracked by index for the geometry-based row moves.
231
- const gridRef = useRef<HTMLDivElement>(null);
232
- const itemEls = useRef<(HTMLElement | null)[]>([]);
233
- itemEls.current.length = items.length; // drop stale refs when the member list shrinks
234
- const [active, setActive] = useState(0);
235
- useEffect(() => {
236
- if (active > items.length - 1) setActive(Math.max(0, items.length - 1));
237
- }, [items.length, active]);
238
-
239
- // On navigating to a new directory, reset the selection and re-arm autofocus (this
240
- // component is reused across nodes, not remounted, so the flag must follow `node.path`).
241
- const wantFocus = useRef(true);
242
- useEffect(() => { setActive(0); wantFocus.current = true; }, [node.path]);
243
- // Focus the first item once the grid has members (a tag's load async), so arrows work right
244
- // after navigating here (the RHS pane handed us focus) — once per node, and never when
245
- // embedded as a chapter chunk (several grids would fight over focus).
246
- useEffect(() => {
247
- if (!wantFocus.current || !items.length) return;
248
- wantFocus.current = false;
249
- if (gridRef.current?.closest(".chunk-body")) return; // embedded — don't steal focus
250
- itemEls.current[0]?.focus({ preventScroll: true });
251
- }, [items.length, node.path]);
252
-
253
- const onKeyDown = (e: React.KeyboardEvent) => {
254
- if (e.ctrlKey || e.altKey || e.metaKey || !items.length) return; // Ctrl/Alt+arrows = TOC nav (App)
255
- if (e.key === "Enter") {
256
- const link = items[active]?.link;
257
- if (link) { e.preventDefault(); onNavigate(link.path); }
258
- return;
259
- }
260
- if (!/^(Arrow(Up|Down|Left|Right)|Home|End)$/.test(e.key)) return;
261
- e.preventDefault(); // don't also scroll the pane
262
- const next = arrowTarget(itemEls.current, active, e.key, items.length);
263
- if (next !== active) { setActive(next); itemEls.current[next]?.focus(); }
264
- };
265
-
266
- // a tag page's description is its BODY (the header bar already names the node)
267
- const desc = (isTag ? tagBody(node.value) : null) ?? node.description;
268
- return (
269
- <div className="explorerview">
270
- {desc && (
271
- <div className="dirhead">
272
- <p className="tagdesc">{desc}</p>
273
- </div>
274
- )}
275
- <div
276
- ref={gridRef}
277
- className={"dirview" + (explorerViewMode() === "large" ? " dirview-lg" : "")}
278
- onKeyDown={onKeyDown}
279
- >
280
- {items.map((it, i) => (
281
- <Item
282
- key={`${it.up ? "^" : ""}${it.link?.path ?? it.key}#${i}`}
283
- it={it}
284
- active={i === active}
285
- setRef={(el) => { itemEls.current[i] = el; }}
286
- onFocus={() => setActive(i)}
287
- onNavigate={onNavigate}
288
- />
289
- ))}
290
- {items.length === 0 && <span className="dirview-empty">empty</span>}
291
- </div>
292
- </div>
293
- );
294
- }
@@ -1,149 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { NodeJson, blobUrl } from "../api";
3
-
4
- const XLINK = "http://www.w3.org/1999/xlink";
5
- // Characters illegal in XML 1.0 (control codes other than tab/newline/cr). FB2
6
- // generators frequently leave these in; strict parsing rejects them, so drop.
7
- // Built from an escaped string to keep literal control bytes out of the source.
8
- const BAD_XML = new RegExp("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]", "g");
9
-
10
- /** Decode the file's bytes using the encoding named in its XML declaration —
11
- * FB2 is frequently windows-1251 (Cyrillic), not UTF-8 — then strip the stray
12
- * control characters that would otherwise fail strict XML parsing. */
13
- function decodeXml(buf: ArrayBuffer): string {
14
- const bytes = new Uint8Array(buf);
15
- const head = new TextDecoder("latin1").decode(bytes.subarray(0, 256));
16
- const enc = (head.match(/encoding=["']([^"']+)["']/i)?.[1] || "utf-8").toLowerCase();
17
- let text: string;
18
- try {
19
- text = new TextDecoder(enc).decode(bytes);
20
- } catch {
21
- text = new TextDecoder("utf-8").decode(bytes);
22
- }
23
- return text.replace(BAD_XML, "");
24
- }
25
-
26
- function esc(s: string): string {
27
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28
- }
29
-
30
- /** Recursively turn an FB2 body element into HTML. `images` maps a `<binary>`
31
- * id to a data URL. Switches on `localName`, so the FB2 namespace is ignored. */
32
- function toHtml(node: Node, images: Map<string, string>): string {
33
- if (node.nodeType === 3) return esc(node.textContent || ""); // text
34
- if (node.nodeType !== 1) return "";
35
- const el = node as Element;
36
- const kids = () => Array.from(el.childNodes).map((n) => toHtml(n, images)).join("");
37
- switch (el.localName) {
38
- case "section": return `<section class="fb2-section">${kids()}</section>`;
39
- case "title": return `<div class="fb2-title">${kids()}</div>`;
40
- case "subtitle": return `<p class="fb2-subtitle">${kids()}</p>`;
41
- case "p": return `<p>${kids()}</p>`;
42
- case "empty-line": return "<br/>";
43
- case "emphasis": return `<em>${kids()}</em>`;
44
- case "strong": return `<strong>${kids()}</strong>`;
45
- case "strikethrough": return `<s>${kids()}</s>`;
46
- case "sub": return `<sub>${kids()}</sub>`;
47
- case "sup": return `<sup>${kids()}</sup>`;
48
- case "code": return `<code>${kids()}</code>`;
49
- case "epigraph":
50
- case "cite": return `<blockquote class="fb2-cite">${kids()}</blockquote>`;
51
- case "text-author": return `<p class="fb2-text-author">${kids()}</p>`;
52
- case "poem": return `<div class="fb2-poem">${kids()}</div>`;
53
- case "stanza": return `<div class="fb2-stanza">${kids()}</div>`;
54
- case "v": return `<div class="fb2-v">${kids()}</div>`;
55
- case "a": return `<span class="fb2-a">${kids()}</span>`; // FB2 links are intra-doc
56
- case "image": {
57
- const href =
58
- el.getAttributeNS(XLINK, "href") || el.getAttribute("l:href") || el.getAttribute("href") || "";
59
- const src = images.get(href.replace(/^#/, ""));
60
- return src ? `<img class="fb2-img" src="${src}" alt=""/>` : "";
61
- }
62
- case "title-info":
63
- case "description":
64
- case "binary":
65
- return ""; // metadata / payload, not body prose
66
- default: return kids();
67
- }
68
- }
69
-
70
- interface Book {
71
- title?: string;
72
- author?: string;
73
- cover?: string;
74
- html: string;
75
- }
76
-
77
- /**
78
- * Renderer for an `application/x-fictionbook+xml` (`.fb2`) ebook. The file is
79
- * served as bytes; here we decode it (honoring its declared encoding), parse the
80
- * FictionBook XML, and present the book — cover, title, author, then the body
81
- * with its sections/paragraphs/poems and embedded images inlined.
82
- */
83
- export function Fb2View({ node }: { node: NodeJson }) {
84
- const [book, setBook] = useState<Book | null>(null);
85
- const [error, setError] = useState<string | null>(null);
86
-
87
- useEffect(() => {
88
- let cancelled = false;
89
- setBook(null);
90
- setError(null);
91
- fetch(blobUrl(node.path))
92
- .then((r) => r.arrayBuffer())
93
- .then((buf) => {
94
- if (cancelled) return;
95
- const doc = new DOMParser().parseFromString(decodeXml(buf), "application/xml");
96
- const perr = doc.getElementsByTagName("parsererror")[0];
97
- if (perr) throw new Error(perr.textContent?.trim().split("\n")[0] || "invalid FB2 XML");
98
-
99
- const images = new Map<string, string>();
100
- for (const b of Array.from(doc.getElementsByTagNameNS("*", "binary"))) {
101
- const id = b.getAttribute("id");
102
- if (id) {
103
- const ct = b.getAttribute("content-type") || "image/jpeg";
104
- images.set(id, `data:${ct};base64,${(b.textContent || "").replace(/\s+/g, "")}`);
105
- }
106
- }
107
-
108
- const ti = doc.getElementsByTagNameNS("*", "title-info")[0] as Element | undefined;
109
- const at = (e: Element | undefined, name: string) =>
110
- e?.getElementsByTagNameNS("*", name)[0]?.textContent?.trim() || "";
111
- const title = at(ti, "book-title") || undefined;
112
- const author =
113
- ti &&
114
- Array.from(ti.getElementsByTagNameNS("*", "author"))
115
- .map((a) =>
116
- [at(a, "first-name"), at(a, "middle-name"), at(a, "last-name")]
117
- .filter(Boolean)
118
- .join(" ") || at(a, "nickname"),
119
- )
120
- .filter(Boolean)
121
- .join(", ");
122
- const coverImg = ti?.getElementsByTagNameNS("*", "coverpage")[0]?.getElementsByTagNameNS("*", "image")[0];
123
- const coverId = (
124
- coverImg?.getAttributeNS(XLINK, "href") || coverImg?.getAttribute("l:href") || ""
125
- ).replace(/^#/, "");
126
-
127
- const bodies = Array.from(doc.getElementsByTagNameNS("*", "body"));
128
- const main = bodies.find((b) => b.getAttribute("name") !== "notes") || bodies[0];
129
- const html = main ? Array.from(main.childNodes).map((n) => toHtml(n, images)).join("") : "";
130
-
131
- setBook({ title, author: author || undefined, cover: coverId ? images.get(coverId) : undefined, html });
132
- })
133
- .catch((e) => !cancelled && setError(String((e as Error).message || e)));
134
- return () => {
135
- cancelled = true;
136
- };
137
- }, [node.path]);
138
-
139
- if (error) return <div className="error">fb2: {error}</div>;
140
- if (!book) return <div className="loading">loading FB2…</div>;
141
- return (
142
- <div className="text fb2">
143
- {book.cover && <img className="fb2-cover" src={book.cover} alt="" />}
144
- {book.title && <h1 className="chapter-title">{book.title}</h1>}
145
- {book.author && <p className="chapter-subtitle">{book.author}</p>}
146
- <div className="markup fb2-body" dangerouslySetInnerHTML={{ __html: book.html }} />
147
- </div>
148
- );
149
- }
@@ -1,69 +0,0 @@
1
- import { useEffect } from "react";
2
-
3
- /**
4
- * Shared heading machinery for the rendered-markup formats (Markdown and AsciiDoc).
5
- *
6
- * A `.md`/`.adoc` page is a single HTML blob dumped via `dangerouslySetInnerHTML`,
7
- * so on its own a heading is not addressable. {@link anchorizeHeadings} gives every
8
- * heading a stable `id` and a small `§` link to it, mirroring the way GitHub renders
9
- * the same documents — so a deep link like `<page>#<slug>` lands on, and scrolls to,
10
- * one section. This is the prose-document counterpart of the chapter renderer's `§N`
11
- * chunk anchors (see `chapter.tsx`): there the locator is the chunk's path; here it
12
- * is the heading's slug.
13
- */
14
-
15
- /** GitHub-style slug of a heading's text: lowercase, punctuation dropped, runs of
16
- * whitespace collapsed to single hyphens. Unicode letters/numbers are kept. */
17
- function slugify(text: string): string {
18
- return text
19
- .trim()
20
- .toLowerCase()
21
- .replace(/[^\p{L}\p{N}\s-]/gu, "")
22
- .replace(/\s+/g, "-");
23
- }
24
-
25
- /** `base`, suffixed `-2`, `-3`, … until it is not already in `used` (which it is
26
- * then added to). Empty `base` (a heading with no sluggable text) yields "". */
27
- function uniqueId(base: string, used: Set<string>): string {
28
- if (!base) return "";
29
- let id = base;
30
- for (let n = 2; used.has(id); n++) id = `${base}-${n}`;
31
- used.add(id);
32
- return id;
33
- }
34
-
35
- /** Give every heading in a block of rendered markup an `id` and a leading `§`
36
- * anchor link to it (placed first so it sits in the left gutter, like a chapter
37
- * chunk's `§N` index). An id already present (Asciidoctor stamps section ids) is
38
- * kept — so its anchor matches the document's own cross-references — otherwise a
39
- * de-duplicated slug of the heading text is assigned. Returns the rewritten HTML.
40
- * Runs in the browser/jsdom; with no `DOMParser` (or no headings) it is a no-op. */
41
- export function anchorizeHeadings(html: string): string {
42
- if (typeof DOMParser === "undefined" || !html.includes("<h")) return html;
43
- const doc = new DOMParser().parseFromString(html, "text/html");
44
- const used = new Set<string>();
45
- for (const h of doc.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
46
- // slug from the text before inserting the anchor, so the `§` is not part of it
47
- const id = uniqueId(h.id || slugify(h.textContent ?? ""), used);
48
- if (!id) continue;
49
- h.id = id;
50
- const a = doc.createElement("a");
51
- a.className = "header-anchor";
52
- a.href = `#${id}`;
53
- a.setAttribute("aria-label", "Link to this section");
54
- a.textContent = "§";
55
- h.insertBefore(a, h.firstChild);
56
- }
57
- return doc.body.innerHTML;
58
- }
59
-
60
- /** Scroll to the element named by the URL hash once `dep` (the rendered node)
61
- * settles. A deep link `<page>#<slug>` lands on the page, but the value is fetched
62
- * async — after the browser's own one-shot scroll — so re-scroll when it arrives.
63
- * The same pattern the chapter renderer uses for `#/chunks[n]`. */
64
- export function useHashScroll(dep: unknown): void {
65
- useEffect(() => {
66
- const id = decodeURIComponent(window.location.hash.slice(1));
67
- if (id) document.getElementById(id)?.scrollIntoView?.(); // optional-call: absent in jsdom
68
- }, [dep]);
69
- }
@@ -1,23 +0,0 @@
1
- import heic2any from "heic2any";
2
- import { NodeJson } from "../api";
3
- import { DecodedImageView } from "./decoded";
4
-
5
- /**
6
- * Renders an HEIC/HEIF image (`image/heic`, `.heic`/`.heif`) — the format iPhones
7
- * shoot. It's HEVC-encoded and patent-encumbered, with no browser support, so we
8
- * decode it with `heic2any` (libheif compiled to wasm), which converts the bytes
9
- * to a PNG blob. An HEIC may hold an image *sequence* (burst/Live Photo); when it
10
- * does, `heic2any` returns several blobs and we show each.
11
- */
12
- export function HeicView({ node }: { node: NodeJson }) {
13
- return (
14
- <DecodedImageView
15
- node={node}
16
- label="heic"
17
- decode={async (buf) => {
18
- const out = await heic2any({ blob: new Blob([buf]), toType: "image/png" });
19
- return Array.isArray(out) ? out : [out];
20
- }}
21
- />
22
- );
23
- }