yamlover 0.3.3 → 0.3.4

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.
@@ -13,6 +13,19 @@ export interface ImageRegion { x: number; y: number; w: number; h: number; title
13
13
 
14
14
  const num = (v: unknown): number => Number(v) || 0;
15
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
+
16
29
  /** The `rect`-type annotations, as pixel regions to overlay on the image. */
17
30
  function imageRegions(anns: Annotation[]): ImageRegion[] {
18
31
  return anns
@@ -33,13 +46,14 @@ export function PanZoomImage({
33
46
  src: string;
34
47
  className: string;
35
48
  regions?: ImageRegion[];
36
- onSelectRegion?: (selector: Record<string, unknown>, screen: { x: number; y: number }) => void;
49
+ onSelectRegion?: (selector: Record<string, unknown>, screen: { x: number; y: number }, imageBase64?: string) => void;
37
50
  onRegionClick?: (ann: Annotation, screen: { x: number; y: number }) => void;
38
51
  selectColor?: () => string;
39
52
  }) {
40
53
  const ref = useRef<HTMLDivElement>(null);
41
54
  const mapRef = useRef<L.Map | null>(null);
42
55
  const layerRef = useRef<L.LayerGroup | null>(null);
56
+ const imgElRef = useRef<HTMLImageElement | null>(null);
43
57
  const sizeRef = useRef({ w: 1, h: 1 });
44
58
  const onSelectRef = useRef(onSelectRegion);
45
59
  const onRegionClickRef = useRef(onRegionClick);
@@ -62,6 +76,7 @@ export function PanZoomImage({
62
76
  const w = img.naturalWidth || 1;
63
77
  const h = img.naturalHeight || 1;
64
78
  sizeRef.current = { w, h };
79
+ imgElRef.current = img; // kept for cropping a selected region (same-origin → un-tainted canvas)
65
80
  // CRS.Simple: coordinates are raw pixels (y, x); negative minZoom allows zooming far out.
66
81
  const map = L.map(ref.current, { crs: L.CRS.Simple, minZoom: -8, attributionControl: false, zoomSnap: 0 });
67
82
  const bounds: L.LatLngBoundsExpression = [[0, 0], [h, w]];
@@ -76,10 +91,8 @@ export function PanZoomImage({
76
91
  // image pixels have y from the top; CRS.Simple lat is from the bottom → flip.
77
92
  const { h: ih } = sizeRef.current;
78
93
  const west = b.getWest(), east = b.getEast(), south = b.getSouth(), north = b.getNorth();
79
- onSelectRef.current?.(
80
- { type: "rect", x: Math.round(west), y: Math.round(ih - north), w: Math.round(east - west), h: Math.round(north - south) },
81
- screen,
82
- );
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));
83
96
  }
84
97
  : undefined,
85
98
  });
@@ -141,7 +154,7 @@ export function ImageView({ node }: { node: NodeJson }) {
141
154
  <PanZoomImage
142
155
  src={blobUrl(node.path)}
143
156
  regions={imageRegions(shown)}
144
- onSelectRegion={openCreate}
157
+ onSelectRegion={(sel, screen, crop) => openCreate(sel, screen, undefined, crop)}
145
158
  onRegionClick={openEdit}
146
159
  selectColor={() => color}
147
160
  className="filemap fileimagemap"
@@ -1,6 +1,7 @@
1
1
  import katex from "katex";
2
2
  import "katex/dist/katex.min.css";
3
3
  import { NodeJson } from "../api";
4
+ import { scalarValue } from "../render";
4
5
  import { Chunk } from "./registry";
5
6
 
6
7
  /**
@@ -24,7 +25,7 @@ export function LatexView({ node }: { node: NodeJson }) {
24
25
  <div className="text">
25
26
  {node.title && <h1 className="chapter-title">{node.title}</h1>}
26
27
  {node.description && <p className="chapter-subtitle">{node.description}</p>}
27
- <div className="markup" dangerouslySetInnerHTML={{ __html: renderMath(node.value, true) }} />
28
+ <div className="markup" dangerouslySetInnerHTML={{ __html: renderMath(scalarValue(node.value), true) }} />
28
29
  </div>
29
30
  );
30
31
  }
@@ -1,5 +1,6 @@
1
1
  import { ReactNode } from "react";
2
2
  import { NodeJson } from "../api";
3
+ import { scalarValue } from "../render";
3
4
  import { Chunk } from "./registry";
4
5
  import { renderMath } from "./latex";
5
6
  import { NavLink } from "../links";
@@ -107,7 +108,7 @@ export function MarklowerView({ node, onNavigate }: { node: NodeJson; onNavigate
107
108
  <div className="marklower">
108
109
  {node.title && <h1 className="chapter-title">{node.title}</h1>}
109
110
  {node.description && <p className="chapter-subtitle">{node.description}</p>}
110
- <p className="chapter-prose">{parse(node.value, onNavigate, node.documentPath)}</p>
111
+ <p className="chapter-prose">{parse(scalarValue(node.value), onNavigate, node.documentPath)}</p>
111
112
  </div>
112
113
  );
113
114
  }
@@ -1,5 +1,6 @@
1
1
  import { deflateSync } from "fflate";
2
2
  import { NodeJson } from "../api";
3
+ import { scalarValue } from "../render";
3
4
  import { Chunk } from "./registry";
4
5
 
5
6
  /**
@@ -71,7 +72,7 @@ export function PlantumlView({ node }: { node: NodeJson }) {
71
72
  <div className="text">
72
73
  {node.title && <h1 className="chapter-title">{node.title}</h1>}
73
74
  {node.description && <p className="chapter-subtitle">{node.description}</p>}
74
- <Diagram source={String(node.value ?? "")} />
75
+ <Diagram source={String(scalarValue(node.value) ?? "")} />
75
76
  </div>
76
77
  );
77
78
  }
@@ -79,6 +79,9 @@ export interface Chunk {
79
79
  path: string;
80
80
  type: string;
81
81
  format: string | null;
82
+ valueType?: string | null; // renderer dispatch facets (TYPES.md §9) — so a tagged chunk still routes
83
+ hasKeyed?: boolean;
84
+ hasOrdinal?: boolean;
82
85
  /** The JSON-space path of the document this chunk belongs to (the enclosing
83
86
  * chapter's `documentPath`) — the anchor a document-relative (`/…`) marklower
84
87
  * link in the chunk resolves against. */
@@ -99,11 +102,32 @@ export interface TocView {
99
102
  loadDepth?: number;
100
103
  }
101
104
 
105
+ /** The three TYPE FACETS a renderer dispatches on (TYPES.md §9): the scalar self-VALUE's type,
106
+ * the node's `format`, and whether it owns keyed/ordinal elements. */
107
+ export interface TypeFacets {
108
+ valueType: string | null;
109
+ format: string | null;
110
+ hasKeyed: boolean;
111
+ hasOrdinal: boolean;
112
+ }
113
+ /** A renderer's acceptance predicate — a hand-coded type formula. What it does NOT test, it
114
+ * TOLERATES: a `byFormat("text/markdown")` matcher ignores the keyed/ordinal facets, so a
115
+ * markdown chunk that gained `yamlover-annotations` keys (an omni node) still matches. */
116
+ export type Accepts = (f: TypeFacets) => boolean;
117
+
118
+ /** Any projected node/chunk/link shape carrying the facet fields. */
119
+ type FacetSource = { type?: string; format?: string | null; valueType?: string | null; hasKeyed?: boolean; hasOrdinal?: boolean };
120
+ const facetsFrom = (n: FacetSource): TypeFacets => ({ valueType: n.valueType ?? null, format: n.format ?? null, hasKeyed: !!n.hasKeyed, hasOrdinal: !!n.hasOrdinal });
121
+ /** The common matcher: claims a node whose `format` is one of `fmts` — tolerant of all structure. */
122
+ const byFormat = (...fmts: string[]): Accepts => (f) => f.format !== null && fmts.includes(f.format);
123
+
102
124
  export interface Renderer {
103
125
  name: string;
104
- /** The (type, format) tuples this renderer claims. A `null` format matches a
105
- * node that carries no `format`; a string format matches that format exactly. */
106
- accepts: ReadonlyArray<readonly [type: string, format: string | null]>;
126
+ /** Whether this renderer claims a node, from its {@link TypeFacets}. */
127
+ accepts: Accepts;
128
+ /** Tie-break among matches the highest wins. Format matchers are 2; the bare-string
129
+ * default (marklower) is 1. */
130
+ specificity: number;
107
131
  /** Value depth `NodeView` must fetch for this renderer (default 1). A chapter
108
132
  * needs 2: its `chunks`/`children` arrays one level, their elements the next. */
109
133
  depth?: number;
@@ -129,12 +153,10 @@ export interface Renderer {
129
153
  */
130
154
  const EXPLORER: Renderer = {
131
155
  name: "explorer",
132
- accepts: [
133
- ["object", "x-yamlover-tag"],
134
- ["variant", "x-yamlover-tag"],
135
- ["string", "x-yamlover-tag"],
136
- ["null", "x-yamlover-tag"],
137
- ],
156
+ // a tag, whatever shape it projects (object / variant / leaf string / bare null) — the format
157
+ // alone identifies it; the grid shows the tagged MATERIALS. Also the dir-concrete fallback below.
158
+ accepts: byFormat("x-yamlover-tag"),
159
+ specificity: 2,
138
160
  render: (node, onNavigate) => <ExplorerView node={node} onNavigate={onNavigate} />,
139
161
  config: (rerender) => <ExplorerViewControl rerender={rerender} />, // large/small icons (`?view=`)
140
162
  };
@@ -145,7 +167,8 @@ const isDirConcrete = (concrete: string | null | undefined): boolean => concrete
145
167
  const REGISTRY: Renderer[] = [
146
168
  {
147
169
  name: "chapter",
148
- accepts: [["object", "x-yamlover-chapter"]],
170
+ accepts: byFormat("x-yamlover-chapter"),
171
+ specificity: 2,
149
172
  depth: 2, // reach the chunk/subchapter elements (arrays one level, items the next)
150
173
  tocView: chapterTocView,
151
174
  render: (node, onNavigate) => <ChapterView node={node} onNavigate={onNavigate} />,
@@ -155,10 +178,8 @@ const REGISTRY: Renderer[] = [
155
178
  // notch below Markdown. A chapter's prose chunks route here — both the bare
156
179
  // (string, null) form and the explicit `text/marklower` the chunk schema applies.
157
180
  name: "marklower",
158
- accepts: [
159
- ["string", null],
160
- ["string", "text/marklower"],
161
- ],
181
+ accepts: (f) => f.format === "text/marklower" || (f.format === null && f.valueType === "string"),
182
+ specificity: 1, // the bare-string default — a tagged bare string (format-less) still routes here
162
183
  render: (node, onNavigate) => <MarklowerView node={node} onNavigate={onNavigate} />,
163
184
  renderChunk: (chunk, onNavigate) => <MarklowerChunk chunk={chunk} onNavigate={onNavigate} />,
164
185
  },
@@ -166,14 +187,16 @@ const REGISTRY: Renderer[] = [
166
187
  // Markdown (the component file is text.tsx for historical reasons; the renderer —
167
188
  // its tab label and `?format=` key — is named for what it renders).
168
189
  name: "markdown",
169
- accepts: [["string", "text/markdown"]],
190
+ accepts: byFormat("text/markdown"),
191
+ specificity: 2,
170
192
  render: (node) => <TextView node={node} />,
171
193
  renderChunk: (chunk) => <TextChunk chunk={chunk} />,
172
194
  config: (rerender) => <MarkupWidthControl rerender={rerender} />,
173
195
  },
174
196
  {
175
197
  name: "asciidoc",
176
- accepts: [["string", "text/asciidoc"]],
198
+ accepts: byFormat("text/asciidoc"),
199
+ specificity: 2,
177
200
  render: (node) => <AsciidocView node={node} />,
178
201
  renderChunk: (chunk) => <AsciidocChunk chunk={chunk} />,
179
202
  config: (rerender) => <MarkupWidthControl rerender={rerender} />,
@@ -182,10 +205,8 @@ const REGISTRY: Renderer[] = [
182
205
  // Delimited text (CSV/TSV, a string) shown as a table; its parsing options
183
206
  // (separator, header) ride in the URL query — see csv.tsx.
184
207
  name: "csv",
185
- accepts: [
186
- ["string", "text/csv"],
187
- ["string", "text/tab-separated-values"],
188
- ],
208
+ accepts: byFormat("text/csv", "text/tab-separated-values"),
209
+ specificity: 2,
189
210
  render: (node) => <CsvView node={node} />,
190
211
  renderChunk: (chunk) => <CsvChunk chunk={chunk} />,
191
212
  config: (rerender) => <CsvControls rerender={rerender} />,
@@ -195,7 +216,8 @@ const REGISTRY: Renderer[] = [
195
216
  // CP866 / Windows-1251 / KOI8-R / UTF-8 (see plaintext.tsx). Served as raw
196
217
  // bytes so the encoding is the client's to choose.
197
218
  name: "plaintext",
198
- accepts: [["binary", "text/plain"]],
219
+ accepts: byFormat("text/plain"),
220
+ specificity: 2,
199
221
  render: (node) => <PlaintextView node={node} />,
200
222
  renderChunk: (chunk) => <PlaintextChunk chunk={chunk} />,
201
223
  config: (rerender) => <EncodingControl rerender={rerender} />,
@@ -203,41 +225,40 @@ const REGISTRY: Renderer[] = [
203
225
  {
204
226
  // RTF — a dependency-free converter to HTML (see rtf.tsx).
205
227
  name: "rtf",
206
- accepts: [["binary", "application/rtf"]],
228
+ accepts: byFormat("application/rtf"),
229
+ specificity: 2,
207
230
  render: (node) => <RtfView node={node} />,
208
231
  renderChunk: (chunk) => <RtfChunk chunk={chunk} />,
209
232
  },
210
233
  {
211
234
  // .docx (Office Open XML) via mammoth, lazily loaded.
212
235
  name: "docx",
213
- accepts: [["binary", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]],
236
+ accepts: byFormat("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
237
+ specificity: 2,
214
238
  render: (node) => lazily(<DocxView node={node} />),
215
239
  renderChunk: (chunk) => lazily(<DocxChunk chunk={chunk} />),
216
240
  },
217
241
  {
218
242
  // Excel workbooks — .xlsx and legacy .xls — via SheetJS, lazily loaded.
219
243
  name: "spreadsheet",
220
- accepts: [
221
- ["binary", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
222
- ["binary", "application/vnd.ms-excel"],
223
- ],
244
+ accepts: byFormat("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel"),
245
+ specificity: 2,
224
246
  render: (node) => lazily(<SpreadsheetView node={node} />),
225
247
  renderChunk: (chunk) => lazily(<SpreadsheetChunk chunk={chunk} />),
226
248
  },
227
249
  {
228
250
  // Legacy .doc (Word 97–2003 binary) — no in-browser parser; download fallback.
229
251
  name: "doc",
230
- accepts: [["binary", "application/msword"]],
252
+ accepts: byFormat("application/msword"),
253
+ specificity: 2,
231
254
  render: (node) => <DocView node={node} />,
232
255
  renderChunk: (chunk) => <DocChunk chunk={chunk} />,
233
256
  },
234
257
  {
235
258
  // KML / KMZ geographic overlays drawn on a Leaflet map (lazily loaded).
236
259
  name: "map",
237
- accepts: [
238
- ["binary", "application/vnd.google-earth.kml+xml"],
239
- ["binary", "application/vnd.google-earth.kmz"],
240
- ],
260
+ accepts: byFormat("application/vnd.google-earth.kml+xml", "application/vnd.google-earth.kmz"),
261
+ specificity: 2,
241
262
  render: (node) => lazily(<MapView node={node} />),
242
263
  renderChunk: (chunk) => lazily(<MapChunk chunk={chunk} />),
243
264
  },
@@ -245,7 +266,8 @@ const REGISTRY: Renderer[] = [
245
266
  // LaTeX math (a string) typeset with KaTeX, both whole and inline. marklower
246
267
  // reuses the same engine for its `$$…$$` spans.
247
268
  name: "latex",
248
- accepts: [["string", "text/x-latex"]],
269
+ accepts: byFormat("text/x-latex"),
270
+ specificity: 2,
249
271
  render: (node) => <LatexView node={node} />,
250
272
  renderChunk: (chunk) => <LatexChunk chunk={chunk} />,
251
273
  },
@@ -253,7 +275,8 @@ const REGISTRY: Renderer[] = [
253
275
  // PlantUML source (a string) shown as the diagram it compiles to, both as a
254
276
  // whole node and inline as a chapter chunk.
255
277
  name: "plantuml",
256
- accepts: [["string", "text/x-plantuml"]],
278
+ accepts: byFormat("text/x-plantuml"),
279
+ specificity: 2,
257
280
  render: (node) => <PlantumlView node={node} />,
258
281
  renderChunk: (chunk) => <PlantumlChunk chunk={chunk} />,
259
282
  },
@@ -261,64 +284,64 @@ const REGISTRY: Renderer[] = [
261
284
  {
262
285
  // File-backed binaries the server tags with an inferred image format.
263
286
  name: "image",
264
- accepts: [
265
- ["binary", "image/png"],
266
- ["binary", "image/jpeg"],
267
- ["binary", "image/gif"],
268
- ["binary", "image/webp"],
269
- ["binary", "image/avif"],
270
- ["binary", "image/bmp"],
271
- ["binary", "image/x-icon"],
272
- ["binary", "image/svg+xml"],
273
- ],
287
+ accepts: byFormat("image/png", "image/jpeg", "image/gif", "image/webp", "image/avif", "image/bmp", "image/x-icon", "image/svg+xml"),
288
+ specificity: 2,
274
289
  render: (node) => lazily(<ImageView node={node} />),
275
290
  renderChunk: (chunk) => lazily(<ImageChunk chunk={chunk} />),
276
291
  },
277
292
  {
278
293
  name: "html",
279
- accepts: [["binary", "text/html"]],
294
+ accepts: byFormat("text/html"),
295
+ specificity: 2,
280
296
  render: (node) => <HtmlView node={node} />,
281
297
  renderChunk: (chunk) => <HtmlView node={chunkNode(chunk)} />,
282
298
  },
283
299
  {
284
300
  name: "fb2",
285
- accepts: [["binary", "application/x-fictionbook+xml"]],
301
+ accepts: byFormat("application/x-fictionbook+xml"),
302
+ specificity: 2,
286
303
  render: (node) => <Fb2View node={node} />,
287
304
  renderChunk: (chunk) => <Fb2View node={chunkNode(chunk)} />,
288
305
  },
289
306
  {
290
307
  name: "epub",
291
- accepts: [["binary", "application/epub+zip"]],
308
+ accepts: byFormat("application/epub+zip"),
309
+ specificity: 2,
292
310
  render: (node) => <EpubView node={node} />,
293
311
  renderChunk: (chunk) => <EpubView node={chunkNode(chunk)} />,
294
312
  },
295
313
  {
296
314
  name: "pdf",
297
- accepts: [["binary", "application/pdf"]],
315
+ accepts: byFormat("application/pdf"),
316
+ specificity: 2,
298
317
  render: (node) => lazily(<PdfView node={node} />),
299
318
  renderChunk: (chunk) => lazily(<PdfView node={chunkNode(chunk)} />),
300
319
  },
301
320
  {
302
321
  name: "djvu",
303
- accepts: [["binary", "image/vnd.djvu"]],
322
+ accepts: byFormat("image/vnd.djvu"),
323
+ specificity: 2,
304
324
  render: (node) => lazily(<DjvuView node={node} />),
305
325
  renderChunk: (chunk) => lazily(<DjvuView node={chunkNode(chunk)} />),
306
326
  },
307
327
  {
308
328
  name: "psd",
309
- accepts: [["binary", "image/vnd.adobe.photoshop"]],
329
+ accepts: byFormat("image/vnd.adobe.photoshop"),
330
+ specificity: 2,
310
331
  render: (node) => lazily(<PsdView node={node} />),
311
332
  renderChunk: (chunk) => lazily(<PsdView node={chunkNode(chunk)} />),
312
333
  },
313
334
  {
314
335
  name: "tiff",
315
- accepts: [["binary", "image/tiff"]],
336
+ accepts: byFormat("image/tiff"),
337
+ specificity: 2,
316
338
  render: (node) => lazily(<TiffView node={node} />),
317
339
  renderChunk: (chunk) => lazily(<TiffView node={chunkNode(chunk)} />),
318
340
  },
319
341
  {
320
342
  name: "heic",
321
- accepts: [["binary", "image/heic"]],
343
+ accepts: byFormat("image/heic"),
344
+ specificity: 2,
322
345
  render: (node) => lazily(<HeicView node={node} />),
323
346
  renderChunk: (chunk) => lazily(<HeicView node={chunkNode(chunk)} />),
324
347
  },
@@ -356,33 +379,32 @@ function chapterTocView(node: TreeNode): TocView {
356
379
  };
357
380
  }
358
381
 
359
- /** The renderer whose `accepts` covers `(type, format)`, or null when none does. */
360
- export function rendererFor(type: string, format: string | null): Renderer | null {
361
- return REGISTRY.find((r) => r.accepts.some(([t, f]) => t === type && f === format)) ?? null;
382
+ /** The renderer that claims `src`'s facets, or null when none does: of the matchers that accept
383
+ * it, the most SPECIFIC (highest `specificity`) wins (TYPES.md §9). */
384
+ export function rendererFor(src: FacetSource): Renderer | null {
385
+ const f = facetsFrom(src);
386
+ let best: Renderer | null = null;
387
+ for (const r of REGISTRY) if (r.accepts(f) && (best === null || r.specificity > best.specificity)) best = r;
388
+ return best;
362
389
  }
363
390
 
364
- /** The renderer for a node: its (type, format) claim, else — for a node stored as a
365
- * filesystem directory that no format renderer claims (a dir-backed chapter stays a
366
- * chapter) — the explorer, else null → the default tabbed view. */
391
+ /** The renderer for a node: its facet claim, else — for a node stored as a filesystem directory
392
+ * that no format renderer claims (a dir-backed chapter stays a chapter) — the explorer, else
393
+ * null → the default tabbed view. */
367
394
  export function getRenderer(node: NodeJson): Renderer | null {
368
- const r = rendererFor(node.type, node.format ?? null);
369
- if (r) return r;
370
- return isDirConcrete(node.concrete) ? EXPLORER : null;
395
+ return rendererFor(node) ?? (isDirConcrete(node.concrete) ? EXPLORER : null);
371
396
  }
372
397
 
373
- /** The name (= representation key / `?format=` value) of the renderer for
374
- * `(type, format)` — with the same directory-concrete explorer fallback as
375
- * {@link getRenderer} or null when none claims it. */
376
- export function rendererName(type: string, format: string | null, concrete?: string | null): string | null {
377
- const r = rendererFor(type, format);
378
- if (r) return r.name;
379
- return isDirConcrete(concrete) ? EXPLORER.name : null;
398
+ /** The name (= representation key / `?format=` value) of the renderer that claims `src` — with the
399
+ * same directory-concrete explorer fallback as {@link getRenderer} — or null when none claims it. */
400
+ export function rendererName(src: FacetSource, concrete?: string | null): string | null {
401
+ return rendererFor(src)?.name ?? (isDirConcrete(concrete) ? EXPLORER.name : null);
380
402
  }
381
403
 
382
404
  /** How `node` appears in the TOC: its renderer's `tocView`, or — when no renderer
383
405
  * claims it — its own children, lazily loaded (the passive default). */
384
406
  export function tocView(node: TreeNode): TocView {
385
- const r = rendererFor(node.type, node.format);
407
+ const r = rendererFor(node);
386
408
  if (r?.tocView) return r.tocView(node);
387
409
  const loaded = node.children.length > 0;
388
410
  return { children: node.children, expandable: loaded ? node.children.length > 0 : node.hasChildren, loaded };
@@ -1,5 +1,6 @@
1
1
  import { marked } from "marked";
2
2
  import { NodeJson } from "../api";
3
+ import { scalarValue } from "../render";
3
4
  import { Chunk } from "./registry";
4
5
  import { anchorizeHeadings, useHashScroll } from "./headings";
5
6
  import { Markup } from "./markup";
@@ -29,7 +30,7 @@ export function TextView({ node }: { node: NodeJson }) {
29
30
  <div className="text">
30
31
  {node.title && <h1 className="chapter-title">{node.title}</h1>}
31
32
  {node.description && <p className="chapter-subtitle">{node.description}</p>}
32
- <Markup html={md(node.value)} />
33
+ <Markup html={md(scalarValue(node.value))} />
33
34
  </div>
34
35
  );
35
36
  }
@@ -873,7 +873,8 @@ a.chunk-index:hover {
873
873
  flex-wrap: wrap;
874
874
  gap: 4px;
875
875
  }
876
- .annotate-recents .tagtag {
876
+ .annotate-recents .tagtag,
877
+ .annotate-suggest .tagtag {
877
878
  border: 0;
878
879
  font: inherit;
879
880
  font-size: 11px;
@@ -885,10 +886,12 @@ a.chunk-index:hover {
885
886
  unclipped WRAPPER the child is clipped first; then a first round of hard offset drop-shadows
886
887
  in the MENU BACKGROUND color grows the silhouette by 1px (the gap), and a second round in
887
888
  the foreground color rings that — gap + ring, both following the actual tag shape. */
888
- .annotate-recents .tagframe {
889
+ .annotate-recents .tagframe,
890
+ .annotate-suggest .tagframe {
889
891
  display: inline-flex;
890
892
  }
891
- .annotate-recents .tagframe.sel {
893
+ .annotate-recents .tagframe.sel,
894
+ .annotate-suggest .tagframe.sel {
892
895
  filter:
893
896
  drop-shadow(1px 0 0 var(--bg-alt))
894
897
  drop-shadow(-1px 0 0 var(--bg-alt))
@@ -915,6 +918,25 @@ a.chunk-index:hover {
915
918
  outline: none;
916
919
  border-color: var(--dim);
917
920
  }
921
+ /* the typeahead: the path input plus a suggestion list below it, spanning the menu width */
922
+ .annotate-typeahead {
923
+ flex-basis: 100%;
924
+ min-width: 0;
925
+ position: relative;
926
+ }
927
+ .annotate-typeahead .annotate-taginput {
928
+ width: 100%;
929
+ box-sizing: border-box;
930
+ }
931
+ /* matched named tags as badges (the highlighted one rings via .tagframe.sel) */
932
+ .annotate-suggest {
933
+ display: flex;
934
+ flex-wrap: wrap;
935
+ gap: 4px;
936
+ margin-top: 5px;
937
+ max-height: 132px;
938
+ overflow-y: auto;
939
+ }
918
940
  /* a region annotation drawn over a PDF page (the image overlay uses a Leaflet rectangle) */
919
941
  .pdf-page {
920
942
  position: relative;