@geml/geml 1.6.0 → 1.7.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/codemap/browser-stub.mjs +5 -0
- package/codemap/build.mjs +20 -6
- package/codemap/exclude.mjs +18 -14
- package/codemap/refresh.mjs +4 -1
- package/codemap/verify.mjs +14 -4
- 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/from-md.js +6 -1
- package/dist/geml.d.ts +5 -1
- package/dist/geml.js +797 -36
- package/dist/mcp.js +23 -2
- package/dist/render-html.d.ts +5 -0
- package/dist/render-html.js +10 -1
- package/dist/render.d.ts +1 -0
- package/dist/render.js +72 -3
- package/dist/serialize.js +12 -0
- package/dist/table.js +27 -1
- package/dist/to-md.js +29 -2
- package/package.json +2 -1
- package/skill/SKILL.md +82 -0
- package/skill/references/authoring.geml +333 -0
package/dist/mcp.js
CHANGED
|
@@ -284,16 +284,37 @@ export const TOOLS = [
|
|
|
284
284
|
type: "string",
|
|
285
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
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
|
+
},
|
|
287
296
|
},
|
|
288
297
|
required: ["file", "id"],
|
|
289
298
|
},
|
|
290
299
|
run: (args) => {
|
|
291
300
|
const real = resolveInRoot(args.file);
|
|
292
301
|
const sel = selectorArg(args.id);
|
|
293
|
-
|
|
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"] : [])]);
|
|
294
309
|
if (!run.ok)
|
|
295
310
|
throw new Error(run.stderr || `nothing matches ${sel}`);
|
|
296
|
-
|
|
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 };
|
|
297
318
|
},
|
|
298
319
|
},
|
|
299
320
|
{
|
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,7 +33,7 @@ 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
39
|
? `<script type="module">
|
|
@@ -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
package/dist/render.js
CHANGED
|
@@ -538,6 +538,42 @@ export class RenderCtx {
|
|
|
538
538
|
const classes = classAttr(["text", ...b.classes]);
|
|
539
539
|
return `<div class="${classes}"${idAttr}>\n${inner}\n</div>`;
|
|
540
540
|
}
|
|
541
|
+
case "data": {
|
|
542
|
+
// GEP-0005: the page shows a PREVIEW under the same row discipline
|
|
543
|
+
// tables use (opts.tableRows, default 500) — the MODEL always keeps
|
|
544
|
+
// everything. Direction follows the format's reading order: jsonl is
|
|
545
|
+
// an append-log, so the newest (last) lines are the preview; json is
|
|
546
|
+
// one value and reads from the top.
|
|
547
|
+
const limit = this.opts.tableRows ?? 500;
|
|
548
|
+
const src = typeof b.attrs["src"] === "string" ? b.attrs["src"] : undefined;
|
|
549
|
+
const fmt = typeof b.attrs["format"] === "string" ? b.attrs["format"]
|
|
550
|
+
: src !== undefined && /\.jsonl$/i.test(src) ? "jsonl" : "json";
|
|
551
|
+
let lines = b.raw ?? [];
|
|
552
|
+
if (lines.every((l) => l.trim() === "")) {
|
|
553
|
+
if (b.value !== undefined) {
|
|
554
|
+
// src= content loaded at build time: preview the canonical form.
|
|
555
|
+
lines = fmt === "jsonl" && Array.isArray(b.value)
|
|
556
|
+
? b.value.map((v) => JSON.stringify(v))
|
|
557
|
+
: JSON.stringify(b.value, null, 2).split("\n");
|
|
558
|
+
}
|
|
559
|
+
else if (src !== undefined) {
|
|
560
|
+
// §9.4: a render-time source (http, or no resolver at build).
|
|
561
|
+
const cap0 = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
|
|
562
|
+
return `<figure${idAttr}><p class="table-note">external data <code>${esc(src)}</code> — loaded at render time</p>${cap0}</figure>`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const tail = fmt === "jsonl" && lines.length > limit;
|
|
566
|
+
const shown = tail ? lines.slice(-limit) : lines.slice(0, limit);
|
|
567
|
+
const omitted = lines.length - shown.length;
|
|
568
|
+
const note = omitted > 0
|
|
569
|
+
? `<p class="table-note">${tail
|
|
570
|
+
? `showing the last ${shown.length} of ${lines.length} lines — earlier lines are in the document source`
|
|
571
|
+
: `showing the first ${shown.length} of ${lines.length} lines — the complete data is in the document source`}</p>`
|
|
572
|
+
: "";
|
|
573
|
+
const cap = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
|
|
574
|
+
const pre = `<pre class="data-src" data-format="${escAttr(fmt)}">${esc(shown.join("\n"))}</pre>`;
|
|
575
|
+
return `<figure${idAttr}>${tail ? note + pre : pre + note}${cap}</figure>`;
|
|
576
|
+
}
|
|
541
577
|
case "table":
|
|
542
578
|
return b.table ? this.table(b.table, b.id, caption) : `<p class="render-error">table failed to parse</p>`;
|
|
543
579
|
case "diagram":
|
|
@@ -1954,7 +1990,24 @@ export function codeGraphRuntime(root) {
|
|
|
1954
1990
|
// Rendered pages sit next to their codemap documents: a live mount
|
|
1955
1991
|
// (viewer/playground) carries data-src, a CLI embed carries the src
|
|
1956
1992
|
// path in data.start — either directory anchors doc-relative links.
|
|
1957
|
-
|
|
1993
|
+
// navBase prefixes EVERY url this view reaches for — the breadcrumb's
|
|
1994
|
+
// `location.href`, the search-index `<script src>`, a hit's jump target.
|
|
1995
|
+
// Both of its inputs are page data (a DOM attribute, the embedded graph
|
|
1996
|
+
// JSON), so a document that carried `data-src="javascript:…"` would turn
|
|
1997
|
+
// a breadcrumb click into script execution, and a `//host/` or
|
|
1998
|
+
// `https://host/` value would pull the search index off another origin.
|
|
1999
|
+
// A base is a doc-relative DIRECTORY and nothing else: anything bearing a
|
|
2000
|
+
// scheme or a network-path prefix is refused outright and links resolve
|
|
2001
|
+
// against the current page instead.
|
|
2002
|
+
// Everything this view navigates to or loads is built from page data, so
|
|
2003
|
+
// every such string is filtered here first: keep it if it is a
|
|
2004
|
+
// document-RELATIVE path, drop it to "" if it carries a scheme
|
|
2005
|
+
// (`javascript:`, `data:`) or a `//host` network-path prefix.
|
|
2006
|
+
function relOnly(u) {
|
|
2007
|
+
var s = String(u == null ? "" : u);
|
|
2008
|
+
return /^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(s) || s.slice(0, 2) === "//" ? "" : s;
|
|
2009
|
+
}
|
|
2010
|
+
var navBase = relOnly(String(mount.getAttribute("data-src") || data.start || "").replace(/[^\/]*$/, ""));
|
|
1958
2011
|
// A live mount (viewer/playground/served page) navigates IN PLACE over
|
|
1959
2012
|
// the geml documents through this loader; only truly static pages fall
|
|
1960
2013
|
// back to their pre-rendered sibling .html pages. Read LAZILY on every
|
|
@@ -1992,7 +2045,16 @@ export function codeGraphRuntime(root) {
|
|
|
1992
2045
|
}
|
|
1993
2046
|
catch (e) { /* stub */ }
|
|
1994
2047
|
}
|
|
1995
|
-
function openDoc(
|
|
2048
|
+
function openDoc(rel0, gpath) {
|
|
2049
|
+
// The target can come from a node's own data (a `data-k` key), not just
|
|
2050
|
+
// from a breadcrumb we built — so it goes through the same relative-path
|
|
2051
|
+
// filter as navBase. An absolute or scheme-bearing target says why it
|
|
2052
|
+
// was refused rather than navigating.
|
|
2053
|
+
var rel = relOnly(rel0);
|
|
2054
|
+
if (!rel) {
|
|
2055
|
+
flash("refusing to open " + String(rel0) + " — not a document-relative path");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
1996
2058
|
var lv = live();
|
|
1997
2059
|
if (lv) {
|
|
1998
2060
|
Promise.resolve(lv({ doc: rel })).then(function (nd) {
|
|
@@ -2304,8 +2366,15 @@ export function codeGraphRuntime(root) {
|
|
|
2304
2366
|
});
|
|
2305
2367
|
}
|
|
2306
2368
|
}
|
|
2307
|
-
function gotoHit(
|
|
2369
|
+
function gotoHit(doc0, id, locate) {
|
|
2308
2370
|
searchMenu.hidden = true;
|
|
2371
|
+
// `doc` is a row from the loaded search index — page data, same as any
|
|
2372
|
+
// other target, so it gets the same relative-path filter.
|
|
2373
|
+
var doc = relOnly(doc0);
|
|
2374
|
+
if (!doc) {
|
|
2375
|
+
flash("refusing to open " + String(doc0) + " — not a document-relative path");
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2309
2378
|
if (live() && !locate) {
|
|
2310
2379
|
showCallees(doc + "#" + id);
|
|
2311
2380
|
return;
|
package/dist/serialize.js
CHANGED
|
@@ -174,6 +174,18 @@ function serTypedBlock(b) {
|
|
|
174
174
|
else if (b.mode === "data") {
|
|
175
175
|
body = Object.entries(b.data ?? {}).map(([k, v]) => `${k} = ${serDataValue(v)}`);
|
|
176
176
|
}
|
|
177
|
+
else if (b.type === "data" && b.value !== undefined && b.attrs["src"] === undefined) {
|
|
178
|
+
// src= content lives in the FILE: a value loaded from it must never be
|
|
179
|
+
// inlined into the document by a re-format — the body stays as authored
|
|
180
|
+
// (normally empty), or the source of truth would silently fork.
|
|
181
|
+
// GEP-0005 canonical form — legal because this is DATA, not verbatim text
|
|
182
|
+
// (code stays byte-preserved): json pretty-prints at two spaces, jsonl is
|
|
183
|
+
// one compact value per line. Engine-less bodies (yaml/toml/unknown)
|
|
184
|
+
// never reach here (no `value`) and fall through byte-preserved.
|
|
185
|
+
body = String(b.attrs["format"] ?? "json") === "jsonl" && Array.isArray(b.value)
|
|
186
|
+
? b.value.map((v) => JSON.stringify(v))
|
|
187
|
+
: JSON.stringify(b.value, null, 2).split("\n");
|
|
188
|
+
}
|
|
177
189
|
else {
|
|
178
190
|
body = b.raw ?? [];
|
|
179
191
|
}
|
package/dist/table.js
CHANGED
|
@@ -60,6 +60,27 @@ function parseDelimited(body, sep, header) {
|
|
|
60
60
|
const width = rows.reduce((m, r) => Math.max(m, r.length), 0);
|
|
61
61
|
return { columns: letters(width), align: [], header: false, cells: rows };
|
|
62
62
|
}
|
|
63
|
+
// The character a data body splits on: the format's natural delimiter unless
|
|
64
|
+
// `delim=` names another single one — `;` for a European CSV, `|` for a
|
|
65
|
+
// pipe-delimited export (§6). Measured in code points, so an astral character
|
|
66
|
+
// counts as one. §4 attribute values carry no escape syntax, so a tab
|
|
67
|
+
// delimiter is spelled `format=tsv`, never `delim="\t"`; a value that is not
|
|
68
|
+
// exactly one character is an error and the natural delimiter is used, which
|
|
69
|
+
// keeps the rest of the table readable.
|
|
70
|
+
function resolveDelim(fmt, raw, diagnostics) {
|
|
71
|
+
const natural = fmt === "tsv" ? "\t" : ",";
|
|
72
|
+
if (raw === undefined)
|
|
73
|
+
return natural;
|
|
74
|
+
const d = String(raw);
|
|
75
|
+
if ([...d].length === 1)
|
|
76
|
+
return d;
|
|
77
|
+
diagnostics.push({
|
|
78
|
+
severity: "error",
|
|
79
|
+
code: "bad-table-delimiter",
|
|
80
|
+
message: `\`delim="${d}"\` must be exactly one character (for a tab use \`format=tsv\`); split on ${fmt === "tsv" ? "a tab" : "`,`"} instead`,
|
|
81
|
+
});
|
|
82
|
+
return natural;
|
|
83
|
+
}
|
|
63
84
|
function letters(n) {
|
|
64
85
|
const out = [];
|
|
65
86
|
for (let i = 0; i < n; i++)
|
|
@@ -271,11 +292,16 @@ export function parseTable(body, attrs, line, sink) {
|
|
|
271
292
|
if (fmt === "csv" || fmt === "tsv") {
|
|
272
293
|
const headerAttr = attrs["header"];
|
|
273
294
|
const header = headerAttr === undefined ? true : headerAttr === true || headerAttr === 1 || headerAttr === "1";
|
|
274
|
-
raw = parseDelimited(body, fmt
|
|
295
|
+
raw = parseDelimited(body, resolveDelim(fmt, attrs["delim"], diagnostics), header);
|
|
275
296
|
}
|
|
276
297
|
else {
|
|
277
298
|
if (fmt !== undefined)
|
|
278
299
|
diagnostics.push({ severity: "warning", code: "unknown-table-format", message: `unknown table format \`${fmt}\`; parsed as visual grid` });
|
|
300
|
+
// `delim` refines the data form; it does not select it. Silently dropping it
|
|
301
|
+
// would leave a `=== table {delim=";"}` parsed as a one-column visual grid
|
|
302
|
+
// with nothing to say why.
|
|
303
|
+
if (attrs["delim"] !== undefined)
|
|
304
|
+
diagnostics.push({ severity: "warning", code: "ignored-table-delimiter", message: "`delim` applies to a data body (`format=csv`/`tsv`); this body was parsed as a visual grid, so it is ignored" });
|
|
279
305
|
raw = parseVisual(body);
|
|
280
306
|
}
|
|
281
307
|
const columns = [...raw.columns];
|
package/dist/to-md.js
CHANGED
|
@@ -48,10 +48,32 @@ function inline(n) {
|
|
|
48
48
|
function seq(ns) {
|
|
49
49
|
return ns.map(inline).join("");
|
|
50
50
|
}
|
|
51
|
+
// Escape a `|` so GFM keeps it inside the cell instead of splitting the row.
|
|
52
|
+
// GFM resolves backslash escapes in a row BEFORE it splits on `|`, so a
|
|
53
|
+
// backslash run sitting right in front of our escape would eat it: a code span
|
|
54
|
+
// holding `a\|b` became `a\\|b`, which reads as a literal backslash followed by
|
|
55
|
+
// an UNescaped pipe — a spurious cell break. Double any such run first, then
|
|
56
|
+
// escape the pipe. Runs already produced by escText (`\\` for a literal
|
|
57
|
+
// backslash) survive this unchanged, so pre-rendered Markdown stays intact.
|
|
58
|
+
//
|
|
59
|
+
// The backslash run is matched as `\\+\|?` — one ATOMIC token, run and pipe
|
|
60
|
+
// together — not as `(\\*)\|`. The latter is quadratic: on a cell holding a
|
|
61
|
+
// long run of backslashes and no pipe, the engine matches the run from every
|
|
62
|
+
// index in it and fails at the required `|` each time. Here the greedy `\\+`
|
|
63
|
+
// takes the whole run in one match and the trailing `\|?` is optional, so
|
|
64
|
+
// nothing backtracks and each character is visited once.
|
|
65
|
+
function escPipe(s) {
|
|
66
|
+
return s.replace(/\\+\|?|\|/g, (m) => {
|
|
67
|
+
if (m.charAt(m.length - 1) !== "|")
|
|
68
|
+
return m; // a run with no pipe after it
|
|
69
|
+
const bs = m.slice(0, -1); // the run that would otherwise eat our escape
|
|
70
|
+
return bs + bs + "\\|";
|
|
71
|
+
});
|
|
72
|
+
}
|
|
51
73
|
// Inline text for a table cell: render inlines, then neutralise the two bytes
|
|
52
74
|
// that would break a GFM cell.
|
|
53
75
|
function cellText(c) {
|
|
54
|
-
return seq(c.inlines)
|
|
76
|
+
return escPipe(seq(c.inlines)).replace(/\n/g, " ");
|
|
55
77
|
}
|
|
56
78
|
// ---------------------------------------------------------------------------
|
|
57
79
|
// Tables
|
|
@@ -72,7 +94,7 @@ function tableToMd(t, notes) {
|
|
|
72
94
|
const lines = [];
|
|
73
95
|
if (t.caption)
|
|
74
96
|
lines.push(`*${t.caption}*`, "");
|
|
75
|
-
lines.push(`| ${cols.map(
|
|
97
|
+
lines.push(`| ${cols.map(escPipe).join(" | ")} |`);
|
|
76
98
|
lines.push(`| ${cols.map((_, i) => sep(t.align[i])).join(" | ")} |`);
|
|
77
99
|
const pad = (cells) => {
|
|
78
100
|
while (cells.length < cols.length)
|
|
@@ -144,6 +166,11 @@ function typedToMd(b, notes) {
|
|
|
144
166
|
const raw = b.raw ?? [];
|
|
145
167
|
if (b.type === "code")
|
|
146
168
|
return fence(attr(b, "lang") ?? "", raw);
|
|
169
|
+
// GEP-0005: a data block projects as a fenced code block in its format —
|
|
170
|
+
// the nearest GFM shape (Markdown has no verified-data construct; that loss
|
|
171
|
+
// is the usual --to md lossiness, not a defect of the projection).
|
|
172
|
+
if (b.type === "data")
|
|
173
|
+
return fence(attr(b, "format") ?? "json", raw);
|
|
147
174
|
if (b.type === "math")
|
|
148
175
|
return ["$$", ...raw, "$$"].join("\n");
|
|
149
176
|
if (b.type === "table" && b.table)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geml/geml",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"mcpName": "io.github.geml-spec/geml",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"codemap",
|
|
18
|
+
"skill",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geml
|
|
3
|
+
description: >-
|
|
4
|
+
Read, author, edit, or validate GEML — the General Expressive Markup Language
|
|
5
|
+
(.geml files) and its .gemlhistory versioning sidecar. Use whenever creating
|
|
6
|
+
or modifying a .geml/.gemlhistory file, converting Markdown to GEML, or when
|
|
7
|
+
the user mentions GEML, typed blocks, === fences, or geml-chart. Ensures the
|
|
8
|
+
output parses cleanly (zero error diagnostics) against the reference parser.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Writing and reading GEML correctly
|
|
12
|
+
|
|
13
|
+
GEML expresses **every** kind of structured content — code, tables, diagrams,
|
|
14
|
+
math, callouts, metadata — through **one** primitive: the **typed block**
|
|
15
|
+
(`=== <type> {#id .class key=val}` … `===`). Always finish by **validating**: a
|
|
16
|
+
GEML file is correct only when `geml check` reports **no error diagnostics**
|
|
17
|
+
(exit 0).
|
|
18
|
+
|
|
19
|
+
## Golden rules (the things that are easy to get wrong)
|
|
20
|
+
|
|
21
|
+
1. **Fences are runs of `=` (≥3).** A block closes at a `=` run of **exactly
|
|
22
|
+
the opening length**, or — when the block has an `#id` — at the labeled
|
|
23
|
+
fence `=== #id` (any `=` run ≥3 followed by the id; no length counting).
|
|
24
|
+
2. **Nest with longer fences.** A body containing `===` lines needs a
|
|
25
|
+
**longer** outer fence: `====` wraps `===`. Careful: a same-length bare
|
|
26
|
+
`===` in the body closes the block even if you intend a labeled close —
|
|
27
|
+
the labeled close only spares you length-counting, it does NOT protect
|
|
28
|
+
same-length inner fences.
|
|
29
|
+
3. **Headings are ATX `#` only** (`#`…`######`). No setext underlines, no
|
|
30
|
+
`---` breaks, no YAML frontmatter — metadata is a `=== meta` block, and the
|
|
31
|
+
document TITLE lives there (`title = "…"`), not in an H1. A heading may
|
|
32
|
+
carry a stable explicit id: `## Title {#sec}`.
|
|
33
|
+
4. **Every `#id` is unique per document**, and **every reference must
|
|
34
|
+
resolve** — `[t](#id)`, `[[#id]]`, `[^id]`, `src=`, `data=`,
|
|
35
|
+
`other.geml#id`. An unresolved reference is a build **error**.
|
|
36
|
+
5. **No raw HTML.** Notes → `=== note`, comments → `%%` lines, hidden content
|
|
37
|
+
→ `{hidden}`, addressable prose → `=== text`, verified data → `=== data`
|
|
38
|
+
(json/jsonl; `code` shows text, `data` IS data).
|
|
39
|
+
|
|
40
|
+
## Validate every time
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
geml check file.geml # diagnostics + exit code only; exit 0 = correct
|
|
44
|
+
geml check --json file.geml # machine-readable diagnostics array
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If `geml` is not on PATH: `npm i -g @geml/geml` (package `@geml/geml`, command
|
|
48
|
+
`geml`), or run without installing via `npx -y @geml/geml check file.geml`.
|
|
49
|
+
Inside the geml-spec repo prefer the local build:
|
|
50
|
+
`node geml-parser/dist/geml.js <args>`. If no parser is reachable, follow the
|
|
51
|
+
golden rules and validate once it is.
|
|
52
|
+
|
|
53
|
+
## Work blockwise (agent editing)
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
geml get file.geml '#id' # read ONE block (a heading id = its whole section)
|
|
57
|
+
geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc)
|
|
58
|
+
geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit
|
|
59
|
+
geml revert file.geml '#id' # roll ONE block back (--rev -2 | changed | <rev-id>)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Full reference — pull ONE section, not the whole file
|
|
63
|
+
|
|
64
|
+
`references/authoring.geml` (under this skill's base directory) holds the
|
|
65
|
+
detailed reference. Fetch just the section you need:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
geml get <skill-base>/references/authoring.geml '#tables'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| section | covers |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `#typed-block` | block anatomy, attribute object, examples of every registered type |
|
|
74
|
+
| `#tables` | pipe/CSV bodies, `compute=`, `summary=`, printf display, `span=` merges |
|
|
75
|
+
| `#charts` | `geml-chart` diagrams bound to a table via `data=#id` |
|
|
76
|
+
| `#data` | the `data` block — value tree, `json`/`jsonl` formats, blind append, chart binding |
|
|
77
|
+
| `#inline` | inline markup, links/refs/footnotes, task lists, media embeds |
|
|
78
|
+
| `#hidden` | `%%` comments, `{hidden}`, `{{key}}` interpolation, `=== embed` |
|
|
79
|
+
| `#cli` | every CLI verb — get/set/add/delete/rename, `--to` conversion, check |
|
|
80
|
+
| `#editing` | the blockwise editing loop + `.gemlhistory` versioning |
|
|
81
|
+
| `#project-config` | carrying a project's Claude config docs in GEML, quietly |
|
|
82
|
+
| `#checklist` | full pre-flight authoring checklist |
|