mosage 0.2.0 → 0.8.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/README.md +16 -15
- package/bin.js +2 -0
- package/dist/build-C7NW_3Pk.js +14 -0
- package/dist/check-CP4873Wx.js +41 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +228 -0
- package/dist/config-DPm1BBAb.js +2619 -0
- package/dist/config-TlTe7Ona.d.ts +24 -0
- package/dist/context-BqsdSrAQ.js +1084 -0
- package/dist/dev-Biz42qlu.js +17 -0
- package/dist/diagram-xlVDekYk.js +763 -0
- package/dist/export-Bi6nuxjT.js +31 -0
- package/dist/import-D2jNB07F.js +25 -0
- package/dist/index.d.ts +455 -0
- package/dist/index.js +693 -0
- package/dist/{cli.js → init-Bbtj2pxF.js} +34 -68
- package/dist/preview-CLm51aRt.js +19 -0
- package/dist/sdk-DjpX6mCv.js +51 -0
- package/dist/vite/index.d.ts +25 -0
- package/dist/vite/index.js +2 -0
- package/env.d.ts +83 -0
- package/package.json +59 -12
- package/{template/.agents/skills → skills}/create-doc/SKILL.md +1 -1
- package/{template/.agents/skills → skills}/create-theme/SKILL.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/SKILL.md +8 -9
- package/{template/.agents/skills → skills}/doc-authoring/references/assets.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/references/design-system.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/references/long-form.md +4 -4
- package/{template/.agents/skills → skills}/doc-authoring/references/pagination.md +1 -1
- package/src/app/app.tsx +42 -0
- package/src/app/components/data-table.tsx +196 -0
- package/src/app/components/design-panel/design-panel.tsx +318 -0
- package/src/app/components/design-panel/design-provider.tsx +121 -0
- package/src/app/components/design-panel/use-design.ts +85 -0
- package/src/app/components/diagram.tsx +76 -0
- package/src/app/components/doc-assets.tsx +129 -0
- package/src/app/components/doc-search.tsx +248 -0
- package/src/app/components/doc-sidebar.tsx +162 -0
- package/src/app/components/flow-page.tsx +93 -0
- package/src/app/components/footnote.tsx +204 -0
- package/src/app/components/image-placeholder.tsx +50 -0
- package/src/app/components/inspector/inspector.tsx +518 -0
- package/src/app/components/numbering.tsx +224 -0
- package/src/app/components/page-frame.tsx +70 -0
- package/src/app/components/sidebar/folder-item.tsx +212 -0
- package/src/app/components/sidebar/icon-picker.tsx +99 -0
- package/src/app/components/sidebar/sidebar.tsx +252 -0
- package/src/app/components/table-of-contents.tsx +93 -0
- package/src/app/components/theme-toggle.tsx +50 -0
- package/src/app/components/themes/markdown.tsx +249 -0
- package/src/app/components/themes/theme-preview.tsx +74 -0
- package/src/app/components/ui/menu.tsx +143 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/agent-bridge.ts +140 -0
- package/src/app/lib/assets.ts +151 -0
- package/src/app/lib/design-presets.ts +109 -0
- package/src/app/lib/design.ts +88 -0
- package/src/app/lib/diagnostics.ts +282 -0
- package/src/app/lib/doc-preview.tsx +29 -0
- package/src/app/lib/docs.ts +26 -0
- package/src/app/lib/docx/extract.ts +1623 -0
- package/src/app/lib/docx/fonts.test.ts +136 -0
- package/src/app/lib/docx/fonts.ts +166 -0
- package/src/app/lib/docx/media.ts +102 -0
- package/src/app/lib/docx/model.ts +206 -0
- package/src/app/lib/docx/paragraph.test.ts +92 -0
- package/src/app/lib/docx/paragraph.ts +107 -0
- package/src/app/lib/docx/props.ts +187 -0
- package/src/app/lib/docx/styles.ts +306 -0
- package/src/app/lib/docx/units.ts +35 -0
- package/src/app/lib/docx/write.test.ts +507 -0
- package/src/app/lib/docx/write.ts +581 -0
- package/src/app/lib/docx/xml.ts +39 -0
- package/src/app/lib/export-docx.ts +289 -0
- package/src/app/lib/export-dom.ts +318 -0
- package/src/app/lib/export-html.ts +156 -0
- package/src/app/lib/export-image.ts +70 -0
- package/src/app/lib/export-pdf.ts +165 -0
- package/src/app/lib/flow-measure.test.ts +31 -0
- package/src/app/lib/flow-measure.ts +183 -0
- package/src/app/lib/flow.test.ts +110 -0
- package/src/app/lib/flow.ts +136 -0
- package/src/app/lib/folders.ts +192 -0
- package/src/app/lib/footnotes.test.tsx +102 -0
- package/src/app/lib/footnotes.ts +94 -0
- package/src/app/lib/inspector/fiber.ts +99 -0
- package/src/app/lib/labels.test.ts +18 -0
- package/src/app/lib/labels.ts +181 -0
- package/src/app/lib/outline.ts +118 -0
- package/src/app/lib/page-context.tsx +43 -0
- package/src/app/lib/page-range.test.ts +95 -0
- package/src/app/lib/page-range.ts +90 -0
- package/src/app/lib/print-ready.ts +69 -0
- package/src/app/lib/rasterize.ts +173 -0
- package/src/app/lib/scan.ts +26 -0
- package/src/app/lib/sdk.test.ts +32 -0
- package/src/app/lib/sdk.ts +115 -0
- package/src/app/lib/themes.ts +31 -0
- package/src/app/lib/use-doc-module.ts +53 -0
- package/src/app/lib/use-doc-pages.ts +147 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/lib/view-mode.test.ts +91 -0
- package/src/app/lib/view-mode.ts +104 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +257 -0
- package/src/app/routes/doc.tsx +877 -0
- package/src/app/routes/home-shell.tsx +203 -0
- package/src/app/routes/home.tsx +269 -0
- package/src/app/routes/themes.tsx +121 -0
- package/src/app/styles.css +97 -0
- package/src/app/virtual.d.ts +30 -0
- package/template/AGENTS.md +1 -1
- package/template/README.md +24 -52
- package/template/docs/getting-started/index.tsx +2 -2
- package/template/mosage.config.ts +1 -1
- package/template/package.json +1 -1
- package/template/tsconfig.json +1 -1
- package/README.zh-TW.md +0 -28
- /package/{template/.agents/skills → skills}/apply-comments/SKILL.md +0 -0
- /package/{template/.agents/skills → skills}/current-doc/SKILL.md +0 -0
- /package/{template/.agents/skills → skills}/doc-authoring/references/tables-and-charts.md +0 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
import { a as PAGE_SIZE_NAMES, o as isOrientation, r as ORIENTATIONS, s as isPageSizeName } from "./sdk-DjpX6mCv.js";
|
|
2
|
+
import { r as readCoreVersion } from "./cli/bin.js";
|
|
3
|
+
import { a as resolveEntry, c as DOC_ID_RE, i as listDocIds, l as loadUserConfig, n as OpsError, o as makeContext, r as createDocument, s as validateAssetName, t as createViteConfig } from "./config-DPm1BBAb.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import "fast-glob";
|
|
8
|
+
import { createServer, mergeConfig } from "vite";
|
|
9
|
+
import { Buffer } from "node:buffer";
|
|
10
|
+
//#region src/import/markdown.ts
|
|
11
|
+
const FENCE_RE = /^(```|~~~)\s*([^\s`]*)\s*$/;
|
|
12
|
+
const ATX_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
13
|
+
const HR_RE = /^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
14
|
+
const QUOTE_RE = /^ {0,3}> ?(.*)$/;
|
|
15
|
+
const UL_RE = /^(\s*)([-*+])\s+(.*)$/;
|
|
16
|
+
const OL_RE = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
|
|
17
|
+
const SETEXT_RE = /^ {0,3}(=+|-+)\s*$/;
|
|
18
|
+
const TABLE_DELIM_RE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
|
|
19
|
+
function splitLines(source) {
|
|
20
|
+
return source.replace(/^/, "").replace(/\r\n?/g, "\n").split("\n");
|
|
21
|
+
}
|
|
22
|
+
function parseFrontmatter(lines) {
|
|
23
|
+
if (lines[0]?.trim() !== "---") return {
|
|
24
|
+
frontmatter: {},
|
|
25
|
+
rest: lines
|
|
26
|
+
};
|
|
27
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
28
|
+
if (end === -1) return {
|
|
29
|
+
frontmatter: {},
|
|
30
|
+
rest: lines
|
|
31
|
+
};
|
|
32
|
+
const frontmatter = {};
|
|
33
|
+
for (const line of lines.slice(1, end)) {
|
|
34
|
+
const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
35
|
+
if (!match) continue;
|
|
36
|
+
frontmatter[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
frontmatter,
|
|
40
|
+
rest: lines.slice(end + 1)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function splitRow(line) {
|
|
44
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
45
|
+
const cells = [];
|
|
46
|
+
let current = "";
|
|
47
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
48
|
+
const char = trimmed[i];
|
|
49
|
+
if (char === "\\" && trimmed[i + 1] === "|") {
|
|
50
|
+
current += "|";
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === "|") {
|
|
55
|
+
cells.push(current.trim());
|
|
56
|
+
current = "";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
current += char;
|
|
60
|
+
}
|
|
61
|
+
cells.push(current.trim());
|
|
62
|
+
return cells;
|
|
63
|
+
}
|
|
64
|
+
function parseAlign(line) {
|
|
65
|
+
return splitRow(line).map((cell) => {
|
|
66
|
+
const left = cell.startsWith(":");
|
|
67
|
+
const right = cell.endsWith(":");
|
|
68
|
+
if (left && right) return "center";
|
|
69
|
+
if (right) return "right";
|
|
70
|
+
if (left) return "left";
|
|
71
|
+
return null;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function indentOf(line) {
|
|
75
|
+
return line.length - line.trimStart().length;
|
|
76
|
+
}
|
|
77
|
+
function isBlockStart(line) {
|
|
78
|
+
return ATX_RE.test(line) || HR_RE.test(line) || QUOTE_RE.test(line) || UL_RE.test(line) || OL_RE.test(line) || FENCE_RE.test(line);
|
|
79
|
+
}
|
|
80
|
+
function parseMarkdown(source) {
|
|
81
|
+
const { frontmatter, rest } = parseFrontmatter(splitLines(source));
|
|
82
|
+
return {
|
|
83
|
+
frontmatter,
|
|
84
|
+
blocks: parseBlocks(rest)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function parseBlocks(lines) {
|
|
88
|
+
const blocks = [];
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < lines.length) {
|
|
91
|
+
const line = lines[i];
|
|
92
|
+
if (line.trim() === "") {
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const fence = line.match(FENCE_RE);
|
|
97
|
+
if (fence) {
|
|
98
|
+
const marker = fence[1];
|
|
99
|
+
const value = [];
|
|
100
|
+
i++;
|
|
101
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
|
|
102
|
+
value.push(lines[i]);
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
i++;
|
|
106
|
+
blocks.push({
|
|
107
|
+
type: "code",
|
|
108
|
+
lang: fence[2] || null,
|
|
109
|
+
value: value.join("\n")
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const atx = line.match(ATX_RE);
|
|
114
|
+
if (atx) {
|
|
115
|
+
blocks.push({
|
|
116
|
+
type: "heading",
|
|
117
|
+
level: atx[1].length,
|
|
118
|
+
children: parseInline(atx[2])
|
|
119
|
+
});
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (HR_RE.test(line)) {
|
|
124
|
+
blocks.push({ type: "hr" });
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (QUOTE_RE.test(line)) {
|
|
129
|
+
const inner = [];
|
|
130
|
+
while (i < lines.length && (QUOTE_RE.test(lines[i]) || lines[i].trim() !== "")) {
|
|
131
|
+
const match = lines[i].match(QUOTE_RE);
|
|
132
|
+
if (!match && lines[i].trim() === "") break;
|
|
133
|
+
inner.push(match ? match[1] : lines[i]);
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
blocks.push({
|
|
137
|
+
type: "quote",
|
|
138
|
+
children: parseBlocks(inner)
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (UL_RE.test(line) || OL_RE.test(line)) {
|
|
143
|
+
const { block, next } = parseList(lines, i);
|
|
144
|
+
blocks.push(block);
|
|
145
|
+
i = next;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (line.includes("|") && lines[i + 1] !== void 0 && TABLE_DELIM_RE.test(lines[i + 1])) {
|
|
149
|
+
const head = splitRow(line).map(parseInline);
|
|
150
|
+
const align = parseAlign(lines[i + 1]);
|
|
151
|
+
const rows = [];
|
|
152
|
+
i += 2;
|
|
153
|
+
while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") {
|
|
154
|
+
rows.push(splitRow(lines[i]).map(parseInline));
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
blocks.push({
|
|
158
|
+
type: "table",
|
|
159
|
+
head,
|
|
160
|
+
rows,
|
|
161
|
+
align
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const paragraph = [];
|
|
166
|
+
while (i < lines.length && lines[i].trim() !== "") {
|
|
167
|
+
const next = lines[i + 1];
|
|
168
|
+
if (next !== void 0 && SETEXT_RE.test(next)) {
|
|
169
|
+
paragraph.push(lines[i]);
|
|
170
|
+
i += 2;
|
|
171
|
+
blocks.push({
|
|
172
|
+
type: "heading",
|
|
173
|
+
level: next.trim().startsWith("=") ? 1 : 2,
|
|
174
|
+
children: parseInline(paragraph.join(" "))
|
|
175
|
+
});
|
|
176
|
+
paragraph.length = 0;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (paragraph.length > 0 && isBlockStart(lines[i])) break;
|
|
180
|
+
paragraph.push(lines[i]);
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
if (paragraph.length > 0) {
|
|
184
|
+
const text = paragraph.join("\n");
|
|
185
|
+
const figure = asStandaloneImage(text);
|
|
186
|
+
blocks.push(figure ?? {
|
|
187
|
+
type: "paragraph",
|
|
188
|
+
children: parseInline(text)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return blocks;
|
|
193
|
+
}
|
|
194
|
+
/** An image alone in its paragraph is a figure, not a run of inline content. */
|
|
195
|
+
function asStandaloneImage(text) {
|
|
196
|
+
const match = text.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
|
|
197
|
+
if (!match) return null;
|
|
198
|
+
return {
|
|
199
|
+
type: "figure",
|
|
200
|
+
alt: match[1],
|
|
201
|
+
src: match[2]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function parseList(lines, start) {
|
|
205
|
+
const first = lines[start];
|
|
206
|
+
const ordered = OL_RE.test(first);
|
|
207
|
+
const startNumber = ordered ? Number(first.match(OL_RE)?.[2] ?? 1) : 1;
|
|
208
|
+
const baseIndent = indentOf(first);
|
|
209
|
+
const items = [];
|
|
210
|
+
let i = start;
|
|
211
|
+
let current = null;
|
|
212
|
+
const flush = () => {
|
|
213
|
+
if (current) items.push(parseBlocks(current));
|
|
214
|
+
current = null;
|
|
215
|
+
};
|
|
216
|
+
while (i < lines.length) {
|
|
217
|
+
const line = lines[i];
|
|
218
|
+
if (line.trim() === "") {
|
|
219
|
+
const next = lines[i + 1];
|
|
220
|
+
if (next === void 0 || next.trim() !== "" && indentOf(next) <= baseIndent && !isItem(next)) break;
|
|
221
|
+
current?.push("");
|
|
222
|
+
i++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const match = line.match(ordered ? OL_RE : UL_RE) ?? line.match(ordered ? UL_RE : OL_RE);
|
|
226
|
+
if (match && indentOf(line) <= baseIndent) {
|
|
227
|
+
flush();
|
|
228
|
+
current = [match[3]];
|
|
229
|
+
i++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (indentOf(line) > baseIndent && current) {
|
|
233
|
+
current.push(line.slice(Math.min(indentOf(line), baseIndent + 2)));
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
flush();
|
|
240
|
+
return {
|
|
241
|
+
block: {
|
|
242
|
+
type: "list",
|
|
243
|
+
ordered,
|
|
244
|
+
start: startNumber,
|
|
245
|
+
items
|
|
246
|
+
},
|
|
247
|
+
next: i
|
|
248
|
+
};
|
|
249
|
+
function isItem(line) {
|
|
250
|
+
return UL_RE.test(line) || OL_RE.test(line);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const INLINE_RE = /(!\[[^\]]*\]\([^)\s]+(?:\s+"[^"]*")?\))|(\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\))|(`+)([^`]*?)\3|(\*\*|__)(?=\S)([\s\S]*?\S)\5|(\*|_)(?=\S)([\s\S]*?\S)\7|(<[^>\s]+@[^>\s]+>)|(https?:\/\/[^\s<>"')\]]+)/;
|
|
254
|
+
function parseInline(source) {
|
|
255
|
+
const out = [];
|
|
256
|
+
let rest = maskEscapes(source);
|
|
257
|
+
const pushText = (value) => {
|
|
258
|
+
if (value === "") return;
|
|
259
|
+
value.split(/(?: {2}|\\)\n|\n/).forEach((part, index) => {
|
|
260
|
+
if (index > 0) out.push({ type: "break" });
|
|
261
|
+
const text = unmask(part);
|
|
262
|
+
if (text !== "") out.push({
|
|
263
|
+
type: "text",
|
|
264
|
+
value: text
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
while (rest.length > 0) {
|
|
269
|
+
const match = rest.match(INLINE_RE);
|
|
270
|
+
if (!match || match.index === void 0) break;
|
|
271
|
+
pushText(rest.slice(0, match.index));
|
|
272
|
+
const token = match[0];
|
|
273
|
+
if (match[1]) {
|
|
274
|
+
const image = token.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
|
|
275
|
+
if (image) out.push({
|
|
276
|
+
type: "image",
|
|
277
|
+
alt: unmask(image[1]),
|
|
278
|
+
src: unmask(image[2])
|
|
279
|
+
});
|
|
280
|
+
} else if (match[2]) {
|
|
281
|
+
const link = token.match(/^\[([^\]]*)\]\(([^)\s]*)(?:\s+"[^"]*")?\)$/);
|
|
282
|
+
if (link) out.push({
|
|
283
|
+
type: "link",
|
|
284
|
+
href: unmask(link[2]),
|
|
285
|
+
children: parseInline(link[1])
|
|
286
|
+
});
|
|
287
|
+
} else if (match[3]) out.push({
|
|
288
|
+
type: "code",
|
|
289
|
+
value: unmask(match[4].trim())
|
|
290
|
+
});
|
|
291
|
+
else if (match[5]) out.push({
|
|
292
|
+
type: "strong",
|
|
293
|
+
children: parseInline(match[6])
|
|
294
|
+
});
|
|
295
|
+
else if (match[7]) out.push({
|
|
296
|
+
type: "em",
|
|
297
|
+
children: parseInline(match[8])
|
|
298
|
+
});
|
|
299
|
+
else if (match[9]) {
|
|
300
|
+
const address = token.slice(1, -1);
|
|
301
|
+
out.push({
|
|
302
|
+
type: "link",
|
|
303
|
+
href: `mailto:${address}`,
|
|
304
|
+
children: [{
|
|
305
|
+
type: "text",
|
|
306
|
+
value: address
|
|
307
|
+
}]
|
|
308
|
+
});
|
|
309
|
+
} else if (match[10]) {
|
|
310
|
+
const url = unmask(token);
|
|
311
|
+
out.push({
|
|
312
|
+
type: "link",
|
|
313
|
+
href: url,
|
|
314
|
+
children: [{
|
|
315
|
+
type: "text",
|
|
316
|
+
value: url
|
|
317
|
+
}]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
rest = rest.slice(match.index + token.length);
|
|
321
|
+
}
|
|
322
|
+
pushText(rest);
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
const ESCAPE_RE = /\\([\\`*_{}[\]()#+\-.!|>~])/g;
|
|
326
|
+
const MASK_RE = /\u0000(\d+)\u0000/g;
|
|
327
|
+
/**
|
|
328
|
+
* The inline scanner is a regex over delimiters, which cannot see a preceding
|
|
329
|
+
* backslash. Escaped characters are swapped for a sentinel before scanning and
|
|
330
|
+
* restored on the way out, so `\*not emphasis\*` survives as literal text.
|
|
331
|
+
*/
|
|
332
|
+
function maskEscapes(value) {
|
|
333
|
+
return value.replace(ESCAPE_RE, (_, char) => `\u0000${char.charCodeAt(0)}\u0000`);
|
|
334
|
+
}
|
|
335
|
+
function unmask(value) {
|
|
336
|
+
return value.replace(MASK_RE, (_, code) => String.fromCharCode(Number(code)));
|
|
337
|
+
}
|
|
338
|
+
/** Flattens inline content to plain text — for titles, alt text, and outline labels. */
|
|
339
|
+
function inlineText(nodes) {
|
|
340
|
+
return nodes.map((node) => {
|
|
341
|
+
switch (node.type) {
|
|
342
|
+
case "text":
|
|
343
|
+
case "code": return node.value;
|
|
344
|
+
case "strong":
|
|
345
|
+
case "em":
|
|
346
|
+
case "link": return inlineText(node.children);
|
|
347
|
+
case "image": return node.alt;
|
|
348
|
+
case "break": return " ";
|
|
349
|
+
default: return "";
|
|
350
|
+
}
|
|
351
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/import/to-tsx.ts
|
|
355
|
+
/** JSX text is written literally; only these characters have to be escaped. */
|
|
356
|
+
function jsxText(text) {
|
|
357
|
+
return text.replace(/[{}<>]/g, (char) => `{'${char}'}`);
|
|
358
|
+
}
|
|
359
|
+
function jsString(value) {
|
|
360
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
361
|
+
}
|
|
362
|
+
function attr(value) {
|
|
363
|
+
return `"${value.replace(/"/g, """)}"`;
|
|
364
|
+
}
|
|
365
|
+
function collectImageSources(blocks) {
|
|
366
|
+
const out = [];
|
|
367
|
+
const walkInline = (nodes) => {
|
|
368
|
+
for (const node of nodes) if (node.type === "image") out.push(node.src);
|
|
369
|
+
else if (node.type === "strong" || node.type === "em" || node.type === "link") walkInline(node.children);
|
|
370
|
+
};
|
|
371
|
+
const walk = (list) => {
|
|
372
|
+
for (const block of list) switch (block.type) {
|
|
373
|
+
case "figure":
|
|
374
|
+
out.push(block.src);
|
|
375
|
+
break;
|
|
376
|
+
case "heading":
|
|
377
|
+
case "paragraph":
|
|
378
|
+
walkInline(block.children);
|
|
379
|
+
break;
|
|
380
|
+
case "quote":
|
|
381
|
+
walk(block.children);
|
|
382
|
+
break;
|
|
383
|
+
case "list":
|
|
384
|
+
for (const item of block.items) walk(item);
|
|
385
|
+
break;
|
|
386
|
+
case "table":
|
|
387
|
+
for (const cell of block.head) walkInline(cell);
|
|
388
|
+
for (const row of block.rows) for (const cell of row) walkInline(cell);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
walk(blocks);
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
function imageSrc(src, images) {
|
|
395
|
+
const known = images?.get(src);
|
|
396
|
+
return known ? `{${known.ident}}` : attr(src);
|
|
397
|
+
}
|
|
398
|
+
function renderInline(nodes, images) {
|
|
399
|
+
return nodes.map((node) => {
|
|
400
|
+
switch (node.type) {
|
|
401
|
+
case "text": return jsxText(node.value);
|
|
402
|
+
case "strong": return `<strong>${renderInline(node.children, images)}</strong>`;
|
|
403
|
+
case "em": return `<em>${renderInline(node.children, images)}</em>`;
|
|
404
|
+
case "code": return `<code style={codeInline}>${jsxText(node.value)}</code>`;
|
|
405
|
+
case "link": return `<a href=${attr(node.href)} style={link}>${renderInline(node.children, images)}</a>`;
|
|
406
|
+
case "image": return `<img src=${imageSrc(node.src, images)} alt=${attr(node.alt)} style={inlineImg} />`;
|
|
407
|
+
case "break": return "<br />";
|
|
408
|
+
default: return "";
|
|
409
|
+
}
|
|
410
|
+
}).join("");
|
|
411
|
+
}
|
|
412
|
+
const HEADING_STYLE = {
|
|
413
|
+
1: "h1",
|
|
414
|
+
2: "h2",
|
|
415
|
+
3: "h3",
|
|
416
|
+
4: "h4"
|
|
417
|
+
};
|
|
418
|
+
function alignStyle(align) {
|
|
419
|
+
return align && align !== "left" ? ` align={${jsString(align)}}` : "";
|
|
420
|
+
}
|
|
421
|
+
function renderBlock(block, images, indent) {
|
|
422
|
+
const pad = indent;
|
|
423
|
+
switch (block.type) {
|
|
424
|
+
case "heading": {
|
|
425
|
+
const level = Math.min(block.level, 4);
|
|
426
|
+
const tag = level >= 4 ? "h4" : `h${level}`;
|
|
427
|
+
return `${pad}<${tag} style={${HEADING_STYLE[level] ?? "h3"}}>${renderInline(block.children, images)}</${tag}>`;
|
|
428
|
+
}
|
|
429
|
+
case "paragraph": return `${pad}<p style={p}>${renderInline(block.children, images)}</p>`;
|
|
430
|
+
case "hr": return `${pad}<hr style={rule} />`;
|
|
431
|
+
case "code": return `${pad}<pre style={pre}>{${jsString(block.value)}}</pre>`;
|
|
432
|
+
case "quote": return [
|
|
433
|
+
`${pad}<blockquote style={quote}>`,
|
|
434
|
+
...block.children.map((child) => renderBlock(child, images, `${pad} `)),
|
|
435
|
+
`${pad}</blockquote>`
|
|
436
|
+
].join("\n");
|
|
437
|
+
case "figure": {
|
|
438
|
+
const caption = block.alt ? `\n${pad} <figcaption style={caption}>${jsxText(block.alt)}</figcaption>` : "";
|
|
439
|
+
return [
|
|
440
|
+
`${pad}<figure style={figure}>`,
|
|
441
|
+
`${pad} <img src=${imageSrc(block.src, images)} alt=${attr(block.alt)} style={img} />${caption}`,
|
|
442
|
+
`${pad}</figure>`
|
|
443
|
+
].join("\n");
|
|
444
|
+
}
|
|
445
|
+
case "list": {
|
|
446
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
447
|
+
const startAttr = block.ordered && block.start !== 1 ? ` start={${block.start}}` : "";
|
|
448
|
+
const items = block.items.map((item) => {
|
|
449
|
+
const inner = item.map((child, index) => child.type === "paragraph" && index === 0 ? `${renderInline(child.children, images)}` : `\n${renderBlock(child, images, `${pad} `)}`).join("");
|
|
450
|
+
return `${pad} <li style={li}>${inner.startsWith("\n") ? `${inner}\n${pad} ` : inner}</li>`;
|
|
451
|
+
});
|
|
452
|
+
return [
|
|
453
|
+
`${pad}<${tag} style={${tag}}${startAttr}>`,
|
|
454
|
+
...items,
|
|
455
|
+
`${pad}</${tag}>`
|
|
456
|
+
].join("\n");
|
|
457
|
+
}
|
|
458
|
+
case "table": {
|
|
459
|
+
const head = block.head.map((cell, i) => `${pad} <Th${alignStyle(block.align[i] ?? null)}>${renderInline(cell, images)}</Th>`).join("\n");
|
|
460
|
+
const rows = block.rows.map((row) => [
|
|
461
|
+
`${pad} <tr>`,
|
|
462
|
+
...row.map((cell, i) => `${pad} <Td${alignStyle(block.align[i] ?? null)}>${renderInline(cell, images)}</Td>`),
|
|
463
|
+
`${pad} </tr>`
|
|
464
|
+
].join("\n")).join("\n");
|
|
465
|
+
return [
|
|
466
|
+
`${pad}<table style={table}>`,
|
|
467
|
+
`${pad} <thead>`,
|
|
468
|
+
`${pad} <tr>`,
|
|
469
|
+
head,
|
|
470
|
+
`${pad} </tr>`,
|
|
471
|
+
`${pad} </thead>`,
|
|
472
|
+
`${pad} <tbody>`,
|
|
473
|
+
rows,
|
|
474
|
+
`${pad} </tbody>`,
|
|
475
|
+
`${pad}</table>`
|
|
476
|
+
].join("\n");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function titleFrom(parsed, opts) {
|
|
481
|
+
if (opts.title) return opts.title;
|
|
482
|
+
if (parsed.frontmatter.title) return parsed.frontmatter.title;
|
|
483
|
+
const heading = parsed.blocks.find((block) => block.type === "heading" && block.level === 1);
|
|
484
|
+
if (heading && heading.type === "heading") return inlineText(heading.children);
|
|
485
|
+
return opts.docId.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
|
486
|
+
}
|
|
487
|
+
const STYLES = `const page = {
|
|
488
|
+
width: '100%',
|
|
489
|
+
height: '100%',
|
|
490
|
+
boxSizing: 'border-box' as const,
|
|
491
|
+
padding: 'var(--od-margin)',
|
|
492
|
+
background: 'var(--od-bg)',
|
|
493
|
+
color: 'var(--od-text)',
|
|
494
|
+
fontFamily: 'var(--od-font-body)',
|
|
495
|
+
fontSize: 'var(--od-size-body)',
|
|
496
|
+
lineHeight: 'var(--od-leading)',
|
|
497
|
+
position: 'relative' as const,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const h1 = {
|
|
501
|
+
fontFamily: 'var(--od-font-heading)',
|
|
502
|
+
fontSize: 'var(--od-size-h1)',
|
|
503
|
+
lineHeight: 1.2,
|
|
504
|
+
fontWeight: 650,
|
|
505
|
+
margin: '0 0 18px',
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const h2 = { ...h1, fontSize: 'var(--od-size-h2)', margin: '26px 0 12px' };
|
|
509
|
+
const h3 = { ...h1, fontSize: 'var(--od-size-h3)', fontWeight: 600, margin: '20px 0 8px' };
|
|
510
|
+
const h4 = { ...h3, fontSize: 'var(--od-size-body)', margin: '16px 0 6px' };
|
|
511
|
+
const p = { margin: '0 0 12px' };
|
|
512
|
+
// The viewer's CSS reset strips list markers, so both are set back explicitly.
|
|
513
|
+
const ul = { margin: '0 0 12px', paddingLeft: 22, listStyle: 'disc outside' };
|
|
514
|
+
const ol = { ...ul, listStyle: 'decimal outside' };
|
|
515
|
+
const li = { margin: '0 0 5px' };
|
|
516
|
+
const link = { color: 'var(--od-accent)', textDecoration: 'none' };
|
|
517
|
+
const rule = { border: 0, borderTop: '1px solid var(--od-rule)', margin: '20px 0' };
|
|
518
|
+
|
|
519
|
+
const codeInline = {
|
|
520
|
+
fontFamily: 'var(--od-font-mono)',
|
|
521
|
+
fontSize: '0.92em',
|
|
522
|
+
background: 'var(--od-rule)',
|
|
523
|
+
borderRadius: 3,
|
|
524
|
+
padding: '1px 4px',
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const pre = {
|
|
528
|
+
fontFamily: 'var(--od-font-mono)',
|
|
529
|
+
fontSize: 11,
|
|
530
|
+
lineHeight: 1.5,
|
|
531
|
+
background: 'var(--od-rule)',
|
|
532
|
+
borderRadius: 'var(--od-radius)',
|
|
533
|
+
padding: '10px 12px',
|
|
534
|
+
margin: '0 0 14px',
|
|
535
|
+
whiteSpace: 'pre-wrap' as const,
|
|
536
|
+
overflowWrap: 'anywhere' as const,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const quote = {
|
|
540
|
+
margin: '0 0 14px',
|
|
541
|
+
padding: '2px 0 2px 14px',
|
|
542
|
+
borderLeft: '2px solid var(--od-accent)',
|
|
543
|
+
color: 'var(--od-muted)',
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const table = {
|
|
547
|
+
width: '100%',
|
|
548
|
+
borderCollapse: 'collapse' as const,
|
|
549
|
+
tableLayout: 'fixed' as const,
|
|
550
|
+
margin: '0 0 16px',
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const figure = { margin: '0 0 16px' };
|
|
554
|
+
const img = { width: '100%', display: 'block' as const };
|
|
555
|
+
const inlineImg = { maxWidth: '100%', verticalAlign: 'middle' as const };
|
|
556
|
+
const caption = { fontSize: 'var(--od-size-caption)', color: 'var(--od-muted)', margin: '6px 0 0' };
|
|
557
|
+
|
|
558
|
+
const Th = ({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'center' | 'right' }) => (
|
|
559
|
+
<th
|
|
560
|
+
style={{
|
|
561
|
+
textAlign: align,
|
|
562
|
+
fontFamily: 'var(--od-font-heading)',
|
|
563
|
+
fontSize: 11,
|
|
564
|
+
fontWeight: 600,
|
|
565
|
+
letterSpacing: '0.04em',
|
|
566
|
+
textTransform: 'uppercase',
|
|
567
|
+
color: 'var(--od-muted)',
|
|
568
|
+
borderBottom: '1px solid var(--od-rule)',
|
|
569
|
+
padding: '0 8px 6px',
|
|
570
|
+
}}
|
|
571
|
+
>
|
|
572
|
+
{children}
|
|
573
|
+
</th>
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const Td = ({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'center' | 'right' }) => (
|
|
577
|
+
<td
|
|
578
|
+
style={{
|
|
579
|
+
textAlign: align,
|
|
580
|
+
fontSize: 12,
|
|
581
|
+
padding: '7px 8px',
|
|
582
|
+
borderBottom: '1px solid var(--od-rule)',
|
|
583
|
+
fontVariantNumeric: align === 'right' ? 'tabular-nums' : undefined,
|
|
584
|
+
}}
|
|
585
|
+
>
|
|
586
|
+
{children}
|
|
587
|
+
</td>
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const Footer = () => {
|
|
591
|
+
const n = useDocPageNumber();
|
|
592
|
+
const total = useDocPageCount();
|
|
593
|
+
return (
|
|
594
|
+
<div
|
|
595
|
+
style={{
|
|
596
|
+
position: 'absolute',
|
|
597
|
+
left: 'var(--od-margin)',
|
|
598
|
+
right: 'var(--od-margin)',
|
|
599
|
+
bottom: 40,
|
|
600
|
+
display: 'flex',
|
|
601
|
+
justifyContent: 'space-between',
|
|
602
|
+
fontSize: 'var(--od-size-caption)',
|
|
603
|
+
color: 'var(--od-muted)',
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
<span>{DOC_TITLE}</span>
|
|
607
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
608
|
+
{n} / {total}
|
|
609
|
+
</span>
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
};`;
|
|
613
|
+
const DESIGN = `export const design: DesignSystem = {
|
|
614
|
+
palette: {
|
|
615
|
+
bg: '#ffffff',
|
|
616
|
+
text: '#16181d',
|
|
617
|
+
muted: '#6b7280',
|
|
618
|
+
accent: '#2563eb',
|
|
619
|
+
rule: '#e5e7eb',
|
|
620
|
+
},
|
|
621
|
+
fonts: {
|
|
622
|
+
heading: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
|
|
623
|
+
body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
|
|
624
|
+
mono: 'ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace',
|
|
625
|
+
},
|
|
626
|
+
typeScale: { title: 44, h1: 28, h2: 20, h3: 16, body: 14, caption: 10 },
|
|
627
|
+
margin: 76,
|
|
628
|
+
leading: 1.55,
|
|
629
|
+
radius: 6,
|
|
630
|
+
};`;
|
|
631
|
+
/**
|
|
632
|
+
* Turns parsed markdown into a document module. The output is ordinary authored
|
|
633
|
+
* TSX — inline styles, real heading tags, plain JSX text — so the outline, the
|
|
634
|
+
* inspector, and the design panel all work on an imported document exactly as
|
|
635
|
+
* they do on a hand-written one.
|
|
636
|
+
*/
|
|
637
|
+
function generateDocumentSource(parsed, opts) {
|
|
638
|
+
const title = titleFrom(parsed, opts);
|
|
639
|
+
const subtitle = opts.subtitle ?? parsed.frontmatter.subtitle;
|
|
640
|
+
const author = opts.author ?? parsed.frontmatter.author;
|
|
641
|
+
const theme = opts.theme ?? parsed.frontmatter.theme;
|
|
642
|
+
const pageSize = opts.pageSize ?? parsed.frontmatter.pageSize;
|
|
643
|
+
const orientation = opts.orientation ?? parsed.frontmatter.orientation;
|
|
644
|
+
const cover = opts.cover !== false;
|
|
645
|
+
let blocks = parsed.blocks;
|
|
646
|
+
const first = blocks[0];
|
|
647
|
+
if (cover && first?.type === "heading" && first.level === 1 && inlineText(first.children) === title) blocks = blocks.slice(1);
|
|
648
|
+
const images = opts.images;
|
|
649
|
+
const imports = images ? [...images.values()] : [];
|
|
650
|
+
const head = [
|
|
651
|
+
`import type { ReactNode } from 'react';`,
|
|
652
|
+
`import {\n${[
|
|
653
|
+
"type DesignSystem",
|
|
654
|
+
"type DocEntry",
|
|
655
|
+
"type DocMeta",
|
|
656
|
+
...cover || opts.contents ? ["type DocPage"] : [],
|
|
657
|
+
...opts.contents ? ["TableOfContents"] : [],
|
|
658
|
+
"flow",
|
|
659
|
+
"useDocPageCount",
|
|
660
|
+
"useDocPageNumber"
|
|
661
|
+
].map((name) => ` ${name},`).join("\n")}\n} from 'mosage';`,
|
|
662
|
+
...imports.map((image) => `import ${image.ident} from './assets/${image.filename}';`)
|
|
663
|
+
].join("\n");
|
|
664
|
+
const styles = STYLES.replace("{DOC_TITLE}", jsxText(title));
|
|
665
|
+
const coverPage = cover ? `const Cover: DocPage = () => (
|
|
666
|
+
<div style={{ ...page, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
|
667
|
+
${subtitle ? ` <p style={{ fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--od-accent)', margin: 0 }}>
|
|
668
|
+
${jsxText(subtitle)}
|
|
669
|
+
</p>\n` : ""} <h1 style={{ ...h1, fontSize: 'var(--od-size-title)', margin: '16px 0 12px' }} data-od-outline="skip">
|
|
670
|
+
${jsxText(title)}
|
|
671
|
+
</h1>
|
|
672
|
+
${author ? ` <p style={{ color: 'var(--od-muted)', margin: 0 }}>${jsxText(author)}</p>\n` : ""} </div>
|
|
673
|
+
);` : "";
|
|
674
|
+
const contentsPage = opts.contents ? `const Contents: DocPage = () => (
|
|
675
|
+
<div style={page}>
|
|
676
|
+
<h1 style={h1} data-od-outline="skip">
|
|
677
|
+
Contents
|
|
678
|
+
</h1>
|
|
679
|
+
<TableOfContents maxLevel={2} />
|
|
680
|
+
</div>
|
|
681
|
+
);` : "";
|
|
682
|
+
const body = `const Body = flow(
|
|
683
|
+
<>
|
|
684
|
+
${blocks.map((block) => renderBlock(block, images, " ")).join("\n")}
|
|
685
|
+
</>,
|
|
686
|
+
{ footer: Footer },
|
|
687
|
+
);`;
|
|
688
|
+
const metaFields = [
|
|
689
|
+
` title: ${jsString(title)},`,
|
|
690
|
+
...subtitle ? [` subtitle: ${jsString(subtitle)},`] : [],
|
|
691
|
+
...author ? [` author: ${jsString(author)},`] : [],
|
|
692
|
+
...pageSize ? [` pageSize: ${jsString(pageSize)},`] : [],
|
|
693
|
+
...orientation ? [` orientation: ${jsString(orientation)},`] : [],
|
|
694
|
+
...theme ? [` theme: ${jsString(theme)},`] : [],
|
|
695
|
+
` createdAt: ${jsString(opts.createdAt)},`
|
|
696
|
+
];
|
|
697
|
+
const entries = [
|
|
698
|
+
...cover ? ["Cover"] : [],
|
|
699
|
+
...opts.contents ? ["Contents"] : [],
|
|
700
|
+
"Body"
|
|
701
|
+
];
|
|
702
|
+
return {
|
|
703
|
+
source: [
|
|
704
|
+
head,
|
|
705
|
+
"",
|
|
706
|
+
DESIGN,
|
|
707
|
+
"",
|
|
708
|
+
styles,
|
|
709
|
+
"",
|
|
710
|
+
...coverPage ? [coverPage, ""] : [],
|
|
711
|
+
...contentsPage ? [contentsPage, ""] : [],
|
|
712
|
+
body,
|
|
713
|
+
"",
|
|
714
|
+
`export const meta: DocMeta = {\n${metaFields.join("\n")}\n};`,
|
|
715
|
+
`export default [${entries.join(", ")}] satisfies DocEntry[];`,
|
|
716
|
+
""
|
|
717
|
+
].join("\n"),
|
|
718
|
+
title,
|
|
719
|
+
blockCount: blocks.length
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/ops/import.ts
|
|
724
|
+
const REMOTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
725
|
+
function slugify(value, fallback = "document") {
|
|
726
|
+
const slug = value.normalize("NFKD").toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
727
|
+
return DOC_ID_RE.test(slug) ? slug : fallback;
|
|
728
|
+
}
|
|
729
|
+
function uniqueId(base, taken) {
|
|
730
|
+
if (!taken.has(base)) return base;
|
|
731
|
+
for (let n = 2; n < 1e3; n++) {
|
|
732
|
+
const candidate = `${base}-${n}`;
|
|
733
|
+
if (!taken.has(candidate)) return candidate;
|
|
734
|
+
}
|
|
735
|
+
throw new OpsError(409, `could not find a free id for ${base}`);
|
|
736
|
+
}
|
|
737
|
+
function identFor(index) {
|
|
738
|
+
return `figure${index + 1}`;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Reads markdown and writes a real MoSage document: `flow()` body, inline
|
|
742
|
+
* styles, a cover, and any local images copied into the document's own assets
|
|
743
|
+
* folder. Nothing about the result is special-cased — it is the same shape an
|
|
744
|
+
* agent would have written by hand, and every editing surface works on it.
|
|
745
|
+
*/
|
|
746
|
+
async function importMarkdown(ctx, opts) {
|
|
747
|
+
const sourceFile = opts.file ? path.resolve(ctx.userCwd, opts.file) : null;
|
|
748
|
+
if (sourceFile && !sourceFile.startsWith(ctx.userCwd + path.sep)) throw new OpsError(400, `file must sit inside the workspace: ${opts.file}`);
|
|
749
|
+
let markdown = opts.markdown;
|
|
750
|
+
if (markdown === void 0) {
|
|
751
|
+
if (!sourceFile) throw new OpsError(400, "pass either `markdown` or `file`");
|
|
752
|
+
try {
|
|
753
|
+
markdown = await fs.readFile(sourceFile, "utf8");
|
|
754
|
+
} catch {
|
|
755
|
+
throw new OpsError(404, `file not found: ${opts.file}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (markdown.trim() === "") throw new OpsError(422, "the markdown is empty");
|
|
759
|
+
const parsed = parseMarkdown(markdown);
|
|
760
|
+
const pageSize = opts.pageSize ?? parsed.frontmatter.pageSize;
|
|
761
|
+
if (pageSize !== void 0 && !isPageSizeName(pageSize)) throw new OpsError(422, `unsupported pageSize \`${pageSize}\` — use ${PAGE_SIZE_NAMES.join(", ")}`);
|
|
762
|
+
const orientation = opts.orientation ?? parsed.frontmatter.orientation;
|
|
763
|
+
if (orientation !== void 0 && !isOrientation(orientation)) throw new OpsError(422, `unsupported orientation \`${orientation}\` — use ${ORIENTATIONS.join(", ")}`);
|
|
764
|
+
const fallbackName = sourceFile ? path.basename(sourceFile).replace(/\.mdx?$/i, "") : "document";
|
|
765
|
+
const requestedId = opts.docId ?? slugify(opts.title ?? parsed.frontmatter.title ?? fallbackName);
|
|
766
|
+
if (opts.docId && !DOC_ID_RE.test(opts.docId)) throw new OpsError(400, `invalid document id: ${opts.docId}`);
|
|
767
|
+
const docId = opts.docId ?? uniqueId(requestedId, new Set(await listDocIds(ctx)));
|
|
768
|
+
const baseDir = sourceFile ? path.dirname(sourceFile) : ctx.userCwd;
|
|
769
|
+
const images = /* @__PURE__ */ new Map();
|
|
770
|
+
const missing = [];
|
|
771
|
+
const staged = [];
|
|
772
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
773
|
+
for (const src of new Set(collectImageSources(parsed.blocks))) {
|
|
774
|
+
if (REMOTE_RE.test(src)) continue;
|
|
775
|
+
const from = path.resolve(baseDir, src.split(/[?#]/)[0]);
|
|
776
|
+
if (!from.startsWith(ctx.userCwd + path.sep) || !existsSync(from)) {
|
|
777
|
+
missing.push(src);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const safe = validateAssetName(path.basename(from));
|
|
781
|
+
if (!safe) {
|
|
782
|
+
missing.push(src);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const filename = usedNames.has(safe) ? `${images.size + 1}-${safe}` : safe;
|
|
786
|
+
usedNames.add(filename);
|
|
787
|
+
images.set(src, {
|
|
788
|
+
source: src,
|
|
789
|
+
ident: identFor(images.size),
|
|
790
|
+
filename
|
|
791
|
+
});
|
|
792
|
+
staged.push({
|
|
793
|
+
from,
|
|
794
|
+
filename
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const generated = generateDocumentSource(parsed, {
|
|
798
|
+
docId,
|
|
799
|
+
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
800
|
+
...opts.subtitle !== void 0 ? { subtitle: opts.subtitle } : {},
|
|
801
|
+
...opts.author !== void 0 ? { author: opts.author } : {},
|
|
802
|
+
...opts.theme !== void 0 ? { theme: opts.theme } : {},
|
|
803
|
+
...opts.pageSize !== void 0 ? { pageSize: opts.pageSize } : {},
|
|
804
|
+
...opts.orientation !== void 0 ? { orientation: opts.orientation } : {},
|
|
805
|
+
...opts.cover !== void 0 ? { cover: opts.cover } : {},
|
|
806
|
+
...opts.contents !== void 0 ? { contents: opts.contents } : {},
|
|
807
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
808
|
+
images
|
|
809
|
+
});
|
|
810
|
+
const created = await createDocument(ctx, docId, generated.source);
|
|
811
|
+
if (staged.length > 0) {
|
|
812
|
+
const assetsDir = path.join(ctx.docsRoot, docId, "assets");
|
|
813
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
814
|
+
for (const { from, filename } of staged) await fs.copyFile(from, path.join(assetsDir, filename));
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
id: created.id,
|
|
818
|
+
entry: created.entry,
|
|
819
|
+
title: generated.title,
|
|
820
|
+
blocks: generated.blockCount,
|
|
821
|
+
assets: staged.map((asset) => asset.filename),
|
|
822
|
+
missingAssets: missing
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/render/session.ts
|
|
827
|
+
var RenderUnavailableError = class extends Error {
|
|
828
|
+
constructor(message) {
|
|
829
|
+
super(message);
|
|
830
|
+
this.name = "RenderUnavailableError";
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const INSTALL_HINT = "Headless rendering needs Playwright. Install it in this workspace:\n pnpm add -D playwright && pnpm exec playwright install chromium";
|
|
834
|
+
async function loadPlaywright() {
|
|
835
|
+
try {
|
|
836
|
+
return await import("playwright");
|
|
837
|
+
} catch {
|
|
838
|
+
throw new RenderUnavailableError(INSTALL_HINT);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
842
|
+
const PRINT_PAGE_SELECTOR = "#od-print-root .od-print-page";
|
|
843
|
+
async function bootServer(userCwd) {
|
|
844
|
+
const base = await createViteConfig({
|
|
845
|
+
userCwd,
|
|
846
|
+
headless: true
|
|
847
|
+
});
|
|
848
|
+
const config = mergeConfig(base, {
|
|
849
|
+
logLevel: "silent",
|
|
850
|
+
server: {
|
|
851
|
+
port: 0,
|
|
852
|
+
strictPort: false,
|
|
853
|
+
open: false
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
const server = await createServer(config);
|
|
857
|
+
await server.listen();
|
|
858
|
+
const origin = server.resolvedUrls?.local?.[0]?.replace(/\/$/, "");
|
|
859
|
+
if (!origin) {
|
|
860
|
+
await server.close();
|
|
861
|
+
throw new Error("Could not determine the dev server URL");
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
server,
|
|
865
|
+
origin
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function createRenderSession(opts) {
|
|
869
|
+
const { chromium } = await loadPlaywright();
|
|
870
|
+
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
871
|
+
let server = null;
|
|
872
|
+
let origin = opts.origin?.replace(/\/$/, "") ?? "";
|
|
873
|
+
if (!origin) {
|
|
874
|
+
const booted = await bootServer(opts.userCwd);
|
|
875
|
+
server = booted.server;
|
|
876
|
+
origin = booted.origin;
|
|
877
|
+
}
|
|
878
|
+
let browser;
|
|
879
|
+
try {
|
|
880
|
+
browser = await chromium.launch({ headless: true });
|
|
881
|
+
} catch (err) {
|
|
882
|
+
await server?.close();
|
|
883
|
+
throw new RenderUnavailableError(`${INSTALL_HINT}\n\nChromium failed to launch: ${err.message}`);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
origin,
|
|
887
|
+
async open(docId) {
|
|
888
|
+
const page = await browser.newPage({
|
|
889
|
+
viewport: {
|
|
890
|
+
width: 1280,
|
|
891
|
+
height: 1024
|
|
892
|
+
},
|
|
893
|
+
...opts.deviceScaleFactor !== void 0 ? { deviceScaleFactor: opts.deviceScaleFactor } : {}
|
|
894
|
+
});
|
|
895
|
+
const errors = [];
|
|
896
|
+
page.on("pageerror", (err) => errors.push(String(err?.message ?? err)));
|
|
897
|
+
await page.goto(`${origin}/d/${encodeURIComponent(docId)}`, {
|
|
898
|
+
waitUntil: "load",
|
|
899
|
+
timeout
|
|
900
|
+
});
|
|
901
|
+
try {
|
|
902
|
+
await page.waitForFunction("globalThis.__mosage ? globalThis.__mosage.status().ready : false", void 0, { timeout });
|
|
903
|
+
} catch {
|
|
904
|
+
await page.close();
|
|
905
|
+
const detail = errors.length ? `\n${errors.join("\n")}` : "";
|
|
906
|
+
throw new Error(`Document "${docId}" never finished rendering.${detail}`);
|
|
907
|
+
}
|
|
908
|
+
const status = await page.evaluate("globalThis.__mosage.status()");
|
|
909
|
+
return {
|
|
910
|
+
status,
|
|
911
|
+
diagnose: () => page.evaluate("globalThis.__mosage.diagnose()"),
|
|
912
|
+
async pdf() {
|
|
913
|
+
await page.evaluate("globalThis.__mosage.preparePrint()");
|
|
914
|
+
try {
|
|
915
|
+
return await page.pdf({
|
|
916
|
+
printBackground: true,
|
|
917
|
+
preferCSSPageSize: true
|
|
918
|
+
});
|
|
919
|
+
} finally {
|
|
920
|
+
await page.evaluate("globalThis.__mosage.releasePrint()");
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
async screenshot(pageNumber) {
|
|
924
|
+
if (pageNumber < 1 || pageNumber > status.pageCount) throw new Error(`page ${pageNumber} is out of range — "${docId}" has ${status.pageCount}`);
|
|
925
|
+
await page.evaluate("globalThis.__mosage.preparePrint()");
|
|
926
|
+
await page.emulateMedia({ media: "print" });
|
|
927
|
+
try {
|
|
928
|
+
const sheet = await page.$(`${PRINT_PAGE_SELECTOR}:nth-child(${pageNumber})`);
|
|
929
|
+
if (!sheet) throw new Error(`page ${pageNumber} did not render`);
|
|
930
|
+
return await sheet.screenshot({ type: "png" });
|
|
931
|
+
} finally {
|
|
932
|
+
await page.emulateMedia({ media: null });
|
|
933
|
+
await page.evaluate("globalThis.__mosage.releasePrint()");
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
html: () => page.evaluate("globalThis.__mosage.htmlBundle()"),
|
|
937
|
+
docx: () => page.evaluate("globalThis.__mosage.docxBundle()"),
|
|
938
|
+
close: () => page.close()
|
|
939
|
+
};
|
|
940
|
+
},
|
|
941
|
+
async close() {
|
|
942
|
+
await browser.close();
|
|
943
|
+
await server?.close();
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region src/ops/layout.ts
|
|
949
|
+
/**
|
|
950
|
+
* Chromium and a Vite server are expensive to start, and an agent checking its
|
|
951
|
+
* work calls these back to back. One session is kept warm and closed once
|
|
952
|
+
* nobody has asked for a while.
|
|
953
|
+
*/
|
|
954
|
+
const IDLE_MS = 12e4;
|
|
955
|
+
let shared = null;
|
|
956
|
+
let pending = null;
|
|
957
|
+
let idleTimer = null;
|
|
958
|
+
let leases = 0;
|
|
959
|
+
function armIdleClose() {
|
|
960
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
961
|
+
idleTimer = setTimeout(() => {
|
|
962
|
+
if (leases > 0) return;
|
|
963
|
+
const current = shared;
|
|
964
|
+
shared = null;
|
|
965
|
+
idleTimer = null;
|
|
966
|
+
current?.session.close().catch(() => {});
|
|
967
|
+
}, IDLE_MS);
|
|
968
|
+
idleTimer.unref?.();
|
|
969
|
+
}
|
|
970
|
+
async function acquire(ctx) {
|
|
971
|
+
if (shared && shared.origin === ctx.serverOrigin) return shared.session;
|
|
972
|
+
if (!pending) {
|
|
973
|
+
const origin = ctx.serverOrigin;
|
|
974
|
+
pending = createRenderSession({
|
|
975
|
+
userCwd: ctx.userCwd,
|
|
976
|
+
origin,
|
|
977
|
+
deviceScaleFactor: 2
|
|
978
|
+
}).then((session) => {
|
|
979
|
+
shared = {
|
|
980
|
+
session,
|
|
981
|
+
origin
|
|
982
|
+
};
|
|
983
|
+
return session;
|
|
984
|
+
}).finally(() => {
|
|
985
|
+
pending = null;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return pending;
|
|
989
|
+
}
|
|
990
|
+
async function withDoc(ctx, docId, fn) {
|
|
991
|
+
if (!resolveEntry(ctx, docId)) throw new OpsError(404, `document not found: ${docId}`);
|
|
992
|
+
let session;
|
|
993
|
+
try {
|
|
994
|
+
session = await acquire(ctx);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
if (err instanceof RenderUnavailableError) throw new OpsError(501, err.message);
|
|
997
|
+
throw new OpsError(500, err.message);
|
|
998
|
+
}
|
|
999
|
+
leases++;
|
|
1000
|
+
let renderer = null;
|
|
1001
|
+
try {
|
|
1002
|
+
renderer = await session.open(docId);
|
|
1003
|
+
return await fn(renderer);
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (err instanceof OpsError) throw err;
|
|
1006
|
+
throw new OpsError(422, err.message);
|
|
1007
|
+
} finally {
|
|
1008
|
+
await renderer?.close().catch(() => {});
|
|
1009
|
+
leases--;
|
|
1010
|
+
armIdleClose();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/** Closes the warm browser. Call before the process exits so nothing lingers. */
|
|
1014
|
+
async function closeRenderSession() {
|
|
1015
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1016
|
+
idleTimer = null;
|
|
1017
|
+
const current = shared;
|
|
1018
|
+
shared = null;
|
|
1019
|
+
await current?.session.close().catch(() => {});
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Renders the document headlessly at true page size and reports what is wrong
|
|
1023
|
+
* with the layout — the part of a document an agent writing React cannot see.
|
|
1024
|
+
*/
|
|
1025
|
+
async function checkLayout(ctx, docId) {
|
|
1026
|
+
return withDoc(ctx, docId, async (renderer) => {
|
|
1027
|
+
const report = await renderer.diagnose();
|
|
1028
|
+
return {
|
|
1029
|
+
docId: report.docId || docId,
|
|
1030
|
+
title: report.title,
|
|
1031
|
+
pageCount: report.pageCount,
|
|
1032
|
+
errors: report.findings.filter((f) => f.severity === "error").length,
|
|
1033
|
+
warnings: report.findings.filter((f) => f.severity === "warn").length,
|
|
1034
|
+
findings: report.findings
|
|
1035
|
+
};
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
function resolveOutDir(ctx, outDir) {
|
|
1039
|
+
const resolved = path.resolve(ctx.userCwd, outDir);
|
|
1040
|
+
if (resolved !== ctx.userCwd && !resolved.startsWith(ctx.userCwd + path.sep)) throw new OpsError(400, `outDir must stay inside the workspace: ${outDir}`);
|
|
1041
|
+
return resolved;
|
|
1042
|
+
}
|
|
1043
|
+
async function exportDocument(ctx, docId, opts = {}) {
|
|
1044
|
+
const format = opts.format ?? "pdf";
|
|
1045
|
+
const dir = resolveOutDir(ctx, opts.outDir ?? "out");
|
|
1046
|
+
return withDoc(ctx, docId, async (renderer) => {
|
|
1047
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1048
|
+
const written = [];
|
|
1049
|
+
const write = async (name, bytes) => {
|
|
1050
|
+
const file = path.join(dir, name);
|
|
1051
|
+
await fs.writeFile(file, bytes);
|
|
1052
|
+
written.push(path.relative(ctx.userCwd, file));
|
|
1053
|
+
};
|
|
1054
|
+
if (format === "pdf") await write(`${docId}.pdf`, await renderer.pdf());
|
|
1055
|
+
else if (format === "html" || format === "docx") {
|
|
1056
|
+
const bundle = await renderer[format]();
|
|
1057
|
+
if (!bundle) throw new OpsError(422, `document has no pages: ${docId}`);
|
|
1058
|
+
await write(bundle.filename, Buffer.from(bundle.base64, "base64"));
|
|
1059
|
+
} else {
|
|
1060
|
+
const width = String(renderer.status.pageCount).length;
|
|
1061
|
+
for (let page = 1; page <= renderer.status.pageCount; page++) await write(`${docId}-${String(page).padStart(width, "0")}.png`, await renderer.screenshot(page));
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
docId,
|
|
1065
|
+
format,
|
|
1066
|
+
pageCount: renderer.status.pageCount,
|
|
1067
|
+
files: written
|
|
1068
|
+
};
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region src/cli/context.ts
|
|
1073
|
+
/** The same context the dev API runs on, for commands that call `ops/`. */
|
|
1074
|
+
async function cliContext(userCwd = process.cwd()) {
|
|
1075
|
+
const config = await loadUserConfig(userCwd);
|
|
1076
|
+
return makeContext({
|
|
1077
|
+
userCwd,
|
|
1078
|
+
docsDir: config.docsDir ?? "docs",
|
|
1079
|
+
assetsDir: config.assetsDir ?? "assets",
|
|
1080
|
+
coreVersion: await readCoreVersion()
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
//#endregion
|
|
1084
|
+
export { importMarkdown as a, exportDocument as i, checkLayout as n, closeRenderSession as r, cliContext as t };
|