@ox-content/vite-plugin 2.8.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
- }));
7090
+ return require_mermaid.importNapiModuleSync().generateDocsNavMetadata(docs.map((doc) => doc.file), basePath);
7210
7091
  }
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;
7254
- }
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).
@@ -10946,223 +10506,35 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10946
10506
  }))
10947
10507
  });
10948
10508
  }
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
10509
  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");
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");
11090
10514
  }));
11091
10515
  return {
11092
- pages: optimizedPages,
11093
- assets: chunks.map((chunk) => chunk.outputPath)
10516
+ pages: optimized.pages,
10517
+ assets: optimized.assets.map((asset) => asset.outputPath)
11094
10518
  };
11095
10519
  }
11096
10520
  /**
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
10521
  * Converts a markdown file path to a relative URL path.
11107
10522
  */
11108
10523
  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;
11112
- }
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}`;
10524
+ return require_mermaid.importNapiModuleSync().getSsgUrlPath(inputPath, srcDir);
11120
10525
  }
11121
10526
  function getPageLocale(urlPath, i18n) {
11122
10527
  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;
11125
- }
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");
11136
- }
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;
10528
+ return require_mermaid.importNapiModuleSync().getSsgPageLocale(urlPath, i18n.defaultLocale, i18n.locales.map((locale) => locale.code)) ?? void 0;
11148
10529
  }
11149
- /**
11150
- * Gets display title from file path.
11151
- */
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);
10530
+ function getRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl) {
10531
+ return require_mermaid.importNapiModuleSync().resolveSsgRoutePaths(inputPath, srcDir, outDir, base, extension, siteUrl);
11160
10532
  }
11161
10533
  /**
11162
10534
  * Formats a file/dir name as a title.
11163
10535
  */
11164
10536
  function formatTitle(name) {
11165
- return name.replace(/[-_]([a-z])/g, (_, char) => " " + char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
10537
+ return require_mermaid.importNapiModuleSync().formatSsgTitle(name);
11166
10538
  }
11167
10539
  /**
11168
10540
  * Collects all markdown files from the source directory.
@@ -11181,110 +10553,13 @@ async function collectMarkdownFiles$1(srcDir) {
11181
10553
  * Builds navigation items from markdown files, grouped by directory.
11182
10554
  */
11183
10555
  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}`;
10556
+ return require_mermaid.importNapiModuleSync().buildSsgNavItems(markdownFiles, srcDir, base, extension);
11252
10557
  }
11253
10558
  /**
11254
10559
  * Builds navigation items from an explicit theme sidebar tree.
11255
10560
  */
11256
10561
  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;
10562
+ return require_mermaid.importNapiModuleSync().buildSsgThemeNavItems(sidebar, base, extension);
11288
10563
  }
11289
10564
  /**
11290
10565
  * Builds all markdown files to static HTML.
@@ -11343,8 +10618,10 @@ async function buildSsg(options, root) {
11343
10618
  transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
11344
10619
  const title = extractTitle$1(transformedHtml, result.frontmatter);
11345
10620
  const description = result.frontmatter.description;
10621
+ const routePaths = getRoutePaths(inputPath, srcDir, outDir, base, ssgOptions.extension, ssgOptions.siteUrl);
11346
10622
  pageResults.push({
11347
10623
  inputPath,
10624
+ routePaths,
11348
10625
  transformedHtml,
11349
10626
  title,
11350
10627
  description,
@@ -11353,7 +10630,6 @@ async function buildSsg(options, root) {
11353
10630
  toc: result.toc
11354
10631
  });
11355
10632
  if (shouldGenerateOgImages) {
11356
- const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
11357
10633
  const { layout: _layout, ...frontmatterRest } = result.frontmatter;
11358
10634
  ogImageEntries.push({
11359
10635
  props: {
@@ -11362,10 +10638,10 @@ async function buildSsg(options, root) {
11362
10638
  description,
11363
10639
  siteName
11364
10640
  },
11365
- outputPath: ogImageOutputPath
10641
+ outputPath: routePaths.ogImagePath
11366
10642
  });
11367
10643
  ogImageInputPaths.push(inputPath);
11368
- ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
10644
+ ogImageUrlMap.set(inputPath, routePaths.ogImageUrl);
11369
10645
  }
11370
10646
  } catch (err) {
11371
10647
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -11394,7 +10670,7 @@ async function buildSsg(options, root) {
11394
10670
  ogImageUrlMap.clear();
11395
10671
  }
11396
10672
  for (const pageResult of pageResults) try {
11397
- const { inputPath, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
10673
+ const { inputPath, routePaths, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
11398
10674
  let pageOgImage = ssgOptions.ogImage;
11399
10675
  if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
11400
10676
  let entryPage;
@@ -11412,16 +10688,15 @@ async function buildSsg(options, root) {
11412
10688
  toc,
11413
10689
  lastUpdated,
11414
10690
  frontmatter,
11415
- path: getUrlPath$1(inputPath, srcDir),
11416
- href: getHref(inputPath, srcDir, base, ssgOptions.extension),
10691
+ path: routePaths.urlPath,
10692
+ href: routePaths.href,
11417
10693
  entryPage
11418
10694
  };
11419
10695
  html = await generateHtmlPage(pageData, navItems, siteName, base, pageOgImage, ssgOptions.theme, getPageLocale(pageData.path, options.i18n), options.i18n ? options.i18n.locales : void 0);
11420
10696
  }
11421
- const outputPath = getOutputPath(inputPath, srcDir, outDir, ssgOptions.extension);
11422
10697
  generatedPages.push({
11423
10698
  inputPath,
11424
- outputPath,
10699
+ outputPath: routePaths.outputPath,
11425
10700
  html
11426
10701
  });
11427
10702
  } catch (err) {
@@ -11549,234 +10824,7 @@ async function writeSearchIndex(indexJson, outDir) {
11549
10824
  * This is injected into the bundle as a virtual module.
11550
10825
  */
11551
10826
  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
- `;
10827
+ return require_mermaid.importNapiModuleSync().generateSearchModule(JSON.stringify(options), indexPath);
11780
10828
  }
11781
10829
  //#endregion
11782
10830
  //#region src/dev-server.ts
@@ -12432,193 +11480,18 @@ function createI18nPlugin(resolvedOptions) {
12432
11480
  */
12433
11481
  function generateI18nModule(options, root) {
12434
11482
  const dictDir = path.resolve(root, options.dir);
12435
- const localesJson = JSON.stringify(options.locales);
12436
- const defaultLocale = JSON.stringify(options.defaultLocale);
12437
- let dictionariesCode = "{}";
11483
+ const config = {
11484
+ defaultLocale: options.defaultLocale,
11485
+ locales: options.locales,
11486
+ hideDefaultLocale: options.hideDefaultLocale
11487
+ };
12438
11488
  try {
12439
11489
  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;
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)}`);
12620
11493
  }
12621
- return dictData;
11494
+ throw new Error("[ox-content:i18n] @ox-content/napi does not expose generateI18nModule. Please rebuild the NAPI package.");
12622
11495
  }
12623
11496
  /**
12624
11497
  * Collects translation keys from source files using NAPI extractTranslationKeys.
@@ -13814,9 +12687,9 @@ function resolveCodeAnnotationsOptions(options) {
13814
12687
  /**
13815
12688
  * Generates virtual module content.
13816
12689
  */
13817
- function generateVirtualModule(path$2, options) {
13818
- if (path$2 === "config") return `export default ${JSON.stringify(options)};`;
13819
- if (path$2 === "runtime") {
12690
+ function generateVirtualModule(path$1, options) {
12691
+ if (path$1 === "config") return `export default ${JSON.stringify(options)};`;
12692
+ if (path$1 === "runtime") {
13820
12693
  const base = normalizeRuntimeBase(options.base);
13821
12694
  return `
13822
12695
  export const base = ${JSON.stringify(base)};