@ferax564/noma-cli 0.11.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/bin/noma.mjs +8 -0
  4. package/dist/ast.d.ts +111 -0
  5. package/dist/ast.js +23 -0
  6. package/dist/ast.js.map +1 -0
  7. package/dist/book.d.ts +56 -0
  8. package/dist/book.js +120 -0
  9. package/dist/book.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/diff.d.ts +29 -0
  14. package/dist/diff.js +77 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/fmt.d.ts +1 -0
  17. package/dist/fmt.js +105 -0
  18. package/dist/fmt.js.map +1 -0
  19. package/dist/ids.d.ts +15 -0
  20. package/dist/ids.js +27 -0
  21. package/dist/ids.js.map +1 -0
  22. package/dist/index.d.ts +20 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/inline.d.ts +14 -0
  26. package/dist/inline.js +83 -0
  27. package/dist/inline.js.map +1 -0
  28. package/dist/loader.d.ts +12 -0
  29. package/dist/loader.js +59 -0
  30. package/dist/loader.js.map +1 -0
  31. package/dist/parser.d.ts +7 -0
  32. package/dist/parser.js +434 -0
  33. package/dist/parser.js.map +1 -0
  34. package/dist/patch.d.ts +61 -0
  35. package/dist/patch.js +530 -0
  36. package/dist/patch.js.map +1 -0
  37. package/dist/renderer-html.d.ts +44 -0
  38. package/dist/renderer-html.js +929 -0
  39. package/dist/renderer-html.js.map +1 -0
  40. package/dist/renderer-json.d.ts +5 -0
  41. package/dist/renderer-json.js +4 -0
  42. package/dist/renderer-json.js.map +1 -0
  43. package/dist/renderer-llm.d.ts +29 -0
  44. package/dist/renderer-llm.js +275 -0
  45. package/dist/renderer-llm.js.map +1 -0
  46. package/dist/renderer-noma.d.ts +10 -0
  47. package/dist/renderer-noma.js +179 -0
  48. package/dist/renderer-noma.js.map +1 -0
  49. package/dist/renderer-site.d.ts +11 -0
  50. package/dist/renderer-site.js +175 -0
  51. package/dist/renderer-site.js.map +1 -0
  52. package/dist/validator.d.ts +24 -0
  53. package/dist/validator.js +699 -0
  54. package/dist/validator.js.map +1 -0
  55. package/dist/verify.d.ts +10 -0
  56. package/dist/verify.js +141 -0
  57. package/dist/verify.js.map +1 -0
  58. package/package.json +83 -0
  59. package/schemas/ast.schema.json +187 -0
  60. package/schemas/capability.schema.json +70 -0
  61. package/schemas/patch-op.schema.json +92 -0
  62. package/schemas/patch-transaction.schema.json +28 -0
  63. package/schemas/transcript.schema.json +95 -0
  64. package/src/ast.ts +152 -0
  65. package/src/book.ts +162 -0
  66. package/src/cli.ts +595 -0
  67. package/src/diff.ts +108 -0
  68. package/src/fmt.ts +126 -0
  69. package/src/ids.ts +42 -0
  70. package/src/index.ts +20 -0
  71. package/src/inline.ts +92 -0
  72. package/src/loader.ts +55 -0
  73. package/src/parser.ts +501 -0
  74. package/src/patch.ts +646 -0
  75. package/src/renderer-html.ts +1047 -0
  76. package/src/renderer-json.ts +9 -0
  77. package/src/renderer-llm.ts +320 -0
  78. package/src/renderer-noma.ts +220 -0
  79. package/src/renderer-site.ts +245 -0
  80. package/src/validator.ts +733 -0
  81. package/src/verify.ts +157 -0
  82. package/themes/dark.css +382 -0
  83. package/themes/default.css +537 -0
package/src/parser.ts ADDED
@@ -0,0 +1,501 @@
1
+ import yaml from "js-yaml";
2
+ import { splitPipeRow } from "./inline.js";
3
+ import type {
4
+ Attrs,
5
+ AttrValue,
6
+ CodeNode,
7
+ DirectiveNode,
8
+ DocumentNode,
9
+ FrontmatterNode,
10
+ ListItemNode,
11
+ ListNode,
12
+ Node,
13
+ ParagraphNode,
14
+ QuoteNode,
15
+ SectionNode,
16
+ TableAlign,
17
+ TableNode,
18
+ ThematicBreakNode,
19
+ } from "./ast.js";
20
+
21
+ export interface ParseOptions {
22
+ /** Optional source filename, kept on the document meta for diagnostics. */
23
+ filename?: string;
24
+ }
25
+
26
+ const FRONTMATTER_RE = /^---\s*$/;
27
+ const HEADING_RE = /^(#{1,6})\s+(.+?)(?:\s+\{([^}]+)\})?\s*$/;
28
+ const FENCE_RE = /^```(\w*)\s*$/;
29
+ const DIRECTIVE_OPEN_RE = /^(:{2,})\s*([a-zA-Z_][\w-]*(?:::[a-zA-Z_][\w-]*)*)\s*(\{.*\})?\s*$/;
30
+ const DIRECTIVE_CLOSE_RE = /^(:{2,})\s*$/;
31
+ const LIST_RE = /^([-*])\s+(.+)$/;
32
+ const ORDERED_LIST_RE = /^(\d+)\.\s+(.+)$/;
33
+ const QUOTE_RE = /^>\s?(.*)$/;
34
+ const THEMATIC_BREAK_RE = /^(?:-{3,}|\*{3,}|_{3,})\s*$/;
35
+ const TABLE_ROW_RE = /^\s*\|.*\|\s*$/;
36
+ const TABLE_SEPARATOR_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
37
+
38
+ const matchOnce = (re: RegExp, s: string): RegExpMatchArray | null => s.match(re);
39
+
40
+ export function parse(source: string, options: ParseOptions = {}): DocumentNode {
41
+ const normalized = source.replace(/\r\n?/g, "\n");
42
+ const lines = normalized.split("\n");
43
+
44
+ const { meta, raw, startLine, endLine: fmEnd } = extractFrontmatter(lines);
45
+ const flatChildren = parseBlocks(lines, startLine, lines.length, 0);
46
+ const children: Node[] = foldSections(flatChildren);
47
+ for (const c of children) computeSectionEndLines(c);
48
+
49
+ if (raw !== "") {
50
+ const fmNode: FrontmatterNode = {
51
+ type: "frontmatter",
52
+ data: meta,
53
+ raw,
54
+ pos: { line: 1, column: 1 },
55
+ endLine: fmEnd,
56
+ };
57
+ children.unshift(fmNode);
58
+ }
59
+
60
+ attachChapterAliases(children, meta, options.filename);
61
+
62
+ return {
63
+ type: "document",
64
+ pos: { line: 1, column: 1 },
65
+ endLine: Math.max(1, lines.length - (normalized.endsWith("\n") ? 1 : 0)),
66
+ meta: { ...(options.filename ? { filename: options.filename } : {}), ...meta },
67
+ children,
68
+ };
69
+ }
70
+
71
+ function attachChapterAliases(
72
+ children: Node[],
73
+ meta: Record<string, unknown>,
74
+ filename: string | undefined,
75
+ ): void {
76
+ const root = children.find((n): n is SectionNode => n.type === "section" && n.level === 1);
77
+ if (!root) return;
78
+
79
+ const aliases = new Set<string>(root.aliases ?? []);
80
+
81
+ if (filename) {
82
+ const base = filename.replace(/\\/g, "/").split("/").pop() ?? filename;
83
+ const stem = base.replace(/\.noma$/i, "").replace(/^\d+[-_]/, "");
84
+ const slug = slugify(stem);
85
+ if (slug && slug !== root.id) aliases.add(slug);
86
+ }
87
+
88
+ const fmAliases = meta.aliases;
89
+ if (Array.isArray(fmAliases)) {
90
+ for (const a of fmAliases) {
91
+ if (typeof a === "string" && a.trim()) aliases.add(a.trim());
92
+ }
93
+ }
94
+
95
+ if (aliases.size > 0) root.aliases = [...aliases];
96
+ }
97
+
98
+ function extractFrontmatter(lines: string[]): {
99
+ meta: Record<string, unknown>;
100
+ raw: string;
101
+ startLine: number;
102
+ endLine: number;
103
+ } {
104
+ if (lines.length === 0 || !FRONTMATTER_RE.test(lines[0] ?? "")) {
105
+ return { meta: {}, raw: "", startLine: 0, endLine: 0 };
106
+ }
107
+ for (let i = 1; i < lines.length; i++) {
108
+ if (FRONTMATTER_RE.test(lines[i] ?? "")) {
109
+ const raw = lines.slice(1, i).join("\n");
110
+ const parsed = yaml.load(raw);
111
+ const meta =
112
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
113
+ ? (parsed as Record<string, unknown>)
114
+ : {};
115
+ return { meta, raw, startLine: i + 1, endLine: i + 1 };
116
+ }
117
+ }
118
+ return { meta: {}, raw: "", startLine: 0, endLine: 0 };
119
+ }
120
+
121
+ function parseBlocks(
122
+ lines: string[],
123
+ from: number,
124
+ to: number,
125
+ parentColons: number,
126
+ ): Node[] {
127
+ const out: Node[] = [];
128
+ let i = from;
129
+
130
+ while (i < to) {
131
+ const line = lines[i] ?? "";
132
+
133
+ if (line.trim() === "") {
134
+ i++;
135
+ continue;
136
+ }
137
+
138
+ const directiveOpen = matchOnce(DIRECTIVE_OPEN_RE, line);
139
+ if (directiveOpen) {
140
+ const colons = directiveOpen[1]!.length;
141
+ if (colons > parentColons || parentColons === 0) {
142
+ const result = parseDirective(lines, i, to, colons);
143
+ out.push(result.node);
144
+ i = result.next;
145
+ continue;
146
+ }
147
+ // Same-or-lower colon count inside a parent: treat as paragraph text
148
+ // rather than spinning forever. Validator will flag the structural issue.
149
+ out.push(paragraph(line, i));
150
+ i++;
151
+ continue;
152
+ }
153
+
154
+ if (matchOnce(DIRECTIVE_CLOSE_RE, line)) {
155
+ out.push(paragraph(line, i));
156
+ i++;
157
+ continue;
158
+ }
159
+
160
+ const heading = matchOnce(HEADING_RE, line);
161
+ if (heading) {
162
+ const level = heading[1]!.length;
163
+ const title = heading[2]!.trim();
164
+ const headingAttrs = heading[3] ? parseAttrs(`{${heading[3]}}`) : {};
165
+ const explicitId =
166
+ typeof headingAttrs.id === "string" ? headingAttrs.id : undefined;
167
+ const section: SectionNode & { _idIsExplicit?: boolean } = {
168
+ type: "section",
169
+ id: explicitId ?? slugify(title),
170
+ level,
171
+ title,
172
+ children: [],
173
+ pos: { line: i + 1, column: 1 },
174
+ };
175
+ if (!explicitId) section._idIsExplicit = false;
176
+ else section._idIsExplicit = true;
177
+ const aliasesAttr = headingAttrs.aliases;
178
+ if (typeof aliasesAttr === "string") {
179
+ const list = aliasesAttr
180
+ .split(/[,\s]+/)
181
+ .map((a) => a.trim())
182
+ .filter(Boolean);
183
+ if (list.length > 0) section.aliases = list;
184
+ }
185
+ out.push(section);
186
+ i++;
187
+ continue;
188
+ }
189
+
190
+ const fence = matchOnce(FENCE_RE, line);
191
+ if (fence) {
192
+ const lang = fence[1] || undefined;
193
+ const start = i + 1;
194
+ let end = start;
195
+ while (end < to && !FENCE_RE.test(lines[end] ?? "")) end++;
196
+ const content = lines.slice(start, end).join("\n");
197
+ const closed = end < to;
198
+ out.push({
199
+ type: "code",
200
+ lang,
201
+ content,
202
+ pos: { line: i + 1, column: 1 },
203
+ endLine: closed ? end + 1 : end,
204
+ } satisfies CodeNode);
205
+ i = closed ? end + 1 : end;
206
+ continue;
207
+ }
208
+
209
+ if (
210
+ TABLE_ROW_RE.test(line) &&
211
+ i + 1 < to &&
212
+ TABLE_SEPARATOR_RE.test(lines[i + 1] ?? "")
213
+ ) {
214
+ const result = parseTable(lines, i, to);
215
+ if (result) {
216
+ result.node.endLine = result.next;
217
+ out.push(result.node);
218
+ i = result.next;
219
+ continue;
220
+ }
221
+ }
222
+
223
+ if (THEMATIC_BREAK_RE.test(line)) {
224
+ out.push({
225
+ type: "thematic_break",
226
+ pos: { line: i + 1, column: 1 },
227
+ endLine: i + 1,
228
+ } satisfies ThematicBreakNode);
229
+ i++;
230
+ continue;
231
+ }
232
+
233
+ if (QUOTE_RE.test(line)) {
234
+ const buf: string[] = [];
235
+ const startLine = i;
236
+ while (i < to) {
237
+ const m = matchOnce(QUOTE_RE, lines[i] ?? "");
238
+ if (!m) break;
239
+ buf.push(m[1] ?? "");
240
+ i++;
241
+ }
242
+ out.push({
243
+ type: "quote",
244
+ content: buf.join("\n"),
245
+ pos: { line: startLine + 1, column: 1 },
246
+ endLine: i,
247
+ } satisfies QuoteNode);
248
+ continue;
249
+ }
250
+
251
+ if (LIST_RE.test(line) || ORDERED_LIST_RE.test(line)) {
252
+ const ordered = ORDERED_LIST_RE.test(line);
253
+ const items: ListItemNode[] = [];
254
+ const re = ordered ? ORDERED_LIST_RE : LIST_RE;
255
+ const startLine = i;
256
+ while (i < to) {
257
+ const m = matchOnce(re, lines[i] ?? "");
258
+ if (!m) break;
259
+ items.push({
260
+ type: "list_item",
261
+ content: m[2] ?? "",
262
+ pos: { line: i + 1, column: 1 },
263
+ endLine: i + 1,
264
+ });
265
+ i++;
266
+ }
267
+ out.push({
268
+ type: "list",
269
+ ordered,
270
+ items,
271
+ pos: { line: startLine + 1, column: 1 },
272
+ endLine: i,
273
+ } satisfies ListNode);
274
+ continue;
275
+ }
276
+
277
+ const buf: string[] = [];
278
+ const startLine = i;
279
+ while (i < to) {
280
+ const cur = lines[i] ?? "";
281
+ const next = lines[i + 1] ?? "";
282
+ if (
283
+ cur.trim() === "" ||
284
+ HEADING_RE.test(cur) ||
285
+ FENCE_RE.test(cur) ||
286
+ DIRECTIVE_OPEN_RE.test(cur) ||
287
+ DIRECTIVE_CLOSE_RE.test(cur) ||
288
+ THEMATIC_BREAK_RE.test(cur) ||
289
+ QUOTE_RE.test(cur) ||
290
+ LIST_RE.test(cur) ||
291
+ ORDERED_LIST_RE.test(cur) ||
292
+ (TABLE_ROW_RE.test(cur) && TABLE_SEPARATOR_RE.test(next))
293
+ ) {
294
+ break;
295
+ }
296
+ buf.push(cur);
297
+ i++;
298
+ }
299
+ if (buf.length > 0) out.push(paragraph(buf.join("\n"), startLine, i));
300
+ }
301
+
302
+ return out;
303
+ }
304
+
305
+ function parseDirective(
306
+ lines: string[],
307
+ i: number,
308
+ to: number,
309
+ colons: number,
310
+ ): { node: DirectiveNode; next: number } {
311
+ const opener = matchOnce(DIRECTIVE_OPEN_RE, lines[i] ?? "")!;
312
+ const name = opener[2]!;
313
+ const attrs = parseAttrs(opener[3] ?? "");
314
+
315
+ let close = -1;
316
+ for (let j = i + 1; j < to; j++) {
317
+ const fence = matchOnce(FENCE_RE, lines[j] ?? "");
318
+ if (fence) {
319
+ j++;
320
+ while (j < to && !FENCE_RE.test(lines[j] ?? "")) j++;
321
+ continue;
322
+ }
323
+ const m = matchOnce(DIRECTIVE_CLOSE_RE, lines[j] ?? "");
324
+ if (m && m[1]!.length === colons) {
325
+ close = j;
326
+ break;
327
+ }
328
+ }
329
+
330
+ const innerEnd = close === -1 ? to : close;
331
+ const children = parseBlocks(lines, i + 1, innerEnd, colons);
332
+ const node: DirectiveNode = {
333
+ type: "directive",
334
+ name,
335
+ attrs,
336
+ children,
337
+ pos: { line: i + 1, column: 1 },
338
+ endLine: close === -1 ? to : close + 1,
339
+ };
340
+
341
+ if (typeof attrs.id === "string") node.id = attrs.id;
342
+
343
+ if (children.length === 1 && children[0]!.type === "paragraph") {
344
+ node.body = (children[0] as ParagraphNode).content;
345
+ }
346
+
347
+ return { node, next: close === -1 ? to : close + 1 };
348
+ }
349
+
350
+ const splitRow = splitPipeRow;
351
+
352
+ function parseTable(
353
+ lines: string[],
354
+ i: number,
355
+ to: number,
356
+ ): { node: TableNode; next: number } | null {
357
+ const headerLine = lines[i] ?? "";
358
+ const sepLine = lines[i + 1] ?? "";
359
+ const header = splitRow(headerLine);
360
+ const sepCells = splitRow(sepLine);
361
+ if (sepCells.length !== header.length) return null;
362
+
363
+ const align: TableAlign[] = sepCells.map((c) => {
364
+ const left = c.startsWith(":");
365
+ const right = c.endsWith(":");
366
+ if (left && right) return "center";
367
+ if (right) return "right";
368
+ if (left) return "left";
369
+ return null;
370
+ });
371
+
372
+ const rows: string[][] = [];
373
+ let j = i + 2;
374
+ while (j < to && TABLE_ROW_RE.test(lines[j] ?? "")) {
375
+ const cells = splitRow(lines[j] ?? "");
376
+ while (cells.length < header.length) cells.push("");
377
+ if (cells.length > header.length) cells.length = header.length;
378
+ rows.push(cells);
379
+ j++;
380
+ }
381
+
382
+ return {
383
+ node: {
384
+ type: "table",
385
+ header,
386
+ align,
387
+ rows,
388
+ pos: { line: i + 1, column: 1 },
389
+ },
390
+ next: j,
391
+ };
392
+ }
393
+
394
+ function parseAttrs(raw: string): Attrs {
395
+ const attrs: Attrs = {};
396
+ if (!raw) return attrs;
397
+ const inner = raw.replace(/^\{/, "").replace(/\}$/, "").trim();
398
+ if (!inner) return attrs;
399
+
400
+ const re = /([a-zA-Z_][\w-]*)(?:=("([^"]*)"|'([^']*)'|([^\s]+)))?/g;
401
+ for (const m of inner.matchAll(re)) {
402
+ const key = m[1]!;
403
+ if (m[2] === undefined) {
404
+ attrs[key] = true;
405
+ continue;
406
+ }
407
+ const quoted = m[3] ?? m[4];
408
+ const bare = m[5];
409
+ const value = quoted !== undefined ? quoted : (bare ?? "");
410
+ attrs[key] = coerce(value);
411
+ }
412
+ return attrs;
413
+ }
414
+
415
+ function coerce(v: string): AttrValue {
416
+ if (v === "true") return true;
417
+ if (v === "false") return false;
418
+ if (/^-?\d+$/.test(v)) return Number(v);
419
+ if (/^-?\d+\.\d+$/.test(v)) return Number(v);
420
+ return v;
421
+ }
422
+
423
+ function paragraph(content: string, line: number, endIdx?: number): ParagraphNode {
424
+ const lineCount = content.split("\n").length;
425
+ return {
426
+ type: "paragraph",
427
+ content: content.replace(/\n+$/, ""),
428
+ pos: { line: line + 1, column: 1 },
429
+ endLine: endIdx !== undefined ? endIdx : line + lineCount,
430
+ };
431
+ }
432
+
433
+ function computeSectionEndLines(node: Node): number {
434
+ if (node.type === "section") {
435
+ let end = node.pos?.line ?? 0;
436
+ for (const c of node.children) {
437
+ const ce = computeSectionEndLines(c);
438
+ if (ce > end) end = ce;
439
+ }
440
+ node.endLine = end;
441
+ return end;
442
+ }
443
+ if (node.type === "directive") {
444
+ for (const c of node.children) computeSectionEndLines(c);
445
+ }
446
+ return node.endLine ?? node.pos?.line ?? 0;
447
+ }
448
+
449
+ function foldSections(nodes: Node[]): Node[] {
450
+ const root: Node[] = [];
451
+ const stack: SectionNode[] = [];
452
+ const seenSlugSections = new Set<string>();
453
+
454
+ const push = (node: Node) => {
455
+ const parent = stack[stack.length - 1];
456
+ if (parent) parent.children.push(node);
457
+ else root.push(node);
458
+ };
459
+
460
+ for (const node of nodes) {
461
+ if (node.type === "section") {
462
+ const section = node as SectionNode & { _idIsExplicit?: boolean };
463
+ const isExplicit = section._idIsExplicit === true;
464
+
465
+ if (!isExplicit) {
466
+ const slug = section.id!;
467
+ if (seenSlugSections.has(slug)) {
468
+ let n = 2;
469
+ while (seenSlugSections.has(`${slug}-${n}`)) n++;
470
+ section.id = `${slug}-${n}`;
471
+ seenSlugSections.add(section.id);
472
+ } else {
473
+ seenSlugSections.add(slug);
474
+ }
475
+ }
476
+
477
+ while (stack.length > 0 && stack[stack.length - 1]!.level >= section.level) {
478
+ stack.pop();
479
+ }
480
+ push(node);
481
+ stack.push(section);
482
+ delete section._idIsExplicit;
483
+ continue;
484
+ }
485
+ push(node);
486
+ }
487
+
488
+ return root;
489
+ }
490
+
491
+ export function slugify(input: string): string {
492
+ return input
493
+ .toLowerCase()
494
+ .normalize("NFKD")
495
+ .replace(/[̀-ͯ]/g, "")
496
+ .replace(/[^a-z0-9\s-]/g, "")
497
+ .trim()
498
+ .replace(/\s+/g, "-")
499
+ .replace(/-+/g, "-")
500
+ .replace(/^-|-$/g, "");
501
+ }