@geml/geml 1.7.8 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/geml.js CHANGED
@@ -91,17 +91,26 @@ function jsonErrorLine(e, text, openLineNo) {
91
91
  export { SEVERITY } from "./diagnostics.js";
92
92
  // Type registry: which body mode each typed block uses. Unknown types are a
93
93
  // warning and fall back to `raw` (forward compatibility, §3/§8).
94
- const REGISTRY = {
95
- code: "raw",
96
- diagram: "raw",
97
- math: "raw",
98
- table: "raw", // structured table parsing lands in M3
99
- data: "raw", // GEP-0005: value tree a format engine parses the raw body in a second stage
100
- embed: "raw", // block transclusion: `src=` points at the content, body unused
101
- note: "flow",
102
- text: "flow", // addressable prose container: an id/attrs for a run of flow, no callout chrome
103
- meta: "data",
104
- };
94
+ //
95
+ // A Map, not an object, because the key is the type name off a fence head — i.e.
96
+ // document-controlled. Indexing a plain object with it answered for the whole
97
+ // prototype chain: `=== constructor` (also toString, valueOf, hasOwnProperty,
98
+ // isPrototypeOf, propertyIsEnumerable, toLocaleString) returned an inherited
99
+ // FUNCTION, which is not undefined, so the unknown-block-type warning never
100
+ // fired and a function reached the model's `mode` field a value the published
101
+ // `BodyMode` type says cannot occur, and one JSON.stringify silently drops.
102
+ // Every other document-keyed registry here is already a Set or a Map.
103
+ const REGISTRY = new Map([
104
+ ["code", "raw"],
105
+ ["diagram", "raw"],
106
+ ["math", "raw"],
107
+ ["table", "raw"], // structured table parsing lands in M3
108
+ ["data", "raw"], // GEP-0005: value tree — a format engine parses the raw body in a second stage
109
+ ["embed", "raw"], // block transclusion: `src=` points at the content, body unused
110
+ ["note", "flow"],
111
+ ["text", "flow"], // addressable prose container: an id/attrs for a run of flow, no callout chrome
112
+ ["meta", "data"],
113
+ ]);
105
114
  // §7: built-in diagram renderer registry. Unknown formats are a warning (the
106
115
  // processor keeps the body raw rather than interpreting it).
107
116
  const DIAGRAM_RENDERERS = new Set(["mermaid", "graphviz", "dot", "d2", "plantuml", "geml-chart", "geml-code-graph"]);
@@ -184,7 +193,7 @@ function slug(text) {
184
193
  return text
185
194
  .toLowerCase()
186
195
  .replace(/`[^`]*`/g, "")
187
- .replace(/[^\p{L}\p{N}\s-]/gu, "")
196
+ .replace(/[^\p{L}\p{N}\s\-_]/gu, "")
188
197
  .trim()
189
198
  .replace(/\s+/g, "-");
190
199
  }
@@ -439,7 +448,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
439
448
  const how = attrs.id !== undefined ? `${"=".repeat(openLen)} or \`=== #${attrs.id}\`` : "=".repeat(openLen);
440
449
  diags.push({ severity: "error", code: "unterminated-block", message: `unterminated \`${type}\` block (no matching ${how})`, line: openLineNo });
441
450
  }
442
- let mode = REGISTRY[type];
451
+ let mode = REGISTRY.get(type);
443
452
  if (mode === undefined) {
444
453
  diags.push({ severity: "warning", code: "unknown-block-type", message: `unknown block type \`${type}\`; body kept as raw`, line: openLineNo });
445
454
  mode = "raw";
@@ -1111,8 +1120,9 @@ function gatherIds(source) {
1111
1120
  }
1112
1121
  // Pre-scan for `=== meta` blocks (at any fence depth) and merge their
1113
1122
  // `key=val` lines, so `{{key}}` interpolation can resolve forward references.
1114
- function collectMeta(lines) {
1123
+ function collectMeta(lines, diags) {
1115
1124
  const meta = new Map();
1125
+ const firstLine = new Map(); // key → 1-based line of the defining fence
1116
1126
  for (let i = 0; i < lines.length; i++) {
1117
1127
  const open = FENCE_OPEN.exec(lines[i]);
1118
1128
  if (!open || open[2] !== "meta")
@@ -1122,8 +1132,17 @@ function collectMeta(lines) {
1122
1132
  let j = i + 1;
1123
1133
  for (; j < lines.length && !isCloseFence(lines[j], len); j++)
1124
1134
  body.push(lines[j]);
1125
- for (const [k, v] of Object.entries(parseData(body)))
1126
- meta.set(k, String(v));
1135
+ for (const [k, v] of Object.entries(parseData(body))) {
1136
+ if (meta.has(k)) {
1137
+ diags?.push({ severity: "warning", code: "duplicate-meta-key",
1138
+ message: `meta key \`${k}\` already defined at line ${firstLine.get(k)}; later definition at line ${i + 1} is ignored`,
1139
+ line: i + 1 });
1140
+ }
1141
+ else {
1142
+ meta.set(k, String(v));
1143
+ firstLine.set(k, i + 1);
1144
+ }
1145
+ }
1127
1146
  i = j;
1128
1147
  }
1129
1148
  return meta;
@@ -1283,13 +1302,14 @@ function resolveCodeSources(ctx, opts) {
1283
1302
  if (slice === null)
1284
1303
  continue;
1285
1304
  const hasBody = (block.raw ?? []).some((l) => l.trim() !== "");
1286
- if (!hasBody) {
1287
- block.raw = slice;
1305
+ if (hasBody) {
1306
+ // A code block must not carry both src= and an inline body (§3.3).
1307
+ ctx.diags.push({ severity: "error", code: "code-src-and-body",
1308
+ message: `code: carries both \`src=\` and an inline body; exactly one is permitted`,
1309
+ line });
1288
1310
  }
1289
- else if ((block.raw ?? []).join("\n") !== slice.join("\n")) {
1290
- // A body alongside `src=` is a cached snapshot, kept for offline reading.
1291
- // Silence would let the two drift — the very thing the route prevents.
1292
- ctx.diags.push({ severity: "warning", code: "stale-code-snapshot", message: `code block body differs from its source \`${target}\` — the body is a snapshot and is now out of date`, line });
1311
+ else {
1312
+ block.raw = slice;
1293
1313
  }
1294
1314
  }
1295
1315
  }
@@ -1547,7 +1567,8 @@ function resolveCharts(ctx, opts) {
1547
1567
  }
1548
1568
  export function parse(source, opts = {}) {
1549
1569
  const lines = normalizeSource(source).split("\n");
1550
- const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines), resolveDoc: opts.resolveDoc };
1570
+ const diags = [];
1571
+ const ctx = { diags, ids: new Map(), refs: [], meta: collectMeta(lines, diags), resolveDoc: opts.resolveDoc };
1551
1572
  const children = scanBlocks(lines, 0, ctx);
1552
1573
  // Table sources first: a chart reads the build-time model of the table it
1553
1574
  // charts, so that model has to be filled before charts are resolved.
@@ -1659,7 +1680,7 @@ units) {
1659
1680
  // Only a flow body is scanned for nested blocks (raw/data bodies are
1660
1681
  // opaque), so an id inside a `code` body is *not* addressable — exactly
1661
1682
  // the parser's contract.
1662
- if ((REGISTRY[type] ?? "raw") === "flow" && depth < MAX_NESTING) {
1683
+ if ((REGISTRY.get(type) ?? "raw") === "flow" && depth < MAX_NESTING) {
1663
1684
  collectSpans(lines.slice(i + 1, closed ? end - 1 : end), base + i + 1, out, ctx, depth + 1, units);
1664
1685
  }
1665
1686
  i = end;
package/dist/inline.d.ts CHANGED
@@ -70,4 +70,10 @@ export interface RefSink {
70
70
  export declare const META_REF_SRC = "\\{\\{\\s*([A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}";
71
71
  export declare function schemeOf(url: string): string | null;
72
72
  export declare function isSafeUrl(url: string, allowDataImage?: boolean): boolean;
73
- export declare function parseInline(s: string, line: number, sink: RefSink, depth?: number): Inline[];
73
+ interface Pairs {
74
+ br: Int32Array;
75
+ pa: Int32Array;
76
+ off: number;
77
+ }
78
+ export declare function parseInline(s: string, line: number, sink: RefSink, depth?: number, pairs?: Pairs): Inline[];
79
+ export {};