@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.
Files changed (48) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +308 -38
  3. package/dist/components.d.mts +2 -2
  4. package/dist/components.mjs +2 -2
  5. package/dist/{config-C8uMevwJ.mjs → config-BeI2zo52.mjs} +1 -1
  6. package/dist/{doc-context-DAfx4O-r.d.mts → doc-context-DBRYOjsH.d.mts} +2 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/dist/{ui-4uP9HfXd.mjs → ui-DkizsCxS.mjs} +107 -20
  10. package/package.json +1 -1
  11. package/skill/SKILL.md +8 -7
  12. package/skill/references/components/document.md +1 -1
  13. package/skill/references/components/extended-diagrams.md +1 -1
  14. package/skill/references/components/output.md +1 -1
  15. package/skill/references/components/shadcn.md +2 -0
  16. package/skill/references/components.md +2 -0
  17. package/src/ascii/extended.ts +22 -0
  18. package/src/assets/css.ts +47 -1
  19. package/src/catalog.ts +5 -1
  20. package/src/cli.ts +14 -7
  21. package/src/client/diagram-viewports.ts +376 -0
  22. package/src/client/entry.ts +8 -0
  23. package/src/client/linked-documents.ts +127 -0
  24. package/src/client/pages.ts +8 -1
  25. package/src/client/workspace-snapshot.ts +15 -3
  26. package/src/extended/catalog.ts +1 -1
  27. package/src/html.ts +2 -0
  28. package/src/init.ts +39 -2
  29. package/src/mdx.ts +14 -3
  30. package/src/rehype/linked-assets.ts +174 -0
  31. package/src/remark/include.ts +122 -38
  32. package/src/render.ts +60 -6
  33. package/src/serve.ts +41 -4
  34. package/src/styles/globals.css +16 -0
  35. package/src/ui/changes.tsx +4 -1
  36. package/src/ui/cmd.tsx +3 -1
  37. package/src/ui/diagram-viewport.tsx +69 -0
  38. package/src/ui/document-references.tsx +24 -0
  39. package/src/ui/file-ref.tsx +4 -1
  40. package/src/ui/files.tsx +9 -4
  41. package/src/ui/graph-viewport.tsx +6 -2
  42. package/src/ui/index.ts +3 -0
  43. package/src/ui/plot-view.tsx +3 -2
  44. package/src/ui/pre.tsx +8 -5
  45. package/src/ui/symbol-ref.tsx +4 -1
  46. package/src/ui/tones.ts +5 -2
  47. package/src/workspace-export-html.ts +40 -23
  48. 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. Documents go in `.mdxr/` unless you specify another location.
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 { $g as REPORT_SPECS, A_ as parseDiff, Bv as parseLineRange, Dh as RESEARCH_CLAIM_SCHEMA, Eh as PERFORMANCE_TARGET_SCHEMA, Ev as numOf, Gv as isComponent, Ha as shadcnModules, Hv as SKIP_LANGS, Iv as parseProps, Jv as own, Kv as isRecord, Nm as PlanHeader, Nv as takeUsedIcons, Oh as performanceDetails, Pv as DocContext, Rv as fenceFilename, Th as PERFORMANCE_LABELS, Tv as attrTrue, Vv as splitPathLines, Wv as langForPath, Xg as calculate, Xv as urlScheme, Yv as safeHref, __ as parseJson, b_ as tableRecords, bm as plotModel, dp as dependencyMatrixModel, e_ as reportModel, fp as queryPlanModel, g_ as numberValue, h_ as isDataRecord, hv as textList, kh as performanceResult, m_ as display, mp as flameModel, mv as numList, n_ as schemaRows, p_ as columnsOf, pp as diagramModel, qv as nonEmpty, rv as isStatus, t as builtinComponents, t_ as comparisonModel, tv as STATUSES, up as bundleModel, v_ as readRecords, wh as CLAIM_LABELS, y_ as records, ym as PLOT_NAMES, zv as fenceLang } from "./ui-4uP9HfXd.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-C8uMevwJ.mjs";
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 { spawn } from "node:child_process";
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 isInclude = (node) => (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "name" in node && node.name === "Include";
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 includedDocument = (target, origin, chain, file) => {
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("Include requires path", target);
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
- const included = parser.parse(readFileSync(abs, "utf-8"));
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
- const tree = {
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
- path: source,
1341
+ document: id,
1342
+ label: jsxAttr(target, "label"),
1343
+ path: jsxAttr(target, "path"),
1319
1344
  section
1320
1345
  });
1321
- return {
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 rootFile = path.resolve(file.path || "document.mdx");
1329
- const root = path.dirname(rootFile);
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 (isInclude(child)) {
1338
- const included = includedDocument(child, origin, chain, file);
1339
- expand(included.tree, included.abs, [...chain, included.abs]);
1340
- child.children = included.tree.children;
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, path.dirname(origin), root);
1379
+ rebase(child, originDirectory, root);
1344
1380
  if (isParent(child)) expand(child, origin, chain);
1345
1381
  }
1346
1382
  };
1347
- if (isParent(tree)) expand(tree, rootFile, [rootFile]);
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 = ["Components:", ""];
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 already covers it.
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: [rehypeKatex, rehypeShiki],
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
- /** Render MDX source text to a standalone HTML document. */
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
- opts.onDependencies?.(dependencies);
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.listen(port, "127.0.0.1");
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>", "Port", { 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) => {
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(status === "added" ? "mdxr: added .mdxr/ to .gitignore" : "mdxr: .gitignore already covers .mdxr/");
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);