@geml/geml 1.1.1 → 1.4.2

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/serialize.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // parse(serialize(parse(src))) ≅ parse(src)
10
10
  //
11
11
  // verified over the conformance corpus by test/roundtrip.test.mjs.
12
- import { parseInline } from "./inline.js";
12
+ import { META_REF_SRC, parseInline } from "./inline.js";
13
13
  // ---------------------------------------------------------------------------
14
14
  // Values & attributes (§4)
15
15
  // ---------------------------------------------------------------------------
@@ -61,6 +61,26 @@ function serDataValue(v) {
61
61
  function escText(s) {
62
62
  return s.replace(/[\\`*~$\[\]]/g, (c) => "\\" + c);
63
63
  }
64
+ // A literal `{{name}}` in a text run would be re-read as a §4 metadata
65
+ // reference by the next document-level parse — a layer serInlines's
66
+ // parseInline check cannot see — so it is escaped unconditionally, in both the
67
+ // lazy and the escalated pass. Applied after escText, which would otherwise
68
+ // escape the inserted backslash itself.
69
+ //
70
+ // The inserted escape must survive interpolate()'s left-to-right `\x`
71
+ // pairing: when the emitted text directly before the reference ends in an ODD
72
+ // run of backslashes, a single inserted `\` would itself be consumed as that
73
+ // run's escapee, re-exposing `{{` — so double it (`\\{`), which pairs as
74
+ // escaped-backslash + escaped-brace and parses back to the same model text.
75
+ const META_REF_G = new RegExp(META_REF_SRC, "g");
76
+ function escMetaRef(s) {
77
+ return s.replace(META_REF_G, (m, _key, offset) => {
78
+ let bs = 0;
79
+ for (let k = offset - 1; k >= 0 && s[k] === "\\"; k--)
80
+ bs++;
81
+ return (bs % 2 === 1 ? "\\\\{" : "\\{") + m.slice(1);
82
+ });
83
+ }
64
84
  function longestRun(s, ch) {
65
85
  let max = 0;
66
86
  let run = 0;
@@ -91,7 +111,7 @@ function linkDest(n) {
91
111
  // when the verbatim form does not round-trip.
92
112
  function serInline(n, esc) {
93
113
  switch (n.type) {
94
- case "text": return esc ? escText(n.value) : n.value;
114
+ case "text": return escMetaRef(esc ? escText(n.value) : n.value);
95
115
  case "emph": return `*${serSeq(n.children, esc)}*`;
96
116
  case "strong": return `**${serSeq(n.children, esc)}**`;
97
117
  case "strike": return `~~${serSeq(n.children, esc)}~~`;
package/dist/table.js CHANGED
@@ -312,10 +312,7 @@ export function parseTable(body, attrs, line, sink) {
312
312
  const ci = colIndex(name);
313
313
  return ci < 0 ? null : cellNum(ci, row);
314
314
  };
315
- const aggResolve = (fn, name) => {
316
- const ci = colIndex(name);
317
- if (ci < 0)
318
- return null;
315
+ const computeAgg = (fn, ci) => {
319
316
  const vals = [];
320
317
  for (let r = 0; r < model.rows.length; r++) {
321
318
  const v = cellNum(ci, r);
@@ -336,6 +333,30 @@ export function parseTable(body, attrs, line, sink) {
336
333
  return Math.max(...vals);
337
334
  return null;
338
335
  };
336
+ // A given aggregate over a given column is constant across rows, yet the
337
+ // per-row `evalExpr` below used to recompute it with a full-table scan every
338
+ // time — O(R²·M) for a table with R rows and M aggregate uses, so a 5000-row
339
+ // ×100-`sum()` sheet took ~a minute. Memoize each `(fn, column)` result once
340
+ // per formula (`aggReset` clears it). The ONE column whose values genuinely
341
+ // change mid-loop is the column the current formula is writing (`aggBypassCi`)
342
+ // — an aggregate over it is row-dependent, so that one is never cached, which
343
+ // preserves the exact behaviour of in-place / self-referential formulas.
344
+ let aggCache = new Map();
345
+ let aggBypassCi = -1;
346
+ const aggReset = (bypassCi) => { aggCache = new Map(); aggBypassCi = bypassCi; };
347
+ const aggResolve = (fn, name) => {
348
+ const ci = colIndex(name);
349
+ if (ci < 0)
350
+ return null;
351
+ if (ci === aggBypassCi)
352
+ return computeAgg(fn, ci);
353
+ const key = `${fn}:${ci}`;
354
+ if (aggCache.has(key))
355
+ return aggCache.get(key);
356
+ const val = computeAgg(fn, ci);
357
+ aggCache.set(key, val);
358
+ return val;
359
+ };
339
360
  // `compute="Name = expr; Name2 = expr2"` — `;`-separated; may also appear as
340
361
  // compute, compute2, … Each formula adds/overwrites a per-row column.
341
362
  const formulas = Object.entries(attrs)
@@ -367,6 +388,9 @@ export function parseTable(body, attrs, line, sink) {
367
388
  columns.push(name);
368
389
  ci = columns.length - 1;
369
390
  }
391
+ // Fresh aggregate cache per formula; the target column is being written
392
+ // row-by-row so aggregates over it stay uncached.
393
+ aggReset(ci);
370
394
  let failed = false;
371
395
  for (let r = 0; r < model.rows.length && !failed; r++) {
372
396
  try {
@@ -399,6 +423,9 @@ export function parseTable(body, attrs, line, sink) {
399
423
  const summary = columns.map(() => ({ text: "", inlines: [] }));
400
424
  // In the summary row a bare column has no value: only aggregates resolve.
401
425
  const noRow = () => null;
426
+ // No column is mutated while building the summary row, so every aggregate is
427
+ // cacheable against the final table state (no bypass column).
428
+ aggReset(-1);
402
429
  for (const s of summaryDecls) {
403
430
  const eq = s.indexOf("=");
404
431
  if (eq <= 0) {
@@ -458,7 +485,15 @@ export function parseTable(body, attrs, line, sink) {
458
485
  diagnostics.push({ severity: "warning", message: `span \`${sd}\` targets a cell outside the table` });
459
486
  continue;
460
487
  }
461
- cell.span = { rows: sp.rows, cols: sp.cols };
488
+ // A span can never extend past the grid: clamp its extent to the rows/cols
489
+ // actually available from the target cell. Without this, `span="r1c1:9e6x9e6"`
490
+ // makes the renderer's O(rows×cols) coverage sweep hang (DoS). Every row has
491
+ // exactly `columns.length` cells (built above), so the column bound is exact.
492
+ const maxRows = model.rows.length - (sp.row - 1);
493
+ const maxCols = columns.length - (sp.col - 1);
494
+ const rows = Math.max(1, Math.min(sp.rows, maxRows));
495
+ const cols = Math.max(1, Math.min(sp.cols, maxCols));
496
+ cell.span = { rows, cols };
462
497
  }
463
498
  return { model, diagnostics };
464
499
  }
package/dist/to-md.js CHANGED
@@ -127,6 +127,10 @@ function typedToMd(b, notes) {
127
127
  return `[^${b.id}]: ${text}`;
128
128
  }
129
129
  const inner = (b.children ?? []).map((c) => block(c, notes)).filter(Boolean).join("\n\n");
130
+ // `text` is an addressable prose container, not a callout: its children
131
+ // project as plain paragraphs. Only `note` carries blockquote semantics.
132
+ if (b.type === "text")
133
+ return inner;
130
134
  return inner.split("\n").map((l) => (l ? `> ${l}` : ">")).join("\n");
131
135
  }
132
136
  // raw modes
package/package.json CHANGED
@@ -1,58 +1,62 @@
1
- {
2
- "name": "@geml/geml",
3
- "version": "1.1.1",
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
- "codemap",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "engines": {
21
- "node": ">=18"
22
- },
23
- "keywords": [
24
- "geml",
25
- "markup",
26
- "markdown",
27
- "parser",
28
- "cli",
29
- "document",
30
- "typed-block",
31
- "ai",
32
- "agent"
33
- ],
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/geml-spec/geml.git",
37
- "directory": "geml-parser"
38
- },
39
- "homepage": "https://github.com/geml-spec/geml#readme",
40
- "bugs": {
41
- "url": "https://github.com/geml-spec/geml/issues"
42
- },
43
- "scripts": {
44
- "build": "tsc",
45
- "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/history.test.mjs && node test/render-html.test.mjs && node test/codemap.test.mjs && node test/cli.test.mjs && node test/get-set.test.mjs && node test/revert.test.mjs",
46
- "convert": "node dist/geml.js convert",
47
- "parse": "node dist/geml.js",
48
- "coverage": "c8 --all --include=dist/**/*.js --reporter=text --reporter=text-summary npm test",
49
- "coverage:check": "c8 --all --include=dist/**/*.js --check-coverage --lines 90 --statements 90 --functions 92 --branches 80 npm test",
50
- "prepublishOnly": "npm run build"
51
- },
52
- "license": "MIT",
53
- "devDependencies": {
54
- "@types/node": "^22.19.21",
55
- "c8": "^10.1.3",
56
- "typescript": "^5.9.3"
57
- }
58
- }
1
+ {
2
+ "name": "@geml/geml",
3
+ "version": "1.4.2",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "GEML(General Expressive Markup Language) — one format, two readers. People and AI agents co-write the same document: plain text that stays legible for people, and addressable, verifiable, and versioned for machines. Reference parser, validator, renderer & CLI for GEML.",
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
+ "codemap",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "keywords": [
24
+ "geml",
25
+ "markup",
26
+ "markdown",
27
+ "parser",
28
+ "cli",
29
+ "document",
30
+ "typed-block",
31
+ "ai",
32
+ "agent",
33
+ "llm",
34
+ "addressable",
35
+ "versioning",
36
+ "docs",
37
+ "code-graph"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/geml-spec/geml.git",
42
+ "directory": "geml-parser"
43
+ },
44
+ "homepage": "https://github.com/geml-spec/geml#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/geml-spec/geml/issues"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc",
50
+ "test": "tsc && node test/all.mjs",
51
+ "parse": "node dist/geml.js",
52
+ "coverage": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --reporter=text --reporter=text-summary node test/all.mjs",
53
+ "coverage:check": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --check-coverage --lines 95 --statements 95 --functions 95 --branches 95 node test/all.mjs",
54
+ "prepublishOnly": "npm run build"
55
+ },
56
+ "license": "MIT",
57
+ "devDependencies": {
58
+ "@types/node": "^22.19.21",
59
+ "c8": "^10.1.3",
60
+ "typescript": "^5.9.3"
61
+ }
62
+ }