@kgdiff/cli 0.0.0-stage → 0.2.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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -2
  3. package/dist/apply-command.d.ts +34 -0
  4. package/dist/apply-command.d.ts.map +1 -0
  5. package/dist/apply-command.js +137 -0
  6. package/dist/apply-command.js.map +1 -0
  7. package/dist/args.d.ts +96 -0
  8. package/dist/args.d.ts.map +1 -0
  9. package/dist/args.js +463 -0
  10. package/dist/args.js.map +1 -0
  11. package/dist/bin.d.ts +3 -0
  12. package/dist/bin.d.ts.map +1 -0
  13. package/dist/bin.js +17 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/diff-command.d.ts +29 -0
  16. package/dist/diff-command.d.ts.map +1 -0
  17. package/dist/diff-command.js +236 -0
  18. package/dist/diff-command.js.map +1 -0
  19. package/dist/errors.d.ts +48 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +92 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/help.d.ts +11 -0
  24. package/dist/help.d.ts.map +1 -0
  25. package/dist/help.js +247 -0
  26. package/dist/help.js.map +1 -0
  27. package/dist/html.d.ts +53 -0
  28. package/dist/html.d.ts.map +1 -0
  29. package/dist/html.js +107 -0
  30. package/dist/html.js.map +1 -0
  31. package/dist/index.d.ts +25 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +24 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/inputs.d.ts +54 -0
  36. package/dist/inputs.d.ts.map +1 -0
  37. package/dist/inputs.js +209 -0
  38. package/dist/inputs.js.map +1 -0
  39. package/dist/io.d.ts +64 -0
  40. package/dist/io.d.ts.map +1 -0
  41. package/dist/io.js +2 -0
  42. package/dist/io.js.map +1 -0
  43. package/dist/locale.d.ts +58 -0
  44. package/dist/locale.d.ts.map +1 -0
  45. package/dist/locale.js +121 -0
  46. package/dist/locale.js.map +1 -0
  47. package/dist/main.d.ts +10 -0
  48. package/dist/main.d.ts.map +1 -0
  49. package/dist/main.js +51 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/markdown.d.ts +30 -0
  52. package/dist/markdown.d.ts.map +1 -0
  53. package/dist/markdown.js +93 -0
  54. package/dist/markdown.js.map +1 -0
  55. package/dist/model.d.ts +63 -0
  56. package/dist/model.d.ts.map +1 -0
  57. package/dist/model.js +151 -0
  58. package/dist/model.js.map +1 -0
  59. package/dist/node-io.d.ts +12 -0
  60. package/dist/node-io.d.ts.map +1 -0
  61. package/dist/node-io.js +135 -0
  62. package/dist/node-io.js.map +1 -0
  63. package/dist/output.d.ts +27 -0
  64. package/dist/output.d.ts.map +1 -0
  65. package/dist/output.js +50 -0
  66. package/dist/output.js.map +1 -0
  67. package/dist/overwrite.d.ts +14 -0
  68. package/dist/overwrite.d.ts.map +1 -0
  69. package/dist/overwrite.js +55 -0
  70. package/dist/overwrite.js.map +1 -0
  71. package/dist/report.d.ts +52 -0
  72. package/dist/report.d.ts.map +1 -0
  73. package/dist/report.js +64 -0
  74. package/dist/report.js.map +1 -0
  75. package/dist/style.d.ts +46 -0
  76. package/dist/style.d.ts.map +1 -0
  77. package/dist/style.js +140 -0
  78. package/dist/style.js.map +1 -0
  79. package/dist/summary.d.ts +40 -0
  80. package/dist/summary.d.ts.map +1 -0
  81. package/dist/summary.js +352 -0
  82. package/dist/summary.js.map +1 -0
  83. package/dist/version.d.ts +6 -0
  84. package/dist/version.d.ts.map +1 -0
  85. package/dist/version.js +19 -0
  86. package/dist/version.js.map +1 -0
  87. package/dist/width.d.ts +45 -0
  88. package/dist/width.d.ts.map +1 -0
  89. package/dist/width.js +232 -0
  90. package/dist/width.js.map +1 -0
  91. package/package.json +56 -3
  92. package/src/apply-command.ts +203 -0
  93. package/src/args.ts +605 -0
  94. package/src/bin.ts +22 -0
  95. package/src/diff-command.ts +299 -0
  96. package/src/errors.ts +131 -0
  97. package/src/help.ts +286 -0
  98. package/src/html.ts +145 -0
  99. package/src/index.ts +71 -0
  100. package/src/inputs.ts +269 -0
  101. package/src/io.ts +68 -0
  102. package/src/locale.ts +177 -0
  103. package/src/main.ts +56 -0
  104. package/src/markdown.ts +119 -0
  105. package/src/model.ts +181 -0
  106. package/src/node-io.ts +146 -0
  107. package/src/output.ts +81 -0
  108. package/src/overwrite.ts +75 -0
  109. package/src/report.ts +101 -0
  110. package/src/style.ts +164 -0
  111. package/src/summary.ts +440 -0
  112. package/src/version.ts +21 -0
  113. package/src/width.ts +222 -0
package/src/help.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * `kgdiff --help` and `kgdiff apply --help`: grouped options, defaults, examples and exit
3
+ * codes, aligned and wrapped to the terminal width.
4
+ */
5
+ import type { Painter } from './style.js';
6
+ import { padEnd, stringWidth, wrap } from './width.js';
7
+
8
+ interface Entry {
9
+ readonly flags: string;
10
+ readonly text: string;
11
+ readonly default?: string;
12
+ }
13
+
14
+ interface Section {
15
+ readonly title: string;
16
+ readonly note?: string;
17
+ readonly entries?: readonly Entry[];
18
+ /** Pre-formatted lines (usage, examples). */
19
+ readonly lines?: readonly (readonly [command: string, comment?: string])[];
20
+ }
21
+
22
+ const MAIN: readonly Section[] = [
23
+ {
24
+ title: 'Usage',
25
+ lines: [
26
+ ['kgdiff <before> <after> [options]'],
27
+ ['kgdiff apply <graph> <delta> [-o <file>] [--no-strict]'],
28
+ ],
29
+ note:
30
+ 'Graphs: JSON (.json), JSON-LD (.jsonld), Turtle (.ttl), N-Triples (.nt), N-Quads (.nq), ' +
31
+ 'TriG (.trig), RDF/XML (.rdf, .owl), OBO (.obo); gzip-compressed files work too. ' +
32
+ 'Use - to read one side from stdin.',
33
+ },
34
+ {
35
+ title: 'Output',
36
+ note: 'Default: a summary on stdout. Outputs combine, e.g. -o report.html --markdown summary.md',
37
+ entries: [
38
+ {
39
+ flags: '--stat',
40
+ text: 'Diffstat only: counters, bar, node and edge counts, changes by type',
41
+ },
42
+ {
43
+ flags: '--name-status',
44
+ text: 'One line per hunk: A added, D removed, M modified, R renamed, V moved, G merged, S split, E relations',
45
+ },
46
+ { flags: '--json [file]', text: 'Full machine-readable diff (DiffJSON)' },
47
+ { flags: '--delta [file]', text: 'Invertible delta only, for kgdiff apply' },
48
+ {
49
+ flags: '--markdown [file]',
50
+ text: 'GitHub-flavoured Markdown for PR comments and job summaries',
51
+ },
52
+ {
53
+ flags: '-o, --html <file>',
54
+ text: 'Self-contained interactive HTML report (works offline)',
55
+ },
56
+ {
57
+ flags: '--open',
58
+ text: 'Open the HTML report in the browser (a temporary file without -o)',
59
+ },
60
+ { flags: '--pretty', text: 'Indent JSON output' },
61
+ ],
62
+ },
63
+ {
64
+ title: 'Diff',
65
+ entries: [
66
+ {
67
+ flags: '-f, --format <fmt>',
68
+ text: 'Input format: auto, json, jsonld, turtle, ntriples, nquads, trig, rdfxml, obo',
69
+ default: 'auto',
70
+ },
71
+ { flags: '--before-format <fmt>', text: 'Format of <before> only' },
72
+ { flags: '--after-format <fmt>', text: 'Format of <after> only' },
73
+ { flags: '--match <strategy>', text: 'Node matching: id, auto, similarity', default: 'auto' },
74
+ { flags: '--threshold <n>', text: 'Similarity acceptance threshold, 0–1', default: '0.82' },
75
+ {
76
+ flags: '--edge-identity <mode>',
77
+ text: 'Edge identity: structure, id',
78
+ default: 'structure',
79
+ },
80
+ {
81
+ flags: '--hierarchy <predicate>',
82
+ text: 'Hierarchy predicate (repeatable); replaces the defaults when given',
83
+ },
84
+ { flags: '--ignore-attr <key>', text: 'Ignore an attribute when comparing (repeatable)' },
85
+ { flags: '--no-merges', text: 'Do not detect merges' },
86
+ { flags: '--no-splits', text: 'Do not detect splits' },
87
+ {
88
+ flags: '--lang <tags>',
89
+ text: 'RDF label language preference, comma-separated, e.g. zh,en',
90
+ },
91
+ ],
92
+ },
93
+ {
94
+ title: 'Report',
95
+ entries: [
96
+ {
97
+ flags: '--title <text>',
98
+ text: 'Title of the summary and report',
99
+ default: '<before> → <after>',
100
+ },
101
+ { flags: '--theme <theme>', text: 'Initial theme: auto, light, dark', default: 'auto' },
102
+ {
103
+ flags: '--locale <locale>',
104
+ text: 'Language of hunk titles and the report: en, zh-CN',
105
+ default: 'from LANG, else en',
106
+ },
107
+ { flags: '--mode <mode>', text: 'Initial viewer mode: overlay, swipe, replay, changes' },
108
+ {
109
+ flags: '--reproducible',
110
+ text: 'Omit the timestamp: byte-identical reports for identical input',
111
+ },
112
+ {
113
+ flags: '--no-compute',
114
+ text: 'Leave out the viewer compute add-on (by default inlined for large graphs laid out around their changes, so collapsed groups can be expanded)',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ title: 'Behaviour',
120
+ entries: [
121
+ {
122
+ flags: '--exit-code',
123
+ text: 'Exit with 1 when the graphs differ, like git diff --exit-code',
124
+ },
125
+ {
126
+ flags: '--allow-empty',
127
+ text: 'Treat an empty input as an empty graph (otherwise an empty file or stdin is an error)',
128
+ },
129
+ { flags: '-q, --quiet', text: 'Print nothing on stdout (files are still written)' },
130
+ {
131
+ flags: '--color <when>',
132
+ text: 'auto, always, never (auto honours NO_COLOR and FORCE_COLOR)',
133
+ default: 'auto',
134
+ },
135
+ { flags: '--limit <n>', text: 'Hunks listed in the summary; 0 lists all', default: '50' },
136
+ { flags: '-h, --help', text: 'Show this help' },
137
+ { flags: '-v, --version', text: 'Show the version' },
138
+ ],
139
+ },
140
+ {
141
+ title: 'Examples',
142
+ lines: [
143
+ ['kgdiff v12.json v13.json', 'colored summary'],
144
+ ['kgdiff old.obo new.obo -o report.html --open', 'interactive report'],
145
+ ['kgdiff a.ttl b.ttl --markdown >> "$GITHUB_STEP_SUMMARY"', 'CI job summary'],
146
+ ['kgdiff a.json b.json --exit-code -q', 'fail when the graphs differ'],
147
+ ['kgdiff a.json b.json --delta patch.json', 'export a delta …'],
148
+ ['kgdiff apply a.json patch.json -o b.json', '… and apply it'],
149
+ ['curl -s https://example.org/latest.ttl | kgdiff pinned.ttl -', 'compare with stdin'],
150
+ ],
151
+ },
152
+ {
153
+ title: 'Environment',
154
+ note:
155
+ 'NO_COLOR, FORCE_COLOR (0–3) and COLORTERM control colors · LANG / LC_ALL the locale · ' +
156
+ 'COLUMNS the width when piped · KGDIFF_MAX_INPUT the input size limit (default 1G, e.g. 4G) · ' +
157
+ 'DEBUG=kgdiff prints stack traces',
158
+ },
159
+ {
160
+ title: 'Exit status',
161
+ note: '0 success (and no differences with --exit-code) · 1 differences with --exit-code · 2 usage or input error',
162
+ },
163
+ ];
164
+
165
+ const APPLY: readonly Section[] = [
166
+ {
167
+ title: 'Usage',
168
+ lines: [['kgdiff apply <graph> <delta> [-o <file>] [--no-strict]']],
169
+ note:
170
+ 'Applies a kgdiff delta (from --delta, or the delta inside --json output) to a graph and ' +
171
+ 'writes the result as kgdiff JSON. Use - to read the graph or the delta from stdin.',
172
+ },
173
+ {
174
+ title: 'Options',
175
+ entries: [
176
+ {
177
+ flags: '-o, --output <file>',
178
+ text: 'Write the patched graph to a file',
179
+ default: 'stdout',
180
+ },
181
+ {
182
+ flags: '--no-strict',
183
+ text: "Apply the operations that fit even if the graph is not the delta's base; reports how many were skipped",
184
+ },
185
+ { flags: '--verbose', text: 'With --no-strict, list the skipped operations' },
186
+ { flags: '--allow-empty', text: 'Treat an empty <graph> as an empty graph' },
187
+ { flags: '-f, --format <fmt>', text: 'Format of <graph>', default: 'auto' },
188
+ { flags: '--lang <tags>', text: 'RDF label language preference, e.g. zh,en' },
189
+ { flags: '--pretty', text: 'Indent the JSON output' },
190
+ { flags: '-q, --quiet', text: 'Print nothing on stdout' },
191
+ { flags: '--color <when>', text: 'auto, always, never', default: 'auto' },
192
+ { flags: '-h, --help', text: 'Show this help' },
193
+ ],
194
+ },
195
+ {
196
+ title: 'Examples',
197
+ lines: [
198
+ ['kgdiff v1.ttl v2.ttl --delta patch.json', 'make a delta'],
199
+ ['kgdiff apply v1.ttl patch.json -o v2.json', 'apply it'],
200
+ ],
201
+ },
202
+ {
203
+ title: 'Exit status',
204
+ note: '0 applied · 2 the delta does not fit the graph, or another error',
205
+ },
206
+ ];
207
+
208
+ const INDENT = ' ';
209
+ const MAX_FLAGS = 26;
210
+
211
+ function paintFlags(flags: string, paint: Painter): string {
212
+ return flags
213
+ .split(/(<[^>]+>|\[[^\]]+\])/)
214
+ .map((part) =>
215
+ part.startsWith('<') || part.startsWith('[') ? paint.dim(part) : paint.tone('accent', part),
216
+ )
217
+ .join('');
218
+ }
219
+
220
+ /**
221
+ * Render help text. `width` is the terminal width (descriptions wrap to it; below 60 columns
222
+ * they go under their flags).
223
+ */
224
+ export function renderHelp(
225
+ topic: 'main' | 'apply',
226
+ version: string,
227
+ paint: Painter,
228
+ width: number,
229
+ ): string {
230
+ const sections = topic === 'apply' ? APPLY : MAIN;
231
+ const w = Math.max(40, Math.min(width, 100));
232
+ const flagWidth = Math.min(
233
+ MAX_FLAGS,
234
+ Math.max(...sections.flatMap((s) => (s.entries ?? []).map((e) => stringWidth(e.flags)))) + 2,
235
+ );
236
+ const stacked = w < 60;
237
+ const out: string[] = [
238
+ `${paint.bold(paint.tone('accent', 'kgdiff'))} ${version} ${paint.dim(
239
+ topic === 'apply' ? 'apply a delta to a graph' : 'git diff for knowledge graphs',
240
+ )}`,
241
+ ];
242
+ for (const section of sections) {
243
+ out.push('', paint.bold(section.title));
244
+ for (const [command, comment] of section.lines ?? []) {
245
+ const gap = Math.max(2, 58 - stringWidth(command));
246
+ if (comment && stringWidth(command) + gap + stringWidth(comment) + 4 <= w) {
247
+ out.push(`${INDENT}${command}${' '.repeat(gap)}${paint.dim(`# ${comment}`)}`);
248
+ continue;
249
+ }
250
+ if (comment) out.push(INDENT + paint.dim(`# ${comment}`));
251
+ // Long commands wrap with a hanging indent (narrow terminals).
252
+ wrap(command, w - INDENT.length - 4).forEach((line, i) => {
253
+ out.push(`${INDENT}${i > 0 ? ' ' : ''}${line}`);
254
+ });
255
+ }
256
+ if (section.note && section.lines) out.push('');
257
+ if (section.note) {
258
+ for (const line of wrap(section.note, w - INDENT.length)) out.push(INDENT + paint.dim(line));
259
+ }
260
+ for (const entry of section.entries ?? []) {
261
+ const room = stacked ? w - INDENT.length * 3 : w - INDENT.length - flagWidth;
262
+ const defaultText = entry.default ? `[default: ${entry.default}]` : '';
263
+ const wrapped = wrap(defaultText ? `${entry.text} ${defaultText}` : entry.text, room).map(
264
+ (line) =>
265
+ defaultText && line.endsWith(defaultText)
266
+ ? line.slice(0, -defaultText.length) + paint.dim(defaultText)
267
+ : line,
268
+ );
269
+ const flagsPlain = entry.flags;
270
+ if (stacked || stringWidth(flagsPlain) >= flagWidth) {
271
+ out.push(INDENT + paintFlags(flagsPlain, paint));
272
+ for (const line of wrapped)
273
+ out.push(INDENT.repeat(stacked ? 3 : 1) + ' '.repeat(stacked ? 0 : flagWidth) + line);
274
+ continue;
275
+ }
276
+ wrapped.forEach((line, i) => {
277
+ const lead =
278
+ i === 0
279
+ ? paintFlags(flagsPlain, paint) + ' '.repeat(flagWidth - stringWidth(flagsPlain))
280
+ : padEnd('', flagWidth);
281
+ out.push(INDENT + lead + line);
282
+ });
283
+ }
284
+ }
285
+ return `${out.join('\n')}\n`;
286
+ }
package/src/html.ts ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The single-file HTML report (docs/cli.md §HTML report): the standalone viewer bundle and the
3
+ * `ReportPayload` inlined in one accessible document that works offline.
4
+ *
5
+ * Pure: compression is injected, so the same inputs always give the same bytes.
6
+ */
7
+ import type { ReportPayload, ThemePreference } from '@kgdiff/viewer';
8
+ import { sanitize } from './width.js';
9
+
10
+ /** MIME type of the payload script (`PAYLOAD_SCRIPT_TYPE` in `@kgdiff/viewer`). */
11
+ export const PAYLOAD_TYPE = 'application/kgdiff+json';
12
+
13
+ /** Payloads larger than this (UTF-8 bytes of the JSON) are embedded gzip + base64. */
14
+ export const GZIP_THRESHOLD = 512 * 1024;
15
+
16
+ const VIEWER_ID = 'kgdiff';
17
+
18
+ /** Theme backgrounds (docs/visual-language.md `canvas.background`, `text.primary`). */
19
+ const DARK = { background: '#0B0E14', text: '#E7EBF3' };
20
+ const LIGHT = { background: '#F7F8FA', text: '#0F172A' };
21
+
22
+ /** Escape text for HTML element content and double-quoted attribute values. */
23
+ export function escapeHtml(text: string): string {
24
+ return text
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&#39;');
30
+ }
31
+
32
+ /**
33
+ * JSON that is safe inside a `<script>` element: `<` is written as `<` (so neither
34
+ * `</script` nor `<!--` can appear), and U+2028 / U+2029 are escaped. Still valid JSON.
35
+ */
36
+ export function scriptSafeJson(json: string): string {
37
+ return json
38
+ .replace(/</g, '\\u003c')
39
+ .replace(/\u2028/g, '\\u2028')
40
+ .replace(/\u2029/g, '\\u2029');
41
+ }
42
+
43
+ /**
44
+ * Make inline JavaScript safe inside a classic `<script>` element: `</script` becomes
45
+ * `<\/script` and `<!--` becomes `\x3C!--`. Both rewrites keep string, template and regular
46
+ * expression literals equivalent (the only places such sequences occur in real bundles).
47
+ */
48
+ export function scriptSafeJs(code: string): string {
49
+ return code.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '\\x3C!--');
50
+ }
51
+
52
+ export interface ReportHtmlOptions {
53
+ readonly payload: ReportPayload;
54
+ /** Source of the standalone viewer bundle (an IIFE). */
55
+ readonly bundle: string;
56
+ /**
57
+ * Source of the optional compute add-on (diff / layout in the browser, an IIFE that sets
58
+ * `globalThis.KgdiffCompute`), inlined right after the viewer bundle when given.
59
+ */
60
+ readonly compute?: string;
61
+ /** gzip compressor used for large payloads. Must be deterministic. */
62
+ readonly gzip: (data: Uint8Array) => Uint8Array;
63
+ /** Short description for `<meta name="description">` and `<noscript>`. */
64
+ readonly description: string;
65
+ /** Payload size above which it is compressed. Default `GZIP_THRESHOLD`. */
66
+ readonly gzipThreshold?: number;
67
+ }
68
+
69
+ function themeStyle(theme: ThemePreference): string {
70
+ const rule = (c: { background: string; text: string }): string =>
71
+ `html,body{background:${c.background};color:${c.text}}`;
72
+ if (theme === 'dark') return `:root{color-scheme:dark}${rule(DARK)}`;
73
+ if (theme === 'light') return `:root{color-scheme:light}${rule(LIGHT)}`;
74
+ return `:root{color-scheme:dark light}${rule(DARK)}@media (prefers-color-scheme:light){${rule(LIGHT)}}`;
75
+ }
76
+
77
+ /** Payload as embedded text plus its `data-encoding` (none, or `gzip-base64`). */
78
+ export function encodePayload(
79
+ payload: ReportPayload,
80
+ gzip: (data: Uint8Array) => Uint8Array,
81
+ threshold = GZIP_THRESHOLD,
82
+ ): { readonly text: string; readonly encoding?: 'gzip-base64' } {
83
+ const json = JSON.stringify(payload);
84
+ const bytes = new TextEncoder().encode(json);
85
+ if (bytes.byteLength <= threshold) return { text: scriptSafeJson(json) };
86
+ return { text: Buffer.from(gzip(bytes)).toString('base64'), encoding: 'gzip-base64' };
87
+ }
88
+
89
+ /**
90
+ * Render the report document: `<!doctype html>`, `lang`, charset, viewport, title, an inline
91
+ * style that paints the theme background before the viewer loads (no white flash), a
92
+ * full-viewport `<kgdiff-viewer>`, the payload script, the inlined viewer bundle and, when
93
+ * given, the compute add-on.
94
+ */
95
+ export function renderReportHtml(options: ReportHtmlOptions): string {
96
+ const { payload } = options;
97
+ const view = payload.view ?? {};
98
+ const theme = view.theme ?? 'auto';
99
+ const locale = view.locale ?? 'en';
100
+ // Titles come from graph metadata: no control or bidi characters in the tab title.
101
+ const title = sanitize(
102
+ payload.meta.title ?? `${payload.meta.before.name} → ${payload.meta.after.name}`,
103
+ );
104
+ const description = sanitize(options.description);
105
+ const encoded = encodePayload(payload, options.gzip, options.gzipThreshold);
106
+ const attrs = [
107
+ `id="${VIEWER_ID}"`,
108
+ // The report page is the viewer: its keyboard shortcuts apply to the whole page.
109
+ 'global-shortcuts',
110
+ `theme="${escapeHtml(theme)}"`,
111
+ `locale="${escapeHtml(locale)}"`,
112
+ ...(view.mode ? [`mode="${escapeHtml(view.mode)}"`] : []),
113
+ ].join(' ');
114
+ const encodingAttr = encoded.encoding ? ` data-encoding="${encoded.encoding}"` : '';
115
+ const style =
116
+ `${themeStyle(theme)}html,body{margin:0;height:100%;overflow:hidden}` +
117
+ 'kgdiff-viewer{display:block;position:fixed;inset:0;width:100%;height:100%}' +
118
+ '.kgdiff-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;' +
119
+ 'clip:rect(0 0 0 0);white-space:nowrap;border:0}' +
120
+ 'noscript p{font:15px/1.5 system-ui,sans-serif;margin:2rem;max-width:40rem}';
121
+ return [
122
+ '<!doctype html>',
123
+ `<html lang="${escapeHtml(locale)}">`,
124
+ '<head>',
125
+ '<meta charset="utf-8">',
126
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
127
+ `<meta name="generator" content="${escapeHtml(payload.meta.generator)}">`,
128
+ `<meta name="description" content="${escapeHtml(description)}">`,
129
+ `<title>${escapeHtml(title)} · kgdiff</title>`,
130
+ `<style>${style}</style>`,
131
+ '</head>',
132
+ '<body>',
133
+ '<main>',
134
+ `<h1 class="kgdiff-sr">${escapeHtml(title)}</h1>`,
135
+ `<kgdiff-viewer ${attrs}></kgdiff-viewer>`,
136
+ `<noscript><p>${escapeHtml(description)}. This interactive report needs JavaScript.</p></noscript>`,
137
+ '</main>',
138
+ `<script type="${PAYLOAD_TYPE}" data-for="${VIEWER_ID}"${encodingAttr}>${encoded.text}</script>`,
139
+ `<script>${scriptSafeJs(options.bundle)}</script>`,
140
+ ...(options.compute !== undefined ? [`<script>${scriptSafeJs(options.compute)}</script>`] : []),
141
+ '</body>',
142
+ '</html>',
143
+ '',
144
+ ].join('\n');
145
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * kgdiff CLI: `kgdiff <before> <after>` and `kgdiff apply <graph> <delta>` (docs/cli.md).
3
+ *
4
+ * `main` runs the CLI in-process against an injected `CliIO` (see `nodeIO` for the real one);
5
+ * the building blocks below are pure and reusable: argument parsing, terminal rendering,
6
+ * Markdown, the HTML report and the width / color helpers they rest on.
7
+ */
8
+
9
+ export { readDelta } from './apply-command.js';
10
+ export {
11
+ type ApplyCommandOptions,
12
+ type Command,
13
+ type CommonOptions,
14
+ DEFAULT_LIMIT,
15
+ type DiffCommandOptions,
16
+ parseCommandLine,
17
+ STDIO,
18
+ } from './args.js';
19
+ export { defaultTitle, describeDiff, graphLabel, outputWidth } from './diff-command.js';
20
+ export { CliError, type CliErrorCode, describeError, formatError } from './errors.js';
21
+ export { renderHelp } from './help.js';
22
+ export {
23
+ encodePayload,
24
+ escapeHtml,
25
+ GZIP_THRESHOLD,
26
+ PAYLOAD_TYPE,
27
+ type ReportHtmlOptions,
28
+ renderReportHtml,
29
+ scriptSafeJs,
30
+ scriptSafeJson,
31
+ } from './html.js';
32
+ export type { CliFileSystem, CliIO, OutputStream } from './io.js';
33
+ export { formatInt, matchLocale, messages, resolveLocale } from './locale.js';
34
+ export { main } from './main.js';
35
+ export {
36
+ escapeMarkdown,
37
+ MARKDOWN_HUNK_LIMIT,
38
+ type MarkdownOptions,
39
+ renderMarkdown,
40
+ } from './markdown.js';
41
+ export {
42
+ type ChangeFamily,
43
+ hunkFamily,
44
+ hunkGlyph,
45
+ KIND_FAMILY,
46
+ type NameStatus,
47
+ nameStatus,
48
+ type Overview,
49
+ overview,
50
+ } from './model.js';
51
+ export { nodeIO, openCommand } from './node-io.js';
52
+ export { buildReportHtml, buildReportPayload, createGzip, type ReportInfo } from './report.js';
53
+ export {
54
+ type ColorLevel,
55
+ type ColorMode,
56
+ createPainter,
57
+ type Painter,
58
+ resolveColorLevel,
59
+ stripAnsi,
60
+ type Tone,
61
+ } from './style.js';
62
+ export {
63
+ diffstatBar,
64
+ renderNameStatus,
65
+ renderStat,
66
+ renderSummary,
67
+ type TextContext,
68
+ totalsText,
69
+ } from './summary.js';
70
+ export { cliVersion } from './version.js';
71
+ export { graphemeWidth, padEnd, padStart, sanitize, stringWidth, truncate, wrap } from './width.js';