@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/to-md.js ADDED
@@ -0,0 +1,213 @@
1
+ // GEML -> Markdown projection (the inverse direction of from-md.ts).
2
+ //
3
+ // This is a *lossy* export: Markdown has no typed-block primitive, so each GEML
4
+ // construct is projected to the nearest GFM shape — headings, fenced code,
5
+ // blockquotes (note/aside), GFM tables (from the computed table model), `$$`
6
+ // math, mermaid fences, YAML frontmatter (meta), footnote definitions. Things
7
+ // GFM cannot express (geml-chart, `{hidden}` blocks, block ids/classes) are
8
+ // dropped or degraded, and each such loss is reported in `notes` so a caller
9
+ // (and an agent) knows the conversion was not faithful.
10
+ // ---------------------------------------------------------------------------
11
+ // Inline
12
+ // ---------------------------------------------------------------------------
13
+ // Escape the characters that could start a Markdown inline construct, so a
14
+ // literal text run renders verbatim. Kept deliberately light — Markdown is
15
+ // forgiving, and over-escaping produces noisy output.
16
+ function escText(s) {
17
+ return s.replace(/[\\`*_\[\]]/g, (c) => "\\" + c);
18
+ }
19
+ function linkDest(n) {
20
+ if (n.href !== undefined)
21
+ return n.href;
22
+ if (n.doc !== undefined)
23
+ return n.anchor !== undefined ? `${n.doc}#${n.anchor}` : n.doc;
24
+ if (n.anchor !== undefined)
25
+ return `#${n.anchor}`;
26
+ return "";
27
+ }
28
+ function inline(n) {
29
+ switch (n.type) {
30
+ case "text": return escText(n.value);
31
+ case "emph": return `*${seq(n.children)}*`;
32
+ case "strong": return `**${seq(n.children)}**`;
33
+ case "strike": return `~~${seq(n.children)}~~`;
34
+ case "code": return "`" + n.value + "`";
35
+ case "math": return `$${n.value}$`;
36
+ case "break": return " \n";
37
+ case "image": return `![${n.alt}](${n.src})`;
38
+ case "link": return `[${seq(n.children)}](${linkDest(n)})`;
39
+ // Markdown has no auto-reference; project to a plain link to the anchor.
40
+ case "autoref": return n.doc !== undefined ? `[${n.doc}#${n.anchor}](${n.doc}#${n.anchor})` : `[#${n.anchor}](#${n.anchor})`;
41
+ case "footnote": return `[^${n.ref}]`;
42
+ }
43
+ }
44
+ function seq(ns) {
45
+ return ns.map(inline).join("");
46
+ }
47
+ // Inline text for a table cell: render inlines, then neutralise the two bytes
48
+ // that would break a GFM cell.
49
+ function cellText(c) {
50
+ return seq(c.inlines).replace(/\|/g, "\\|").replace(/\n/g, " ");
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Tables
54
+ // ---------------------------------------------------------------------------
55
+ function sep(a) {
56
+ if (a === "center")
57
+ return ":--:";
58
+ if (a === "right")
59
+ return "---:";
60
+ if (a === "left")
61
+ return ":---";
62
+ return "---";
63
+ }
64
+ function tableToMd(t, notes) {
65
+ if (t.src !== undefined)
66
+ notes.add(`table from external source \`${t.src}\` is not inlined; emitted header only`);
67
+ const cols = t.columns;
68
+ const lines = [];
69
+ if (t.caption)
70
+ lines.push(`*${t.caption}*`, "");
71
+ lines.push(`| ${cols.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`);
72
+ lines.push(`| ${cols.map((_, i) => sep(t.align[i])).join(" | ")} |`);
73
+ const pad = (cells) => {
74
+ while (cells.length < cols.length)
75
+ cells.push("");
76
+ return cells.slice(0, cols.length);
77
+ };
78
+ for (const row of t.rows)
79
+ lines.push(`| ${pad(row.map(cellText)).join(" | ")} |`);
80
+ if (t.summary)
81
+ lines.push(`| ${pad(t.summary.map(cellText)).join(" | ")} |`);
82
+ return lines.join("\n");
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Blocks
86
+ // ---------------------------------------------------------------------------
87
+ function listToMd(b, indent, notes) {
88
+ const out = [];
89
+ const start = b.start ?? 1;
90
+ b.items.forEach((item, k) => {
91
+ const marker = b.ordered ? `${start + k}. ` : "- ";
92
+ const task = item.checked === undefined ? "" : item.checked ? "[x] " : "[ ] ";
93
+ out.push(indent + marker + task + seq(item.inlines));
94
+ for (const child of item.children ?? []) {
95
+ out.push(child.kind === "list" ? listToMd(child, indent + " ", notes) : block(child, notes));
96
+ }
97
+ if (b.loose && k < b.items.length - 1)
98
+ out.push("");
99
+ });
100
+ return out.join("\n");
101
+ }
102
+ function fence(lang, body) {
103
+ // Use a longer fence than any backtick run in the body so it can't close early.
104
+ let max = 2;
105
+ for (const ln of body) {
106
+ const m = /^(`+)/.exec(ln.trim());
107
+ if (m)
108
+ max = Math.max(max, m[1].length);
109
+ }
110
+ const f = "`".repeat(Math.max(3, max + 1));
111
+ return [f + lang, ...body, f].join("\n");
112
+ }
113
+ function attr(b, key) {
114
+ const v = b.attrs[key];
115
+ return typeof v === "string" ? v : v === undefined ? undefined : String(v);
116
+ }
117
+ // A typed block (raw / flow). meta is hoisted to frontmatter elsewhere.
118
+ function typedToMd(b, notes) {
119
+ if (b.hidden) {
120
+ notes.add("`{hidden}` block(s) dropped (not part of the rendered output)");
121
+ return "";
122
+ }
123
+ if (b.mode === "flow") {
124
+ // Footnote definition: a `note.footnote` carrying its ref as the id.
125
+ if (b.type === "note" && b.classes.includes("footnote") && b.id) {
126
+ const text = (b.children ?? []).map((c) => block(c, notes)).join(" ").replace(/\n+/g, " ").trim();
127
+ return `[^${b.id}]: ${text}`;
128
+ }
129
+ if (b.type === "aside")
130
+ notes.add("`aside` block(s) projected to blockquote (Markdown has no aside)");
131
+ const inner = (b.children ?? []).map((c) => block(c, notes)).filter(Boolean).join("\n\n");
132
+ return inner.split("\n").map((l) => (l ? `> ${l}` : ">")).join("\n");
133
+ }
134
+ // raw modes
135
+ const raw = b.raw ?? [];
136
+ if (b.type === "code")
137
+ return fence(attr(b, "lang") ?? "", raw);
138
+ if (b.type === "math")
139
+ return ["$$", ...raw, "$$"].join("\n");
140
+ if (b.type === "output")
141
+ return fence("", raw);
142
+ if (b.type === "table" && b.table)
143
+ return tableToMd(b.table, notes);
144
+ if (b.type === "diagram") {
145
+ const fmt = attr(b, "format") ?? "";
146
+ if (fmt === "geml-chart") {
147
+ // No Markdown chart primitive: degrade to a labelled descriptor.
148
+ notes.add("`geml-chart` block(s) cannot render in Markdown; emitted a descriptor");
149
+ const desc = ["type", "data", "x", "y", "series"].map((k) => { const v = attr(b, k); return v ? `${k}=${v}` : ""; }).filter(Boolean).join(" ");
150
+ return fence("geml-chart", [desc]);
151
+ }
152
+ return fence(fmt, raw); // mermaid renders on GitHub; others stay as a code block
153
+ }
154
+ // Unknown raw type: preserve the body in a fenced block tagged with the type.
155
+ notes.add(`unknown block type \`${b.type}\` emitted as a fenced code block`);
156
+ return fence(b.type, raw);
157
+ }
158
+ function block(b, notes) {
159
+ switch (b.kind) {
160
+ case "heading": {
161
+ if (b.hidden) {
162
+ notes.add("hidden heading dropped");
163
+ return "";
164
+ }
165
+ if (b.id)
166
+ notes.add("heading id/attributes dropped (Markdown has no attribute syntax)");
167
+ return "#".repeat(b.level) + " " + seq(b.inlines);
168
+ }
169
+ case "paragraph": return seq(b.inlines);
170
+ case "hidden": return ""; // `%%` line: never rendered
171
+ case "list": return listToMd(b, "", notes);
172
+ case "block": return typedToMd(b, notes);
173
+ }
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Frontmatter (meta)
177
+ // ---------------------------------------------------------------------------
178
+ function yamlValue(v) {
179
+ if (typeof v === "boolean" || typeof v === "number")
180
+ return String(v);
181
+ return /^[\w .,/@-]+$/.test(v) && v.trim() === v && v !== "" ? v : JSON.stringify(v);
182
+ }
183
+ function frontmatter(metas) {
184
+ const merged = {};
185
+ for (const m of metas)
186
+ Object.assign(merged, m);
187
+ const keys = Object.keys(merged);
188
+ if (!keys.length)
189
+ return "";
190
+ return ["---", ...keys.map((k) => `${k}: ${yamlValue(merged[k])}`), "---"].join("\n");
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Public API
194
+ // ---------------------------------------------------------------------------
195
+ export function gemlToMd(doc) {
196
+ const notes = new Set();
197
+ const metas = [];
198
+ const parts = [];
199
+ for (const b of doc.children) {
200
+ // Hoist every `meta` block to a single YAML frontmatter at the top.
201
+ if (b.kind === "block" && b.type === "meta" && b.mode === "data") {
202
+ metas.push(b.data ?? {});
203
+ continue;
204
+ }
205
+ const md = block(b, notes);
206
+ if (md !== "")
207
+ parts.push(md);
208
+ }
209
+ const fm = frontmatter(metas);
210
+ const body = parts.join("\n\n");
211
+ const md = (fm ? fm + "\n\n" : "") + body + "\n";
212
+ return { md, notes: [...notes] };
213
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@geml/geml",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Reference parser, validator, renderer and CLI for GEML (General Expressive Markup Language) — a plain-text document format that stays legible to people and reliable for machines.",
8
+ "type": "module",
9
+ "bin": {
10
+ "geml": "dist/geml.js"
11
+ },
12
+ "main": "dist/geml.js",
13
+ "types": "dist/geml.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "geml",
24
+ "markup",
25
+ "markdown",
26
+ "parser",
27
+ "cli",
28
+ "document",
29
+ "typed-block",
30
+ "ai",
31
+ "agent"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/xiongjy2104/geml-spec.git",
36
+ "directory": "geml-parser"
37
+ },
38
+ "homepage": "https://github.com/xiongjy2104/geml-spec#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/xiongjy2104/geml-spec/issues"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc",
44
+ "test": "tsc && node test/m2.test.mjs && node test/m3.test.mjs && node test/m4.test.mjs && node test/convert.test.mjs && node test/fixtures.test.mjs && node test/features.test.mjs && node test/render.test.mjs && node test/conformance.test.mjs && node test/second-impl.test.mjs && node test/roundtrip.test.mjs && node test/to-md.test.mjs && node test/cli.test.mjs",
45
+ "convert": "node dist/geml.js convert",
46
+ "parse": "node dist/geml.js",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "license": "MIT",
50
+ "devDependencies": {
51
+ "@types/node": "^22.19.21",
52
+ "typescript": "^5.9.3"
53
+ }
54
+ }