@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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/attrs.d.ts +9 -0
- package/dist/attrs.js +64 -0
- package/dist/chart.d.ts +28 -0
- package/dist/chart.js +128 -0
- package/dist/from-md.d.ts +5 -0
- package/dist/from-md.js +242 -0
- package/dist/geml.d.ts +70 -0
- package/dist/geml.js +730 -0
- package/dist/history.d.ts +55 -0
- package/dist/history.js +507 -0
- package/dist/inline.d.ts +52 -0
- package/dist/inline.js +418 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.js +508 -0
- package/dist/serialize.d.ts +2 -0
- package/dist/serialize.js +187 -0
- package/dist/table.d.ts +32 -0
- package/dist/table.js +469 -0
- package/dist/to-md.d.ts +5 -0
- package/dist/to-md.js +213 -0
- package/package.json +54 -0
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20
|
+
}
|
|
21
|
+
function escAttr(s) {
|
|
22
|
+
return esc(s).replace(/"/g, """);
|
|
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
|
+
}
|