@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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import type { DocumentNode, Node, SectionNode } from "./ast.js";
|
|
4
|
+
import { walk } from "./ast.js";
|
|
5
|
+
import { escapeAttr, escapeHtml, inlineToHtml } from "./inline.js";
|
|
6
|
+
import { renderHtml } from "./renderer-html.js";
|
|
7
|
+
import type { LoadedChapter } from "./book.js";
|
|
8
|
+
|
|
9
|
+
export interface SiteRenderOptions {
|
|
10
|
+
themeCss?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
allowEscapeHatches?: boolean;
|
|
13
|
+
math?: "katex" | "none";
|
|
14
|
+
externalAssets?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IdLocation {
|
|
18
|
+
chapterSlug: string;
|
|
19
|
+
/** Anchor name to link to (block ID or alias). */
|
|
20
|
+
anchor: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderSite(
|
|
24
|
+
manifest: Record<string, unknown>,
|
|
25
|
+
chapters: LoadedChapter[],
|
|
26
|
+
outDir: string,
|
|
27
|
+
options: SiteRenderOptions = {},
|
|
28
|
+
): void {
|
|
29
|
+
const absOut = resolve(outDir);
|
|
30
|
+
mkdirSync(absOut, { recursive: true });
|
|
31
|
+
|
|
32
|
+
const themeCss = options.themeCss ?? "";
|
|
33
|
+
const hasTheme = themeCss.length > 0;
|
|
34
|
+
if (hasTheme) {
|
|
35
|
+
const assetsDir = join(absOut, "_assets");
|
|
36
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
37
|
+
writeFileSync(join(assetsDir, "theme.css"), themeCss, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const idMap = buildIdMap(chapters);
|
|
41
|
+
const bookTitle =
|
|
42
|
+
options.title ||
|
|
43
|
+
(typeof manifest.title === "string" ? manifest.title : undefined) ||
|
|
44
|
+
"Noma Book";
|
|
45
|
+
|
|
46
|
+
for (const ch of chapters) {
|
|
47
|
+
const prefix = pagePrefix(ch.slug);
|
|
48
|
+
const html = renderHtml(ch.doc, {
|
|
49
|
+
standalone: true,
|
|
50
|
+
...(hasTheme ? { stylesheetHref: `${prefix}${THEME_HREF}` } : { themeCss: "" }),
|
|
51
|
+
title: chapterTitle(ch) || bookTitle,
|
|
52
|
+
allowEscapeHatches: options.allowEscapeHatches !== false,
|
|
53
|
+
externalAssets: options.externalAssets !== false,
|
|
54
|
+
...(options.math ? { math: options.math } : {}),
|
|
55
|
+
});
|
|
56
|
+
const rewritten = rewriteWikilinks(html, ch.slug, idMap, prefix);
|
|
57
|
+
const withNav = injectNav(rewritten, chapters, ch.slug, bookTitle, prefix);
|
|
58
|
+
const target = join(absOut, `${ch.slug}.html`);
|
|
59
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
60
|
+
writeFileSync(target, withNav, "utf8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const indexHtml = renderIndex(bookTitle, manifest, chapters, hasTheme, idMap);
|
|
64
|
+
writeFileSync(join(absOut, "index.html"), indexHtml, "utf8");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pagePrefix(slug: string | null): string {
|
|
68
|
+
if (!slug) return "";
|
|
69
|
+
const depth = (slug.match(/\//g) ?? []).length;
|
|
70
|
+
return "../".repeat(depth);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function chapterTitle(ch: LoadedChapter): string | undefined {
|
|
74
|
+
const root = ch.doc.children.find(
|
|
75
|
+
(n): n is SectionNode => n.type === "section" && n.level === 1,
|
|
76
|
+
);
|
|
77
|
+
return root?.title;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildIdMap(chapters: LoadedChapter[]): Map<string, IdLocation> {
|
|
81
|
+
const map = new Map<string, IdLocation>();
|
|
82
|
+
for (const ch of chapters) {
|
|
83
|
+
for (const node of walk(ch.doc)) {
|
|
84
|
+
if (node.id) {
|
|
85
|
+
map.set(node.id, { chapterSlug: ch.slug, anchor: node.id });
|
|
86
|
+
}
|
|
87
|
+
if (node.aliases) {
|
|
88
|
+
for (const a of node.aliases) {
|
|
89
|
+
if (!map.has(a)) map.set(a, { chapterSlug: ch.slug, anchor: a });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return map;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const THEME_HREF = "_assets/theme.css";
|
|
98
|
+
|
|
99
|
+
const WIKILINK_HREF_RE =
|
|
100
|
+
/<a\s+class="noma-ref"\s+href="#([^"]+)">([^<]+)<\/a>/g;
|
|
101
|
+
|
|
102
|
+
function rewriteWikilinks(
|
|
103
|
+
html: string,
|
|
104
|
+
currentSlug: string,
|
|
105
|
+
idMap: Map<string, IdLocation>,
|
|
106
|
+
prefix: string,
|
|
107
|
+
): string {
|
|
108
|
+
return html.replace(WIKILINK_HREF_RE, (match, id: string, label: string) => {
|
|
109
|
+
const loc = idMap.get(id);
|
|
110
|
+
if (!loc || loc.chapterSlug === currentSlug) return match;
|
|
111
|
+
return `<a class="noma-ref noma-xchapter" href="${escapeAttr(prefix + loc.chapterSlug)}.html#${escapeAttr(loc.anchor)}">${label}</a>`;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function injectNav(
|
|
116
|
+
html: string,
|
|
117
|
+
chapters: LoadedChapter[],
|
|
118
|
+
currentSlug: string,
|
|
119
|
+
bookTitle: string,
|
|
120
|
+
prefix: string,
|
|
121
|
+
): string {
|
|
122
|
+
const nav = buildNav(chapters, currentSlug, bookTitle, prefix);
|
|
123
|
+
return html.replace(
|
|
124
|
+
/<main class="noma-doc">/,
|
|
125
|
+
`${nav}\n<main class="noma-doc">`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildNav(
|
|
130
|
+
chapters: LoadedChapter[],
|
|
131
|
+
currentSlug: string | null,
|
|
132
|
+
bookTitle: string,
|
|
133
|
+
prefix: string,
|
|
134
|
+
): string {
|
|
135
|
+
const items = chapters
|
|
136
|
+
.map((c) => {
|
|
137
|
+
const isCurrent = c.slug === currentSlug;
|
|
138
|
+
const label = chapterTitle(c) ?? c.slug;
|
|
139
|
+
return isCurrent
|
|
140
|
+
? `<li class="noma-nav-current"><span>${escapeHtml(label)}</span></li>`
|
|
141
|
+
: `<li><a href="${escapeAttr(prefix + c.slug)}.html">${escapeHtml(label)}</a></li>`;
|
|
142
|
+
})
|
|
143
|
+
.join("");
|
|
144
|
+
const homeMarkup =
|
|
145
|
+
currentSlug === null
|
|
146
|
+
? `<span class="noma-site-home noma-nav-current">${escapeHtml(bookTitle)}</span>`
|
|
147
|
+
: `<a class="noma-site-home" href="${escapeAttr(prefix)}index.html">${escapeHtml(bookTitle)}</a>`;
|
|
148
|
+
return `<nav class="noma-site-nav" aria-label="Chapters">
|
|
149
|
+
${homeMarkup}
|
|
150
|
+
<ol>${items}</ol>
|
|
151
|
+
</nav>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderIndex(
|
|
155
|
+
bookTitle: string,
|
|
156
|
+
manifest: Record<string, unknown>,
|
|
157
|
+
chapters: LoadedChapter[],
|
|
158
|
+
hasTheme: boolean,
|
|
159
|
+
idMap: Map<string, IdLocation>,
|
|
160
|
+
): string {
|
|
161
|
+
const author =
|
|
162
|
+
typeof manifest.author === "string" ? manifest.author : undefined;
|
|
163
|
+
const nav = buildNav(chapters, null, bookTitle, "");
|
|
164
|
+
const items = chapters
|
|
165
|
+
.map((c) => {
|
|
166
|
+
const label = chapterTitle(c) ?? c.slug;
|
|
167
|
+
const raw = chapterSummaryRaw(c.doc);
|
|
168
|
+
const descHtml = raw ? renderCardDescription(raw, idMap) : "";
|
|
169
|
+
return `<li>
|
|
170
|
+
<a class="noma-site-chapter" href="${escapeAttr(c.slug)}.html">
|
|
171
|
+
<span class="noma-site-chapter-title">${escapeHtml(label)}</span>
|
|
172
|
+
${descHtml ? `<span class="noma-site-chapter-summary">${descHtml}</span>` : ""}
|
|
173
|
+
</a>
|
|
174
|
+
</li>`;
|
|
175
|
+
})
|
|
176
|
+
.join("\n");
|
|
177
|
+
|
|
178
|
+
const themeLink = hasTheme
|
|
179
|
+
? `<link rel="stylesheet" href="${THEME_HREF}" />`
|
|
180
|
+
: "";
|
|
181
|
+
return `<!doctype html>
|
|
182
|
+
<html lang="en">
|
|
183
|
+
<head>
|
|
184
|
+
<meta charset="utf-8" />
|
|
185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
186
|
+
<meta name="generator" content="noma" />
|
|
187
|
+
<title>${escapeHtml(bookTitle)}</title>
|
|
188
|
+
${themeLink}
|
|
189
|
+
</head>
|
|
190
|
+
<body>
|
|
191
|
+
${nav}
|
|
192
|
+
<main class="noma-doc noma-site-index">
|
|
193
|
+
<header class="noma-site-header">
|
|
194
|
+
<h1>${escapeHtml(bookTitle)}</h1>
|
|
195
|
+
${author ? `<p class="noma-site-author">${escapeHtml(author)}</p>` : ""}
|
|
196
|
+
</header>
|
|
197
|
+
<ol class="noma-site-toc">
|
|
198
|
+
${items}
|
|
199
|
+
</ol>
|
|
200
|
+
</main>
|
|
201
|
+
</body>
|
|
202
|
+
</html>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function chapterSummaryRaw(doc: DocumentNode): string | undefined {
|
|
206
|
+
for (const node of walk(doc)) {
|
|
207
|
+
if (node.type === "directive" && (node.name === "summary" || node.name === "abstract")) {
|
|
208
|
+
const body = (node.body ?? "").trim();
|
|
209
|
+
if (body) return body;
|
|
210
|
+
}
|
|
211
|
+
if (node.type === "paragraph") {
|
|
212
|
+
const body = node.content.trim();
|
|
213
|
+
if (body) return body;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const SENTENCE_END_RE = /([.!?])(?=\s|$)/;
|
|
220
|
+
|
|
221
|
+
function firstSentence(s: string): string {
|
|
222
|
+
const collapsed = s.replace(/\s+/g, " ").trim();
|
|
223
|
+
const m = collapsed.match(SENTENCE_END_RE);
|
|
224
|
+
if (m && m.index !== undefined) {
|
|
225
|
+
return collapsed.slice(0, m.index + 1);
|
|
226
|
+
}
|
|
227
|
+
return collapsed.length > 200 ? collapsed.slice(0, 197) + "…" : collapsed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const CARD_REF_RE = /<a class="noma-ref" href="#([^"]+)">([^<]+)<\/a>/g;
|
|
231
|
+
|
|
232
|
+
function renderCardDescription(
|
|
233
|
+
raw: string,
|
|
234
|
+
idMap: Map<string, IdLocation>,
|
|
235
|
+
): string {
|
|
236
|
+
const sentence = firstSentence(raw);
|
|
237
|
+
const html = inlineToHtml(sentence);
|
|
238
|
+
return html.replace(CARD_REF_RE, (_m, id: string, label: string) => {
|
|
239
|
+
const loc = idMap.get(id);
|
|
240
|
+
if (!loc) return label;
|
|
241
|
+
return `<a class="noma-ref noma-xchapter" href="${escapeAttr(loc.chapterSlug)}.html#${escapeAttr(loc.anchor)}">${label}</a>`;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type { LoadedChapter, Node };
|