yamlover 0.3.0

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/bin/yamlover.js +202 -0
  4. package/dist/server.js +4681 -0
  5. package/index.html +12 -0
  6. package/package.json +72 -0
  7. package/src/client/App.tsx +372 -0
  8. package/src/client/NodeView.tsx +422 -0
  9. package/src/client/TaskStrip.tsx +34 -0
  10. package/src/client/Tree.tsx +97 -0
  11. package/src/client/api.ts +186 -0
  12. package/src/client/icons.ts +91 -0
  13. package/src/client/links.tsx +108 -0
  14. package/src/client/live.ts +42 -0
  15. package/src/client/main.tsx +10 -0
  16. package/src/client/paste-html.ts +228 -0
  17. package/src/client/paste-links.ts +42 -0
  18. package/src/client/paths.ts +109 -0
  19. package/src/client/render.tsx +326 -0
  20. package/src/client/renderers/annotate.tsx +507 -0
  21. package/src/client/renderers/asciidoc.tsx +35 -0
  22. package/src/client/renderers/chapter.tsx +138 -0
  23. package/src/client/renderers/csv.tsx +233 -0
  24. package/src/client/renderers/decoded.tsx +72 -0
  25. package/src/client/renderers/djvu.tsx +97 -0
  26. package/src/client/renderers/doc.tsx +40 -0
  27. package/src/client/renderers/docx.tsx +49 -0
  28. package/src/client/renderers/epub.tsx +147 -0
  29. package/src/client/renderers/explorer.tsx +209 -0
  30. package/src/client/renderers/fb2.tsx +149 -0
  31. package/src/client/renderers/headings.ts +69 -0
  32. package/src/client/renderers/heic.tsx +23 -0
  33. package/src/client/renderers/imagemap.tsx +157 -0
  34. package/src/client/renderers/kml.ts +46 -0
  35. package/src/client/renderers/latex.tsx +36 -0
  36. package/src/client/renderers/map.tsx +205 -0
  37. package/src/client/renderers/marklower.tsx +119 -0
  38. package/src/client/renderers/markup.tsx +64 -0
  39. package/src/client/renderers/media.tsx +19 -0
  40. package/src/client/renderers/panzoom.ts +101 -0
  41. package/src/client/renderers/pdf.tsx +176 -0
  42. package/src/client/renderers/plaintext.tsx +120 -0
  43. package/src/client/renderers/plantuml.tsx +82 -0
  44. package/src/client/renderers/psd.tsx +25 -0
  45. package/src/client/renderers/registry.tsx +389 -0
  46. package/src/client/renderers/rtf.tsx +210 -0
  47. package/src/client/renderers/spreadsheet.tsx +105 -0
  48. package/src/client/renderers/tag.tsx +113 -0
  49. package/src/client/renderers/text.tsx +41 -0
  50. package/src/client/renderers/tiff.tsx +33 -0
  51. package/src/client/styles.css +1115 -0
  52. package/src/client/vendor/README.md +30 -0
  53. package/src/client/vendor/djvu.js +15535 -0
  54. package/src/client/vite-env.d.ts +31 -0
  55. package/src/server/api.ts +147 -0
  56. package/src/server/engine-api.ts +1442 -0
  57. package/src/server/gitignore.ts +81 -0
  58. package/src/server/node-kind.ts +48 -0
  59. package/src/server/tasks.ts +83 -0
  60. package/src/server/yamlover.ts +1133 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * gitignore.ts — a predicate telling whether an absolute path is git-ignored.
3
+ *
4
+ * It honors `.gitignore` files from the repository root down to the file's
5
+ * parent (nested `.gitignore`s included), plus an always-ignored `.git`. Built
6
+ * once per server and handed to the materializer (see `setIgnoreFilter`), it only
7
+ * filters *undescribed* (stray) entries — schema-described children are always
8
+ * kept.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import ignore, { type Ignore } from "ignore";
14
+
15
+ export function buildGitIgnore(dataRoot: string): (absPath: string) => boolean {
16
+ const gitRoot = findGitRoot(dataRoot) ?? dataRoot;
17
+ const cache = new Map<string, Ignore | null>(); // dir → its .gitignore matcher
18
+
19
+ const matcherFor = (dir: string): Ignore | null => {
20
+ if (!cache.has(dir)) {
21
+ let ig: Ignore | null = null;
22
+ try {
23
+ const gi = path.join(dir, ".gitignore");
24
+ if (fs.statSync(gi).isFile()) ig = ignore().add(fs.readFileSync(gi, "utf-8"));
25
+ } catch {
26
+ /* no .gitignore here */
27
+ }
28
+ cache.set(dir, ig);
29
+ }
30
+ return cache.get(dir)!;
31
+ };
32
+
33
+ return (absPath: string): boolean => {
34
+ const fromRoot = path.relative(gitRoot, absPath);
35
+ if (fromRoot === ".git" || fromRoot.startsWith(".git" + path.sep)) return true;
36
+
37
+ // A directory must be tested with a trailing slash, otherwise a `dir/`
38
+ // pattern (e.g. `node_modules/`) does not match the bare name.
39
+ let isDir = false;
40
+ try {
41
+ isDir = fs.statSync(absPath).isDirectory();
42
+ } catch {
43
+ /* gone */
44
+ }
45
+
46
+ // Test the path against every .gitignore from the repo root down to its
47
+ // parent directory; each file's patterns are relative to its own location.
48
+ const rel = path.relative(gitRoot, path.dirname(absPath));
49
+ const segs = rel === "" ? [] : rel.split(path.sep);
50
+ let dir = gitRoot;
51
+ const dirs = [gitRoot];
52
+ for (const s of segs) {
53
+ dir = path.join(dir, s);
54
+ dirs.push(dir);
55
+ }
56
+ for (const d of dirs) {
57
+ const ig = matcherFor(d);
58
+ if (!ig) continue;
59
+ const relToDir = path.relative(d, absPath);
60
+ if (!relToDir) continue;
61
+ if (ig.ignores(relToDir) || (isDir && ig.ignores(relToDir + "/"))) return true;
62
+ }
63
+ return false;
64
+ };
65
+ }
66
+
67
+ function findGitRoot(start: string): string | null {
68
+ let dir = path.resolve(start);
69
+ // If start is a file, begin at its directory.
70
+ try {
71
+ if (!fs.statSync(dir).isDirectory()) dir = path.dirname(dir);
72
+ } catch {
73
+ return null;
74
+ }
75
+ for (;;) {
76
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
77
+ const parent = path.dirname(dir);
78
+ if (parent === dir) return null;
79
+ dir = parent;
80
+ }
81
+ }
@@ -0,0 +1,48 @@
1
+ // Node-KIND classification — how a Store node is presented (the `type:` the client routes on). Kept
2
+ // in its own module (no http/fs/gitignore deps) so it can be unit-tested under `node --test` against
3
+ // a Store, independently of the HTTP layer (engine-api.ts), which only Vite can load (node:sqlite).
4
+ import type { NodeRow, Store } from "../../../engine/ts/src/index.ts";
5
+
6
+ // One ordered container, classified for display: a pure-keyed mapping is `object`, a pure-keyless
7
+ // one `array`; a mapping mixing keyed + keyless OWNED entries is `mix`; a scalar/blob that ALSO
8
+ // carries OWNED fields is `omni` (the `!!mix`/`!!omni` shapes); plain scalars/blobs are
9
+ // `scalar`/`binary`.
10
+ export type Kind = "object" | "array" | "scalar" | "binary" | "omni" | "mix";
11
+
12
+ /** A node's OWNED entries — the ones it authors, that constitute its content: containment children
13
+ * and forward `*` refs. A `~` back-edge (a REVERSE member, e.g. tag membership) is an upstream
14
+ * relation the node does NOT own, so it is excluded — it must not change the node's type. */
15
+ export function ownedEntries(s: Store, p: string): ReturnType<Store["entries"]> {
16
+ return s.entries(p).filter((e) => e.kind !== "back");
17
+ }
18
+
19
+ /** A node's display {@link Kind}. A scalar/blob carrying OWNED fields is `omni`; a mapping that
20
+ * mixes keyed and keyless OWNED entries is `mix`; otherwise object|array|scalar|binary. The
21
+ * `is_array` flag marks a pure-keyless container. Reverse (`~`) members never count — a tagged PDF
22
+ * is still a `binary`, not an `omni` (they are upstream relations, not owned content). */
23
+ export function displayKind(s: Store, p: string, row: NodeRow): Kind {
24
+ const ents = ownedEntries(s, p);
25
+ if (row.type === "blob") return ents.length ? "omni" : "binary";
26
+ if (row.type === "scalar") return ents.length ? "omni" : "scalar";
27
+ if (!ents.length) return row.is_array ? "array" : "object"; // empty container
28
+ if (row.is_array) return "array";
29
+ return ents.some((e) => e.label === null) ? "mix" : "object";
30
+ }
31
+
32
+ // Internal kind → the JSON-Schema-style `type:` name shown in the header/TOC and the schema view.
33
+ // The YAML-tag shapes `!!mix`/`!!omni` get full-word schema names (cf. !!seq→array, !!map→object):
34
+ // `mix` → "mixed", `omni` → "variant". Scalars resolve to their JSON-ish primitive type.
35
+ export function typeName(s: Store, p: string, row: NodeRow): string {
36
+ const k = displayKind(s, p, row);
37
+ if (k === "scalar") return scalarType(row.value);
38
+ if (k === "mix") return "mixed";
39
+ if (k === "omni") return "variant";
40
+ return k; // object | array | binary
41
+ }
42
+
43
+ export function scalarType(v: unknown): string {
44
+ if (v === null) return "null";
45
+ if (typeof v === "boolean") return "boolean";
46
+ if (typeof v === "number") return Number.isInteger(v) ? "integer" : "number";
47
+ return "string";
48
+ }
@@ -0,0 +1,83 @@
1
+ // tasks.ts — the server's LONG-RUNNING TASK registry. Anything slow (the initial index, the
2
+ // background hasher, a watcher reconcile, …) registers here and reports progress; every state
3
+ // change is emitted (engine-api pushes it to the UI over the existing /api/events SSE stream
4
+ // as a `{type:"task", task}` frame) and GET /api/tasks snapshots in-flight tasks for a freshly
5
+ // loaded page. Generic on purpose: a future slow operation is just `registry.start(label)`.
6
+
7
+ export interface TaskProgress {
8
+ done: number;
9
+ total?: number; // absent ⇒ indeterminate (the UI shows an activity bar, not a percent)
10
+ message?: string; // human-readable detail (current path, "writing index…", …)
11
+ }
12
+
13
+ export interface TaskInfo {
14
+ id: string;
15
+ label: string;
16
+ state: "running" | "done" | "error";
17
+ progress: TaskProgress;
18
+ startedAt: number; // epoch ms
19
+ finishedAt?: number;
20
+ error?: string;
21
+ }
22
+
23
+ export interface TaskHandle {
24
+ readonly id: string;
25
+ /** Report progress. Throttled (state changes are not) so a per-file caller cannot flood SSE. */
26
+ progress(done: number, total?: number, message?: string): void;
27
+ done(): void;
28
+ fail(err: unknown): void;
29
+ }
30
+
31
+ const PROGRESS_EMIT_MS = 150; // at most one progress frame per task per this interval
32
+ const KEEP_FINISHED_MS = 5_000; // finished tasks stay listed briefly so completion is visible
33
+
34
+ export class TaskRegistry {
35
+ private seq = 0;
36
+ private readonly tasks = new Map<string, TaskInfo>();
37
+ private readonly lastEmit = new Map<string, number>();
38
+
39
+ constructor(private readonly emit: (t: TaskInfo) => void) {}
40
+
41
+ start(label: string): TaskHandle {
42
+ const id = "t" + ++this.seq;
43
+ const info: TaskInfo = { id, label, state: "running", progress: { done: 0 }, startedAt: Date.now() };
44
+ this.tasks.set(id, info);
45
+ this.send(info, true);
46
+ const finish = (state: "done" | "error", error?: string): void => {
47
+ if (info.state !== "running") return; // done/fail are one-shot
48
+ info.state = state;
49
+ info.finishedAt = Date.now();
50
+ if (error !== undefined) info.error = error;
51
+ this.send(info, true);
52
+ };
53
+ return {
54
+ id,
55
+ progress: (done, total, message) => {
56
+ if (info.state !== "running") return;
57
+ info.progress = { done, ...(total !== undefined && { total }), ...(message !== undefined && { message }) };
58
+ this.send(info, false);
59
+ },
60
+ done: () => finish("done"),
61
+ fail: (err) => finish("error", String((err as Error)?.message ?? err)),
62
+ };
63
+ }
64
+
65
+ /** Running tasks + ones finished within the last few seconds (pruning the rest). */
66
+ list(): TaskInfo[] {
67
+ const now = Date.now();
68
+ for (const [id, t] of this.tasks) {
69
+ if (t.state !== "running" && now - (t.finishedAt ?? 0) > KEEP_FINISHED_MS) {
70
+ this.tasks.delete(id);
71
+ this.lastEmit.delete(id);
72
+ }
73
+ }
74
+ return [...this.tasks.values()].map((t) => ({ ...t, progress: { ...t.progress } }));
75
+ }
76
+
77
+ private send(t: TaskInfo, always: boolean): void {
78
+ const now = Date.now();
79
+ if (!always && now - (this.lastEmit.get(t.id) ?? 0) < PROGRESS_EMIT_MS) return;
80
+ this.lastEmit.set(t.id, now);
81
+ this.emit({ ...t, progress: { ...t.progress } });
82
+ }
83
+ }