@geml/geml 1.0.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.
@@ -0,0 +1,55 @@
1
+ /** UTC basic ISO-8601, e.g. 20260617T103012Z. */
2
+ export declare function stampUTC(d: Date): string;
3
+ type Anchor = "at-start" | "at-end" | {
4
+ after: string;
5
+ };
6
+ interface Op {
7
+ kind: "delete" | "replace" | "insert" | "move";
8
+ key?: string;
9
+ blob?: string;
10
+ anchor?: Anchor;
11
+ }
12
+ interface Revision {
13
+ id: string;
14
+ parent?: string;
15
+ author?: string;
16
+ summary?: string;
17
+ hash: string;
18
+ ops: Op[];
19
+ }
20
+ interface History {
21
+ nl: string;
22
+ current: string;
23
+ keyframes: Map<string, string>;
24
+ revisions: Map<string, Revision>;
25
+ blobs: Map<string, string>;
26
+ }
27
+ /** Reconstruct the content of revision `targetId`. */
28
+ export declare function reconstruct(h: History, targetId: string): string;
29
+ export interface CommitOpts {
30
+ gemlPath: string;
31
+ historyPath: string;
32
+ summary: string;
33
+ author?: string;
34
+ at?: Date;
35
+ }
36
+ export declare function commit(o: CommitOpts): {
37
+ id: string;
38
+ hash: string;
39
+ };
40
+ export interface VerifyResult {
41
+ ok: boolean;
42
+ errors: string[];
43
+ warnings: string[];
44
+ checked: number;
45
+ }
46
+ export declare function verify(historyPath: string, gemlPath?: string): VerifyResult;
47
+ export interface RestoreOpts {
48
+ historyPath: string;
49
+ gemlPath: string;
50
+ revision: string;
51
+ write?: boolean;
52
+ force?: boolean;
53
+ }
54
+ export declare function restore(o: RestoreOpts): string;
55
+ export {};
@@ -0,0 +1,507 @@
1
+ // GEML History extension — commit / restore / verify.
2
+ //
3
+ // Implements the `.gemlhistory` companion spec: a self-contained, reverse-delta
4
+ // version history beside the live `.geml` file. The history file is itself a
5
+ // GEML document (meta + keyframe + revision + blob blocks). Reverse patches and
6
+ // hashes are tool-generated here; every commit re-applies its reverse patch and
7
+ // asserts a byte-exact round-trip before writing (the spec's verify gate).
8
+ //
9
+ // Revision id = `<YYYYMMDDTHHMMSSZ>-<first 8 hex of the version content hash>`.
10
+ import { createHash } from "node:crypto";
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ function loadBytes(path) {
13
+ const raw = readFileSync(path);
14
+ const nl = raw.includes(13) /* \r */ && raw.includes(10) ? "\r\n" : "\n";
15
+ return { lf: raw.toString("utf8").replace(/\r\n/g, "\n"), nl };
16
+ }
17
+ function bytesOf(lf, nl) {
18
+ return Buffer.from(lf.replace(/\n/g, nl), "utf8");
19
+ }
20
+ function writeBytes(path, lf, nl) {
21
+ writeFileSync(path, bytesOf(lf, nl));
22
+ }
23
+ function fullHash(lf, nl) {
24
+ return "sha256:" + createHash("sha256").update(bytesOf(lf, nl)).digest("hex");
25
+ }
26
+ function shortOf(hash) {
27
+ return hash.replace(/^sha256:/, "").slice(0, 8);
28
+ }
29
+ function makeId(stamp, hash) {
30
+ return `${stamp}-${shortOf(hash)}`;
31
+ }
32
+ /** UTC basic ISO-8601, e.g. 20260617T103012Z. */
33
+ export function stampUTC(d) {
34
+ const p = (n, w = 2) => String(n).padStart(w, "0");
35
+ return (`${p(d.getUTCFullYear(), 4)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}` +
36
+ `T${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}Z`);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Top-level fenced-block locator (for the history file's own structure)
40
+ // ---------------------------------------------------------------------------
41
+ const FENCE_OPEN = /^(={3,})[ \t]+([A-Za-z][A-Za-z0-9_-]*)[ \t]*(\{.*\})?[ \t]*$/;
42
+ function locate(lines) {
43
+ const out = [];
44
+ let i = 0;
45
+ while (i < lines.length) {
46
+ const m = FENCE_OPEN.exec(lines[i]);
47
+ if (m) {
48
+ const fenceLen = m[1].length;
49
+ const attrLine = m[3] ?? "";
50
+ const idm = /#([A-Za-z][A-Za-z0-9_-]*)/.exec(attrLine);
51
+ let j = i + 1;
52
+ while (j < lines.length) {
53
+ const t = lines[j].replace(/\s+$/, "");
54
+ if (/^=+$/.test(t) && t.length === fenceLen)
55
+ break;
56
+ j++;
57
+ }
58
+ out.push({ type: m[2], id: idm?.[1], attrLine, fenceLen, start: i, end: Math.min(j, lines.length - 1) });
59
+ i = j + 1;
60
+ }
61
+ else {
62
+ i++;
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ function attr(attrLine, key) {
68
+ const m = new RegExp(`${key}=("([^"]*)"|[^\\s}]+)`).exec(attrLine);
69
+ return m ? (m[2] !== undefined ? m[2] : m[1]) : undefined;
70
+ }
71
+ function fenceFor(contentLf) {
72
+ let longest = 0;
73
+ for (const line of contentLf.split("\n")) {
74
+ const m = /^=+/.exec(line);
75
+ if (m)
76
+ longest = Math.max(longest, m[0].length);
77
+ }
78
+ return "=".repeat(Math.max(longest + 1, 3));
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // Document units & reverse-patch engine (gap-aware)
82
+ // ---------------------------------------------------------------------------
83
+ // Unit-key = `#id` (explicit), or `@<8hex content hash>` (derived) with `~n`
84
+ // disambiguating equal-content units by document-order occurrence (§4).
85
+ const KEY = String.raw `(#[A-Za-z][A-Za-z0-9_-]*|@[0-9a-f]+(?:~\d+)?)`;
86
+ function sha8(s) {
87
+ return createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex").slice(0, 8);
88
+ }
89
+ function tile(lines) {
90
+ const units = [];
91
+ const n = lines.length;
92
+ let i = 0;
93
+ while (i < n) {
94
+ const start = i;
95
+ if (lines[i].trim() === "") { // leading / standalone blank run
96
+ while (i < n && lines[i].trim() === "")
97
+ i++;
98
+ units.push({ start, bodyEnd: i, endExcl: i });
99
+ continue;
100
+ }
101
+ const fo = FENCE_OPEN.exec(lines[i]);
102
+ let id;
103
+ if (fo) {
104
+ const fenceLen = fo[1].length;
105
+ id = /#([A-Za-z][A-Za-z0-9_-]*)/.exec(fo[3] ?? "")?.[1];
106
+ i++;
107
+ while (i < n) {
108
+ const t = lines[i].replace(/\s+$/, "");
109
+ const close = /^=+$/.test(t) && t.length === fenceLen;
110
+ i++;
111
+ if (close)
112
+ break;
113
+ }
114
+ }
115
+ else { // flow segment: consecutive non-blank, non-fence lines
116
+ i++;
117
+ while (i < n && lines[i].trim() !== "" && !FENCE_OPEN.test(lines[i]))
118
+ i++;
119
+ }
120
+ const bodyEnd = i;
121
+ while (i < n && lines[i].trim() === "")
122
+ i++; // own trailing blanks
123
+ units.push({ start, bodyEnd, endExcl: i, id });
124
+ }
125
+ return units;
126
+ }
127
+ function keyedUnits(lines) {
128
+ const counts = new Map();
129
+ return tile(lines).map((u) => {
130
+ if (u.id)
131
+ return { u, key: `#${u.id}` };
132
+ const base = `@${sha8(lines.slice(u.start, u.bodyEnd).join("\n"))}`;
133
+ const n = counts.get(base) ?? 0;
134
+ counts.set(base, n + 1);
135
+ return { u, key: n === 0 ? base : `${base}~${n}` };
136
+ });
137
+ }
138
+ function locateUnit(lines, key) {
139
+ const ku = keyedUnits(lines).find((x) => x.key === key);
140
+ if (!ku)
141
+ throw new Error(`history: unit ${key} not found while applying reverse patch`);
142
+ return ku.u;
143
+ }
144
+ function parseAnchor(s) {
145
+ if (s === "at-start" || s === "at-end")
146
+ return s;
147
+ const m = new RegExp("^after\\s+" + KEY + "$").exec(s);
148
+ if (!m)
149
+ throw new Error(`history: bad anchor: ${s}`);
150
+ return { after: m[1] };
151
+ }
152
+ function anchorStr(a) {
153
+ return a === "at-start" || a === "at-end" ? a : `after ${a.after}`;
154
+ }
155
+ function parseOps(body) {
156
+ const ops = [];
157
+ for (const raw of body.split("\n")) {
158
+ const line = raw.trim();
159
+ if (!line)
160
+ continue;
161
+ let m;
162
+ if ((m = new RegExp("^delete\\s+" + KEY + "$").exec(line))) {
163
+ ops.push({ kind: "delete", key: m[1] });
164
+ }
165
+ else if ((m = new RegExp("^replace\\s+" + KEY + "\\s+<-\\s+blob:(\\S+)$").exec(line))) {
166
+ ops.push({ kind: "replace", key: m[1], blob: m[2] });
167
+ }
168
+ else if ((m = /^insert\s+<-\s+blob:(\S+)\s+(.+)$/.exec(line))) {
169
+ ops.push({ kind: "insert", blob: m[1], anchor: parseAnchor(m[2]) });
170
+ }
171
+ else if ((m = new RegExp("^move\\s+" + KEY + "\\s+(.+)$").exec(line))) {
172
+ ops.push({ kind: "move", key: m[1], anchor: parseAnchor(m[2]) });
173
+ }
174
+ else {
175
+ throw new Error(`history: unrecognized reverse-patch op: ${line}`);
176
+ }
177
+ }
178
+ return ops;
179
+ }
180
+ // Each blob carries a unit's full text (its lines plus the blank lines it owns),
181
+ // so insert / replace are byte-exact without separate spacing bookkeeping.
182
+ function insertAt(lines, anchor, payload) {
183
+ if (anchor === "at-start") {
184
+ lines.splice(0, 0, ...payload);
185
+ return;
186
+ }
187
+ if (anchor === "at-end") {
188
+ lines.push(...payload);
189
+ return;
190
+ }
191
+ const a = locateUnit(lines, anchor.after);
192
+ lines.splice(a.endExcl, 0, ...payload);
193
+ }
194
+ /** Apply a reverse patch to `textLf`, returning the parent-revision text. */
195
+ function applyReverse(textLf, ops, blobs) {
196
+ const lines = textLf.split("\n");
197
+ const blob = (id) => {
198
+ const p = blobs.get(id);
199
+ if (p === undefined)
200
+ throw new Error(`history: unresolved blob:${id}`);
201
+ return p.split("\n");
202
+ };
203
+ for (const op of ops) {
204
+ if (op.kind === "delete") {
205
+ const u = locateUnit(lines, op.key);
206
+ lines.splice(u.start, u.endExcl - u.start);
207
+ }
208
+ else if (op.kind === "replace") {
209
+ const u = locateUnit(lines, op.key);
210
+ lines.splice(u.start, u.endExcl - u.start, ...blob(op.blob));
211
+ }
212
+ else if (op.kind === "insert") {
213
+ insertAt(lines, op.anchor, blob(op.blob));
214
+ }
215
+ else { // move: cut the unit (with its owned blanks) and re-insert at anchor
216
+ const u = locateUnit(lines, op.key);
217
+ const cut = lines.slice(u.start, u.endExcl);
218
+ lines.splice(u.start, u.endExcl - u.start);
219
+ insertAt(lines, op.anchor, cut);
220
+ }
221
+ }
222
+ return lines.join("\n");
223
+ }
224
+ /** LCS alignment of unit-key sequences; aMatch[i] = matched index in b, or -1. */
225
+ function lcsMatch(a, b) {
226
+ const n = a.length, m = b.length;
227
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
228
+ for (let i = n - 1; i >= 0; i--)
229
+ for (let j = m - 1; j >= 0; j--)
230
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
231
+ const aMatch = new Array(n).fill(-1);
232
+ let i = 0, j = 0;
233
+ while (i < n && j < m) {
234
+ if (a[i] === b[j]) {
235
+ aMatch[i] = j;
236
+ i++;
237
+ j++;
238
+ }
239
+ else if (dp[i + 1][j] >= dp[i][j + 1])
240
+ i++;
241
+ else
242
+ j++;
243
+ }
244
+ return aMatch;
245
+ }
246
+ function diffReverse(oldLf, newLf) {
247
+ const oldLines = oldLf.split("\n");
248
+ const newLines = newLf.split("\n");
249
+ const oldU = keyedUnits(oldLines);
250
+ const newU = keyedUnits(newLines);
251
+ const oldKeys = oldU.map((x) => x.key);
252
+ const newKeys = newU.map((x) => x.key);
253
+ const aMatch = lcsMatch(newKeys, oldKeys);
254
+ const oldMatched = new Array(oldU.length).fill(false);
255
+ for (const j of aMatch)
256
+ if (j >= 0)
257
+ oldMatched[j] = true;
258
+ const ops = [];
259
+ const blobs = [];
260
+ let blobN = 0;
261
+ const addBlob = (payload) => { const id = `b${++blobN}`; blobs.push({ id, payload }); return id; };
262
+ const full = (lines, u) => lines.slice(u.start, u.endExcl).join("\n");
263
+ // 1. units in new but unmatched -> reverse delete
264
+ for (let i = 0; i < newU.length; i++)
265
+ if (aMatch[i] === -1)
266
+ ops.push({ kind: "delete", key: newKeys[i] });
267
+ // 2. matched units whose full text differs (id'd content change, or spacing) -> reverse replace
268
+ for (let i = 0; i < newU.length; i++) {
269
+ const j = aMatch[i];
270
+ if (j >= 0 && full(newLines, newU[i].u) !== full(oldLines, oldU[j].u)) {
271
+ ops.push({ kind: "replace", key: newKeys[i], blob: addBlob(full(oldLines, oldU[j].u)) });
272
+ }
273
+ }
274
+ // 3. units in old but unmatched -> reverse insert, in old order, anchored by predecessor
275
+ for (let j = 0; j < oldU.length; j++) {
276
+ if (!oldMatched[j]) {
277
+ const prev = j > 0 ? oldKeys[j - 1] : null;
278
+ ops.push({ kind: "insert", blob: addBlob(full(oldLines, oldU[j].u)), anchor: prev ? { after: prev } : "at-start" });
279
+ }
280
+ }
281
+ return { ops, blobs };
282
+ }
283
+ function opLine(op) {
284
+ if (op.kind === "delete")
285
+ return `delete ${op.key}`;
286
+ if (op.kind === "replace")
287
+ return `replace ${op.key} <- blob:${op.blob}`;
288
+ if (op.kind === "insert")
289
+ return `insert <- blob:${op.blob} ${anchorStr(op.anchor)}`;
290
+ return `move ${op.key} ${anchorStr(op.anchor)}`;
291
+ }
292
+ function parseHistory(path) {
293
+ const { lf, nl } = loadBytes(path);
294
+ const lines = lf.split("\n");
295
+ const blocks = locate(lines);
296
+ const keyframes = new Map();
297
+ const revisions = new Map();
298
+ const blobs = new Map();
299
+ let current = "";
300
+ for (const b of blocks) {
301
+ const body = lines.slice(b.start + 1, b.end).join("\n");
302
+ if (b.type === "meta") {
303
+ const m = /^\s*current\s*=\s*"?([^"\n]+?)"?\s*$/m.exec(body);
304
+ if (m)
305
+ current = m[1];
306
+ }
307
+ else if (b.type === "keyframe") {
308
+ keyframes.set(attr(b.attrLine, "id"), body);
309
+ }
310
+ else if (b.type === "revision") {
311
+ const id = attr(b.attrLine, "id");
312
+ revisions.set(id, {
313
+ id,
314
+ parent: attr(b.attrLine, "parent"),
315
+ author: attr(b.attrLine, "author"),
316
+ summary: attr(b.attrLine, "summary"),
317
+ hash: attr(b.attrLine, "hash") ?? "",
318
+ ops: parseOps(body),
319
+ });
320
+ }
321
+ else if (b.type === "blob") {
322
+ blobs.set(b.id, body);
323
+ }
324
+ }
325
+ return { nl, current, keyframes, revisions, blobs };
326
+ }
327
+ function chainFrom(h) {
328
+ const out = [];
329
+ let id = h.current;
330
+ const seen = new Set();
331
+ while (id) {
332
+ const r = h.revisions.get(id);
333
+ if (!r)
334
+ throw new Error(`history: revision ${id} missing (broken chain)`);
335
+ if (seen.has(id))
336
+ throw new Error(`history: cycle at ${id}`);
337
+ seen.add(id);
338
+ out.push(r);
339
+ id = r.parent;
340
+ }
341
+ return out; // newest -> oldest
342
+ }
343
+ /** Reconstruct the content of revision `targetId`. */
344
+ export function reconstruct(h, targetId) {
345
+ const chain = chainFrom(h);
346
+ const t = chain.findIndex((r) => r.id === targetId);
347
+ if (t < 0)
348
+ throw new Error(`history: unknown revision ${targetId}`);
349
+ // nearest keyframe at-or-newer than target (chain[0] = newest)
350
+ let kf = -1;
351
+ for (let i = t; i >= 0; i--)
352
+ if (h.keyframes.has(chain[i].id)) {
353
+ kf = i;
354
+ break;
355
+ }
356
+ if (kf < 0)
357
+ throw new Error(`history: no keyframe to reconstruct ${targetId}`);
358
+ let text = h.keyframes.get(chain[kf].id);
359
+ for (let i = kf; i < t; i++)
360
+ text = applyReverse(text, chain[i].ops, h.blobs);
361
+ return text;
362
+ }
363
+ // ---------------------------------------------------------------------------
364
+ // Render history (newest-first)
365
+ // ---------------------------------------------------------------------------
366
+ function renderHistory(h, baseName) {
367
+ const chain = chainFrom(h);
368
+ const parts = [];
369
+ parts.push(`# History of ${baseName}\n`);
370
+ parts.push("=== meta\n" +
371
+ `history-of = "${baseName}"\n` +
372
+ 'geml-version = "0.1"\n' +
373
+ `current = "${h.current}"\n` +
374
+ "keyframe-interval = 10\n" +
375
+ "===\n");
376
+ // committed-current keyframe
377
+ const kfContent = h.keyframes.get(h.current);
378
+ const kf = fenceFor(kfContent);
379
+ parts.push("# Committed-current mirror (always present):\n" +
380
+ `${kf} keyframe {id="${h.current}" hash="${chain[0].hash}"}\n` +
381
+ `${kfContent}\n${kf}\n`);
382
+ for (const r of chain) {
383
+ const at = [
384
+ `id="${r.id}"`,
385
+ r.parent ? `parent="${r.parent}"` : "",
386
+ r.author ? `author="${r.author}"` : "",
387
+ r.summary ? `summary="${r.summary}"` : "",
388
+ `hash="${r.hash}"`,
389
+ ].filter(Boolean).join(" ");
390
+ parts.push(`=== revision {${at}}\n${r.ops.map(opLine).join("\n")}${r.ops.length ? "\n" : ""}===\n`);
391
+ // blobs referenced by this revision
392
+ for (const op of r.ops) {
393
+ if (op.blob && h.blobs.has(op.blob)) {
394
+ const payload = h.blobs.get(op.blob);
395
+ const bf = fenceFor(payload);
396
+ parts.push(`${bf} blob {#${op.blob} lang=geml}\n${payload}\n${bf}\n`);
397
+ }
398
+ }
399
+ }
400
+ return parts.join("\n");
401
+ }
402
+ export function commit(o) {
403
+ const { lf: working, nl } = loadBytes(o.gemlPath);
404
+ const hash = fullHash(working, nl);
405
+ const stamp = stampUTC(o.at ?? new Date());
406
+ const id = makeId(stamp, hash);
407
+ const baseName = o.gemlPath.replace(/^.*[\\/]/, "");
408
+ let h;
409
+ if (existsSync(o.historyPath)) {
410
+ h = parseHistory(o.historyPath);
411
+ const prevId = h.current;
412
+ const prevContent = reconstruct(h, prevId); // committed current
413
+ const patch = diffReverse(prevContent, working);
414
+ // verify gate: the reverse patch must reproduce the parent byte-for-byte
415
+ const blobMap = new Map(patch.blobs.map((b) => [b.id, b.payload]));
416
+ const back = applyReverse(working, patch.ops, blobMap);
417
+ if (bytesOf(back, nl).compare(bytesOf(prevContent, nl)) !== 0) {
418
+ throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting commit");
419
+ }
420
+ for (const b of patch.blobs)
421
+ h.blobs.set(b.id, b.payload);
422
+ h.revisions.set(id, { id, parent: prevId, author: o.author, summary: o.summary, hash, ops: patch.ops });
423
+ h.keyframes.delete(prevId); // demote previous tip mirror (keyframes at intervals only)
424
+ h.keyframes.set(id, working);
425
+ h.current = id;
426
+ }
427
+ else {
428
+ h = {
429
+ nl, current: id,
430
+ keyframes: new Map([[id, working]]),
431
+ revisions: new Map([[id, { id, author: o.author, summary: o.summary, hash, ops: [] }]]),
432
+ blobs: new Map(),
433
+ };
434
+ }
435
+ writeBytes(o.historyPath, renderHistory(h, baseName), nl);
436
+ return { id, hash };
437
+ }
438
+ export function verify(historyPath, gemlPath) {
439
+ const errors = [];
440
+ const warnings = [];
441
+ const h = parseHistory(historyPath);
442
+ let checked = 0;
443
+ let chain = [];
444
+ try {
445
+ chain = chainFrom(h);
446
+ }
447
+ catch (e) {
448
+ errors.push(String(e.message));
449
+ }
450
+ for (const r of chain) {
451
+ try {
452
+ const content = reconstruct(h, r.id);
453
+ const got = fullHash(content, h.nl);
454
+ if (got !== r.hash)
455
+ errors.push(`revision ${r.id}: reconstructed hash ${got} != recorded ${r.hash}`);
456
+ checked++;
457
+ }
458
+ catch (e) {
459
+ errors.push(`revision ${r.id}: ${e.message}`);
460
+ }
461
+ }
462
+ if (gemlPath && existsSync(gemlPath)) {
463
+ const { lf, nl } = loadBytes(gemlPath);
464
+ if (fullHash(lf, nl) !== (chain[0]?.hash ?? "")) {
465
+ warnings.push("uncommitted changes: hash(doc.geml) differs from current");
466
+ }
467
+ }
468
+ return { ok: errors.length === 0, errors, warnings, checked };
469
+ }
470
+ export function restore(o) {
471
+ const h = parseHistory(o.historyPath);
472
+ // accept an unambiguous id prefix
473
+ const ids = [...h.revisions.keys()];
474
+ const matches = ids.filter((x) => x === o.revision || x.startsWith(o.revision) || x.endsWith(o.revision));
475
+ if (matches.length !== 1)
476
+ throw new Error(`history: revision selector "${o.revision}" matched ${matches.length} revisions`);
477
+ const target = matches[0];
478
+ const content = reconstruct(h, target);
479
+ if (o.write) {
480
+ if (existsSync(o.gemlPath)) {
481
+ const { lf, nl } = loadBytes(o.gemlPath);
482
+ if (fullHash(lf, nl) !== h.revisions.get(h.current).hash && !o.force) {
483
+ throw new Error("history: uncommitted changes in doc.geml; rerun with force to discard them, or commit first");
484
+ }
485
+ }
486
+ // destructive linear truncation to `target`
487
+ const chain = chainFrom(h);
488
+ const keep = new Set();
489
+ let id = target;
490
+ while (id) {
491
+ keep.add(id);
492
+ id = h.revisions.get(id).parent;
493
+ }
494
+ for (const r of chain)
495
+ if (!keep.has(r.id)) {
496
+ h.revisions.delete(r.id);
497
+ h.keyframes.delete(r.id);
498
+ }
499
+ h.keyframes.clear();
500
+ h.keyframes.set(target, content);
501
+ h.current = target;
502
+ const { nl } = loadBytes(o.historyPath);
503
+ writeBytes(o.gemlPath, content, nl);
504
+ writeBytes(o.historyPath, renderHistory(h, o.gemlPath.replace(/^.*[\\/]/, "")), nl);
505
+ }
506
+ return content;
507
+ }
@@ -0,0 +1,52 @@
1
+ import { type Value } from "./attrs.js";
2
+ export type Inline = {
3
+ type: "text";
4
+ value: string;
5
+ } | {
6
+ type: "emph";
7
+ children: Inline[];
8
+ } | {
9
+ type: "strong";
10
+ children: Inline[];
11
+ } | {
12
+ type: "strike";
13
+ children: Inline[];
14
+ } | {
15
+ type: "code";
16
+ value: string;
17
+ } | {
18
+ type: "math";
19
+ value: string;
20
+ } | {
21
+ type: "break";
22
+ } | {
23
+ type: "image";
24
+ alt: string;
25
+ src: string;
26
+ as?: string;
27
+ attrs: Record<string, Value>;
28
+ } | {
29
+ type: "link";
30
+ children: Inline[];
31
+ href?: string;
32
+ doc?: string;
33
+ anchor?: string;
34
+ attrs: Record<string, Value>;
35
+ } | {
36
+ type: "autoref";
37
+ anchor: string;
38
+ doc?: string;
39
+ } | {
40
+ type: "footnote";
41
+ ref: string;
42
+ };
43
+ export interface Ref {
44
+ kind: "internal" | "cross" | "footnote" | "autoref";
45
+ doc?: string;
46
+ anchor?: string;
47
+ line: number;
48
+ }
49
+ export interface RefSink {
50
+ refs: Ref[];
51
+ }
52
+ export declare function parseInline(s: string, line: number, sink: RefSink): Inline[];