@wgtechlabs/mdd-engine 0.1.0 → 0.1.1
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 +6 -2
- package/dist/index.js +30 -3
- package/dist/links.js +20 -25
- package/dist/markdown.js +5 -10
- package/dist/project.d.ts +0 -2
- package/dist/project.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The headless Markdown documentation compiler behind mdd. Give it a local project
|
|
|
4
4
|
|
|
5
5
|
Built with TypeScript and Bun. Runs on Node.js 22, 24, and 26 without Bun. The default is the latest Node LTS, currently pinned to **24.21.0**.
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> Build Flow's default channels publish development, PR, and manual preview packages alongside regular releases to npm and GitHub Packages. See [build channels and releasing](docs/RELEASING.md).
|
|
8
8
|
|
|
9
9
|
## A documentation project
|
|
10
10
|
|
|
@@ -38,7 +38,9 @@ All settings are optional. Custom paths are relative to `mdd/config.json` and mu
|
|
|
38
38
|
|
|
39
39
|
## Compile without a website
|
|
40
40
|
|
|
41
|
-
After
|
|
41
|
+
Install the regular package with `bun add @wgtechlabs/mdd-engine`. After a successful preview publication, use `bun add @wgtechlabs/mdd-engine@dev` for development builds or `bun add @wgtechlabs/mdd-engine@pr` for PRs targeting `dev`. Preview tags track the most recently published package in their channel; install an exact version to test a particular PR. You can also install a locally packed copy.
|
|
42
|
+
|
|
43
|
+
In your Node project:
|
|
42
44
|
|
|
43
45
|
```js
|
|
44
46
|
import { compileProject } from '@wgtechlabs/mdd-engine';
|
|
@@ -59,6 +61,8 @@ if (!result.site) {
|
|
|
59
61
|
|
|
60
62
|
`site` is absent whenever authoring errors exist. Diagnostics contain a stable code, severity, message, and source location where available. Unexpected filesystem failures reject the promise with context. Identical inputs produce identical output.
|
|
61
63
|
|
|
64
|
+
Excessive Markdown nesting that exceeds the runtime's call stack produces a `CONTENT_TOO_DEEP` diagnostic identifying the source file.
|
|
65
|
+
|
|
62
66
|
The result includes:
|
|
63
67
|
|
|
64
68
|
- `pages`: source, route, public URL, title, description, article HTML, readable Markdown, headings, and navigation metadata.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolveLinks } from "./links.js";
|
|
3
|
-
import { parseDocument, renderDocument } from "./markdown.js";
|
|
3
|
+
import { parseDocument, renderDocument, } from "./markdown.js";
|
|
4
4
|
import { loadProject, relativeSource } from "./project.js";
|
|
5
5
|
import { navigationFor, normalizeBasePath, publicUrl, routeFor, } from "./routes.js";
|
|
6
6
|
const reserved = new Set([
|
|
@@ -13,6 +13,18 @@ const reserved = new Set([
|
|
|
13
13
|
"robots.txt",
|
|
14
14
|
"404.html",
|
|
15
15
|
]);
|
|
16
|
+
function nestingDiagnostic(error, file) {
|
|
17
|
+
// V8 reports this error for excessive nesting in both parsing and rendering.
|
|
18
|
+
if (!(error instanceof RangeError) ||
|
|
19
|
+
error.message !== "Maximum call stack size exceeded")
|
|
20
|
+
throw error;
|
|
21
|
+
return {
|
|
22
|
+
severity: "error",
|
|
23
|
+
code: "CONTENT_TOO_DEEP",
|
|
24
|
+
message: "Markdown nesting is too deep; reduce nested blocks or formatting.",
|
|
25
|
+
file,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
16
28
|
/** Compile a local checkout; never fetch repositories, execute author code, or emit files. */
|
|
17
29
|
export async function compileProject(options) {
|
|
18
30
|
const diagnostics = [];
|
|
@@ -58,7 +70,15 @@ export async function compileProject(options) {
|
|
|
58
70
|
file: source,
|
|
59
71
|
});
|
|
60
72
|
routes.set(key, source);
|
|
61
|
-
const
|
|
73
|
+
const text = await readFile(file, "utf8");
|
|
74
|
+
let document;
|
|
75
|
+
try {
|
|
76
|
+
document = parseDocument(text, source, diagnostics);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
diagnostics.push(nestingDiagnostic(error, source));
|
|
80
|
+
return { diagnostics };
|
|
81
|
+
}
|
|
62
82
|
sources.push({ file, relative, route, document });
|
|
63
83
|
}
|
|
64
84
|
const assets = await resolveLinks(sources, project, basePath, diagnostics);
|
|
@@ -67,7 +87,14 @@ export async function compileProject(options) {
|
|
|
67
87
|
const pages = [];
|
|
68
88
|
for (const source of sources) {
|
|
69
89
|
const { document } = source;
|
|
70
|
-
|
|
90
|
+
let rendered;
|
|
91
|
+
try {
|
|
92
|
+
rendered = await renderDocument(document);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
diagnostics.push(nestingDiagnostic(error, relativeSource(project.root, source.file)));
|
|
96
|
+
return { diagnostics };
|
|
97
|
+
}
|
|
71
98
|
pages.push({
|
|
72
99
|
source: relativeSource(project.root, source.file),
|
|
73
100
|
route: source.route,
|
package/dist/links.js
CHANGED
|
@@ -60,8 +60,10 @@ export async function resolveLinks(pages, project, basePath, diagnostics) {
|
|
|
60
60
|
throw new AuthoringError("PATH_ESCAPE", "Local links must stay inside the content root");
|
|
61
61
|
let target = !decoded ? page : byFile.get(targetFile);
|
|
62
62
|
const extension = path.extname(decoded).toLowerCase();
|
|
63
|
-
let
|
|
63
|
+
let asset = decoded.endsWith("/") ? undefined : assets.get(targetFile);
|
|
64
|
+
let assetExists = asset !== undefined;
|
|
64
65
|
if (!target &&
|
|
66
|
+
!assetExists &&
|
|
65
67
|
!decoded.endsWith("/") &&
|
|
66
68
|
assetExtensions.has(extension)) {
|
|
67
69
|
try {
|
|
@@ -106,21 +108,23 @@ export async function resolveLinks(pages, project, basePath, diagnostics) {
|
|
|
106
108
|
if (isImage && [".pdf", ".txt"].includes(extension)) {
|
|
107
109
|
throw new AuthoringError("INVALID_IMAGE", "Images must reference a supported raster image");
|
|
108
110
|
}
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
if (!asset) {
|
|
112
|
+
if (!assetExists)
|
|
113
|
+
await safeFile(project.contentsRoot, targetFile);
|
|
114
|
+
const source = relativeSource(project.root, targetFile);
|
|
115
|
+
const destination = `_assets/${relativeSource(project.contentsRoot, targetFile)}`;
|
|
116
|
+
const key = destination.normalize("NFC").toLowerCase();
|
|
117
|
+
const existing = destinations.get(key);
|
|
118
|
+
if (existing && existing !== source)
|
|
119
|
+
throw new AuthoringError("ASSET_COLLISION", `Asset output collides with ${existing}`);
|
|
120
|
+
destinations.set(key, source);
|
|
121
|
+
asset = {
|
|
122
|
+
source,
|
|
123
|
+
destination,
|
|
124
|
+
url: publicUrl(basePath, encodePath(destination)),
|
|
125
|
+
};
|
|
126
|
+
assets.set(targetFile, asset);
|
|
127
|
+
}
|
|
124
128
|
node.url =
|
|
125
129
|
asset.url + query + (hash ? `#${encodeURIComponent(hash)}` : "");
|
|
126
130
|
}
|
|
@@ -139,15 +143,6 @@ export async function resolveLinks(pages, project, basePath, diagnostics) {
|
|
|
139
143
|
message: `Malformed URL encoding in '${url}'`,
|
|
140
144
|
...location,
|
|
141
145
|
});
|
|
142
|
-
else if (error instanceof Error &&
|
|
143
|
-
"code" in error &&
|
|
144
|
-
error.code === "ENOENT")
|
|
145
|
-
diagnostics.push({
|
|
146
|
-
severity: "error",
|
|
147
|
-
code: "MISSING_ASSET",
|
|
148
|
-
message: `Asset not found: '${url}'`,
|
|
149
|
-
...location,
|
|
150
|
-
});
|
|
151
146
|
else
|
|
152
147
|
throw new Error(`Unable to resolve '${url}' in ${location.file}`, {
|
|
153
148
|
cause: error,
|
package/dist/markdown.js
CHANGED
|
@@ -160,6 +160,10 @@ export function parseDocument(source, file, diagnostics) {
|
|
|
160
160
|
visit(tree, (node, index, parent) => {
|
|
161
161
|
if (node.type === "html") {
|
|
162
162
|
report("RAW_HTML", "Raw HTML is not supported; use Markdown or an mdd component.", node);
|
|
163
|
+
if (parent && index !== undefined) {
|
|
164
|
+
parent.children.splice(index, 1);
|
|
165
|
+
return [SKIP, index];
|
|
166
|
+
}
|
|
163
167
|
}
|
|
164
168
|
if ((node.type === "link" ||
|
|
165
169
|
node.type === "image" ||
|
|
@@ -238,17 +242,8 @@ export function parseDocument(source, file, diagnostics) {
|
|
|
238
242
|
}
|
|
239
243
|
export async function renderDocument(document) {
|
|
240
244
|
const htmlTree = await htmlRenderer.run(document.tree);
|
|
241
|
-
const markdownTree = structuredClone(document.tree);
|
|
242
|
-
visit(markdownTree, (node, index, parent) => {
|
|
243
|
-
if ((node.type === "html" || node.type === "yaml") &&
|
|
244
|
-
parent &&
|
|
245
|
-
index !== undefined) {
|
|
246
|
-
parent.children.splice(index, 1);
|
|
247
|
-
return [SKIP, index];
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
245
|
return {
|
|
251
246
|
html: String(htmlRenderer.stringify(htmlTree)),
|
|
252
|
-
markdown: String(markdownRenderer.stringify(
|
|
247
|
+
markdown: String(markdownRenderer.stringify(document.tree)),
|
|
253
248
|
};
|
|
254
249
|
}
|
package/dist/project.d.ts
CHANGED
package/dist/project.js
CHANGED
|
@@ -236,7 +236,7 @@ export async function loadProject(options, diagnostics) {
|
|
|
236
236
|
}
|
|
237
237
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error"))
|
|
238
238
|
return undefined;
|
|
239
|
-
return { root,
|
|
239
|
+
return { root, contentsRoot, title, theme, files };
|
|
240
240
|
}
|
|
241
241
|
catch (error) {
|
|
242
242
|
if (!(error instanceof AuthoringError))
|