@suzumiyaaoba/mdxr 0.13.0 → 0.14.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 +3 -0
- package/dist/cli.mjs +227 -77
- package/dist/components.d.mts +14 -13
- package/dist/components.mjs +1 -1
- package/dist/{config-BeI2zo52.mjs → config-DssZ4uXC.mjs} +1 -1
- package/dist/{doc-context-CWoaAPd4.d.mts → doc-context-rjANTj9L.d.mts} +3 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{ui-DkizsCxS.mjs → ui-DuJK74P7.mjs} +675 -202
- package/package.json +4 -3
- package/skill/SKILL.md +18 -1
- package/skill/references/components/investigation.md +1 -1
- package/skill/references/components.md +3 -3
- package/src/catalog.ts +1 -1
- package/src/cli.ts +2 -1
- package/src/components/ui/dialog.tsx +3 -1
- package/src/doc-context.ts +2 -0
- package/src/file-preview-data.ts +67 -0
- package/src/file-preview-http.ts +217 -0
- package/src/html.ts +25 -13
- package/src/hydrate-runtime.ts +12 -0
- package/src/hydrate.ts +6 -0
- package/src/mdx.ts +35 -2
- package/src/rehype/linked-assets.ts +1 -1
- package/src/rehype/shiki.ts +24 -7
- package/src/render.ts +19 -1
- package/src/serve.ts +19 -2
- package/src/ui/file-preview.tsx +444 -0
- package/src/ui/file-ref.tsx +78 -15
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Run directly with `npx` — no install required:
|
|
|
10
10
|
npx @suzumiyaaoba/mdxr render plan.mdx # writes plan.html
|
|
11
11
|
npx @suzumiyaaoba/mdxr serve plan.mdx # live preview at http://localhost:3737
|
|
12
12
|
npx @suzumiyaaoba/mdxr catalog # list available components
|
|
13
|
+
npx @suzumiyaaoba/mdxr --version # print the package version
|
|
13
14
|
npx @suzumiyaaoba/mdxr init # install mdxr instructions for Codex and Claude
|
|
14
15
|
```
|
|
15
16
|
|
|
@@ -76,6 +77,8 @@ mdxr init --skill # install the Agent Skill instead
|
|
|
76
77
|
|
|
77
78
|
The global install creates `$CODEX_HOME/MDXR.md` (or `~/.codex/MDXR.md` when `CODEX_HOME` is unset) and its `mdxr/references/`, plus matching files under `~/.claude/`. It adds a read instruction to `$CODEX_HOME/AGENTS.md`; Claude's `~/.claude/CLAUDE.md` imports `@./MDXR.md`. Use `--local` to create `.codex/MDXR.md` and `.claude/MDXR.md` in the project, with references from the root `AGENTS.md` and `CLAUDE.md`. A local install also adds `.mdxr/` to `.gitignore` for agent-authored documents and rendered HTML. `--force` refreshes generated instruction files and references while preserving the rest of your config files.
|
|
78
79
|
|
|
80
|
+
The bundled prompts tell the agent to compare their recorded version with `--version` from the CLI used for the task. If they differ, the agent reports both versions and recommends refreshing with `init --force` from that same CLI, preserving the original tool and install scope; Skill installs also keep `--skill`. See [Agent setup](docs/docs/agent-skill.mdx) for details.
|
|
81
|
+
|
|
79
82
|
## Wireframes
|
|
80
83
|
|
|
81
84
|
Build screen mockups with the built-in `Wireframe*` components, adapted from [wireframe-ui](https://wireframe-ui.vercel.app/components). All 45 upstream UI families and nine blocks are included, with their compound parts and wireframe helpers. No imports or extra installation are needed in MDX:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Bv as
|
|
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 fenceFilename, Dv as attrTrue, Fv as takeUsedIcons, Ha as shadcnModules, Hv as parseLineRange, Iv as DocContext, Jf as dependencyMatrixModel, Jv as isRecord, Kv as langForPath, M_ as parseDiff, Mh as parseSyntaxLines, Ov as numOf, Qg as calculate, Qv as urlScheme, Rv as parseProps, S_ as tableRecords, Uv as splitPathLines, Vv as fenceLang, Wv as SKIP_LANGS, Xf as diagramModel, Xv as own, Yf as queryPlanModel, Yv as nonEmpty, Zf as flameModel, Zv as safeHref, __ as isDataRecord, _v as textList, av as isStatus, b_ as readRecords, ch as PERFORMANCE_LABELS, dh as performanceDetails, fh as performanceResult, g_ as display, gm as PlanHeader, gv as numList, h_ as columnsOf, i_ as schemaRows, lh as PERFORMANCE_TARGET_SCHEMA, n_ as reportModel, nm as PLOT_NAMES, qf as bundleModel, qv as isComponent, r_ as comparisonModel, rm as plotModel, rv as STATUSES, sh as CLAIM_LABELS, t as builtinComponents, t_ as REPORT_SPECS, uh as RESEARCH_CLAIM_SCHEMA, v_ as numberValue, x_ as records, y_ as parseJson } from "./ui-DuJK74P7.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-DssZ4uXC.mjs";
|
|
4
4
|
import module from "node:module";
|
|
5
|
-
import fsp, { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import fsp, { access, mkdir, open, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { cac } from "cac";
|
|
8
8
|
import { directiveToMarkdown } from "mdast-util-directive";
|
|
@@ -20,7 +20,7 @@ import { unified } from "unified";
|
|
|
20
20
|
import { VFile } from "vfile";
|
|
21
21
|
import { matter } from "vfile-matter";
|
|
22
22
|
import { visit } from "unist-util-visit";
|
|
23
|
-
import fs, { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
23
|
+
import fs, { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
24
24
|
import { visitParents } from "unist-util-visit-parents";
|
|
25
25
|
import * as valibot from "valibot";
|
|
26
26
|
import { Fragment, createElement } from "react";
|
|
@@ -45,6 +45,9 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
45
45
|
import { connect } from "node:net";
|
|
46
46
|
import { WebSocket } from "ws";
|
|
47
47
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
48
|
+
//#region package.json
|
|
49
|
+
var version = "0.14.0";
|
|
50
|
+
//#endregion
|
|
48
51
|
//#region src/format-error.ts
|
|
49
52
|
/**
|
|
50
53
|
* Validate the `--format` flag: omitted and "text" mean human-readable
|
|
@@ -5582,7 +5585,7 @@ const CONVENTIONS = [
|
|
|
5582
5585
|
syntax: "```lang title=\"file.ts\""
|
|
5583
5586
|
},
|
|
5584
5587
|
{
|
|
5585
|
-
result: "FileRef chip (editor link
|
|
5588
|
+
result: "FileRef chip (local preview in serve; editor link in static HTML)",
|
|
5586
5589
|
syntax: "`src/x.ts` inline code naming a real file"
|
|
5587
5590
|
},
|
|
5588
5591
|
{
|
|
@@ -6110,19 +6113,21 @@ const iconSvg = (name) => {
|
|
|
6110
6113
|
* `handleDocEvent`); THEME_JS restores the stored choice before paint.
|
|
6111
6114
|
*/
|
|
6112
6115
|
const THEME_TOGGLE_HTML = `<button type="button" class="mdxr-theme" data-mdxr-theme data-mode="auto" title="Theme: auto" aria-label="Switch theme (current: auto)"><span class="mdxr-theme-i mdxr-theme-i-auto">${iconSvg("sun-moon")}</span><span class="mdxr-theme-i mdxr-theme-i-light">${iconSvg("sun")}</span><span class="mdxr-theme-i mdxr-theme-i-dark">${iconSvg("moon")}</span></button>`;
|
|
6113
|
-
const htmlDocument = (o) =>
|
|
6116
|
+
const htmlDocument = (o) => {
|
|
6117
|
+
const annotations = o.documentControls === false ? void 0 : o.annotations;
|
|
6118
|
+
return `<!doctype html>
|
|
6114
6119
|
<html lang="en"${o.initialTheme === void 0 ? "" : ` class="${o.initialTheme}" style="color-scheme: ${o.initialTheme}"`}>
|
|
6115
6120
|
<head>
|
|
6116
6121
|
<meta charset="utf-8">
|
|
6117
6122
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6118
6123
|
<meta name="generator" content="mdxr">
|
|
6119
6124
|
<title>${escapeHtml(o.title)}</title>
|
|
6120
|
-
|
|
6125
|
+
${o.documentControls === false ? "" : `<script>${inlineScript(THEME_JS)}<\/script>`}
|
|
6121
6126
|
${o.needsKatex === true ? `<link rel="stylesheet" href="${KATEX_CDN_URL}">` : ""}
|
|
6122
|
-
<style>${inlineStyle(o.css)}${
|
|
6127
|
+
<style>${inlineStyle(o.css)}${annotations === void 0 ? "" : ANNOTATION_HIGHLIGHT_CSS}</style>
|
|
6123
6128
|
</head>
|
|
6124
6129
|
<body class="bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
|
|
6125
|
-
${THEME_TOGGLE_HTML}
|
|
6130
|
+
${o.documentControls === false ? "" : `${THEME_TOGGLE_HTML}
|
|
6126
6131
|
<div class="mdxr-view-controls" role="group" aria-label="Document view" hidden>
|
|
6127
6132
|
<button type="button" data-mdxr-view="document" aria-pressed="true">${iconSvg("file-text")}<span>Document</span></button>
|
|
6128
6133
|
<button type="button" data-mdxr-view="pages" aria-pressed="false">${iconSvg("panel-left")}<span>Pages</span></button>
|
|
@@ -6131,16 +6136,16 @@ ${THEME_TOGGLE_HTML}
|
|
|
6131
6136
|
<p class="mdxr-pages-title">${escapeHtml(o.title)}</p>
|
|
6132
6137
|
<p data-section-review-summary role="status" hidden></p>
|
|
6133
6138
|
<div data-mdxr-page-tabs role="tablist" aria-label="Sections" aria-orientation="vertical"></div>
|
|
6134
|
-
</aside
|
|
6139
|
+
</aside>`}
|
|
6135
6140
|
<div id="mdxr-content">
|
|
6136
6141
|
<main id="mdxr-root" class="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-6 py-10">${o.body}</main>
|
|
6137
|
-
|
|
6142
|
+
${o.documentControls === false ? "" : `<nav class="mdxr-page-navigation" aria-label="Section navigation" hidden>
|
|
6138
6143
|
<button type="button" data-mdxr-page-previous>${iconSvg("chevron-left")}<span class="mdxr-page-navigation-labels"><span>Previous</span><span data-mdxr-page-previous-title hidden></span></span></button>
|
|
6139
6144
|
<button type="button" data-mdxr-page-next><span class="mdxr-page-navigation-labels"><span>Next</span><span data-mdxr-page-next-title hidden></span></span>${iconSvg("chevron-right")}</button>
|
|
6140
|
-
</nav
|
|
6145
|
+
</nav>`}
|
|
6141
6146
|
</div>
|
|
6142
|
-
${
|
|
6143
|
-
${
|
|
6147
|
+
${annotations === void 0 ? "" : `${annotationHtml(iconSvg)}<script type="application/json" id="mdxr-annotation-document">${JSON.stringify(annotations).replaceAll("<", "\\u003c")}<\/script>`}
|
|
6148
|
+
${annotations === void 0 ? "" : sectionReviewHtml(iconSvg)}
|
|
6144
6149
|
${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>`}
|
|
6145
6150
|
<script>${inlineScript(o.clientJs)}<\/script>
|
|
6146
6151
|
${o.needsMermaid ? `<script type="module">${inlineScript(MERMAID_JS)}<\/script>` : ""}
|
|
@@ -6149,6 +6154,7 @@ ${o.hydrateJs === void 0 ? "" : `<script>${inlineScript(o.hydrateJs)}<\/script>`
|
|
|
6149
6154
|
</body>
|
|
6150
6155
|
</html>
|
|
6151
6156
|
`;
|
|
6157
|
+
};
|
|
6152
6158
|
//#endregion
|
|
6153
6159
|
//#region src/hydrate/export-index.ts
|
|
6154
6160
|
/**
|
|
@@ -6617,6 +6623,8 @@ ${mountDocument}({
|
|
|
6617
6623
|
components: { ${entries.join(", ")} },
|
|
6618
6624
|
doc: __mdxrDoc,
|
|
6619
6625
|
fileLinks: ${JSON.stringify(spec.fileLinks)},
|
|
6626
|
+
filePreviews: ${JSON.stringify(spec.filePreviews)},
|
|
6627
|
+
filePreviewEnabled: ${JSON.stringify(spec.filePreviewEnabled)},
|
|
6620
6628
|
headerProps: ${JSON.stringify(spec.header)},
|
|
6621
6629
|
now: ${JSON.stringify(spec.now)},
|
|
6622
6630
|
planHeader: ${planHeader},
|
|
@@ -6808,47 +6816,6 @@ const rehypeLinkedAssets = (opts = {}) => (tree, file) => {
|
|
|
6808
6816
|
});
|
|
6809
6817
|
};
|
|
6810
6818
|
//#endregion
|
|
6811
|
-
//#region src/workspace-syntax.ts
|
|
6812
|
-
const ALLOWED_STYLE_PROPERTIES = [
|
|
6813
|
-
"--shiki-light",
|
|
6814
|
-
"--shiki-dark",
|
|
6815
|
-
"--shiki-light-font-style",
|
|
6816
|
-
"--shiki-dark-font-style",
|
|
6817
|
-
"--shiki-light-font-weight",
|
|
6818
|
-
"--shiki-dark-font-weight",
|
|
6819
|
-
"--shiki-light-text-decoration",
|
|
6820
|
-
"--shiki-dark-text-decoration"
|
|
6821
|
-
];
|
|
6822
|
-
const parseToken = (value) => {
|
|
6823
|
-
if (!isRecord(value) || typeof value.text !== "string" || !isRecord(value.style) || Array.isArray(value.style)) return;
|
|
6824
|
-
const style = {};
|
|
6825
|
-
for (const property of ALLOWED_STYLE_PROPERTIES) {
|
|
6826
|
-
if (!Object.hasOwn(value.style, property)) continue;
|
|
6827
|
-
const propertyValue = value.style[property];
|
|
6828
|
-
if (typeof propertyValue === "string") style[property] = propertyValue;
|
|
6829
|
-
}
|
|
6830
|
-
return {
|
|
6831
|
-
style,
|
|
6832
|
-
text: value.text
|
|
6833
|
-
};
|
|
6834
|
-
};
|
|
6835
|
-
/** Validate and narrow syntax tokens received through a workspace API. */
|
|
6836
|
-
const parseSyntaxLines = (value) => {
|
|
6837
|
-
if (!Array.isArray(value)) return;
|
|
6838
|
-
const lines = [];
|
|
6839
|
-
for (const lineValue of value) {
|
|
6840
|
-
if (!Array.isArray(lineValue)) return;
|
|
6841
|
-
const line = [];
|
|
6842
|
-
for (const tokenValue of lineValue) {
|
|
6843
|
-
const token = parseToken(tokenValue);
|
|
6844
|
-
if (token === void 0) return;
|
|
6845
|
-
line.push(token);
|
|
6846
|
-
}
|
|
6847
|
-
lines.push(line);
|
|
6848
|
-
}
|
|
6849
|
-
return lines;
|
|
6850
|
-
};
|
|
6851
|
-
//#endregion
|
|
6852
6819
|
//#region src/rehype/shiki.ts
|
|
6853
6820
|
/**
|
|
6854
6821
|
* Fenced code blocks are syntax-highlighted with shiki at render time.
|
|
@@ -6993,16 +6960,17 @@ const plainSyntaxLines = (source) => source.split("\n").map((text) => [{
|
|
|
6993
6960
|
style: {},
|
|
6994
6961
|
text
|
|
6995
6962
|
}]);
|
|
6996
|
-
|
|
6997
|
-
|
|
6963
|
+
const isBundledLanguage = (highlighter, lang) => Object.hasOwn(highlighter.getBundledLanguages(), lang);
|
|
6964
|
+
/** Tokenize source without interpreting Shiki's code-fence annotations. */
|
|
6965
|
+
const highlightSource = async (source, lang) => {
|
|
6998
6966
|
const normalizedSource = source.replaceAll("\r\n", "\n");
|
|
6999
6967
|
if (normalizedSource.includes("\r")) return plainSyntaxLines(normalizedSource);
|
|
7000
|
-
const highlighter = await readyHighlighter(
|
|
7001
|
-
if (highlighter === void 0) return plainSyntaxLines(normalizedSource);
|
|
6968
|
+
const highlighter = await readyHighlighter(lang);
|
|
6969
|
+
if (highlighter === void 0 || !isBundledLanguage(highlighter, lang)) return plainSyntaxLines(normalizedSource);
|
|
7002
6970
|
try {
|
|
7003
6971
|
const result = highlighter.codeToTokens(normalizedSource, {
|
|
7004
6972
|
defaultColor: false,
|
|
7005
|
-
lang
|
|
6973
|
+
lang,
|
|
7006
6974
|
themes: THEMES
|
|
7007
6975
|
});
|
|
7008
6976
|
return parseSyntaxLines(result.tokens.map((line) => line.map((token) => ({
|
|
@@ -7013,6 +6981,8 @@ const highlightMdxSource = async (source) => {
|
|
|
7013
6981
|
return plainSyntaxLines(normalizedSource);
|
|
7014
6982
|
}
|
|
7015
6983
|
};
|
|
6984
|
+
/** Tokenize a complete MDX document for the workspace source and diff views. */
|
|
6985
|
+
const highlightMdxSource = async (source) => await highlightSource(source, "mdx");
|
|
7016
6986
|
/**
|
|
7017
6987
|
* One side of a hunk, highlighted as the file's own language: `ctx`/`add`
|
|
7018
6988
|
* rows read from the new side, `ctx`/`del` rows from the old side. Shiki's
|
|
@@ -7371,6 +7341,14 @@ const extractUsedComponents = (code, components) => {
|
|
|
7371
7341
|
if (catalogKeys.has("wrapper")) used.add("wrapper");
|
|
7372
7342
|
return [...used];
|
|
7373
7343
|
};
|
|
7344
|
+
const createFilePreview = (dir, resolver, filePreviews) => {
|
|
7345
|
+
if (resolver === void 0) return;
|
|
7346
|
+
return (relPath) => {
|
|
7347
|
+
const url = resolver(path.resolve(dir, relPath));
|
|
7348
|
+
if (url !== void 0) filePreviews.set(relPath, url);
|
|
7349
|
+
return url;
|
|
7350
|
+
};
|
|
7351
|
+
};
|
|
7374
7352
|
const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {}) => {
|
|
7375
7353
|
const file = new VFile({
|
|
7376
7354
|
path: filePath,
|
|
@@ -7382,12 +7360,14 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7382
7360
|
const editor = typeof frontmatter.editor === "string" && frontmatter.editor !== "" ? frontmatter.editor : opts.editor;
|
|
7383
7361
|
const dir = file.dirname ?? ".";
|
|
7384
7362
|
const fileLinks = /* @__PURE__ */ new Map();
|
|
7363
|
+
const filePreviews = /* @__PURE__ */ new Map();
|
|
7385
7364
|
const fileLink = (rel, line) => {
|
|
7386
7365
|
const abs = path.resolve(dir, rel);
|
|
7387
7366
|
const url = existsSync(abs) ? editorUrl(editor, abs, line) : void 0;
|
|
7388
7367
|
if (url !== void 0) fileLinks.set(`${rel}\0${line ?? ""}`, url);
|
|
7389
7368
|
return url;
|
|
7390
7369
|
};
|
|
7370
|
+
const filePreview = createFilePreview(dir, opts.filePreview, filePreviews);
|
|
7391
7371
|
const compiled = await compile(file, {
|
|
7392
7372
|
baseUrl: import.meta.url,
|
|
7393
7373
|
format: "mdx",
|
|
@@ -7426,6 +7406,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7426
7406
|
const renderedAt = /* @__PURE__ */ new Date();
|
|
7427
7407
|
const context = {
|
|
7428
7408
|
fileLink,
|
|
7409
|
+
filePreview,
|
|
7429
7410
|
now: renderedAt
|
|
7430
7411
|
};
|
|
7431
7412
|
const hydrated = (opts.hydrate ?? true) && used.length > 0;
|
|
@@ -7444,6 +7425,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7444
7425
|
context,
|
|
7445
7426
|
dependencies: [...new Set(includeDependencies)],
|
|
7446
7427
|
fileLinks: Object.fromEntries(fileLinks),
|
|
7428
|
+
filePreviews: Object.fromEntries(filePreviews),
|
|
7447
7429
|
frontmatter,
|
|
7448
7430
|
hydrated,
|
|
7449
7431
|
linkedDocuments,
|
|
@@ -7452,6 +7434,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
|
|
|
7452
7434
|
const html = renderDocument(header);
|
|
7453
7435
|
return {
|
|
7454
7436
|
fileLinks: Object.fromEntries(fileLinks),
|
|
7437
|
+
filePreviews: Object.fromEntries(filePreviews),
|
|
7455
7438
|
html,
|
|
7456
7439
|
usedIcons: takeUsedIcons()
|
|
7457
7440
|
};
|
|
@@ -8082,6 +8065,8 @@ const buildHydrateBundle = async (args) => {
|
|
|
8082
8065
|
code: args.code,
|
|
8083
8066
|
componentsPath: args.config.componentsPath === void 0 ? void 0 : resolveModuleEntry(args.config.componentsPath),
|
|
8084
8067
|
fileLinks: args.fileLinks,
|
|
8068
|
+
filePreviewEnabled: args.filePreviewEnabled,
|
|
8069
|
+
filePreviews: args.filePreviews,
|
|
8085
8070
|
header: args.headerProps,
|
|
8086
8071
|
now: args.now,
|
|
8087
8072
|
usedComponents: args.usedComponents,
|
|
@@ -8131,8 +8116,9 @@ const renderDocument = async (source, opts, include = {}) => {
|
|
|
8131
8116
|
...builtinComponents,
|
|
8132
8117
|
...user.components
|
|
8133
8118
|
};
|
|
8134
|
-
const { annotationSources, body, code, dependencies: includeDependencies, fileLinks, frontmatter, linkedDocuments, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
|
|
8119
|
+
const { annotationSources, body, code, dependencies: includeDependencies, fileLinks, filePreviews, frontmatter, linkedDocuments, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
|
|
8135
8120
|
editor: config.editor,
|
|
8121
|
+
filePreview: opts.filePreview,
|
|
8136
8122
|
hydrate: opts.hydrate,
|
|
8137
8123
|
include
|
|
8138
8124
|
});
|
|
@@ -8148,11 +8134,13 @@ const renderDocument = async (source, opts, include = {}) => {
|
|
|
8148
8134
|
const headerProps = frontmatterHeader(fmStr, body);
|
|
8149
8135
|
let docBody = body;
|
|
8150
8136
|
let docFileLinks = fileLinks;
|
|
8137
|
+
let docFilePreviews = filePreviews;
|
|
8151
8138
|
let docIcons = usedIcons;
|
|
8152
8139
|
if (headerProps !== void 0) {
|
|
8153
8140
|
const pass = renderWithHeader(createElement(PlanHeader, headerProps));
|
|
8154
8141
|
docBody = pass.html;
|
|
8155
8142
|
docFileLinks = pass.fileLinks;
|
|
8143
|
+
docFilePreviews = pass.filePreviews;
|
|
8156
8144
|
docIcons = pass.usedIcons;
|
|
8157
8145
|
}
|
|
8158
8146
|
const themeCss = config.themePath === void 0 ? void 0 : await readFile(config.themePath, "utf-8");
|
|
@@ -8185,6 +8173,8 @@ const renderDocument = async (source, opts, include = {}) => {
|
|
|
8185
8173
|
code,
|
|
8186
8174
|
config,
|
|
8187
8175
|
fileLinks: docFileLinks,
|
|
8176
|
+
filePreviewEnabled: opts.filePreview !== void 0,
|
|
8177
|
+
filePreviews: docFilePreviews,
|
|
8188
8178
|
headerProps,
|
|
8189
8179
|
hydrate: opts.hydrate,
|
|
8190
8180
|
now: renderedAt,
|
|
@@ -8225,6 +8215,7 @@ const renderDocument = async (source, opts, include = {}) => {
|
|
|
8225
8215
|
body: docBody,
|
|
8226
8216
|
clientJs: js,
|
|
8227
8217
|
css,
|
|
8218
|
+
documentControls: opts.documentControls,
|
|
8228
8219
|
hydrateJs,
|
|
8229
8220
|
initialTheme: opts.initialTheme,
|
|
8230
8221
|
linkedDocuments: documents,
|
|
@@ -8235,7 +8226,7 @@ const renderDocument = async (source, opts, include = {}) => {
|
|
|
8235
8226
|
});
|
|
8236
8227
|
};
|
|
8237
8228
|
/** Render MDX source text to a standalone HTML document. */
|
|
8238
|
-
const render = async (source, opts = {}) => await renderDocument(source, opts);
|
|
8229
|
+
const render = async (source, opts = {}) => await renderDocument(source, opts, { inlineAssets: opts.inlineAssets });
|
|
8239
8230
|
/** Read `mdxPath` and render it to a standalone HTML document. */
|
|
8240
8231
|
const renderFile = async (mdxPath, opts = {}) => {
|
|
8241
8232
|
const abs = path.resolve(mdxPath);
|
|
@@ -8322,7 +8313,7 @@ const reply = (res, status, body, headers = {}) => {
|
|
|
8322
8313
|
});
|
|
8323
8314
|
res.end(JSON.stringify(body));
|
|
8324
8315
|
};
|
|
8325
|
-
const sameOrigin$
|
|
8316
|
+
const sameOrigin$3 = (req) => {
|
|
8326
8317
|
const port = req.socket.localPort;
|
|
8327
8318
|
const host = req.headers.host ?? "";
|
|
8328
8319
|
return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (req.headers.origin === void 0 || req.headers.origin === `http://${host}`);
|
|
@@ -8358,7 +8349,7 @@ const handleAgentRequest = async (req, res, agent, notify, beforeSend) => {
|
|
|
8358
8349
|
reply(res, 404, { error: "Agent chat is disabled" });
|
|
8359
8350
|
return;
|
|
8360
8351
|
}
|
|
8361
|
-
if (!sameOrigin$
|
|
8352
|
+
if (!sameOrigin$3(req)) {
|
|
8362
8353
|
reply(res, 403, { error: "Request origin is not allowed" });
|
|
8363
8354
|
return;
|
|
8364
8355
|
}
|
|
@@ -9567,7 +9558,7 @@ const parsePreviewTheme = (value) => {
|
|
|
9567
9558
|
if (value === null) return;
|
|
9568
9559
|
return value === "light" || value === "dark" ? value : null;
|
|
9569
9560
|
};
|
|
9570
|
-
const replyJson$
|
|
9561
|
+
const replyJson$2 = (response, status, body, headers = {}) => {
|
|
9571
9562
|
response.writeHead(status, {
|
|
9572
9563
|
"cache-control": "no-store",
|
|
9573
9564
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -9576,7 +9567,7 @@ const replyJson$1 = (response, status, body, headers = {}) => {
|
|
|
9576
9567
|
});
|
|
9577
9568
|
response.end(JSON.stringify(body));
|
|
9578
9569
|
};
|
|
9579
|
-
const sameOrigin$
|
|
9570
|
+
const sameOrigin$2 = (request) => {
|
|
9580
9571
|
const port = request.socket.localPort;
|
|
9581
9572
|
const host = request.headers.host ?? "";
|
|
9582
9573
|
return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
|
|
@@ -9591,7 +9582,7 @@ const replyDiff = async (response, history, from, to) => {
|
|
|
9591
9582
|
if (line.type !== "remove") afterSource.push(line.text);
|
|
9592
9583
|
}
|
|
9593
9584
|
const [before, after] = await Promise.all([highlightMdxSource(beforeSource.join("\n")), highlightMdxSource(afterSource.join("\n"))]);
|
|
9594
|
-
replyJson$
|
|
9585
|
+
replyJson$2(response, 200, {
|
|
9595
9586
|
from,
|
|
9596
9587
|
lines,
|
|
9597
9588
|
syntax: {
|
|
@@ -9604,7 +9595,7 @@ const replyDiff = async (response, history, from, to) => {
|
|
|
9604
9595
|
const replyPreview = async (response, history, filePath, id, requestedTheme) => {
|
|
9605
9596
|
const theme = parsePreviewTheme(requestedTheme);
|
|
9606
9597
|
if (theme === null) {
|
|
9607
|
-
replyJson$
|
|
9598
|
+
replyJson$2(response, 400, { error: "Invalid preview theme" });
|
|
9608
9599
|
return;
|
|
9609
9600
|
}
|
|
9610
9601
|
const source = await history.read(id);
|
|
@@ -9624,14 +9615,14 @@ const replyPreview = async (response, history, filePath, id, requestedTheme) =>
|
|
|
9624
9615
|
const replyHistoryView = async (response, history, filePath, params) => {
|
|
9625
9616
|
const view = params.get("view");
|
|
9626
9617
|
if (view === null) {
|
|
9627
|
-
replyJson$
|
|
9618
|
+
replyJson$2(response, 200, await history.list());
|
|
9628
9619
|
return;
|
|
9629
9620
|
}
|
|
9630
9621
|
const id = params.get("id");
|
|
9631
9622
|
if (view === "source" && id !== null) {
|
|
9632
9623
|
const source = await history.read(id);
|
|
9633
9624
|
const syntax = await highlightMdxSource(source);
|
|
9634
|
-
replyJson$
|
|
9625
|
+
replyJson$2(response, 200, {
|
|
9635
9626
|
id,
|
|
9636
9627
|
source,
|
|
9637
9628
|
syntax
|
|
@@ -9650,20 +9641,20 @@ const replyHistoryView = async (response, history, filePath, params) => {
|
|
|
9650
9641
|
await replyPreview(response, history, filePath, id, params.get("theme"));
|
|
9651
9642
|
return;
|
|
9652
9643
|
}
|
|
9653
|
-
replyJson$
|
|
9644
|
+
replyJson$2(response, 400, { error: "Invalid history view or version" });
|
|
9654
9645
|
};
|
|
9655
9646
|
/** Read-only endpoints for the local, token-free MDX version store. */
|
|
9656
9647
|
const handleDocumentHistoryRequest = async (request, response, history, filePath) => {
|
|
9657
9648
|
if (history === void 0 || filePath === void 0) {
|
|
9658
|
-
replyJson$
|
|
9649
|
+
replyJson$2(response, 404, { error: "Document history is unavailable" });
|
|
9659
9650
|
return;
|
|
9660
9651
|
}
|
|
9661
|
-
if (!sameOrigin$
|
|
9662
|
-
replyJson$
|
|
9652
|
+
if (!sameOrigin$2(request)) {
|
|
9653
|
+
replyJson$2(response, 403, { error: "Request origin is not allowed" });
|
|
9663
9654
|
return;
|
|
9664
9655
|
}
|
|
9665
9656
|
if (request.method !== "GET") {
|
|
9666
|
-
replyJson$
|
|
9657
|
+
replyJson$2(response, 405, { error: "Method not allowed" }, { allow: "GET" });
|
|
9667
9658
|
return;
|
|
9668
9659
|
}
|
|
9669
9660
|
const url = new URL(request.url ?? "", "http://localhost");
|
|
@@ -9672,10 +9663,157 @@ const handleDocumentHistoryRequest = async (request, response, history, filePath
|
|
|
9672
9663
|
} catch (error) {
|
|
9673
9664
|
const detail = formatError(error);
|
|
9674
9665
|
const missingVersion = error instanceof DocumentHistoryError && (error.kind === "invalid-version" || error.kind === "unknown-version");
|
|
9675
|
-
replyJson$
|
|
9666
|
+
replyJson$2(response, missingVersion ? 404 : 500, { error: detail });
|
|
9676
9667
|
}
|
|
9677
9668
|
};
|
|
9678
9669
|
//#endregion
|
|
9670
|
+
//#region src/file-preview-http.ts
|
|
9671
|
+
const MAX_TEXT_BYTES = 2097152;
|
|
9672
|
+
const MAX_IMAGE_BYTES = 33554432;
|
|
9673
|
+
const MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9674
|
+
".md",
|
|
9675
|
+
".markdown",
|
|
9676
|
+
".mdx",
|
|
9677
|
+
".mdown"
|
|
9678
|
+
]);
|
|
9679
|
+
var FilePreviewError = class extends Error {
|
|
9680
|
+
status;
|
|
9681
|
+
constructor(status, message) {
|
|
9682
|
+
super(message);
|
|
9683
|
+
this.name = "FilePreviewError";
|
|
9684
|
+
this.status = status;
|
|
9685
|
+
}
|
|
9686
|
+
};
|
|
9687
|
+
const replyJson$1 = (response, status, body) => {
|
|
9688
|
+
response.writeHead(status, {
|
|
9689
|
+
"cache-control": "no-store",
|
|
9690
|
+
"content-type": "application/json; charset=utf-8",
|
|
9691
|
+
"x-content-type-options": "nosniff",
|
|
9692
|
+
...status === 405 ? { allow: "GET" } : {}
|
|
9693
|
+
});
|
|
9694
|
+
response.end(JSON.stringify(body));
|
|
9695
|
+
};
|
|
9696
|
+
const sameOrigin$1 = (request) => {
|
|
9697
|
+
const port = request.socket.localPort;
|
|
9698
|
+
const host = request.headers.host ?? "";
|
|
9699
|
+
return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`) && request.headers["sec-fetch-site"] !== "cross-site";
|
|
9700
|
+
};
|
|
9701
|
+
const readPreviewFile = async (file, limit) => {
|
|
9702
|
+
if (await realpath(file.path) !== file.canonical) throw new FilePreviewError(403, "The referenced file has changed location");
|
|
9703
|
+
const handle = await open(file.canonical, "r");
|
|
9704
|
+
try {
|
|
9705
|
+
const stat = await handle.stat();
|
|
9706
|
+
if (!stat.isFile()) throw new FilePreviewError(415, "Only regular files can be previewed");
|
|
9707
|
+
if (stat.size > limit) throw new FilePreviewError(413, "This file is too large to preview. Open it in your editor.");
|
|
9708
|
+
return await handle.readFile();
|
|
9709
|
+
} finally {
|
|
9710
|
+
await handle.close();
|
|
9711
|
+
}
|
|
9712
|
+
};
|
|
9713
|
+
const decodeText = (contents) => {
|
|
9714
|
+
try {
|
|
9715
|
+
const source = new TextDecoder("utf-8", { fatal: true }).decode(contents);
|
|
9716
|
+
if (source.includes("\0")) throw new Error("Binary content");
|
|
9717
|
+
return source;
|
|
9718
|
+
} catch {
|
|
9719
|
+
throw new FilePreviewError(415, "This file is not UTF-8 text. Open it in your editor.");
|
|
9720
|
+
}
|
|
9721
|
+
};
|
|
9722
|
+
const previewData = async (file, theme) => {
|
|
9723
|
+
const extension = path.extname(file.path).toLowerCase();
|
|
9724
|
+
const mime = IMAGE_MIME_TYPES[extension];
|
|
9725
|
+
const contents = await readPreviewFile(file, mime === void 0 ? MAX_TEXT_BYTES : MAX_IMAGE_BYTES);
|
|
9726
|
+
if (mime !== void 0) return {
|
|
9727
|
+
kind: "image",
|
|
9728
|
+
src: `data:${mime};base64,${contents.toString("base64")}`
|
|
9729
|
+
};
|
|
9730
|
+
const source = decodeText(contents);
|
|
9731
|
+
const markdown = MARKDOWN_EXTENSIONS.has(extension);
|
|
9732
|
+
const lang = extension === ".mdx" ? "mdx" : langForPath(file.path);
|
|
9733
|
+
const syntax = await highlightSource(source, lang ?? (markdown ? "markdown" : "text"));
|
|
9734
|
+
if (!markdown) return {
|
|
9735
|
+
kind: "text",
|
|
9736
|
+
source,
|
|
9737
|
+
syntax
|
|
9738
|
+
};
|
|
9739
|
+
try {
|
|
9740
|
+
return {
|
|
9741
|
+
html: await render(source, {
|
|
9742
|
+
dir: path.dirname(file.path),
|
|
9743
|
+
documentControls: false,
|
|
9744
|
+
filePath: file.path,
|
|
9745
|
+
initialTheme: theme,
|
|
9746
|
+
inlineAssets: true
|
|
9747
|
+
}),
|
|
9748
|
+
kind: "markdown",
|
|
9749
|
+
source,
|
|
9750
|
+
syntax
|
|
9751
|
+
};
|
|
9752
|
+
} catch (error) {
|
|
9753
|
+
return {
|
|
9754
|
+
kind: "markdown",
|
|
9755
|
+
renderError: formatError(error),
|
|
9756
|
+
source,
|
|
9757
|
+
syntax
|
|
9758
|
+
};
|
|
9759
|
+
}
|
|
9760
|
+
};
|
|
9761
|
+
const replyError = (response, error) => {
|
|
9762
|
+
if (isRecord(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
9763
|
+
replyJson$1(response, 404, { error: "The referenced file no longer exists" });
|
|
9764
|
+
return;
|
|
9765
|
+
}
|
|
9766
|
+
const status = error instanceof FilePreviewError ? error.status : 500;
|
|
9767
|
+
replyJson$1(response, status, { error: formatError(error) });
|
|
9768
|
+
};
|
|
9769
|
+
/** Per-server capabilities, issued only for files a rendered FileRef uses. */
|
|
9770
|
+
const createFilePreviews = () => {
|
|
9771
|
+
const paths = /* @__PURE__ */ new Map();
|
|
9772
|
+
const files = /* @__PURE__ */ new Map();
|
|
9773
|
+
return {
|
|
9774
|
+
async handle(request, response) {
|
|
9775
|
+
if (!sameOrigin$1(request)) {
|
|
9776
|
+
replyJson$1(response, 403, { error: "Request origin is not allowed" });
|
|
9777
|
+
return;
|
|
9778
|
+
}
|
|
9779
|
+
if (request.method !== "GET") {
|
|
9780
|
+
replyJson$1(response, 405, { error: "Method not allowed" });
|
|
9781
|
+
return;
|
|
9782
|
+
}
|
|
9783
|
+
const params = new URL(request.url ?? "", "http://localhost").searchParams;
|
|
9784
|
+
const file = files.get(params.get("id") ?? "");
|
|
9785
|
+
if (file === void 0) {
|
|
9786
|
+
replyJson$1(response, 404, { error: "The referenced file is unavailable" });
|
|
9787
|
+
return;
|
|
9788
|
+
}
|
|
9789
|
+
try {
|
|
9790
|
+
replyJson$1(response, 200, await previewData(file, params.get("theme") === "dark" ? "dark" : "light"));
|
|
9791
|
+
} catch (error) {
|
|
9792
|
+
replyError(response, error);
|
|
9793
|
+
}
|
|
9794
|
+
},
|
|
9795
|
+
register: (filePath) => {
|
|
9796
|
+
try {
|
|
9797
|
+
const canonical = realpathSync(filePath);
|
|
9798
|
+
if (!statSync(canonical).isFile()) return;
|
|
9799
|
+
let file = paths.get(filePath);
|
|
9800
|
+
if (file === void 0 || file.canonical !== canonical) {
|
|
9801
|
+
file = {
|
|
9802
|
+
canonical,
|
|
9803
|
+
id: randomUUID(),
|
|
9804
|
+
path: filePath
|
|
9805
|
+
};
|
|
9806
|
+
paths.set(filePath, file);
|
|
9807
|
+
files.set(file.id, file);
|
|
9808
|
+
}
|
|
9809
|
+
return `/__mdxr_file?id=${file.id}`;
|
|
9810
|
+
} catch {
|
|
9811
|
+
return;
|
|
9812
|
+
}
|
|
9813
|
+
}
|
|
9814
|
+
};
|
|
9815
|
+
};
|
|
9816
|
+
//#endregion
|
|
9679
9817
|
//#region src/workspace-export-data.ts
|
|
9680
9818
|
/** Collect a complete, internally consistent archive of one served document. */
|
|
9681
9819
|
const createWorkspaceExportData = async (filePath, history, agent) => {
|
|
@@ -9894,6 +10032,10 @@ const servePreview = async (target, port) => {
|
|
|
9894
10032
|
};
|
|
9895
10033
|
target.agent?.setOnUpdate(notifyAgent);
|
|
9896
10034
|
const server = http.createServer((req, res) => {
|
|
10035
|
+
if (req.url?.split("?")[0] === "/__mdxr_file") {
|
|
10036
|
+
target.filePreviews.handle(req, res);
|
|
10037
|
+
return;
|
|
10038
|
+
}
|
|
9897
10039
|
if (req.url === "/__mdxr_workspace.js") {
|
|
9898
10040
|
if (target.history === void 0) {
|
|
9899
10041
|
res.writeHead(404);
|
|
@@ -10051,13 +10193,16 @@ const serve = async (mdxPath, port, opts = {}) => {
|
|
|
10051
10193
|
if (opts.server !== void 0 && opts.agent !== "codex") throw new Error("--server requires --agent codex");
|
|
10052
10194
|
const abs = path.resolve(mdxPath);
|
|
10053
10195
|
const history = createDocumentHistory(abs);
|
|
10196
|
+
const filePreviews = createFilePreviews();
|
|
10054
10197
|
const server = await servePreview({
|
|
10055
10198
|
agent: opts.agent === void 0 ? void 0 : createAgentSession(abs, opts.agent, opts.session, opts.server),
|
|
10056
10199
|
agentProvider: opts.agent,
|
|
10200
|
+
filePreviews,
|
|
10057
10201
|
history,
|
|
10058
10202
|
historyFile: abs,
|
|
10059
10203
|
label: mdxPath,
|
|
10060
10204
|
...trackDeps(async (onDeps) => await renderFile(abs, {
|
|
10205
|
+
filePreview: filePreviews.register,
|
|
10061
10206
|
liveReload: true,
|
|
10062
10207
|
onDependencies: onDeps
|
|
10063
10208
|
})),
|
|
@@ -10071,13 +10216,18 @@ const serve = async (mdxPath, port, opts = {}) => {
|
|
|
10071
10216
|
const serveSource = async (source, port, opts = {}) => {
|
|
10072
10217
|
if (opts.agent !== void 0 || opts.session !== void 0 || opts.server !== void 0) throw new Error("Agent chat requires an MDX file");
|
|
10073
10218
|
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
10219
|
+
const filePreviews = createFilePreviews();
|
|
10074
10220
|
const server = await servePreview({
|
|
10221
|
+
filePreviews,
|
|
10075
10222
|
label: opts.filePath ?? "stdin",
|
|
10076
10223
|
...trackDeps(async (onDeps) => await render(source, {
|
|
10077
10224
|
dir,
|
|
10225
|
+
documentControls: opts.documentControls,
|
|
10078
10226
|
filePath: opts.filePath,
|
|
10227
|
+
filePreview: filePreviews.register,
|
|
10079
10228
|
hydrate: opts.hydrate,
|
|
10080
10229
|
initialTheme: opts.initialTheme,
|
|
10230
|
+
inlineAssets: opts.inlineAssets,
|
|
10081
10231
|
liveReload: true,
|
|
10082
10232
|
onDependencies: onDeps
|
|
10083
10233
|
})),
|
|
@@ -10124,7 +10274,7 @@ const validateAgentOptions = (agent, session, server) => {
|
|
|
10124
10274
|
if (session !== void 0 && session.trim() === "") throw new Error("--session requires a non-empty ID");
|
|
10125
10275
|
return agent;
|
|
10126
10276
|
};
|
|
10127
|
-
const cli = cac("mdxr");
|
|
10277
|
+
const cli = cac("mdxr").version(version);
|
|
10128
10278
|
cli.command("render [file]", "Render an .mdx document to a standalone HTML file").option("-o, --out <path>", "Output path (default: <file>.html; stdout for stdin input or '-')").option("--format <format>", "Error output: text | json").option("--no-hydrate", "Emit static HTML without the client hydration bundle").option("--open", "Open the rendered file in the default browser").action(async (file, opts) => {
|
|
10129
10279
|
const json = opts.format === "json";
|
|
10130
10280
|
try {
|