@geml/geml 1.5.0 → 1.5.1

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/dist/geml.js CHANGED
@@ -154,7 +154,7 @@ function registerId(ctx, id, line) {
154
154
  }
155
155
  }
156
156
  // §5: a list marker — `-`/`*` (unordered) or `N.` (ordered) — capturing the
157
- // leading indent (in spaces; a tab counts as one) and the item content. Nesting
157
+ // leading indent (in spaces; a tab counts as 4) and the item content. Nesting
158
158
  // is decided by that indent.
159
159
  const MARKER = /^([ \t]*)(?:[-*]|(\d+)\.)[ \t]+(.*)$/;
160
160
  function matchMarker(line) {
@@ -162,7 +162,14 @@ function matchMarker(line) {
162
162
  if (!m)
163
163
  return null;
164
164
  const ordered = m[2] !== undefined;
165
- const mk = { indent: m[1].length, ordered, rest: m[3] };
165
+ let indent = 0;
166
+ for (const ch of m[1]) {
167
+ if (ch === '\t')
168
+ indent += 4;
169
+ else
170
+ indent += 1;
171
+ }
172
+ const mk = { indent, ordered, rest: m[3] };
166
173
  if (ordered)
167
174
  mk.start = parseInt(m[2], 10);
168
175
  return mk;
@@ -249,7 +256,26 @@ function scanBlocks(lines, base, ctx, depth = 0) {
249
256
  const diags = ctx.diags;
250
257
  let i = 0;
251
258
  while (i < lines.length) {
252
- const line = lines[i];
259
+ let line = lines[i];
260
+ let consumed = 1;
261
+ // C-01: Attribute line continuation via `\`.
262
+ // If a line looks like a fence or heading and ends with `\`, fold subsequent lines.
263
+ if ((line.startsWith("===") || line.startsWith("#")) && line.endsWith("\\")) {
264
+ let folded = line.slice(0, -1).trimEnd();
265
+ while (i + consumed < lines.length) {
266
+ const next = lines[i + consumed].trim();
267
+ if (next.endsWith("\\")) {
268
+ folded += " " + next.slice(0, -1).trimEnd();
269
+ consumed++;
270
+ }
271
+ else {
272
+ folded += " " + next;
273
+ consumed++;
274
+ break;
275
+ }
276
+ }
277
+ line = folded;
278
+ }
253
279
  if (line.trim() === "") {
254
280
  i++;
255
281
  continue;
@@ -259,24 +285,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
259
285
  const hid = /^[ \t]*%%[ \t]?(.*)$/.exec(line);
260
286
  if (hid) {
261
287
  blocks.push({ kind: "hidden", text: hid[1] });
262
- i++;
263
- continue;
264
- }
265
- // §5.2: a Markdown-style footnote definition `[^id]: text` defines the
266
- // target a `[^id]` reference points at — recorded as a note block with that
267
- // id, so the reference resolves. (A model that reaches for Markdown
268
- // footnotes by habit then "just works" instead of leaving a dangling ref.)
269
- const fndef = /^\[\^([^\]]+)\]:[ \t]?(.*)$/.exec(line);
270
- if (fndef) {
271
- const id = fndef[1].trim();
272
- const lineNo = base + i + 1;
273
- registerId(ctx, id, lineNo);
274
- const text = interpolate(fndef[2], lineNo, ctx);
275
- blocks.push({
276
- kind: "block", type: "note", mode: "flow", id, classes: ["footnote"], attrs: {},
277
- children: [{ kind: "paragraph", text, inlines: parseInline(text, lineNo, ctx) }],
278
- });
279
- i++;
288
+ i += consumed;
280
289
  continue;
281
290
  }
282
291
  const open = FENCE_OPEN.exec(line);
@@ -292,7 +301,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
292
301
  // safe way to nest (§3).
293
302
  const labeled = attrs.id !== undefined ? new RegExp(`^={3,}[ \\t]+#${reLit(attrs.id)}[ \\t]*$`) : null;
294
303
  const body = [];
295
- let j = i + 1;
304
+ let j = i + consumed;
296
305
  let closed = false;
297
306
  for (; j < lines.length; j++) {
298
307
  if (isCloseFence(lines[j], openLen) || (labeled && labeled.test(lines[j]))) {
@@ -310,6 +319,31 @@ function scanBlocks(lines, base, ctx, depth = 0) {
310
319
  diags.push({ severity: "warning", code: "unknown-block-type", message: `unknown block type \`${type}\`; body kept as raw`, line: openLineNo });
311
320
  mode = "raw";
312
321
  }
322
+ else {
323
+ // `hidden` (§4) and `caption` (§4, and the label an auto-reference takes
324
+ // per §5.2) are not type-specific: every typed block may carry them. Only
325
+ // the extras below are per type.
326
+ let validRe;
327
+ if (type === "table")
328
+ validRe = /^(src|format|header|format-data|compute\d*|summary\d*|span\d*)$/;
329
+ else if (type === "embed")
330
+ validRe = /^(src)$/;
331
+ else if (type === "diagram")
332
+ validRe = /^(src|data|format|type|rows|x|y|size|series)$/;
333
+ // `src`/`anchor` on a `code` block are the code-graph profile's
334
+ // (docs/codemap-profile.md): every document `geml codemap build` writes
335
+ // carries them, so warning on them would warn on our own output.
336
+ else if (type === "code")
337
+ validRe = /^(lang|src|anchor|name|entry-via)$/;
338
+ else
339
+ validRe = /^$/;
340
+ const universal = /^(hidden|caption)$/;
341
+ for (const key of Object.keys(attrs.attrs)) {
342
+ if (!universal.test(key) && !validRe.test(key)) {
343
+ diags.push({ severity: "warning", code: "unknown-attribute", message: `unknown attribute \`${key}\` for block type \`${type}\``, line: openLineNo });
344
+ }
345
+ }
346
+ }
313
347
  const block = {
314
348
  kind: "block", type, mode, classes: attrs.classes, attrs: attrs.attrs,
315
349
  };
@@ -382,18 +416,11 @@ function scanBlocks(lines, base, ctx, depth = 0) {
382
416
  else {
383
417
  block.raw = body;
384
418
  if (type === "table") {
385
- // `src=` and `data=` are one attribute in two spellings: where this
386
- // table's data comes from. Recorded for the post-scan pass.
387
419
  const srcAttr = typeof attrs.attrs["src"] === "string" ? attrs.attrs["src"].trim() : undefined;
388
- const dataAttr = typeof attrs.attrs["data"] === "string" ? attrs.attrs["data"].trim() : undefined;
389
- if (srcAttr !== undefined && dataAttr !== undefined) {
390
- diags.push({ severity: "error", code: "source-attr-conflict", message: "table has both `src=` and `data=`; they mean the same thing — use one", line: openLineNo });
391
- }
392
- const target = srcAttr ?? dataAttr;
393
420
  // §6: parse the raw body (visual or csv/tsv) into one table model.
394
- const { model, diagnostics } = parseTable(body, target === undefined ? attrs.attrs : { ...attrs.attrs, src: target }, openLineNo, ctx);
395
- if (target !== undefined)
396
- (ctx.tableSources ??= []).push({ block, line: openLineNo, target });
421
+ const { model, diagnostics } = parseTable(body, attrs.attrs, openLineNo, ctx);
422
+ if (srcAttr !== undefined)
423
+ (ctx.tableSources ??= []).push({ block, line: openLineNo, target: srcAttr });
397
424
  block.table = model;
398
425
  for (const d of diagnostics)
399
426
  diags.push({ ...d, line: openLineNo });
@@ -442,9 +469,10 @@ function scanBlocks(lines, base, ctx, depth = 0) {
442
469
  if (h) {
443
470
  const lineNo = base + i + 1;
444
471
  const level = h[1].length;
445
- const a = h[3] ? parseAttrs(h[3]) : { classes: [], attrs: {} };
446
- const text = interpolate(h[2], lineNo, ctx);
447
- const id = a.id ?? slug(text);
472
+ const rawText = h[2];
473
+ const a = parseAttrs(h[3] ?? "");
474
+ const text = interpolate(rawText, lineNo, ctx);
475
+ const id = a.id ?? slug(rawText);
448
476
  registerId(ctx, id, lineNo);
449
477
  const block = {
450
478
  kind: "heading", level, text, inlines: parseInline(text, lineNo, ctx), id, classes: a.classes, attrs: a.attrs,
@@ -452,7 +480,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
452
480
  if (a.attrs["hidden"] === true)
453
481
  block.hidden = true;
454
482
  blocks.push(block);
455
- i++;
483
+ i += consumed;
456
484
  continue;
457
485
  }
458
486
  if (LIST_ITEM.test(line)) {
@@ -711,7 +739,7 @@ function gatherEmbeds(source) {
711
739
  scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
712
740
  return (ctx.embeds ?? []).map((e) => (e.anchor === undefined ? { doc: e.doc } : { doc: e.doc, anchor: e.anchor }));
713
741
  }
714
- // One rule for "where this data comes from", shared by a table's `src=`/`data=`
742
+ // One rule for "where this data comes from", shared by a table's `src=`
715
743
  // and a chart's `data=`. Three target forms: a data file, `#id` naming a table
716
744
  // block in this document, or `doc.geml#id` naming one in another document. An
717
745
  // unresolvable target is an error — a table whose source silently produced no
@@ -773,11 +801,10 @@ function resolveTableSources(ctx, opts) {
773
801
  err(line, "unresolvable-table-source", `cannot resolve table source \`${target}\``);
774
802
  continue;
775
803
  }
776
- // Reuse the body parser: with `src`/`data` dropped, the file's lines are just
804
+ // Reuse the body parser: with `src` dropped, the file's lines are just
777
805
  // this table's body, so format/header/compute/summary all behave identically.
778
806
  const attrs = { ...block.attrs };
779
807
  delete attrs["src"];
780
- delete attrs["data"];
781
808
  const { model, diagnostics } = parseTable(normalizeSource(text).split("\n"), attrs, line, ctx);
782
809
  model.src = target;
783
810
  block.table = model;
@@ -1004,13 +1031,11 @@ export function parse(source, opts = {}) {
1004
1031
  }
1005
1032
  // The id that a fence/heading line defines, matching how scanBlocks derives it
1006
1033
  // (parseAttrs for the attribute object; heading text slug when no explicit id).
1007
- // The slug MUST come from the INTERPOLATED text scanBlocks slugs after
1008
- // interpolate(), so `# {{title}} Setup` registers the substituted slug; slugging
1009
- // the raw text here would create a phantom id the parser never registered and
1010
- // make the real one unaddressable. `ctx` is an inert context carrying the
1011
- // document's meta (diagnostics are discarded — spans never report).
1034
+ // The slug MUST come from the RAW text, before interpolation, so that changing
1035
+ // a meta variable does not silently change the block's addressable id.
1036
+ // `ctx` is passed just in case future features need context.
1012
1037
  function idOfHeading(braces, text, line, ctx) {
1013
- return (braces ? parseAttrs(braces).id : undefined) ?? slug(interpolate(text, line, ctx));
1038
+ return (braces ? parseAttrs(braces).id : undefined) ?? slug(text);
1014
1039
  }
1015
1040
  // The matching close of the fence opened at lines[i] (equal-length run, or the
1016
1041
  // labeled `=== #id` close when the block carries an id): the index just past
@@ -1047,7 +1072,7 @@ function sectionEnd(lines, i, level) {
1047
1072
  }
1048
1073
  // Walk `lines` exactly as scanBlocks does — same fence close rules (equal-length
1049
1074
  // or labeled `=== #id`), same flow-only recursion via REGISTRY — recording the
1050
- // source span of every addressable id (typed block, heading, footnote def).
1075
+ // source span of every addressable id (typed block, heading).
1051
1076
  // First definition wins, mirroring ctx.ids (a duplicate id is a build error, so
1052
1077
  // `get`/`set` operate on the one the parser actually registered). `base` is the
1053
1078
  // absolute line offset of this slice within the whole document.
@@ -1115,8 +1140,8 @@ types) {
1115
1140
  export function blockSpans(source) {
1116
1141
  const out = new Map();
1117
1142
  const lines = normalizeSource(source).split("\n");
1118
- // Inert context: heading auto-ids slug the interpolated text (parser parity);
1119
- // its diagnostics are discarded the span scan never reports.
1143
+ // Inert context: heading auto-ids slug the raw text, but parseDoc still
1144
+ // requires a valid context to parse the document.
1120
1145
  const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines) };
1121
1146
  collectSpans(lines, 0, out, ctx);
1122
1147
  return out;
@@ -1153,8 +1178,8 @@ function toNewline(text, nl) {
1153
1178
  return nl === "\n" ? lf : lf.replace(/\n/g, nl);
1154
1179
  }
1155
1180
  // `--head`: narrow any id's span to its HEAD line — the single declaring line
1156
- // (a heading's `# … {#id}` line, a typed block's opening fence, a footnote's
1157
- // `[^id]:` line). The head is by construction the FIRST line of the span, so
1181
+ // (a heading's `# … {#id}` line, or a typed block's opening fence). The head is
1182
+ // by construction the FIRST line of the span, so
1158
1183
  // the narrowing is parse-free and needs no type check. Main use: `set --head`
1159
1184
  // edits a block's attributes (caption/compute/lang/…) without re-sending its
1160
1185
  // body, or renames a heading without rewriting its section.
@@ -1247,54 +1272,54 @@ export const PARSER_VERSION = (() => {
1247
1272
  }
1248
1273
  return "0.0.0";
1249
1274
  })();
1250
- const USAGE = `geml — GEML reference CLI
1251
-
1252
- Usage:
1253
- geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
1254
- (--root widens cross-doc resolution to dir d, as on check — an
1255
- === embed whose target sits above the file's own directory
1256
- needs it, or it renders unresolved)
1257
- --to <output>: json | html | md | geml
1258
- --to md -> Markdown (lossy)
1259
- --to html -> self-contained HTML
1260
- --to geml -> canonical re-format
1261
- --to json -> document-model JSON (default)
1262
- --from <input>: geml | md | json (overrides extension; html is output-only)
1263
- geml notes.md -> GEML (md inferred from extension)
1264
- geml model.json --to geml -> GEML (round-trips a prior --to json)
1265
- geml - --from md read Markdown on stdin
1266
- geml get <file.geml|-> [#id] [--json] [--head] with #id: print that block
1267
- (a heading id = its whole section; --head = head line;
1268
- --json = model node). Without #id: list all addressable
1269
- ids (--json = array).
1270
- geml set <file.geml|-> #id [--head|--body] [--in f[#src]|-] [-o f] replace ONE block by id
1271
- (--in F takes F's block #id, F#src takes #src, else stdin raw;
1272
- default = whole block · --head = head line · --body = body)
1273
- geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
1274
- (1+ blocks and/or prose; content keeps its own ids, a clash is refused)
1275
- geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
1276
- (a missing id is skipped; a dangling reference is a warning, not a refusal)
1277
- geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
1278
- geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
1279
- (sel: 0 | -N | id-prefix | changed; default -1)
1280
- geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
1281
- (--root widens cross-doc refs to dir d, e.g. the repo root)
1282
- geml history <commit|verify|show|restore|log> <file.geml> [...] .gemlhistory version sidecar
1283
- geml codemap <build|verify|render|serve|refresh|find> [...] code-graph toolkit (alias: codegraph)
1284
- geml mcp --root <dir> [--graph <dir>] [--no-history] serve documents (and the code graph) over MCP (stdio)
1285
- (10 tools, each geml_ + its CLI verb: list/get/check/history/to +
1286
- set/add/delete/rename/revert; every write is validated before it
1287
- reaches disk. A code graph under --root adds four read-only
1288
- geml_codemap_* tools to the same server)
1289
- geml --help | --version [--json]
1290
-
1291
- Use '-' as the file to read from stdin.
1292
- Mutations (set/add/delete/rename) write the whole updated document in place for a
1293
- file, or to stdout for '-' input; -o redirects it (-o - = stdout).
1294
- Exit codes:
1295
- 0 ok
1296
- 1 document/operation error
1297
- 2 command usage error.
1275
+ const USAGE = `geml — GEML reference CLI
1276
+
1277
+ Usage:
1278
+ geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
1279
+ (--root widens cross-doc resolution to dir d, as on check — an
1280
+ === embed whose target sits above the file's own directory
1281
+ needs it, or it renders unresolved)
1282
+ --to <output>: json | html | md | geml
1283
+ --to md -> Markdown (lossy)
1284
+ --to html -> self-contained HTML
1285
+ --to geml -> canonical re-format
1286
+ --to json -> document-model JSON (default)
1287
+ --from <input>: geml | md | json (overrides extension; html is output-only)
1288
+ geml notes.md -> GEML (md inferred from extension)
1289
+ geml model.json --to geml -> GEML (round-trips a prior --to json)
1290
+ geml - --from md read Markdown on stdin
1291
+ geml get <file.geml|-> [#id] [--json] [--head] with #id: print that block
1292
+ (a heading id = its whole section; --head = head line;
1293
+ --json = model node). Without #id: list all addressable
1294
+ ids (--json = array).
1295
+ geml set <file.geml|-> #id [--head|--body] [--in f[#src]|-] [-o f] replace ONE block by id
1296
+ (--in F takes F's block #id, F#src takes #src, else stdin raw;
1297
+ default = whole block · --head = head line · --body = body)
1298
+ geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
1299
+ (1+ blocks and/or prose; content keeps its own ids, a clash is refused)
1300
+ geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
1301
+ (a missing id is skipped; a dangling reference is a warning, not a refusal)
1302
+ geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
1303
+ geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
1304
+ (sel: 0 | -N | id-prefix | changed; default -1)
1305
+ geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
1306
+ (--root widens cross-doc refs to dir d, e.g. the repo root)
1307
+ geml history <commit|verify|show|restore|log> <file.geml> [...] .gemlhistory version sidecar
1308
+ geml codemap <build|verify|render|serve|refresh|find> [...] code-graph toolkit (alias: codegraph)
1309
+ geml mcp --root <dir> [--graph <dir>] [--no-history] serve documents (and the code graph) over MCP (stdio)
1310
+ (10 tools, each geml_ + its CLI verb: list/get/check/history/to +
1311
+ set/add/delete/rename/revert; every write is validated before it
1312
+ reaches disk. A code graph under --root adds four read-only
1313
+ geml_codemap_* tools to the same server)
1314
+ geml --help | --version [--json]
1315
+
1316
+ Use '-' as the file to read from stdin.
1317
+ Mutations (set/add/delete/rename) write the whole updated document in place for a
1318
+ file, or to stdout for '-' input; -o redirects it (-o - = stdout).
1319
+ Exit codes:
1320
+ 0 ok
1321
+ 1 document/operation error
1322
+ 2 command usage error.
1298
1323
  `;
1299
1324
  // One-line usage for each subcommand — the single source for both the error
1300
1325
  // shown on misuse and the `<cmd> --help` text.
@@ -1307,35 +1332,35 @@ const SUBHELP = {
1307
1332
  check: "usage: geml check <file.geml|-> [--root <dir>] [--json] (--root: resolve cross-doc refs within <dir> instead of the file's own directory)",
1308
1333
  revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--append|--before #x|--after #x] [--head] [--dry-run] [-o out] (reconcile #id to a revision: splice / resurrect / remove; sel: 0 | -N | id-prefix | changed; default -1)",
1309
1334
  history: "usage: geml history <commit|verify|show|restore|log> <file.geml> [...]",
1310
- codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
1311
- geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
1312
- geml codemap verify [dir] geml check + profile reference checks
1313
- geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
1314
- geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
1315
- geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
1316
- geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
1335
+ codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
1336
+ geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
1337
+ geml codemap verify [dir] geml check + profile reference checks
1338
+ geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
1339
+ geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
1340
+ geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
1341
+ geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
1317
1342
  (<dir> for verify/render/serve/refresh/find defaults to ./.geml-code-graph; codegraph and code-graph are accepted as aliases of codemap)`,
1318
- mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
1319
-
1320
- Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
1321
- Every tool is geml_ + its CLI verb, so the terminal and the assistant share
1322
- one vocabulary.
1323
- Ten tools: geml_list · geml_get · geml_check · geml_history · geml_to
1324
- geml_set · geml_add · geml_delete · geml_rename · geml_revert
1325
- With a code graph under --root, four more (read-only), so one client entry
1326
- covers both: geml_codemap_search · geml_codemap_callchain
1327
- geml_codemap_list · geml_codemap_node
1328
-
1329
- --root <dir> REQUIRED. Root holding the .geml documents. Every path a
1330
- client names is confined here; a client cannot widen it.
1331
- --graph <dir> Code-graph directory, inside --root. Defaults to
1332
- <root>/.geml-code-graph when it holds an index.geml; with
1333
- no graph the four graph tools are not served at all.
1334
- --no-history Skip the .gemlhistory commit taken before each write
1335
- (default: commit, so geml_revert always has a revision to
1336
- undo to).
1337
-
1338
- Register with a client:
1343
+ mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
1344
+
1345
+ Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
1346
+ Every tool is geml_ + its CLI verb, so the terminal and the assistant share
1347
+ one vocabulary.
1348
+ Ten tools: geml_list · geml_get · geml_check · geml_history · geml_to
1349
+ geml_set · geml_add · geml_delete · geml_rename · geml_revert
1350
+ With a code graph under --root, four more (read-only), so one client entry
1351
+ covers both: geml_codemap_search · geml_codemap_callchain
1352
+ geml_codemap_list · geml_codemap_node
1353
+
1354
+ --root <dir> REQUIRED. Root holding the .geml documents. Every path a
1355
+ client names is confined here; a client cannot widen it.
1356
+ --graph <dir> Code-graph directory, inside --root. Defaults to
1357
+ <root>/.geml-code-graph when it holds an index.geml; with
1358
+ no graph the four graph tools are not served at all.
1359
+ --no-history Skip the .gemlhistory commit taken before each write
1360
+ (default: commit, so geml_revert always has a revision to
1361
+ undo to).
1362
+
1363
+ Register with a client:
1339
1364
  claude mcp add geml -- geml mcp --root /abs/path/to/repo`,
1340
1365
  };
1341
1366
  // Set from argv at dispatch time; when true, errors are emitted as a JSON
@@ -1794,7 +1819,8 @@ function resolveSelector(source, file, raw) {
1794
1819
  // a heading, its level and text); `--json` is a machine-readable array so an
1795
1820
  // agent can pick its next `get #id` target. Ids are listed in document order
1796
1821
  // (the registration order parse() records), covering the same set `get #id`
1797
- // resolves against: typed blocks, headings, and footnote definitions.
1822
+ // resolves against: typed blocks and headings. A `[^id]` reference names one
1823
+ // of those (§5.2); the `[^id]: text` definition line was withdrawn.
1798
1824
  function listIds(source, file, json) {
1799
1825
  const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
1800
1826
  const rows = doc.ids.map((id) => {
@@ -1804,8 +1830,10 @@ function listIds(source, file, json) {
1804
1830
  return { id, kind: "heading", level: b.level, text: b.text };
1805
1831
  if (b?.kind === "block") {
1806
1832
  const row = { id, kind: b.type };
1833
+ // `.footnote` is authored, not synthesized (the `[^id]: text` definition
1834
+ // line was withdrawn) — but it still marks a block meant as a footnote.
1807
1835
  if (b.classes.includes("footnote"))
1808
- row.footnote = true; // §5.2 footnote definition
1836
+ row.footnote = true;
1809
1837
  return row;
1810
1838
  }
1811
1839
  return { id, kind: b?.kind ?? "unknown" };
package/dist/inline.js CHANGED
@@ -315,8 +315,13 @@ function scanAtoms(s, line, sink, depth = 0) {
315
315
  // guesswork. Delimiters pair only *within* one text run: they never reach across
316
316
  // a code span, inline math, a link or image (atoms from phase A), or a block
317
317
  // boundary. Any delimiter left unpaired is literal text.
318
- const ASCII_PUNCT = /[!-\/:-@\[-`{-~]/;
319
- const isPunct = (c) => c !== undefined && ASCII_PUNCT.test(c);
318
+ // Unicode punctuation, not just ASCII (§5.3). With an ASCII-only test, `“` and
319
+ // `,` count as ordinary letters, and a run hugged by CJK punctuation on the
320
+ // outside and ASCII punctuation on the inside stops flanking: `“*(foo)*”` loses
321
+ // its emphasis. CommonMark's rule is Unicode-wide, and the algorithm here is
322
+ // meant to be that rule restricted to `*` and `~~` — not a narrower one.
323
+ const PUNCT = /[\p{P}\p{S}]/u;
324
+ const isPunct = (c) => c !== undefined && PUNCT.test(c);
320
325
  const isWS = (c) => c === undefined || /\s/.test(c);
321
326
  // Left/right-flanking for a delimiter run, given the chars on either side.
322
327
  function flank(before, after) {
package/dist/mcp.js CHANGED
@@ -619,25 +619,25 @@ export function handleLine(line, write = (s) => process.stdout.write(s)) {
619
619
  // ---------------------------------------------------------------------------
620
620
  // Entry
621
621
  // ---------------------------------------------------------------------------
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 auto-commit a .gemlhistory revision before each
637
- write. Default is to commit, so geml_revert always
638
- has a revision to undo to.
639
-
640
- Register with a client:
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 auto-commit a .gemlhistory revision before each
637
+ write. Default is to commit, so geml_revert always
638
+ has a revision to undo to.
639
+
640
+ Register with a client:
641
641
  claude mcp add geml -- geml mcp --root /abs/path/to/repo`;
642
642
  export function parseArgs(args) {
643
643
  let root;
@@ -36,43 +36,43 @@ function page(title, body, ctx, source) {
36
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`
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 = {}) {