@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.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/noma.mjs +8 -0
- package/dist/ast.d.ts +111 -0
- package/dist/ast.js +23 -0
- package/dist/ast.js.map +1 -0
- package/dist/book.d.ts +56 -0
- package/dist/book.js +120 -0
- package/dist/book.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +573 -0
- package/dist/cli.js.map +1 -0
- package/dist/diff.d.ts +29 -0
- package/dist/diff.js +77 -0
- package/dist/diff.js.map +1 -0
- package/dist/fmt.d.ts +1 -0
- package/dist/fmt.js +105 -0
- package/dist/fmt.js.map +1 -0
- package/dist/ids.d.ts +15 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/inline.d.ts +14 -0
- package/dist/inline.js +83 -0
- package/dist/inline.js.map +1 -0
- package/dist/loader.d.ts +12 -0
- package/dist/loader.js +59 -0
- package/dist/loader.js.map +1 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +434 -0
- package/dist/parser.js.map +1 -0
- package/dist/patch.d.ts +61 -0
- package/dist/patch.js +530 -0
- package/dist/patch.js.map +1 -0
- package/dist/renderer-html.d.ts +44 -0
- package/dist/renderer-html.js +929 -0
- package/dist/renderer-html.js.map +1 -0
- package/dist/renderer-json.d.ts +5 -0
- package/dist/renderer-json.js +4 -0
- package/dist/renderer-json.js.map +1 -0
- package/dist/renderer-llm.d.ts +29 -0
- package/dist/renderer-llm.js +275 -0
- package/dist/renderer-llm.js.map +1 -0
- package/dist/renderer-noma.d.ts +10 -0
- package/dist/renderer-noma.js +179 -0
- package/dist/renderer-noma.js.map +1 -0
- package/dist/renderer-site.d.ts +11 -0
- package/dist/renderer-site.js +175 -0
- package/dist/renderer-site.js.map +1 -0
- package/dist/validator.d.ts +24 -0
- package/dist/validator.js +699 -0
- package/dist/validator.js.map +1 -0
- package/dist/verify.d.ts +10 -0
- package/dist/verify.js +141 -0
- package/dist/verify.js.map +1 -0
- package/package.json +83 -0
- package/schemas/ast.schema.json +187 -0
- package/schemas/capability.schema.json +70 -0
- package/schemas/patch-op.schema.json +92 -0
- package/schemas/patch-transaction.schema.json +28 -0
- package/schemas/transcript.schema.json +95 -0
- package/src/ast.ts +152 -0
- package/src/book.ts +162 -0
- package/src/cli.ts +595 -0
- package/src/diff.ts +108 -0
- package/src/fmt.ts +126 -0
- package/src/ids.ts +42 -0
- package/src/index.ts +20 -0
- package/src/inline.ts +92 -0
- package/src/loader.ts +55 -0
- package/src/parser.ts +501 -0
- package/src/patch.ts +646 -0
- package/src/renderer-html.ts +1047 -0
- package/src/renderer-json.ts +9 -0
- package/src/renderer-llm.ts +320 -0
- package/src/renderer-noma.ts +220 -0
- package/src/renderer-site.ts +245 -0
- package/src/validator.ts +733 -0
- package/src/verify.ts +157 -0
- package/themes/dark.css +382 -0
- package/themes/default.css +537 -0
package/src/fmt.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source formatter — currently scoped to GitHub-style pipe tables.
|
|
3
|
+
*
|
|
4
|
+
* The parser already accepts misaligned tables; this rewriter exists so the
|
|
5
|
+
* *source* stays readable even when cell widths drift (mixed `✓`/long-prose
|
|
6
|
+
* columns are the friction point). It only touches recognised pipe-table
|
|
7
|
+
* blocks; unrelated lines are byte-identical to the input.
|
|
8
|
+
*/
|
|
9
|
+
import { splitPipeRow } from "./inline.js";
|
|
10
|
+
|
|
11
|
+
const TABLE_ROW_RE = /^\s*\|.*\|\s*$/;
|
|
12
|
+
const TABLE_SEPARATOR_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
13
|
+
const FENCE_RE = /^```(\w*)\s*$/;
|
|
14
|
+
|
|
15
|
+
interface Alignment {
|
|
16
|
+
left: boolean;
|
|
17
|
+
right: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function visibleWidth(s: string): number {
|
|
21
|
+
// js String.length — good enough for ASCII; not perfect for full-width
|
|
22
|
+
// CJK or emoji clusters, but predictable and dependency-free.
|
|
23
|
+
return Array.from(s).length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const splitRow = splitPipeRow;
|
|
27
|
+
|
|
28
|
+
function parseSeparator(line: string): Alignment[] | null {
|
|
29
|
+
if (!TABLE_SEPARATOR_RE.test(line)) return null;
|
|
30
|
+
return splitRow(line).map((c) => ({
|
|
31
|
+
left: c.startsWith(":"),
|
|
32
|
+
right: c.endsWith(":"),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildSeparator(widths: number[], aligns: Alignment[]): string {
|
|
37
|
+
const cells = widths.map((w, i) => {
|
|
38
|
+
const a = aligns[i] ?? { left: false, right: false };
|
|
39
|
+
const target = Math.max(3, w);
|
|
40
|
+
if (a.left && a.right) return `:${"-".repeat(target - 2)}:`;
|
|
41
|
+
if (a.right) return `${"-".repeat(target - 1)}:`;
|
|
42
|
+
if (a.left) return `:${"-".repeat(target - 1)}`;
|
|
43
|
+
return "-".repeat(target);
|
|
44
|
+
});
|
|
45
|
+
return `| ${cells.join(" | ")} |`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pad(cell: string, width: number, align: Alignment): string {
|
|
49
|
+
const w = visibleWidth(cell);
|
|
50
|
+
const slack = Math.max(0, width - w);
|
|
51
|
+
if (align.left && align.right) {
|
|
52
|
+
const left = Math.floor(slack / 2);
|
|
53
|
+
const right = slack - left;
|
|
54
|
+
return " ".repeat(left) + cell + " ".repeat(right);
|
|
55
|
+
}
|
|
56
|
+
if (align.right) return " ".repeat(slack) + cell;
|
|
57
|
+
return cell + " ".repeat(slack);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildRow(
|
|
61
|
+
cells: string[],
|
|
62
|
+
widths: number[],
|
|
63
|
+
aligns: Alignment[],
|
|
64
|
+
): string {
|
|
65
|
+
const padded = cells.map((c, i) =>
|
|
66
|
+
pad(c, widths[i] ?? visibleWidth(c), aligns[i] ?? { left: false, right: false }),
|
|
67
|
+
);
|
|
68
|
+
return `| ${padded.join(" | ")} |`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatSource(source: string): string {
|
|
72
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
73
|
+
const out: string[] = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
let inFence = false;
|
|
76
|
+
|
|
77
|
+
while (i < lines.length) {
|
|
78
|
+
const line = lines[i] ?? "";
|
|
79
|
+
if (FENCE_RE.test(line)) {
|
|
80
|
+
inFence = !inFence;
|
|
81
|
+
out.push(line);
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (inFence) {
|
|
86
|
+
out.push(line);
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
TABLE_ROW_RE.test(line) &&
|
|
92
|
+
i + 1 < lines.length &&
|
|
93
|
+
TABLE_SEPARATOR_RE.test(lines[i + 1] ?? "")
|
|
94
|
+
) {
|
|
95
|
+
const header = splitRow(line);
|
|
96
|
+
const sepAligns = parseSeparator(lines[i + 1] ?? "") ?? [];
|
|
97
|
+
const rows: string[][] = [];
|
|
98
|
+
let j = i + 2;
|
|
99
|
+
while (j < lines.length && TABLE_ROW_RE.test(lines[j] ?? "")) {
|
|
100
|
+
const cells = splitRow(lines[j] ?? "");
|
|
101
|
+
while (cells.length < header.length) cells.push("");
|
|
102
|
+
if (cells.length > header.length) cells.length = header.length;
|
|
103
|
+
rows.push(cells);
|
|
104
|
+
j++;
|
|
105
|
+
}
|
|
106
|
+
const widths = header.map((h, idx) =>
|
|
107
|
+
Math.max(
|
|
108
|
+
visibleWidth(h),
|
|
109
|
+
...rows.map((r) => visibleWidth(r[idx] ?? "")),
|
|
110
|
+
3,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
const aligns: Alignment[] = header.map(
|
|
114
|
+
(_, idx) => sepAligns[idx] ?? { left: false, right: false },
|
|
115
|
+
);
|
|
116
|
+
out.push(buildRow(header, widths, aligns));
|
|
117
|
+
out.push(buildSeparator(widths, aligns));
|
|
118
|
+
for (const r of rows) out.push(buildRow(r, widths, aligns));
|
|
119
|
+
i = j;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
out.push(line);
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
return out.join("\n");
|
|
126
|
+
}
|
package/src/ids.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DocumentNode, Node } from "./ast.js";
|
|
2
|
+
import { walk } from "./ast.js";
|
|
3
|
+
|
|
4
|
+
export interface IdRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
type: Node["type"];
|
|
7
|
+
name?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
aliases?: string[];
|
|
10
|
+
line?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IdRegistry {
|
|
14
|
+
ids: string[];
|
|
15
|
+
aliases: Record<string, string>;
|
|
16
|
+
records: IdRecord[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function collectIdRegistry(doc: DocumentNode): IdRegistry {
|
|
20
|
+
const ids: string[] = [];
|
|
21
|
+
const aliases: Record<string, string> = {};
|
|
22
|
+
const records: IdRecord[] = [];
|
|
23
|
+
|
|
24
|
+
for (const node of walk(doc)) {
|
|
25
|
+
if (!node.id) continue;
|
|
26
|
+
ids.push(node.id);
|
|
27
|
+
const record: IdRecord = {
|
|
28
|
+
id: node.id,
|
|
29
|
+
type: node.type,
|
|
30
|
+
...(node.aliases && node.aliases.length > 0 ? { aliases: node.aliases } : {}),
|
|
31
|
+
...(node.pos?.line ? { line: node.pos.line } : {}),
|
|
32
|
+
};
|
|
33
|
+
if (node.type === "directive") record.name = node.name;
|
|
34
|
+
if (node.type === "section") record.title = node.title;
|
|
35
|
+
records.push(record);
|
|
36
|
+
for (const alias of node.aliases ?? []) {
|
|
37
|
+
aliases[alias] = node.id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { ids, aliases, records };
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from "./ast.js";
|
|
2
|
+
export { parse, slugify } from "./parser.js";
|
|
3
|
+
export type { ParseOptions } from "./parser.js";
|
|
4
|
+
export { renderHtml } from "./renderer-html.js";
|
|
5
|
+
export type { HtmlRenderOptions } from "./renderer-html.js";
|
|
6
|
+
export { renderLlm } from "./renderer-llm.js";
|
|
7
|
+
export { renderJson } from "./renderer-json.js";
|
|
8
|
+
export type { JsonRenderOptions } from "./renderer-json.js";
|
|
9
|
+
export { renderNoma } from "./renderer-noma.js";
|
|
10
|
+
export type { NomaRenderOptions } from "./renderer-noma.js";
|
|
11
|
+
export { patch, patchAll, patchSource, findById, PatchError } from "./patch.js";
|
|
12
|
+
export type { PatchOp } from "./patch.js";
|
|
13
|
+
export { loadBook, isBookManifestPath, listChapters } from "./book.js";
|
|
14
|
+
export type { BookManifest } from "./book.js";
|
|
15
|
+
export { validate, formatDiagnostics } from "./validator.js";
|
|
16
|
+
export type { ValidateOptions } from "./validator.js";
|
|
17
|
+
export { diffDocs } from "./diff.js";
|
|
18
|
+
export type { DiffOptions } from "./diff.js";
|
|
19
|
+
export { collectIdRegistry } from "./ids.js";
|
|
20
|
+
export type { IdRecord, IdRegistry } from "./ids.js";
|
package/src/inline.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny inline markup parser. Operates on plain text and emits HTML.
|
|
3
|
+
* Order matters: handle code spans first so emphasis inside `code` stays raw.
|
|
4
|
+
*/
|
|
5
|
+
export function inlineToHtml(src: string): string {
|
|
6
|
+
let text = escapeHtml(src);
|
|
7
|
+
|
|
8
|
+
// Code spans go first AND get placeholdered so subsequent inline rules
|
|
9
|
+
// (emphasis, links, wikilinks) don't reach into their content. Without the
|
|
10
|
+
// placeholder, a sequence like `x_y` ... `a_b` lets the underscore regex
|
|
11
|
+
// greedily span across the rendered <code> tags.
|
|
12
|
+
const codeSpans: string[] = [];
|
|
13
|
+
const PH_OPEN = String.fromCharCode(2);
|
|
14
|
+
const PH_CLOSE = String.fromCharCode(3);
|
|
15
|
+
text = text.replace(/`([^`]+)`/g, (_m, body) => {
|
|
16
|
+
const i = codeSpans.push("<code>" + body + "</code>") - 1;
|
|
17
|
+
return PH_OPEN + i + PH_CLOSE;
|
|
18
|
+
});
|
|
19
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
20
|
+
text = text.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
21
|
+
text = text.replace(/\b_([^_]+)_\b/g, "<em>$1</em>");
|
|
22
|
+
text = text.replace(
|
|
23
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
24
|
+
(_m, label, href) => `<a href="${escapeAttr(href)}">${label}</a>`,
|
|
25
|
+
);
|
|
26
|
+
text = text.replace(/\[\[([a-zA-Z_][\w\-./:]*)\]\]/g, (_m, id) =>
|
|
27
|
+
`<a class="noma-ref" href="#${escapeAttr(id)}">${id}</a>`,
|
|
28
|
+
);
|
|
29
|
+
// CommonMark: a single newline inside a paragraph is a soft line break
|
|
30
|
+
// (renders as a space); two trailing spaces or a trailing backslash before
|
|
31
|
+
// the newline make it a hard break (`<br/>`).
|
|
32
|
+
text = text.replace(/(?: +|\\)\n/g, "<br />");
|
|
33
|
+
text = text.replace(/\n/g, " ");
|
|
34
|
+
// Restore code-span placeholders.
|
|
35
|
+
const restoreRe = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
|
36
|
+
text = text.replace(restoreRe, (_m, i) => codeSpans[Number(i)] ?? "");
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function inlineToPlain(src: string): string {
|
|
41
|
+
return src
|
|
42
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
43
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
44
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
45
|
+
.replace(/\b_([^_]+)_\b/g, "$1")
|
|
46
|
+
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "$1 ($2)")
|
|
47
|
+
.replace(/\[\[([a-zA-Z_][\w\-./:]*)\]\]/g, "$1");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function escapeHtml(s: string): string {
|
|
51
|
+
return s
|
|
52
|
+
.replace(/&/g, "&")
|
|
53
|
+
.replace(/</g, "<")
|
|
54
|
+
.replace(/>/g, ">");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function escapeAttr(s: string): string {
|
|
58
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Split a pipe-table row respecting `code spans` and `\|` escapes — pipes
|
|
63
|
+
* inside backticks or escaped with a backslash are kept verbatim inside the
|
|
64
|
+
* cell. Used by the parser and by `noma fmt` so both agree on cell counts.
|
|
65
|
+
*/
|
|
66
|
+
export function splitPipeRow(line: string): string[] {
|
|
67
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
68
|
+
const cells: string[] = [];
|
|
69
|
+
let buf = "";
|
|
70
|
+
let inBacktick = false;
|
|
71
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
72
|
+
const ch = trimmed[i]!;
|
|
73
|
+
if (ch === "\\" && trimmed[i + 1] === "|") {
|
|
74
|
+
buf += "\\|";
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === "`") {
|
|
79
|
+
inBacktick = !inBacktick;
|
|
80
|
+
buf += ch;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === "|" && !inBacktick) {
|
|
84
|
+
cells.push(buf.trim());
|
|
85
|
+
buf = "";
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
buf += ch;
|
|
89
|
+
}
|
|
90
|
+
cells.push(buf.trim());
|
|
91
|
+
return cells;
|
|
92
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import type { DocumentNode } from "./ast.js";
|
|
4
|
+
import { walk } from "./ast.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-place: for every `::dataset{src="..."}` block without an inline body,
|
|
8
|
+
* read the referenced file and stuff its contents into `body`. Sets the
|
|
9
|
+
* `format` attribute (csv/tsv/json/yaml) when not already provided.
|
|
10
|
+
*
|
|
11
|
+
* Path resolution: absolute paths used as-is, relative paths resolved
|
|
12
|
+
* against `baseDir` (or the document's own filename's directory).
|
|
13
|
+
*
|
|
14
|
+
* Renderers stay pure — they read `body` and `format`, never the filesystem.
|
|
15
|
+
*/
|
|
16
|
+
export function inlineDatasetSources(
|
|
17
|
+
doc: DocumentNode,
|
|
18
|
+
baseDir?: string,
|
|
19
|
+
): DocumentNode {
|
|
20
|
+
const dir =
|
|
21
|
+
baseDir ??
|
|
22
|
+
(typeof doc.meta.filename === "string"
|
|
23
|
+
? dirname(doc.meta.filename)
|
|
24
|
+
: process.cwd());
|
|
25
|
+
for (const node of walk(doc)) {
|
|
26
|
+
if (node.type !== "directive" || node.name !== "dataset") continue;
|
|
27
|
+
const src = node.attrs.src;
|
|
28
|
+
if (typeof src !== "string" || !src.trim()) continue;
|
|
29
|
+
if (node.body && node.body.trim()) continue;
|
|
30
|
+
const path = isAbsolute(src) ? src : resolve(dir, src);
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(path, "utf8");
|
|
33
|
+
node.body = content;
|
|
34
|
+
if (!node.attrs.format) {
|
|
35
|
+
node.attrs.format = inferFormat(src, content);
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
node.body = `# error loading ${src}: ${(e as Error).message}`;
|
|
39
|
+
node.attrs.format = "error";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return doc;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inferFormat(src: string, content: string): string {
|
|
46
|
+
const ext = src.split(".").pop()?.toLowerCase();
|
|
47
|
+
if (ext === "csv") return "csv";
|
|
48
|
+
if (ext === "tsv") return "tsv";
|
|
49
|
+
if (ext === "json") return "json";
|
|
50
|
+
if (ext === "yaml" || ext === "yml") return "yaml";
|
|
51
|
+
const head = content.trimStart()[0];
|
|
52
|
+
if (head === "{" || head === "[") return "json";
|
|
53
|
+
if (/^[^\n]*,[^\n]*\n/.test(content)) return "csv";
|
|
54
|
+
return "yaml";
|
|
55
|
+
}
|