@ox-content/vite-plugin 2.8.0 → 2.10.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/dist/index.cjs CHANGED
@@ -5,6 +5,7 @@ const require_tabs = require("./tabs.cjs");
5
5
  const require_youtube = require("./youtube.cjs");
6
6
  const require_github = require("./github.cjs");
7
7
  const require_ogp = require("./ogp.cjs");
8
+ const require_vitepress = require("./vitepress.cjs");
8
9
  let path = require("path");
9
10
  path = require_chunk.__toESM(path);
10
11
  let unified = require("unified");
@@ -17,7 +18,6 @@ let node_path = require("node:path");
17
18
  node_path = require_chunk.__toESM(node_path);
18
19
  let fs = require("fs");
19
20
  fs = require_chunk.__toESM(fs);
20
- let node_crypto = require("node:crypto");
21
21
  let fs_promises = require("fs/promises");
22
22
  fs_promises = require_chunk.__toESM(fs_promises);
23
23
  let glob = require("glob");
@@ -7087,241 +7087,11 @@ if (import.meta.hot) {
7087
7087
  }
7088
7088
  //#endregion
7089
7089
  //#region src/nav-generator.ts
7090
- /**
7091
- * Navigation Metadata Generator for API Documentation
7092
- *
7093
- * This module provides utilities for generating sidebar navigation structures
7094
- * from extracted documentation. It automatically:
7095
- *
7096
- * - **Extracts file information**: Gets display names and file paths
7097
- * - **Formats names**: Converts technical names to readable titles
7098
- * - **Generates TypeScript**: Creates importable nav.ts files
7099
- * - **Maintains hierarchy**: Supports nested navigation structures
7100
- *
7101
- * ## Generated Navigation Format
7102
- *
7103
- * The generated navigation is TypeScript-based for type safety and IDE support:
7104
- *
7105
- * ```typescript
7106
- * export const apiNav: NavItem[] = [
7107
- * { title: 'Overview', path: '/api/index' },
7108
- * { title: 'Transform', path: '/api/transform' },
7109
- * { title: 'Types', path: '/api/types' },
7110
- * // ... auto-generated from documentation
7111
- * ] as const;
7112
- * ```
7113
- *
7114
- * ## Integration
7115
- *
7116
- * The generated nav.ts file can be imported directly:
7117
- *
7118
- * ```typescript
7119
- * // In your Vue/React component
7120
- * import { apiNav } from '../api/nav';
7121
- *
7122
- * const apiItems = apiNav.map(item => ({
7123
- * ...item,
7124
- * file: () => import(`../api/${item.path.split('/').pop()}.md`)
7125
- * }));
7126
- * ```
7127
- *
7128
- * @example
7129
- * ```typescript
7130
- * import { generateNavMetadata, generateNavCode } from './nav-generator';
7131
- *
7132
- * const extracted = [
7133
- * { file: 'transform.ts', entries: [...] },
7134
- * { file: 'types.ts', entries: [...] },
7135
- * ];
7136
- *
7137
- * const navItems = generateNavMetadata(extracted);
7138
- * // => [
7139
- * // { title: 'Transform', path: '/api/transform' },
7140
- * // { title: 'Types', path: '/api/types' },
7141
- * // ]
7142
- *
7143
- * const code = generateNavCode(navItems);
7144
- * // => TypeScript code ready to write to nav.ts
7145
- * ```
7146
- */
7147
- /**
7148
- * Generates sidebar navigation metadata from extracted documentation.
7149
- *
7150
- * Takes an array of extracted documentation and produces a flat navigation
7151
- * structure suitable for sidebar menus. Items are:
7152
- * - Sorted alphabetically by display name
7153
- * - Formatted with readable titles
7154
- * - Prefixed with the specified base path
7155
- *
7156
- * ## Naming Conventions
7157
- *
7158
- * - `transform.ts` → `{ title: 'Transform', path: '/api/transform' }`
7159
- * - `nav-generator.ts` → `{ title: 'Nav Generator', path: '/api/nav-generator' }`
7160
- * - `index.ts` or `index-module.ts` → `{ title: 'Overview', path: '/api/index' }`
7161
- * - `types.ts` → `{ title: 'Types', path: '/api/types' }`
7162
- *
7163
- * ## Sorting
7164
- *
7165
- * Items are sorted alphabetically by display title for consistent ordering.
7166
- * Special item 'Overview' sorts naturally with others (O comes after most letters).
7167
- *
7168
- * ## Path Generation
7169
- *
7170
- * The generated paths are used to import corresponding Markdown files:
7171
- * - Path `/api/transform` → Import from `../api/transform.md`
7172
- * - Path `/api/index` → Import from `../api/index.md`
7173
- *
7174
- * @param docs - Array of extracted documentation (file + entries)
7175
- * @param basePath - Base path prefix for navigation URLs (default: '/api')
7176
- * Use '/api' for main API docs, '/helpers' for utilities, etc.
7177
- *
7178
- * @returns Array of navigation items ready to use or export to TypeScript
7179
- *
7180
- * @example
7181
- * ```typescript
7182
- * const navItems = generateNavMetadata(
7183
- * [
7184
- * { file: 'transform.ts', entries: [...] },
7185
- * { file: 'docs.ts', entries: [...] },
7186
- * { file: 'types.ts', entries: [...] },
7187
- * ],
7188
- * '/api'
7189
- * );
7190
- *
7191
- * // Returns:
7192
- * // [
7193
- * // { title: 'Docs', path: '/api/docs' },
7194
- * // { title: 'Transform', path: '/api/transform' },
7195
- * // { title: 'Types', path: '/api/types' },
7196
- * // ]
7197
- * ```
7198
- *
7199
- * @see generateNavCode For converting these items to TypeScript code
7200
- */
7201
7090
  function generateNavMetadata(docs, basePath = "/api") {
7202
- return [...docs].sort((a, b) => {
7203
- const aName = getDocDisplayName(a.file);
7204
- const bName = getDocDisplayName(b.file);
7205
- return aName.localeCompare(bName);
7206
- }).map((doc) => ({
7207
- title: getDocDisplayName(doc.file),
7208
- path: `${basePath}/${getDocFileName(doc.file)}`
7209
- }));
7210
- }
7211
- /**
7212
- * Gets the human-readable display name for a documentation file.
7213
- *
7214
- * Transforms file paths and names into proper title case:
7215
- * - Extracts base name (e.g., 'transform.ts' → 'transform')
7216
- * - Converts kebab-case to Title Case (e.g., 'nav-generator' → 'Nav Generator')
7217
- * - Converts camelCase to Title Case (e.g., 'transformMarkdown' → 'Transform Markdown')
7218
- * - Handles special cases (index → 'Overview')
7219
- *
7220
- * ## Examples
7221
- *
7222
- * - `'/path/to/transform.ts'` → `'Transform'`
7223
- * - `'nav-generator.ts'` → `'Nav Generator'`
7224
- * - `'index.ts'` → `'Overview'`
7225
- * - `'index-module.ts'` → `'Overview'`
7226
- * - `'myFunction.ts'` → `'My Function'` (with camelCase handling)
7227
- *
7228
- * @param filePath - Full or relative file path
7229
- * @returns Formatted display name suitable for UI labels
7230
- *
7231
- * @internal
7232
- */
7233
- function getDocDisplayName(filePath) {
7234
- const fileName = path.default.basename(filePath, path.default.extname(filePath));
7235
- if (fileName === "index" || fileName === "index-module") return "Overview";
7236
- return fileName.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
7237
- }
7238
- /**
7239
- * Gets the file name (without extension) for use in navigation paths.
7240
- *
7241
- * This handles filename conflicts that may occur during generation:
7242
- * - Preserves most names as-is
7243
- * - Special handling for index files to maintain consistency
7244
- *
7245
- * @param filePath - Source file path
7246
- * @returns File name without extension, ready for URL paths
7247
- *
7248
- * @internal
7249
- */
7250
- function getDocFileName(filePath) {
7251
- const fileName = path.default.basename(filePath, path.default.extname(filePath));
7252
- if (fileName === "index") return "index";
7253
- return fileName;
7091
+ return require_mermaid.importNapiModuleSync().generateDocsNavMetadata(docs.map((doc) => doc.file), basePath);
7254
7092
  }
7255
- /**
7256
- * Generates TypeScript code for navigation metadata export.
7257
- *
7258
- * Creates a complete, self-contained TypeScript file that:
7259
- * - Defines the NavItem interface
7260
- * - Exports navigation items as a const
7261
- * - Uses `as const` for type-safe literal types
7262
- * - Includes auto-generation notice
7263
- *
7264
- * The generated code is production-ready and suitable for direct import
7265
- * in Vue, React, or vanilla TypeScript applications.
7266
- *
7267
- * ## Generated Code Example
7268
- *
7269
- * ```typescript
7270
- * export interface NavItem {
7271
- * title: string;
7272
- * path: string;
7273
- * children?: NavItem[];
7274
- * }
7275
- *
7276
- * export const apiNav: NavItem[] = [
7277
- * { "title": "Docs", "path": "/api/docs" },
7278
- * { "title": "Transform", "path": "/api/transform" },
7279
- * // ...
7280
- * ] as const;
7281
- * ```
7282
- *
7283
- * ## Features
7284
- *
7285
- * - **Type Safety**: Includes NavItem interface definition
7286
- * - **Readonly**: Uses `as const` to ensure immutability
7287
- * - **IDE Support**: Full IntelliSense and autocomplete
7288
- * - **Self-Documenting**: Includes notice that file is auto-generated
7289
- *
7290
- * @param navItems - Array of navigation items to export
7291
- * @param exportName - Name of the exported const (default: 'apiNav')
7292
- * Use custom names for different navigation sections
7293
- *
7294
- * @returns Complete TypeScript source code as string,
7295
- * ready to write to a .ts file
7296
- *
7297
- * @example
7298
- * ```typescript
7299
- * const navItems = [
7300
- * { title: 'Home', path: '/api/index' },
7301
- * { title: 'Transform', path: '/api/transform' },
7302
- * ];
7303
- *
7304
- * const code = generateNavCode(navItems, 'apiNav');
7305
- * await fs.promises.writeFile('docs/api/nav.ts', code, 'utf-8');
7306
- * ```
7307
- *
7308
- * @see generateNavMetadata For generating NavItem arrays from extracted docs
7309
- */
7310
7093
  function generateNavCode(navItems, exportName = "apiNav") {
7311
- return `/**
7312
- * Auto-generated API documentation navigation.
7313
- * This file is automatically generated by the docs plugin.
7314
- * Do not edit manually.
7315
- */
7316
-
7317
- export interface NavItem {
7318
- title: string;
7319
- path: string;
7320
- children?: NavItem[];
7321
- }
7322
-
7323
- export const ${exportName}: NavItem[] = ${JSON.stringify(navItems, null, 2)} as const;
7324
- `;
7094
+ return require_mermaid.importNapiModuleSync().generateDocsNavCode(navItems, exportName);
7325
7095
  }
7326
7096
  //#endregion
7327
7097
  //#region src/docs.ts
@@ -7648,86 +7418,6 @@ function buildDocsData(docs) {
7648
7418
  }))
7649
7419
  };
7650
7420
  }
7651
- function consumeJSDocType(value) {
7652
- const trimmed = value.trimStart();
7653
- if (!trimmed.startsWith("{")) return { rest: trimmed };
7654
- let depth = 0;
7655
- for (let index = 0; index < trimmed.length; index++) {
7656
- const char = trimmed[index];
7657
- if (char === "{") depth++;
7658
- else if (char === "}") {
7659
- depth--;
7660
- if (depth === 0) return {
7661
- type: trimmed.slice(1, index).trim() || void 0,
7662
- rest: trimmed.slice(index + 1).trimStart()
7663
- };
7664
- }
7665
- }
7666
- return { rest: trimmed };
7667
- }
7668
- function cleanTagDescription(value) {
7669
- return value.trim().replace(/^-\s*/, "").trim();
7670
- }
7671
- function splitTagNameAndDescription(value) {
7672
- const trimmed = value.trimStart();
7673
- if (trimmed.startsWith("[")) {
7674
- const closeIndex = trimmed.indexOf("]");
7675
- if (closeIndex >= 0) return {
7676
- name: trimmed.slice(0, closeIndex + 1),
7677
- description: trimmed.slice(closeIndex + 1).trimStart()
7678
- };
7679
- }
7680
- const match = /^(\S+)(?:\s+([\s\S]*))?$/u.exec(trimmed);
7681
- return {
7682
- name: match?.[1] ?? "",
7683
- description: match?.[2] ?? ""
7684
- };
7685
- }
7686
- function parseParamTagValue(value) {
7687
- const { type, rest } = consumeJSDocType(value);
7688
- const { name: rawName, description } = splitTagNameAndDescription(rest);
7689
- let name = rawName.trim();
7690
- if (!name) return null;
7691
- let optional = false;
7692
- let defaultValue;
7693
- const optionalMatch = /^\[(.*)\]$/u.exec(name);
7694
- if (optionalMatch) {
7695
- optional = true;
7696
- const [innerName, innerDefault] = optionalMatch[1].split(/=(.*)/su);
7697
- name = innerName.trim();
7698
- defaultValue = innerDefault?.trim() || void 0;
7699
- }
7700
- if (!name) return null;
7701
- return {
7702
- name,
7703
- type: type || "unknown",
7704
- description: cleanTagDescription(description),
7705
- optional: optional || void 0,
7706
- default: defaultValue
7707
- };
7708
- }
7709
- function parseReturnsTagValue(value) {
7710
- const { type, rest } = consumeJSDocType(value);
7711
- return {
7712
- type: type || "unknown",
7713
- description: cleanTagDescription(rest)
7714
- };
7715
- }
7716
- function normalizeReturnType(value) {
7717
- const parsed = parseReturnsTagValue(value);
7718
- return parsed.type === "unknown" ? value : parsed.type;
7719
- }
7720
- function mergeParam(params, next) {
7721
- const existing = params.find((param) => param.name === next.name);
7722
- if (!existing) {
7723
- params.push(next);
7724
- return;
7725
- }
7726
- if (next.type && (existing.type === "unknown" || next.type !== "unknown")) existing.type = next.type;
7727
- if (next.description) existing.description = next.description;
7728
- if (next.optional) existing.optional = true;
7729
- if (next.default) existing.default = next.default;
7730
- }
7731
7421
  /**
7732
7422
  * Extracts JSDoc documentation from source files in specified directories.
7733
7423
  *
@@ -7794,13 +7484,13 @@ function mergeParam(params, next) {
7794
7484
  * ```
7795
7485
  */
7796
7486
  async function extractDocs(srcDirs, options) {
7797
- const extractFileDocs = (await require_mermaid.importNapiModule()).extractFileDocs;
7798
- if (!extractFileDocs) throw new Error("[ox-content] extractFileDocs is not available from @ox-content/napi.");
7487
+ const extractFileDocEntries = (await require_mermaid.importNapiModule()).extractFileDocEntries;
7488
+ if (!extractFileDocEntries) throw new Error("[ox-content] extractFileDocEntries is not available from @ox-content/napi.");
7799
7489
  const results = [];
7800
7490
  for (const srcDir of srcDirs) {
7801
7491
  const files = await findFiles(srcDir, options);
7802
7492
  for (const file of files) {
7803
- const entries = extractFileDocs(file, options.private).map(parseNapiDocItem).filter((entry) => Boolean(entry));
7493
+ const entries = extractFileDocEntries(file, options.private);
7804
7494
  if (entries.length > 0) results.push({
7805
7495
  file,
7806
7496
  entries
@@ -7851,132 +7541,6 @@ function isExcluded(file, patterns) {
7851
7541
  return false;
7852
7542
  });
7853
7543
  }
7854
- function parseNapiDocItem(item) {
7855
- const kind = normalizeNapiKind(item.kind);
7856
- if (!kind) return null;
7857
- const params = [];
7858
- const examples = [];
7859
- const tags = {};
7860
- let description = "";
7861
- let returns;
7862
- let isPrivate = false;
7863
- const rawLines = (item.jsdoc ?? "").split("\n").map((line) => {
7864
- const trimmedStart = line.trimStart();
7865
- const withoutStar = trimmedStart.startsWith("*") ? trimmedStart.slice(1) : trimmedStart;
7866
- return withoutStar.startsWith(" ") ? withoutStar.slice(1) : withoutStar;
7867
- });
7868
- const cleanedLines = rawLines.map((line) => line.trim()).filter(Boolean);
7869
- let currentExample = "";
7870
- let inExample = false;
7871
- let rawLineIndex = 0;
7872
- for (const lineText of cleanedLines) {
7873
- while (rawLineIndex < rawLines.length && rawLines[rawLineIndex].trim() !== lineText) rawLineIndex++;
7874
- const rawLine = rawLineIndex < rawLines.length ? rawLines[rawLineIndex] : lineText;
7875
- rawLineIndex++;
7876
- if (lineText.startsWith("@")) {
7877
- if (inExample) {
7878
- examples.push(currentExample.trim());
7879
- currentExample = "";
7880
- inExample = false;
7881
- }
7882
- const tagMatch = /^@(\S+)\s*([\s\S]*)$/u.exec(lineText);
7883
- if (tagMatch) {
7884
- const [, tagName, tagValue = ""] = tagMatch;
7885
- switch (tagName) {
7886
- case "param":
7887
- case "arg":
7888
- case "argument": {
7889
- const param = parseParamTagValue(tagValue);
7890
- if (param) mergeParam(params, param);
7891
- break;
7892
- }
7893
- case "returns":
7894
- case "return":
7895
- returns = parseReturnsTagValue(tagValue);
7896
- break;
7897
- case "example":
7898
- inExample = true;
7899
- currentExample = tagValue.trim() ? `${tagValue.trim()}\n` : "";
7900
- break;
7901
- case "private":
7902
- isPrivate = true;
7903
- break;
7904
- default: tags[tagName] = tagValue.trim();
7905
- }
7906
- }
7907
- } else if (inExample) currentExample += rawLine + "\n";
7908
- else if (!description) description = lineText;
7909
- else description += "\n" + lineText;
7910
- }
7911
- if (inExample && currentExample) examples.push(currentExample.trim());
7912
- for (const param of item.params) {
7913
- if (params.length > 0 && param.name === "param" && !param.typeAnnotation && !param.description && !param.defaultValue) continue;
7914
- mergeParam(params, {
7915
- name: param.name,
7916
- type: param.typeAnnotation ?? "unknown",
7917
- description: param.description ?? "",
7918
- optional: param.optional || void 0,
7919
- default: param.defaultValue
7920
- });
7921
- }
7922
- if (!returns && item.returnType) returns = {
7923
- type: normalizeReturnType(item.returnType),
7924
- description: ""
7925
- };
7926
- else if (returns && item.returnType) returns.type = normalizeReturnType(item.returnType);
7927
- if (!description) description = item.doc ?? "";
7928
- for (const tag of item.tags) {
7929
- if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument" || tag.tag === "returns" || tag.tag === "return") {
7930
- if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument") {
7931
- const param = parseParamTagValue(tag.value);
7932
- if (param) mergeParam(params, param);
7933
- } else {
7934
- const parsedReturns = parseReturnsTagValue(tag.value);
7935
- if (!returns) returns = parsedReturns;
7936
- else {
7937
- returns.type = returns.type === "unknown" ? parsedReturns.type : returns.type;
7938
- returns.description ||= parsedReturns.description;
7939
- }
7940
- }
7941
- continue;
7942
- }
7943
- if (tag.tag === "example") {
7944
- if (tag.value && !examples.includes(tag.value)) examples.push(tag.value);
7945
- continue;
7946
- }
7947
- if (tag.tag === "private") {
7948
- isPrivate = true;
7949
- continue;
7950
- }
7951
- if (!tags[tag.tag]) tags[tag.tag] = tag.value;
7952
- }
7953
- return {
7954
- name: item.name,
7955
- kind,
7956
- description,
7957
- params: params.length > 0 ? params : void 0,
7958
- returns,
7959
- examples: examples.length > 0 ? examples : void 0,
7960
- tags: Object.keys(tags).length > 0 ? tags : void 0,
7961
- private: isPrivate,
7962
- file: item.sourcePath,
7963
- line: item.line,
7964
- endLine: item.endLine,
7965
- signature: item.signature
7966
- };
7967
- }
7968
- function normalizeNapiKind(kind) {
7969
- switch (kind) {
7970
- case "function":
7971
- case "class":
7972
- case "interface":
7973
- case "type":
7974
- case "variable":
7975
- case "module": return kind;
7976
- case "enum": return "type";
7977
- default: return null;
7978
- }
7979
- }
7980
7544
  /**
7981
7545
  * Generates Markdown documentation from extracted docs.
7982
7546
  */
@@ -9246,213 +8810,6 @@ initIslands((el, props) => {
9246
8810
  `;
9247
8811
  }
9248
8812
  //#endregion
9249
- //#region src/theme.ts
9250
- /**
9251
- * Default theme configuration.
9252
- * Based on the current ox-content SSG styles.
9253
- */
9254
- const defaultTheme = {
9255
- name: "default",
9256
- colors: {
9257
- primary: "#4f6fae",
9258
- primaryHover: "#425f96",
9259
- background: "#ffffff",
9260
- backgroundAlt: "#f5f7fb",
9261
- text: "#131a30",
9262
- textMuted: "#4f607b",
9263
- border: "#d2dbea",
9264
- codeBackground: "#101a31",
9265
- codeText: "#edf3ff"
9266
- },
9267
- darkColors: {
9268
- primary: "#86a4da",
9269
- primaryHover: "#a3bbe8",
9270
- background: "#060816",
9271
- backgroundAlt: "#0d1528",
9272
- text: "#ebf2ff",
9273
- textMuted: "#8ea0bf",
9274
- border: "#223252",
9275
- codeBackground: "#0a1020",
9276
- codeText: "#e7f0ff"
9277
- },
9278
- fonts: {
9279
- sans: "\"IBM Plex Sans\", \"Avenir Next\", \"Segoe UI Variable\", \"Segoe UI\", sans-serif",
9280
- mono: "\"IBM Plex Mono\", \"SFMono-Regular\", Consolas, monospace"
9281
- },
9282
- entryPage: { mode: "default" },
9283
- layout: {
9284
- sidebarWidth: "260px",
9285
- headerHeight: "60px",
9286
- maxContentWidth: "960px"
9287
- },
9288
- header: {
9289
- logo: void 0,
9290
- logoLight: void 0,
9291
- logoDark: void 0,
9292
- showSiteNameText: true,
9293
- logoWidth: 28,
9294
- logoHeight: 28
9295
- },
9296
- footer: {
9297
- message: void 0,
9298
- copyright: void 0
9299
- },
9300
- socialLinks: {},
9301
- embed: {},
9302
- css: "",
9303
- js: ""
9304
- };
9305
- /**
9306
- * Deep merge two objects.
9307
- */
9308
- function deepMerge(target, source) {
9309
- const result = { ...target };
9310
- for (const key of Object.keys(source)) {
9311
- const sourceValue = source[key];
9312
- const targetValue = target[key];
9313
- if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) result[key] = deepMerge(targetValue, sourceValue);
9314
- else if (sourceValue !== void 0) result[key] = sourceValue;
9315
- }
9316
- return result;
9317
- }
9318
- /**
9319
- * Defines a theme configuration with type checking.
9320
- *
9321
- * @example
9322
- * ```ts
9323
- * const myTheme = defineTheme({
9324
- * extends: defaultTheme,
9325
- * colors: {
9326
- * primary: '#3498db',
9327
- * },
9328
- * footer: {
9329
- * copyright: '2025 My Company',
9330
- * },
9331
- * });
9332
- * ```
9333
- */
9334
- function defineTheme(config) {
9335
- return config;
9336
- }
9337
- /**
9338
- * Merges multiple theme configurations.
9339
- * Later themes override earlier ones.
9340
- *
9341
- * @example
9342
- * ```ts
9343
- * const merged = mergeThemes(defaultTheme, customTheme, overrides);
9344
- * ```
9345
- */
9346
- function mergeThemes(...themes) {
9347
- if (themes.length === 0) return { ...defaultTheme };
9348
- let result = {};
9349
- for (const theme of themes) result = deepMerge(result, theme);
9350
- return result;
9351
- }
9352
- /**
9353
- * Resolves a theme configuration by merging with its extends chain and defaults.
9354
- */
9355
- function resolveTheme(config) {
9356
- if (!config) return resolveTheme(defaultTheme);
9357
- const chain = [];
9358
- let current = config;
9359
- while (current) {
9360
- chain.unshift(current);
9361
- current = current.extends;
9362
- }
9363
- if (chain[0] !== defaultTheme && chain[0]?.name !== "default") chain.unshift(defaultTheme);
9364
- const merged = mergeThemes(...chain);
9365
- return {
9366
- name: merged.name ?? "custom",
9367
- colors: merged.colors ?? defaultTheme.colors,
9368
- darkColors: merged.darkColors ?? defaultTheme.darkColors,
9369
- fonts: merged.fonts ?? defaultTheme.fonts,
9370
- entryPage: merged.entryPage ?? defaultTheme.entryPage,
9371
- layout: merged.layout ?? defaultTheme.layout,
9372
- header: merged.header ?? defaultTheme.header,
9373
- footer: merged.footer ?? defaultTheme.footer,
9374
- socialLinks: merged.socialLinks ?? defaultTheme.socialLinks,
9375
- sidebar: merged.sidebar ?? [],
9376
- embed: merged.embed ?? {},
9377
- css: merged.css ?? "",
9378
- js: merged.js ?? ""
9379
- };
9380
- }
9381
- /**
9382
- * Converts resolved theme to the format expected by Rust NAPI.
9383
- */
9384
- function themeToNapi(theme) {
9385
- const socialLinks = socialLinksToNapi(theme.socialLinks);
9386
- return {
9387
- colors: theme.colors.primary ? {
9388
- primary: theme.colors.primary,
9389
- primaryHover: theme.colors.primaryHover,
9390
- background: theme.colors.background,
9391
- backgroundAlt: theme.colors.backgroundAlt,
9392
- text: theme.colors.text,
9393
- textMuted: theme.colors.textMuted,
9394
- border: theme.colors.border,
9395
- codeBackground: theme.colors.codeBackground,
9396
- codeText: theme.colors.codeText
9397
- } : void 0,
9398
- darkColors: theme.darkColors.primary ? {
9399
- primary: theme.darkColors.primary,
9400
- primaryHover: theme.darkColors.primaryHover,
9401
- background: theme.darkColors.background,
9402
- backgroundAlt: theme.darkColors.backgroundAlt,
9403
- text: theme.darkColors.text,
9404
- textMuted: theme.darkColors.textMuted,
9405
- border: theme.darkColors.border,
9406
- codeBackground: theme.darkColors.codeBackground,
9407
- codeText: theme.darkColors.codeText
9408
- } : void 0,
9409
- fonts: theme.fonts.sans ? {
9410
- sans: theme.fonts.sans,
9411
- mono: theme.fonts.mono
9412
- } : void 0,
9413
- entryPage: theme.entryPage.mode ? { mode: theme.entryPage.mode } : void 0,
9414
- layout: theme.layout.sidebarWidth ? {
9415
- sidebarWidth: theme.layout.sidebarWidth,
9416
- headerHeight: theme.layout.headerHeight,
9417
- maxContentWidth: theme.layout.maxContentWidth
9418
- } : void 0,
9419
- header: theme.header.logo || theme.header.logoLight || theme.header.logoDark ? {
9420
- logo: theme.header.logo,
9421
- logoLight: theme.header.logoLight,
9422
- logoDark: theme.header.logoDark,
9423
- showSiteNameText: theme.header.showSiteNameText,
9424
- logoWidth: theme.header.logoWidth,
9425
- logoHeight: theme.header.logoHeight
9426
- } : void 0,
9427
- footer: theme.footer.message || theme.footer.copyright ? {
9428
- message: theme.footer.message,
9429
- copyright: theme.footer.copyright
9430
- } : void 0,
9431
- socialLinks,
9432
- embed: Object.keys(theme.embed).length > 0 ? theme.embed : void 0,
9433
- css: theme.css || void 0,
9434
- js: theme.js || void 0
9435
- };
9436
- }
9437
- function socialLinksToNapi(links) {
9438
- if (Array.isArray(links)) {
9439
- const items = links.map((item) => {
9440
- return {
9441
- icon: typeof item.icon === "string" ? item.icon : void 0,
9442
- iconSvg: typeof item.icon === "object" ? item.icon.svg : void 0,
9443
- link: item.link,
9444
- ariaLabel: item.ariaLabel
9445
- };
9446
- });
9447
- return items.length > 0 ? { links: items } : void 0;
9448
- }
9449
- return links.github || links.twitter || links.discord ? {
9450
- github: links.github,
9451
- twitter: links.twitter,
9452
- discord: links.discord
9453
- } : void 0;
9454
- }
9455
- //#endregion
9456
8813
  //#region src/ssg.ts
9457
8814
  /**
9458
8815
  * SSG (Static Site Generation) module for ox-content
@@ -10817,7 +10174,7 @@ function resolveSsgOptions(ssg) {
10817
10174
  bare: false,
10818
10175
  generateOgImage: false,
10819
10176
  lastUpdated: false,
10820
- theme: resolveTheme(void 0)
10177
+ theme: require_vitepress.resolveTheme(void 0)
10821
10178
  };
10822
10179
  return {
10823
10180
  enabled: ssg.enabled ?? true,
@@ -10829,7 +10186,8 @@ function resolveSsgOptions(ssg) {
10829
10186
  generateOgImage: ssg.generateOgImage ?? false,
10830
10187
  lastUpdated: ssg.lastUpdated ?? false,
10831
10188
  siteUrl: ssg.siteUrl,
10832
- theme: resolveTheme(ssg.theme)
10189
+ theme: require_vitepress.resolveTheme(ssg.theme),
10190
+ navigation: ssg.navigation
10833
10191
  };
10834
10192
  }
10835
10193
  /**
@@ -10857,10 +10215,7 @@ function renderTemplate(template, data) {
10857
10215
  * Extracts title from content or frontmatter.
10858
10216
  */
10859
10217
  function extractTitle$1(content, frontmatter) {
10860
- if (frontmatter.title && typeof frontmatter.title === "string") return frontmatter.title;
10861
- const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
10862
- if (h1Match) return h1Match[1].trim();
10863
- return "Untitled";
10218
+ return require_mermaid.importNapiModuleSync().extractSsgTitle(content, typeof frontmatter.title === "string" ? frontmatter.title : void 0);
10864
10219
  }
10865
10220
  /**
10866
10221
  * Generates bare HTML page (no navigation, no styles).
@@ -10893,7 +10248,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10893
10248
  collapsed: group.collapsed,
10894
10249
  items: group.items.map(toRustNavItem)
10895
10250
  }));
10896
- const themeForRust = theme ? themeToNapi(theme) : void 0;
10251
+ const themeForRust = theme ? require_vitepress.themeToNapi(theme) : void 0;
10897
10252
  const entryPageForRust = pageData.entryPage ? {
10898
10253
  hero: pageData.entryPage.hero ? {
10899
10254
  name: pageData.entryPage.hero.name,
@@ -10946,223 +10301,88 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10946
10301
  }))
10947
10302
  });
10948
10303
  }
10949
- const SSG_STYLE_BLOCK_RE = /[ \t]*<!-- ox-content:styles:start -->\s*<style>([\s\S]*?)<\/style>\s*<!-- ox-content:styles:end -->/;
10950
- const SSG_SCRIPT_BLOCK_RE = /[ \t]*<!-- ox-content:scripts:start -->\s*<script>([\s\S]*?)<\/script>\s*<!-- ox-content:scripts:end -->/;
10951
- const FIRST_INLINE_STYLE_RE = /[ \t]*<style>([\s\S]*?)<\/style>/;
10952
- const LAST_INLINE_BODY_SCRIPT_RE = /[ \t]*<script>([\s\S]*?)<\/script>\s*<\/body>/;
10953
- const CSS_SECTION_RE = /\/\* ox-content:css:([a-z0-9-]+):start \*\/\s*([\s\S]*?)\s*\/\* ox-content:css:\1:end \*\//g;
10954
- const SEARCH_CHUNK_RE = /\/\/ ox-content:search:start\s*([\s\S]*?)\s*\/\/ ox-content:search:end/;
10955
- const SEARCH_CHUNK_PLACEHOLDER = "__OX_CONTENT_SEARCH_CHUNK__";
10956
- const CORE_CSS_SECTION_NAMES = new Set(["base", "footer"]);
10957
- const THEME_INLINE_CSS_MAX_BYTES = 2048;
10958
- function createContentHash(content) {
10959
- return (0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 10);
10960
- }
10961
- function sanitizeChunkLabel(label) {
10962
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "asset";
10963
- }
10964
- function toPublicAssetPath(base, fileName) {
10965
- return `${base.endsWith("/") ? base : `${base}/`}assets/${fileName}`;
10966
- }
10967
- function hasRelativeCssUrls(css) {
10968
- let cursor = 0;
10969
- while (cursor < css.length) {
10970
- const urlIndex = css.indexOf("url(", cursor);
10971
- if (urlIndex === -1) return false;
10972
- let valueStart = urlIndex + 4;
10973
- while (valueStart < css.length && /\s/.test(css[valueStart])) valueStart++;
10974
- const quote = css[valueStart] === "\"" || css[valueStart] === "'" ? css[valueStart] : "";
10975
- if (quote) valueStart++;
10976
- let valueEnd = valueStart;
10977
- while (valueEnd < css.length) {
10978
- const char = css[valueEnd];
10979
- if (quote) {
10980
- if (char === "\\") {
10981
- valueEnd += 2;
10982
- continue;
10983
- }
10984
- if (char === quote) break;
10985
- } else if (char === ")") break;
10986
- valueEnd++;
10987
- }
10988
- const value = css.slice(valueStart, valueEnd).trim();
10989
- if (value && !value.startsWith("data:") && !value.startsWith("http:") && !value.startsWith("https:") && !value.startsWith("//") && !value.startsWith("/") && !value.startsWith("#") && !value.startsWith("blob:") && !value.startsWith("var(")) return true;
10990
- cursor = valueEnd + 1;
10991
- }
10992
- return false;
10993
- }
10994
- function createSharedAssetChunk(type, label, content, outDir, base) {
10995
- const hash = createContentHash(content);
10996
- const fileName = `ox-content-${sanitizeChunkLabel(label)}-${hash}.${type}`;
10997
- return {
10998
- outputPath: path.join(outDir, "assets", fileName),
10999
- publicPath: toPublicAssetPath(base, fileName),
11000
- content
11001
- };
11002
- }
11003
- function extractCssSections(cssContent) {
11004
- return Array.from(cssContent.matchAll(CSS_SECTION_RE)).map(([, name, content]) => ({
11005
- name,
11006
- content: content.trim()
11007
- })).filter((section) => section.content.length > 0);
11008
- }
11009
- function getOrCreateSharedChunk(chunks, type, label, content, outDir, base) {
11010
- let chunk = chunks.get(content);
11011
- if (!chunk) {
11012
- chunk = createSharedAssetChunk(type, label, content, outDir, base);
11013
- chunks.set(content, chunk);
11014
- }
11015
- return chunk;
11016
- }
11017
- function buildStyleReplacement(cssContent, cssChunks, outDir, base) {
11018
- const sections = extractCssSections(cssContent);
11019
- const effectiveSections = sections.length > 0 ? sections : [{
11020
- name: "css",
11021
- content: cssContent.trim()
11022
- }];
11023
- const coreContent = effectiveSections.filter((section) => CORE_CSS_SECTION_NAMES.has(section.name)).map((section) => section.content).join("\n").trim();
11024
- const fragments = [];
11025
- if (coreContent) {
11026
- const coreChunk = getOrCreateSharedChunk(cssChunks, "css", "core", coreContent, outDir, base);
11027
- fragments.push(` <link rel="stylesheet" href="${coreChunk.publicPath}">`);
11028
- }
11029
- for (const section of effectiveSections) {
11030
- if (CORE_CSS_SECTION_NAMES.has(section.name)) continue;
11031
- if (section.name === "theme" && (hasRelativeCssUrls(section.content) || section.content.length <= THEME_INLINE_CSS_MAX_BYTES) || hasRelativeCssUrls(section.content)) {
11032
- fragments.push(` <style>${section.content}</style>`);
11033
- continue;
11034
- }
11035
- const chunk = getOrCreateSharedChunk(cssChunks, "css", section.name, section.content, outDir, base);
11036
- fragments.push(` <link rel="stylesheet" href="${chunk.publicPath}">`);
11037
- }
11038
- return fragments.join("\n");
11039
- }
11040
- function buildScriptReplacement(jsContent, jsChunks, outDir, base) {
11041
- const searchMatch = jsContent.match(SEARCH_CHUNK_RE);
11042
- if (searchMatch && jsContent.includes(SEARCH_CHUNK_PLACEHOLDER)) {
11043
- const searchContent = searchMatch[1].trim();
11044
- if (searchContent) {
11045
- const searchChunk = getOrCreateSharedChunk(jsChunks, "js", "search", searchContent, outDir, base);
11046
- const coreContent = jsContent.replace(SEARCH_CHUNK_RE, "").replaceAll(SEARCH_CHUNK_PLACEHOLDER, searchChunk.publicPath).trim();
11047
- if (coreContent) return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "core", coreContent, outDir, base).publicPath}"><\/script>`;
11048
- }
11049
- }
11050
- const fallbackContent = jsContent.trim();
11051
- if (!fallbackContent) return "";
11052
- return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "js", fallbackContent, outDir, base).publicPath}"><\/script>`;
11053
- }
11054
10304
  async function externalizeSharedPageAssets(pages, outDir, base) {
11055
- const cssChunks = /* @__PURE__ */ new Map();
11056
- const jsChunks = /* @__PURE__ */ new Map();
11057
- const optimizedPages = pages.map((page) => {
11058
- let html = page.html;
11059
- const styleMatch = html.match(SSG_STYLE_BLOCK_RE);
11060
- if (styleMatch) {
11061
- const replacement = buildStyleReplacement(styleMatch[1], cssChunks, outDir, base);
11062
- html = html.replace(SSG_STYLE_BLOCK_RE, replacement);
11063
- } else {
11064
- const inlineStyleMatch = html.match(FIRST_INLINE_STYLE_RE);
11065
- if (inlineStyleMatch) {
11066
- const replacement = buildStyleReplacement(inlineStyleMatch[1], cssChunks, outDir, base);
11067
- html = html.replace(FIRST_INLINE_STYLE_RE, replacement);
11068
- }
11069
- }
11070
- const scriptMatch = html.match(SSG_SCRIPT_BLOCK_RE);
11071
- if (scriptMatch) {
11072
- const replacement = buildScriptReplacement(scriptMatch[1], jsChunks, outDir, base);
11073
- html = html.replace(SSG_SCRIPT_BLOCK_RE, replacement);
11074
- } else {
11075
- const inlineScriptMatch = html.match(LAST_INLINE_BODY_SCRIPT_RE);
11076
- if (inlineScriptMatch) {
11077
- const replacement = buildScriptReplacement(inlineScriptMatch[1], jsChunks, outDir, base);
11078
- html = html.replace(LAST_INLINE_BODY_SCRIPT_RE, replacement ? `${replacement}\n</body>` : "</body>");
11079
- }
11080
- }
11081
- return {
11082
- ...page,
11083
- html
11084
- };
11085
- });
11086
- const chunks = [...cssChunks.values(), ...jsChunks.values()];
11087
- await Promise.all(chunks.map(async (chunk) => {
11088
- await fs_promises.mkdir(path.dirname(chunk.outputPath), { recursive: true });
11089
- await fs_promises.writeFile(chunk.outputPath, chunk.content, "utf-8");
10305
+ const optimized = (await require_mermaid.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
10306
+ await Promise.all(optimized.assets.map(async (asset) => {
10307
+ await fs_promises.mkdir(path.dirname(asset.outputPath), { recursive: true });
10308
+ await fs_promises.writeFile(asset.outputPath, asset.content, "utf-8");
11090
10309
  }));
11091
10310
  return {
11092
- pages: optimizedPages,
11093
- assets: chunks.map((chunk) => chunk.outputPath)
10311
+ pages: optimized.pages,
10312
+ assets: optimized.assets.map((asset) => asset.outputPath)
11094
10313
  };
11095
10314
  }
11096
10315
  /**
11097
- * Converts a markdown file path to its corresponding HTML output path.
11098
- */
11099
- function getOutputPath(inputPath, srcDir, outDir, extension) {
11100
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, extension);
11101
- if (baseName.endsWith(`index${extension}`)) return path.join(outDir, baseName);
11102
- const dirName = baseName.replace(new RegExp(`\\${extension}$`), "");
11103
- return path.join(outDir, dirName, `index${extension}`);
11104
- }
11105
- /**
11106
10316
  * Converts a markdown file path to a relative URL path.
11107
10317
  */
11108
10318
  function getUrlPath$1(inputPath, srcDir) {
11109
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
11110
- if (baseName === "index" || baseName.endsWith("/index")) return baseName.replace(/\/?index$/, "") || "/";
11111
- return baseName;
10319
+ return require_mermaid.importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
11112
10320
  }
11113
- /**
11114
- * Converts a markdown file path to an href.
11115
- */
11116
- function getHref(inputPath, srcDir, base, extension) {
11117
- const urlPath = getUrlPath$1(inputPath, srcDir);
11118
- if (urlPath === "/" || urlPath === "") return `${base}index${extension}`;
11119
- return `${base}${urlPath}/index${extension}`;
10321
+ function isExternalHref(value) {
10322
+ return /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//");
11120
10323
  }
11121
- function getPageLocale(urlPath, i18n) {
11122
- if (!i18n) return void 0;
11123
- const firstSegment = urlPath.split("/").filter(Boolean)[0];
11124
- return i18n.locales.some((l) => l.code === firstSegment) ? firstSegment : i18n.defaultLocale;
10324
+ function splitHrefSuffix(value) {
10325
+ const match = /^([^?#]*)([?#].*)?$/.exec(value);
10326
+ return {
10327
+ pathname: match?.[1] ?? value,
10328
+ suffix: match?.[2] ?? ""
10329
+ };
11125
10330
  }
11126
- /**
11127
- * Gets the OG image output path for a given markdown file.
11128
- */
11129
- function getOgImagePath(inputPath, srcDir, outDir) {
11130
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
11131
- if (baseName === "index" || baseName.endsWith("/index")) {
11132
- const dirPath = baseName.replace(/\/?index$/, "") || "";
11133
- return path.join(outDir, dirPath, "og-image.png");
11134
- }
11135
- return path.join(outDir, baseName, "og-image.png");
10331
+ function normalizeNavigationPath(value) {
10332
+ const { pathname, suffix } = splitHrefSuffix(value.trim());
10333
+ let normalized = pathname || "/";
10334
+ if (!normalized.startsWith("/")) normalized = `/${normalized}`;
10335
+ normalized = normalized.replace(/\/index(?:\.(?:html?|md|markdown))?$/i, "/").replace(/\.(?:html?|md|markdown)$/i, "");
10336
+ if (normalized !== "/") normalized = normalized.replace(/\/+$/, "");
10337
+ return {
10338
+ path: normalized || "/",
10339
+ suffix
10340
+ };
11136
10341
  }
11137
- /**
11138
- * Gets the OG image URL for use in meta tags.
11139
- * If siteUrl is provided, returns an absolute URL (required for SNS sharing).
11140
- */
11141
- function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
11142
- const urlPath = getUrlPath$1(inputPath, srcDir);
11143
- let relativePath;
11144
- if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
11145
- else relativePath = `${base}${urlPath}/og-image.png`;
11146
- if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
11147
- return relativePath;
10342
+ function buildHrefFromNavigationPath(pathname, base, extension) {
10343
+ if (pathname === "/" || pathname === "") return `${base}index${extension}`;
10344
+ return `${base}${pathname.replace(/^\/+/, "")}/index${extension}`;
11148
10345
  }
11149
10346
  /**
11150
- * Gets display title from file path.
10347
+ * Resolves manual navigation config to the format used by the built-in SSG renderer.
11151
10348
  */
11152
- function getDisplayTitle(filePath) {
11153
- const fileName = path.basename(filePath, path.extname(filePath));
11154
- if (fileName === "index") {
11155
- const dirName = path.basename(path.dirname(filePath));
11156
- if (dirName && dirName !== ".") return formatTitle(dirName);
11157
- return "Home";
11158
- }
11159
- return formatTitle(fileName);
10349
+ function resolveNavigationGroups(navigation, base, extension) {
10350
+ if (!navigation) return;
10351
+ return navigation.map((group) => ({
10352
+ title: group.title,
10353
+ items: group.items.flatMap((item) => {
10354
+ const rawHref = item.href ?? item.path;
10355
+ if (!rawHref) return [];
10356
+ if (isExternalHref(rawHref) || rawHref.startsWith("#")) return [{
10357
+ title: item.title,
10358
+ path: item.path ?? rawHref,
10359
+ href: rawHref
10360
+ }];
10361
+ const { path: path$2 } = normalizeNavigationPath(item.path ?? rawHref);
10362
+ const href = item.href ? (() => {
10363
+ const normalized = normalizeNavigationPath(item.href);
10364
+ return `${buildHrefFromNavigationPath(normalized.path, base, extension)}${normalized.suffix}`;
10365
+ })() : buildHrefFromNavigationPath(path$2, base, extension);
10366
+ return [{
10367
+ title: item.title,
10368
+ path: path$2,
10369
+ href
10370
+ }];
10371
+ })
10372
+ }));
10373
+ }
10374
+ function getPageLocale(urlPath, i18n) {
10375
+ if (!i18n) return void 0;
10376
+ return require_mermaid.importNapiModuleSync().getSsgPageLocale(urlPath, i18n.defaultLocale, i18n.locales.map((locale) => locale.code)) ?? void 0;
10377
+ }
10378
+ function getRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl) {
10379
+ return require_mermaid.importNapiModuleSync().resolveSsgRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl);
11160
10380
  }
11161
10381
  /**
11162
10382
  * Formats a file/dir name as a title.
11163
10383
  */
11164
10384
  function formatTitle(name) {
11165
- return name.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
10385
+ return require_mermaid.importNapiModuleSync().formatSsgTitle(name);
11166
10386
  }
11167
10387
  /**
11168
10388
  * Collects all markdown files from the source directory.
@@ -11181,110 +10401,13 @@ async function collectMarkdownFiles$1(srcDir) {
11181
10401
  * Builds navigation items from markdown files, grouped by directory.
11182
10402
  */
11183
10403
  function buildNavItems(markdownFiles, srcDir, base, extension) {
11184
- const groups = /* @__PURE__ */ new Map();
11185
- const groupOrder = [
11186
- "",
11187
- "examples",
11188
- "packages",
11189
- "api"
11190
- ];
11191
- for (const file of markdownFiles) {
11192
- const parts = path.relative(srcDir, file).split(path.sep);
11193
- let groupKey = "";
11194
- if (parts.length > 1) groupKey = parts[0];
11195
- if (!groups.has(groupKey)) groups.set(groupKey, []);
11196
- const urlPath = getUrlPath$1(file, srcDir);
11197
- let title;
11198
- if (urlPath === "/" || urlPath === "") title = "Overview";
11199
- else title = getDisplayTitle(file);
11200
- groups.get(groupKey).push({
11201
- title,
11202
- path: urlPath,
11203
- href: getHref(file, srcDir, base, extension)
11204
- });
11205
- }
11206
- const sortItems = (items) => {
11207
- return items.sort((a, b) => {
11208
- const aIsRoot = a.path === "/" || a.path === "";
11209
- const bIsRoot = b.path === "/" || b.path === "";
11210
- if (aIsRoot && !bIsRoot) return -1;
11211
- if (!aIsRoot && bIsRoot) return 1;
11212
- return a.title.localeCompare(b.title);
11213
- });
11214
- };
11215
- const result = [];
11216
- for (const key of groupOrder) {
11217
- const items = groups.get(key);
11218
- if (items && items.length > 0) {
11219
- result.push({
11220
- title: key === "" ? "Guide" : formatTitle(key),
11221
- items: sortItems(items)
11222
- });
11223
- groups.delete(key);
11224
- }
11225
- }
11226
- for (const [key, items] of groups) if (items.length > 0) result.push({
11227
- title: formatTitle(key),
11228
- items: sortItems(items)
11229
- });
11230
- return result;
11231
- }
11232
- function isSafeSidebarLink(link) {
11233
- const trimmed = link.trim();
11234
- if (trimmed.startsWith("//")) return false;
11235
- return !/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || /^(https?:|mailto:)/i.test(trimmed);
11236
- }
11237
- function sidebarPath(link) {
11238
- if (!link || !isSafeSidebarLink(link)) return "";
11239
- if (/^(https?:|mailto:|#)/i.test(link.trim())) return "";
11240
- const bare = link.trim().split("#", 1)[0].split("?", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11241
- if (!bare || bare === "index") return "/";
11242
- return bare.replace(/\/index$/, "");
11243
- }
11244
- function sidebarHref(link, base, extension) {
11245
- if (!link) return "#";
11246
- const trimmed = link.trim();
11247
- if (!isSafeSidebarLink(trimmed)) return "#";
11248
- if (/^(https?:|mailto:|#)/i.test(trimmed)) return trimmed;
11249
- const hash = trimmed.includes("#") ? `#${trimmed.split("#").slice(1).join("#")}` : "";
11250
- const withoutExt = trimmed.split("#", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11251
- return `${base}${!withoutExt || withoutExt === "index" ? "index" : `${withoutExt.replace(/\/index$/, "")}/index`}${extension}${hash}`;
10404
+ return require_mermaid.importNapiModuleSync().buildSsgNavItems(markdownFiles, srcDir, base, extension);
11252
10405
  }
11253
10406
  /**
11254
10407
  * Builds navigation items from an explicit theme sidebar tree.
11255
10408
  */
11256
10409
  function buildThemeNavItems(sidebar, base, extension) {
11257
- const toNavItem = (item) => {
11258
- const navItem = {
11259
- title: item.text ?? item.link ?? "Untitled",
11260
- path: sidebarPath(item.link),
11261
- href: sidebarHref(item.link, base, extension)
11262
- };
11263
- if (item.items?.length) navItem.children = item.items.map(toNavItem);
11264
- if (item.collapsed !== void 0) navItem.collapsed = item.collapsed;
11265
- return navItem;
11266
- };
11267
- const groups = [];
11268
- let looseItems = [];
11269
- const flushLooseItems = () => {
11270
- if (looseItems.length > 0) {
11271
- groups.push({
11272
- title: "Guide",
11273
- items: looseItems
11274
- });
11275
- looseItems = [];
11276
- }
11277
- };
11278
- for (const item of sidebar) if (item.items?.length && !item.link) {
11279
- flushLooseItems();
11280
- groups.push({
11281
- title: item.text ?? "Guide",
11282
- items: item.items.map(toNavItem),
11283
- collapsed: item.collapsed
11284
- });
11285
- } else looseItems.push(toNavItem(item));
11286
- flushLooseItems();
11287
- return groups;
10410
+ return require_mermaid.importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
11288
10411
  }
11289
10412
  /**
11290
10413
  * Builds all markdown files to static HTML.
@@ -11308,7 +10431,7 @@ async function buildSsg(options, root) {
11308
10431
  });
11309
10432
  } catch {}
11310
10433
  const markdownFiles = await collectMarkdownFiles$1(srcDir);
11311
- const navItems = ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension);
10434
+ const navItems = resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension));
11312
10435
  let siteName = ssgOptions.siteName ?? "Documentation";
11313
10436
  if (!ssgOptions.siteName) try {
11314
10437
  const pkgPath = path.join(root, "package.json");
@@ -11327,6 +10450,7 @@ async function buildSsg(options, root) {
11327
10450
  baseUrl: base,
11328
10451
  sourcePath: inputPath
11329
10452
  });
10453
+ const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
11330
10454
  let transformedHtml = result.html;
11331
10455
  const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(transformedHtml);
11332
10456
  transformedHtml = protectedHtml;
@@ -11341,20 +10465,21 @@ async function buildSsg(options, root) {
11341
10465
  transformedHtml = await transformAllPlugins(transformedHtml, pluginOptions);
11342
10466
  if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
11343
10467
  transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
11344
- const title = extractTitle$1(transformedHtml, result.frontmatter);
11345
- const description = result.frontmatter.description;
10468
+ const title = extractTitle$1(transformedHtml, frontmatter);
10469
+ const description = frontmatter.description;
10470
+ const routePaths = getRoutePaths(inputPath, srcDir, outDir, base, ssgOptions.extension, ssgOptions.siteUrl);
11346
10471
  pageResults.push({
11347
10472
  inputPath,
10473
+ routePaths,
11348
10474
  transformedHtml,
11349
10475
  title,
11350
10476
  description,
11351
10477
  lastUpdated: napi?.getGitLastUpdated(inputPath, root) ?? void 0,
11352
- frontmatter: result.frontmatter,
10478
+ frontmatter,
11353
10479
  toc: result.toc
11354
10480
  });
11355
10481
  if (shouldGenerateOgImages) {
11356
- const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
11357
- const { layout: _layout, ...frontmatterRest } = result.frontmatter;
10482
+ const { layout: _layout, ...frontmatterRest } = frontmatter;
11358
10483
  ogImageEntries.push({
11359
10484
  props: {
11360
10485
  ...frontmatterRest,
@@ -11362,10 +10487,10 @@ async function buildSsg(options, root) {
11362
10487
  description,
11363
10488
  siteName
11364
10489
  },
11365
- outputPath: ogImageOutputPath
10490
+ outputPath: routePaths.ogImagePath
11366
10491
  });
11367
10492
  ogImageInputPaths.push(inputPath);
11368
- ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
10493
+ ogImageUrlMap.set(inputPath, routePaths.ogImageUrl);
11369
10494
  }
11370
10495
  } catch (err) {
11371
10496
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -11394,7 +10519,7 @@ async function buildSsg(options, root) {
11394
10519
  ogImageUrlMap.clear();
11395
10520
  }
11396
10521
  for (const pageResult of pageResults) try {
11397
- const { inputPath, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
10522
+ const { inputPath, routePaths, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
11398
10523
  let pageOgImage = ssgOptions.ogImage;
11399
10524
  if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
11400
10525
  let entryPage;
@@ -11412,16 +10537,15 @@ async function buildSsg(options, root) {
11412
10537
  toc,
11413
10538
  lastUpdated,
11414
10539
  frontmatter,
11415
- path: getUrlPath$1(inputPath, srcDir),
11416
- href: getHref(inputPath, srcDir, base, ssgOptions.extension),
10540
+ path: routePaths.urlPath,
10541
+ href: routePaths.href,
11417
10542
  entryPage
11418
10543
  };
11419
10544
  html = await generateHtmlPage(pageData, navItems, siteName, base, pageOgImage, ssgOptions.theme, getPageLocale(pageData.path, options.i18n), options.i18n ? options.i18n.locales : void 0);
11420
10545
  }
11421
- const outputPath = getOutputPath(inputPath, srcDir, outDir, ssgOptions.extension);
11422
10546
  generatedPages.push({
11423
10547
  inputPath,
11424
- outputPath,
10548
+ outputPath: routePaths.outputPath,
11425
10549
  html
11426
10550
  });
11427
10551
  } catch (err) {
@@ -11549,234 +10673,7 @@ async function writeSearchIndex(indexJson, outDir) {
11549
10673
  * This is injected into the bundle as a virtual module.
11550
10674
  */
11551
10675
  function generateSearchModule(options, indexPath) {
11552
- return `
11553
- // Search module generated by ox-content
11554
- const searchOptions = ${JSON.stringify(options)};
11555
-
11556
- let searchIndex = null;
11557
- let indexPromise = null;
11558
-
11559
- function parseScopedQuery(query) {
11560
- const scopes = [];
11561
- const terms = [];
11562
-
11563
- for (const part of query.trim().split(/\\s+/).filter(Boolean)) {
11564
- if (part.startsWith('@') && part.length > 1) {
11565
- scopes.push(part.slice(1).toLowerCase());
11566
- } else {
11567
- terms.push(part);
11568
- }
11569
- }
11570
-
11571
- return {
11572
- text: terms.join(' ').trim(),
11573
- scopes: [...new Set(scopes)],
11574
- };
11575
- }
11576
-
11577
- function getScopesForDoc(doc) {
11578
- const source = (doc.id || doc.url || '').replace(/^\\/+/, '').toLowerCase();
11579
- const segments = source.split('/').filter(Boolean);
11580
-
11581
- if (segments.length <= 1) {
11582
- return [];
11583
- }
11584
-
11585
- const scopes = [];
11586
- let current = '';
11587
- for (const segment of segments.slice(0, -1)) {
11588
- current = current ? current + '/' + segment : segment;
11589
- scopes.push(current);
11590
- }
11591
-
11592
- return scopes;
11593
- }
11594
-
11595
- function matchesScopes(doc, scopes) {
11596
- if (!scopes.length) {
11597
- return true;
11598
- }
11599
-
11600
- const docScopes = new Set(getScopesForDoc(doc));
11601
- return scopes.some(scope => docScopes.has(scope));
11602
- }
11603
-
11604
- // Tokenizer for queries
11605
- function tokenizeQuery(text) {
11606
- const tokens = [];
11607
- let current = '';
11608
-
11609
- for (const char of text) {
11610
- const isCjk = /[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\u3040-\\u309F\\u30A0-\\u30FF\\uAC00-\\uD7AF]/.test(char);
11611
-
11612
- if (isCjk) {
11613
- if (current) {
11614
- tokens.push(current.toLowerCase());
11615
- current = '';
11616
- }
11617
- tokens.push(char);
11618
- } else if (/[a-zA-Z0-9_]/.test(char)) {
11619
- current += char;
11620
- } else if (current) {
11621
- tokens.push(current.toLowerCase());
11622
- current = '';
11623
- }
11624
- }
11625
-
11626
- if (current) {
11627
- tokens.push(current.toLowerCase());
11628
- }
11629
-
11630
- return tokens;
11631
- }
11632
-
11633
- // BM25 scoring
11634
- function computeIdf(df, docCount) {
11635
- return Math.log((docCount - df + 0.5) / (df + 0.5) + 1.0);
11636
- }
11637
-
11638
- function getFieldBoost(field) {
11639
- switch (field) {
11640
- case 'Title': return 10.0;
11641
- case 'Heading': return 5.0;
11642
- case 'Body': return 1.0;
11643
- case 'Code': return 0.5;
11644
- default: return 1.0;
11645
- }
11646
- }
11647
-
11648
- // Load the index
11649
- async function loadIndex() {
11650
- if (searchIndex) return searchIndex;
11651
- if (indexPromise) return indexPromise;
11652
-
11653
- indexPromise = fetch('${indexPath}')
11654
- .then(res => res.json())
11655
- .then(data => {
11656
- searchIndex = data;
11657
- return data;
11658
- })
11659
- .catch(err => {
11660
- console.error('[ox-content] Failed to load search index:', err);
11661
- return null;
11662
- });
11663
-
11664
- return indexPromise;
11665
- }
11666
-
11667
- // Search function
11668
- export async function search(query, options = {}) {
11669
- const index = await loadIndex();
11670
-
11671
- if (!index) {
11672
- return [];
11673
- }
11674
-
11675
- const parsedQuery = parseScopedQuery(query);
11676
-
11677
- if (!parsedQuery.text && parsedQuery.scopes.length === 0) {
11678
- return [];
11679
- }
11680
-
11681
- const limit = options.limit ?? searchOptions.limit;
11682
- const prefix = options.prefix ?? searchOptions.prefix;
11683
- const tokens = tokenizeQuery(parsedQuery.text);
11684
-
11685
- const k1 = 1.2;
11686
- const b = 0.75;
11687
- const docScores = new Map();
11688
-
11689
- if (tokens.length === 0) {
11690
- index.documents.forEach((doc, docIdx) => {
11691
- if (matchesScopes(doc, parsedQuery.scopes)) {
11692
- docScores.set(docIdx, { score: 0, matches: new Set() });
11693
- }
11694
- });
11695
- }
11696
-
11697
- for (let i = 0; i < tokens.length; i++) {
11698
- const token = tokens[i];
11699
- const isLast = i === tokens.length - 1;
11700
-
11701
- // Find matching terms
11702
- let matchingTerms = [];
11703
- if (prefix && isLast && token.length >= 2) {
11704
- matchingTerms = Object.keys(index.index).filter(term => term.startsWith(token));
11705
- } else if (index.index[token]) {
11706
- matchingTerms = [token];
11707
- }
11708
-
11709
- for (const term of matchingTerms) {
11710
- const postings = index.index[term] || [];
11711
- const df = index.df[term] || 1;
11712
- const idf = computeIdf(df, index.doc_count);
11713
-
11714
- for (const posting of postings) {
11715
- const doc = index.documents[posting.doc_idx];
11716
- if (!doc) continue;
11717
- if (!matchesScopes(doc, parsedQuery.scopes)) continue;
11718
-
11719
- const docLen = doc.body.length;
11720
- const tf = posting.tf;
11721
- const boost = getFieldBoost(posting.field);
11722
-
11723
- const score = idf * ((tf * (k1 + 1.0)) / (tf + k1 * (1.0 - b + b * docLen / index.avg_dl))) * boost;
11724
-
11725
- if (!docScores.has(posting.doc_idx)) {
11726
- docScores.set(posting.doc_idx, { score: 0, matches: new Set() });
11727
- }
11728
- const entry = docScores.get(posting.doc_idx);
11729
- entry.score += score;
11730
- entry.matches.add(term);
11731
- }
11732
- }
11733
- }
11734
-
11735
- // Convert to results
11736
- const results = Array.from(docScores.entries())
11737
- .map(([docIdx, data]) => {
11738
- const doc = index.documents[docIdx];
11739
- const matches = Array.from(data.matches);
11740
- const scopes = getScopesForDoc(doc);
11741
-
11742
- // Generate snippet
11743
- let snippet = '';
11744
- if (doc.body) {
11745
- const bodyLower = doc.body.toLowerCase();
11746
- let firstPos = -1;
11747
- for (const match of matches) {
11748
- const pos = bodyLower.indexOf(match);
11749
- if (pos !== -1 && (firstPos === -1 || pos < firstPos)) {
11750
- firstPos = pos;
11751
- }
11752
- }
11753
-
11754
- const start = firstPos === -1 ? 0 : Math.max(0, firstPos - 50);
11755
- const end = Math.min(doc.body.length, start + 150);
11756
- snippet = doc.body.slice(start, end);
11757
- if (start > 0) snippet = '...' + snippet;
11758
- if (end < doc.body.length) snippet = snippet + '...';
11759
- }
11760
-
11761
- return {
11762
- id: doc.id,
11763
- title: doc.title,
11764
- url: doc.url,
11765
- score: data.score,
11766
- matches,
11767
- snippet,
11768
- scopes,
11769
- };
11770
- })
11771
- .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
11772
- .slice(0, limit);
11773
-
11774
- return results;
11775
- }
11776
-
11777
- export { searchOptions };
11778
- export default { search, searchOptions, loadIndex };
11779
- `;
10676
+ return require_mermaid.importNapiModuleSync().generateSearchModule(JSON.stringify(options), indexPath);
11780
10677
  }
11781
10678
  //#endregion
11782
10679
  //#region src/dev-server.ts
@@ -11909,6 +10806,7 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root)
11909
10806
  baseUrl: base,
11910
10807
  sourcePath: filePath
11911
10808
  });
10809
+ const frontmatter = require_vitepress.normalizeVitePressFrontmatter(result.frontmatter);
11912
10810
  let transformedHtml = result.html;
11913
10811
  const { html: protectedHtml, svgs: mermaidSvgs } = protectMermaidSvgs(transformedHtml);
11914
10812
  transformedHtml = protectedHtml;
@@ -11922,19 +10820,19 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root)
11922
10820
  });
11923
10821
  if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
11924
10822
  transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
11925
- const title = extractTitle$1(transformedHtml, result.frontmatter);
11926
- const description = result.frontmatter.description;
10823
+ const title = extractTitle$1(transformedHtml, frontmatter);
10824
+ const description = frontmatter.description;
11927
10825
  let entryPage;
11928
- if (result.frontmatter.layout === "entry") entryPage = {
11929
- hero: result.frontmatter.hero,
11930
- features: result.frontmatter.features
10826
+ if (frontmatter.layout === "entry") entryPage = {
10827
+ hero: frontmatter.hero,
10828
+ features: frontmatter.features
11931
10829
  };
11932
10830
  let html = await generateHtmlPage({
11933
10831
  title,
11934
10832
  description,
11935
10833
  content: transformedHtml,
11936
10834
  toc: result.toc,
11937
- frontmatter: result.frontmatter,
10835
+ frontmatter,
11938
10836
  path: getUrlPath$1(filePath, srcDir),
11939
10837
  href: getUrlPath$1(filePath, srcDir) || "/",
11940
10838
  entryPage
@@ -11965,7 +10863,10 @@ function createDevServerMiddleware(options, root, cache) {
11965
10863
  return;
11966
10864
  }
11967
10865
  if (!cache.siteName) cache.siteName = await resolveSiteName(options, root);
11968
- if (!cache.navGroups) cache.navGroups = buildNavItems(await collectMarkdownFiles$1(srcDir), srcDir, base, ".html");
10866
+ if (!cache.navGroups) {
10867
+ const markdownFiles = await collectMarkdownFiles$1(srcDir);
10868
+ cache.navGroups = resolveNavigationGroups(options.ssg.navigation, base, options.ssg.extension) ?? (options.ssg.theme?.sidebar.length ? buildThemeNavItems(options.ssg.theme.sidebar, base, options.ssg.extension) : buildNavItems(markdownFiles, srcDir, base, options.ssg.extension));
10869
+ }
11969
10870
  const html = await renderPage$1(filePath, options, cache.navGroups, cache.siteName, base, root);
11970
10871
  cache.pages.set(filePath, html);
11971
10872
  res.setHeader("Content-Type", "text/html");
@@ -12060,7 +10961,7 @@ async function collectPages(options, root) {
12060
10961
  const generateOgImage = options.ogImage || options.ssg.generateOgImage;
12061
10962
  for (const file of files.sort()) {
12062
10963
  const content = fs.readFileSync(file, "utf-8");
12063
- const frontmatter = parseFrontmatter(content);
10964
+ const frontmatter = require_vitepress.normalizeVitePressFrontmatter(parseFrontmatter(content));
12064
10965
  if (frontmatter.layout === "entry") continue;
12065
10966
  const title = extractTitle(content, frontmatter);
12066
10967
  const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
@@ -12432,193 +11333,18 @@ function createI18nPlugin(resolvedOptions) {
12432
11333
  */
12433
11334
  function generateI18nModule(options, root) {
12434
11335
  const dictDir = path.resolve(root, options.dir);
12435
- const localesJson = JSON.stringify(options.locales);
12436
- const defaultLocale = JSON.stringify(options.defaultLocale);
12437
- let dictionariesCode = "{}";
11336
+ const config = {
11337
+ defaultLocale: options.defaultLocale,
11338
+ locales: options.locales,
11339
+ hideDefaultLocale: options.hideDefaultLocale
11340
+ };
12438
11341
  try {
12439
11342
  const napi = require("@ox-content/napi");
12440
- if (napi.loadDictionariesFlat) {
12441
- const dictData = napi.loadDictionariesFlat(dictDir);
12442
- dictionariesCode = JSON.stringify(dictData);
12443
- } else dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
12444
- } catch {
12445
- try {
12446
- dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
12447
- } catch {}
12448
- }
12449
- return `
12450
- export const i18nConfig = {
12451
- enabled: true,
12452
- defaultLocale: ${defaultLocale},
12453
- locales: ${localesJson},
12454
- hideDefaultLocale: ${JSON.stringify(options.hideDefaultLocale)},
12455
- };
12456
-
12457
- export const dictionaries = ${dictionariesCode};
12458
-
12459
- export function t(key, params, locale) {
12460
- const dict = dictionaries[locale || i18nConfig.defaultLocale] || {};
12461
- let message = dict[key];
12462
- if (!message) {
12463
- const fallback = dictionaries[i18nConfig.defaultLocale] || {};
12464
- message = fallback[key] || key;
12465
- }
12466
- if (params) {
12467
- for (const [k, v] of Object.entries(params)) {
12468
- message = message.split('{$' + k + '}').join(String(v));
12469
- }
12470
- }
12471
- return message;
12472
- }
12473
-
12474
- export function getLocaleFromPath(pathname) {
12475
- const match = pathname.match(new RegExp('^/([A-Za-z]{2,3}(?:-[A-Za-z0-9]+)*)(/|$)'));
12476
- if (match) {
12477
- const code = match[1];
12478
- if (i18nConfig.locales.some(l => l.code === code)) {
12479
- return code;
12480
- }
12481
- }
12482
- return i18nConfig.defaultLocale;
12483
- }
12484
-
12485
- export function localePath(pathname, locale) {
12486
- const current = getLocaleFromPath(pathname);
12487
- let clean = pathname;
12488
- if (current !== i18nConfig.defaultLocale || !i18nConfig.hideDefaultLocale) {
12489
- const prefix = '/' + current;
12490
- if (clean === prefix) clean = '/';
12491
- else if (clean.startsWith(prefix + '/')) clean = clean.slice(prefix.length);
12492
- }
12493
- if (locale === i18nConfig.defaultLocale && i18nConfig.hideDefaultLocale) {
12494
- return clean || '/';
12495
- }
12496
- return '/' + locale + (clean.startsWith('/') ? clean : '/' + clean);
12497
- }
12498
-
12499
- const formatterCache = new Map();
12500
-
12501
- function getFormatter(kind, locale, options) {
12502
- const key = kind + ':' + locale + ':' + JSON.stringify(options || {});
12503
- if (!formatterCache.has(key)) {
12504
- formatterCache.set(key, new Intl[kind](locale, options));
12505
- }
12506
- return formatterCache.get(key);
12507
- }
12508
-
12509
- export function getLocaleMeta(locale) {
12510
- const code = locale || i18nConfig.defaultLocale;
12511
- return i18nConfig.locales.find(l => l.code === code) || { code, name: code, dir: 'ltr' };
12512
- }
12513
-
12514
- export function formatDate(value, options, locale) {
12515
- return getFormatter('DateTimeFormat', locale || i18nConfig.defaultLocale, options).format(
12516
- value instanceof Date ? value : new Date(value),
12517
- );
12518
- }
12519
-
12520
- export function formatDateParts(value, options, locale) {
12521
- return getFormatter('DateTimeFormat', locale || i18nConfig.defaultLocale, options).formatToParts(
12522
- value instanceof Date ? value : new Date(value),
12523
- );
12524
- }
12525
-
12526
- export function formatNumber(value, options, locale) {
12527
- return getFormatter('NumberFormat', locale || i18nConfig.defaultLocale, options).format(value);
12528
- }
12529
-
12530
- export function formatNumberParts(value, options, locale) {
12531
- return getFormatter('NumberFormat', locale || i18nConfig.defaultLocale, options).formatToParts(value);
12532
- }
12533
-
12534
- export function formatRelativeTime(value, unit, options, locale) {
12535
- return getFormatter('RelativeTimeFormat', locale || i18nConfig.defaultLocale, options).format(value, unit);
12536
- }
12537
-
12538
- export function formatList(values, options, locale) {
12539
- return getFormatter('ListFormat', locale || i18nConfig.defaultLocale, options).format(values);
12540
- }
12541
-
12542
- export function formatListParts(values, options, locale) {
12543
- return getFormatter('ListFormat', locale || i18nConfig.defaultLocale, options).formatToParts(values);
12544
- }
12545
-
12546
- export function formatDisplayName(value, type, options, locale) {
12547
- if (!Intl.DisplayNames) return String(value);
12548
- const displayType = type || 'language';
12549
- return getFormatter('DisplayNames', locale || i18nConfig.defaultLocale, { type: displayType, ...options }).of(value) || String(value);
12550
- }
12551
-
12552
- export function createIntl(locale, defaults = {}) {
12553
- const meta = getLocaleMeta(locale);
12554
- const code = meta.code;
12555
- return {
12556
- locale: code,
12557
- meta,
12558
- dir: meta.dir || 'ltr',
12559
- date: (value, options) => formatDate(value, { ...defaults.date, ...options }, code),
12560
- dateParts: (value, options) => formatDateParts(value, { ...defaults.date, ...options }, code),
12561
- number: (value, options) => formatNumber(value, { ...defaults.number, ...options }, code),
12562
- numberParts: (value, options) => formatNumberParts(value, { ...defaults.number, ...options }, code),
12563
- relativeTime: (value, unit, options) => formatRelativeTime(value, unit, { ...defaults.relativeTime, ...options }, code),
12564
- list: (values, options) => formatList(values, { ...defaults.list, ...options }, code),
12565
- listParts: (values, options) => formatListParts(values, { ...defaults.list, ...options }, code),
12566
- displayName: (value, type, options) => formatDisplayName(value, type, { ...defaults.displayName, ...options }, code),
12567
- };
12568
- }
12569
-
12570
- export default {
12571
- i18nConfig,
12572
- dictionaries,
12573
- t,
12574
- getLocaleFromPath,
12575
- localePath,
12576
- getLocaleMeta,
12577
- createIntl,
12578
- formatDate,
12579
- formatDateParts,
12580
- formatNumber,
12581
- formatNumberParts,
12582
- formatRelativeTime,
12583
- formatList,
12584
- formatListParts,
12585
- formatDisplayName,
12586
- };
12587
- `;
12588
- }
12589
- /**
12590
- * Flattens a nested object into dot-separated keys.
12591
- */
12592
- function flattenObject(obj, prefix, result) {
12593
- for (const [key, value] of Object.entries(obj)) {
12594
- const fullKey = `${prefix}.${key}`;
12595
- if (typeof value === "string") result[fullKey] = value;
12596
- else if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenObject(value, fullKey, result);
12597
- else result[fullKey] = String(value);
12598
- }
12599
- }
12600
- /**
12601
- * Fallback dictionary loading using TS-based JSON file reading.
12602
- */
12603
- function loadDictionariesFallback(options, dictDir) {
12604
- const dictData = {};
12605
- for (const locale of options.locales) {
12606
- const localeDir = path.join(dictDir, locale.code);
12607
- if (!fs.existsSync(localeDir)) continue;
12608
- const files = fs.readdirSync(localeDir);
12609
- const localeDict = {};
12610
- for (const file of files) {
12611
- if (!file.endsWith(".json")) continue;
12612
- const filePath = path.join(localeDir, file);
12613
- const content = fs.readFileSync(filePath, "utf-8");
12614
- const namespace = path.basename(file, ".json");
12615
- try {
12616
- flattenObject(JSON.parse(content), namespace, localeDict);
12617
- } catch {}
12618
- }
12619
- dictData[locale.code] = localeDict;
11343
+ if (typeof napi.generateI18nModule === "function") return napi.generateI18nModule(dictDir, config);
11344
+ } catch (error) {
11345
+ throw new Error(`[ox-content:i18n] Failed to load @ox-content/napi for i18n module generation: ${String(error)}`);
12620
11346
  }
12621
- return dictData;
11347
+ throw new Error("[ox-content:i18n] @ox-content/napi does not expose generateI18nModule. Please rebuild the NAPI package.");
12622
11348
  }
12623
11349
  /**
12624
11350
  * Collects translation keys from source files using NAPI extractTranslationKeys.
@@ -13814,9 +12540,9 @@ function resolveCodeAnnotationsOptions(options) {
13814
12540
  /**
13815
12541
  * Generates virtual module content.
13816
12542
  */
13817
- function generateVirtualModule(path$2, options) {
13818
- if (path$2 === "config") return `export default ${JSON.stringify(options)};`;
13819
- if (path$2 === "runtime") {
12543
+ function generateVirtualModule(path$1, options) {
12544
+ if (path$1 === "config") return `export default ${JSON.stringify(options)};`;
12545
+ if (path$1 === "runtime") {
13820
12546
  const base = normalizeRuntimeBase(options.base);
13821
12547
  return `
13822
12548
  export const base = ${JSON.stringify(base)};
@@ -13871,17 +12597,20 @@ exports.buildSsg = buildSsg;
13871
12597
  exports.clearRenderContext = clearRenderContext;
13872
12598
  exports.collectGitHubRepos = require_github.collectGitHubRepos;
13873
12599
  exports.collectOgpUrls = require_ogp.collectOgpUrls;
12600
+ exports.convertVitePressNav = require_vitepress.convertVitePressNav;
12601
+ exports.convertVitePressSidebar = require_vitepress.convertVitePressSidebar;
13874
12602
  exports.createI18nPlugin = createI18nPlugin;
13875
12603
  exports.createMarkdownEnvironment = createMarkdownEnvironment;
13876
12604
  exports.createTheme = createTheme;
13877
- exports.defaultTheme = defaultTheme;
13878
- exports.defineTheme = defineTheme;
12605
+ exports.defaultTheme = require_vitepress.defaultTheme;
12606
+ exports.defineTheme = require_vitepress.defineTheme;
13879
12607
  exports.each = each;
13880
12608
  exports.extractDocs = extractDocs;
13881
12609
  exports.extractIslandInfo = extractIslandInfo;
13882
12610
  exports.extractVideoId = require_youtube.extractVideoId;
13883
12611
  exports.fetchOgpData = require_ogp.fetchOgpData;
13884
12612
  exports.fetchRepoData = require_github.fetchRepoData;
12613
+ exports.fromVitePressConfig = require_vitepress.fromVitePressConfig;
13885
12614
  exports.generateFrontmatterTypes = generateFrontmatterTypes;
13886
12615
  exports.generateHydrationScript = generateHydrationScript;
13887
12616
  exports.generateMarkdown = generateMarkdown;
@@ -13889,6 +12618,7 @@ exports.generateOgImages = generateOgImages;
13889
12618
  exports.generateTabsCSS = require_tabs.generateTabsCSS;
13890
12619
  exports.generateTypes = generateTypes;
13891
12620
  exports.generateVirtualModule = generateVirtualModule;
12621
+ exports.generateVitePressMigrationConfig = require_vitepress.generateVitePressMigrationConfig;
13892
12622
  exports.hasIslands = hasIslands;
13893
12623
  exports.inferType = inferType;
13894
12624
  exports.jsx = jsx;
@@ -13897,8 +12627,9 @@ exports.lintMarkdown = lintMarkdown;
13897
12627
  exports.lintMarkdownAsync = lintMarkdownAsync;
13898
12628
  exports.lintMarkdownFile = lintMarkdownFile;
13899
12629
  exports.lintMarkdownFiles = lintMarkdownFiles;
13900
- exports.mergeThemes = mergeThemes;
12630
+ exports.mergeThemes = require_vitepress.mergeThemes;
13901
12631
  exports.mermaidClientScript = require_mermaid.mermaidClientScript;
12632
+ exports.normalizeVitePressFrontmatter = require_vitepress.normalizeVitePressFrontmatter;
13902
12633
  exports.oxContent = oxContent;
13903
12634
  exports.prefetchGitHubRepos = require_github.prefetchGitHubRepos;
13904
12635
  exports.prefetchOgpData = require_ogp.prefetchOgpData;
@@ -13911,7 +12642,7 @@ exports.resolveI18nOptions = resolveI18nOptions;
13911
12642
  exports.resolveOgImageOptions = resolveOgImageOptions;
13912
12643
  exports.resolveSearchOptions = resolveSearchOptions;
13913
12644
  exports.resolveSsgOptions = resolveSsgOptions;
13914
- exports.resolveTheme = resolveTheme;
12645
+ exports.resolveTheme = require_vitepress.resolveTheme;
13915
12646
  exports.setRenderContext = setRenderContext;
13916
12647
  exports.shouldLintMarkdownFile = shouldLintMarkdownFile;
13917
12648
  exports.transformAllPlugins = transformAllPlugins;