@geml/geml 1.8.2 → 1.8.4

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
@@ -309,14 +309,32 @@ function matchMarker(line) {
309
309
  mk.start = parseInt(m[2], 10);
310
310
  return mk;
311
311
  }
312
+ // Leading indentation in COLUMNS (a tab counts as 4), the same measure the
313
+ // marker regex feeds into nesting decisions.
314
+ function indentColumns(line) {
315
+ let n = 0;
316
+ for (const ch of line) {
317
+ if (ch === " ")
318
+ n += 1;
319
+ else if (ch === "\t")
320
+ n += 4;
321
+ else
322
+ break;
323
+ }
324
+ return n;
325
+ }
312
326
  function makeListItem(mk, lineNo, ctx) {
313
327
  let text = interpolate(mk.rest, lineNo, ctx);
314
- // Task list item: a leading `[ ]` (open) or `[x]`/`[X]` (done) marker.
315
- const task = /^\[([ xX])\](?:[ \t]+(.*))?$/.exec(text);
328
+ // Task list item: a leading `[ ]` (open) or `[x]`/`[X]` (done) marker — on
329
+ // the item's FIRST line; continuation lines (§2.2) are already joined in,
330
+ // and a `[x]` there is content.
331
+ const nl = text.indexOf("\n");
332
+ const first = nl === -1 ? text : text.slice(0, nl);
333
+ const task = /^\[([ xX])\](?:[ \t]+(.*))?$/.exec(first);
316
334
  const item = { text, inlines: [] };
317
335
  if (task) {
318
336
  item.checked = task[1] !== " ";
319
- text = task[2] ?? "";
337
+ text = (task[2] ?? "") + (nl === -1 ? "" : text.slice(nl));
320
338
  item.text = text;
321
339
  }
322
340
  item.inlines = parseInline(text, lineNo, ctx);
@@ -380,9 +398,23 @@ function parseList(lines, i, base, ctx) {
380
398
  }
381
399
  if (prevBlank && cur.items.length > 0)
382
400
  cur.loose = true;
401
+ // §2.2 continuation lines: a non-blank line directly below the
402
+ // item, indented past its MARKER, that is neither an item line nor a `%%`
403
+ // comment, continues the item's inline content as a soft wrap — the same
404
+ // join a paragraph gives its lines. A blank line still ends the item, so
405
+ // multi-paragraph items remain outside the language.
406
+ let j = i + 1;
407
+ while (j < lines.length) {
408
+ const cand = lines[j];
409
+ const body = cand.trim();
410
+ if (body === "" || body.startsWith("%%") || matchMarker(cand) !== null || indentColumns(cand) <= mk.indent)
411
+ break;
412
+ mk.rest += "\n" + body;
413
+ j++;
414
+ }
383
415
  cur.items.push(makeListItem(mk, base + i + 1, ctx));
384
416
  prevBlank = false;
385
- i++;
417
+ i = j;
386
418
  }
387
419
  return { block: root, next: i };
388
420
  }
@@ -1135,33 +1167,49 @@ function gatherIds(source) {
1135
1167
  scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
1136
1168
  return new Set(ctx.ids.keys());
1137
1169
  }
1138
- // Pre-scan for `=== meta` blocks (at any fence depth) and merge their
1139
- // `key=val` lines, so `{{key}}` interpolation can resolve forward references.
1170
+ // Pre-scan for `=== meta` blocks and merge their `key=val` lines, so `{{key}}`
1171
+ // interpolation can resolve forward references.
1172
+ //
1173
+ // The walk descends exactly as scanBlocks/collectSpans do — into FLOW bodies
1174
+ // only, because a raw or data body is opaque (§3). This used to be a flat regex
1175
+ // sweep over every line, which read a `=== meta` shown as an EXAMPLE inside a
1176
+ // longer-fenced `==== code` as a real definition: its keys supplied live
1177
+ // `{{key}}` values (`geml check` clean, so nothing said so), and a key the
1178
+ // document also defined at top level warned `duplicate-meta-key` against a
1179
+ // definition that does not exist. Closing is fenceClose's job too now, so a
1180
+ // `=== meta {#m}` may close on its labeled fence `=== #m` like any other block.
1140
1181
  function collectMeta(lines, diags) {
1141
1182
  const meta = new Map();
1142
1183
  const firstLine = new Map(); // key → 1-based line of the defining fence
1143
- for (let i = 0; i < lines.length; i++) {
1144
- const open = FENCE_OPEN.exec(lines[i]);
1145
- if (!open || open[2] !== "meta")
1146
- continue;
1147
- const len = open[1].length;
1148
- const body = [];
1149
- let j = i + 1;
1150
- for (; j < lines.length && !isCloseFence(lines[j], len); j++)
1151
- body.push(lines[j]);
1152
- for (const [k, v] of Object.entries(parseData(body))) {
1153
- if (meta.has(k)) {
1154
- diags?.push({ severity: "warning", code: "duplicate-meta-key",
1155
- message: `meta key \`${k}\` already defined at line ${firstLine.get(k)}; later definition at line ${i + 1} is ignored`,
1156
- line: i + 1 });
1184
+ const walk = (ls, base, depth) => {
1185
+ for (let i = 0; i < ls.length; i++) {
1186
+ const open = FENCE_OPEN.exec(ls[i]);
1187
+ if (!open)
1188
+ continue;
1189
+ const type = open[2];
1190
+ const { end, closed } = fenceClose(ls, i, open);
1191
+ const body = ls.slice(i + 1, closed ? end - 1 : end);
1192
+ if (type === "meta") {
1193
+ const line = base + i + 1; // 1-based, in the original stream
1194
+ for (const [k, v] of Object.entries(parseData(body))) {
1195
+ if (meta.has(k)) {
1196
+ diags?.push({ severity: "warning", code: "duplicate-meta-key",
1197
+ message: `meta key \`${k}\` already defined at line ${firstLine.get(k)}; later definition at line ${line} is ignored`,
1198
+ line });
1199
+ }
1200
+ else {
1201
+ meta.set(k, String(v));
1202
+ firstLine.set(k, line);
1203
+ }
1204
+ }
1157
1205
  }
1158
- else {
1159
- meta.set(k, String(v));
1160
- firstLine.set(k, i + 1);
1206
+ else if ((REGISTRY.get(type) ?? "raw") === "flow" && depth < MAX_NESTING) {
1207
+ walk(body, base + i + 1, depth + 1);
1161
1208
  }
1209
+ i = end - 1; // the loop's ++ lands on `end`
1162
1210
  }
1163
- i = j;
1164
- }
1211
+ };
1212
+ walk(lines, 0, 0);
1165
1213
  return meta;
1166
1214
  }
1167
1215
  // §8: resolve every discovered reference. Internal/autoref/footnote anchors
package/dist/mcp.js CHANGED
@@ -769,25 +769,25 @@ export function handleLine(line, write = (s) => process.stdout.write(s)) {
769
769
  // ---------------------------------------------------------------------------
770
770
  // Entry
771
771
  // ---------------------------------------------------------------------------
772
- export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
773
-
774
- Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
775
- read-only code-graph tools when the root holds a code graph.
776
-
777
- --root <dir> REQUIRED. Root directory holding the .geml documents.
778
- Relative paths resolve against the server process's CWD,
779
- which the CLIENT chooses — pass an absolute path.
780
- Every path a client names is confined to this directory;
781
- a client cannot widen or override it.
782
- --graph <dir> Code-graph directory, inside --root. Defaults to
783
- <root>/.geml-code-graph when that holds an index.geml.
784
- With no graph, the code-graph tools are not served
785
- at all (a client sees only the document tools).
786
- --no-history Do not save a .gemlhistory revision before each
787
- write. Default is to save one, so geml_revert always
788
- has a revision to undo to.
789
-
790
- Register with a client:
772
+ export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
773
+
774
+ Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
775
+ read-only code-graph tools when the root holds a code graph.
776
+
777
+ --root <dir> REQUIRED. Root directory holding the .geml documents.
778
+ Relative paths resolve against the server process's CWD,
779
+ which the CLIENT chooses — pass an absolute path.
780
+ Every path a client names is confined to this directory;
781
+ a client cannot widen or override it.
782
+ --graph <dir> Code-graph directory, inside --root. Defaults to
783
+ <root>/.geml-code-graph when that holds an index.geml.
784
+ With no graph, the code-graph tools are not served
785
+ at all (a client sees only the document tools).
786
+ --no-history Do not save a .gemlhistory revision before each
787
+ write. Default is to save one, so geml_revert always
788
+ has a revision to undo to.
789
+
790
+ Register with a client:
791
791
  claude mcp add geml -- geml mcp --root /abs/path/to/repo`;
792
792
  export function parseArgs(args) {
793
793
  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","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 = {}) {
package/dist/render.js CHANGED
@@ -1273,164 +1273,164 @@ function trunc(s, n) {
1273
1273
  // ---------------------------------------------------------------------------
1274
1274
  // Page shell, inline CSS, inline interactivity JS
1275
1275
  // ---------------------------------------------------------------------------
1276
- export const CSS = `
1277
- :root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
1278
- * { box-sizing: border-box; }
1279
- body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
1280
- main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
1281
- h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
1282
- h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1283
- h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1284
- h3 { font-size:1.25em; } h4 { font-size:1em; }
1285
- p { margin:.7em 0; }
1286
- a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
1287
- code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
1288
- pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
1289
- pre code { background:none; padding:0; font-size:.85em; }
1290
- pre.output { background:#0d1117; color:#e6edf3; }
1291
- pre.output code { color:inherit; }
1292
- ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
1293
- ul.task-list { list-style:none; padding-left:.2em; }
1294
- li.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; }
1295
- li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
1296
- li.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; }
1297
- aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
1298
- aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
1299
- aside.warning { border-left-color:#d97706; background:#fff8f0; }
1300
- aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
1301
- figure { margin:1.2em 0; }
1302
- figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
1303
- table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
1304
- table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
1305
- table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
1306
- table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
1307
- table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
1308
- table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
1309
- table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
1310
- table.geml-table td.computed { color:#0a7c52; }
1311
- table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
1312
- .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; }
1313
- /* A table wider than the column scrolls INSIDE its own box — the page itself
1314
- must never scroll sideways. Only the table is in here: the filter box above
1315
- and the caption below stay put instead of sliding out of view with it. */
1316
- .table-scroll { overflow-x:auto; max-width:100%; }
1317
- .table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
1318
- .table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
1319
- .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
1320
- .c-title { font-size:15px; font-weight:600; fill:var(--fg); }
1321
- .c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
1322
- .media { max-width:100%; border-radius:8px; }
1323
- .diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
1324
- .math-block { overflow-x:auto; padding:.4em 0; }
1325
- sup.fn a { font-size:.75em; }
1326
- .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
1327
- .geml-footer code { font-size:.95em; }
1328
- .code-graph { margin:1.4em 0; }
1329
- /* The graph is the widest artifact on the page: let it break out of main's
1330
- 860px reading column and take the viewport, centred, leaving the prose
1331
- around it untouched. Negative inline margins, NOT transform — a transform
1332
- would become the containing block for the fullscreen overlay below. */
1333
- @media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }
1334
- .cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
1335
- .cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }
1336
- .cg-svg { display:block; }
1337
- /* Fullscreen: one class drives the layout, whether the native Fullscreen API
1338
- took (mount is in the top layer) or we fell back to a fixed overlay. The
1339
- :fullscreen pseudo-class is deliberately NOT in these selectors — a browser
1340
- that doesn't know it would drop the whole rule; the runtime keeps the class
1341
- in sync with fullscreenchange instead. Toolbar/legend keep their height,
1342
- the stage eats the rest. */
1343
- .cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }
1344
- .cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }
1345
- .cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }
1346
- .cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }
1347
- .cg-mount.cg-full .cg-src-body { max-height:none; }
1348
- .cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }
1349
- .cg-search-wrap { position:relative; display:inline-block; }
1350
- .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; }
1351
- .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); }
1352
- .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; }
1353
- .cg-search-row:hover { background:var(--bd); }
1354
- .cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
1355
- .cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
1356
- .cg-search-grp:first-of-type { border-top:0; }
1357
- .cg-stage { display:flex; gap:10px; align-items:flex-start; }
1358
- .cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
1359
- .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); }
1360
- .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; }
1361
- .cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
1362
- .cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:84vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
1363
- .cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
1364
- .cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
1365
- .cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
1366
- .cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
1367
- .cg-crumb .cg-seg:hover { text-decoration:underline; }
1368
- .cg-frame { display:block; width:100%; height:84vh; border:0; background:var(--bg); }
1369
- .cg-flash { color:#b42318; }
1370
- .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; }
1371
- .cg-upbtn { cursor:pointer; }
1372
- .cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
1373
- .cg-upbtn text { font-size:11px; fill:#57606a; }
1374
- .cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
1375
- .cg-upbtn:hover text { fill:var(--accent); }
1376
- .cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
1377
- .cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
1378
- .cg-chip { display:inline-flex; align-items:center; gap:4px; }
1379
- .cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
1380
- .cg-note { font-size:.8em; color:#9a6700; }
1381
- .cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
1382
- .cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
1383
- .cg-n { cursor:pointer; }
1384
- .cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
1385
- .cg-n.leaf { opacity:.45; }
1386
- .cg-n.test rect { stroke-dasharray:3 2; }
1387
- .cg-n.grp rect { stroke-width:1.8; }
1388
- .cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
1389
- .cg-e.cand { stroke-dasharray:2 3; }
1390
- .cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
1391
- .cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
1392
- .cg-e.soft { opacity:.55; }
1393
- .cg-svg.hl .cg-n { opacity:.22; }
1394
- .cg-svg.hl .cg-e { opacity:.1; }
1395
- .cg-svg.hl .cg-n.hl { opacity:1; }
1396
- .cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
1276
+ export const CSS = `
1277
+ :root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
1278
+ * { box-sizing: border-box; }
1279
+ body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
1280
+ main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
1281
+ h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
1282
+ h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1283
+ h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
1284
+ h3 { font-size:1.25em; } h4 { font-size:1em; }
1285
+ p { margin:.7em 0; }
1286
+ a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
1287
+ code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
1288
+ pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
1289
+ pre code { background:none; padding:0; font-size:.85em; }
1290
+ pre.output { background:#0d1117; color:#e6edf3; }
1291
+ pre.output code { color:inherit; }
1292
+ ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
1293
+ ul.task-list { list-style:none; padding-left:.2em; }
1294
+ li.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; }
1295
+ li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
1296
+ li.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; }
1297
+ aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
1298
+ aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
1299
+ aside.warning { border-left-color:#d97706; background:#fff8f0; }
1300
+ aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
1301
+ figure { margin:1.2em 0; }
1302
+ figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
1303
+ table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
1304
+ table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
1305
+ table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
1306
+ table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
1307
+ table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
1308
+ table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
1309
+ table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
1310
+ table.geml-table td.computed { color:#0a7c52; }
1311
+ table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
1312
+ .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; }
1313
+ /* A table wider than the column scrolls INSIDE its own box — the page itself
1314
+ must never scroll sideways. Only the table is in here: the filter box above
1315
+ and the caption below stay put instead of sliding out of view with it. */
1316
+ .table-scroll { overflow-x:auto; max-width:100%; }
1317
+ .table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
1318
+ .table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
1319
+ .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
1320
+ .c-title { font-size:15px; font-weight:600; fill:var(--fg); }
1321
+ .c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
1322
+ .media { max-width:100%; border-radius:8px; }
1323
+ .diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
1324
+ .math-block { overflow-x:auto; padding:.4em 0; }
1325
+ sup.fn a { font-size:.75em; }
1326
+ .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
1327
+ .geml-footer code { font-size:.95em; }
1328
+ .code-graph { margin:1.4em 0; }
1329
+ /* The graph is the widest artifact on the page: let it break out of main's
1330
+ 860px reading column and take the viewport, centred, leaving the prose
1331
+ around it untouched. Negative inline margins, NOT transform — a transform
1332
+ would become the containing block for the fullscreen overlay below. */
1333
+ @media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }
1334
+ .cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
1335
+ .cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }
1336
+ .cg-svg { display:block; }
1337
+ /* Fullscreen: one class drives the layout, whether the native Fullscreen API
1338
+ took (mount is in the top layer) or we fell back to a fixed overlay. The
1339
+ :fullscreen pseudo-class is deliberately NOT in these selectors — a browser
1340
+ that doesn't know it would drop the whole rule; the runtime keeps the class
1341
+ in sync with fullscreenchange instead. Toolbar/legend keep their height,
1342
+ the stage eats the rest. */
1343
+ .cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }
1344
+ .cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }
1345
+ .cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }
1346
+ .cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }
1347
+ .cg-mount.cg-full .cg-src-body { max-height:none; }
1348
+ .cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }
1349
+ .cg-search-wrap { position:relative; display:inline-block; }
1350
+ .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; }
1351
+ .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); }
1352
+ .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; }
1353
+ .cg-search-row:hover { background:var(--bd); }
1354
+ .cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
1355
+ .cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
1356
+ .cg-search-grp:first-of-type { border-top:0; }
1357
+ .cg-stage { display:flex; gap:10px; align-items:flex-start; }
1358
+ .cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
1359
+ .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); }
1360
+ .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; }
1361
+ .cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
1362
+ .cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:84vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
1363
+ .cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
1364
+ .cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
1365
+ .cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
1366
+ .cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
1367
+ .cg-crumb .cg-seg:hover { text-decoration:underline; }
1368
+ .cg-frame { display:block; width:100%; height:84vh; border:0; background:var(--bg); }
1369
+ .cg-flash { color:#b42318; }
1370
+ .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; }
1371
+ .cg-upbtn { cursor:pointer; }
1372
+ .cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
1373
+ .cg-upbtn text { font-size:11px; fill:#57606a; }
1374
+ .cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
1375
+ .cg-upbtn:hover text { fill:var(--accent); }
1376
+ .cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
1377
+ .cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
1378
+ .cg-chip { display:inline-flex; align-items:center; gap:4px; }
1379
+ .cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
1380
+ .cg-note { font-size:.8em; color:#9a6700; }
1381
+ .cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
1382
+ .cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
1383
+ .cg-n { cursor:pointer; }
1384
+ .cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
1385
+ .cg-n.leaf { opacity:.45; }
1386
+ .cg-n.test rect { stroke-dasharray:3 2; }
1387
+ .cg-n.grp rect { stroke-width:1.8; }
1388
+ .cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
1389
+ .cg-e.cand { stroke-dasharray:2 3; }
1390
+ .cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
1391
+ .cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
1392
+ .cg-e.soft { opacity:.55; }
1393
+ .cg-svg.hl .cg-n { opacity:.22; }
1394
+ .cg-svg.hl .cg-e { opacity:.1; }
1395
+ .cg-svg.hl .cg-n.hl { opacity:1; }
1396
+ .cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
1397
1397
  `;
1398
- export const JS = `
1399
- (function () {
1400
- function cmp(a, b) {
1401
- var na = a.dataset.sort, nb = b.dataset.sort;
1402
- if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
1403
- return (a.textContent || "").localeCompare(b.textContent || "");
1404
- }
1405
- document.querySelectorAll("table.geml-table").forEach(function (table) {
1406
- var tbody = table.tBodies[0];
1407
- if (!tbody) return;
1408
- // Sort on header click.
1409
- var ths = table.tHead ? table.tHead.rows[0].cells : [];
1410
- Array.prototype.forEach.call(ths, function (th, col) {
1411
- th.addEventListener("click", function () {
1412
- var dir = th.classList.contains("asc") ? "desc" : "asc";
1413
- Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
1414
- th.classList.add(dir);
1415
- var rows = Array.prototype.slice.call(tbody.rows);
1416
- rows.sort(function (r1, r2) {
1417
- var c = cmp(r1.cells[col], r2.cells[col]);
1418
- return dir === "asc" ? c : -c;
1419
- });
1420
- rows.forEach(function (r) { tbody.appendChild(r); });
1421
- });
1422
- });
1423
- // Filter rows.
1424
- var fig = table.closest(".table-figure");
1425
- var input = fig ? fig.querySelector(".table-filter") : null;
1426
- if (input) input.addEventListener("input", function () {
1427
- var q = input.value.toLowerCase();
1428
- Array.prototype.forEach.call(tbody.rows, function (r) {
1429
- r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
1430
- });
1431
- });
1432
- });
1433
- })();
1398
+ export const JS = `
1399
+ (function () {
1400
+ function cmp(a, b) {
1401
+ var na = a.dataset.sort, nb = b.dataset.sort;
1402
+ if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
1403
+ return (a.textContent || "").localeCompare(b.textContent || "");
1404
+ }
1405
+ document.querySelectorAll("table.geml-table").forEach(function (table) {
1406
+ var tbody = table.tBodies[0];
1407
+ if (!tbody) return;
1408
+ // Sort on header click.
1409
+ var ths = table.tHead ? table.tHead.rows[0].cells : [];
1410
+ Array.prototype.forEach.call(ths, function (th, col) {
1411
+ th.addEventListener("click", function () {
1412
+ var dir = th.classList.contains("asc") ? "desc" : "asc";
1413
+ Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
1414
+ th.classList.add(dir);
1415
+ var rows = Array.prototype.slice.call(tbody.rows);
1416
+ rows.sort(function (r1, r2) {
1417
+ var c = cmp(r1.cells[col], r2.cells[col]);
1418
+ return dir === "asc" ? c : -c;
1419
+ });
1420
+ rows.forEach(function (r) { tbody.appendChild(r); });
1421
+ });
1422
+ });
1423
+ // Filter rows.
1424
+ var fig = table.closest(".table-figure");
1425
+ var input = fig ? fig.querySelector(".table-filter") : null;
1426
+ if (input) input.addEventListener("input", function () {
1427
+ var q = input.value.toLowerCase();
1428
+ Array.prototype.forEach.call(tbody.rows, function (r) {
1429
+ r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
1430
+ });
1431
+ });
1432
+ });
1433
+ })();
1434
1434
  `;
1435
1435
  // geml-code-graph runtime: layered layout AT DRAW TIME (GEP-0003 / v2-D8) so
1436
1436
  // clicking a node re-roots the view inside the embedded slice. Algorithm as
package/dist/serialize.js CHANGED
@@ -157,7 +157,13 @@ function serList(list, indent) {
157
157
  list.items.forEach((item, k) => {
158
158
  const marker = list.ordered ? `${start + k}. ` : "- ";
159
159
  const task = item.checked === undefined ? "" : item.checked ? "[x] " : "[ ] ";
160
- out.push(indent + marker + task + serInlines(item.inlines));
160
+ // A soft-wrapped item (§2.2) serializes as continuation lines aligned under
161
+ // its content, so the emission re-parses to the same item.
162
+ const [head, ...cont] = serInlines(item.inlines).split("\n");
163
+ out.push(indent + marker + task + head);
164
+ const contIndent = indent + " ".repeat(marker.length + task.length);
165
+ for (const l of cont)
166
+ out.push(contIndent + l);
161
167
  for (const child of item.children ?? []) {
162
168
  out.push(child.kind === "list" ? serList(child, indent + " ") : serBlock(child));
163
169
  }
package/dist/to-md.js CHANGED
@@ -116,7 +116,14 @@ function listToMd(b, indent, notes) {
116
116
  b.items.forEach((item, k) => {
117
117
  const marker = b.ordered ? `${start + k}. ` : "- ";
118
118
  const task = item.checked === undefined ? "" : item.checked ? "[x] " : "[ ] ";
119
- out.push(indent + marker + task + seq(item.inlines));
119
+ // A soft-wrapped item (§2.2) keeps its wrap: continuation lines indented to
120
+ // the content column, which both GFM and a GEML re-parse read as the same
121
+ // single item.
122
+ const [head, ...cont] = seq(item.inlines).split("\n");
123
+ out.push(indent + marker + task + head);
124
+ const contIndent = indent + " ".repeat(marker.length + task.length);
125
+ for (const l of cont)
126
+ out.push(contIndent + l);
120
127
  for (const child of item.children ?? []) {
121
128
  out.push(child.kind === "list" ? listToMd(child, indent + " ", notes) : block(child, notes));
122
129
  }