@suzumiyaaoba/mdxr 0.10.0 → 0.12.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 +2 -2
- package/dist/cli.mjs +308 -38
- package/dist/components.d.mts +2 -2
- package/dist/components.mjs +2 -2
- package/dist/{config-C8uMevwJ.mjs → config-BeI2zo52.mjs} +1 -1
- package/dist/{doc-context-DAfx4O-r.d.mts → doc-context-DBRYOjsH.d.mts} +2 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{ui-4uP9HfXd.mjs → ui-DkizsCxS.mjs} +107 -20
- package/package.json +1 -1
- package/skill/SKILL.md +8 -7
- package/skill/references/components/document.md +1 -1
- package/skill/references/components/extended-diagrams.md +1 -1
- package/skill/references/components/output.md +1 -1
- package/skill/references/components/shadcn.md +2 -0
- package/skill/references/components.md +2 -0
- package/src/ascii/extended.ts +22 -0
- package/src/assets/css.ts +47 -1
- package/src/catalog.ts +5 -1
- package/src/cli.ts +14 -7
- package/src/client/diagram-viewports.ts +376 -0
- package/src/client/entry.ts +8 -0
- package/src/client/linked-documents.ts +127 -0
- package/src/client/pages.ts +8 -1
- package/src/client/workspace-snapshot.ts +15 -3
- package/src/extended/catalog.ts +1 -1
- package/src/html.ts +2 -0
- package/src/init.ts +39 -2
- package/src/mdx.ts +14 -3
- package/src/rehype/linked-assets.ts +174 -0
- package/src/remark/include.ts +122 -38
- package/src/render.ts +60 -6
- package/src/serve.ts +41 -4
- package/src/styles/globals.css +16 -0
- package/src/ui/changes.tsx +4 -1
- package/src/ui/cmd.tsx +3 -1
- package/src/ui/diagram-viewport.tsx +69 -0
- package/src/ui/document-references.tsx +24 -0
- package/src/ui/file-ref.tsx +4 -1
- package/src/ui/files.tsx +9 -4
- package/src/ui/graph-viewport.tsx +6 -2
- package/src/ui/index.ts +3 -0
- package/src/ui/plot-view.tsx +3 -2
- package/src/ui/pre.tsx +8 -5
- package/src/ui/symbol-ref.tsx +4 -1
- package/src/ui/tones.ts +5 -2
- package/src/workspace-export-html.ts +40 -23
- package/src/workspace-export.tsx +10 -11
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ mdxr render plan.mdx --format json # machine-readable errors
|
|
|
35
35
|
|
|
36
36
|
### `mdxr serve [file]`
|
|
37
37
|
|
|
38
|
-
Preview a document in the browser with live reload (`-p, --port`, default `3737`; `--open` to launch the browser once serving).
|
|
38
|
+
Preview a document in the browser with live reload (`-p, --port`, default `3737`; if taken, the next free port is used; `--open` to launch the browser once serving).
|
|
39
39
|
|
|
40
40
|
### Review the rendered document
|
|
41
41
|
|
|
@@ -107,7 +107,7 @@ Once the instructions or skill are installed, ask your agent, for example:
|
|
|
107
107
|
|
|
108
108
|
> Write an implementation plan for the auth feature and render it to HTML with mdxr.
|
|
109
109
|
|
|
110
|
-
Plans, reports, reviews, investigation summaries, release notes, and postmortems all work.
|
|
110
|
+
Plans, reports, reviews, investigation summaries, release notes, and postmortems all work. By default, new documents use `.mdxr/yyyyMMddhhmmss-<name>.mdx`, with a zero-padded 14-digit local creation time in 24-hour format (for example, `.mdxr/20260929140530-auth-plan.mdx`). A filename or file path you provide takes priority. If you specify only a directory, the document gets a timestamped filename there.
|
|
111
111
|
|
|
112
112
|
## License
|
|
113
113
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-
|
|
2
|
+
import { Bv as fenceLang, Dh as RESEARCH_CLAIM_SCHEMA, Dv as numOf, Eh as PERFORMANCE_TARGET_SCHEMA, Ev as attrTrue, Fv as DocContext, Gv as langForPath, Ha as shadcnModules, Hv as splitPathLines, Jv as nonEmpty, Kv as isComponent, Lv as parseProps, Nm as PlanHeader, Oh as performanceDetails, Pv as takeUsedIcons, Th as PERFORMANCE_LABELS, Uv as SKIP_LANGS, Vv as parseLineRange, Xv as safeHref, Yv as own, Zg as calculate, Zv as urlScheme, __ as numberValue, b_ as records, bm as plotModel, dp as dependencyMatrixModel, e_ as REPORT_SPECS, fp as queryPlanModel, g_ as isDataRecord, gv as textList, h_ as display, hv as numList, iv as isStatus, j_ as parseDiff, kh as performanceResult, m_ as columnsOf, mp as flameModel, n_ as comparisonModel, nv as STATUSES, pp as diagramModel, qv as isRecord, r_ as schemaRows, t as builtinComponents, t_ as reportModel, up as bundleModel, v_ as parseJson, wh as CLAIM_LABELS, x_ as tableRecords, y_ as readRecords, ym as PLOT_NAMES, zv as fenceFilename } from "./ui-DkizsCxS.mjs";
|
|
3
|
+
import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-BeI2zo52.mjs";
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
import fsp, { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
@@ -29,8 +29,9 @@ import { icons as icons$1 } from "@iconify-json/vscode-icons";
|
|
|
29
29
|
import { differenceInCalendarDays, isValid, parseISO } from "date-fns";
|
|
30
30
|
import { createHash, randomUUID } from "node:crypto";
|
|
31
31
|
import { build } from "esbuild";
|
|
32
|
+
import { execFile, spawn } from "node:child_process";
|
|
32
33
|
import os, { homedir } from "node:os";
|
|
33
|
-
import {
|
|
34
|
+
import { promisify } from "node:util";
|
|
34
35
|
import { once } from "node:events";
|
|
35
36
|
import { compile } from "@mdx-js/mdx";
|
|
36
37
|
import { renderToStaticMarkup, renderToString } from "react-dom/server";
|
|
@@ -290,7 +291,7 @@ const EXTENDED_FEATURES = [
|
|
|
290
291
|
},
|
|
291
292
|
{
|
|
292
293
|
category: "evidence",
|
|
293
|
-
components: ["Include"],
|
|
294
|
+
components: ["Include", "DocumentLink"],
|
|
294
295
|
name: "Include",
|
|
295
296
|
number: 9
|
|
296
297
|
},
|
|
@@ -1255,7 +1256,7 @@ const remarkNoJs = () => (tree, file) => {
|
|
|
1255
1256
|
//#region src/remark/include.ts
|
|
1256
1257
|
const parser = unified().use(remarkParse).use(remarkMdx).use(remarkFrontmatter).use(remarkGfm).use(remarkMath).use(remarkDirective);
|
|
1257
1258
|
const REMOTE = /^(?:[a-z][a-z\d+.-]*:|#|\/)/iu;
|
|
1258
|
-
const
|
|
1259
|
+
const isDocumentReference = (node) => (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "name" in node && (node.name === "Include" || node.name === "DocumentLink");
|
|
1259
1260
|
const sectionNodes = (nodes, section) => {
|
|
1260
1261
|
const slugFor = createHeadingSlugger();
|
|
1261
1262
|
const slugs = /* @__PURE__ */ new Map();
|
|
@@ -1299,52 +1300,90 @@ const rebase = (node, origin, root) => {
|
|
|
1299
1300
|
"href"
|
|
1300
1301
|
].includes(String(attr.name)) && typeof attr.value === "string") attr.value = relative(attr.value);
|
|
1301
1302
|
};
|
|
1302
|
-
const
|
|
1303
|
-
if (target.type === "mdxJsxTextElement") file.fail("Include must be used as a block element", target, "mdxr:include");
|
|
1303
|
+
const includePath = (target, origin, chain, file) => {
|
|
1304
1304
|
const source = jsxAttr(target, "path");
|
|
1305
|
-
if (source === void 0 || source === "") file.fail(
|
|
1305
|
+
if (source === void 0 || source === "") file.fail(`${target.name} requires path`, target);
|
|
1306
|
+
if (target.name === "DocumentLink" && ![".md", ".mdx"].includes(path.extname(source).toLowerCase())) file.fail("DocumentLink requires an .md or .mdx file", target, "mdxr:include");
|
|
1306
1307
|
const abs = realpathSync(path.resolve(path.dirname(origin), source));
|
|
1307
1308
|
if (new Set(chain).has(abs)) file.fail(`Include cycle: ${[...chain, abs].join(" → ")}`, target);
|
|
1308
|
-
|
|
1309
|
+
file.data.includeDependencies ??= [];
|
|
1310
|
+
file.data.includeDependencies.push(abs);
|
|
1311
|
+
return abs;
|
|
1312
|
+
};
|
|
1313
|
+
const includedDocument = (target, abs, parentFile) => {
|
|
1314
|
+
const file = new VFile({
|
|
1315
|
+
path: abs,
|
|
1316
|
+
value: readFileSync(abs, "utf-8")
|
|
1317
|
+
});
|
|
1318
|
+
const included = parser.parse(file);
|
|
1309
1319
|
remarkNoJs()(included, file);
|
|
1310
1320
|
remarkMdxrDirectives()(included, file);
|
|
1321
|
+
parentFile.messages.push(...file.messages);
|
|
1311
1322
|
const section = jsxAttr(target, "section");
|
|
1312
1323
|
const nodes = included.children.filter((node) => node.type !== "yaml");
|
|
1313
|
-
|
|
1324
|
+
return {
|
|
1314
1325
|
children: section === void 0 || section === "" ? nodes : sectionNodes(nodes, section),
|
|
1315
1326
|
type: "root"
|
|
1316
1327
|
};
|
|
1328
|
+
};
|
|
1329
|
+
const linkDocument = (target, abs, chain, file) => {
|
|
1330
|
+
file.data.linkedDocuments ??= [];
|
|
1331
|
+
const documents = file.data.linkedDocuments;
|
|
1332
|
+
const section = jsxAttr(target, "section");
|
|
1333
|
+
const id = `mdxr-document-${documents.length + 1}`;
|
|
1334
|
+
documents.push({
|
|
1335
|
+
ancestors: chain,
|
|
1336
|
+
id,
|
|
1337
|
+
path: abs,
|
|
1338
|
+
section
|
|
1339
|
+
});
|
|
1317
1340
|
target.attributes = jsxAttrs({
|
|
1318
|
-
|
|
1341
|
+
document: id,
|
|
1342
|
+
label: jsxAttr(target, "label"),
|
|
1343
|
+
path: jsxAttr(target, "path"),
|
|
1319
1344
|
section
|
|
1320
1345
|
});
|
|
1321
|
-
|
|
1322
|
-
abs,
|
|
1323
|
-
tree
|
|
1324
|
-
};
|
|
1346
|
+
target.children = [];
|
|
1325
1347
|
};
|
|
1326
|
-
const remarkInclude = () => (tree, file) => {
|
|
1348
|
+
const remarkInclude = (opts = {}) => (tree, file) => {
|
|
1327
1349
|
remarkNoJs()(tree, file);
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1350
|
+
const filename = path.resolve(file.path || "document.mdx");
|
|
1351
|
+
const rootFile = existsSync(filename) ? realpathSync(filename) : filename;
|
|
1352
|
+
const directory = path.dirname(filename);
|
|
1353
|
+
const root = existsSync(directory) ? realpathSync(directory) : directory;
|
|
1330
1354
|
const expand = (parent, origin, chain) => {
|
|
1331
1355
|
if (chain.length > 32) file.fail("Include: nesting exceeds 32 files");
|
|
1356
|
+
const originDirectory = origin === filename ? root : path.dirname(origin);
|
|
1332
1357
|
for (const child of parent.children) {
|
|
1333
1358
|
child.data = {
|
|
1334
1359
|
...child.data,
|
|
1335
1360
|
mdxrSourceFile: origin
|
|
1336
1361
|
};
|
|
1337
|
-
if (
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
child.
|
|
1362
|
+
if (isDocumentReference(child)) {
|
|
1363
|
+
if (child.name === "Include" && child.type === "mdxJsxTextElement") file.fail("Include must be used as a block element", child, "mdxr:include");
|
|
1364
|
+
const abs = includePath(child, origin, chain, file);
|
|
1365
|
+
if (child.name === "DocumentLink") {
|
|
1366
|
+
linkDocument(child, abs, chain, file);
|
|
1367
|
+
rebase(child, originDirectory, root);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
const included = includedDocument(child, abs, file);
|
|
1371
|
+
expand(included, abs, [...chain, abs]);
|
|
1372
|
+
child.attributes = jsxAttrs({
|
|
1373
|
+
path: jsxAttr(child, "path"),
|
|
1374
|
+
section: jsxAttr(child, "section")
|
|
1375
|
+
});
|
|
1376
|
+
child.children = included.children;
|
|
1341
1377
|
continue;
|
|
1342
1378
|
}
|
|
1343
|
-
rebase(child,
|
|
1379
|
+
rebase(child, originDirectory, root);
|
|
1344
1380
|
if (isParent(child)) expand(child, origin, chain);
|
|
1345
1381
|
}
|
|
1346
1382
|
};
|
|
1347
|
-
if (isParent(tree))
|
|
1383
|
+
if (isParent(tree)) {
|
|
1384
|
+
if (opts.section !== void 0) tree.children = [...tree.children.filter((node) => node.type === "yaml"), ...sectionNodes(tree.children.filter((node) => node.type !== "yaml"), opts.section)];
|
|
1385
|
+
expand(tree, filename, [...opts.ancestors ?? [], rootFile]);
|
|
1386
|
+
}
|
|
1348
1387
|
};
|
|
1349
1388
|
//#endregion
|
|
1350
1389
|
//#region src/remark/references.ts
|
|
@@ -2530,6 +2569,18 @@ Object.assign(extendedRenderers, {
|
|
|
2530
2569
|
Cite: { text: (node) => [link(attr(node, "href") ?? `#${attr(node, "source")}`, [txt(attr(node, "label") ?? attr(node, "source") ?? "")])] },
|
|
2531
2570
|
CrossRef: { text: (node, ctx) => [link(attr(node, "href") ?? `#${attr(node, "target")}`, (node.children?.length ?? 0) > 0 ? ctx.inline(node) : [txt(attr(node, "label") ?? attr(node, "target") ?? "")])] },
|
|
2532
2571
|
DependencyMatrix: { flow: (node) => recordsTable(dependencyMatrixModel(nodeRows(node)).rows) },
|
|
2572
|
+
DocumentLink: {
|
|
2573
|
+
flow: (node) => {
|
|
2574
|
+
const path = attr(node, "path") ?? "";
|
|
2575
|
+
const section = attr(node, "section");
|
|
2576
|
+
return [para([link(section === void 0 ? path : `${path}#${section}`, [txt(attr(node, "label") ?? path)])])];
|
|
2577
|
+
},
|
|
2578
|
+
text: (node) => {
|
|
2579
|
+
const path = attr(node, "path") ?? "";
|
|
2580
|
+
const section = attr(node, "section");
|
|
2581
|
+
return [link(section === void 0 ? path : `${path}#${section}`, [txt(attr(node, "label") ?? path)])];
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2533
2584
|
DocumentSearch: { flow: () => [para([txt("Search this document using your viewer’s Find command.")])] },
|
|
2534
2585
|
EntityRelations: { flow: dataBlock },
|
|
2535
2586
|
Flamegraph: { flow: (node) => {
|
|
@@ -5556,7 +5607,11 @@ const CONVENTIONS = [
|
|
|
5556
5607
|
}
|
|
5557
5608
|
];
|
|
5558
5609
|
const formatCatalog = (entries) => {
|
|
5559
|
-
const lines = [
|
|
5610
|
+
const lines = [
|
|
5611
|
+
"Components:",
|
|
5612
|
+
" Pick the most specific component for the job: a dedicated semantic component (<Steps>, <Tests>, <Endpoints>, <Findings>, …) beats a generic Card/Table/bullet list, which loses its badges, counts, and file links. shadcn/ui and Wireframe* entries are generic building blocks for content with no dedicated component.",
|
|
5613
|
+
""
|
|
5614
|
+
];
|
|
5560
5615
|
for (const e of entries) {
|
|
5561
5616
|
const props = Object.entries(e.props).map(([k, p]) => `${k}${p.required ? "" : "?"}: ${p.type}`).join(", ");
|
|
5562
5617
|
lines.push(` <${e.name}${props === "" ? "" : ` ${props}`}>`);
|
|
@@ -5599,11 +5654,32 @@ const docsDirIsIgnored = (source) => {
|
|
|
5599
5654
|
}
|
|
5600
5655
|
return ignored;
|
|
5601
5656
|
};
|
|
5657
|
+
const execFileAsync = promisify(execFile);
|
|
5658
|
+
/** `git config --get core.excludesFile`, or git's default global ignore. */
|
|
5659
|
+
const globalExcludesFile = async () => {
|
|
5660
|
+
const fallback = path.join(os.homedir(), ".config", "git", "ignore");
|
|
5661
|
+
try {
|
|
5662
|
+
const { stdout } = await execFileAsync("git", [
|
|
5663
|
+
"config",
|
|
5664
|
+
"--get",
|
|
5665
|
+
"core.excludesFile"
|
|
5666
|
+
]);
|
|
5667
|
+
const configured = stdout.trim();
|
|
5668
|
+
if (configured === "") return fallback;
|
|
5669
|
+
return configured.startsWith("~/") ? path.join(os.homedir(), configured.slice(2)) : configured;
|
|
5670
|
+
} catch {
|
|
5671
|
+
return fallback;
|
|
5672
|
+
}
|
|
5673
|
+
};
|
|
5602
5674
|
/**
|
|
5603
5675
|
* Ensures the project's .gitignore covers `.mdxr/` — created or appended
|
|
5604
|
-
* as needed. Returns "present" when an existing rule
|
|
5676
|
+
* as needed. Returns "present" when an existing project rule covers it,
|
|
5677
|
+
* "global" when the user's global excludes file already does (nothing is
|
|
5678
|
+
* written — the project file would just duplicate it).
|
|
5605
5679
|
*/
|
|
5606
5680
|
const ensureDocsDirIgnored = async (base) => {
|
|
5681
|
+
const globalIgnore = await fsp.readFile(await globalExcludesFile(), "utf-8").catch(() => null);
|
|
5682
|
+
if (globalIgnore !== null && docsDirIsIgnored(globalIgnore)) return "global";
|
|
5607
5683
|
const file = path.join(base, ".gitignore");
|
|
5608
5684
|
const existing = await fsp.readFile(file, "utf-8").catch(() => null);
|
|
5609
5685
|
if (existing !== null && docsDirIsIgnored(existing)) return "present";
|
|
@@ -6065,6 +6141,7 @@ ${THEME_TOGGLE_HTML}
|
|
|
6065
6141
|
</div>
|
|
6066
6142
|
${o.annotations === void 0 ? "" : `${annotationHtml(iconSvg)}<script type="application/json" id="mdxr-annotation-document">${JSON.stringify(o.annotations).replaceAll("<", "\\u003c")}<\/script>`}
|
|
6067
6143
|
${o.annotations === void 0 ? "" : sectionReviewHtml(iconSvg)}
|
|
6144
|
+
${o.linkedDocuments === void 0 || Object.keys(o.linkedDocuments).length === 0 ? "" : `<script type="application/json" id="mdxr-linked-documents">${JSON.stringify(o.linkedDocuments).replaceAll("<", "\\u003c")}<\/script>`}
|
|
6068
6145
|
<script>${inlineScript(o.clientJs)}<\/script>
|
|
6069
6146
|
${o.needsMermaid ? `<script type="module">${inlineScript(MERMAID_JS)}<\/script>` : ""}
|
|
6070
6147
|
${o.liveReload === true ? `<script>${inlineScript(LIVE_RELOAD_JS)}<\/script>` : ""}
|
|
@@ -6641,6 +6718,96 @@ const editorUrl = (editor, absPath, line) => {
|
|
|
6641
6718
|
return scheme !== void 0 && SCRIPTABLE.has(scheme) ? void 0 : url;
|
|
6642
6719
|
};
|
|
6643
6720
|
//#endregion
|
|
6721
|
+
//#region src/rehype/linked-assets.ts
|
|
6722
|
+
const IMAGE_MIME_TYPES = {
|
|
6723
|
+
".apng": "image/apng",
|
|
6724
|
+
".avif": "image/avif",
|
|
6725
|
+
".bmp": "image/bmp",
|
|
6726
|
+
".gif": "image/gif",
|
|
6727
|
+
".heic": "image/heic",
|
|
6728
|
+
".heif": "image/heif",
|
|
6729
|
+
".ico": "image/x-icon",
|
|
6730
|
+
".jpeg": "image/jpeg",
|
|
6731
|
+
".jpg": "image/jpeg",
|
|
6732
|
+
".jxl": "image/jxl",
|
|
6733
|
+
".png": "image/png",
|
|
6734
|
+
".svg": "image/svg+xml",
|
|
6735
|
+
".tif": "image/tiff",
|
|
6736
|
+
".tiff": "image/tiff",
|
|
6737
|
+
".webp": "image/webp"
|
|
6738
|
+
};
|
|
6739
|
+
const JSX_IMAGE_ATTRIBUTES = {
|
|
6740
|
+
Figure: ["src"],
|
|
6741
|
+
img: ["src"],
|
|
6742
|
+
video: ["poster"]
|
|
6743
|
+
};
|
|
6744
|
+
const REMOTE_OR_ROOTED = /^(?:[a-z][a-z\d+.-]*:|\/|#)/iu;
|
|
6745
|
+
const localAssetPath = (source) => {
|
|
6746
|
+
const query = source.indexOf("?");
|
|
6747
|
+
const hash = source.indexOf("#");
|
|
6748
|
+
let suffixStart = source.length;
|
|
6749
|
+
for (const index of [query, hash]) if (index >= 0) suffixStart = Math.min(suffixStart, index);
|
|
6750
|
+
const sourcePath = source.slice(0, suffixStart);
|
|
6751
|
+
try {
|
|
6752
|
+
return decodeURIComponent(sourcePath);
|
|
6753
|
+
} catch {
|
|
6754
|
+
return sourcePath;
|
|
6755
|
+
}
|
|
6756
|
+
};
|
|
6757
|
+
const withFragment = (source) => {
|
|
6758
|
+
const hash = source.indexOf("#");
|
|
6759
|
+
return hash === -1 ? "" : source.slice(hash);
|
|
6760
|
+
};
|
|
6761
|
+
const dataUrl = (source, node, file, root) => {
|
|
6762
|
+
if (source === "" || REMOTE_OR_ROOTED.test(source)) return;
|
|
6763
|
+
const assetPath = localAssetPath(source);
|
|
6764
|
+
const mime = IMAGE_MIME_TYPES[path.extname(assetPath).toLowerCase()];
|
|
6765
|
+
if (mime === void 0) return;
|
|
6766
|
+
let canonicalPath;
|
|
6767
|
+
let contents;
|
|
6768
|
+
try {
|
|
6769
|
+
canonicalPath = realpathSync(path.resolve(root, assetPath));
|
|
6770
|
+
contents = readFileSync(canonicalPath);
|
|
6771
|
+
} catch (error) {
|
|
6772
|
+
const code = isRecord(error) ? error.code : void 0;
|
|
6773
|
+
if (code === "ENOENT" || code === "ENOTDIR") file.fail(`Linked image not found: ${source}`, node, "mdxr:linked-assets");
|
|
6774
|
+
throw error;
|
|
6775
|
+
}
|
|
6776
|
+
file.data.includeDependencies ??= [];
|
|
6777
|
+
if (!file.data.includeDependencies.includes(canonicalPath)) file.data.includeDependencies.push(canonicalPath);
|
|
6778
|
+
return `data:${mime};base64,${contents.toString("base64")}${withFragment(source)}`;
|
|
6779
|
+
};
|
|
6780
|
+
const inlineHastMedia = (node, file, root) => {
|
|
6781
|
+
if (!isRecord(node)) return;
|
|
6782
|
+
const { properties, tagName, type } = node;
|
|
6783
|
+
if (type !== "element" || !isRecord(properties)) return;
|
|
6784
|
+
if (tagName !== "img" && tagName !== "video") return;
|
|
6785
|
+
const attribute = tagName === "img" ? "src" : "poster";
|
|
6786
|
+
const source = properties[attribute];
|
|
6787
|
+
if (typeof source !== "string") return;
|
|
6788
|
+
const inline = dataUrl(source, node, file, root);
|
|
6789
|
+
if (inline !== void 0) properties[attribute] = inline;
|
|
6790
|
+
};
|
|
6791
|
+
const inlineMdxMedia = (node, file, root) => {
|
|
6792
|
+
if (!isRecord(node) || node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement" || typeof node.name !== "string" || !Array.isArray(node.attributes)) return;
|
|
6793
|
+
const imageAttributes = own(JSX_IMAGE_ATTRIBUTES, node.name);
|
|
6794
|
+
if (imageAttributes === void 0) return;
|
|
6795
|
+
for (const attribute of node.attributes) {
|
|
6796
|
+
if (!isRecord(attribute) || typeof attribute.name !== "string" || !imageAttributes.includes(attribute.name) || typeof attribute.value !== "string") continue;
|
|
6797
|
+
const inline = dataUrl(attribute.value, node, file, root);
|
|
6798
|
+
if (inline !== void 0) attribute.value = inline;
|
|
6799
|
+
}
|
|
6800
|
+
};
|
|
6801
|
+
/** Inline local image assets used by a linked document's standalone HTML. */
|
|
6802
|
+
const rehypeLinkedAssets = (opts = {}) => (tree, file) => {
|
|
6803
|
+
if (opts.enabled !== true) return;
|
|
6804
|
+
const root = file.dirname ?? process.cwd();
|
|
6805
|
+
visit(tree, (node) => {
|
|
6806
|
+
inlineHastMedia(node, file, root);
|
|
6807
|
+
inlineMdxMedia(node, file, root);
|
|
6808
|
+
});
|
|
6809
|
+
};
|
|
6810
|
+
//#endregion
|
|
6644
6811
|
//#region src/workspace-syntax.ts
|
|
6645
6812
|
const ALLOWED_STYLE_PROPERTIES = [
|
|
6646
6813
|
"--shiki-light",
|
|
@@ -7224,14 +7391,18 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7224
7391
|
const compiled = await compile(file, {
|
|
7225
7392
|
baseUrl: import.meta.url,
|
|
7226
7393
|
format: "mdx",
|
|
7227
|
-
rehypePlugins: [
|
|
7394
|
+
rehypePlugins: [
|
|
7395
|
+
rehypeKatex,
|
|
7396
|
+
rehypeShiki,
|
|
7397
|
+
[rehypeLinkedAssets, { enabled: opts.include?.inlineAssets }]
|
|
7398
|
+
],
|
|
7228
7399
|
remarkPlugins: [
|
|
7229
7400
|
remarkFrontmatter,
|
|
7230
7401
|
remarkGfm,
|
|
7231
7402
|
remarkMath,
|
|
7232
7403
|
remarkDirective,
|
|
7233
7404
|
remarkMdxrDirectives,
|
|
7234
|
-
remarkInclude,
|
|
7405
|
+
[remarkInclude, opts.include ?? {}],
|
|
7235
7406
|
remarkMdxrAlerts,
|
|
7236
7407
|
remarkNoJs,
|
|
7237
7408
|
remarkMdxrHeadings,
|
|
@@ -7244,6 +7415,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7244
7415
|
remarkSectionReviews
|
|
7245
7416
|
]
|
|
7246
7417
|
});
|
|
7418
|
+
const { includeDependencies, linkedDocuments = [] } = compiled.data;
|
|
7247
7419
|
for (const m of compiled.messages) process.stderr.write(`mdxr: warning: ${formatError(m)}\n`);
|
|
7248
7420
|
const code = String(compiled);
|
|
7249
7421
|
const mod = await importBundledCode(code, "doc");
|
|
@@ -7270,9 +7442,11 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7270
7442
|
body,
|
|
7271
7443
|
code,
|
|
7272
7444
|
context,
|
|
7445
|
+
dependencies: [...new Set(includeDependencies)],
|
|
7273
7446
|
fileLinks: Object.fromEntries(fileLinks),
|
|
7274
7447
|
frontmatter,
|
|
7275
7448
|
hydrated,
|
|
7449
|
+
linkedDocuments,
|
|
7276
7450
|
renderWithHeader: (header) => {
|
|
7277
7451
|
try {
|
|
7278
7452
|
const html = renderDocument(header);
|
|
@@ -7306,8 +7480,54 @@ ul.contains-task-list { padding-left: 1.25rem; }
|
|
|
7306
7480
|
max-width: none !important;
|
|
7307
7481
|
}
|
|
7308
7482
|
.mdxr-graph-tools:focus-within { outline: 2px solid rgb(14 165 233); outline-offset: -2px; }
|
|
7483
|
+
/* Navigation is a progressive enhancement: no-JS documents keep native sizing
|
|
7484
|
+
* and scrolling. Transform only the outer SVG, never its icons/edge layers. */
|
|
7485
|
+
.mdxr-diagram-tools { display: none; }
|
|
7486
|
+
.mdxr-diagrams-ready .mdxr-graph-tools { display: none; }
|
|
7487
|
+
.mdxr-diagrams-ready .mdxr-diagram:has(.mdxr-diagram-canvas svg) > .mdxr-diagram-tools {
|
|
7488
|
+
display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;
|
|
7489
|
+
flex-wrap: wrap; padding: 0.375rem 0.75rem; color: rgb(82 82 82);
|
|
7490
|
+
border-bottom: 1px solid rgb(229 229 229); font-size: 0.75rem;
|
|
7491
|
+
}
|
|
7492
|
+
.mdxr-diagram-hint { flex: 1 1 12rem; }
|
|
7493
|
+
.mdxr-diagram-actions {
|
|
7494
|
+
display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0;
|
|
7495
|
+
min-width: 0; margin: 0; padding: 0; border: 0;
|
|
7496
|
+
}
|
|
7497
|
+
.mdxr-diagram-tools button {
|
|
7498
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
7499
|
+
width: 2rem; height: 2rem; padding: 0; border-radius: 0.375rem;
|
|
7500
|
+
background: transparent; color: inherit; cursor: pointer;
|
|
7501
|
+
}
|
|
7502
|
+
.mdxr-diagram-tools button:hover { background: rgb(229 229 229); }
|
|
7503
|
+
.mdxr-diagram-tools button:disabled { opacity: 0.35; cursor: default; }
|
|
7504
|
+
.mdxr-diagram-tools button:focus-visible, .mdxr-diagram-canvas:focus-visible {
|
|
7505
|
+
outline: 2px solid rgb(14 165 233); outline-offset: -2px;
|
|
7506
|
+
}
|
|
7507
|
+
.mdxr-diagram-tools svg { width: 1rem; height: 1rem; }
|
|
7508
|
+
.mdxr-diagram-tools output { min-width: 3rem; text-align: center; font-variant-numeric: tabular-nums; }
|
|
7509
|
+
.dark .mdxr-diagram-tools { color: rgb(163 163 163) !important; border-color: rgb(64 64 64) !important; }
|
|
7510
|
+
.dark .mdxr-diagram-tools button:hover { background: rgb(64 64 64); }
|
|
7511
|
+
.mdxr-diagrams-ready .mdxr-diagram-canvas:has(svg) {
|
|
7512
|
+
display: flex; align-items: safe center; justify-content: safe center;
|
|
7513
|
+
overflow: hidden; min-height: 12rem; max-height: 70vh;
|
|
7514
|
+
cursor: grab; touch-action: none;
|
|
7515
|
+
}
|
|
7516
|
+
.mdxr-diagram-canvas > svg, .mdxr-diagram-canvas > .mermaid { flex: none; }
|
|
7517
|
+
.mdxr-diagram-canvas > .mermaid { width: 100%; }
|
|
7518
|
+
.mdxr-diagrams-ready .mdxr-diagram-canvas[data-dragging] { cursor: grabbing; user-select: none; }
|
|
7519
|
+
.mdxr-diagram-canvas > svg, .mdxr-diagram-canvas > .mermaid > svg {
|
|
7520
|
+
transform: var(--mdxr-diagram-transform, none); transform-origin: 0 0;
|
|
7521
|
+
}
|
|
7522
|
+
.mdxr-diagram-canvas > .mermaid:has(svg) { overflow: visible; padding: 0; background: transparent; }
|
|
7309
7523
|
@media print {
|
|
7310
|
-
.mdxr-graph-tools { display: none; }
|
|
7524
|
+
.mdxr-graph-tools, .mdxr-diagram-tools { display: none !important; }
|
|
7525
|
+
.mdxr-diagram-canvas {
|
|
7526
|
+
overflow: visible !important; min-height: 0 !important; max-height: none !important;
|
|
7527
|
+
}
|
|
7528
|
+
.mdxr-diagram-canvas > svg, .mdxr-diagram-canvas > .mermaid > svg {
|
|
7529
|
+
transform: none !important; min-width: 0 !important; max-width: 100% !important;
|
|
7530
|
+
}
|
|
7311
7531
|
.mdxr-graph-scroll { overflow: visible; }
|
|
7312
7532
|
.mdxr-graph-view:has(.mdxr-graph-actual:checked) .mdxr-graph-image,
|
|
7313
7533
|
.mdxr-graph-view .mdxr-graph-image {
|
|
@@ -7900,8 +8120,7 @@ const frontmatterHeader = (fm, body) => {
|
|
|
7900
8120
|
version: fm("version")
|
|
7901
8121
|
};
|
|
7902
8122
|
};
|
|
7903
|
-
|
|
7904
|
-
const render = async (source, opts = {}) => {
|
|
8123
|
+
const renderDocument = async (source, opts, include = {}) => {
|
|
7905
8124
|
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
7906
8125
|
const filePath = path.resolve(dir, opts.filePath ?? "document.mdx");
|
|
7907
8126
|
const config = await loadConfig(dir);
|
|
@@ -7912,9 +8131,10 @@ const render = async (source, opts = {}) => {
|
|
|
7912
8131
|
...builtinComponents,
|
|
7913
8132
|
...user.components
|
|
7914
8133
|
};
|
|
7915
|
-
const { annotationSources, body, code, fileLinks, frontmatter, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
|
|
8134
|
+
const { annotationSources, body, code, dependencies: includeDependencies, fileLinks, frontmatter, linkedDocuments, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
|
|
7916
8135
|
editor: config.editor,
|
|
7917
|
-
hydrate: opts.hydrate
|
|
8136
|
+
hydrate: opts.hydrate,
|
|
8137
|
+
include
|
|
7918
8138
|
});
|
|
7919
8139
|
const fmStr = (key) => {
|
|
7920
8140
|
const val = frontmatter[key];
|
|
@@ -7960,7 +8180,7 @@ const render = async (source, opts = {}) => {
|
|
|
7960
8180
|
}]
|
|
7961
8181
|
];
|
|
7962
8182
|
const { css, dependencies } = await buildCss(sources, config.themePath);
|
|
7963
|
-
|
|
8183
|
+
const documentDependencies = /* @__PURE__ */ new Set([...dependencies, ...includeDependencies]);
|
|
7964
8184
|
const [js, hydrateJs] = await Promise.all([clientJs(), buildHydrateBundle({
|
|
7965
8185
|
code,
|
|
7966
8186
|
config,
|
|
@@ -7971,6 +8191,30 @@ const render = async (source, opts = {}) => {
|
|
|
7971
8191
|
usedComponents,
|
|
7972
8192
|
usedIcons: docIcons
|
|
7973
8193
|
})]);
|
|
8194
|
+
const documents = {};
|
|
8195
|
+
const sourceDirectory = path.dirname(filePath);
|
|
8196
|
+
const documentBase = existsSync(sourceDirectory) ? realpathSync(sourceDirectory) : sourceDirectory;
|
|
8197
|
+
for (const linked of linkedDocuments) {
|
|
8198
|
+
const linkedSource = await readFile(linked.path, "utf-8");
|
|
8199
|
+
const html = await renderDocument(linkedSource, {
|
|
8200
|
+
...opts,
|
|
8201
|
+
dir,
|
|
8202
|
+
filePath: linked.path,
|
|
8203
|
+
liveReload: false,
|
|
8204
|
+
onDependencies: (paths) => {
|
|
8205
|
+
for (const dependency of paths) documentDependencies.add(dependency);
|
|
8206
|
+
}
|
|
8207
|
+
}, {
|
|
8208
|
+
ancestors: linked.ancestors,
|
|
8209
|
+
inlineAssets: true,
|
|
8210
|
+
section: linked.section
|
|
8211
|
+
});
|
|
8212
|
+
documents[linked.id] = {
|
|
8213
|
+
html,
|
|
8214
|
+
path: path.relative(documentBase, linked.path).split(path.sep).map(encodeURIComponent).join("/")
|
|
8215
|
+
};
|
|
8216
|
+
}
|
|
8217
|
+
opts.onDependencies?.([...documentDependencies]);
|
|
7974
8218
|
return htmlDocument({
|
|
7975
8219
|
annotations: {
|
|
7976
8220
|
file: filePath,
|
|
@@ -7983,12 +8227,15 @@ const render = async (source, opts = {}) => {
|
|
|
7983
8227
|
css,
|
|
7984
8228
|
hydrateJs,
|
|
7985
8229
|
initialTheme: opts.initialTheme,
|
|
8230
|
+
linkedDocuments: documents,
|
|
7986
8231
|
liveReload: opts.liveReload,
|
|
7987
8232
|
needsKatex: /class="[^"]*katex/u.test(docBody),
|
|
7988
8233
|
needsMermaid: /class="[^"]*mermaid/u.test(docBody),
|
|
7989
8234
|
title
|
|
7990
8235
|
});
|
|
7991
8236
|
};
|
|
8237
|
+
/** Render MDX source text to a standalone HTML document. */
|
|
8238
|
+
const render = async (source, opts = {}) => await renderDocument(source, opts);
|
|
7992
8239
|
/** Read `mdxPath` and render it to a standalone HTML document. */
|
|
7993
8240
|
const renderFile = async (mdxPath, opts = {}) => {
|
|
7994
8241
|
const abs = path.resolve(mdxPath);
|
|
@@ -9526,6 +9773,26 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
9526
9773
|
"node_modules",
|
|
9527
9774
|
"storybook-static"
|
|
9528
9775
|
]);
|
|
9776
|
+
/** Upper bound on consecutive EADDRINUSE retries before give-up. */
|
|
9777
|
+
const MAX_PORT_ATTEMPTS = 100;
|
|
9778
|
+
/**
|
|
9779
|
+
* Bind `server` on loopback to `port`, or to the next free port after it —
|
|
9780
|
+
* a probe-then-bind helper would just race the real listen. Port 0 already
|
|
9781
|
+
* asks the OS for a port, so it binds once and never retries.
|
|
9782
|
+
*/
|
|
9783
|
+
const listenOnFreePort = async (server, port) => {
|
|
9784
|
+
for (let attempt = 0;; attempt += 1) {
|
|
9785
|
+
const candidate = port + attempt;
|
|
9786
|
+
try {
|
|
9787
|
+
server.listen(candidate, "127.0.0.1");
|
|
9788
|
+
await once(server, "listening");
|
|
9789
|
+
if (attempt > 0) console.log(`mdxr: port ${port} is taken, using ${candidate}`);
|
|
9790
|
+
return;
|
|
9791
|
+
} catch (error) {
|
|
9792
|
+
if (!(error instanceof Error && error.code === "EADDRINUSE") || port === 0 || attempt >= MAX_PORT_ATTEMPTS || candidate >= 65535) throw error;
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
};
|
|
9529
9796
|
const handleDocumentResponse = (req, res, target, html) => {
|
|
9530
9797
|
if (req.url?.startsWith("/__mdxr_history") === true) {
|
|
9531
9798
|
handleDocumentHistoryRequest(req, res, target.history, target.historyFile);
|
|
@@ -9741,8 +10008,7 @@ const servePreview = async (target, port) => {
|
|
|
9741
10008
|
reloading = rebuild(notify);
|
|
9742
10009
|
await reloading;
|
|
9743
10010
|
try {
|
|
9744
|
-
server
|
|
9745
|
-
await once(server, "listening");
|
|
10011
|
+
await listenOnFreePort(server, port);
|
|
9746
10012
|
} catch (error) {
|
|
9747
10013
|
server.close();
|
|
9748
10014
|
throw error;
|
|
@@ -9887,7 +10153,7 @@ cli.command("render [file]", "Render an .mdx document to a standalone HTML file"
|
|
|
9887
10153
|
fail(error, json);
|
|
9888
10154
|
}
|
|
9889
10155
|
});
|
|
9890
|
-
cli.command("serve [file]", "Preview a document in the browser with live reload").option("-p, --port <port>", "
|
|
10156
|
+
cli.command("serve [file]", "Preview a document in the browser with live reload").option("-p, --port <port>", "Preferred port; if taken, the next free port is used", { default: 3737 }).option("--open", "Open the preview in the default browser once serving").option("--agent <agent>", "Chat with codex or claude in the preview").option("--session <id>", "Send to an existing Codex thread").option("--server <url>", "Codex App Server ws:// or unix:// endpoint").action(async (file, opts) => {
|
|
9891
10157
|
try {
|
|
9892
10158
|
const port = Number(opts.port);
|
|
9893
10159
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`invalid --port: ${opts.port}`);
|
|
@@ -9966,7 +10232,11 @@ cli.command("init", "Install mdxr instructions for Codex and Claude").option("--
|
|
|
9966
10232
|
}
|
|
9967
10233
|
if (!global) {
|
|
9968
10234
|
const status = await ensureDocsDirIgnored(process.cwd());
|
|
9969
|
-
console.log(
|
|
10235
|
+
console.log({
|
|
10236
|
+
added: "mdxr: added .mdxr/ to .gitignore",
|
|
10237
|
+
global: "mdxr: .mdxr/ is already covered by your global git excludes",
|
|
10238
|
+
present: "mdxr: .gitignore already covers .mdxr/"
|
|
10239
|
+
}[status]);
|
|
9970
10240
|
}
|
|
9971
10241
|
} catch (error) {
|
|
9972
10242
|
fail(error, false);
|