@geml/geml 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codemap/browser-stub.mjs +5 -0
- package/codemap/verify.mjs +10 -3
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +4 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +16 -0
- package/dist/geml.d.ts +5 -1
- package/dist/geml.js +790 -35
- 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 +36 -0
- package/dist/serialize.js +12 -0
- package/dist/table.js +27 -1
- package/dist/to-md.js +5 -0
- 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":
|
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
|
@@ -144,6 +144,11 @@ function typedToMd(b, notes) {
|
|
|
144
144
|
const raw = b.raw ?? [];
|
|
145
145
|
if (b.type === "code")
|
|
146
146
|
return fence(attr(b, "lang") ?? "", raw);
|
|
147
|
+
// GEP-0005: a data block projects as a fenced code block in its format —
|
|
148
|
+
// the nearest GFM shape (Markdown has no verified-data construct; that loss
|
|
149
|
+
// is the usual --to md lossiness, not a defect of the projection).
|
|
150
|
+
if (b.type === "data")
|
|
151
|
+
return fence(attr(b, "format") ?? "json", raw);
|
|
147
152
|
if (b.type === "math")
|
|
148
153
|
return ["$$", ...raw, "$$"].join("\n");
|
|
149
154
|
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.0",
|
|
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 |
|