@geml/geml 1.3.2 → 1.4.3

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.
@@ -0,0 +1,112 @@
1
+ // Structural id-rewriting for `geml set`. `set #id` names the block to edit,
2
+ // so the content spliced in must ADOPT that id — whatever id it declared (or
3
+ // none). This module performs that rewrite parse-aware (per head form) rather
4
+ // than by blind byte replacement, touching ONLY the id: type, classes,
5
+ // attributes, body and the fence pairing all ride along unchanged.
6
+ //
7
+ // Deliberately self-contained — it imports only the shared attribute parser,
8
+ // never geml.ts: geml.ts's module body runs the CLI on import, so a back-import
9
+ // would fire the whole command line just by loading this helper.
10
+ import { parseAttrs } from "./attrs.js";
11
+ // The two head forms, spelled to MIRROR geml.ts's FENCE_OPEN / HEADING (same
12
+ // language) but with the id-bearing brace tail split out so the id can be
13
+ // rewritten while every other byte is copied verbatim:
14
+ // FENCE_HEAD g1 = `=== type` g2 = ws g3 = `{…}`? g4 = trailing ws
15
+ // HEAD_HEAD g1 = `## text` g2 = ws g3 = `{…}`? g4 = trailing ws
16
+ const FENCE_HEAD = /^(={3,}[ \t]+[A-Za-z][A-Za-z0-9_-]*)([ \t]*)(\{.*\})?([ \t]*)$/;
17
+ const HEAD_HEAD = /^(#{1,6}[ \t]+.*?)([ \t]*)(\{[^}]*\})?([ \t]*)$/;
18
+ // Split into physical lines while keeping each line's terminator, so join("")
19
+ // is byte-exact — the same boundaries geml.ts's splitLines() uses. A line ends
20
+ // at `\n`, `\r\n`, or a lone `\r`.
21
+ function splitLines(source) {
22
+ return source.split(/(?<=\n|\r(?!\n))/);
23
+ }
24
+ // Strip a single trailing terminator from one physical line.
25
+ function stripEnding(line) {
26
+ return line.replace(/(\r\n|\r|\n)$/, "");
27
+ }
28
+ // Rewrite the id inside a `{…}` attribute block to `#newId`, keeping the braces
29
+ // and every other class/attr byte. If no id is present, insert `#newId` as the
30
+ // first token. The id token sits at a token boundary (`{` or whitespace) and
31
+ // never inside a quoted value, so the anchored match can't disturb a value like
32
+ // `caption="#x"`.
33
+ function rewriteBraces(braces, newId) {
34
+ if (parseAttrs(braces).id !== undefined) {
35
+ return braces.replace(/([{\s])#[^\s}]+/, `$1#${newId}`);
36
+ }
37
+ const inner = braces.slice(1, -1).replace(/^[ \t]*/, "");
38
+ return `{#${newId}${inner.length ? " " + inner : ""}}`;
39
+ }
40
+ // Rewrite a HEAD line's id declaration to `#newId`. Handles both head forms and
41
+ // all id states: existing brace id, brace attrs without an id, and no braces at
42
+ // all (append `{#newId}`). A line that is neither form is returned unchanged.
43
+ function rewriteHead(head, newId) {
44
+ const rebuild = (m) => {
45
+ const lead = m[1], ws = m[2] ?? "", braces = m[3], trail = m[4] ?? "";
46
+ if (braces)
47
+ return lead + ws + rewriteBraces(braces, newId) + trail;
48
+ return `${lead} {#${newId}}${ws}${trail}`;
49
+ };
50
+ const f = FENCE_HEAD.exec(head);
51
+ if (f)
52
+ return rebuild(f);
53
+ const h = HEAD_HEAD.exec(head);
54
+ if (h)
55
+ return rebuild(h);
56
+ return head;
57
+ }
58
+ // Locate the block's HEAD: the first non-blank, non-`%%` line that opens a fence
59
+ // or a heading. Returns its line index, or -1 when the content has no head
60
+ // (pure prose, or a structural line that is not a head) — the caller decides
61
+ // what that means.
62
+ function findHead(lines) {
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const t = stripEnding(lines[i]);
65
+ if (t.trim() === "" || /^[ \t]*%%/.test(t))
66
+ continue;
67
+ if (FENCE_HEAD.test(t) || HEAD_HEAD.test(t))
68
+ return i;
69
+ return -1; // the first structural line isn't a head: no addressable block
70
+ }
71
+ return -1;
72
+ }
73
+ // Rewrite the HEAD id of the first block in `blockSrc` to `newId`, across every
74
+ // head form:
75
+ // • fence attrs `{#x …}` -> `{#newId …}` (other classes/attrs kept)
76
+ // • fence with attrs but no id, or no braces -> gains `{#newId}`
77
+ // • labeled close `=== #x` -> `=== #newId` (renamed to match the open)
78
+ // • heading `## T {#x}` -> `## T {#newId}`
79
+ // • heading auto-slug (no braces) -> `## T {#newId}` appended
80
+ // Only the id changes; type / classes / attrs / body / fence length are byte-
81
+ // preserved, as are line terminators. Content with no head is returned as-is.
82
+ export function normalizeBlockId(blockSrc, newId) {
83
+ const lines = splitLines(blockSrc);
84
+ const hi = findHead(lines);
85
+ if (hi < 0)
86
+ return blockSrc;
87
+ const headText = stripEnding(lines[hi]);
88
+ const headTerm = lines[hi].slice(headText.length);
89
+ lines[hi] = rewriteHead(headText, newId) + headTerm;
90
+ // For a fence carrying an id, a labeled close `=== #oldId` names that id and
91
+ // must be renamed too — otherwise the open declares #newId while the close
92
+ // still labels #oldId and the block no longer parses. The FIRST close wins
93
+ // (plain equal-length OR labeled), matching geml.ts's fenceClose scan; a
94
+ // plain close needs no rewrite.
95
+ const f = FENCE_HEAD.exec(headText);
96
+ const oldId = f && f[3] ? parseAttrs(f[3]).id : undefined;
97
+ if (f && oldId !== undefined) {
98
+ const openLen = /^=+/.exec(f[1])[0].length;
99
+ for (let j = hi + 1; j < lines.length; j++) {
100
+ const ct = stripEnding(lines[j]);
101
+ const trimmed = ct.replace(/[ \t]+$/, "");
102
+ if (/^=+$/.test(trimmed) && trimmed.length === openLen)
103
+ break; // plain close: done
104
+ const cm = /^(={3,}[ \t]+#)([^\s}]+)([ \t]*)$/.exec(ct);
105
+ if (cm && cm[2] === oldId) {
106
+ lines[j] = cm[1] + newId + cm[3] + lines[j].slice(ct.length);
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ return lines.join("");
112
+ }
package/dist/chart.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type DiagnosticCode } from "./diagnostics.js";
1
2
  import { type Value } from "./attrs.js";
2
3
  import { type TableModel } from "./table.js";
3
4
  export type ChartType = "bar" | "line" | "area" | "pie" | "scatter";
@@ -19,6 +20,7 @@ export interface ChartModel {
19
20
  }
20
21
  export interface ChartDiag {
21
22
  severity: "error" | "warning";
23
+ code: DiagnosticCode;
22
24
  message: string;
23
25
  }
24
26
  export interface ChartResult {
package/dist/chart.js CHANGED
@@ -20,16 +20,16 @@ function str(v) {
20
20
  }
21
21
  export function buildChart(attrs, table) {
22
22
  const diagnostics = [];
23
- const err = (m) => diagnostics.push({ severity: "error", message: m });
24
- const warn = (m) => diagnostics.push({ severity: "warning", message: m });
23
+ const err = (code, m) => diagnostics.push({ severity: "error", code, message: m });
24
+ const warn = (code, m) => diagnostics.push({ severity: "warning", code, message: m });
25
25
  const fail = () => ({ model: null, diagnostics });
26
26
  const typeRaw = str(attrs["type"]);
27
27
  if (!typeRaw) {
28
- err("chart: missing `type`");
28
+ err("chart-missing-type", "chart: missing `type`");
29
29
  return fail();
30
30
  }
31
31
  if (!TYPES.has(typeRaw)) {
32
- err(`chart: unknown type \`${typeRaw}\` (supported: bar, line, area, pie, scatter; use format=vega-lite for others)`);
32
+ err("chart-unknown-type", `chart: unknown type \`${typeRaw}\` (supported: bar, line, area, pie, scatter; use format=vega-lite for others)`);
33
33
  return fail();
34
34
  }
35
35
  const type = typeRaw;
@@ -37,29 +37,29 @@ export function buildChart(attrs, table) {
37
37
  // name is also wrong.
38
38
  const rowsAttr = (str(attrs["rows"]) ?? "data");
39
39
  if (!["data", "all", "summary"].includes(rowsAttr)) {
40
- err(`chart: unknown rows scope \`${rowsAttr}\` (data|all|summary)`);
40
+ err("chart-unknown-rows-scope", `chart: unknown rows scope \`${rowsAttr}\` (data|all|summary)`);
41
41
  return fail();
42
42
  }
43
43
  const x = str(attrs["x"]);
44
44
  const yRaw = str(attrs["y"]);
45
45
  if (!x)
46
- err("chart: missing required channel `x`");
46
+ err("chart-missing-channel", "chart: missing required channel `x`");
47
47
  if (!yRaw)
48
- err("chart: missing required channel `y`");
48
+ err("chart-missing-channel", "chart: missing required channel `y`");
49
49
  if (!x || !yRaw)
50
50
  return fail();
51
51
  let y = yRaw.split(",").map((s) => s.trim()).filter((s) => s !== "");
52
52
  if (y.length === 0) {
53
- err("chart: `y` lists no columns");
53
+ err("chart-empty-channel", "chart: `y` lists no columns");
54
54
  return fail();
55
55
  }
56
56
  // Wrong-channel warnings (channel present but unused by this type).
57
57
  if (attrs["size"] !== undefined && !USES[type].has("size"))
58
- warn(`chart: \`size\` is ignored for type \`${type}\``);
58
+ warn("chart-unused-channel", `chart: \`size\` is ignored for type \`${type}\``);
59
59
  if (attrs["series"] !== undefined && !USES[type].has("series"))
60
- warn(`chart: \`series\` is ignored for type \`${type}\``);
60
+ warn("chart-unused-channel", `chart: \`series\` is ignored for type \`${type}\``);
61
61
  if (type === "pie" && y.length > 1) {
62
- warn("chart: pie uses a single `y`; extra columns ignored");
62
+ warn("chart-unused-channel", "chart: pie uses a single `y`; extra columns ignored");
63
63
  y = [y[0]];
64
64
  }
65
65
  // Optional channels, only when used by this type.
@@ -69,7 +69,7 @@ export function buildChart(attrs, table) {
69
69
  const idx = (name) => table.columns.indexOf(name);
70
70
  for (const name of [x, ...y, ...(series ? [series] : []), ...(size ? [size] : [])]) {
71
71
  if (idx(name) < 0)
72
- err(`chart: column \`${name}\` not found in table`);
72
+ err("chart-unknown-column", `chart: column \`${name}\` not found in table`);
73
73
  }
74
74
  if (diagnostics.some((d) => d.severity === "error"))
75
75
  return fail();
@@ -77,14 +77,14 @@ export function buildChart(attrs, table) {
77
77
  let picked;
78
78
  if (rowsAttr === "summary") {
79
79
  if (!table.summary) {
80
- err("chart: rows=summary but the table has no summary row");
80
+ err("chart-missing-summary-row", "chart: rows=summary but the table has no summary row");
81
81
  return fail();
82
82
  }
83
83
  picked = [table.summary];
84
84
  }
85
85
  else if (rowsAttr === "all") {
86
86
  if (!table.summary)
87
- warn("chart: rows=all but the table has no summary row; using data rows");
87
+ warn("chart-summary-row-unavailable", "chart: rows=all but the table has no summary row; using data rows");
88
88
  picked = table.summary ? [...table.rows, table.summary] : table.rows;
89
89
  }
90
90
  else {
@@ -105,7 +105,7 @@ export function buildChart(attrs, table) {
105
105
  for (const row of picked) {
106
106
  const cells = numIs.map((i) => row[i]);
107
107
  if (cells.some((cell) => (cell?.text ?? "") !== "" && typeof cell?.value !== "number")) {
108
- err("chart: non-numeric value in a y column");
108
+ err("chart-non-numeric-value", "chart: non-numeric value in a y column");
109
109
  return fail();
110
110
  }
111
111
  if (cells.some((cell) => (cell?.text ?? "") === ""))
@@ -0,0 +1,9 @@
1
+ export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "bad-span" | "span-outside-table" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value";
2
+ export interface Diagnostic {
3
+ severity: "error" | "warning";
4
+ code: DiagnosticCode;
5
+ message: string;
6
+ line: number;
7
+ }
8
+ export declare const SEVERITY: Record<DiagnosticCode, "error" | "warning">;
9
+ export declare function normalizeSource(source: string): string;
@@ -0,0 +1,73 @@
1
+ // GEML reference parser — the diagnostic catalogue (spec Appendix A).
2
+ //
3
+ // Every diagnostic a conforming parser emits carries a STABLE `code` in
4
+ // addition to its human-readable `message`. The message is prose: it may be
5
+ // reworded, translated, or given more context between releases. The code is
6
+ // the contract — it is what a conformance test, an editor integration, or a CI
7
+ // gate matches on, and it is what the specification's Appendix A enumerates.
8
+ //
9
+ // `DiagnosticCode` below is the single source of truth: it is a closed union,
10
+ // so a misspelled or unregistered code is a compile error, and the spec's
11
+ // catalogue can be checked against this list mechanically.
12
+ // The severity each code is emitted with. The specification fixes severity per
13
+ // code (Appendix A), so this table is normative, not advisory: a second
14
+ // implementation reporting `unknown-block-type` as an error does not conform.
15
+ export const SEVERITY = {
16
+ "unterminated-block": "error",
17
+ "unknown-block-type": "warning",
18
+ "block-nesting-too-deep": "error",
19
+ "list-nesting-too-deep": "error",
20
+ "inline-nesting-too-deep": "error",
21
+ "duplicate-id": "error",
22
+ "unresolved-reference": "error",
23
+ "unresolved-footnote": "error",
24
+ "unresolved-cross-document-reference": "error",
25
+ "unresolvable-document": "error",
26
+ "unchecked-cross-document-reference": "warning",
27
+ "unknown-metadata-reference": "error",
28
+ "table-src-and-body": "error",
29
+ "unknown-table-format": "warning",
30
+ "bad-compute-formula": "error",
31
+ "unlexable-compute-formula": "error",
32
+ "compute-error": "error",
33
+ "bad-summary-entry": "error",
34
+ "summary-unknown-column": "error",
35
+ "unlexable-summary-expression": "error",
36
+ "summary-error": "error",
37
+ "bad-span": "error",
38
+ "span-outside-table": "warning",
39
+ "unknown-diagram-format": "warning",
40
+ "ignored-diagram-body": "warning",
41
+ "code-graph-missing-src": "warning",
42
+ "code-graph-unresolvable-document": "warning",
43
+ "chart-missing-data": "error",
44
+ "chart-data-not-a-table": "error",
45
+ "chart-missing-type": "error",
46
+ "chart-unknown-type": "error",
47
+ "chart-unknown-rows-scope": "error",
48
+ "chart-missing-channel": "error",
49
+ "chart-empty-channel": "error",
50
+ "chart-unknown-column": "error",
51
+ "chart-unused-channel": "warning",
52
+ "chart-missing-summary-row": "error",
53
+ "chart-summary-row-unavailable": "warning",
54
+ "chart-non-numeric-value": "error",
55
+ };
56
+ // ---------------------------------------------------------------------------
57
+ // Source normalization (spec §0)
58
+ // ---------------------------------------------------------------------------
59
+ // A conforming parser normalizes its input before scanning:
60
+ //
61
+ // 1. a single leading BOM (U+FEFF) is removed;
62
+ // 2. every line ending (CRLF, or a lone CR) becomes LF;
63
+ // 3. U+0000 becomes U+FFFD.
64
+ //
65
+ // All three preserve the LINE COUNT, which is what lets `blockSpans` index the
66
+ // original bytes by line: normalization only ever rewrites bytes *within* a
67
+ // line, never splits or joins one. (1) only touches the first line's leading
68
+ // bytes; (3) is a same-line substitution; (2) is per-line trailing bytes, and
69
+ // splitting on the normalized LF yields exactly the lines the original had.
70
+ export function normalizeSource(source) {
71
+ const noBom = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
72
+ return noBom.replace(/\r\n?/g, "\n").replace(/\0/g, "�");
73
+ }
package/dist/geml.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type Diagnostic } from "./diagnostics.js";
2
3
  import { type Value } from "./attrs.js";
3
4
  import { type Inline } from "./inline.js";
4
5
  import { type TableModel } from "./table.js";
@@ -54,11 +55,7 @@ export type Block = {
54
55
  chart?: ChartModel;
55
56
  hidden?: boolean;
56
57
  };
57
- export interface Diagnostic {
58
- severity: "error" | "warning";
59
- message: string;
60
- line: number;
61
- }
58
+ export { type Diagnostic, type DiagnosticCode, SEVERITY } from "./diagnostics.js";
62
59
  export interface Document {
63
60
  kind: "document";
64
61
  children: Block[];