@ox-content/vite-plugin 2.7.0 → 2.9.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
@@ -17,7 +17,6 @@ let node_path = require("node:path");
17
17
  node_path = require_chunk.__toESM(node_path);
18
18
  let fs = require("fs");
19
19
  fs = require_chunk.__toESM(fs);
20
- let node_crypto = require("node:crypto");
21
20
  let fs_promises = require("fs/promises");
22
21
  fs_promises = require_chunk.__toESM(fs_promises);
23
22
  let glob = require("glob");
@@ -7087,241 +7086,11 @@ if (import.meta.hot) {
7087
7086
  }
7088
7087
  //#endregion
7089
7088
  //#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
7089
  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;
7090
+ return require_mermaid.importNapiModuleSync().generateDocsNavMetadata(docs.map((doc) => doc.file), basePath);
7254
7091
  }
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
7092
  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
- `;
7093
+ return require_mermaid.importNapiModuleSync().generateDocsNavCode(navItems, exportName);
7325
7094
  }
7326
7095
  //#endregion
7327
7096
  //#region src/docs.ts
@@ -7648,86 +7417,6 @@ function buildDocsData(docs) {
7648
7417
  }))
7649
7418
  };
7650
7419
  }
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
7420
  /**
7732
7421
  * Extracts JSDoc documentation from source files in specified directories.
7733
7422
  *
@@ -7794,13 +7483,13 @@ function mergeParam(params, next) {
7794
7483
  * ```
7795
7484
  */
7796
7485
  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.");
7486
+ const extractFileDocEntries = (await require_mermaid.importNapiModule()).extractFileDocEntries;
7487
+ if (!extractFileDocEntries) throw new Error("[ox-content] extractFileDocEntries is not available from @ox-content/napi.");
7799
7488
  const results = [];
7800
7489
  for (const srcDir of srcDirs) {
7801
7490
  const files = await findFiles(srcDir, options);
7802
7491
  for (const file of files) {
7803
- const entries = extractFileDocs(file, options.private).map(parseNapiDocItem).filter((entry) => Boolean(entry));
7492
+ const entries = extractFileDocEntries(file, options.private);
7804
7493
  if (entries.length > 0) results.push({
7805
7494
  file,
7806
7495
  entries
@@ -7851,132 +7540,6 @@ function isExcluded(file, patterns) {
7851
7540
  return false;
7852
7541
  });
7853
7542
  }
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
7543
  /**
7981
7544
  * Generates Markdown documentation from extracted docs.
7982
7545
  */
@@ -10857,10 +10420,7 @@ function renderTemplate(template, data) {
10857
10420
  * Extracts title from content or frontmatter.
10858
10421
  */
10859
10422
  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";
10423
+ return require_mermaid.importNapiModuleSync().extractSsgTitle(content, typeof frontmatter.title === "string" ? frontmatter.title : void 0);
10864
10424
  }
10865
10425
  /**
10866
10426
  * Generates bare HTML page (no navigation, no styles).
@@ -10874,7 +10434,7 @@ function generateBareHtmlPage(content, title) {
10874
10434
  /**
10875
10435
  * Generates HTML page with navigation using Rust NAPI bindings.
10876
10436
  */
10877
- async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme) {
10437
+ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales) {
10878
10438
  const mod = await require_mermaid.importNapiModule();
10879
10439
  const tocForRust = pageData.toc.map((entry) => ({
10880
10440
  depth: entry.depth,
@@ -10937,221 +10497,44 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10937
10497
  siteName,
10938
10498
  base,
10939
10499
  ogImage,
10940
- theme: themeForRust
10500
+ theme: themeForRust,
10501
+ locale,
10502
+ availableLocales: availableLocales?.map((l) => ({
10503
+ code: l.code,
10504
+ name: l.name,
10505
+ dir: l.dir ?? "ltr"
10506
+ }))
10941
10507
  });
10942
10508
  }
10943
- const SSG_STYLE_BLOCK_RE = /[ \t]*<!-- ox-content:styles:start -->\s*<style>([\s\S]*?)<\/style>\s*<!-- ox-content:styles:end -->/;
10944
- const SSG_SCRIPT_BLOCK_RE = /[ \t]*<!-- ox-content:scripts:start -->\s*<script>([\s\S]*?)<\/script>\s*<!-- ox-content:scripts:end -->/;
10945
- const FIRST_INLINE_STYLE_RE = /[ \t]*<style>([\s\S]*?)<\/style>/;
10946
- const LAST_INLINE_BODY_SCRIPT_RE = /[ \t]*<script>([\s\S]*?)<\/script>\s*<\/body>/;
10947
- const CSS_SECTION_RE = /\/\* ox-content:css:([a-z0-9-]+):start \*\/\s*([\s\S]*?)\s*\/\* ox-content:css:\1:end \*\//g;
10948
- const SEARCH_CHUNK_RE = /\/\/ ox-content:search:start\s*([\s\S]*?)\s*\/\/ ox-content:search:end/;
10949
- const SEARCH_CHUNK_PLACEHOLDER = "__OX_CONTENT_SEARCH_CHUNK__";
10950
- const CORE_CSS_SECTION_NAMES = new Set(["base", "footer"]);
10951
- const THEME_INLINE_CSS_MAX_BYTES = 2048;
10952
- function createContentHash(content) {
10953
- return (0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 10);
10954
- }
10955
- function sanitizeChunkLabel(label) {
10956
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "asset";
10957
- }
10958
- function toPublicAssetPath(base, fileName) {
10959
- return `${base.endsWith("/") ? base : `${base}/`}assets/${fileName}`;
10960
- }
10961
- function hasRelativeCssUrls(css) {
10962
- let cursor = 0;
10963
- while (cursor < css.length) {
10964
- const urlIndex = css.indexOf("url(", cursor);
10965
- if (urlIndex === -1) return false;
10966
- let valueStart = urlIndex + 4;
10967
- while (valueStart < css.length && /\s/.test(css[valueStart])) valueStart++;
10968
- const quote = css[valueStart] === "\"" || css[valueStart] === "'" ? css[valueStart] : "";
10969
- if (quote) valueStart++;
10970
- let valueEnd = valueStart;
10971
- while (valueEnd < css.length) {
10972
- const char = css[valueEnd];
10973
- if (quote) {
10974
- if (char === "\\") {
10975
- valueEnd += 2;
10976
- continue;
10977
- }
10978
- if (char === quote) break;
10979
- } else if (char === ")") break;
10980
- valueEnd++;
10981
- }
10982
- const value = css.slice(valueStart, valueEnd).trim();
10983
- 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;
10984
- cursor = valueEnd + 1;
10985
- }
10986
- return false;
10987
- }
10988
- function createSharedAssetChunk(type, label, content, outDir, base) {
10989
- const hash = createContentHash(content);
10990
- const fileName = `ox-content-${sanitizeChunkLabel(label)}-${hash}.${type}`;
10991
- return {
10992
- outputPath: path.join(outDir, "assets", fileName),
10993
- publicPath: toPublicAssetPath(base, fileName),
10994
- content
10995
- };
10996
- }
10997
- function extractCssSections(cssContent) {
10998
- return Array.from(cssContent.matchAll(CSS_SECTION_RE)).map(([, name, content]) => ({
10999
- name,
11000
- content: content.trim()
11001
- })).filter((section) => section.content.length > 0);
11002
- }
11003
- function getOrCreateSharedChunk(chunks, type, label, content, outDir, base) {
11004
- let chunk = chunks.get(content);
11005
- if (!chunk) {
11006
- chunk = createSharedAssetChunk(type, label, content, outDir, base);
11007
- chunks.set(content, chunk);
11008
- }
11009
- return chunk;
11010
- }
11011
- function buildStyleReplacement(cssContent, cssChunks, outDir, base) {
11012
- const sections = extractCssSections(cssContent);
11013
- const effectiveSections = sections.length > 0 ? sections : [{
11014
- name: "css",
11015
- content: cssContent.trim()
11016
- }];
11017
- const coreContent = effectiveSections.filter((section) => CORE_CSS_SECTION_NAMES.has(section.name)).map((section) => section.content).join("\n").trim();
11018
- const fragments = [];
11019
- if (coreContent) {
11020
- const coreChunk = getOrCreateSharedChunk(cssChunks, "css", "core", coreContent, outDir, base);
11021
- fragments.push(` <link rel="stylesheet" href="${coreChunk.publicPath}">`);
11022
- }
11023
- for (const section of effectiveSections) {
11024
- if (CORE_CSS_SECTION_NAMES.has(section.name)) continue;
11025
- if (section.name === "theme" && (hasRelativeCssUrls(section.content) || section.content.length <= THEME_INLINE_CSS_MAX_BYTES) || hasRelativeCssUrls(section.content)) {
11026
- fragments.push(` <style>${section.content}</style>`);
11027
- continue;
11028
- }
11029
- const chunk = getOrCreateSharedChunk(cssChunks, "css", section.name, section.content, outDir, base);
11030
- fragments.push(` <link rel="stylesheet" href="${chunk.publicPath}">`);
11031
- }
11032
- return fragments.join("\n");
11033
- }
11034
- function buildScriptReplacement(jsContent, jsChunks, outDir, base) {
11035
- const searchMatch = jsContent.match(SEARCH_CHUNK_RE);
11036
- if (searchMatch && jsContent.includes(SEARCH_CHUNK_PLACEHOLDER)) {
11037
- const searchContent = searchMatch[1].trim();
11038
- if (searchContent) {
11039
- const searchChunk = getOrCreateSharedChunk(jsChunks, "js", "search", searchContent, outDir, base);
11040
- const coreContent = jsContent.replace(SEARCH_CHUNK_RE, "").replaceAll(SEARCH_CHUNK_PLACEHOLDER, searchChunk.publicPath).trim();
11041
- if (coreContent) return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "core", coreContent, outDir, base).publicPath}"><\/script>`;
11042
- }
11043
- }
11044
- const fallbackContent = jsContent.trim();
11045
- if (!fallbackContent) return "";
11046
- return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "js", fallbackContent, outDir, base).publicPath}"><\/script>`;
11047
- }
11048
10509
  async function externalizeSharedPageAssets(pages, outDir, base) {
11049
- const cssChunks = /* @__PURE__ */ new Map();
11050
- const jsChunks = /* @__PURE__ */ new Map();
11051
- const optimizedPages = pages.map((page) => {
11052
- let html = page.html;
11053
- const styleMatch = html.match(SSG_STYLE_BLOCK_RE);
11054
- if (styleMatch) {
11055
- const replacement = buildStyleReplacement(styleMatch[1], cssChunks, outDir, base);
11056
- html = html.replace(SSG_STYLE_BLOCK_RE, replacement);
11057
- } else {
11058
- const inlineStyleMatch = html.match(FIRST_INLINE_STYLE_RE);
11059
- if (inlineStyleMatch) {
11060
- const replacement = buildStyleReplacement(inlineStyleMatch[1], cssChunks, outDir, base);
11061
- html = html.replace(FIRST_INLINE_STYLE_RE, replacement);
11062
- }
11063
- }
11064
- const scriptMatch = html.match(SSG_SCRIPT_BLOCK_RE);
11065
- if (scriptMatch) {
11066
- const replacement = buildScriptReplacement(scriptMatch[1], jsChunks, outDir, base);
11067
- html = html.replace(SSG_SCRIPT_BLOCK_RE, replacement);
11068
- } else {
11069
- const inlineScriptMatch = html.match(LAST_INLINE_BODY_SCRIPT_RE);
11070
- if (inlineScriptMatch) {
11071
- const replacement = buildScriptReplacement(inlineScriptMatch[1], jsChunks, outDir, base);
11072
- html = html.replace(LAST_INLINE_BODY_SCRIPT_RE, replacement ? `${replacement}\n</body>` : "</body>");
11073
- }
11074
- }
11075
- return {
11076
- ...page,
11077
- html
11078
- };
11079
- });
11080
- const chunks = [...cssChunks.values(), ...jsChunks.values()];
11081
- await Promise.all(chunks.map(async (chunk) => {
11082
- await fs_promises.mkdir(path.dirname(chunk.outputPath), { recursive: true });
11083
- await fs_promises.writeFile(chunk.outputPath, chunk.content, "utf-8");
10510
+ const optimized = (await require_mermaid.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
10511
+ await Promise.all(optimized.assets.map(async (asset) => {
10512
+ await fs_promises.mkdir(path.dirname(asset.outputPath), { recursive: true });
10513
+ await fs_promises.writeFile(asset.outputPath, asset.content, "utf-8");
11084
10514
  }));
11085
10515
  return {
11086
- pages: optimizedPages,
11087
- assets: chunks.map((chunk) => chunk.outputPath)
10516
+ pages: optimized.pages,
10517
+ assets: optimized.assets.map((asset) => asset.outputPath)
11088
10518
  };
11089
10519
  }
11090
10520
  /**
11091
- * Converts a markdown file path to its corresponding HTML output path.
11092
- */
11093
- function getOutputPath(inputPath, srcDir, outDir, extension) {
11094
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, extension);
11095
- if (baseName.endsWith(`index${extension}`)) return path.join(outDir, baseName);
11096
- const dirName = baseName.replace(new RegExp(`\\${extension}$`), "");
11097
- return path.join(outDir, dirName, `index${extension}`);
11098
- }
11099
- /**
11100
10521
  * Converts a markdown file path to a relative URL path.
11101
10522
  */
11102
10523
  function getUrlPath$1(inputPath, srcDir) {
11103
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
11104
- if (baseName === "index" || baseName.endsWith("/index")) return baseName.replace(/\/?index$/, "") || "/";
11105
- return baseName;
10524
+ return require_mermaid.importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
11106
10525
  }
11107
- /**
11108
- * Converts a markdown file path to an href.
11109
- */
11110
- function getHref(inputPath, srcDir, base, extension) {
11111
- const urlPath = getUrlPath$1(inputPath, srcDir);
11112
- if (urlPath === "/" || urlPath === "") return `${base}index${extension}`;
11113
- return `${base}${urlPath}/index${extension}`;
10526
+ function getPageLocale(urlPath, i18n) {
10527
+ if (!i18n) return void 0;
10528
+ return require_mermaid.importNapiModuleSync().getSsgPageLocale(urlPath, i18n.defaultLocale, i18n.locales.map((locale) => locale.code)) ?? void 0;
11114
10529
  }
11115
- /**
11116
- * Gets the OG image output path for a given markdown file.
11117
- */
11118
- function getOgImagePath(inputPath, srcDir, outDir) {
11119
- const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
11120
- if (baseName === "index" || baseName.endsWith("/index")) {
11121
- const dirPath = baseName.replace(/\/?index$/, "") || "";
11122
- return path.join(outDir, dirPath, "og-image.png");
11123
- }
11124
- return path.join(outDir, baseName, "og-image.png");
11125
- }
11126
- /**
11127
- * Gets the OG image URL for use in meta tags.
11128
- * If siteUrl is provided, returns an absolute URL (required for SNS sharing).
11129
- */
11130
- function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
11131
- const urlPath = getUrlPath$1(inputPath, srcDir);
11132
- let relativePath;
11133
- if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
11134
- else relativePath = `${base}${urlPath}/og-image.png`;
11135
- if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
11136
- return relativePath;
11137
- }
11138
- /**
11139
- * Gets display title from file path.
11140
- */
11141
- function getDisplayTitle(filePath) {
11142
- const fileName = path.basename(filePath, path.extname(filePath));
11143
- if (fileName === "index") {
11144
- const dirName = path.basename(path.dirname(filePath));
11145
- if (dirName && dirName !== ".") return formatTitle(dirName);
11146
- return "Home";
11147
- }
11148
- return formatTitle(fileName);
10530
+ function getRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl) {
10531
+ return require_mermaid.importNapiModuleSync().resolveSsgRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl);
11149
10532
  }
11150
10533
  /**
11151
10534
  * Formats a file/dir name as a title.
11152
10535
  */
11153
10536
  function formatTitle(name) {
11154
- return name.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
10537
+ return require_mermaid.importNapiModuleSync().formatSsgTitle(name);
11155
10538
  }
11156
10539
  /**
11157
10540
  * Collects all markdown files from the source directory.
@@ -11170,110 +10553,13 @@ async function collectMarkdownFiles$1(srcDir) {
11170
10553
  * Builds navigation items from markdown files, grouped by directory.
11171
10554
  */
11172
10555
  function buildNavItems(markdownFiles, srcDir, base, extension) {
11173
- const groups = /* @__PURE__ */ new Map();
11174
- const groupOrder = [
11175
- "",
11176
- "examples",
11177
- "packages",
11178
- "api"
11179
- ];
11180
- for (const file of markdownFiles) {
11181
- const parts = path.relative(srcDir, file).split(path.sep);
11182
- let groupKey = "";
11183
- if (parts.length > 1) groupKey = parts[0];
11184
- if (!groups.has(groupKey)) groups.set(groupKey, []);
11185
- const urlPath = getUrlPath$1(file, srcDir);
11186
- let title;
11187
- if (urlPath === "/" || urlPath === "") title = "Overview";
11188
- else title = getDisplayTitle(file);
11189
- groups.get(groupKey).push({
11190
- title,
11191
- path: urlPath,
11192
- href: getHref(file, srcDir, base, extension)
11193
- });
11194
- }
11195
- const sortItems = (items) => {
11196
- return items.sort((a, b) => {
11197
- const aIsRoot = a.path === "/" || a.path === "";
11198
- const bIsRoot = b.path === "/" || b.path === "";
11199
- if (aIsRoot && !bIsRoot) return -1;
11200
- if (!aIsRoot && bIsRoot) return 1;
11201
- return a.title.localeCompare(b.title);
11202
- });
11203
- };
11204
- const result = [];
11205
- for (const key of groupOrder) {
11206
- const items = groups.get(key);
11207
- if (items && items.length > 0) {
11208
- result.push({
11209
- title: key === "" ? "Guide" : formatTitle(key),
11210
- items: sortItems(items)
11211
- });
11212
- groups.delete(key);
11213
- }
11214
- }
11215
- for (const [key, items] of groups) if (items.length > 0) result.push({
11216
- title: formatTitle(key),
11217
- items: sortItems(items)
11218
- });
11219
- return result;
11220
- }
11221
- function isSafeSidebarLink(link) {
11222
- const trimmed = link.trim();
11223
- if (trimmed.startsWith("//")) return false;
11224
- return !/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || /^(https?:|mailto:)/i.test(trimmed);
11225
- }
11226
- function sidebarPath(link) {
11227
- if (!link || !isSafeSidebarLink(link)) return "";
11228
- if (/^(https?:|mailto:|#)/i.test(link.trim())) return "";
11229
- const bare = link.trim().split("#", 1)[0].split("?", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11230
- if (!bare || bare === "index") return "/";
11231
- return bare.replace(/\/index$/, "");
11232
- }
11233
- function sidebarHref(link, base, extension) {
11234
- if (!link) return "#";
11235
- const trimmed = link.trim();
11236
- if (!isSafeSidebarLink(trimmed)) return "#";
11237
- if (/^(https?:|mailto:|#)/i.test(trimmed)) return trimmed;
11238
- const hash = trimmed.includes("#") ? `#${trimmed.split("#").slice(1).join("#")}` : "";
11239
- const withoutExt = trimmed.split("#", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11240
- return `${base}${!withoutExt || withoutExt === "index" ? "index" : `${withoutExt.replace(/\/index$/, "")}/index`}${extension}${hash}`;
10556
+ return require_mermaid.importNapiModuleSync().buildSsgNavItems(markdownFiles, srcDir, base, extension);
11241
10557
  }
11242
10558
  /**
11243
10559
  * Builds navigation items from an explicit theme sidebar tree.
11244
10560
  */
11245
10561
  function buildThemeNavItems(sidebar, base, extension) {
11246
- const toNavItem = (item) => {
11247
- const navItem = {
11248
- title: item.text ?? item.link ?? "Untitled",
11249
- path: sidebarPath(item.link),
11250
- href: sidebarHref(item.link, base, extension)
11251
- };
11252
- if (item.items?.length) navItem.children = item.items.map(toNavItem);
11253
- if (item.collapsed !== void 0) navItem.collapsed = item.collapsed;
11254
- return navItem;
11255
- };
11256
- const groups = [];
11257
- let looseItems = [];
11258
- const flushLooseItems = () => {
11259
- if (looseItems.length > 0) {
11260
- groups.push({
11261
- title: "Guide",
11262
- items: looseItems
11263
- });
11264
- looseItems = [];
11265
- }
11266
- };
11267
- for (const item of sidebar) if (item.items?.length && !item.link) {
11268
- flushLooseItems();
11269
- groups.push({
11270
- title: item.text ?? "Guide",
11271
- items: item.items.map(toNavItem),
11272
- collapsed: item.collapsed
11273
- });
11274
- } else looseItems.push(toNavItem(item));
11275
- flushLooseItems();
11276
- return groups;
10562
+ return require_mermaid.importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
11277
10563
  }
11278
10564
  /**
11279
10565
  * Builds all markdown files to static HTML.
@@ -11332,8 +10618,10 @@ async function buildSsg(options, root) {
11332
10618
  transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
11333
10619
  const title = extractTitle$1(transformedHtml, result.frontmatter);
11334
10620
  const description = result.frontmatter.description;
10621
+ const routePaths = getRoutePaths(inputPath, srcDir, outDir, base, ssgOptions.extension, ssgOptions.siteUrl);
11335
10622
  pageResults.push({
11336
10623
  inputPath,
10624
+ routePaths,
11337
10625
  transformedHtml,
11338
10626
  title,
11339
10627
  description,
@@ -11342,7 +10630,6 @@ async function buildSsg(options, root) {
11342
10630
  toc: result.toc
11343
10631
  });
11344
10632
  if (shouldGenerateOgImages) {
11345
- const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
11346
10633
  const { layout: _layout, ...frontmatterRest } = result.frontmatter;
11347
10634
  ogImageEntries.push({
11348
10635
  props: {
@@ -11351,10 +10638,10 @@ async function buildSsg(options, root) {
11351
10638
  description,
11352
10639
  siteName
11353
10640
  },
11354
- outputPath: ogImageOutputPath
10641
+ outputPath: routePaths.ogImagePath
11355
10642
  });
11356
10643
  ogImageInputPaths.push(inputPath);
11357
- ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
10644
+ ogImageUrlMap.set(inputPath, routePaths.ogImageUrl);
11358
10645
  }
11359
10646
  } catch (err) {
11360
10647
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -11383,7 +10670,7 @@ async function buildSsg(options, root) {
11383
10670
  ogImageUrlMap.clear();
11384
10671
  }
11385
10672
  for (const pageResult of pageResults) try {
11386
- const { inputPath, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
10673
+ const { inputPath, routePaths, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
11387
10674
  let pageOgImage = ssgOptions.ogImage;
11388
10675
  if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
11389
10676
  let entryPage;
@@ -11393,21 +10680,23 @@ async function buildSsg(options, root) {
11393
10680
  };
11394
10681
  let html;
11395
10682
  if (ssgOptions.bare) html = generateBareHtmlPage(transformedHtml, title);
11396
- else html = await generateHtmlPage({
11397
- title,
11398
- description,
11399
- content: transformedHtml,
11400
- toc,
11401
- lastUpdated,
11402
- frontmatter,
11403
- path: getUrlPath$1(inputPath, srcDir),
11404
- href: getHref(inputPath, srcDir, base, ssgOptions.extension),
11405
- entryPage
11406
- }, navItems, siteName, base, pageOgImage, ssgOptions.theme);
11407
- const outputPath = getOutputPath(inputPath, srcDir, outDir, ssgOptions.extension);
10683
+ else {
10684
+ const pageData = {
10685
+ title,
10686
+ description,
10687
+ content: transformedHtml,
10688
+ toc,
10689
+ lastUpdated,
10690
+ frontmatter,
10691
+ path: routePaths.urlPath,
10692
+ href: routePaths.href,
10693
+ entryPage
10694
+ };
10695
+ html = await generateHtmlPage(pageData, navItems, siteName, base, pageOgImage, ssgOptions.theme, getPageLocale(pageData.path, options.i18n), options.i18n ? options.i18n.locales : void 0);
10696
+ }
11408
10697
  generatedPages.push({
11409
10698
  inputPath,
11410
- outputPath,
10699
+ outputPath: routePaths.outputPath,
11411
10700
  html
11412
10701
  });
11413
10702
  } catch (err) {
@@ -11535,234 +10824,7 @@ async function writeSearchIndex(indexJson, outDir) {
11535
10824
  * This is injected into the bundle as a virtual module.
11536
10825
  */
11537
10826
  function generateSearchModule(options, indexPath) {
11538
- return `
11539
- // Search module generated by ox-content
11540
- const searchOptions = ${JSON.stringify(options)};
11541
-
11542
- let searchIndex = null;
11543
- let indexPromise = null;
11544
-
11545
- function parseScopedQuery(query) {
11546
- const scopes = [];
11547
- const terms = [];
11548
-
11549
- for (const part of query.trim().split(/\\s+/).filter(Boolean)) {
11550
- if (part.startsWith('@') && part.length > 1) {
11551
- scopes.push(part.slice(1).toLowerCase());
11552
- } else {
11553
- terms.push(part);
11554
- }
11555
- }
11556
-
11557
- return {
11558
- text: terms.join(' ').trim(),
11559
- scopes: [...new Set(scopes)],
11560
- };
11561
- }
11562
-
11563
- function getScopesForDoc(doc) {
11564
- const source = (doc.id || doc.url || '').replace(/^\\/+/, '').toLowerCase();
11565
- const segments = source.split('/').filter(Boolean);
11566
-
11567
- if (segments.length <= 1) {
11568
- return [];
11569
- }
11570
-
11571
- const scopes = [];
11572
- let current = '';
11573
- for (const segment of segments.slice(0, -1)) {
11574
- current = current ? current + '/' + segment : segment;
11575
- scopes.push(current);
11576
- }
11577
-
11578
- return scopes;
11579
- }
11580
-
11581
- function matchesScopes(doc, scopes) {
11582
- if (!scopes.length) {
11583
- return true;
11584
- }
11585
-
11586
- const docScopes = new Set(getScopesForDoc(doc));
11587
- return scopes.some(scope => docScopes.has(scope));
11588
- }
11589
-
11590
- // Tokenizer for queries
11591
- function tokenizeQuery(text) {
11592
- const tokens = [];
11593
- let current = '';
11594
-
11595
- for (const char of text) {
11596
- const isCjk = /[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\u3040-\\u309F\\u30A0-\\u30FF\\uAC00-\\uD7AF]/.test(char);
11597
-
11598
- if (isCjk) {
11599
- if (current) {
11600
- tokens.push(current.toLowerCase());
11601
- current = '';
11602
- }
11603
- tokens.push(char);
11604
- } else if (/[a-zA-Z0-9_]/.test(char)) {
11605
- current += char;
11606
- } else if (current) {
11607
- tokens.push(current.toLowerCase());
11608
- current = '';
11609
- }
11610
- }
11611
-
11612
- if (current) {
11613
- tokens.push(current.toLowerCase());
11614
- }
11615
-
11616
- return tokens;
11617
- }
11618
-
11619
- // BM25 scoring
11620
- function computeIdf(df, docCount) {
11621
- return Math.log((docCount - df + 0.5) / (df + 0.5) + 1.0);
11622
- }
11623
-
11624
- function getFieldBoost(field) {
11625
- switch (field) {
11626
- case 'Title': return 10.0;
11627
- case 'Heading': return 5.0;
11628
- case 'Body': return 1.0;
11629
- case 'Code': return 0.5;
11630
- default: return 1.0;
11631
- }
11632
- }
11633
-
11634
- // Load the index
11635
- async function loadIndex() {
11636
- if (searchIndex) return searchIndex;
11637
- if (indexPromise) return indexPromise;
11638
-
11639
- indexPromise = fetch('${indexPath}')
11640
- .then(res => res.json())
11641
- .then(data => {
11642
- searchIndex = data;
11643
- return data;
11644
- })
11645
- .catch(err => {
11646
- console.error('[ox-content] Failed to load search index:', err);
11647
- return null;
11648
- });
11649
-
11650
- return indexPromise;
11651
- }
11652
-
11653
- // Search function
11654
- export async function search(query, options = {}) {
11655
- const index = await loadIndex();
11656
-
11657
- if (!index) {
11658
- return [];
11659
- }
11660
-
11661
- const parsedQuery = parseScopedQuery(query);
11662
-
11663
- if (!parsedQuery.text && parsedQuery.scopes.length === 0) {
11664
- return [];
11665
- }
11666
-
11667
- const limit = options.limit ?? searchOptions.limit;
11668
- const prefix = options.prefix ?? searchOptions.prefix;
11669
- const tokens = tokenizeQuery(parsedQuery.text);
11670
-
11671
- const k1 = 1.2;
11672
- const b = 0.75;
11673
- const docScores = new Map();
11674
-
11675
- if (tokens.length === 0) {
11676
- index.documents.forEach((doc, docIdx) => {
11677
- if (matchesScopes(doc, parsedQuery.scopes)) {
11678
- docScores.set(docIdx, { score: 0, matches: new Set() });
11679
- }
11680
- });
11681
- }
11682
-
11683
- for (let i = 0; i < tokens.length; i++) {
11684
- const token = tokens[i];
11685
- const isLast = i === tokens.length - 1;
11686
-
11687
- // Find matching terms
11688
- let matchingTerms = [];
11689
- if (prefix && isLast && token.length >= 2) {
11690
- matchingTerms = Object.keys(index.index).filter(term => term.startsWith(token));
11691
- } else if (index.index[token]) {
11692
- matchingTerms = [token];
11693
- }
11694
-
11695
- for (const term of matchingTerms) {
11696
- const postings = index.index[term] || [];
11697
- const df = index.df[term] || 1;
11698
- const idf = computeIdf(df, index.doc_count);
11699
-
11700
- for (const posting of postings) {
11701
- const doc = index.documents[posting.doc_idx];
11702
- if (!doc) continue;
11703
- if (!matchesScopes(doc, parsedQuery.scopes)) continue;
11704
-
11705
- const docLen = doc.body.length;
11706
- const tf = posting.tf;
11707
- const boost = getFieldBoost(posting.field);
11708
-
11709
- const score = idf * ((tf * (k1 + 1.0)) / (tf + k1 * (1.0 - b + b * docLen / index.avg_dl))) * boost;
11710
-
11711
- if (!docScores.has(posting.doc_idx)) {
11712
- docScores.set(posting.doc_idx, { score: 0, matches: new Set() });
11713
- }
11714
- const entry = docScores.get(posting.doc_idx);
11715
- entry.score += score;
11716
- entry.matches.add(term);
11717
- }
11718
- }
11719
- }
11720
-
11721
- // Convert to results
11722
- const results = Array.from(docScores.entries())
11723
- .map(([docIdx, data]) => {
11724
- const doc = index.documents[docIdx];
11725
- const matches = Array.from(data.matches);
11726
- const scopes = getScopesForDoc(doc);
11727
-
11728
- // Generate snippet
11729
- let snippet = '';
11730
- if (doc.body) {
11731
- const bodyLower = doc.body.toLowerCase();
11732
- let firstPos = -1;
11733
- for (const match of matches) {
11734
- const pos = bodyLower.indexOf(match);
11735
- if (pos !== -1 && (firstPos === -1 || pos < firstPos)) {
11736
- firstPos = pos;
11737
- }
11738
- }
11739
-
11740
- const start = firstPos === -1 ? 0 : Math.max(0, firstPos - 50);
11741
- const end = Math.min(doc.body.length, start + 150);
11742
- snippet = doc.body.slice(start, end);
11743
- if (start > 0) snippet = '...' + snippet;
11744
- if (end < doc.body.length) snippet = snippet + '...';
11745
- }
11746
-
11747
- return {
11748
- id: doc.id,
11749
- title: doc.title,
11750
- url: doc.url,
11751
- score: data.score,
11752
- matches,
11753
- snippet,
11754
- scopes,
11755
- };
11756
- })
11757
- .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
11758
- .slice(0, limit);
11759
-
11760
- return results;
11761
- }
11762
-
11763
- export { searchOptions };
11764
- export default { search, searchOptions, loadIndex };
11765
- `;
10827
+ return require_mermaid.importNapiModuleSync().generateSearchModule(JSON.stringify(options), indexPath);
11766
10828
  }
11767
10829
  //#endregion
11768
10830
  //#region src/dev-server.ts
@@ -12403,7 +11465,7 @@ function createI18nPlugin(resolvedOptions) {
12403
11465
  }
12404
11466
  server.middlewares.use((req, _res, next) => {
12405
11467
  if (!req.url) return next();
12406
- const localeMatch = req.url.match(/^\/([a-z]{2}(?:-[a-zA-Z]+)?)(\/|$)/);
11468
+ const localeMatch = req.url.match(/^\/([A-Za-z]{2,3}(?:-[A-Za-z0-9]+)*)(\/|$)/);
12407
11469
  if (localeMatch) {
12408
11470
  const localeCode = localeMatch[1];
12409
11471
  if (i18nOptions.locales.some((l) => l.code === localeCode)) req.__oxLocale = localeCode;
@@ -12418,104 +11480,18 @@ function createI18nPlugin(resolvedOptions) {
12418
11480
  */
12419
11481
  function generateI18nModule(options, root) {
12420
11482
  const dictDir = path.resolve(root, options.dir);
12421
- const localesJson = JSON.stringify(options.locales);
12422
- const defaultLocale = JSON.stringify(options.defaultLocale);
12423
- let dictionariesCode = "{}";
11483
+ const config = {
11484
+ defaultLocale: options.defaultLocale,
11485
+ locales: options.locales,
11486
+ hideDefaultLocale: options.hideDefaultLocale
11487
+ };
12424
11488
  try {
12425
11489
  const napi = require("@ox-content/napi");
12426
- if (napi.loadDictionariesFlat) {
12427
- const dictData = napi.loadDictionariesFlat(dictDir);
12428
- dictionariesCode = JSON.stringify(dictData);
12429
- } else dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
12430
- } catch {
12431
- try {
12432
- dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
12433
- } catch {}
12434
- }
12435
- return `
12436
- export const i18nConfig = {
12437
- enabled: true,
12438
- defaultLocale: ${defaultLocale},
12439
- locales: ${localesJson},
12440
- hideDefaultLocale: ${JSON.stringify(options.hideDefaultLocale)},
12441
- };
12442
-
12443
- export const dictionaries = ${dictionariesCode};
12444
-
12445
- export function t(key, params, locale) {
12446
- const dict = dictionaries[locale || i18nConfig.defaultLocale] || {};
12447
- let message = dict[key];
12448
- if (!message) {
12449
- const fallback = dictionaries[i18nConfig.defaultLocale] || {};
12450
- message = fallback[key] || key;
12451
- }
12452
- if (params) {
12453
- for (const [k, v] of Object.entries(params)) {
12454
- message = message.replace(new RegExp('\\\\{\\\\$' + k + '\\\\}', 'g'), String(v));
12455
- }
12456
- }
12457
- return message;
12458
- }
12459
-
12460
- export function getLocaleFromPath(pathname) {
12461
- const match = pathname.match(/^\\/([a-z]{2}(?:-[a-zA-Z]+)?)(\\//|$)/);
12462
- if (match) {
12463
- const code = match[1];
12464
- if (i18nConfig.locales.some(l => l.code === code)) {
12465
- return code;
12466
- }
12467
- }
12468
- return i18nConfig.defaultLocale;
12469
- }
12470
-
12471
- export function localePath(pathname, locale) {
12472
- const current = getLocaleFromPath(pathname);
12473
- let clean = pathname;
12474
- if (current !== i18nConfig.defaultLocale || !i18nConfig.hideDefaultLocale) {
12475
- clean = pathname.replace(new RegExp('^/' + current + '(/|$)'), '/');
12476
- }
12477
- if (locale === i18nConfig.defaultLocale && i18nConfig.hideDefaultLocale) {
12478
- return clean || '/';
12479
- }
12480
- return '/' + locale + (clean.startsWith('/') ? clean : '/' + clean);
12481
- }
12482
-
12483
- export default { i18nConfig, dictionaries, t, getLocaleFromPath, localePath };
12484
- `;
12485
- }
12486
- /**
12487
- * Flattens a nested object into dot-separated keys.
12488
- */
12489
- function flattenObject(obj, prefix, result) {
12490
- for (const [key, value] of Object.entries(obj)) {
12491
- const fullKey = `${prefix}.${key}`;
12492
- if (typeof value === "string") result[fullKey] = value;
12493
- else if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenObject(value, fullKey, result);
12494
- else result[fullKey] = String(value);
12495
- }
12496
- }
12497
- /**
12498
- * Fallback dictionary loading using TS-based JSON file reading.
12499
- */
12500
- function loadDictionariesFallback(options, dictDir) {
12501
- const dictData = {};
12502
- for (const locale of options.locales) {
12503
- const localeDir = path.join(dictDir, locale.code);
12504
- if (!fs.existsSync(localeDir)) continue;
12505
- const files = fs.readdirSync(localeDir);
12506
- const localeDict = {};
12507
- for (const file of files) {
12508
- if (!file.endsWith(".json")) continue;
12509
- const filePath = path.join(localeDir, file);
12510
- const content = fs.readFileSync(filePath, "utf-8");
12511
- const namespace = path.basename(file, ".json");
12512
- try {
12513
- flattenObject(JSON.parse(content), namespace, localeDict);
12514
- } catch {}
12515
- }
12516
- dictData[locale.code] = localeDict;
11490
+ if (typeof napi.generateI18nModule === "function") return napi.generateI18nModule(dictDir, config);
11491
+ } catch (error) {
11492
+ throw new Error(`[ox-content:i18n] Failed to load @ox-content/napi for i18n module generation: ${String(error)}`);
12517
11493
  }
12518
- return dictData;
11494
+ throw new Error("[ox-content:i18n] @ox-content/napi does not expose generateI18nModule. Please rebuild the NAPI package.");
12519
11495
  }
12520
11496
  /**
12521
11497
  * Collects translation keys from source files using NAPI extractTranslationKeys.
@@ -13711,20 +12687,54 @@ function resolveCodeAnnotationsOptions(options) {
13711
12687
  /**
13712
12688
  * Generates virtual module content.
13713
12689
  */
13714
- function generateVirtualModule(path$2, options) {
13715
- if (path$2 === "config") return `export default ${JSON.stringify(options)};`;
13716
- if (path$2 === "runtime") return `
12690
+ function generateVirtualModule(path$1, options) {
12691
+ if (path$1 === "config") return `export default ${JSON.stringify(options)};`;
12692
+ if (path$1 === "runtime") {
12693
+ const base = normalizeRuntimeBase(options.base);
12694
+ return `
12695
+ export const base = ${JSON.stringify(base)};
12696
+ export const runtimeConfig = { base };
12697
+
12698
+ export function isExternalUrl(value) {
12699
+ return /^(?:https?:)?\\/\\//i.test(value) || /^(?:mailto|tel):/i.test(value);
12700
+ }
12701
+
12702
+ export function withBase(pathname = "") {
12703
+ const value = String(pathname);
12704
+ if (!value || value === "/") return base;
12705
+ if (value.startsWith("#") || isExternalUrl(value)) return value;
12706
+ return base + (value.startsWith("/") ? value.slice(1) : value);
12707
+ }
12708
+
12709
+ export function withoutBase(pathname = "") {
12710
+ const value = String(pathname);
12711
+ if (base === "/" || value.startsWith("#") || isExternalUrl(value)) return value;
12712
+ const bareBase = base.slice(0, -1);
12713
+ if (value === bareBase) return "/";
12714
+ if (value.startsWith(base)) return "/" + value.slice(base.length);
12715
+ return value;
12716
+ }
12717
+
13717
12718
  export function useMarkdown() {
13718
12719
  return {
12720
+ base,
12721
+ withBase,
12722
+ withoutBase,
13719
12723
  render: (content) => {
13720
- // Client-side rendering if needed
13721
12724
  return content;
13722
12725
  },
13723
12726
  };
13724
12727
  }
13725
12728
  `;
12729
+ }
13726
12730
  return "export default {};";
13727
12731
  }
12732
+ function normalizeRuntimeBase(base) {
12733
+ const trimmed = base.trim();
12734
+ if (!trimmed || trimmed === "/") return "/";
12735
+ const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
12736
+ return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
12737
+ }
13728
12738
  //#endregion
13729
12739
  exports.DEFAULT_HTML_TEMPLATE = DEFAULT_HTML_TEMPLATE;
13730
12740
  exports.DefaultTheme = DefaultTheme;
@@ -13751,6 +12761,7 @@ exports.generateMarkdown = generateMarkdown;
13751
12761
  exports.generateOgImages = generateOgImages;
13752
12762
  exports.generateTabsCSS = require_tabs.generateTabsCSS;
13753
12763
  exports.generateTypes = generateTypes;
12764
+ exports.generateVirtualModule = generateVirtualModule;
13754
12765
  exports.hasIslands = hasIslands;
13755
12766
  exports.inferType = inferType;
13756
12767
  exports.jsx = jsx;