@ox-content/vite-plugin 3.0.0-alpha.13 → 3.0.0-alpha.15

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/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_vitepress = require("./vitepress.cjs");
3
+ const require_theme_tokens = require("./theme-tokens.cjs");
3
4
  const require_jsx_html = require("./jsx-html.cjs");
4
5
  const require_markdown_tables = require("./markdown-tables.cjs");
5
6
  let path = require("path");
@@ -686,8 +687,14 @@ let missingWarned = false;
686
687
  /**
687
688
  * Replaces rust `ox-math` placeholders with static KaTeX HTML.
688
689
  * Leaves the escaped TeX fallback when `katex` is not installed.
690
+ *
691
+ * `onError` decides what a run KaTeX cannot parse becomes. Prose that
692
+ * quotes math syntax is picked up as math by the `$…$` heuristics, and the
693
+ * default — putting the source back the way it was written — keeps that
694
+ * page readable instead of stamping red error text into the middle of a
695
+ * sentence. Failures are collected either way, so the caller can warn.
689
696
  */
690
- async function renderKatexMath(html) {
697
+ async function renderKatexMath(html, onError = "literal", failures) {
691
698
  if (!html.includes("data-ox-tex")) return html;
692
699
  const katex = loadKatex();
693
700
  if (!katex) {
@@ -695,14 +702,39 @@ async function renderKatexMath(html) {
695
702
  return html;
696
703
  }
697
704
  return html.replace(MATH_TAG, (_match, tag, kind, encoded) => {
698
- return `<${tag} class="ox-math ox-math-${kind}">${katex.renderToString(decodeHtmlAttr$3(encoded), {
699
- displayMode: kind === "block",
700
- throwOnError: false,
705
+ const block = kind === "block";
706
+ const tex = decodeHtmlAttr$3(encoded);
707
+ const options = {
708
+ displayMode: block,
701
709
  trust: false,
702
710
  output: "htmlAndMathml"
703
- })}</${tag}>`;
711
+ };
712
+ let rendered;
713
+ try {
714
+ rendered = katex.renderToString(tex, {
715
+ ...options,
716
+ throwOnError: true
717
+ });
718
+ } catch (error) {
719
+ const message = error instanceof Error ? error.message : String(error);
720
+ failures?.push({
721
+ tex,
722
+ block,
723
+ message
724
+ });
725
+ if (onError === "error") throw new Error(`${message} (in ${block ? "$$" : "$"}${tex}${block ? "$$" : "$"})`);
726
+ if (onError === "literal") return escapeHtmlText(block ? `$$${tex}$$` : `$${tex}$`);
727
+ rendered = katex.renderToString(tex, {
728
+ ...options,
729
+ throwOnError: false
730
+ });
731
+ }
732
+ return `<${tag} class="ox-math ox-math-${kind}">${rendered}</${tag}>`;
704
733
  });
705
734
  }
735
+ function escapeHtmlText(value) {
736
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
737
+ }
706
738
  /** Directory that contains `katex.min.css` and `fonts/`, or `null`. */
707
739
  function resolveKatexDist() {
708
740
  for (const resolver of createKatexResolvers()) try {
@@ -727,8 +759,34 @@ function createKatexResolvers() {
727
759
  resolvers.push((0, node_module.createRequire)(require("url").pathToFileURL(__filename).href));
728
760
  return resolvers;
729
761
  }
762
+ const NAMED_ENTITIES$1 = {
763
+ amp: "&",
764
+ quot: "\"",
765
+ apos: "'",
766
+ lt: "<",
767
+ gt: ">",
768
+ nbsp: "\xA0"
769
+ };
770
+ /**
771
+ * Decodes the attribute back to the TeX the author wrote.
772
+ *
773
+ * The placeholder is escaped by Rust and re-serialized by the rehype passes
774
+ * that run before this one, and those two do not agree on a spelling — an
775
+ * apostrophe leaves Rust untouched and comes back from rehype as `&#x27;`.
776
+ * A general decoder covers whichever spelling arrives; chained `replaceAll`
777
+ * calls over a fixed list did not, and the leftover `&#x27;` reached KaTeX
778
+ * as five literal characters.
779
+ *
780
+ * One left-to-right pass, so `&amp;lt;` decodes to `&lt;` rather than `<`.
781
+ */
730
782
  function decodeHtmlAttr$3(value) {
731
- return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
783
+ return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body) => {
784
+ if (body.startsWith("#")) {
785
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
786
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : match;
787
+ }
788
+ return NAMED_ENTITIES$1[body.toLowerCase()] ?? match;
789
+ });
732
790
  }
733
791
  function warnMissingKatexOnce() {
734
792
  if (missingWarned) return;
@@ -1070,12 +1128,27 @@ function citationKeysFromMarkdown(markdown, options, diagnostics) {
1070
1128
  for (const match of visibleMarkdown.matchAll(BRACKET_RE)) {
1071
1129
  const body = (match[1] ?? "").trim();
1072
1130
  if (!body.startsWith("@") && !body.startsWith("-@")) continue;
1131
+ if (isLinkLabel(visibleMarkdown, match)) continue;
1073
1132
  const parsed = parseCitationGroup(body, options, diagnostics);
1074
1133
  if (!parsed) continue;
1075
1134
  keys.push(...parsed.map((citation) => citation.key));
1076
1135
  }
1077
1136
  return keys;
1078
1137
  }
1138
+ /**
1139
+ * A bracketed span that a destination or reference immediately follows is a
1140
+ * Markdown link label, not a citation group.
1141
+ *
1142
+ * The HTML pass never sees these — by then the label is inside an `<a>` and has
1143
+ * no brackets left — but the search-index pass reads the Markdown source, where
1144
+ * a linked scoped package name is indistinguishable from a citation by its
1145
+ * opening `@` alone. `[@ox-content/vite-plugin](./packages/vite-plugin.md)`
1146
+ * reported a malformed citation and failed the whole index.
1147
+ */
1148
+ function isLinkLabel(markdown, match) {
1149
+ const end = (match.index ?? 0) + match[0].length;
1150
+ return markdown[end] === "(" || markdown[end] === "[";
1151
+ }
1079
1152
  function stripMarkdownCitationProtectedText(markdown) {
1080
1153
  return markdown.replace(/^([ \t]*)(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1\2[ \t]*$/gm, "").replace(/^(?: {4}|\t).+$/gm, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<(pre|code|script|style|textarea|a)\b[\s\S]*?<\/\1>/gi, "").replace(/`+[^`\n]*`+/g, "");
1081
1154
  }
@@ -6918,7 +6991,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
6918
6991
  markdown: source,
6919
6992
  replacements: /* @__PURE__ */ new Map()
6920
6993
  };
6921
- const result = napi.transform(graphviz.markdown, {
6994
+ const napiOptions = {
6922
6995
  gfm: options.gfm,
6923
6996
  mdx: resolveMdxForFilePath(filePath, options.mdx),
6924
6997
  footnotes: options.footnotes,
@@ -7003,10 +7076,15 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
7003
7076
  repoUrl: options.editThisPage.repoUrl,
7004
7077
  branch: options.editThisPage.branch,
7005
7078
  rootDir: options.editThisPage.rootDir,
7079
+ srcDir: ssgOptions?.srcDir,
7080
+ provider: options.editThisPage.provider,
7081
+ urlPattern: options.editThisPage.urlPattern,
7006
7082
  label: options.editThisPage.label
7007
7083
  } : void 0,
7008
7084
  math: isMathEnabled(options.math)
7009
- });
7085
+ };
7086
+ const transformers = options.transformers ?? [];
7087
+ const result = transformers.length ? await runTransformers(napi, graphviz.markdown, napiOptions, filePath, options, transformers) : napi.transform(graphviz.markdown, napiOptions);
7010
7088
  if (result.errors.length > 0) console.warn("[ox-content] Transform warnings:", result.errors);
7011
7089
  let html = normalizeSelfClosingEmbeds(restoreGraphvizPlaceholders(result.html, graphviz.replacements));
7012
7090
  const frontmatter = parseFrontmatterJson(result.frontmatter);
@@ -7030,7 +7108,11 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
7030
7108
  const citationResult = await transformCitations(html, options.citations);
7031
7109
  html = citationResult.html;
7032
7110
  if (options.sanitize?.enabled) html = napi.sanitizeHtml(html, toJsSanitizeOptions(options.sanitize));
7033
- if (isMathEnabled(options.math)) html = await renderKatexMath(html);
7111
+ if (isMathEnabled(options.math)) {
7112
+ const failures = [];
7113
+ html = await renderKatexMath(html, options.math?.onError ?? "literal", failures);
7114
+ warnMathFailures(failures, filePath);
7115
+ }
7034
7116
  html = await transformBudouxHtml(html, options.budoux);
7035
7117
  const imports = result.imports ?? [];
7036
7118
  const exports = result.exports ?? [];
@@ -7086,6 +7168,52 @@ function toJsSanitizeOptions(options) {
7086
7168
  allowedUrlSchemes: options.allowedUrlSchemes
7087
7169
  };
7088
7170
  }
7171
+ /**
7172
+ * Runs the configured `transformers` over the parsed tree.
7173
+ *
7174
+ * Markdown never passes through Vite's `transform` hook — the native layer
7175
+ * reads it directly — so this is the only place user config can reach the
7176
+ * AST. The tree is handed over after frontmatter parsing and Markdown
7177
+ * feature expansion, and handed back for rendering, HTML postprocessing,
7178
+ * and sanitization, so a transformer costs a document nothing else.
7179
+ *
7180
+ * A transformer that throws, or returns something that is not a node, is
7181
+ * reported and skipped: one bad hook should not take the page down with it.
7182
+ */
7183
+ async function runTransformers(napi, markdown, napiOptions, filePath, options, transformers) {
7184
+ const parsed = napi.transformMdast(markdown, napiOptions);
7185
+ if (!parsed.astJson) return {
7186
+ html: "",
7187
+ frontmatter: parsed.frontmatter,
7188
+ toc: [],
7189
+ errors: parsed.errors,
7190
+ imports: [],
7191
+ exports: [],
7192
+ components: []
7193
+ };
7194
+ const errors = [...parsed.errors];
7195
+ const context = {
7196
+ filePath,
7197
+ frontmatter: parseFrontmatterJson(parsed.frontmatter),
7198
+ options
7199
+ };
7200
+ let ast = JSON.parse(parsed.astJson);
7201
+ for (const transformer of transformers) try {
7202
+ const next = await transformer.transform(ast, context);
7203
+ if (!next || typeof next !== "object") {
7204
+ errors.push(`transformer "${transformer.name}" returned ${next === void 0 ? "undefined" : String(next)} instead of a node`);
7205
+ continue;
7206
+ }
7207
+ ast = next;
7208
+ } catch (error) {
7209
+ errors.push(`transformer "${transformer.name}" failed: ${error instanceof Error ? error.message : String(error)}`);
7210
+ }
7211
+ const result = napi.transformFromMdast(JSON.stringify(ast), parsed.frontmatter, napiOptions);
7212
+ return {
7213
+ ...result,
7214
+ errors: [...errors, ...result.errors]
7215
+ };
7216
+ }
7089
7217
  function parseFrontmatterJson(json) {
7090
7218
  if (!json) return {};
7091
7219
  try {
@@ -7185,6 +7313,19 @@ if (import.meta.hot) {
7185
7313
  }
7186
7314
  `;
7187
7315
  }
7316
+ /**
7317
+ * Reports every `$…$` run KaTeX refused.
7318
+ *
7319
+ * Under the default policy the page keeps its prose, which is the readable
7320
+ * outcome but also a silent one — a genuine mistake in a formula would
7321
+ * otherwise leave no trace at all.
7322
+ */
7323
+ function warnMathFailures(failures, filePath) {
7324
+ for (const failure of failures) {
7325
+ const delimiter = failure.block ? "$$" : "$";
7326
+ console.warn(`[ox-content] ${filePath}: math left as written — ${failure.message} (in ${delimiter}${failure.tex}${delimiter})`);
7327
+ }
7328
+ }
7188
7329
  function isMathEnabled(math) {
7189
7330
  if (math === true) return true;
7190
7331
  if (math === false || math == null) return false;
@@ -8528,18 +8669,39 @@ async function renderSingleSatoriPage(entry, templateFn, options, cacheDir, root
8528
8669
  /**
8529
8670
  * Serve and copy KaTeX CSS/fonts only when the optional `katex` package exists.
8530
8671
  */
8672
+ /** Font formats KaTeX ships, newest first. */
8673
+ const FONT_FORMATS = {
8674
+ woff2: [".woff2"],
8675
+ all: [
8676
+ ".woff2",
8677
+ ".woff",
8678
+ ".ttf"
8679
+ ]
8680
+ };
8531
8681
  /**
8532
- * Copies `katex.min.css` and `fonts/` into the SSG output.
8682
+ * Copies `katex.min.css` and the requested font formats into the SSG output.
8533
8683
  * Returns an empty list when KaTeX is not installed.
8684
+ *
8685
+ * The `.ttf` and `.woff` sets are three quarters of the font bytes and no
8686
+ * browser that can run the rest of the site needs them: `@font-face` lists
8687
+ * `woff2` first and stops at the first format it supports. `formats: "all"`
8688
+ * brings them back.
8534
8689
  */
8535
- async function copyKatexAssets(outDir) {
8690
+ async function copyKatexAssets(outDir, formats = "woff2") {
8536
8691
  const dist = resolveKatexDist();
8537
8692
  if (!dist) return [];
8538
8693
  const dest = (0, node_path.join)(outDir, KATEX_ASSET_DIR);
8539
8694
  await (0, node_fs_promises.mkdir)((0, node_path.join)(dest, "fonts"), { recursive: true });
8540
8695
  const cssDest = (0, node_path.join)(dest, "katex.min.css");
8541
8696
  await (0, node_fs_promises.copyFile)((0, node_path.join)(dist, "katex.min.css"), cssDest);
8542
- await (0, node_fs_promises.cp)((0, node_path.join)(dist, "fonts"), (0, node_path.join)(dest, "fonts"), { recursive: true });
8697
+ const wanted = FONT_FORMATS[formats] ?? FONT_FORMATS.woff2;
8698
+ await (0, node_fs_promises.cp)((0, node_path.join)(dist, "fonts"), (0, node_path.join)(dest, "fonts"), {
8699
+ recursive: true,
8700
+ filter: (source) => {
8701
+ const extension = (0, node_path.extname)(source);
8702
+ return extension === "" || wanted.includes(extension);
8703
+ }
8704
+ });
8543
8705
  return [cssDest];
8544
8706
  }
8545
8707
  /** Dev-server middleware that serves `/__ox_katex__/*` from `katex/dist`. */
@@ -16842,7 +17004,7 @@ async function buildSsg(options, root) {
16842
17004
  });
16843
17005
  await applyDocumentationVersions(generatedPages, context, errors);
16844
17006
  await writeGeneratedPages(generatedPages, context, generatedFiles, listedPages, outputPages, errors);
16845
- if (options.math?.enabled) generatedFiles.push(...await copyKatexAssets(outDir));
17007
+ if (options.math?.enabled && generatedPages.some((page) => page.html.includes("__ox_katex__"))) generatedFiles.push(...await copyKatexAssets(outDir, options.math.fontFormats));
16846
17008
  generatedFiles.push(...await require_vitepress.writeSelfHostedThemeFonts({
16847
17009
  fonts: ssgOptions.theme?.fonts ?? {},
16848
17010
  outDir,
@@ -17007,7 +17169,8 @@ async function transformSsgPage(context, inputPath) {
17007
17169
  const result = await transformMarkdown(content, inputPath, context.options, {
17008
17170
  convertMdLinks: true,
17009
17171
  baseUrl: publicBase(context.base, context.ssgOptions.routePrefix),
17010
- sourcePath: inputPath
17172
+ sourcePath: inputPath,
17173
+ srcDir: context.srcDir
17011
17174
  });
17012
17175
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17013
17176
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17296,7 +17459,8 @@ async function transformNotFoundMarkdown(context, inputPath, markdown) {
17296
17459
  const result = await transformMarkdown(markdown, inputPath, context.options, {
17297
17460
  convertMdLinks: true,
17298
17461
  baseUrl: context.base,
17299
- sourcePath: path.join(context.srcDir, "index.md")
17462
+ sourcePath: path.join(context.srcDir, "index.md"),
17463
+ srcDir: context.srcDir
17300
17464
  });
17301
17465
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17302
17466
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17638,7 +17802,8 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
17638
17802
  const result = await transformMarkdown(await fs_promises.readFile(filePath, "utf-8"), filePath, options, {
17639
17803
  convertMdLinks: true,
17640
17804
  baseUrl: base,
17641
- sourcePath: filePath
17805
+ sourcePath: filePath,
17806
+ srcDir
17642
17807
  });
17643
17808
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17644
17809
  let transformedHtml = result.html;
@@ -18594,9 +18759,21 @@ function resolveEmojiShortcodeOptions(options) {
18594
18759
  };
18595
18760
  }
18596
18761
  function resolveMathOptions(options) {
18597
- if (!options) return { enabled: false };
18598
- if (options === true) return { enabled: true };
18599
- return { enabled: options.enabled ?? true };
18762
+ if (!options) return {
18763
+ enabled: false,
18764
+ onError: "literal",
18765
+ fontFormats: "woff2"
18766
+ };
18767
+ if (options === true) return {
18768
+ enabled: true,
18769
+ onError: "literal",
18770
+ fontFormats: "woff2"
18771
+ };
18772
+ return {
18773
+ enabled: options.enabled ?? true,
18774
+ onError: options.onError ?? "literal",
18775
+ fontFormats: options.fontFormats ?? "woff2"
18776
+ };
18600
18777
  }
18601
18778
  function resolveAttrsOptions(options) {
18602
18779
  if (!options) return { enabled: false };
@@ -18673,6 +18850,8 @@ function resolveEditThisPageOptions(options) {
18673
18850
  repoUrl: options.repoUrl,
18674
18851
  branch: options.branch ?? "main",
18675
18852
  rootDir: options.rootDir,
18853
+ provider: options.provider,
18854
+ urlPattern: options.urlPattern,
18676
18855
  label: options.label ?? "Edit this page"
18677
18856
  };
18678
18857
  }
@@ -20882,6 +21061,7 @@ exports.renderIslandComponentImports = renderIslandComponentImports;
20882
21061
  exports.renderMarkdown = renderMarkdown;
20883
21062
  exports.renderMarkdownStream = renderMarkdownStream;
20884
21063
  exports.renderPage = renderPage;
21064
+ exports.renderThemeTokenCss = require_theme_tokens.renderThemeTokenCss;
20885
21065
  exports.renderToString = require_jsx_html.renderToString;
20886
21066
  exports.resolveAbbreviationsOptions = resolveAbbreviationsOptions;
20887
21067
  exports.resolveBadgeOptions = resolveBadgeOptions;
@@ -20939,6 +21119,7 @@ exports.setRenderContext = setRenderContext;
20939
21119
  exports.shouldLintMarkdownFile = shouldLintMarkdownFile;
20940
21120
  exports.stripMarkdownExtension = stripMarkdownExtension;
20941
21121
  exports.stripViteQuery = stripViteQuery;
21122
+ exports.tokensToCss = require_theme_tokens.tokensToCss;
20942
21123
  exports.transformAllPlugins = transformAllPlugins;
20943
21124
  exports.transformBudouxHtml = transformBudouxHtml;
20944
21125
  exports.transformGitHub = transformGitHub;