@geml/geml 1.0.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/dist/render.js ADDED
@@ -0,0 +1,508 @@
1
+ // GEML reference renderer — P0 runtime: a GEML document -> one self-contained,
2
+ // interactive HTML artifact.
3
+ //
4
+ // What an agent hands a person is the `.geml` file. This runtime turns it into a
5
+ // page a browser can open and *use*: prose and headings, callouts, code, math,
6
+ // diagrams, tables you can sort and filter, and charts drawn as inline SVG
7
+ // straight from their bound table (no second copy of the data).
8
+ //
9
+ // Self-containment: the CSS, the table interactivity, and every chart are inlined
10
+ // into the single HTML file. Math (KaTeX) and Mermaid diagrams are the one
11
+ // exception. They load from a CDN, and only when the document actually uses them,
12
+ // so a document of prose, tables and charts is fully self-contained with zero
13
+ // network. Bundling those two engines offline is the next step (roadmap P0 #6).
14
+ const PALETTE = ["#2563eb", "#dc2626", "#059669", "#d97706", "#7c3aed", "#db2777", "#0891b2", "#ea580c"];
15
+ // ---------------------------------------------------------------------------
16
+ // Escaping
17
+ // ---------------------------------------------------------------------------
18
+ function esc(s) {
19
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20
+ }
21
+ function escAttr(s) {
22
+ return esc(s).replace(/"/g, "&quot;");
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Render context
26
+ // ---------------------------------------------------------------------------
27
+ class RenderCtx {
28
+ doc;
29
+ usedMath = false;
30
+ usedMermaid = false;
31
+ labels = new Map(); // id -> link label for [[#id]] auto-refs
32
+ constructor(doc) {
33
+ this.doc = doc;
34
+ this.indexLabels(doc.children);
35
+ }
36
+ // Build the id -> label map: a heading's text, or a block's caption, or its id.
37
+ indexLabels(blocks) {
38
+ for (const b of blocks) {
39
+ if (b.kind === "heading")
40
+ this.labels.set(b.id ?? "", b.text);
41
+ else if (b.kind === "block") {
42
+ if (b.id) {
43
+ const cap = b.attrs["caption"];
44
+ this.labels.set(b.id, typeof cap === "string" ? cap : (b.table?.caption ?? b.id));
45
+ }
46
+ if (b.children)
47
+ this.indexLabels(b.children);
48
+ }
49
+ }
50
+ }
51
+ docTitle() {
52
+ for (const b of this.doc.children) {
53
+ if (b.kind === "block" && b.type === "meta" && b.data && typeof b.data["title"] === "string") {
54
+ return b.data["title"];
55
+ }
56
+ }
57
+ for (const b of this.doc.children)
58
+ if (b.kind === "heading")
59
+ return b.text;
60
+ return undefined;
61
+ }
62
+ // ----- inline -----
63
+ inlines(ns) {
64
+ return ns.map((n) => this.inline(n)).join("");
65
+ }
66
+ inline(n) {
67
+ switch (n.type) {
68
+ case "text": return esc(n.value);
69
+ case "emph": return `<em>${this.inlines(n.children)}</em>`;
70
+ case "strong": return `<strong>${this.inlines(n.children)}</strong>`;
71
+ case "strike": return `<del>${this.inlines(n.children)}</del>`;
72
+ case "code": return `<code>${esc(n.value)}</code>`;
73
+ case "math":
74
+ this.usedMath = true;
75
+ return `<span class="math">\\(${esc(n.value)}\\)</span>`;
76
+ case "break": return "<br>\n";
77
+ case "image": return this.media(n);
78
+ case "link": return this.link(n);
79
+ case "autoref": {
80
+ const href = n.doc ? `${n.doc.replace(/\.geml$/, ".html")}#${n.anchor}` : `#${n.anchor}`;
81
+ const label = n.doc ? (n.anchor ?? n.doc) : (this.labels.get(n.anchor) ?? n.anchor);
82
+ return `<a href="${escAttr(href)}">${esc(label)}</a>`;
83
+ }
84
+ case "footnote": return `<sup class="fn"><a href="#${escAttr(n.ref)}">${esc(n.ref)}</a></sup>`;
85
+ }
86
+ }
87
+ media(n) {
88
+ const src = escAttr(n.src);
89
+ if (n.as === "video")
90
+ return `<video class="media" src="${src}" controls></video>`;
91
+ if (n.as === "audio")
92
+ return `<audio class="media" src="${src}" controls></audio>`;
93
+ return `<img class="media" src="${src}" alt="${escAttr(n.alt)}">`;
94
+ }
95
+ link(n) {
96
+ let href = "#";
97
+ if (n.href)
98
+ href = n.href;
99
+ else if (n.doc)
100
+ href = `${n.doc.replace(/\.geml$/, ".html")}${n.anchor ? "#" + n.anchor : ""}`;
101
+ else if (n.anchor)
102
+ href = `#${n.anchor}`;
103
+ const rel = typeof n.attrs["rel"] === "string" ? ` rel="${escAttr(n.attrs["rel"])}"` : "";
104
+ const target = typeof n.attrs["target"] === "string" ? ` target="${escAttr(n.attrs["target"])}"` : "";
105
+ return `<a href="${escAttr(href)}"${rel}${target}>${this.inlines(n.children)}</a>`;
106
+ }
107
+ // ----- blocks -----
108
+ block(b) {
109
+ switch (b.kind) {
110
+ case "hidden": return "";
111
+ case "heading": {
112
+ if (b.hidden)
113
+ return "";
114
+ const id = b.id ? ` id="${escAttr(b.id)}"` : "";
115
+ const lvl = Math.min(6, Math.max(1, b.level));
116
+ return `<h${lvl}${id}>${this.inlines(b.inlines)}</h${lvl}>`;
117
+ }
118
+ case "paragraph": {
119
+ const html = this.inlines(b.inlines).trim();
120
+ return html === "" ? "" : `<p>${html}</p>`;
121
+ }
122
+ case "list": return this.list(b);
123
+ case "block": return this.typed(b);
124
+ }
125
+ }
126
+ list(b) {
127
+ const tag = b.ordered ? "ol" : "ul";
128
+ const start = b.ordered && b.start !== undefined && b.start !== 1 ? ` start="${b.start}"` : "";
129
+ const isTask = b.items.some((it) => it.checked !== undefined);
130
+ const items = b.items.map((it) => {
131
+ let inner = this.inlines(it.inlines);
132
+ if (b.loose)
133
+ inner = `<p>${inner}</p>`;
134
+ const box = it.checked === undefined ? "" : `<input type="checkbox" disabled${it.checked ? " checked" : ""}> `;
135
+ const kids = (it.children ?? []).map((c) => this.block(c)).filter((s) => s).join("\n");
136
+ const cls = it.checked === undefined ? "" : ' class="task"';
137
+ return ` <li${cls}>${box}${inner}${kids ? "\n" + kids : ""}</li>`;
138
+ }).join("\n");
139
+ return `<${tag}${isTask ? ' class="task-list"' : ""}${start}>\n${items}\n</${tag}>`;
140
+ }
141
+ typed(b) {
142
+ if (b.hidden)
143
+ return ""; // {hidden}: in the model, never rendered
144
+ const raw = (b.raw ?? []).join("\n");
145
+ const caption = typeof b.attrs["caption"] === "string" ? b.attrs["caption"] : undefined;
146
+ const idAttr = b.id ? ` id="${escAttr(b.id)}"` : "";
147
+ switch (b.type) {
148
+ case "meta": return ""; // header metadata, not body content
149
+ case "code": {
150
+ const lang = typeof b.attrs["lang"] === "string" ? b.attrs["lang"] : "";
151
+ const cls = lang ? ` class="language-${escAttr(lang)}"` : "";
152
+ return `<pre${idAttr}><code${cls}>${esc(raw)}</code></pre>`;
153
+ }
154
+ case "output":
155
+ return `<pre class="output"${idAttr}><code>${esc(raw)}</code></pre>`;
156
+ case "math":
157
+ this.usedMath = true;
158
+ return `<div class="math-block"${idAttr}>\\[${esc(raw)}\\]</div>`;
159
+ case "note":
160
+ case "aside": {
161
+ const classes = ["callout", b.type, ...b.classes].join(" ");
162
+ const inner = (b.children ?? []).map((c) => this.block(c)).filter((s) => s).join("\n");
163
+ return `<aside class="${classes}"${idAttr}>\n${inner}\n</aside>`;
164
+ }
165
+ case "table":
166
+ return b.table ? this.table(b.table, b.id, caption) : `<p class="render-error">table failed to parse</p>`;
167
+ case "diagram":
168
+ return this.diagram(b, raw, caption);
169
+ default: {
170
+ // Unknown type: preserved as raw (spec §3). Show it, labelled.
171
+ return `<figure${idAttr}><pre class="diagram-src" data-type="${escAttr(b.type)}">${esc(raw)}</pre>` +
172
+ `<figcaption>unknown block type <code>${esc(b.type)}</code>; shown as raw</figcaption></figure>`;
173
+ }
174
+ }
175
+ }
176
+ diagram(b, raw, caption) {
177
+ const idAttr = b.id ? ` id="${escAttr(b.id)}"` : "";
178
+ const fmt = typeof b.attrs["format"] === "string" ? b.attrs["format"] : "";
179
+ const cap = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
180
+ if (fmt === "geml-chart") {
181
+ if (b.chart)
182
+ return `<figure class="chart"${idAttr}>${chartSvg(b.chart, caption)}${cap}</figure>`;
183
+ return `<figure${idAttr}><p class="render-error">chart could not be built (see diagnostics)</p>${cap}</figure>`;
184
+ }
185
+ if (fmt === "mermaid") {
186
+ this.usedMermaid = true;
187
+ return `<figure${idAttr}><pre class="mermaid">${esc(raw)}</pre>${cap}</figure>`;
188
+ }
189
+ // graphviz / d2 / plantuml / vega-lite / unknown: no bundled engine yet.
190
+ return `<figure${idAttr}><pre class="diagram-src" data-format="${escAttr(fmt)}">${esc(raw)}</pre>` +
191
+ `<figcaption>${caption ? esc(caption) + " — " : ""}<code>${esc(fmt || "diagram")}</code> (no bundled renderer in this build)</figcaption></figure>`;
192
+ }
193
+ table(t, id, caption) {
194
+ const idAttr = id ? ` id="${escAttr(id)}"` : "";
195
+ const alignStyle = (a) => (a ? ` style="text-align:${a}"` : "");
196
+ // Coverage grid for declared spans, so cells a span covers are not emitted.
197
+ const covered = t.rows.map((r) => r.map(() => false));
198
+ t.rows.forEach((row, r) => row.forEach((cell, c) => {
199
+ if (!cell.span)
200
+ return;
201
+ for (let dr = 0; dr < cell.span.rows; dr++)
202
+ for (let dc = 0; dc < cell.span.cols; dc++) {
203
+ if (dr === 0 && dc === 0)
204
+ continue;
205
+ const rr = r + dr, cc = c + dc;
206
+ if (covered[rr]?.[cc] !== undefined)
207
+ covered[rr][cc] = true;
208
+ }
209
+ }));
210
+ const thead = t.header
211
+ ? `<thead><tr>${t.columns.map((col, c) => `<th${alignStyle(t.align[c])}>${esc(col)}</th>`).join("")}</tr></thead>`
212
+ : "";
213
+ const bodyRows = t.rows.map((row, r) => {
214
+ const cells = row.map((cell, c) => {
215
+ if (covered[r]?.[c])
216
+ return "";
217
+ const span = cell.span ? `${cell.span.rows > 1 ? ` rowspan="${cell.span.rows}"` : ""}${cell.span.cols > 1 ? ` colspan="${cell.span.cols}"` : ""}` : "";
218
+ const cls = cell.computed ? ' class="computed"' : "";
219
+ const sortVal = typeof cell.value === "number" ? ` data-sort="${cell.value}"` : "";
220
+ return `<td${alignStyle(cell.align ?? t.align[c])}${span}${cls}${sortVal}>${this.inlines(cell.inlines)}</td>`;
221
+ }).join("");
222
+ return `<tr>${cells}</tr>`;
223
+ }).join("\n");
224
+ const tfoot = t.summary
225
+ ? `<tfoot><tr>${t.summary.map((cell, c) => {
226
+ const sortVal = typeof cell.value === "number" ? ` data-sort="${cell.value}"` : "";
227
+ return `<td${alignStyle(cell.align ?? t.align[c])}${sortVal}>${this.inlines(cell.inlines)}</td>`;
228
+ }).join("")}</tr></tfoot>`
229
+ : "";
230
+ const cap = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
231
+ const tools = `<div class="table-tools"><input class="table-filter" type="search" placeholder="Filter rows…" aria-label="Filter table rows"></div>`;
232
+ return `<figure class="table-figure"${idAttr}>${tools}` +
233
+ `<table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table>${cap}</figure>`;
234
+ }
235
+ }
236
+ // ---------------------------------------------------------------------------
237
+ // Charts: a ChartModel -> inline SVG (fully self-contained, no dependency)
238
+ // ---------------------------------------------------------------------------
239
+ function niceMax(v) {
240
+ if (v <= 0)
241
+ return 1;
242
+ const pow = Math.pow(10, Math.floor(Math.log10(v)));
243
+ const f = v / pow;
244
+ const nice = f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;
245
+ return nice * pow;
246
+ }
247
+ function chartSvg(m, title) {
248
+ if (m.type === "pie")
249
+ return pieSvg(m, title);
250
+ if (m.type === "scatter")
251
+ return scatterSvg(m, title);
252
+ return cartesianSvg(m, title); // bar | line | area
253
+ }
254
+ function svgFrame(title, W, H, body) {
255
+ const t = title ? `<text x="${W / 2}" y="22" text-anchor="middle" class="c-title">${esc(title)}</text>` : "";
256
+ return `<svg viewBox="0 0 ${W} ${H}" class="geml-chart" role="img" aria-label="${escAttr(title ?? "chart")}">${t}${body}</svg>`;
257
+ }
258
+ function legend(names, x, y) {
259
+ return names.map((n, i) => {
260
+ const yy = y + i * 18;
261
+ return `<rect x="${x}" y="${yy}" width="11" height="11" rx="2" fill="${PALETTE[i % PALETTE.length]}"></rect>` +
262
+ `<text x="${x + 16}" y="${yy + 10}" class="c-legend">${esc(n)}</text>`;
263
+ }).join("");
264
+ }
265
+ function cartesianSvg(m, title) {
266
+ const W = 760, H = 380;
267
+ const top = title ? 40 : 22, right = 20, bottom = 64, left = 56;
268
+ const pw = W - left - right, ph = H - top - bottom;
269
+ const cats = m.dataset.categories;
270
+ const series = m.y;
271
+ const vals = series.map((s) => m.dataset.numbers[s] ?? []);
272
+ const flat = vals.flat();
273
+ const dataMax = Math.max(0, ...flat);
274
+ const dataMin = Math.min(0, ...flat);
275
+ const yMax = niceMax(dataMax);
276
+ const yMin = dataMin < 0 ? -niceMax(-dataMin) : 0;
277
+ const range = yMax - yMin || 1;
278
+ const yOf = (v) => top + ph * (1 - (v - yMin) / range);
279
+ const n = Math.max(1, cats.length);
280
+ const band = pw / n;
281
+ const cx = (i) => left + band * (i + 0.5);
282
+ // y grid + ticks
283
+ const ticks = 5;
284
+ let grid = "";
285
+ for (let i = 0; i <= ticks; i++) {
286
+ const v = yMin + (range * i) / ticks;
287
+ const y = yOf(v);
288
+ grid += `<line x1="${left}" y1="${y}" x2="${left + pw}" y2="${y}" class="c-grid"></line>`;
289
+ grid += `<text x="${left - 8}" y="${y + 4}" text-anchor="end" class="c-tick">${fmtNum(v)}</text>`;
290
+ }
291
+ // x labels
292
+ let xlab = "";
293
+ cats.forEach((c, i) => {
294
+ xlab += `<text x="${cx(i)}" y="${top + ph + 18}" text-anchor="middle" class="c-tick">${esc(trunc(c, 12))}</text>`;
295
+ });
296
+ let marks = "";
297
+ if (m.type === "bar") {
298
+ const groupW = band * 0.8;
299
+ const bw = groupW / series.length;
300
+ series.forEach((s, si) => {
301
+ (m.dataset.numbers[s] ?? []).forEach((v, i) => {
302
+ const x = cx(i) - groupW / 2 + si * bw;
303
+ const y0 = yOf(0), y1 = yOf(v);
304
+ const y = Math.min(y0, y1), h = Math.abs(y1 - y0);
305
+ marks += `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${(bw * 0.92).toFixed(1)}" height="${h.toFixed(1)}" fill="${PALETTE[si % PALETTE.length]}"><title>${esc(s)} · ${esc(cats[i] ?? "")}: ${fmtNum(v)}</title></rect>`;
306
+ });
307
+ });
308
+ }
309
+ else {
310
+ // line / area
311
+ series.forEach((s, si) => {
312
+ const color = PALETTE[si % PALETTE.length];
313
+ const pts = (m.dataset.numbers[s] ?? []).map((v, i) => `${cx(i).toFixed(1)},${yOf(v).toFixed(1)}`);
314
+ if (pts.length === 0)
315
+ return;
316
+ if (m.type === "area") {
317
+ const base = yOf(Math.max(yMin, 0));
318
+ marks += `<polygon points="${cx(0).toFixed(1)},${base} ${pts.join(" ")} ${cx(cats.length - 1).toFixed(1)},${base}" fill="${color}" fill-opacity="0.18"></polygon>`;
319
+ }
320
+ marks += `<polyline points="${pts.join(" ")}" fill="none" stroke="${color}" stroke-width="2.5"></polyline>`;
321
+ (m.dataset.numbers[s] ?? []).forEach((v, i) => {
322
+ marks += `<circle cx="${cx(i).toFixed(1)}" cy="${yOf(v).toFixed(1)}" r="3.5" fill="${color}"><title>${esc(s)} · ${esc(cats[i] ?? "")}: ${fmtNum(v)}</title></circle>`;
323
+ });
324
+ });
325
+ }
326
+ const axis = `<line x1="${left}" y1="${yOf(Math.max(yMin, 0))}" x2="${left + pw}" y2="${yOf(Math.max(yMin, 0))}" class="c-axis"></line>`;
327
+ const leg = series.length > 1 ? legend(series, left + 8, top + 4) : "";
328
+ return svgFrame(title, W, H, grid + axis + marks + xlab + leg);
329
+ }
330
+ function pieSvg(m, title) {
331
+ const W = 760, H = 380, top = title ? 40 : 22;
332
+ const cx = 250, cy = top + (H - top) / 2, r = Math.min(140, (H - top) / 2 - 16);
333
+ const col = m.y[0];
334
+ const data = m.dataset.numbers[col] ?? [];
335
+ const total = data.reduce((a, b) => a + b, 0) || 1;
336
+ let a0 = -Math.PI / 2;
337
+ let slices = "";
338
+ data.forEach((v, i) => {
339
+ const a1 = a0 + (v / total) * Math.PI * 2;
340
+ const large = a1 - a0 > Math.PI ? 1 : 0;
341
+ const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0);
342
+ const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
343
+ slices += `<path d="M${cx},${cy} L${x0.toFixed(1)},${y0.toFixed(1)} A${r},${r} 0 ${large} 1 ${x1.toFixed(1)},${y1.toFixed(1)} Z" fill="${PALETTE[i % PALETTE.length]}"><title>${esc(m.dataset.categories[i] ?? "")}: ${fmtNum(v)} (${((v / total) * 100).toFixed(1)}%)</title></path>`;
344
+ a0 = a1;
345
+ });
346
+ const leg = legend(m.dataset.categories, 470, top + 16);
347
+ return svgFrame(title, W, H, slices + leg);
348
+ }
349
+ function scatterSvg(m, title) {
350
+ const W = 760, H = 380;
351
+ const top = title ? 40 : 22, right = 20, bottom = 64, left = 56;
352
+ const pw = W - left - right, ph = H - top - bottom;
353
+ const yCol = m.y[0];
354
+ const ys = m.dataset.numbers[yCol] ?? [];
355
+ // x: parse the category text as a number; fall back to the row index.
356
+ const xs = m.dataset.categories.map((c, i) => { const v = parseFloat(c); return Number.isFinite(v) ? v : i; });
357
+ const sizes = m.size ? (m.dataset.numbers[m.size] ?? []) : [];
358
+ const xMax = niceMax(Math.max(1, ...xs)), xMin = Math.min(0, ...xs);
359
+ const yMax = niceMax(Math.max(1, ...ys)), yMin = Math.min(0, ...ys);
360
+ const xr = xMax - xMin || 1, yr = yMax - yMin || 1;
361
+ const xOf = (v) => left + pw * ((v - xMin) / xr);
362
+ const yOf = (v) => top + ph * (1 - (v - yMin) / yr);
363
+ const sMax = Math.max(1, ...sizes);
364
+ const rOf = (i) => m.size ? 4 + 14 * Math.sqrt((sizes[i] ?? 0) / sMax) : 5;
365
+ let grid = "";
366
+ for (let i = 0; i <= 5; i++) {
367
+ const v = yMin + (yr * i) / 5, y = yOf(v);
368
+ grid += `<line x1="${left}" y1="${y}" x2="${left + pw}" y2="${y}" class="c-grid"></line>`;
369
+ grid += `<text x="${left - 8}" y="${y + 4}" text-anchor="end" class="c-tick">${fmtNum(v)}</text>`;
370
+ }
371
+ let pts = "";
372
+ ys.forEach((v, i) => {
373
+ pts += `<circle cx="${xOf(xs[i] ?? 0).toFixed(1)}" cy="${yOf(v).toFixed(1)}" r="${rOf(i).toFixed(1)}" fill="${PALETTE[0]}" fill-opacity="0.7"><title>${esc(m.dataset.categories[i] ?? "")}: (${fmtNum(xs[i] ?? 0)}, ${fmtNum(v)})</title></circle>`;
374
+ });
375
+ const axis = `<line x1="${left}" y1="${top + ph}" x2="${left + pw}" y2="${top + ph}" class="c-axis"></line>`;
376
+ return svgFrame(title, W, H, grid + axis + pts);
377
+ }
378
+ function fmtNum(v) {
379
+ if (Math.abs(v) >= 1000)
380
+ return v.toLocaleString("en-US");
381
+ return String(parseFloat(v.toPrecision(4)));
382
+ }
383
+ function trunc(s, n) {
384
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
385
+ }
386
+ // ---------------------------------------------------------------------------
387
+ // Page shell, inline CSS, inline interactivity JS
388
+ // ---------------------------------------------------------------------------
389
+ const CSS = `
390
+ :root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
391
+ * { box-sizing: border-box; }
392
+ 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; }
393
+ main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
394
+ h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
395
+ h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
396
+ h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
397
+ h3 { font-size:1.25em; } h4 { font-size:1em; }
398
+ p { margin:.7em 0; }
399
+ a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
400
+ code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
401
+ pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
402
+ pre code { background:none; padding:0; font-size:.85em; }
403
+ pre.output { background:#0d1117; color:#e6edf3; }
404
+ pre.output code { color:inherit; }
405
+ ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
406
+ ul.task-list { list-style:none; padding-left:.2em; } li.task input { margin-right:.5em; }
407
+ aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
408
+ aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
409
+ aside.warning { border-left-color:#d97706; background:#fff8f0; }
410
+ aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
411
+ figure { margin:1.2em 0; }
412
+ figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
413
+ table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
414
+ table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
415
+ table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
416
+ table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
417
+ table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
418
+ table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
419
+ table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
420
+ table.geml-table td.computed { color:#0a7c52; }
421
+ table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
422
+ .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; }
423
+ .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
424
+ .c-title { font-size:15px; font-weight:600; fill:var(--fg); }
425
+ .c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
426
+ .media { max-width:100%; border-radius:8px; }
427
+ .diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
428
+ .math-block { overflow-x:auto; padding:.4em 0; }
429
+ sup.fn a { font-size:.75em; }
430
+ .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
431
+ .geml-footer code { font-size:.95em; }
432
+ `;
433
+ const JS = `
434
+ (function () {
435
+ function cmp(a, b) {
436
+ var na = a.dataset.sort, nb = b.dataset.sort;
437
+ if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
438
+ return (a.textContent || "").localeCompare(b.textContent || "");
439
+ }
440
+ document.querySelectorAll("table.geml-table").forEach(function (table) {
441
+ var tbody = table.tBodies[0];
442
+ if (!tbody) return;
443
+ // Sort on header click.
444
+ var ths = table.tHead ? table.tHead.rows[0].cells : [];
445
+ Array.prototype.forEach.call(ths, function (th, col) {
446
+ th.addEventListener("click", function () {
447
+ var dir = th.classList.contains("asc") ? "desc" : "asc";
448
+ Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
449
+ th.classList.add(dir);
450
+ var rows = Array.prototype.slice.call(tbody.rows);
451
+ rows.sort(function (r1, r2) {
452
+ var c = cmp(r1.cells[col], r2.cells[col]);
453
+ return dir === "asc" ? c : -c;
454
+ });
455
+ rows.forEach(function (r) { tbody.appendChild(r); });
456
+ });
457
+ });
458
+ // Filter rows.
459
+ var fig = table.closest(".table-figure");
460
+ var input = fig ? fig.querySelector(".table-filter") : null;
461
+ if (input) input.addEventListener("input", function () {
462
+ var q = input.value.toLowerCase();
463
+ Array.prototype.forEach.call(tbody.rows, function (r) {
464
+ r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
465
+ });
466
+ });
467
+ });
468
+ })();
469
+ `;
470
+ function page(title, body, ctx, source) {
471
+ const mathHead = ctx.usedMath
472
+ ? `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">\n` +
473
+ `<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>\n` +
474
+ `<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:'\\\\[',right:'\\\\]',display:true},{left:'\\\\(',right:'\\\\)',display:false}]})"></script>\n`
475
+ : "";
476
+ const mermaidHead = ctx.usedMermaid
477
+ ? `<script type="module">import m from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";m.initialize({startOnLoad:true});</script>\n`
478
+ : "";
479
+ const footer = source
480
+ ? `<footer class="geml-footer">Rendered from <code>${esc(source)}</code> by the GEML runtime. Tables are sortable and filterable; the chart is inline SVG drawn from its bound table.</footer>`
481
+ : "";
482
+ return `<!doctype html>
483
+ <html lang="en">
484
+ <head>
485
+ <meta charset="utf-8">
486
+ <meta name="viewport" content="width=device-width, initial-scale=1">
487
+ <title>${esc(title)}</title>
488
+ <style>${CSS}</style>
489
+ ${mathHead}${mermaidHead}</head>
490
+ <body>
491
+ <main>
492
+ ${body}
493
+ </main>
494
+ ${footer}
495
+ <script>${JS}</script>
496
+ </body>
497
+ </html>
498
+ `;
499
+ }
500
+ // ---------------------------------------------------------------------------
501
+ // Public entry
502
+ // ---------------------------------------------------------------------------
503
+ export function renderHtml(doc, opts = {}) {
504
+ const ctx = new RenderCtx(doc);
505
+ const body = doc.children.map((b) => ctx.block(b)).filter((s) => s !== "").join("\n");
506
+ const title = opts.title ?? ctx.docTitle() ?? "GEML document";
507
+ return page(title, body, ctx, opts.source);
508
+ }
@@ -0,0 +1,2 @@
1
+ import type { Block, Document } from "./geml.js";
2
+ export declare function serialize(doc: Document | Block[]): string;