@geml/geml 1.5.1 → 1.7.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.
- package/LICENSE +21 -21
- package/README.md +248 -217
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +34 -29
- package/codemap/build.mjs +609 -609
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +480 -480
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +431 -431
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +310 -310
- package/codemap/render-all.mjs +77 -77
- package/codemap/serve.mjs +585 -585
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +155 -148
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +4 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +16 -0
- package/dist/geml.d.ts +5 -1
- package/dist/geml.js +1277 -283
- package/dist/history.d.ts +11 -8
- package/dist/history.js +20 -15
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +108 -38
- package/dist/render-html.d.ts +5 -0
- package/dist/render-html.js +45 -36
- package/dist/render.d.ts +1 -0
- package/dist/render.js +172 -136
- package/dist/selector.d.ts +55 -0
- package/dist/selector.js +112 -0
- package/dist/serialize.js +12 -0
- package/dist/table.js +27 -1
- package/dist/to-md.js +5 -0
- package/package.json +67 -66
- package/skill/SKILL.md +82 -0
- package/skill/references/authoring.geml +333 -0
package/dist/history.d.ts
CHANGED
|
@@ -27,14 +27,14 @@ interface History {
|
|
|
27
27
|
}
|
|
28
28
|
/** Reconstruct the content of revision `targetId`. */
|
|
29
29
|
export declare function reconstruct(h: History, targetId: string): string;
|
|
30
|
-
export interface
|
|
30
|
+
export interface SaveOpts {
|
|
31
31
|
gemlPath: string;
|
|
32
32
|
historyPath: string;
|
|
33
33
|
summary: string;
|
|
34
34
|
author?: string;
|
|
35
35
|
at?: Date;
|
|
36
36
|
}
|
|
37
|
-
export declare function
|
|
37
|
+
export declare function save(o: SaveOpts): {
|
|
38
38
|
id: string;
|
|
39
39
|
hash: string;
|
|
40
40
|
};
|
|
@@ -63,7 +63,9 @@ export interface RevisionInfo {
|
|
|
63
63
|
current: boolean;
|
|
64
64
|
}
|
|
65
65
|
/** Is the working file byte-identical to the sidecar's tip revision? False
|
|
66
|
-
* means
|
|
66
|
+
* means unsaved drift (e.g. an earlier save was refused by the round-trip
|
|
67
|
+
* gate). Both write paths gate on this so a no-change `save` appends nothing:
|
|
68
|
+
* `geml mcp` before each write (mcp.ts) and `geml history save` (geml.ts). */
|
|
67
69
|
export declare function isCurrent(historyPath: string, gemlPath: string): boolean;
|
|
68
70
|
/** Revisions newest-first, each tagged with the `-N` offset that selects it. */
|
|
69
71
|
export declare function listRevisions(historyPath: string): RevisionInfo[];
|
|
@@ -74,11 +76,12 @@ export declare function listRevisions(historyPath: string): RevisionInfo[];
|
|
|
74
76
|
*
|
|
75
77
|
* There is exactly one selector grammar — `0` (the tip), `-N` (N revisions
|
|
76
78
|
* back), or an unambiguous revision id (prefix, suffix, or exact) — and it is
|
|
77
|
-
* the grammar `history
|
|
78
|
-
* copy-pasteable into `revert --rev`, `history
|
|
79
|
-
* alike. Keeping this in one function is what makes that
|
|
80
|
-
* written twice, and the copy in `restore` never grew the
|
|
81
|
-
* selectors
|
|
79
|
+
* the grammar `history get` prints in its first column, so its output is
|
|
80
|
+
* copy-pasteable into `revert --rev`, `history get <rev>`, and
|
|
81
|
+
* `history restore` alike. Keeping this in one function is what makes that
|
|
82
|
+
* true: it used to be written twice, and the copy in `restore` never grew the
|
|
83
|
+
* `0`/`-N` arm, so the selectors the revision list advertised were rejected by
|
|
84
|
+
* the command that printed a revision. */
|
|
82
85
|
export declare function resolveRevision(h: History, selector: string): string;
|
|
83
86
|
export declare function resolveContent(historyPath: string, selector: string): {
|
|
84
87
|
id: string;
|
package/dist/history.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// GEML History extension —
|
|
1
|
+
// GEML History extension — save / restore / verify.
|
|
2
2
|
//
|
|
3
3
|
// Implements the `.gemlhistory` companion spec: a self-contained, reverse-delta
|
|
4
4
|
// version history beside the live `.geml` file. The history file is itself a
|
|
5
5
|
// GEML document (meta + keyframe + revision + blob blocks). Reverse patches and
|
|
6
|
-
// hashes are tool-generated here; every
|
|
6
|
+
// hashes are tool-generated here; every save re-applies its reverse patch and
|
|
7
7
|
// asserts a byte-exact round-trip before writing (the spec's verify gate).
|
|
8
8
|
//
|
|
9
9
|
// Revision id = `<YYYYMMDDTHHMMSSZ>-<first 8 hex of the version content hash>`.
|
|
@@ -83,7 +83,7 @@ function fenceFor(contentLf) {
|
|
|
83
83
|
// Unit-key = `#id` (explicit), or `@<8hex content hash>` (derived), with `~n`
|
|
84
84
|
// disambiguating equal keys by document-order occurrence (§4). `~n` on an #id
|
|
85
85
|
// key only arises for OUT-OF-SPEC documents that repeat an id — without it the
|
|
86
|
-
// key is ambiguous, reverse-patch ops hit the wrong occurrence, and
|
|
86
|
+
// key is ambiguous, reverse-patch ops hit the wrong occurrence, and save()'s
|
|
87
87
|
// round-trip gate (correctly) aborts. Well-formed documents never emit it.
|
|
88
88
|
const KEY = String.raw `(#[A-Za-z][A-Za-z0-9_-]*(?:~\d+)?|@[0-9a-f]+(?:~\d+)?)`;
|
|
89
89
|
function sha8(s) {
|
|
@@ -267,7 +267,7 @@ function applyReverse(textLf, ops, blobs) {
|
|
|
267
267
|
*
|
|
268
268
|
* If keys were ever non-unique (no public entry point produces that — the only
|
|
269
269
|
* caller, diffReverse, feeds keyedUnits output), posInB keeps b's LAST index
|
|
270
|
-
* per key and the LIS still yields *a* valid monotonic matching;
|
|
270
|
+
* per key and the LIS still yields *a* valid monotonic matching; save()'s
|
|
271
271
|
* byte-exact round-trip gate rejects any diff that fails to reproduce the
|
|
272
272
|
* parent regardless. */
|
|
273
273
|
function lcsMatch(a, b) {
|
|
@@ -478,7 +478,7 @@ function renderHistory(h, baseName) {
|
|
|
478
478
|
}
|
|
479
479
|
return parts.join("\n");
|
|
480
480
|
}
|
|
481
|
-
export function
|
|
481
|
+
export function save(o) {
|
|
482
482
|
const { lf: working, nl } = loadBytes(o.gemlPath);
|
|
483
483
|
const hash = fullHash(working, nl);
|
|
484
484
|
const stamp = stampUTC(o.at ?? new Date());
|
|
@@ -494,10 +494,10 @@ export function commit(o) {
|
|
|
494
494
|
const blobMap = new Map(patch.blobs.map((b) => [b.id, b.payload]));
|
|
495
495
|
const back = applyReverse(working, patch.ops, blobMap);
|
|
496
496
|
if (bytesOf(back, nl).compare(bytesOf(prevContent, nl)) !== 0) {
|
|
497
|
-
throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting
|
|
497
|
+
throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting save");
|
|
498
498
|
}
|
|
499
|
-
// Blob ids are minted per-diff (b1, b2, …). Renumber this
|
|
500
|
-
// start past the highest id already stored, so a later
|
|
499
|
+
// Blob ids are minted per-diff (b1, b2, …). Renumber this save's blobs to
|
|
500
|
+
// start past the highest id already stored, so a later save never reuses
|
|
501
501
|
// an earlier revision's blob id — an overwrite in the shared store silently
|
|
502
502
|
// corrupts reconstruction of older revisions, whose `replace … <- blob:bN`
|
|
503
503
|
// would then resolve to the wrong (newer) content.
|
|
@@ -597,7 +597,9 @@ export function restore(o) {
|
|
|
597
597
|
if (existsSync(o.gemlPath)) {
|
|
598
598
|
const { lf, nl } = loadBytes(o.gemlPath);
|
|
599
599
|
if (fullHash(lf, nl) !== h.revisions.get(h.current).hash && !o.force) {
|
|
600
|
-
|
|
600
|
+
// "save first" names the live verb: this string used to say `commit`,
|
|
601
|
+
// which the four-verb collapse removed (design §2).
|
|
602
|
+
throw new Error("history: uncommitted changes in doc.geml; rerun with force to discard them, or save first");
|
|
601
603
|
}
|
|
602
604
|
}
|
|
603
605
|
// destructive linear truncation to `target`
|
|
@@ -623,7 +625,9 @@ export function restore(o) {
|
|
|
623
625
|
return content;
|
|
624
626
|
}
|
|
625
627
|
/** Is the working file byte-identical to the sidecar's tip revision? False
|
|
626
|
-
* means
|
|
628
|
+
* means unsaved drift (e.g. an earlier save was refused by the round-trip
|
|
629
|
+
* gate). Both write paths gate on this so a no-change `save` appends nothing:
|
|
630
|
+
* `geml mcp` before each write (mcp.ts) and `geml history save` (geml.ts). */
|
|
627
631
|
export function isCurrent(historyPath, gemlPath) {
|
|
628
632
|
const h = parseHistory(historyPath);
|
|
629
633
|
const tip = h.revisions.get(h.current);
|
|
@@ -647,11 +651,12 @@ export function listRevisions(historyPath) {
|
|
|
647
651
|
*
|
|
648
652
|
* There is exactly one selector grammar — `0` (the tip), `-N` (N revisions
|
|
649
653
|
* back), or an unambiguous revision id (prefix, suffix, or exact) — and it is
|
|
650
|
-
* the grammar `history
|
|
651
|
-
* copy-pasteable into `revert --rev`, `history
|
|
652
|
-
* alike. Keeping this in one function is what makes that
|
|
653
|
-
* written twice, and the copy in `restore` never grew the
|
|
654
|
-
* selectors
|
|
654
|
+
* the grammar `history get` prints in its first column, so its output is
|
|
655
|
+
* copy-pasteable into `revert --rev`, `history get <rev>`, and
|
|
656
|
+
* `history restore` alike. Keeping this in one function is what makes that
|
|
657
|
+
* true: it used to be written twice, and the copy in `restore` never grew the
|
|
658
|
+
* `0`/`-N` arm, so the selectors the revision list advertised were rejected by
|
|
659
|
+
* the command that printed a revision. */
|
|
655
660
|
export function resolveRevision(h, selector) {
|
|
656
661
|
const off = /^(0|-\d+)$/.exec(selector);
|
|
657
662
|
if (off) {
|
package/dist/mcp.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export declare function allTools(): Tool[];
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function loadGraphTools(): Promise<Tool[]>;
|
|
24
24
|
export declare function handleLine(line: string, write?: (s: string) => void): void;
|
|
25
|
-
export declare const MCP_USAGE = "usage: geml mcp --root <dir> [--graph <dir>] [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the\n read-only code-graph tools when the root holds a code graph.\n\n --root <dir> REQUIRED. Root directory holding the .geml documents.\n Relative paths resolve against the server process's CWD,\n which the CLIENT chooses \u2014 pass an absolute path.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --graph <dir> Code-graph directory, inside --root. Defaults to\n <root>/.geml-code-graph when that holds an index.geml.\n With no graph, the code-graph tools are not served\n at all (a client sees only the document tools).\n --no-history Do not
|
|
25
|
+
export declare const MCP_USAGE = "usage: geml mcp --root <dir> [--graph <dir>] [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the\n read-only code-graph tools when the root holds a code graph.\n\n --root <dir> REQUIRED. Root directory holding the .geml documents.\n Relative paths resolve against the server process's CWD,\n which the CLIENT chooses \u2014 pass an absolute path.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --graph <dir> Code-graph directory, inside --root. Defaults to\n <root>/.geml-code-graph when that holds an index.geml.\n With no graph, the code-graph tools are not served\n at all (a client sees only the document tools).\n --no-history Do not save a .gemlhistory revision before each\n write. Default is to save one, so geml_revert always\n has a revision to undo to.\n\n Register with a client:\n claude mcp add geml -- geml mcp --root /abs/path/to/repo";
|
|
26
26
|
export declare function parseArgs(args: string[]): McpOptions;
|
package/dist/mcp.js
CHANGED
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
// The mutations run through the CLI rather than re-implementing block editing:
|
|
36
36
|
// the tool table is *defined* as CLI equivalences, and `-o -` already yields
|
|
37
37
|
// the mutated document without touching the file — exactly the "produce, then
|
|
38
|
-
// validate, then
|
|
38
|
+
// validate, then save" order invariant 1 needs.
|
|
39
39
|
import { readFileSync, writeFileSync, existsSync, realpathSync, statSync } from "node:fs";
|
|
40
40
|
import { resolve, dirname, sep } from "node:path";
|
|
41
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
42
|
import { spawnSync } from "node:child_process";
|
|
43
43
|
import { createInterface } from "node:readline";
|
|
44
44
|
import { parse, PARSER_VERSION } from "./geml.js";
|
|
45
|
-
import {
|
|
45
|
+
import { save, listRevisions, isCurrent, resolveContent } from "./history.js";
|
|
46
46
|
// One version for the whole package: `geml --version` and the MCP handshake
|
|
47
47
|
// must not disagree. This used to be its own literal and had drifted to 0.1.0
|
|
48
48
|
// against a 1.4.x package — invisible to everyone except the user reading their
|
|
@@ -200,12 +200,12 @@ function applyWrite(spec) {
|
|
|
200
200
|
if (after === before) {
|
|
201
201
|
return { ok: true, file: spec.file, diagnostics: diags, hint: "No change: the document already had this content." };
|
|
202
202
|
}
|
|
203
|
-
// 3.
|
|
203
|
+
// 3. Save the PRE-write state so this edit is revertible, then write.
|
|
204
204
|
const revision = spec.summary && OPTS.history ? snapshot(real, spec.summary) : undefined;
|
|
205
205
|
writeFileSync(real, after, "utf8");
|
|
206
206
|
return { ok: true, file: spec.file, diagnostics: diags, revision };
|
|
207
207
|
}
|
|
208
|
-
//
|
|
208
|
+
// Save the file's CURRENT bytes as a revision, so the about-to-happen write
|
|
209
209
|
// has something to revert to. A file already identical to its tip needs no
|
|
210
210
|
// second revision.
|
|
211
211
|
function snapshot(realPath, summary) {
|
|
@@ -213,7 +213,7 @@ function snapshot(realPath, summary) {
|
|
|
213
213
|
try {
|
|
214
214
|
if (existsSync(historyPath) && isCurrent(historyPath, realPath))
|
|
215
215
|
return undefined;
|
|
216
|
-
return
|
|
216
|
+
return save({ gemlPath: realPath, historyPath, summary }).id;
|
|
217
217
|
}
|
|
218
218
|
catch {
|
|
219
219
|
// A sidecar that cannot be written must not cost the caller their edit;
|
|
@@ -243,12 +243,27 @@ function docResolver(root, fromFile) {
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
const hashId = (id) => (id.startsWith("#") ? id : `#${id}`);
|
|
246
|
+
// `geml get`/`geml set` take a full block SELECTOR, not only an id: a content
|
|
247
|
+
// address reaches a block the author never named, which is the whole point of
|
|
248
|
+
// `geml_list` now reporting one for those. So a value that is ALREADY a
|
|
249
|
+
// selector must pass through untouched — hashId would turn `@a3f9c1d2` into
|
|
250
|
+
// `#@a3f9c1d2` and address nothing. A bare word is still an id, so the
|
|
251
|
+
// long-standing "id with or without #" contract is unchanged.
|
|
252
|
+
//
|
|
253
|
+
// The parameter is still NAMED `id`: renaming it to `selector` would break
|
|
254
|
+
// every registered client for a cosmetic gain, and both design docs park that
|
|
255
|
+
// rename as a follow-up. The other verbs keep hashId — their CLI counterparts
|
|
256
|
+
// (add/delete/rename/revert) take ids only, so accepting a selector here would
|
|
257
|
+
// promise something the CLI would then refuse.
|
|
258
|
+
// A selector starts with `#` (id or heading line), `@` (content address), or a
|
|
259
|
+
// `=` fence run (type filter). Anything else is a bare id.
|
|
260
|
+
const selectorArg = (s) => (/^([#@]|={3,})/.test(s.trim()) ? s.trim() : `#${s}`);
|
|
246
261
|
const FILE_ARG = { type: "string", description: "Document path relative to the server's --root directory, e.g. notes/spec.geml" };
|
|
247
262
|
export const TOOLS = [
|
|
248
263
|
// ----- read -----
|
|
249
264
|
{
|
|
250
265
|
name: "geml_list",
|
|
251
|
-
description: "List every addressable block in a GEML document: its
|
|
266
|
+
description: "List every addressable block in a GEML document: its address, kind, and heading text. Call this FIRST — the `id` values it returns are what every other tool in this server addresses. Cheaper and more reliable than reading the file to find out what is in it. Rows marked `anon` have no `#id` (their `address` is a type or content address the CLI understands); this server's other tools take an `id`, so give such a block an id before addressing it here.",
|
|
252
267
|
inputSchema: { type: "object", properties: { file: FILE_ARG }, required: ["file"] },
|
|
253
268
|
run: (args) => {
|
|
254
269
|
const real = resolveInRoot(args.file);
|
|
@@ -260,21 +275,46 @@ export const TOOLS = [
|
|
|
260
275
|
},
|
|
261
276
|
{
|
|
262
277
|
name: "geml_get",
|
|
263
|
-
description: "Read ONE block from a GEML document
|
|
278
|
+
description: "Read ONE block from a GEML document. Use this instead of reading the whole file: it returns only that block, typically a few percent of the document. Call `geml_list` first and pass back the `address` it gives — that also reaches blocks with no `#id`, which an id alone cannot.",
|
|
264
279
|
inputSchema: {
|
|
265
280
|
type: "object",
|
|
266
281
|
properties: {
|
|
267
282
|
file: FILE_ARG,
|
|
268
|
-
id: {
|
|
283
|
+
id: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, or a `@<hex>` content address for a block with no id — the forms `geml_list` prints",
|
|
286
|
+
},
|
|
287
|
+
view: {
|
|
288
|
+
type: "boolean",
|
|
289
|
+
description: "Read THROUGH an `embed` block to the entity block it stands for, following a multi-layer chain to its end. An `embed` has no content of its own, so this is the only way to see what it points at; on any other block it changes nothing. Returns {from, content}: `from` names the document the content actually came from, and its references and relative paths resolve against THAT document, not this one.",
|
|
290
|
+
},
|
|
291
|
+
part: {
|
|
292
|
+
type: "string",
|
|
293
|
+
enum: ["whole", "head", "body"],
|
|
294
|
+
description: "How much of the block to return (default: whole). `body` is usually what you want together with `view`.",
|
|
295
|
+
},
|
|
269
296
|
},
|
|
270
297
|
required: ["file", "id"],
|
|
271
298
|
},
|
|
272
299
|
run: (args) => {
|
|
273
300
|
const real = resolveInRoot(args.file);
|
|
274
|
-
const
|
|
301
|
+
const sel = selectorArg(args.id);
|
|
302
|
+
// Same name, same enum, same validation as `geml_set` — one concept for a
|
|
303
|
+
// model to learn, and `body` is already taken there for the replacement text.
|
|
304
|
+
const part = args.part ?? "whole";
|
|
305
|
+
if (!["whole", "head", "body"].includes(part))
|
|
306
|
+
throw new Error(`part must be whole|head|body, got \`${part}\``);
|
|
307
|
+
const flag = part === "head" ? ["--head"] : part === "body" ? ["--body"] : [];
|
|
308
|
+
const run = runCli(["get", real, sel, ...flag, ...(args.view ? ["--view"] : [])]);
|
|
275
309
|
if (!run.ok)
|
|
276
|
-
throw new Error(run.stderr || `
|
|
277
|
-
|
|
310
|
+
throw new Error(run.stderr || `nothing matches ${sel}`);
|
|
311
|
+
if (!args.view)
|
|
312
|
+
return run.stdout;
|
|
313
|
+
// There is no stderr across an MCP call, and provenance is mandatory: lift
|
|
314
|
+
// it out of the CLI's pinned `view: <sel> -> <doc>[#<id>]` line into a
|
|
315
|
+
// field of its own.
|
|
316
|
+
const m = /^view: .*? -> (.+)$/m.exec(run.stderr);
|
|
317
|
+
return { from: m ? m[1].trim() : null, content: run.stdout };
|
|
278
318
|
},
|
|
279
319
|
},
|
|
280
320
|
{
|
|
@@ -304,14 +344,41 @@ export const TOOLS = [
|
|
|
304
344
|
},
|
|
305
345
|
{
|
|
306
346
|
name: "geml_history",
|
|
307
|
-
|
|
308
|
-
|
|
347
|
+
// The name mirrors the CLI COMMAND PATH (`geml history`), not a verb: this
|
|
348
|
+
// group's only read verb is `get`, and it is the only one that belongs on a
|
|
349
|
+
// server an agent drives (`save` would insert hand-made revisions between
|
|
350
|
+
// the automatic pre-write ones, and `restore` rewrites a whole file where
|
|
351
|
+
// the agent already has block-level geml_revert). So there will be no second
|
|
352
|
+
// history tool to disambiguate from, and `_get` would be a suffix that
|
|
353
|
+
// distinguishes nothing — design §5.
|
|
354
|
+
description: "Read a document's recorded history. WITHOUT `rev`: list every revision, newest first — each entry's `offset` is the selector `geml_revert` takes as `rev` (-1 is the revision before the current one), and an empty list means the document has no sidecar yet and nothing can be reverted. WITH `rev`: the full text of that one revision, for reading what the document looked like then without restoring it.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {
|
|
358
|
+
file: FILE_ARG,
|
|
359
|
+
rev: { type: "string", description: "Revision selector — `0` for the current tip, `-N` for N revisions back, or a revision id from the list. Omit it to get the list instead of one revision's text." },
|
|
360
|
+
},
|
|
361
|
+
required: ["file"],
|
|
362
|
+
},
|
|
309
363
|
run: (args) => {
|
|
310
364
|
const real = resolveInRoot(args.file);
|
|
311
365
|
const historyPath = real.replace(/\.geml$/, "") + ".gemlhistory";
|
|
312
|
-
|
|
366
|
+
const rev = args.rev === undefined ? undefined : String(args.rev);
|
|
367
|
+
if (!existsSync(historyPath)) {
|
|
368
|
+
// Naming a revision of a document that has no history at all is an
|
|
369
|
+
// error, not an empty result: the caller asked for specific content.
|
|
370
|
+
// The LIST tier stays a plain empty answer — "nothing yet" is a real,
|
|
371
|
+
// useful state there.
|
|
372
|
+
if (rev !== undefined)
|
|
373
|
+
throw new Error(`no .gemlhistory sidecar for ${args.file} yet, so revision ${rev} does not exist — the first write through this server creates one`);
|
|
313
374
|
return { file: args.file, revisions: [], note: "no .gemlhistory sidecar yet — the first write through this server creates one" };
|
|
314
|
-
|
|
375
|
+
}
|
|
376
|
+
if (rev === undefined)
|
|
377
|
+
return { file: args.file, revisions: listRevisions(historyPath) };
|
|
378
|
+
// resolveContent() is the CLI's own path for `geml history get <file>
|
|
379
|
+
// <rev>`, so one selector grammar answers on both surfaces.
|
|
380
|
+
const { id, text } = resolveContent(historyPath, rev);
|
|
381
|
+
return { file: args.file, id, text };
|
|
315
382
|
},
|
|
316
383
|
},
|
|
317
384
|
{
|
|
@@ -362,12 +429,15 @@ export const TOOLS = [
|
|
|
362
429
|
// ----- write -----
|
|
363
430
|
{
|
|
364
431
|
name: "geml_set",
|
|
365
|
-
description: "Replace ONE block,
|
|
432
|
+
description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. `part` selects whole block (default), just the head/fence line, or just the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.",
|
|
366
433
|
inputSchema: {
|
|
367
434
|
type: "object",
|
|
368
435
|
properties: {
|
|
369
436
|
file: FILE_ARG,
|
|
370
|
-
id: {
|
|
437
|
+
id: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Which block to replace: an id (with or without `#`), or a `@<hex>` content address from `geml_list` for a block with no id. Must match exactly one block",
|
|
440
|
+
},
|
|
371
441
|
body: { type: "string", description: "The replacement text" },
|
|
372
442
|
part: { type: "string", enum: ["whole", "head", "body"], description: "What to replace (default: whole)" },
|
|
373
443
|
},
|
|
@@ -381,9 +451,9 @@ export const TOOLS = [
|
|
|
381
451
|
const flag = part === "head" ? ["--head"] : part === "body" ? ["--body"] : [];
|
|
382
452
|
return applyWrite({
|
|
383
453
|
file: args.file,
|
|
384
|
-
cliArgs: ["set", real,
|
|
454
|
+
cliArgs: ["set", real, selectorArg(args.id), ...flag, "--in", "-", "-o", "-"],
|
|
385
455
|
input: args.body,
|
|
386
|
-
summary: `mcp: before write to ${
|
|
456
|
+
summary: `mcp: before write to ${selectorArg(args.id)}`,
|
|
387
457
|
});
|
|
388
458
|
},
|
|
389
459
|
},
|
|
@@ -619,25 +689,25 @@ export function handleLine(line, write = (s) => process.stdout.write(s)) {
|
|
|
619
689
|
// ---------------------------------------------------------------------------
|
|
620
690
|
// Entry
|
|
621
691
|
// ---------------------------------------------------------------------------
|
|
622
|
-
export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
623
|
-
|
|
624
|
-
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
|
|
625
|
-
read-only code-graph tools when the root holds a code graph.
|
|
626
|
-
|
|
627
|
-
--root <dir> REQUIRED. Root directory holding the .geml documents.
|
|
628
|
-
Relative paths resolve against the server process's CWD,
|
|
629
|
-
which the CLIENT chooses — pass an absolute path.
|
|
630
|
-
Every path a client names is confined to this directory;
|
|
631
|
-
a client cannot widen or override it.
|
|
632
|
-
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
633
|
-
<root>/.geml-code-graph when that holds an index.geml.
|
|
634
|
-
With no graph, the code-graph tools are not served
|
|
635
|
-
at all (a client sees only the document tools).
|
|
636
|
-
--no-history Do not
|
|
637
|
-
write. Default is to
|
|
638
|
-
has a revision to undo to.
|
|
639
|
-
|
|
640
|
-
Register with a client:
|
|
692
|
+
export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
693
|
+
|
|
694
|
+
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
|
|
695
|
+
read-only code-graph tools when the root holds a code graph.
|
|
696
|
+
|
|
697
|
+
--root <dir> REQUIRED. Root directory holding the .geml documents.
|
|
698
|
+
Relative paths resolve against the server process's CWD,
|
|
699
|
+
which the CLIENT chooses — pass an absolute path.
|
|
700
|
+
Every path a client names is confined to this directory;
|
|
701
|
+
a client cannot widen or override it.
|
|
702
|
+
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
703
|
+
<root>/.geml-code-graph when that holds an index.geml.
|
|
704
|
+
With no graph, the code-graph tools are not served
|
|
705
|
+
at all (a client sees only the document tools).
|
|
706
|
+
--no-history Do not save a .gemlhistory revision before each
|
|
707
|
+
write. Default is to save one, so geml_revert always
|
|
708
|
+
has a revision to undo to.
|
|
709
|
+
|
|
710
|
+
Register with a client:
|
|
641
711
|
claude mcp add geml -- geml mcp --root /abs/path/to/repo`;
|
|
642
712
|
export function parseArgs(args) {
|
|
643
713
|
let root;
|
package/dist/render-html.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { type Document } from "./geml.js";
|
|
2
2
|
import { type RenderOptions } from "./render.js";
|
|
3
3
|
export declare function renderHtml(doc: Document, opts?: RenderOptions): string;
|
|
4
|
+
export declare const pageAssets: {
|
|
5
|
+
readonly css: "\n:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }\n* { box-sizing: border-box; }\nbody { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,\"PingFang SC\",\"Microsoft Yahei\",sans-serif; }\nmain { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }\nh1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }\nh1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh3 { font-size:1.25em; } h4 { font-size:1em; }\np { margin:.7em 0; }\na { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }\ncode { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\npre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }\npre code { background:none; padding:0; font-size:.85em; }\npre.output { background:#0d1117; color:#e6edf3; }\npre.output code { color:inherit; }\nul,ol { padding-left:1.6em; } li { margin:.2em 0; }\nul.task-list { list-style:none; padding-left:.2em; }\nli.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }\nli.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }\nli.task input[type=checkbox]:checked::after { content:\"✓\"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }\naside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }\naside.aside { border-left-color:#8b949e; background:#f6f8fa; }\naside.warning { border-left-color:#d97706; background:#fff8f0; }\naside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }\nfigure { margin:1.2em 0; }\nfigcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }\ntable.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }\ntable.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }\ntable.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }\ntable.geml-table thead th::after { content:\" \\2195\"; color:var(--muted); font-size:.8em; }\ntable.geml-table thead th.asc::after { content:\" \\2191\"; color:var(--accent); }\ntable.geml-table thead th.desc::after { content:\" \\2193\"; color:var(--accent); }\ntable.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }\ntable.geml-table td.computed { color:#0a7c52; }\ntable.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }\n.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }\n.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }\n.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }\n.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }\n.c-title { font-size:15px; font-weight:600; fill:var(--fg); }\n.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }\n.media { max-width:100%; border-radius:8px; }\n.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }\n.math-block { overflow-x:auto; padding:.4em 0; }\nsup.fn a { font-size:.75em; }\n.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }\n.geml-footer code { font-size:.95em; }\n.code-graph { margin:1.4em 0; }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }\n.cg-svg { display:block; }\n.cg-search-wrap { position:relative; display:inline-block; }\n.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }\n.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }\n.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }\n.cg-search-row:hover { background:var(--bd); }\n.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }\n.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }\n.cg-search-grp:first-of-type { border-top:0; }\n.cg-stage { display:flex; gap:10px; align-items:flex-start; }\n.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }\n.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }\n.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }\n.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }\n.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }\n.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }\n.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }\n.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }\n.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }\n.cg-crumb .cg-seg:hover { text-decoration:underline; }\n.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }\n.cg-flash { color:#b42318; }\n.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }\n.cg-upbtn { cursor:pointer; }\n.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }\n.cg-upbtn text { font-size:11px; fill:#57606a; }\n.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }\n.cg-upbtn:hover text { fill:var(--accent); }\n.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }\n.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }\n.cg-chip { display:inline-flex; align-items:center; gap:4px; }\n.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }\n.cg-note { font-size:.8em; color:#9a6700; }\n.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }\n.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }\n.cg-n { cursor:pointer; }\n.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }\n.cg-n.leaf { opacity:.45; }\n.cg-n.test rect { stroke-dasharray:3 2; }\n.cg-n.grp rect { stroke-width:1.8; }\n.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }\n.cg-e.cand { stroke-dasharray:2 3; }\n.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }\n.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\n.cg-e.soft { opacity:.55; }\n.cg-svg.hl .cg-n { opacity:.22; }\n.cg-svg.hl .cg-e { opacity:.1; }\n.cg-svg.hl .cg-n.hl { opacity:1; }\n.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }\n";
|
|
6
|
+
readonly js: "\n(function () {\n function cmp(a, b) {\n var na = a.dataset.sort, nb = b.dataset.sort;\n if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);\n return (a.textContent || \"\").localeCompare(b.textContent || \"\");\n }\n document.querySelectorAll(\"table.geml-table\").forEach(function (table) {\n var tbody = table.tBodies[0];\n if (!tbody) return;\n // Sort on header click.\n var ths = table.tHead ? table.tHead.rows[0].cells : [];\n Array.prototype.forEach.call(ths, function (th, col) {\n th.addEventListener(\"click\", function () {\n var dir = th.classList.contains(\"asc\") ? \"desc\" : \"asc\";\n Array.prototype.forEach.call(ths, function (h) { h.classList.remove(\"asc\", \"desc\"); });\n th.classList.add(dir);\n var rows = Array.prototype.slice.call(tbody.rows);\n rows.sort(function (r1, r2) {\n var c = cmp(r1.cells[col], r2.cells[col]);\n return dir === \"asc\" ? c : -c;\n });\n rows.forEach(function (r) { tbody.appendChild(r); });\n });\n });\n // Filter rows.\n var fig = table.closest(\".table-figure\");\n var input = fig ? fig.querySelector(\".table-filter\") : null;\n if (input) input.addEventListener(\"input\", function () {\n var q = input.value.toLowerCase();\n Array.prototype.forEach.call(tbody.rows, function (r) {\n r.style.display = (r.textContent || \"\").toLowerCase().indexOf(q) >= 0 ? \"\" : \"none\";\n });\n });\n });\n})();\n";
|
|
7
|
+
readonly codeGraphJs: string;
|
|
8
|
+
};
|
package/dist/render-html.js
CHANGED
|
@@ -33,46 +33,46 @@ function page(title, body, ctx, source) {
|
|
|
33
33
|
const wantLive = ctx.usedCodeGraph && !!ctx.opts.liveGraph;
|
|
34
34
|
const lg = wantLive ? escAttr(ctx.opts.liveGraph) : "";
|
|
35
35
|
const importMap = wantLive
|
|
36
|
-
? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n`
|
|
36
|
+
? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js","node:os":"${lg}_node-stub.js"}}</script>\n`
|
|
37
37
|
: "";
|
|
38
38
|
const liveJs = wantLive
|
|
39
|
-
? `<script type="module">
|
|
40
|
-
globalThis.process ??= { argv: [], env: {} };
|
|
41
|
-
const { parse } = await import("${lg}geml.js");
|
|
42
|
-
const { codeGraphWaves } = await import("${lg}render.js");
|
|
43
|
-
const w = codeGraphWaves(async (rel) => {
|
|
44
|
-
try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
|
|
45
|
-
}, parse);
|
|
46
|
-
for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
|
|
47
|
-
const start = m.getAttribute("data-start");
|
|
48
|
-
m._cgView = async (view) => {
|
|
49
|
-
// A directed view builds from the node's OWN document (its meta names the
|
|
50
|
-
// module and graph-depth); {doc} opens that document; else the mount's.
|
|
51
|
-
const src = view && view.doc ? view.doc
|
|
52
|
-
: view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
|
|
53
|
-
: start;
|
|
54
|
-
const r = await w.build(src, view && view.doc ? undefined : view);
|
|
55
|
-
return r.error !== undefined ? null : r.data;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
39
|
+
? `<script type="module">
|
|
40
|
+
globalThis.process ??= { argv: [], env: {} };
|
|
41
|
+
const { parse } = await import("${lg}geml.js");
|
|
42
|
+
const { codeGraphWaves } = await import("${lg}render.js");
|
|
43
|
+
const w = codeGraphWaves(async (rel) => {
|
|
44
|
+
try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
|
|
45
|
+
}, parse);
|
|
46
|
+
for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
|
|
47
|
+
const start = m.getAttribute("data-start");
|
|
48
|
+
m._cgView = async (view) => {
|
|
49
|
+
// A directed view builds from the node's OWN document (its meta names the
|
|
50
|
+
// module and graph-depth); {doc} opens that document; else the mount's.
|
|
51
|
+
const src = view && view.doc ? view.doc
|
|
52
|
+
: view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
|
|
53
|
+
: start;
|
|
54
|
+
const r = await w.build(src, view && view.doc ? undefined : view);
|
|
55
|
+
return r.error !== undefined ? null : r.data;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
58
|
</script>\n`
|
|
59
59
|
: "";
|
|
60
|
-
return `<!doctype html>
|
|
61
|
-
<html lang="en">
|
|
62
|
-
<head>
|
|
63
|
-
<meta charset="utf-8">
|
|
64
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
65
|
-
<title>${esc(title)}</title>
|
|
66
|
-
<style>${CSS}</style>
|
|
67
|
-
${importMap}${mathHead}${mermaidHead}</head>
|
|
68
|
-
<body>
|
|
69
|
-
<main>
|
|
70
|
-
${body}
|
|
71
|
-
</main>
|
|
72
|
-
${footer}
|
|
73
|
-
<script>${JS}</script>
|
|
74
|
-
${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
|
|
75
|
-
</html>
|
|
60
|
+
return `<!doctype html>
|
|
61
|
+
<html lang="en">
|
|
62
|
+
<head>
|
|
63
|
+
<meta charset="utf-8">
|
|
64
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
65
|
+
<title>${esc(title)}</title>
|
|
66
|
+
<style>${CSS}</style>
|
|
67
|
+
${importMap}${mathHead}${mermaidHead}</head>
|
|
68
|
+
<body>
|
|
69
|
+
<main>
|
|
70
|
+
${body}
|
|
71
|
+
</main>
|
|
72
|
+
${footer}
|
|
73
|
+
<script>${JS}</script>
|
|
74
|
+
${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
|
|
75
|
+
</html>
|
|
76
76
|
`;
|
|
77
77
|
}
|
|
78
78
|
export function renderHtml(doc, opts = {}) {
|
|
@@ -90,6 +90,15 @@ export function renderHtml(doc, opts = {}) {
|
|
|
90
90
|
: `layered method flow — roots: in-degree-zero methods (no <code>entry</code> declared)`;
|
|
91
91
|
body = ctx.codeGraphFigure(opts.source, "", `<figcaption>${cap}</figcaption>`) + "\n" + body;
|
|
92
92
|
}
|
|
93
|
+
// Fragment mode: the body markup alone, for embedding into an existing
|
|
94
|
+
// layout. No shell, no CDN tags, no inline CSS/JS — see RenderOptions.
|
|
95
|
+
if (opts.fragment)
|
|
96
|
+
return body + "\n";
|
|
93
97
|
const title = opts.title ?? ctx.docTitle() ?? "GEML document";
|
|
94
98
|
return page(title, body, ctx, opts.source);
|
|
95
99
|
}
|
|
100
|
+
// The page shell's static assets, for fragment consumers: `css` styles every
|
|
101
|
+
// geml-* class a fragment emits (include once per site); `js` is the tables'
|
|
102
|
+
// sort/filter enhancement (once per page); `codeGraphJs` matters only when a
|
|
103
|
+
// fragment carries a code-graph mount. The full-page output inlines all three.
|
|
104
|
+
export const pageAssets = { css: CSS, js: JS, codeGraphJs: CODE_GRAPH_JS };
|
package/dist/render.d.ts
CHANGED