@ox-content/vite-plugin 3.0.0-alpha.12 → 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,6 +1,8 @@
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");
5
+ const require_markdown_tables = require("./markdown-tables.cjs");
4
6
  let path = require("path");
5
7
  path = require_vitepress.__toESM(path, 1);
6
8
  let unified = require("unified");
@@ -685,8 +687,14 @@ let missingWarned = false;
685
687
  /**
686
688
  * Replaces rust `ox-math` placeholders with static KaTeX HTML.
687
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.
688
696
  */
689
- async function renderKatexMath(html) {
697
+ async function renderKatexMath(html, onError = "literal", failures) {
690
698
  if (!html.includes("data-ox-tex")) return html;
691
699
  const katex = loadKatex();
692
700
  if (!katex) {
@@ -694,14 +702,39 @@ async function renderKatexMath(html) {
694
702
  return html;
695
703
  }
696
704
  return html.replace(MATH_TAG, (_match, tag, kind, encoded) => {
697
- return `<${tag} class="ox-math ox-math-${kind}">${katex.renderToString(decodeHtmlAttr$3(encoded), {
698
- displayMode: kind === "block",
699
- throwOnError: false,
705
+ const block = kind === "block";
706
+ const tex = decodeHtmlAttr$3(encoded);
707
+ const options = {
708
+ displayMode: block,
700
709
  trust: false,
701
710
  output: "htmlAndMathml"
702
- })}</${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}>`;
703
733
  });
704
734
  }
735
+ function escapeHtmlText(value) {
736
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
737
+ }
705
738
  /** Directory that contains `katex.min.css` and `fonts/`, or `null`. */
706
739
  function resolveKatexDist() {
707
740
  for (const resolver of createKatexResolvers()) try {
@@ -726,8 +759,34 @@ function createKatexResolvers() {
726
759
  resolvers.push((0, node_module.createRequire)(require("url").pathToFileURL(__filename).href));
727
760
  return resolvers;
728
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
+ */
729
782
  function decodeHtmlAttr$3(value) {
730
- 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
+ });
731
790
  }
732
791
  function warnMissingKatexOnce() {
733
792
  if (missingWarned) return;
@@ -1069,12 +1128,27 @@ function citationKeysFromMarkdown(markdown, options, diagnostics) {
1069
1128
  for (const match of visibleMarkdown.matchAll(BRACKET_RE)) {
1070
1129
  const body = (match[1] ?? "").trim();
1071
1130
  if (!body.startsWith("@") && !body.startsWith("-@")) continue;
1131
+ if (isLinkLabel(visibleMarkdown, match)) continue;
1072
1132
  const parsed = parseCitationGroup(body, options, diagnostics);
1073
1133
  if (!parsed) continue;
1074
1134
  keys.push(...parsed.map((citation) => citation.key));
1075
1135
  }
1076
1136
  return keys;
1077
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
+ }
1078
1152
  function stripMarkdownCitationProtectedText(markdown) {
1079
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, "");
1080
1154
  }
@@ -6917,7 +6991,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
6917
6991
  markdown: source,
6918
6992
  replacements: /* @__PURE__ */ new Map()
6919
6993
  };
6920
- const result = napi.transform(graphviz.markdown, {
6994
+ const napiOptions = {
6921
6995
  gfm: options.gfm,
6922
6996
  mdx: resolveMdxForFilePath(filePath, options.mdx),
6923
6997
  footnotes: options.footnotes,
@@ -7002,10 +7076,15 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
7002
7076
  repoUrl: options.editThisPage.repoUrl,
7003
7077
  branch: options.editThisPage.branch,
7004
7078
  rootDir: options.editThisPage.rootDir,
7079
+ srcDir: ssgOptions?.srcDir,
7080
+ provider: options.editThisPage.provider,
7081
+ urlPattern: options.editThisPage.urlPattern,
7005
7082
  label: options.editThisPage.label
7006
7083
  } : void 0,
7007
7084
  math: isMathEnabled(options.math)
7008
- });
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);
7009
7088
  if (result.errors.length > 0) console.warn("[ox-content] Transform warnings:", result.errors);
7010
7089
  let html = normalizeSelfClosingEmbeds(restoreGraphvizPlaceholders(result.html, graphviz.replacements));
7011
7090
  const frontmatter = parseFrontmatterJson(result.frontmatter);
@@ -7029,7 +7108,11 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
7029
7108
  const citationResult = await transformCitations(html, options.citations);
7030
7109
  html = citationResult.html;
7031
7110
  if (options.sanitize?.enabled) html = napi.sanitizeHtml(html, toJsSanitizeOptions(options.sanitize));
7032
- 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
+ }
7033
7116
  html = await transformBudouxHtml(html, options.budoux);
7034
7117
  const imports = result.imports ?? [];
7035
7118
  const exports = result.exports ?? [];
@@ -7085,6 +7168,52 @@ function toJsSanitizeOptions(options) {
7085
7168
  allowedUrlSchemes: options.allowedUrlSchemes
7086
7169
  };
7087
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
+ }
7088
7217
  function parseFrontmatterJson(json) {
7089
7218
  if (!json) return {};
7090
7219
  try {
@@ -7184,6 +7313,19 @@ if (import.meta.hot) {
7184
7313
  }
7185
7314
  `;
7186
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
+ }
7187
7329
  function isMathEnabled(math) {
7188
7330
  if (math === true) return true;
7189
7331
  if (math === false || math == null) return false;
@@ -17006,7 +17148,8 @@ async function transformSsgPage(context, inputPath) {
17006
17148
  const result = await transformMarkdown(content, inputPath, context.options, {
17007
17149
  convertMdLinks: true,
17008
17150
  baseUrl: publicBase(context.base, context.ssgOptions.routePrefix),
17009
- sourcePath: inputPath
17151
+ sourcePath: inputPath,
17152
+ srcDir: context.srcDir
17010
17153
  });
17011
17154
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17012
17155
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17295,7 +17438,8 @@ async function transformNotFoundMarkdown(context, inputPath, markdown) {
17295
17438
  const result = await transformMarkdown(markdown, inputPath, context.options, {
17296
17439
  convertMdLinks: true,
17297
17440
  baseUrl: context.base,
17298
- sourcePath: path.join(context.srcDir, "index.md")
17441
+ sourcePath: path.join(context.srcDir, "index.md"),
17442
+ srcDir: context.srcDir
17299
17443
  });
17300
17444
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17301
17445
  const transformedHtml = await transformSsgHtml(result.html, context.options);
@@ -17637,7 +17781,8 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
17637
17781
  const result = await transformMarkdown(await fs_promises.readFile(filePath, "utf-8"), filePath, options, {
17638
17782
  convertMdLinks: true,
17639
17783
  baseUrl: base,
17640
- sourcePath: filePath
17784
+ sourcePath: filePath,
17785
+ srcDir
17641
17786
  });
17642
17787
  const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
17643
17788
  let transformedHtml = result.html;
@@ -18593,9 +18738,18 @@ function resolveEmojiShortcodeOptions(options) {
18593
18738
  };
18594
18739
  }
18595
18740
  function resolveMathOptions(options) {
18596
- if (!options) return { enabled: false };
18597
- if (options === true) return { enabled: true };
18598
- 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
+ };
18599
18753
  }
18600
18754
  function resolveAttrsOptions(options) {
18601
18755
  if (!options) return { enabled: false };
@@ -18672,6 +18826,8 @@ function resolveEditThisPageOptions(options) {
18672
18826
  repoUrl: options.repoUrl,
18673
18827
  branch: options.branch ?? "main",
18674
18828
  rootDir: options.rootDir,
18829
+ provider: options.provider,
18830
+ urlPattern: options.urlPattern,
18675
18831
  label: options.label ?? "Edit this page"
18676
18832
  };
18677
18833
  }
@@ -20213,61 +20369,6 @@ function createEmptyLintResult() {
20213
20369
  };
20214
20370
  }
20215
20371
  //#endregion
20216
- //#region src/markdown-tables.ts
20217
- const TABINDEX_FLAG = "oxTableScrollTabindex";
20218
- const LABEL_FLAG = "oxTableScrollLabel";
20219
- const SCROLLABLE_ATTR = "data-ox-table-scrollable";
20220
- function markdownTableScrollLabel(locale) {
20221
- if (locale?.split("-")[0]?.toLowerCase() === "ja") return "横スクロールできる表";
20222
- return "Scrollable table";
20223
- }
20224
- /**
20225
- * Makes overflowing Markdown tables keyboard-scrollable without wrapping or
20226
- * replacing the native `<table>` element.
20227
- *
20228
- * Run this after rendering Markdown and again after layout-changing updates.
20229
- * Narrow tables stay out of the tab order when overflow can be measured.
20230
- */
20231
- function enhanceMarkdownTables(root, options = {}) {
20232
- const target = root ?? globalThis.document;
20233
- if (!target?.querySelectorAll) return 0;
20234
- const selector = options.selector ?? ".content table";
20235
- const locale = ownerDocument(target)?.documentElement.lang;
20236
- const label = options.label ?? markdownTableScrollLabel(locale);
20237
- let scrollableCount = 0;
20238
- for (const table of target.querySelectorAll(selector)) {
20239
- if (typeof HTMLElement === "undefined" || !(table instanceof HTMLElement)) continue;
20240
- if (table.tagName !== "TABLE") continue;
20241
- const scrollable = table.scrollWidth > table.clientWidth + 1;
20242
- table.toggleAttribute(SCROLLABLE_ATTR, scrollable);
20243
- if (scrollable) {
20244
- scrollableCount++;
20245
- if (!table.hasAttribute("tabindex")) {
20246
- table.tabIndex = 0;
20247
- table.dataset[TABINDEX_FLAG] = "true";
20248
- }
20249
- if (!table.hasAttribute("aria-label") && !table.hasAttribute("aria-labelledby")) {
20250
- table.setAttribute("aria-label", label);
20251
- table.dataset[LABEL_FLAG] = "true";
20252
- }
20253
- } else {
20254
- if (table.dataset[TABINDEX_FLAG] === "true") {
20255
- table.removeAttribute("tabindex");
20256
- delete table.dataset[TABINDEX_FLAG];
20257
- }
20258
- if (table.dataset[LABEL_FLAG] === "true") {
20259
- table.removeAttribute("aria-label");
20260
- delete table.dataset[LABEL_FLAG];
20261
- }
20262
- }
20263
- }
20264
- return scrollableCount;
20265
- }
20266
- function ownerDocument(root) {
20267
- if (typeof Document !== "undefined" && root instanceof Document) return root;
20268
- return root.ownerDocument ?? globalThis.document;
20269
- }
20270
- //#endregion
20271
20372
  //#region src/ssg-output-write.ts
20272
20373
  /**
20273
20374
  * Writers for composable SSG outputs. Custom hosts call these without `buildSsg()`.
@@ -20870,7 +20971,7 @@ exports.defineTheme = require_vitepress.defineTheme;
20870
20971
  exports.discoverDocumentMdxIslands = discoverDocumentMdxIslands;
20871
20972
  exports.discoverRegisteredMdxComponents = discoverRegisteredMdxComponents;
20872
20973
  exports.each = require_jsx_html.each;
20873
- exports.enhanceMarkdownTables = enhanceMarkdownTables;
20974
+ exports.enhanceMarkdownTables = require_markdown_tables.enhanceMarkdownTables;
20874
20975
  exports.escapeSvelteMarkup = escapeSvelteMarkup;
20875
20976
  exports.extractCodeBlocks = extractCodeBlocks;
20876
20977
  exports.extractDocs = extractDocs;
@@ -20906,7 +21007,7 @@ exports.lintMarkdown = lintMarkdown;
20906
21007
  exports.lintMarkdownAsync = lintMarkdownAsync;
20907
21008
  exports.lintMarkdownFile = lintMarkdownFile;
20908
21009
  exports.lintMarkdownFiles = lintMarkdownFiles;
20909
- exports.markdownTableScrollLabel = markdownTableScrollLabel;
21010
+ exports.markdownTableScrollLabel = require_markdown_tables.markdownTableScrollLabel;
20910
21011
  exports.mergeThemes = require_vitepress.mergeThemes;
20911
21012
  exports.mermaidClientScript = mermaidClientScript;
20912
21013
  exports.normalizeMarkdownExtensions = normalizeMarkdownExtensions;
@@ -20936,6 +21037,7 @@ exports.renderIslandComponentImports = renderIslandComponentImports;
20936
21037
  exports.renderMarkdown = renderMarkdown;
20937
21038
  exports.renderMarkdownStream = renderMarkdownStream;
20938
21039
  exports.renderPage = renderPage;
21040
+ exports.renderThemeTokenCss = require_theme_tokens.renderThemeTokenCss;
20939
21041
  exports.renderToString = require_jsx_html.renderToString;
20940
21042
  exports.resolveAbbreviationsOptions = resolveAbbreviationsOptions;
20941
21043
  exports.resolveBadgeOptions = resolveBadgeOptions;
@@ -20993,6 +21095,7 @@ exports.setRenderContext = setRenderContext;
20993
21095
  exports.shouldLintMarkdownFile = shouldLintMarkdownFile;
20994
21096
  exports.stripMarkdownExtension = stripMarkdownExtension;
20995
21097
  exports.stripViteQuery = stripViteQuery;
21098
+ exports.tokensToCss = require_theme_tokens.tokensToCss;
20996
21099
  exports.transformAllPlugins = transformAllPlugins;
20997
21100
  exports.transformBudouxHtml = transformBudouxHtml;
20998
21101
  exports.transformGitHub = transformGitHub;