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,30 +0,0 @@
|
|
|
1
|
-
# Vendored: DjVu.js
|
|
2
|
-
|
|
3
|
-
`djvu.js` is a prebuilt bundle of the **DjVu.js library** by Roman Chistokhodov
|
|
4
|
-
(RussCoder), used by the `djvu` renderer to decode `.djvu` documents in the
|
|
5
|
-
browser (the browser has no native DjVu support).
|
|
6
|
-
|
|
7
|
-
- Upstream: https://github.com/RussCoder/djvujs — https://djvu.js.org
|
|
8
|
-
- Version: 0.5.4
|
|
9
|
-
- **License: GNU GPL v2** (the library only; the upstream viewer/extension are
|
|
10
|
-
Unlicense). By bundling it here, the distributed client includes GPL-v2 code.
|
|
11
|
-
See https://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
|
|
12
|
-
|
|
13
|
-
## Why it's vendored
|
|
14
|
-
|
|
15
|
-
The npm package `djvujs-dist` ships *source*, not an importable module (no
|
|
16
|
-
`main`/`module`/`types`), and pulls a large dev toolchain. We build its IIFE
|
|
17
|
-
bundle once and commit the single output here instead of depending on the
|
|
18
|
-
package at runtime.
|
|
19
|
-
|
|
20
|
-
## Regenerating
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
npm install djvujs-dist # temporary
|
|
24
|
-
cd node_modules/djvujs-dist/library && npm run build
|
|
25
|
-
cp dist/djvu.js <repo>/tools/server/src/client/vendor/djvu.js
|
|
26
|
-
npm uninstall djvujs-dist
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The bundle defines a global `DjVu` (`var DjVu = (function(){…})()`); the renderer
|
|
30
|
-
loads it as a classic `<script>` and uses `new DjVu.Document(arrayBuffer)`.
|
package/src/client/vite-env.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
|
-
// UTIF.js ships no types. Declare the minimal surface our TIFF renderer uses:
|
|
4
|
-
// decode the container into IFDs, decode each IFD's pixels in place, then read it
|
|
5
|
-
// back as RGBA. See https://github.com/photopea/UTIF.js.
|
|
6
|
-
declare module "utif" {
|
|
7
|
-
interface IFD {
|
|
8
|
-
width?: number;
|
|
9
|
-
height?: number;
|
|
10
|
-
[key: string]: unknown;
|
|
11
|
-
}
|
|
12
|
-
const UTIF: {
|
|
13
|
-
decode(buf: ArrayBuffer | Uint8Array): IFD[];
|
|
14
|
-
decodeImage(buf: ArrayBuffer | Uint8Array, ifd: IFD): void;
|
|
15
|
-
toRGBA8(ifd: IFD): Uint8Array;
|
|
16
|
-
};
|
|
17
|
-
export default UTIF;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// mammoth's browser build ships no types. Declare the one call our docx renderer
|
|
21
|
-
// makes: convert a .docx ArrayBuffer to an HTML string. See https://github.com/mwilliamson/mammoth.js.
|
|
22
|
-
declare module "mammoth/mammoth.browser" {
|
|
23
|
-
interface ConvertResult {
|
|
24
|
-
value: string;
|
|
25
|
-
messages: { type: string; message: string }[];
|
|
26
|
-
}
|
|
27
|
-
const mammoth: {
|
|
28
|
-
convertToHtml(input: { arrayBuffer: ArrayBuffer }): Promise<ConvertResult>;
|
|
29
|
-
};
|
|
30
|
-
export default mammoth;
|
|
31
|
-
}
|
package/src/server/api.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* api.ts — the JSON API the SPA calls.
|
|
3
|
-
*
|
|
4
|
-
* Endpoints, all keyed by a JSON-space `path` (`/key[0]/sub`, no "properties"):
|
|
5
|
-
*
|
|
6
|
-
* GET /api/tree the table of contents (containers only)
|
|
7
|
-
* GET /api/json?path&depth the node's value as JSON
|
|
8
|
-
* GET /api/schema?path&depth&format the node's instance schema (json | yaml)
|
|
9
|
-
* GET /api/blob?path a file-backed node's raw bytes (for the
|
|
10
|
-
* image / pdf / html / djvu / markup renderers)
|
|
11
|
-
*
|
|
12
|
-
* The materialized tree is cached per server for a short window (leaf bytes load
|
|
13
|
-
* lazily within it), so a burst of clicks does not re-read the filesystem; edits
|
|
14
|
-
* show up after the cache window on reload.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import fs from "node:fs";
|
|
19
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
20
|
-
import {
|
|
21
|
-
loadEntity,
|
|
22
|
-
getNode,
|
|
23
|
-
strToSegs,
|
|
24
|
-
buildTree,
|
|
25
|
-
labelForSeg,
|
|
26
|
-
toPlain,
|
|
27
|
-
toSchema,
|
|
28
|
-
buildRelations,
|
|
29
|
-
binaryContent,
|
|
30
|
-
displayTypeLabel,
|
|
31
|
-
documentPath,
|
|
32
|
-
formatFromExt,
|
|
33
|
-
setIgnoreFilter,
|
|
34
|
-
YNode,
|
|
35
|
-
} from "./yamlover.js";
|
|
36
|
-
import { buildGitIgnore } from "./gitignore.js";
|
|
37
|
-
|
|
38
|
-
type Handler = (req: IncomingMessage, res: ServerResponse, url: URL) => void;
|
|
39
|
-
|
|
40
|
-
interface Options {
|
|
41
|
-
gitignore?: boolean; // honor .gitignore for stray files (default: true)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// The materialized tree is cached this long. With lazy directory loading the
|
|
45
|
-
// cached tree *accumulates* the levels you browse (each read once and memoized),
|
|
46
|
-
// so a longer window keeps navigation warm; edits show after it on reload.
|
|
47
|
-
const CACHE_TTL_MS = 15000;
|
|
48
|
-
|
|
49
|
-
export function createHandlers(dataRoot: string, opts: Options = {}): Handler {
|
|
50
|
-
// The root label (breadcrumb head and TOC root): the yamlover title if set,
|
|
51
|
-
// else the served directory's name — always non-empty.
|
|
52
|
-
const rootName = path.basename(path.resolve(dataRoot)) || "/";
|
|
53
|
-
setIgnoreFilter(opts.gitignore === false ? () => false : buildGitIgnore(dataRoot));
|
|
54
|
-
|
|
55
|
-
let cache: { root: YNode; time: number } | null = null;
|
|
56
|
-
const root = (): YNode => {
|
|
57
|
-
const now = Date.now();
|
|
58
|
-
if (!cache || now - cache.time > CACHE_TTL_MS) cache = { root: loadEntity(dataRoot), time: now };
|
|
59
|
-
return cache.root;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return (_req, res, url) => {
|
|
63
|
-
try {
|
|
64
|
-
const segs = strToSegs(url.searchParams.get("path") || "/");
|
|
65
|
-
const depth = parseDepth(url.searchParams.get("depth"));
|
|
66
|
-
|
|
67
|
-
if (url.pathname === "/api/info") {
|
|
68
|
-
sendJson(res, 200, { root: root().title || rootName }); // breadcrumb head
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (url.pathname === "/api/tree") {
|
|
73
|
-
// The TOC loads lazily: a subtree rooted at `path`, `depth` levels deep
|
|
74
|
-
// (default 3). Past the boundary, nodes carry hasChildren for the client
|
|
75
|
-
// to fetch on expand.
|
|
76
|
-
const start = getNode(root(), segs);
|
|
77
|
-
const label = segs.length === 0 ? start.title || rootName : labelForSeg(start, segs[segs.length - 1]);
|
|
78
|
-
sendJson(res, 200, buildTree(start, segs, label, depth ?? 3));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (url.pathname === "/api/blob") {
|
|
83
|
-
// Raw file bytes, served with the node's (inferred) format as the
|
|
84
|
-
// Content-Type, so a renderer can point an <img>/<iframe>/pdf loader at
|
|
85
|
-
// the URL — or fetch the text of a markdown/asciidoc file — directly.
|
|
86
|
-
const n = getNode(root(), segs);
|
|
87
|
-
if (!n.path || !fs.existsSync(n.path) || fs.statSync(n.path).isDirectory()) {
|
|
88
|
-
sendJson(res, 404, { error: `not a file-backed node: ${url.searchParams.get("path")}` });
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const data = fs.readFileSync(n.path);
|
|
92
|
-
res.statusCode = 200;
|
|
93
|
-
res.setHeader("Content-Type", n.format ?? formatFromExt(n.path) ?? "application/octet-stream");
|
|
94
|
-
res.setHeader("Content-Length", String(data.length));
|
|
95
|
-
res.end(data);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const node = getNode(root(), segs);
|
|
100
|
-
// display type: a virtual-children overlay reads as `object` (matches the TOC)
|
|
101
|
-
const type = displayTypeLabel(node);
|
|
102
|
-
// Every representation is one level deep with nested containers as links;
|
|
103
|
-
// the client picks the YAML/JSON syntax. `depth` defaults to 1.
|
|
104
|
-
const viewDepth = depth ?? 1;
|
|
105
|
-
|
|
106
|
-
if (url.pathname === "/api/json") {
|
|
107
|
-
// A binary leaf's bytes load only on explicit request (?binary=1) — the
|
|
108
|
-
// selection's header fetch shows its cheap `repr` (size) via toPlain.
|
|
109
|
-
const wantBytes = type === "binary" && url.searchParams.get("binary") === "1";
|
|
110
|
-
sendJson(res, 200, {
|
|
111
|
-
path: url.searchParams.get("path") || "/",
|
|
112
|
-
type,
|
|
113
|
-
format: node.format ?? null, // with `type`, the (type, format) renderer key
|
|
114
|
-
concrete: node.concrete,
|
|
115
|
-
// the document (nearest yamlover entity) this node belongs to — the anchor
|
|
116
|
-
// a document-relative (`/…`) marklower link resolves against
|
|
117
|
-
documentPath: documentPath(root(), segs),
|
|
118
|
-
title: node.title ?? null,
|
|
119
|
-
description: node.description ?? null,
|
|
120
|
-
// virtual children are merged into the value; named up-edges (+ `..`)
|
|
121
|
-
// ride alongside as `relations` for the panel above the divider
|
|
122
|
-
value: wantBytes ? binaryContent(node) : toPlain(node, viewDepth, segs, true, root()),
|
|
123
|
-
relations: buildRelations(node, segs, root()),
|
|
124
|
-
});
|
|
125
|
-
} else if (url.pathname === "/api/schema") {
|
|
126
|
-
// pass the real root so a node's rel pointers (esp. absolute /…) resolve
|
|
127
|
-
sendJson(res, 200, toSchema(node, viewDepth, segs, true, root()));
|
|
128
|
-
} else {
|
|
129
|
-
sendJson(res, 404, { error: `no such endpoint: ${url.pathname}` });
|
|
130
|
-
}
|
|
131
|
-
} catch (exc) {
|
|
132
|
-
sendJson(res, 400, { error: (exc as Error).message || String(exc) });
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function parseDepth(raw: string | null): number | null {
|
|
138
|
-
if (raw == null || raw === "") return null;
|
|
139
|
-
const n = Number(raw);
|
|
140
|
-
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
144
|
-
res.statusCode = status;
|
|
145
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
146
|
-
res.end(JSON.stringify(body, null, 2));
|
|
147
|
-
}
|
package/src/server/embed.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* embed.ts — surgical, indentation-aware edits that EMBED fragments and annotations into a
|
|
3
|
-
* yamlover host body (a standalone `*.yamlover` document, or a directory's
|
|
4
|
-
* `.yamlover/body.yamlover` overlay). Pure string→string transforms, like the chapter-list
|
|
5
|
-
* insertion in engine-api.ts — the parser tracks no spans, so we edit the source text directly,
|
|
6
|
-
* preserving the rest of the file (comments, formatting). See ANNOTATIONS.md.
|
|
7
|
-
*
|
|
8
|
-
* A host body is YAML-shaped: a mapping's keys at one indent; a sequence's `- ` items at the
|
|
9
|
-
* SAME indent as their key; an item/value body 2 deeper. We descend a `within` path of mapping
|
|
10
|
-
* KEYS (creating any that are absent, as empty blocks) to reach a target node, then:
|
|
11
|
-
* • append an element to its `yamlover-annotations:` sequence (creating the key), or
|
|
12
|
-
* • upsert a `<slug>:` entry into its `yamlover-fragments:` mapping (creating the key).
|
|
13
|
-
*
|
|
14
|
-
* Index (sequence-position) descent — e.g. tagging a chapter CHUNK, which would turn its
|
|
15
|
-
* block-scalar into an omni node — is intentionally NOT handled here; the server resolves such a
|
|
16
|
-
* target to a key-addressable host. Keep this module free of fs / Store coupling so it unit-tests
|
|
17
|
-
* in isolation (see test/embed.test.ts).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const ANNOTATIONS_KEY = "yamlover-annotations";
|
|
21
|
-
const FRAGMENTS_KEY = "yamlover-fragments";
|
|
22
|
-
|
|
23
|
-
const indentOf = (line: string): number => { let i = 0; while (line[i] === " ") i++; return i; };
|
|
24
|
-
const isContentLine = (line: string): boolean => { const t = line.trim(); return t.length > 0 && !t.startsWith("#"); };
|
|
25
|
-
|
|
26
|
-
/** The indent of the first content line — the top mapping's key column (0 for most bodies). */
|
|
27
|
-
function firstContentIndent(lines: string[]): number {
|
|
28
|
-
for (const l of lines) if (isContentLine(l)) return indentOf(l);
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A yamlover plain/quoted key token: bare when it is a safe plain scalar, else double-quoted
|
|
33
|
-
* (JSON escapes — the subset the parser reads back). Mirrors how filenames with dots/spaces are
|
|
34
|
-
* authored as overlay keys (e.g. `"S0002-9904.pdf":`). */
|
|
35
|
-
export function keyToken(key: string): string {
|
|
36
|
-
return /^[A-Za-z0-9_][A-Za-z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Line index of `key:` at exactly `indent` within [lo,hi); -1 once the mapping ends (a dedent
|
|
40
|
-
* to a shallower content line). Skips deeper lines (a nested value / block scalar). */
|
|
41
|
-
function findKeyLine(lines: string[], lo: number, hi: number, indent: number, key: string): number {
|
|
42
|
-
const tok = keyToken(key);
|
|
43
|
-
for (let i = lo; i < hi; i++) {
|
|
44
|
-
if (!isContentLine(lines[i])) continue;
|
|
45
|
-
const ind = indentOf(lines[i]);
|
|
46
|
-
if (ind < indent) return -1; // left the mapping
|
|
47
|
-
if (ind !== indent) continue; // deeper — a nested value
|
|
48
|
-
const t = lines[i].trim();
|
|
49
|
-
if (t === `${key}:` || t.startsWith(`${key}: `) || t === `${tok}:` || t.startsWith(`${tok}: `)) return i;
|
|
50
|
-
}
|
|
51
|
-
return -1;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Walk `end` back over trailing blank lines, so an insert lands right after the last content. */
|
|
55
|
-
function trimBack(lines: string[], floor: number, end: number): number {
|
|
56
|
-
let e = end;
|
|
57
|
-
while (e > floor + 1 && !isContentLine(lines[e - 1])) e--;
|
|
58
|
-
return e;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The end (exclusive) of the block owned by the content starting at `from`, whose own lines sit
|
|
62
|
-
* at >= `indent`: the first later content line shallower than `indent`, sans trailing blanks. */
|
|
63
|
-
function blockEnd(lines: string[], from: number, hi: number, indent: number): number {
|
|
64
|
-
let last = from;
|
|
65
|
-
for (let i = from; i < hi; i++) {
|
|
66
|
-
if (!isContentLine(lines[i])) continue;
|
|
67
|
-
if (indentOf(lines[i]) < indent) return trimBack(lines, last, i);
|
|
68
|
-
last = i;
|
|
69
|
-
}
|
|
70
|
-
return trimBack(lines, last, hi);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Region { lo: number; hi: number; indent: number } // a mapping body: child keys at `indent`, within [lo,hi)
|
|
74
|
-
|
|
75
|
-
/** Descend the mapping-KEY path `within` to the target node's body region, CREATING any missing
|
|
76
|
-
* key as an empty block (so a fresh overlay grows the `"file":` → `yamlover-fragments:` →
|
|
77
|
-
* `<slug>:` spine on demand). Mutates `lines` in place; returns the region under the last key. */
|
|
78
|
-
function reachBody(lines: string[], within: string[]): Region {
|
|
79
|
-
let lo = 0;
|
|
80
|
-
let hi = lines.length;
|
|
81
|
-
let indent = firstContentIndent(lines);
|
|
82
|
-
if (lines.length === 1 && lines[0] === "") { lines.length = 0; hi = 0; indent = 0; } // empty file
|
|
83
|
-
|
|
84
|
-
for (const key of within) {
|
|
85
|
-
const L = findKeyLine(lines, lo, hi, indent, key);
|
|
86
|
-
if (L < 0) {
|
|
87
|
-
const at = trimBack(lines, lo - 1, hi); // append the new key at the end of the current body
|
|
88
|
-
lines.splice(at, 0, `${" ".repeat(indent)}${keyToken(key)}:`);
|
|
89
|
-
lo = at + 1; hi = at + 1; indent += 2; // its (empty) body
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const inline = lines[L].slice(indentOf(lines[L])).slice(`${lines[L].trim().split(":")[0]}:`.length);
|
|
93
|
-
const bodyLo = L + 1;
|
|
94
|
-
const bodyHi = blockEnd(lines, bodyLo, hi, indent + 1); // anything deeper than the key
|
|
95
|
-
// the child key column: a deeper content line's indent if the node already has a block body,
|
|
96
|
-
// else key-indent + 2 (a leaf/inline value gains its first field there — an omni node).
|
|
97
|
-
let childIndent = indent + 2;
|
|
98
|
-
for (let i = bodyLo; i < bodyHi; i++) {
|
|
99
|
-
if (isContentLine(lines[i]) && indentOf(lines[i]) > indent) { childIndent = indentOf(lines[i]); break; }
|
|
100
|
-
}
|
|
101
|
-
void inline;
|
|
102
|
-
lo = bodyLo; hi = bodyHi; indent = childIndent;
|
|
103
|
-
}
|
|
104
|
-
return { lo, hi, indent };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Start lines of the `- ` items of a sequence whose key sits at `indent` (items at the same
|
|
108
|
-
* indent), scanning the region body for the `key:` then its items. */
|
|
109
|
-
function seqItemLines(lines: string[], region: Region, key: string): { keyLine: number; items: number[]; end: number } | null {
|
|
110
|
-
const keyLine = findKeyLine(lines, region.lo, region.hi, region.indent, key);
|
|
111
|
-
if (keyLine < 0) return null;
|
|
112
|
-
const items: number[] = [];
|
|
113
|
-
let end = keyLine + 1;
|
|
114
|
-
for (let i = keyLine + 1; i < region.hi; i++) {
|
|
115
|
-
if (!isContentLine(lines[i])) continue;
|
|
116
|
-
const ind = indentOf(lines[i]);
|
|
117
|
-
if (ind < region.indent) break;
|
|
118
|
-
if (ind === region.indent) {
|
|
119
|
-
const t = lines[i].trim();
|
|
120
|
-
if (t === "-" || t.startsWith("- ")) { items.push(i); end = i; continue; }
|
|
121
|
-
break; // a sibling key at the list indent → the sequence ended
|
|
122
|
-
}
|
|
123
|
-
end = i; // deeper — the current item's body
|
|
124
|
-
}
|
|
125
|
-
return { keyLine, items, end: trimBack(lines, end, region.hi) };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Append one annotation element (rendered at the list indent) to the `yamlover-annotations:`
|
|
129
|
-
* sequence of the node addressed by `within`, creating the key (and any missing path) if absent.
|
|
130
|
-
* `render(indent)` returns the element's source lines (a `- *…tag` item, or a `- {…}` object). */
|
|
131
|
-
export function appendAnnotation(text: string, within: string[], render: (indent: number) => string[]): string {
|
|
132
|
-
const lines = text.replace(/\n$/, "").split("\n");
|
|
133
|
-
const region = reachBody(lines, within);
|
|
134
|
-
const seq = seqItemLines(lines, region, ANNOTATIONS_KEY);
|
|
135
|
-
if (!seq) {
|
|
136
|
-
const at = trimBack(lines, region.lo - 1, region.hi);
|
|
137
|
-
lines.splice(at, 0, `${" ".repeat(region.indent)}${ANNOTATIONS_KEY}:`, ...render(region.indent));
|
|
138
|
-
} else {
|
|
139
|
-
lines.splice(seq.end, 0, ...render(region.indent));
|
|
140
|
-
}
|
|
141
|
-
return lines.join("\n") + "\n";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Upsert a `<slug>:` entry into the `yamlover-fragments:` mapping of the node addressed by
|
|
145
|
-
* `within`, creating the key (and any missing path) if absent. `render(indent)` returns the
|
|
146
|
-
* fragment's source lines INCLUDING the `<slug>:` line, at the mapping's child indent. A slug
|
|
147
|
-
* that already exists is REPLACED (its whole block). */
|
|
148
|
-
export function upsertFragment(text: string, within: string[], slug: string, render: (indent: number) => string[]): string {
|
|
149
|
-
const lines = text.replace(/\n$/, "").split("\n");
|
|
150
|
-
const region = reachBody(lines, within);
|
|
151
|
-
let fragKey = findKeyLine(lines, region.lo, region.hi, region.indent, FRAGMENTS_KEY);
|
|
152
|
-
if (fragKey < 0) {
|
|
153
|
-
const at = trimBack(lines, region.lo - 1, region.hi);
|
|
154
|
-
lines.splice(at, 0, `${" ".repeat(region.indent)}${FRAGMENTS_KEY}:`);
|
|
155
|
-
fragKey = at;
|
|
156
|
-
}
|
|
157
|
-
const mapIndent = region.indent + 2;
|
|
158
|
-
const mapBody: Region = { lo: fragKey + 1, hi: blockEnd(lines, fragKey + 1, lines.length, region.indent + 1), indent: mapIndent };
|
|
159
|
-
const existing = findKeyLine(lines, mapBody.lo, mapBody.hi, mapIndent, slug);
|
|
160
|
-
if (existing >= 0) {
|
|
161
|
-
const end = blockEnd(lines, existing + 1, mapBody.hi, mapIndent + 1);
|
|
162
|
-
lines.splice(existing, end - existing, ...render(mapIndent));
|
|
163
|
-
} else {
|
|
164
|
-
const at = trimBack(lines, fragKey, mapBody.hi);
|
|
165
|
-
lines.splice(at, 0, ...render(mapIndent));
|
|
166
|
-
}
|
|
167
|
-
return lines.join("\n") + "\n";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Remove an annotation element from the `yamlover-annotations:` of the node at `within` — the
|
|
171
|
-
* first `- ` item whose trimmed text matches `predicate`. Returns the text unchanged if none
|
|
172
|
-
* matches. The block of a multi-line object item is removed whole. */
|
|
173
|
-
export function removeAnnotation(text: string, within: string[], predicate: (itemText: string) => boolean): string {
|
|
174
|
-
const lines = text.replace(/\n$/, "").split("\n");
|
|
175
|
-
const region = reachBody(lines, within);
|
|
176
|
-
const seq = seqItemLines(lines, region, ANNOTATIONS_KEY);
|
|
177
|
-
if (!seq) return text;
|
|
178
|
-
for (let k = 0; k < seq.items.length; k++) {
|
|
179
|
-
const i = seq.items[k];
|
|
180
|
-
const itemText = lines[i].trim().replace(/^-\s*/, "");
|
|
181
|
-
if (!predicate(itemText)) continue;
|
|
182
|
-
const next = k + 1 < seq.items.length ? seq.items[k + 1] : seq.end;
|
|
183
|
-
lines.splice(i, next - i);
|
|
184
|
-
return lines.join("\n") + "\n";
|
|
185
|
-
}
|
|
186
|
-
return text;
|
|
187
|
-
}
|