@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 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
- > Initial implementation. Publishing to npm and GitHub Packages is configured for eligible pushes to `main`. Verify the first release before using registry installation instructions. See [releasing](docs/RELEASING.md).
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 installing a locally packed copy of `@wgtechlabs/mdd-engine` in your Node project:
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 document = parseDocument(await readFile(file, "utf8"), source, diagnostics);
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
- const rendered = await renderDocument(document);
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 assetExists = false;
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 (!assetExists)
110
- await safeFile(project.contentsRoot, targetFile);
111
- const source = relativeSource(project.root, targetFile);
112
- const destination = `_assets/${relativeSource(project.contentsRoot, targetFile)}`;
113
- const key = destination.normalize("NFC").toLowerCase();
114
- const existing = destinations.get(key);
115
- if (existing && existing !== source)
116
- throw new AuthoringError("ASSET_COLLISION", `Asset output collides with ${existing}`);
117
- destinations.set(key, source);
118
- const asset = {
119
- source,
120
- destination,
121
- url: publicUrl(basePath, encodePath(destination)),
122
- };
123
- assets.set(source, asset);
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(markdownTree)),
247
+ markdown: String(markdownRenderer.stringify(document.tree)),
253
248
  };
254
249
  }
package/dist/project.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import type { CompileOptions, Diagnostic, Theme } from "./types.js";
2
2
  export interface Project {
3
3
  root: string;
4
- mddRoot: string;
5
4
  contentsRoot: string;
6
- themesRoot: string;
7
5
  title?: string;
8
6
  theme: Theme;
9
7
  files: string[];
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, mddRoot, contentsRoot, themesRoot, title, theme, files };
239
+ return { root, contentsRoot, title, theme, files };
240
240
  }
241
241
  catch (error) {
242
242
  if (!(error instanceof AuthoringError))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wgtechlabs/mdd-engine",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Headless Markdown documentation compiler for mdd",
5
5
  "type": "module",
6
6
  "license": "MIT",