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.
- package/bin/yamlover.js +199 -112
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/client/assets/decoded-CFkcuQ1f.js +1 -0
- package/dist/client/assets/djvu-B3wuFWwI.js +1 -0
- package/dist/client/assets/docx-putHYXzO.js +230 -0
- package/dist/client/assets/heic-oIxhIbIW.js +66 -0
- package/dist/client/assets/imagemap-CUKUIcGe.js +1 -0
- package/dist/client/assets/index-CQ3TGVSx.js +617 -0
- package/dist/client/assets/index-wTjOzmSY.css +1 -0
- package/dist/client/assets/map-CZS083CS.js +1 -0
- package/dist/client/assets/paged-CjxSuuik.js +1 -0
- package/dist/client/assets/panzoom-C7OuIIda.js +4 -0
- package/dist/client/assets/panzoom-CIGW-MKW.css +1 -0
- package/dist/client/assets/pdf-Dz3gUx0B.js +12 -0
- package/dist/client/assets/pdf-m5e3jsV4.css +1 -0
- package/dist/client/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
- package/dist/client/assets/psd-C8yNwHFw.js +11 -0
- package/dist/client/assets/spreadsheet-DBv7aqoN.js +38 -0
- package/dist/client/assets/tiff-73RKtIux.js +1 -0
- package/{index.html → dist/client/index.html} +2 -1
- package/package.json +18 -21
- package/src/client/App.tsx +0 -424
- package/src/client/NodeView.tsx +0 -422
- package/src/client/TaskStrip.tsx +0 -34
- package/src/client/Tree.tsx +0 -97
- package/src/client/api.ts +0 -213
- package/src/client/icons.ts +0 -91
- package/src/client/links.tsx +0 -108
- package/src/client/live.ts +0 -42
- package/src/client/main.tsx +0 -10
- package/src/client/paste-html.ts +0 -228
- package/src/client/paste-links.ts +0 -42
- package/src/client/paths.ts +0 -139
- package/src/client/render.tsx +0 -339
- package/src/client/renderers/annotate.tsx +0 -641
- package/src/client/renderers/asciidoc.tsx +0 -36
- package/src/client/renderers/chapter.tsx +0 -142
- package/src/client/renderers/csv.tsx +0 -234
- package/src/client/renderers/decoded.tsx +0 -72
- package/src/client/renderers/djvu.tsx +0 -271
- package/src/client/renderers/djvuWorker.ts +0 -99
- package/src/client/renderers/doc.tsx +0 -40
- package/src/client/renderers/docx.tsx +0 -49
- package/src/client/renderers/epub.tsx +0 -147
- package/src/client/renderers/explorer.tsx +0 -294
- package/src/client/renderers/fb2.tsx +0 -149
- package/src/client/renderers/headings.ts +0 -69
- package/src/client/renderers/heic.tsx +0 -23
- package/src/client/renderers/imagemap.tsx +0 -170
- package/src/client/renderers/kml.ts +0 -46
- package/src/client/renderers/latex.tsx +0 -37
- package/src/client/renderers/map.tsx +0 -205
- package/src/client/renderers/marklower.tsx +0 -120
- package/src/client/renderers/markup.tsx +0 -64
- package/src/client/renderers/media.tsx +0 -19
- package/src/client/renderers/paged.ts +0 -137
- package/src/client/renderers/panzoom.ts +0 -101
- package/src/client/renderers/pdf.tsx +0 -316
- package/src/client/renderers/plaintext.tsx +0 -120
- package/src/client/renderers/plantuml.tsx +0 -83
- package/src/client/renderers/psd.tsx +0 -25
- package/src/client/renderers/registry.tsx +0 -411
- package/src/client/renderers/rtf.tsx +0 -210
- package/src/client/renderers/spreadsheet.tsx +0 -105
- package/src/client/renderers/tag.tsx +0 -113
- package/src/client/renderers/text.tsx +0 -42
- package/src/client/renderers/tiff.tsx +0 -33
- package/src/client/styles.css +0 -1212
- package/src/client/vendor/README.md +0 -30
- package/src/client/vite-env.d.ts +0 -31
- package/src/server/api.ts +0 -147
- package/src/server/embed.ts +0 -187
- package/src/server/engine-api.ts +0 -1563
- package/src/server/gitignore.ts +0 -81
- package/src/server/node-kind.ts +0 -61
- package/src/server/tasks.ts +0 -83
- package/src/server/yamlover.ts +0 -1133
- /package/{src/client/vendor/djvu.js → dist/client/assets/djvu-Ci0lFKxE.js} +0 -0
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { NodeJson } from "../api";
|
|
2
|
-
import { asLink, Link } from "../render";
|
|
3
|
-
import { segsToStr, strToSegs } from "../paths";
|
|
4
|
-
import { Chunk, rendererFor } from "./registry";
|
|
5
|
-
import { useHashScroll } from "./headings";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The renderer for an `object`/`x-yamlover-chapter`: a chapter shown as a readable
|
|
9
|
-
* page. This is yamlover's first instance of **partial flattening** (see the global
|
|
10
|
-
* README): a deeper subtree is presented shallowly, pulling some descendants up as
|
|
11
|
-
* constituent parts of *this* page rather than as nodes you navigate away to.
|
|
12
|
-
*
|
|
13
|
-
* A chapter is a heading (`title`/`description`) plus two arrays:
|
|
14
|
-
*
|
|
15
|
-
* - `chunks` — the body, **flattened** into this page as numbered blocks. Each
|
|
16
|
-
* chunk is delegated to the renderer for its own (type, format), so
|
|
17
|
-
* a chapter is not prose-only: a `text/markdown` chunk routes to the
|
|
18
|
-
* text renderer, a `text/x-plantuml` chunk to the diagram renderer,
|
|
19
|
-
* and any file-backed binary (image, html, pdf, fb2, epub, psd,
|
|
20
|
-
* tiff, …) to its own renderer via `renderChunk`. A flattened chunk
|
|
21
|
-
* exposes its location as a **fragment anchor** whose syntax is the
|
|
22
|
-
* chunk's path continuation: chunk `[1]`, still reachable in full at
|
|
23
|
-
* `<chapter>:chunks[1]`, is anchored here at `#:chunks[1]` (so
|
|
24
|
-
* `<chapter>#:chunks[1]` scrolls to it). The `§N` marker is that
|
|
25
|
-
* in-page anchor link. See 16-all-formats-chunks.
|
|
26
|
-
* - `children` — the subchapters, *not* flattened: rendered as heading links you
|
|
27
|
-
* navigate to (and surfaced in the TOC; see `chapterTocView`).
|
|
28
|
-
*
|
|
29
|
-
* The value arrives two levels deep (see the chapter renderer's `depth`): the
|
|
30
|
-
* arrays are present, and each element is a link marker carrying its (type,
|
|
31
|
-
* format), value/title, and path.
|
|
32
|
-
*/
|
|
33
|
-
export function ChapterView({
|
|
34
|
-
node,
|
|
35
|
-
onNavigate,
|
|
36
|
-
}: {
|
|
37
|
-
node: NodeJson;
|
|
38
|
-
onNavigate: (path: string) => void;
|
|
39
|
-
}) {
|
|
40
|
-
const v = (node.value ?? {}) as { chunks?: unknown; children?: unknown };
|
|
41
|
-
const chunks = Array.isArray(v.chunks) ? v.chunks : [];
|
|
42
|
-
const children = Array.isArray(v.children) ? v.children : [];
|
|
43
|
-
|
|
44
|
-
// A deep link to a flattened chunk (`<chapter>#/chunks[1]`) lands on the chapter
|
|
45
|
-
// page, so scroll to the anchored chunk once it has rendered (the browser's own
|
|
46
|
-
// scroll fires before the async value arrives). Re-runs when the chapter changes.
|
|
47
|
-
useHashScroll(node);
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div className="chapter">
|
|
51
|
-
{node.title && <h1 className="chapter-title">{node.title}</h1>}
|
|
52
|
-
{node.description && <p className="chapter-subtitle">{node.description}</p>}
|
|
53
|
-
|
|
54
|
-
{chunks.map((item, i) => (
|
|
55
|
-
<ChunkBlock key={i} index={i} item={item} basePath={node.path} documentPath={node.documentPath} onNavigate={onNavigate} />
|
|
56
|
-
))}
|
|
57
|
-
|
|
58
|
-
{children.map((item, i) => {
|
|
59
|
-
const link = asLink(item);
|
|
60
|
-
return (
|
|
61
|
-
<h2 className="chapter-link" key={i}>
|
|
62
|
-
<a
|
|
63
|
-
className="descend"
|
|
64
|
-
href={link?.path ?? "#"}
|
|
65
|
-
onClick={(e) => {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
if (link) onNavigate(link.path);
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
{chapterTitle(link)}
|
|
71
|
-
</a>
|
|
72
|
-
</h2>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** One numbered chunk, flattened into the chapter page: its zero-based index `§N`
|
|
80
|
-
* as an in-page anchor link to the chunk's own location (the fragment mirrors the
|
|
81
|
-
* chunk's path continuation), and the chunk rendered by the renderer for its
|
|
82
|
-
* (type, format) — falling back to a plain paragraph when none claims it (or the
|
|
83
|
-
* chunk is a bare inline value with no path). */
|
|
84
|
-
function ChunkBlock({
|
|
85
|
-
index,
|
|
86
|
-
item,
|
|
87
|
-
basePath,
|
|
88
|
-
documentPath,
|
|
89
|
-
onNavigate,
|
|
90
|
-
}: {
|
|
91
|
-
index: number;
|
|
92
|
-
item: unknown;
|
|
93
|
-
basePath: string;
|
|
94
|
-
documentPath?: string;
|
|
95
|
-
onNavigate: (path: string) => void;
|
|
96
|
-
}) {
|
|
97
|
-
const link = asLink(item);
|
|
98
|
-
const chunk: Chunk = {
|
|
99
|
-
value: link ? link.value : item,
|
|
100
|
-
path: link?.path ?? "",
|
|
101
|
-
type: link?.type ?? "string",
|
|
102
|
-
format: link?.format ?? null,
|
|
103
|
-
// the renderer-dispatch facets (TYPES.md §9): a link carries them; a bare inline chunk is a string
|
|
104
|
-
valueType: link?.valueType ?? "string",
|
|
105
|
-
hasKeyed: link?.hasKeyed ?? false,
|
|
106
|
-
hasOrdinal: link?.hasOrdinal ?? false,
|
|
107
|
-
documentPath, // carried so a marklower chunk's `/…` link resolves to its document
|
|
108
|
-
};
|
|
109
|
-
const renderer = rendererFor(chunk);
|
|
110
|
-
const body = renderer?.renderChunk
|
|
111
|
-
? renderer.renderChunk(chunk, onNavigate)
|
|
112
|
-
: <p className="chapter-prose">{String(chunk.value ?? "")}</p>;
|
|
113
|
-
// The chunk's location *within this page*: its path continuation past the chapter
|
|
114
|
-
// (e.g. `/chunks[1]`), used as both the element id and the `§N` anchor link. The
|
|
115
|
-
// full path stays navigable; this is the flattened, in-page locator.
|
|
116
|
-
const anchor = chunk.path ? pathContinuation(basePath, chunk.path) : null;
|
|
117
|
-
return (
|
|
118
|
-
<div className="chunk" id={anchor ?? undefined}>
|
|
119
|
-
{anchor ? (
|
|
120
|
-
<a className="chunk-index" href={`#${anchor}`}>
|
|
121
|
-
§{index}
|
|
122
|
-
</a>
|
|
123
|
-
) : (
|
|
124
|
-
<span className="chunk-index">§{index}</span>
|
|
125
|
-
)}
|
|
126
|
-
<div className="chunk-body">{body}</div>
|
|
127
|
-
</div>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** The path continuation from `base` to `full` — the segments of `full` past
|
|
132
|
-
* `base`, in JSON-path syntax. E.g. ("/book", "/book/chunks[1]") → "/chunks[1]".
|
|
133
|
-
* A flattened child's fragment anchor is exactly this continuation, so the anchor
|
|
134
|
-
* spelling matches the still-navigable full path. */
|
|
135
|
-
function pathContinuation(base: string, full: string): string {
|
|
136
|
-
return segsToStr(strToSegs(full).slice(strToSegs(base).length));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** A subchapter link's label: its schema title, else a generic fallback. */
|
|
140
|
-
function chapterTitle(link: Link | null): string {
|
|
141
|
-
return link?.title ?? "(untitled chapter)";
|
|
142
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { NodeJson } from "../api";
|
|
2
|
-
import { scalarValue } from "../render";
|
|
3
|
-
import { Chunk } from "./registry";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The renderer for a `string`/`text/csv` (or `text/tab-separated-values`) node:
|
|
7
|
-
* delimited text shown as a table. Like the markdown/asciidoc renderers it works
|
|
8
|
-
* from the node's string value, so it serves both a whole `.csv`/`.tsv` file
|
|
9
|
-
* (`render`) and a single inline chunk (`renderChunk`).
|
|
10
|
-
*
|
|
11
|
-
* The **main parsing parameters live in the URL**, alongside `?format=csv`, so a
|
|
12
|
-
* particular reading of a file is a shareable link:
|
|
13
|
-
*
|
|
14
|
-
* - `sep` — the field separator. `,` `;` `|`, the word `tab` (or `space`), or
|
|
15
|
-
* empty/absent for **auto-detect** (the most frequent candidate on
|
|
16
|
-
* the first line; `\t` for a `.tsv`).
|
|
17
|
-
* - `header` — whether the first row is a header (default true; `false`/`0` off).
|
|
18
|
-
*
|
|
19
|
-
* The full-page view exposes these in the node bar beside the renderer's tab (the
|
|
20
|
-
* {@link CsvControls} `config` control, like the markdown/asciidoc width input),
|
|
21
|
-
* writing the same query params (via `history.replaceState`, preserving the path +
|
|
22
|
-
* `format`), so the URL stays the single source of truth — editing it by hand and
|
|
23
|
-
* reloading is equivalent to using the controls.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/** Parse delimited text into rows of fields, RFC-4180-ish: fields may be wrapped in
|
|
27
|
-
* `quote`, a doubled quote is a literal one, and separators/newlines inside quotes
|
|
28
|
-
* are data. Handles `\n` and `\r\n`; a trailing newline does not yield an empty
|
|
29
|
-
* row. */
|
|
30
|
-
export function parseDelimited(text: string, sep: string, quote = '"'): string[][] {
|
|
31
|
-
const rows: string[][] = [];
|
|
32
|
-
let row: string[] = [];
|
|
33
|
-
let field = "";
|
|
34
|
-
let inQuotes = false;
|
|
35
|
-
let i = 0;
|
|
36
|
-
const pushField = () => {
|
|
37
|
-
row.push(field);
|
|
38
|
-
field = "";
|
|
39
|
-
};
|
|
40
|
-
const pushRow = () => {
|
|
41
|
-
pushField();
|
|
42
|
-
rows.push(row);
|
|
43
|
-
row = [];
|
|
44
|
-
};
|
|
45
|
-
while (i < text.length) {
|
|
46
|
-
const c = text[i];
|
|
47
|
-
if (inQuotes) {
|
|
48
|
-
if (c === quote) {
|
|
49
|
-
if (text[i + 1] === quote) {
|
|
50
|
-
field += quote;
|
|
51
|
-
i += 2;
|
|
52
|
-
} else {
|
|
53
|
-
inQuotes = false;
|
|
54
|
-
i++;
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
field += c;
|
|
58
|
-
i++;
|
|
59
|
-
}
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (c === quote) {
|
|
63
|
-
inQuotes = true;
|
|
64
|
-
i++;
|
|
65
|
-
} else if (c === sep) {
|
|
66
|
-
pushField();
|
|
67
|
-
i++;
|
|
68
|
-
} else if (c === "\n" || c === "\r") {
|
|
69
|
-
if (c === "\r" && text[i + 1] === "\n") i++;
|
|
70
|
-
pushRow();
|
|
71
|
-
i++;
|
|
72
|
-
} else {
|
|
73
|
-
field += c;
|
|
74
|
-
i++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// flush the final field/row unless the text ended exactly on a row break
|
|
78
|
-
if (field !== "" || row.length) pushRow();
|
|
79
|
-
return rows;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Decode a `sep` URL value to the actual separator character, or null for
|
|
83
|
-
* auto-detect (empty/absent). `tab`/`space` are spelled out for URL-friendliness. */
|
|
84
|
-
function decodeSep(v: string | null): string | null {
|
|
85
|
-
if (!v) return null;
|
|
86
|
-
if (v === "tab" || v === "\\t") return "\t";
|
|
87
|
-
if (v === "space") return " ";
|
|
88
|
-
return v[0];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Auto-detect a separator from the first line: a `.tsv` is tab; otherwise the most
|
|
92
|
-
* frequent of `, ; \t |` (comma when none appears). */
|
|
93
|
-
function autoSep(text: string, format: string | null): string {
|
|
94
|
-
if (format === "text/tab-separated-values") return "\t";
|
|
95
|
-
const firstLine = text.split(/\r?\n/, 1)[0] ?? "";
|
|
96
|
-
let best = ",";
|
|
97
|
-
let bestN = 0;
|
|
98
|
-
for (const c of [",", ";", "\t", "|"]) {
|
|
99
|
-
const n = firstLine.split(c).length - 1;
|
|
100
|
-
if (n > bestN) {
|
|
101
|
-
bestN = n;
|
|
102
|
-
best = c;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return best;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const SEP_OPTIONS: { label: string; value: string }[] = [
|
|
109
|
-
{ label: "auto", value: "" },
|
|
110
|
-
{ label: "comma ,", value: "," },
|
|
111
|
-
{ label: "semicolon ;", value: ";" },
|
|
112
|
-
{ label: "tab", value: "tab" },
|
|
113
|
-
{ label: "pipe |", value: "|" },
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
const params = () => new URLSearchParams(window.location.search);
|
|
117
|
-
|
|
118
|
-
/** Read the (header) flag from the URL — default true, off for `false`/`0`. */
|
|
119
|
-
function headerOn(p: URLSearchParams): boolean {
|
|
120
|
-
const h = p.get("header");
|
|
121
|
-
return !(h === "false" || h === "0");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Pad a row to `cols` cells so every `<tr>` is rectangular. */
|
|
125
|
-
function pad(row: string[], cols: number): string[] {
|
|
126
|
-
return row.length >= cols ? row : [...row, ...Array(cols - row.length).fill("")];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The table itself — shared by the full page and the inline chunk. */
|
|
130
|
-
function Table({ rows, header }: { rows: string[][]; header: boolean }) {
|
|
131
|
-
if (!rows.length) return <p className="csv-empty">(empty)</p>;
|
|
132
|
-
const cols = rows.reduce((m, r) => Math.max(m, r.length), 0);
|
|
133
|
-
const head = header ? rows[0] : null;
|
|
134
|
-
const body = header ? rows.slice(1) : rows;
|
|
135
|
-
return (
|
|
136
|
-
<div className="csv-scroll">
|
|
137
|
-
<table className="csv-table">
|
|
138
|
-
{head && (
|
|
139
|
-
<thead>
|
|
140
|
-
<tr>
|
|
141
|
-
{pad(head, cols).map((c, i) => (
|
|
142
|
-
<th key={i}>{c}</th>
|
|
143
|
-
))}
|
|
144
|
-
</tr>
|
|
145
|
-
</thead>
|
|
146
|
-
)}
|
|
147
|
-
<tbody>
|
|
148
|
-
{body.map((r, ri) => (
|
|
149
|
-
<tr key={ri}>
|
|
150
|
-
{pad(r, cols).map((c, ci) => (
|
|
151
|
-
<td key={ci}>{c}</td>
|
|
152
|
-
))}
|
|
153
|
-
</tr>
|
|
154
|
-
))}
|
|
155
|
-
</tbody>
|
|
156
|
-
</table>
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function CsvView({ node }: { node: NodeJson }) {
|
|
162
|
-
// Options are read straight from the URL each render; the node bar's CsvControls
|
|
163
|
-
// (see registry `config`) write them and re-render the node view, so the URL stays
|
|
164
|
-
// the single source of truth — this view holds no parsing state of its own.
|
|
165
|
-
const p = params();
|
|
166
|
-
const header = headerOn(p);
|
|
167
|
-
const text = String(scalarValue(node.value) ?? "");
|
|
168
|
-
const sep = decodeSep(p.get("sep")) ?? autoSep(text, node.format ?? null);
|
|
169
|
-
const rows = parseDelimited(text, sep);
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<div className="csv">
|
|
173
|
-
{node.title && <h1 className="chapter-title">{node.title}</h1>}
|
|
174
|
-
{node.description && <p className="chapter-subtitle">{node.description}</p>}
|
|
175
|
-
<Table rows={rows} header={header} />
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* The CSV parsing controls (separator + header row) shown in the node bar beside
|
|
182
|
-
* the renderer's tab — the `config` hook, mirroring the markdown/asciidoc width
|
|
183
|
-
* input. Each writes a query param (preserving the path + other params) and calls
|
|
184
|
-
* `rerender` so {@link CsvView} re-reads the URL and re-parses.
|
|
185
|
-
*/
|
|
186
|
-
export function CsvControls({ rerender }: { rerender: () => void }) {
|
|
187
|
-
const p = params();
|
|
188
|
-
const sepParam = p.get("sep") ?? "";
|
|
189
|
-
const header = headerOn(p);
|
|
190
|
-
|
|
191
|
-
const setParam = (key: string, value: string) => {
|
|
192
|
-
const q = params();
|
|
193
|
-
if (value) q.set(key, value);
|
|
194
|
-
else q.delete(key);
|
|
195
|
-
const qs = q.toString();
|
|
196
|
-
window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : ""));
|
|
197
|
-
rerender();
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<div className="csv-toolbar">
|
|
202
|
-
<label>
|
|
203
|
-
separator{" "}
|
|
204
|
-
<select value={sepParam} onChange={(e) => setParam("sep", e.target.value)}>
|
|
205
|
-
{SEP_OPTIONS.map((o) => (
|
|
206
|
-
<option key={o.value} value={o.value}>
|
|
207
|
-
{o.label}
|
|
208
|
-
</option>
|
|
209
|
-
))}
|
|
210
|
-
</select>
|
|
211
|
-
</label>
|
|
212
|
-
<label>
|
|
213
|
-
<input
|
|
214
|
-
type="checkbox"
|
|
215
|
-
checked={header}
|
|
216
|
-
onChange={(e) => setParam("header", e.target.checked ? "" : "false")}
|
|
217
|
-
/>{" "}
|
|
218
|
-
header row
|
|
219
|
-
</label>
|
|
220
|
-
</div>
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** A CSV chunk embedded inline in a chapter: just the table, auto-detecting the
|
|
225
|
-
* separator and treating the first row as a header (no per-chunk URL controls). */
|
|
226
|
-
export function CsvChunk({ chunk }: { chunk: Chunk }) {
|
|
227
|
-
const text = String(chunk.value ?? "");
|
|
228
|
-
const rows = parseDelimited(text, autoSep(text, chunk.format ?? null));
|
|
229
|
-
return (
|
|
230
|
-
<div className="csv">
|
|
231
|
-
<Table rows={rows} header />
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { NodeJson, blobUrl } from "../api";
|
|
3
|
-
import { PanZoomImage } from "./imagemap";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Shared scaffold for the file formats the browser cannot display natively but
|
|
7
|
-
* that we can decode client-side to ordinary raster images (PSD, TIFF, HEIC).
|
|
8
|
-
* It fetches the node's bytes from `/api/blob`, hands them to a format-specific
|
|
9
|
-
* `decode` that returns one PNG `Blob` per page, and shows each in the same
|
|
10
|
-
* pan/zoom viewer as a native image (each decoded page is its own object-URL).
|
|
11
|
-
* Object-URLs are revoked on unmount / path change so decoded pages don't leak.
|
|
12
|
-
*/
|
|
13
|
-
export function DecodedImageView({
|
|
14
|
-
node,
|
|
15
|
-
label,
|
|
16
|
-
decode,
|
|
17
|
-
}: {
|
|
18
|
-
node: NodeJson;
|
|
19
|
-
label: string;
|
|
20
|
-
decode: (buf: ArrayBuffer) => Promise<Blob[]>;
|
|
21
|
-
}) {
|
|
22
|
-
const [urls, setUrls] = useState<string[]>([]);
|
|
23
|
-
const [error, setError] = useState<string | null>(null);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
let cancelled = false;
|
|
27
|
-
const created: string[] = [];
|
|
28
|
-
setUrls([]);
|
|
29
|
-
setError(null);
|
|
30
|
-
(async () => {
|
|
31
|
-
const buf = await fetch(blobUrl(node.path)).then((r) => r.arrayBuffer());
|
|
32
|
-
const blobs = await decode(buf);
|
|
33
|
-
if (cancelled) return;
|
|
34
|
-
for (const b of blobs) created.push(URL.createObjectURL(b));
|
|
35
|
-
if (!cancelled) setUrls(created);
|
|
36
|
-
})().catch((e) => !cancelled && setError(String((e as Error).message || e)));
|
|
37
|
-
return () => {
|
|
38
|
-
cancelled = true;
|
|
39
|
-
created.forEach(URL.revokeObjectURL);
|
|
40
|
-
};
|
|
41
|
-
}, [node.path]);
|
|
42
|
-
|
|
43
|
-
if (error) return <div className="error">{label}: {error}</div>;
|
|
44
|
-
if (!urls.length) return <div className="loading">decoding {label}…</div>;
|
|
45
|
-
return (
|
|
46
|
-
<>
|
|
47
|
-
{urls.map((url, i) => (
|
|
48
|
-
<PanZoomImage key={i} src={url} className="filemap fileimagemap" />
|
|
49
|
-
))}
|
|
50
|
-
</>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Paint RGBA pixels onto a canvas and export it as a PNG blob. */
|
|
55
|
-
export async function rgbaToPng(rgba: Uint8ClampedArray | Uint8Array, width: number, height: number): Promise<Blob> {
|
|
56
|
-
const canvas = document.createElement("canvas");
|
|
57
|
-
canvas.width = width;
|
|
58
|
-
canvas.height = height;
|
|
59
|
-
const ctx = canvas.getContext("2d");
|
|
60
|
-
if (!ctx) throw new Error("no 2d context");
|
|
61
|
-
const img = ctx.createImageData(width, height);
|
|
62
|
-
img.data.set(rgba);
|
|
63
|
-
ctx.putImageData(img, 0, 0);
|
|
64
|
-
return canvasToPng(canvas);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Export a canvas as a PNG blob (Promise wrapper over the callback API). */
|
|
68
|
-
export function canvasToPng(canvas: HTMLCanvasElement): Promise<Blob> {
|
|
69
|
-
return new Promise((resolve, reject) =>
|
|
70
|
-
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("canvas export failed"))), "image/png"),
|
|
71
|
-
);
|
|
72
|
-
}
|