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,170 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import L from "leaflet";
3
- import "leaflet/dist/leaflet.css";
4
- import { NodeJson, blobUrl } from "../api";
5
- import { Chunk } from "./registry";
6
- import { Annotation } from "../api";
7
- import { DEFAULT_COLOR, colorOf, editable, useAnnotationMenu, useMaterialAnnotations } from "./annotate";
8
- import { wireGestures } from "./panzoom";
9
-
10
- /** A rectangular annotation region in the image's own pixel space (origin top-left). `ann` is the
11
- * source annotation when it is a real saved one (→ clickable to edit); absent for the live preview. */
12
- export interface ImageRegion { x: number; y: number; w: number; h: number; title?: string; color?: string; ann?: Annotation }
13
-
14
- const num = (v: unknown): number => Number(v) || 0;
15
-
16
- /** A PNG data-URL crop of the natural-pixel region (x,y,w,h) of `img`, for an image-like
17
- * fragment's embedded preview; undefined if the region is empty or the canvas reads back tainted
18
- * (cross-origin — image blobs are same-origin, so this is just a guard). */
19
- function cropPng(img: HTMLImageElement | null, x: number, y: number, w: number, h: number): string | undefined {
20
- if (!img || w <= 0 || h <= 0) return undefined;
21
- const cv = document.createElement("canvas");
22
- cv.width = w; cv.height = h;
23
- const ctx = cv.getContext("2d");
24
- if (!ctx) return undefined;
25
- ctx.drawImage(img, x, y, w, h, 0, 0, w, h);
26
- try { return cv.toDataURL("image/png"); } catch { return undefined; }
27
- }
28
-
29
- /** The `rect`-type annotations, as pixel regions to overlay on the image. */
30
- function imageRegions(anns: Annotation[]): ImageRegion[] {
31
- return anns
32
- .filter((a) => a.selector?.type === "rect")
33
- .map((a) => ({ x: num(a.selector!.x), y: num(a.selector!.y), w: num(a.selector!.w), h: num(a.selector!.h), title: a.description, color: colorOf(a), ann: editable(a) ? a : undefined }));
34
- }
35
-
36
- /**
37
- * Pan/zoom image viewer — the same widget the KML map uses, over a flat picture. The image is an
38
- * `imageOverlay` on a `CRS.Simple` map sized to its natural pixels; the view fits it initially.
39
- * Gestures follow the unified model (see {@link wireGestures} / the UI guide): plain drag selects a
40
- * region (when `onSelectRegion` is set), ctrl/alt-drag pans, plain wheel pans vertically, ctrl/alt-
41
- * wheel zooms. The map is built once (per `src`); regions redraw in place without resetting the view.
42
- */
43
- export function PanZoomImage({
44
- src, className, regions, onSelectRegion, onRegionClick, selectColor,
45
- }: {
46
- src: string;
47
- className: string;
48
- regions?: ImageRegion[];
49
- onSelectRegion?: (selector: Record<string, unknown>, screen: { x: number; y: number }, imageBase64?: string) => void;
50
- onRegionClick?: (ann: Annotation, screen: { x: number; y: number }) => void;
51
- selectColor?: () => string;
52
- }) {
53
- const ref = useRef<HTMLDivElement>(null);
54
- const mapRef = useRef<L.Map | null>(null);
55
- const layerRef = useRef<L.LayerGroup | null>(null);
56
- const imgElRef = useRef<HTMLImageElement | null>(null);
57
- const sizeRef = useRef({ w: 1, h: 1 });
58
- const onSelectRef = useRef(onSelectRegion);
59
- const onRegionClickRef = useRef(onRegionClick);
60
- const colorRef = useRef(selectColor);
61
- const [error, setError] = useState<string | null>(null);
62
- const [ready, setReady] = useState(0); // bumps once the map (and its overlay layer) exist
63
- const regionsKey = JSON.stringify(regions ?? []);
64
- const selectable = !!onSelectRegion; // fixed per instance: full view annotates, chunk doesn't
65
-
66
- useEffect(() => { onSelectRef.current = onSelectRegion; onRegionClickRef.current = onRegionClick; colorRef.current = selectColor; });
67
-
68
- // Build the map once per src; gestures + the overlay layer live with it.
69
- useEffect(() => {
70
- let cancelled = false;
71
- let dispose: (() => void) | null = null;
72
- setError(null);
73
- const img = new Image();
74
- img.onload = () => {
75
- if (cancelled || !ref.current) return;
76
- const w = img.naturalWidth || 1;
77
- const h = img.naturalHeight || 1;
78
- sizeRef.current = { w, h };
79
- imgElRef.current = img; // kept for cropping a selected region (same-origin → un-tainted canvas)
80
- // CRS.Simple: coordinates are raw pixels (y, x); negative minZoom allows zooming far out.
81
- const map = L.map(ref.current, { crs: L.CRS.Simple, minZoom: -8, attributionControl: false, zoomSnap: 0 });
82
- const bounds: L.LatLngBoundsExpression = [[0, 0], [h, w]];
83
- L.imageOverlay(src, bounds).addTo(map);
84
- map.fitBounds(bounds); // initial view frames the whole image
85
- layerRef.current = L.layerGroup().addTo(map);
86
- mapRef.current = map;
87
- dispose = wireGestures(map, {
88
- color: () => colorRef.current?.() ?? DEFAULT_COLOR,
89
- onSelect: selectable
90
- ? (b, screen) => {
91
- // image pixels have y from the top; CRS.Simple lat is from the bottom → flip.
92
- const { h: ih } = sizeRef.current;
93
- const west = b.getWest(), east = b.getEast(), south = b.getSouth(), north = b.getNorth();
94
- const x = Math.round(west), y = Math.round(ih - north), w = Math.round(east - west), hh = Math.round(north - south);
95
- onSelectRef.current?.({ type: "rect", x, y, w, h: hh }, screen, cropPng(imgElRef.current, x, y, w, hh));
96
- }
97
- : undefined,
98
- });
99
- setReady((r) => r + 1);
100
- };
101
- img.onerror = () => { if (!cancelled) setError("could not load image"); };
102
- img.src = src;
103
- return () => {
104
- cancelled = true;
105
- dispose?.();
106
- mapRef.current?.remove();
107
- mapRef.current = null;
108
- layerRef.current = null;
109
- };
110
- // eslint-disable-next-line react-hooks/exhaustive-deps
111
- }, [src]);
112
-
113
- // Draw the region rectangles into the overlay layer — in place, so creating one keeps the view.
114
- useEffect(() => {
115
- const lg = layerRef.current;
116
- if (!lg) return;
117
- lg.clearLayers();
118
- const { h } = sizeRef.current;
119
- for (const r of regions ?? []) {
120
- const c = r.color || DEFAULT_COLOR;
121
- // image y is from the top; CRS.Simple lat from the bottom, so flip y
122
- const rect = L.rectangle([[h - r.y, r.x], [h - (r.y + r.h), r.x + r.w]], {
123
- className: "yo-region", color: c, weight: 3, fillColor: c, fillOpacity: 0.25,
124
- });
125
- if (r.title) rect.bindTooltip(r.title);
126
- if (r.ann) {
127
- const ann = r.ann;
128
- rect.on("click", (ev) => { L.DomEvent.stop(ev); onRegionClickRef.current?.(ann, { x: ev.originalEvent.clientX, y: ev.originalEvent.clientY }); });
129
- }
130
- rect.addTo(lg);
131
- }
132
- // eslint-disable-next-line react-hooks/exhaustive-deps
133
- }, [regionsKey, ready]);
134
-
135
- return (
136
- <>
137
- {error && <div className="error">image: {error}</div>}
138
- <div ref={ref} className={className + (selectable ? " yo-selectable" : "")} />
139
- </>
140
- );
141
- }
142
-
143
- export function ImageView({ node }: { node: NodeJson }) {
144
- const material = useMaterialAnnotations(node.path);
145
- const { openCreate, openEdit, palette, preview, color } = useAnnotationMenu(material);
146
- // include the live PREVIEW selector so the rectangle stays drawn while the menu is open
147
- const shown = preview
148
- ? [...material.annotations, { path: "(preview)", selector: preview.selector, tag: preview.tag } as Annotation]
149
- : material.annotations;
150
- return (
151
- <div className="text">
152
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
153
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
154
- <PanZoomImage
155
- src={blobUrl(node.path)}
156
- regions={imageRegions(shown)}
157
- onSelectRegion={(sel, screen, crop) => openCreate(sel, screen, undefined, crop)}
158
- onRegionClick={openEdit}
159
- selectColor={() => color}
160
- className="filemap fileimagemap"
161
- />
162
- {palette}
163
- </div>
164
- );
165
- }
166
-
167
- /** An image embedded inline in a chapter — pan/zoom only (no annotation target → plain drag pans). */
168
- export function ImageChunk({ chunk }: { chunk: Chunk }) {
169
- return <PanZoomImage src={blobUrl(chunk.path)} className="filemap chunk-map fileimagemap" />;
170
- }
@@ -1,46 +0,0 @@
1
- import { kml as kmlDomToGeoJSON } from "@tmcw/togeojson";
2
- import { unzipSync, strFromU8 } from "fflate";
3
-
4
- /**
5
- * The pure (Leaflet-free) half of the map renderer: turn the bytes of a `.kml` or
6
- * `.kmz` file into GeoJSON. Kept apart from `map.tsx` so it can be unit-tested
7
- * without importing Leaflet (a browser-only library with CSS side effects).
8
- *
9
- * - **KML** is XML — decoded as text, parsed to a DOM, then converted with
10
- * `@tmcw/togeojson`.
11
- * - **KMZ** is a ZIP — unzipped (with the already-bundled `fflate`); its first
12
- * `.kml` entry is the document, then the same KML path.
13
- */
14
-
15
- /** A GeoJSON FeatureCollection (the shape togeojson returns). Loosely typed — the
16
- * map renderer only hands it to Leaflet's `L.geoJSON`. */
17
- export interface GeoJSON {
18
- type: "FeatureCollection";
19
- features: unknown[];
20
- }
21
-
22
- /** Whether `bytes` begin with the ZIP local-file signature `PK\x03\x04` — i.e. a
23
- * KMZ rather than a bare KML. Robust regardless of the file's extension. */
24
- export function isZip(bytes: Uint8Array): boolean {
25
- return bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
26
- }
27
-
28
- /** Parse a KML XML string into GeoJSON via a DOM (available in the browser and in
29
- * jsdom tests). */
30
- export function kmlStringToGeoJSON(xml: string): GeoJSON {
31
- const dom = new DOMParser().parseFromString(xml, "application/xml");
32
- if (dom.getElementsByTagName("parsererror").length) throw new Error("malformed KML XML");
33
- return kmlDomToGeoJSON(dom) as unknown as GeoJSON;
34
- }
35
-
36
- /** Parse `.kml`/`.kmz` bytes into GeoJSON. KMZ is detected by its ZIP signature
37
- * (not just the extension); the first `.kml` entry inside is the document. */
38
- export function bytesToGeoJSON(bytes: Uint8Array): GeoJSON {
39
- if (isZip(bytes)) {
40
- const files = unzipSync(bytes);
41
- const name = Object.keys(files).find((n) => n.toLowerCase().endsWith(".kml"));
42
- if (!name) throw new Error("no .kml document inside the KMZ archive");
43
- return kmlStringToGeoJSON(strFromU8(files[name]));
44
- }
45
- return kmlStringToGeoJSON(new TextDecoder().decode(bytes));
46
- }
@@ -1,37 +0,0 @@
1
- import katex from "katex";
2
- import "katex/dist/katex.min.css";
3
- import { NodeJson } from "../api";
4
- import { scalarValue } from "../render";
5
- import { Chunk } from "./registry";
6
-
7
- /**
8
- * The renderer for a `string`/`text/x-latex` node: LaTeX math typeset with KaTeX.
9
- * Like the markdown/asciidoc renderers it works straight from the node's string
10
- * value, so it serves both a whole formula node (`render`) and a single inline
11
- * chunk (`renderChunk`).
12
- *
13
- * `renderMath` is the one place KaTeX is invoked, exported so **marklower** can
14
- * reuse it for its inline `$$…$$` spans — math is rendered the same way whether it
15
- * is a standalone `text/x-latex` string or embedded in marklower prose.
16
- */
17
- export function renderMath(tex: unknown, displayMode: boolean): string {
18
- // `throwOnError: false` makes KaTeX emit the offending source in red rather than
19
- // throwing, so a typo in one formula never blanks the whole page.
20
- return katex.renderToString(String(tex ?? ""), { displayMode, throwOnError: false });
21
- }
22
-
23
- export function LatexView({ node }: { node: NodeJson }) {
24
- return (
25
- <div className="text">
26
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
27
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
28
- <div className="markup" dangerouslySetInnerHTML={{ __html: renderMath(scalarValue(node.value), true) }} />
29
- </div>
30
- );
31
- }
32
-
33
- /** A LaTeX chunk embedded inline in a chapter: the formula typeset as a display
34
- * block (the chapter supplies the surrounding number + anchor). */
35
- export function LatexChunk({ chunk }: { chunk: Chunk }) {
36
- return <div className="markup" dangerouslySetInnerHTML={{ __html: renderMath(chunk.value, true) }} />;
37
- }
@@ -1,205 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import L from "leaflet";
3
- import "leaflet/dist/leaflet.css";
4
- import { NodeJson, blobUrl } from "../api";
5
- import { Chunk } from "./registry";
6
- import { bytesToGeoJSON, GeoJSON } from "./kml";
7
- import { Annotation } from "../api";
8
- import { DEFAULT_COLOR, colorOf, editable, useAnnotationMenu, useMaterialAnnotations } from "./annotate";
9
- import { wireGestures } from "./panzoom";
10
-
11
- /**
12
- * Renderer for geographic overlays — `.kml` and `.kmz` (zipped KML). The file is
13
- * served as bytes; we convert it to GeoJSON (see `kml.ts`) and draw it on a
14
- * Leaflet slippy map over OpenStreetMap tiles, fitting the view to the data.
15
- * Points become circle markers, lines/polygons keep their KML colours, and a
16
- * feature's name/description show in a popup.
17
- *
18
- * Gestures follow the unified model (see {@link wireGestures} / the UI guide): plain drag selects a
19
- * geographic region to annotate, ctrl/alt-drag pans, plain wheel pans vertically, ctrl/alt-wheel
20
- * zooms.
21
- *
22
- * **Network note:** the vector overlay and KML/KMZ parsing are fully local, but the
23
- * *map tiles* are fetched from a tile server — by default OpenStreetMap. Point
24
- * `VITE_MAP_TILE_URL` (and optionally `VITE_MAP_TILE_ATTRIBUTION`) at a self-hosted
25
- * tile server to keep map traffic off the public one. Leaflet is heavy and
26
- * browser-only, so the registry loads this module lazily.
27
- */
28
- const TILE_URL =
29
- ((import.meta as any).env?.VITE_MAP_TILE_URL as string | undefined) ??
30
- "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
31
- const TILE_ATTRIBUTION =
32
- ((import.meta as any).env?.VITE_MAP_TILE_ATTRIBUTION as string | undefined) ??
33
- '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
34
-
35
- /** A rectangular annotation region on the map, in geographic edges (degrees). `ann` is the source
36
- * annotation when it is a real saved one (→ clickable to edit); absent for the live preview. */
37
- interface MapRegion { n: number; s: number; e: number; w: number; title?: string; color?: string; ann?: Annotation }
38
- const num = (v: unknown): number => Number(v) || 0;
39
-
40
- /** The `map`-type annotations, as geographic rectangles to overlay. */
41
- function mapRegions(anns: Annotation[]): MapRegion[] {
42
- return anns
43
- .filter((a) => a.selector?.type === "map")
44
- .map((a) => ({ n: num(a.selector!.n), s: num(a.selector!.s), e: num(a.selector!.e), w: num(a.selector!.w), title: a.description, color: colorOf(a), ann: editable(a) ? a : undefined }));
45
- }
46
-
47
- function escapeHtml(s: string): string {
48
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
49
- }
50
-
51
- /** A feature's description as popup HTML. togeojson returns a plain-text KML
52
- * description as a string (escaped here) and an HTML/CDATA one as
53
- * `{ "@type": "html", value }` — authored markup, kept as-is (this is a local
54
- * viewer of the user's own files). */
55
- function descriptionHtml(description: unknown): string {
56
- if (!description) return "";
57
- if (typeof description === "string") return escapeHtml(description);
58
- if (typeof description === "object" && (description as any)["@type"] === "html") {
59
- return String((description as any).value ?? "");
60
- }
61
- return "";
62
- }
63
-
64
- /** Draw `geo` onto a fresh Leaflet map in `el`, fit to the data, and return the map
65
- * (so the caller can dispose it). */
66
- function drawMap(el: HTMLElement, geo: GeoJSON): L.Map {
67
- const map = L.map(el);
68
- L.tileLayer(TILE_URL, { maxZoom: 19, attribution: TILE_ATTRIBUTION }).addTo(map);
69
- const layer = L.geoJSON(geo as any, {
70
- // honour KML styling that togeojson surfaces as simplestyle properties
71
- style: (f) => {
72
- const p = (f?.properties ?? {}) as Record<string, unknown>;
73
- return {
74
- color: (p["stroke"] as string) || "#3388ff",
75
- weight: (p["stroke-width"] as number) || 2,
76
- opacity: (p["stroke-opacity"] as number) ?? 1,
77
- fillColor: (p["fill"] as string) || "#3388ff",
78
- fillOpacity: (p["fill-opacity"] as number) ?? 0.2,
79
- };
80
- },
81
- pointToLayer: (_f, latlng) => L.circleMarker(latlng, { radius: 5, color: "#e23", weight: 2, fillOpacity: 0.8 }),
82
- onEachFeature: (f, lyr) => {
83
- const p = (f.properties ?? {}) as { name?: string; description?: unknown };
84
- const name = p.name ? `<strong>${escapeHtml(p.name)}</strong>` : "";
85
- const body = descriptionHtml(p.description);
86
- const desc = body ? `<div class="map-popup-desc">${body}</div>` : "";
87
- if (name || desc) lyr.bindPopup(`${name}${desc}`);
88
- },
89
- }).addTo(map);
90
-
91
- const bounds = layer.getBounds();
92
- if (bounds.isValid()) map.fitBounds(bounds, { padding: [24, 24], maxZoom: 16 });
93
- else map.setView([0, 0], 2); // nothing geocoded — show the whole world
94
- return map;
95
- }
96
-
97
- /** Fetch the file, convert to GeoJSON, and render a Leaflet map into `className`.
98
- * Shared by the full page and the inline chunk (which only differ in height/CSS + annotation). */
99
- function MapBody({
100
- path, className, regions, onSelectRegion, onRegionClick, selectColor,
101
- }: {
102
- path: string;
103
- className: string;
104
- regions?: MapRegion[];
105
- onSelectRegion?: (selector: Record<string, unknown>, screen: { x: number; y: number }) => void;
106
- onRegionClick?: (ann: Annotation, screen: { x: number; y: number }) => void;
107
- selectColor?: () => string;
108
- }) {
109
- const ref = useRef<HTMLDivElement>(null);
110
- const mapRef = useRef<L.Map | null>(null);
111
- const layerRef = useRef<L.LayerGroup | null>(null);
112
- const onSelectRef = useRef(onSelectRegion);
113
- const onRegionClickRef = useRef(onRegionClick);
114
- const colorRef = useRef(selectColor);
115
- const [error, setError] = useState<string | null>(null);
116
- const [loading, setLoading] = useState(true);
117
- const [ready, setReady] = useState(0);
118
- const regionsKey = JSON.stringify(regions ?? []);
119
- const selectable = !!onSelectRegion;
120
-
121
- useEffect(() => { onSelectRef.current = onSelectRegion; onRegionClickRef.current = onRegionClick; colorRef.current = selectColor; });
122
-
123
- useEffect(() => {
124
- let cancelled = false;
125
- let dispose: (() => void) | null = null;
126
- setError(null);
127
- setLoading(true);
128
- fetch(blobUrl(path))
129
- .then((r) => r.arrayBuffer())
130
- .then((buf) => {
131
- if (cancelled || !ref.current) return;
132
- const map = drawMap(ref.current, bytesToGeoJSON(new Uint8Array(buf)));
133
- layerRef.current = L.layerGroup().addTo(map);
134
- mapRef.current = map;
135
- dispose = wireGestures(map, {
136
- color: () => colorRef.current?.() ?? DEFAULT_COLOR,
137
- onSelect: selectable
138
- ? (b, screen) => onSelectRef.current?.({ type: "map", n: b.getNorth(), s: b.getSouth(), e: b.getEast(), w: b.getWest() }, screen)
139
- : undefined,
140
- });
141
- setReady((x) => x + 1);
142
- setLoading(false);
143
- })
144
- .catch((e) => {
145
- if (!cancelled) {
146
- setError(String((e as Error).message || e));
147
- setLoading(false);
148
- }
149
- });
150
- return () => {
151
- cancelled = true;
152
- dispose?.();
153
- mapRef.current?.remove();
154
- mapRef.current = null;
155
- layerRef.current = null;
156
- };
157
- // eslint-disable-next-line react-hooks/exhaustive-deps
158
- }, [path]);
159
-
160
- // Draw region rectangles into the overlay layer — in place, so creating one keeps the view.
161
- useEffect(() => {
162
- const lg = layerRef.current;
163
- if (!lg) return;
164
- lg.clearLayers();
165
- for (const r of regions ?? []) {
166
- const c = r.color || DEFAULT_COLOR;
167
- const rect = L.rectangle([[r.s, r.w], [r.n, r.e]], { className: "yo-region", color: c, weight: 3, fillColor: c, fillOpacity: 0.25 });
168
- if (r.title) rect.bindTooltip(r.title);
169
- if (r.ann) {
170
- const ann = r.ann;
171
- rect.on("click", (ev) => { L.DomEvent.stop(ev); onRegionClickRef.current?.(ann, { x: ev.originalEvent.clientX, y: ev.originalEvent.clientY }); });
172
- }
173
- rect.addTo(lg);
174
- }
175
- // eslint-disable-next-line react-hooks/exhaustive-deps
176
- }, [regionsKey, ready]);
177
-
178
- return (
179
- <>
180
- {error && <div className="error">map: {error}</div>}
181
- <div ref={ref} className={className + (selectable ? " yo-selectable" : "")} />
182
- {loading && !error && <div className="loading">loading map…</div>}
183
- </>
184
- );
185
- }
186
-
187
- export function MapView({ node }: { node: NodeJson }) {
188
- const material = useMaterialAnnotations(node.path);
189
- const { openCreate, openEdit, palette, preview, color } = useAnnotationMenu(material);
190
- const shown = preview
191
- ? [...material.annotations, { path: "(preview)", selector: preview.selector, tag: preview.tag } as Annotation]
192
- : material.annotations;
193
- return (
194
- <div className="text">
195
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
196
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
197
- <MapBody path={node.path} regions={mapRegions(shown)} onSelectRegion={openCreate} onRegionClick={openEdit} selectColor={() => color} className="filemap" />
198
- {palette}
199
- </div>
200
- );
201
- }
202
-
203
- export function MapChunk({ chunk }: { chunk: Chunk }) {
204
- return <MapBody path={chunk.path} className="filemap chunk-map" />;
205
- }
@@ -1,120 +0,0 @@
1
- import { ReactNode } from "react";
2
- import { NodeJson } from "../api";
3
- import { scalarValue } from "../render";
4
- import { Chunk } from "./registry";
5
- import { renderMath } from "./latex";
6
- import { NavLink } from "../links";
7
-
8
- /**
9
- * The renderer for a bare `string` with no explicit format — `(string, null)` —
10
- * our own lightweight markup language, **marklower**: deliberately a notch below
11
- * Markdown ("downshifted" from it). It is the *default* format for prose strings
12
- * inside a chapter: a chunk that declares no format routes here rather than to the
13
- * plain-paragraph fallback.
14
- *
15
- * The language is meant to cover inline concerns only — font styling, hyperlinks,
16
- * images, math, and (perhaps) embedded code — but deliberately **no** chapter
17
- * structure: no headings/subheadings, since chapters are modeled by the chapter
18
- * renderer's `children`, not by markup.
19
- *
20
- * The syntax so far is all inline:
21
- *
22
- * - **atomic tokens**, whose contents are *not* re-interpreted as markup:
23
- * `$$…$$` math (typeset with KaTeX via the shared {@link renderMath}, the same
24
- * path the `text/x-latex` renderer uses) and `` `code` `` spans;
25
- * - **links**: `[text](target)`, where `target` is a path in the app's JSON
26
- * instance space (the same space the whole app navigates). Resolved and made
27
- * clickable through the shared {@link NavLink} — the one link concept that refs
28
- * and rels are expected to adopt later;
29
- * - **text styling** on the plain runs between those: `**bold**`/`__bold__`,
30
- * `*italic*`/`_italic_`, and `~~strikethrough~~`.
31
- *
32
- * Anything else is passed through verbatim. `parse` is the single seam every entry
33
- * point goes through, so there is one place to teach the grammar.
34
- */
35
-
36
- /** Escape a plain-text run so it can be dropped into HTML (`parse` emits HTML now
37
- * that some tokens — math, code, emphasis — render to markup). */
38
- function escapeHtml(s: string): string {
39
- return s
40
- .replace(/&/g, "&amp;")
41
- .replace(/</g, "&lt;")
42
- .replace(/>/g, "&gt;");
43
- }
44
-
45
- /** Style a plain-text run (one of the stretches between the atomic tokens): escape
46
- * it, then apply emphasis. Bold (`**`/`__`) runs before italic (`*`/`_`) so a
47
- * double marker isn't mistaken for two single ones; non-greedy so neighbours don't
48
- * merge. The markers (`* _ ~`) survive `escapeHtml`, so styling the escaped text
49
- * is safe. */
50
- function styleText(text: string): string {
51
- return escapeHtml(text)
52
- .replace(/~~(.+?)~~/g, "<del>$1</del>")
53
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
54
- .replace(/__(.+?)__/g, "<strong>$1</strong>")
55
- .replace(/\*(.+?)\*/g, "<em>$1</em>")
56
- .replace(/_(.+?)_/g, "<em>$1</em>");
57
- }
58
-
59
- /** The non-text tokens, in one alternation matched in source order: `$$…$$` math
60
- * (group 1; `[\s\S]` so a formula may span lines), a `` `code` `` span (group 2),
61
- * or a `[label](target)` link (groups 3 = label, 4 = target). Non-greedy so
62
- * adjacent tokens don't merge. The label may contain a balanced `[…]` (so a path
63
- * used as its own label — `[/children[0]](/children[0])` — works), but a stray `]`
64
- * is not a label, so a non-link `[a]` in prose is left alone. */
65
- const TOKEN = /\$\$([\s\S]+?)\$\$|`([^`]+?)`|\[((?:[^\[\]]|\[[^\]]*\])*?)\]\(([^)]+?)\)/g;
66
-
67
- /** Parse marklower into React nodes. Most syntax renders to an HTML string (math,
68
- * code, emphasis), accumulated and flushed into `<span>`s; a link must be a real
69
- * element so it navigates in-app (an HTML `<a href>` would reload), so the result
70
- * is a node list, not one HTML string. `documentPath` anchors a link's `/…`
71
- * (document-relative) target. */
72
- function parse(value: unknown, onNavigate: (path: string) => void, documentPath?: string): ReactNode[] {
73
- const src = String(value ?? "");
74
- const nodes: ReactNode[] = [];
75
- let html = ""; // buffer of HTML-rendered runs between links
76
- let key = 0;
77
- const flush = () => {
78
- if (!html) return;
79
- nodes.push(<span key={key++} dangerouslySetInnerHTML={{ __html: html }} />);
80
- html = "";
81
- };
82
- let last = 0;
83
- for (const m of src.matchAll(TOKEN)) {
84
- html += styleText(src.slice(last, m.index)); // plain run before this token
85
- if (m[1] !== undefined) {
86
- html += renderMath(m[1], false); // $$ inline math $$
87
- } else if (m[2] !== undefined) {
88
- html += `<code>${escapeHtml(m[2])}</code>`; // `code` — contents literal
89
- } else {
90
- // [label](target) — a real anchor so it navigates in JSON instance space; the
91
- // label keeps its own inline styling.
92
- flush();
93
- nodes.push(
94
- <NavLink key={key++} target={m[4]} documentPath={documentPath} onNavigate={onNavigate}>
95
- <span dangerouslySetInnerHTML={{ __html: styleText(m[3]) }} />
96
- </NavLink>,
97
- );
98
- }
99
- last = m.index + m[0].length;
100
- }
101
- html += styleText(src.slice(last));
102
- flush();
103
- return nodes;
104
- }
105
-
106
- export function MarklowerView({ node, onNavigate }: { node: NodeJson; onNavigate: (path: string) => void }) {
107
- return (
108
- <div className="marklower">
109
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
110
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
111
- <p className="chapter-prose">{parse(scalarValue(node.value), onNavigate, node.documentPath)}</p>
112
- </div>
113
- );
114
- }
115
-
116
- /** A marklower chunk embedded inline in a chapter (the chapter supplies the
117
- * surrounding number + anchor). */
118
- export function MarklowerChunk({ chunk, onNavigate }: { chunk: Chunk; onNavigate: (path: string) => void }) {
119
- return <p className="chapter-prose">{parse(chunk.value, onNavigate, chunk.documentPath)}</p>;
120
- }
@@ -1,64 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- /**
4
- * The rendered-HTML body for the markdown/asciidoc page views, plus the control that sets its
5
- * line-wrap measure. The **reading width is a URL parameter** — `?width=<ch>`, alongside
6
- * `?format=` — so a particular width is a shareable link (the CSV renderer keeps its options in
7
- * the query the same way). Default 72ch. The control lives in the tab bar next to the renderer
8
- * button (see NodeView), not in the body. Chapter *chunks* render plain `.markup`, unaffected.
9
- */
10
- const DEFAULT_WIDTH_CH = 72;
11
- const MIN_CH = 20;
12
- const MAX_CH = 400;
13
- const params = () => new URLSearchParams(window.location.search);
14
-
15
- /** The reading width in `ch` from the URL's `?width=`, or the default (out-of-range ignored). */
16
- export function markupWidthCh(): number {
17
- const w = Number(params().get("width"));
18
- return Number.isFinite(w) && w >= MIN_CH && w <= MAX_CH ? w : DEFAULT_WIDTH_CH;
19
- }
20
-
21
- function writeWidth(ch: number): void {
22
- const q = params();
23
- if (ch === DEFAULT_WIDTH_CH) q.delete("width");
24
- else q.set("width", String(ch));
25
- const qs = q.toString();
26
- window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : ""));
27
- }
28
-
29
- /** The markdown/asciidoc body at the URL-configured reading width. */
30
- export function Markup({ html }: { html: string }) {
31
- return <div className="markup" style={{ maxWidth: `${markupWidthCh()}ch` }} dangerouslySetInnerHTML={{ __html: html }} />;
32
- }
33
-
34
- /**
35
- * The width control beside the markdown/asciidoc renderer button (in the tab bar). It accepts
36
- * ANY input — a valid measure (20–400 ch) is applied to the URL and `rerender()` re-wraps the
37
- * body; an impossible/half-typed value is simply left unapplied and the field turns red (no
38
- * editing is blocked). No visible label: the hover title reads "width, ch".
39
- */
40
- export function MarkupWidthControl({ rerender }: { rerender: () => void }) {
41
- const urlWidth = markupWidthCh();
42
- const [text, setText] = useState(String(urlWidth));
43
- useEffect(() => setText(String(urlWidth)), [urlWidth]); // resync when the URL changes (nav / apply)
44
- const n = Number(text);
45
- const valid = text.trim() !== "" && Number.isInteger(n) && n >= MIN_CH && n <= MAX_CH;
46
- return (
47
- <input
48
- className={"markup-width" + (valid ? "" : " invalid")}
49
- type="text"
50
- inputMode="numeric"
51
- title="width, ch"
52
- value={text}
53
- onChange={(e) => {
54
- const v = e.target.value;
55
- setText(v);
56
- const num = Number(v);
57
- if (v.trim() !== "" && Number.isInteger(num) && num >= MIN_CH && num <= MAX_CH) {
58
- writeWidth(num);
59
- rerender();
60
- }
61
- }}
62
- />
63
- );
64
- }
@@ -1,19 +0,0 @@
1
- import { NodeJson, blobUrl } from "../api";
2
-
3
- /**
4
- * Renderer for a file-backed HTML document (`<iframe>`), pointing at the server's `/api/blob`
5
- * endpoint, which streams the file's raw bytes with its inferred Content-Type — so there is no
6
- * base64 round-trip through the JSON API. (Images live in `imagemap.tsx`: a pan/zoom viewer.)
7
- */
8
- export function HtmlView({ node }: { node: NodeJson }) {
9
- // sandboxed: same-origin so the page's own CSS/images (served from /api/blob)
10
- // load, but no scripts run — a saved web page renders without taking over.
11
- return (
12
- <iframe
13
- className="filehtml"
14
- src={blobUrl(node.path)}
15
- sandbox="allow-same-origin"
16
- title={node.title ?? node.path}
17
- />
18
- );
19
- }