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

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;
@@ -17007,7 +17148,8 @@ async function transformSsgPage(context, inputPath) {
17007
17148
  const result = await transformMarkdown(content, inputPath, context.options, {
17008
17149
  convertMdLinks: true,
17009
17150
  baseUrl: publicBase(context.base, context.ssgOptions.routePrefix),
17010
- sourcePath: inputPath
17151
+ sourcePath: inputPath,
17152
+ srcDir: context.srcDir
17011
17153
  });
17012
17154
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17013
17155
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17296,7 +17438,8 @@ async function transformNotFoundMarkdown(context, inputPath, markdown) {
17296
17438
  const result = await transformMarkdown(markdown, inputPath, context.options, {
17297
17439
  convertMdLinks: true,
17298
17440
  baseUrl: context.base,
17299
- sourcePath: path.join(context.srcDir, "index.md")
17441
+ sourcePath: path.join(context.srcDir, "index.md"),
17442
+ srcDir: context.srcDir
17300
17443
  });
17301
17444
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17302
17445
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17638,7 +17781,8 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
17638
17781
  const result = await transformMarkdown(await fs_promises.readFile(filePath, "utf-8"), filePath, options, {
17639
17782
  convertMdLinks: true,
17640
17783
  baseUrl: base,
17641
- sourcePath: filePath
17784
+ sourcePath: filePath,
17785
+ srcDir
17642
17786
  });
17643
17787
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17644
17788
  let transformedHtml = result.html;
@@ -18594,9 +18738,18 @@ function resolveEmojiShortcodeOptions(options) {
18594
18738
  };
18595
18739
  }
18596
18740
  function resolveMathOptions(options) {
18597
- if (!options) return { enabled: false };
18598
- if (options === true) return { enabled: true };
18599
- return { enabled: options.enabled ?? true };
18741
+ if (!options) return {
18742
+ enabled: false,
18743
+ onError: "literal"
18744
+ };
18745
+ if (options === true) return {
18746
+ enabled: true,
18747
+ onError: "literal"
18748
+ };
18749
+ return {
18750
+ enabled: options.enabled ?? true,
18751
+ onError: options.onError ?? "literal"
18752
+ };
18600
18753
  }
18601
18754
  function resolveAttrsOptions(options) {
18602
18755
  if (!options) return { enabled: false };
@@ -18673,6 +18826,8 @@ function resolveEditThisPageOptions(options) {
18673
18826
  repoUrl: options.repoUrl,
18674
18827
  branch: options.branch ?? "main",
18675
18828
  rootDir: options.rootDir,
18829
+ provider: options.provider,
18830
+ urlPattern: options.urlPattern,
18676
18831
  label: options.label ?? "Edit this page"
18677
18832
  };
18678
18833
  }
@@ -20882,6 +21037,7 @@ exports.renderIslandComponentImports = renderIslandComponentImports;
20882
21037
  exports.renderMarkdown = renderMarkdown;
20883
21038
  exports.renderMarkdownStream = renderMarkdownStream;
20884
21039
  exports.renderPage = renderPage;
21040
+ exports.renderThemeTokenCss = require_theme_tokens.renderThemeTokenCss;
20885
21041
  exports.renderToString = require_jsx_html.renderToString;
20886
21042
  exports.resolveAbbreviationsOptions = resolveAbbreviationsOptions;
20887
21043
  exports.resolveBadgeOptions = resolveBadgeOptions;
@@ -20939,6 +21095,7 @@ exports.setRenderContext = setRenderContext;
20939
21095
  exports.shouldLintMarkdownFile = shouldLintMarkdownFile;
20940
21096
  exports.stripMarkdownExtension = stripMarkdownExtension;
20941
21097
  exports.stripViteQuery = stripViteQuery;
21098
+ exports.tokensToCss = require_theme_tokens.tokensToCss;
20942
21099
  exports.transformAllPlugins = transformAllPlugins;
20943
21100
  exports.transformBudouxHtml = transformBudouxHtml;
20944
21101
  exports.transformGitHub = transformGitHub;