fumadocs-core 15.0.18 → 15.1.1

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.
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { R as Root, N as Node } from './page-tree-CfT5zlWh.js';
2
+ import { R as Root, N as Node } from './page-tree-9q98UqWL.js';
3
3
 
4
4
  interface BreadcrumbItem {
5
5
  name: ReactNode;
@@ -2,7 +2,7 @@ import {
2
2
  _highlight,
3
3
  _renderHighlight,
4
4
  highlight
5
- } from "./chunk-E7AASGCN.js";
5
+ } from "./chunk-BUCUQ3WX.js";
6
6
 
7
7
  // src/highlight/client.tsx
8
8
  import {
@@ -19,9 +19,8 @@ var defaultThemes = {
19
19
  light: "github-light",
20
20
  dark: "github-dark"
21
21
  };
22
- var defaultEngine;
22
+ var highlighters = /* @__PURE__ */ new Map();
23
23
  async function _highlight(code, options) {
24
- const { getSingletonHighlighter } = await import("shiki");
25
24
  const { lang, components: _, engine, ...rest } = options;
26
25
  let themes = { themes: defaultThemes };
27
26
  if ("theme" in options && options.theme) {
@@ -29,11 +28,9 @@ async function _highlight(code, options) {
29
28
  } else if ("themes" in options && options.themes) {
30
29
  themes = { themes: options.themes };
31
30
  }
32
- const highlighter = await getSingletonHighlighter({
31
+ const highlighter = await getHighlighter("custom", {
32
+ engine,
33
33
  langs: [lang],
34
- engine: engine ?? (defaultEngine ??= await import("shiki/engine/oniguruma").then(
35
- (res) => res.createOnigurumaEngine(import("shiki/wasm"))
36
- )),
37
34
  themes: "theme" in themes ? [themes.theme] : Object.values(themes.themes).filter((v) => v !== void 0)
38
35
  });
39
36
  return highlighter.codeToHast(code, {
@@ -53,6 +50,38 @@ function _renderHighlight(hast, options) {
53
50
  Fragment
54
51
  });
55
52
  }
53
+ async function getHighlighter(engineType, options) {
54
+ const { createHighlighter } = await import("shiki");
55
+ let highlighter = highlighters.get(engineType);
56
+ if (!highlighter) {
57
+ let engine = options.engine;
58
+ if (engineType === "js") {
59
+ engine = import("shiki/engine/javascript").then(
60
+ (res) => res.createJavaScriptRegexEngine()
61
+ );
62
+ }
63
+ if (engineType === "oniguruma" || !engine) {
64
+ engine = import("shiki/engine/oniguruma").then(
65
+ (res) => res.createOnigurumaEngine(import("shiki/wasm"))
66
+ );
67
+ }
68
+ highlighter = createHighlighter({
69
+ ...options,
70
+ engine
71
+ });
72
+ highlighters.set(engineType, highlighter);
73
+ return highlighter;
74
+ }
75
+ return highlighter.then(async (instance) => {
76
+ await Promise.all([
77
+ // @ts-expect-error unknown
78
+ instance.loadLanguage(...options.langs),
79
+ // @ts-expect-error unknown
80
+ instance.loadTheme(...options.themes)
81
+ ]);
82
+ return instance;
83
+ });
84
+ }
56
85
  async function highlight(code, options) {
57
86
  return _renderHighlight(await _highlight(code, options), options);
58
87
  }
@@ -62,5 +91,6 @@ export {
62
91
  defaultThemes,
63
92
  _highlight,
64
93
  _renderHighlight,
94
+ getHighlighter,
65
95
  highlight
66
96
  };
@@ -2,7 +2,7 @@
2
2
  function splitPath(path) {
3
3
  return path.split("/").filter((p) => p.length > 0);
4
4
  }
5
- function resolvePath(from, join) {
5
+ function joinPath(from, join) {
6
6
  const v1 = splitPath(from), v2 = splitPath(join);
7
7
  while (v2.length > 0) {
8
8
  switch (v2[0]) {
@@ -28,6 +28,6 @@ function slash(path) {
28
28
 
29
29
  export {
30
30
  splitPath,
31
- resolvePath,
31
+ joinPath,
32
32
  slash
33
33
  };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import {
3
3
  useShiki
4
- } from "../chunk-Q73QZKHO.js";
5
- import "../chunk-E7AASGCN.js";
4
+ } from "../chunk-62S4EN7J.js";
5
+ import "../chunk-BUCUQ3WX.js";
6
6
  import "../chunk-MLKGABMK.js";
7
7
  export {
8
8
  useShiki
@@ -2,7 +2,7 @@ import "../chunk-WQMD6AUR.js";
2
2
  import {
3
3
  createStyleTransformer,
4
4
  highlight
5
- } from "../chunk-E7AASGCN.js";
5
+ } from "../chunk-BUCUQ3WX.js";
6
6
  import "../chunk-MLKGABMK.js";
7
7
  export {
8
8
  createStyleTransformer,
@@ -3,13 +3,14 @@ import {
3
3
  remarkHeading
4
4
  } from "../chunk-IYQ35KI2.js";
5
5
  import {
6
- resolvePath,
6
+ joinPath,
7
7
  slash
8
- } from "../chunk-SHGL6VBO.js";
8
+ } from "../chunk-QTVCCXFT.js";
9
9
  import {
10
10
  createStyleTransformer,
11
- defaultThemes
12
- } from "../chunk-E7AASGCN.js";
11
+ defaultThemes,
12
+ getHighlighter
13
+ } from "../chunk-BUCUQ3WX.js";
13
14
  import "../chunk-MLKGABMK.js";
14
15
 
15
16
  // src/mdx-plugins/index.ts
@@ -25,11 +26,8 @@ import {
25
26
  transformerNotationWordHighlight
26
27
  } from "@shikijs/transformers";
27
28
  import {
28
- getSingletonHighlighter,
29
29
  bundledLanguages
30
30
  } from "shiki";
31
- import { createOnigurumaEngine } from "shiki/engine/oniguruma";
32
- import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
33
31
 
34
32
  // src/mdx-plugins/transformer-icon.ts
35
33
  var defaultShortcuts = {
@@ -248,13 +246,15 @@ function rehypeCode(_options = {}) {
248
246
  if (options.tab !== false) {
249
247
  transformers.push(transformerTab());
250
248
  }
251
- const highlighter = getSingletonHighlighter({
252
- engine: options.experimentalJSEngine ? createJavaScriptRegexEngine() : createOnigurumaEngine(() => import("shiki/wasm")),
253
- themes: "themes" in options ? Object.values(options.themes).filter(Boolean) : [options.theme],
254
- langs: options.langs ?? (options.lazy ? void 0 : Object.keys(bundledLanguages))
255
- });
249
+ const highlighter = getHighlighter(
250
+ options.experimentalJSEngine ? "js" : "oniguruma",
251
+ {
252
+ themes: "themes" in options ? Object.values(options.themes).filter(Boolean) : [options.theme],
253
+ langs: options.langs ?? (options.lazy ? [] : Object.keys(bundledLanguages))
254
+ }
255
+ );
256
256
  const transformer = highlighter.then(
257
- (instance) => rehypeShikiFromHighlighter(instance, {
257
+ (loaded) => rehypeShikiFromHighlighter(loaded, {
258
258
  ...options,
259
259
  transformers
260
260
  })
@@ -430,7 +430,7 @@ async function getImageSize(src, dir) {
430
430
  url = src;
431
431
  } else if (EXTERNAL_URL_REGEX.test(dir) && isRelative) {
432
432
  const base = new URL(dir);
433
- base.pathname = resolvePath(base.pathname, src);
433
+ base.pathname = joinPath(base.pathname, src);
434
434
  url = base.toString();
435
435
  } else {
436
436
  return imageSizeFromFile(isRelative ? path.join(dir, src) : src);
@@ -15,6 +15,7 @@ interface Item {
15
15
  name: ReactNode;
16
16
  url: string;
17
17
  external?: boolean;
18
+ description?: ReactNode;
18
19
  icon?: ReactElement;
19
20
  }
20
21
  interface Separator {
@@ -1,5 +1,4 @@
1
1
  import { TypedDocument, Orama, Language, RawData, create, SearchParams } from '@orama/orama';
2
- import { NextRequest } from 'next/server';
3
2
  import { S as StructuredData } from '../remark-structure-1kvQbrfH.js';
4
3
  import { S as SortedResult } from '../types-Ch8gnVgO.js';
5
4
  import { I as I18nConfig } from '../config-inq6kP6y.js';
@@ -7,7 +6,7 @@ import { LoaderOutput, LoaderConfig, InferPageType } from '../source/index.js';
7
6
  import 'mdast';
8
7
  import 'unified';
9
8
  import 'react';
10
- import '../page-tree-CfT5zlWh.js';
9
+ import '../page-tree-9q98UqWL.js';
11
10
 
12
11
  type AdvancedDocument = TypedDocument<Orama<typeof advancedSchema>>;
13
12
  declare const advancedSchema: {
@@ -70,7 +69,7 @@ interface SearchServer {
70
69
  export: () => Promise<ExportedData>;
71
70
  }
72
71
  interface SearchAPI extends SearchServer {
73
- GET: (request: NextRequest) => Promise<Response>;
72
+ GET: (request: Request) => Promise<Response>;
74
73
  /**
75
74
  * `GET` route handler that exports search indexes for static search.
76
75
  */
@@ -19,12 +19,13 @@ function createEndpoint(server) {
19
19
  return Response.json(await server.export());
20
20
  },
21
21
  async GET(request) {
22
- const query = request.nextUrl.searchParams.get("query");
22
+ const url = new URL(request.url);
23
+ const query = url.searchParams.get("query");
23
24
  if (!query) return Response.json([]);
24
25
  return Response.json(
25
26
  await search(query, {
26
- tag: request.nextUrl.searchParams.get("tag") ?? void 0,
27
- locale: request.nextUrl.searchParams.get("locale") ?? void 0
27
+ tag: url.searchParams.get("tag") ?? void 0,
28
+ locale: url.searchParams.get("locale") ?? void 0
28
29
  })
29
30
  );
30
31
  }
@@ -1,6 +1,6 @@
1
1
  export { a as TOCItemType, T as TableOfContents, g as getTableOfContents } from '../get-toc-Cr2URuiP.js';
2
- import { N as Node, I as Item, R as Root } from '../page-tree-CfT5zlWh.js';
3
- export { p as PageTree } from '../page-tree-CfT5zlWh.js';
2
+ import { N as Node, I as Item, R as Root, F as Folder } from '../page-tree-9q98UqWL.js';
3
+ export { p as PageTree } from '../page-tree-9q98UqWL.js';
4
4
  export { S as SortedResult } from '../types-Ch8gnVgO.js';
5
5
  import { Metadata } from 'next';
6
6
  import { NextRequest } from 'next/server';
@@ -21,10 +21,13 @@ declare function flattenTree(tree: Node[]): Item[];
21
21
  /**
22
22
  * Get neighbours of a page, useful for implementing "previous & next" buttons
23
23
  */
24
- declare function findNeighbour(tree: Root, url: string): {
24
+ declare function findNeighbour(tree: Root, url: string, options?: {
25
+ separateRoot?: boolean;
26
+ }): {
25
27
  previous?: Item;
26
28
  next?: Item;
27
29
  };
30
+ declare function getPageTreeRoots(pageTree: Root | Folder): (Root | Folder)[];
28
31
  /**
29
32
  * Separate the folder nodes of a root into multiple roots
30
33
  */
@@ -111,4 +114,4 @@ declare function createMetadataImage<S extends LoaderOutput<LoaderConfig>>(optio
111
114
  }) => Response | Promise<Response>) => (request: NextRequest, options: any) => Response | Promise<Response>;
112
115
  };
113
116
 
114
- export { type GetGithubLastCommitOptions, createMetadataImage, findNeighbour, flattenTree, getGithubLastEdit, separatePageTree };
117
+ export { type GetGithubLastCommitOptions, createMetadataImage, findNeighbour, flattenTree, getGithubLastEdit, getPageTreeRoots, separatePageTree };
@@ -5,7 +5,7 @@ import "../chunk-WQMD6AUR.js";
5
5
  import {
6
6
  createStyleTransformer,
7
7
  highlight
8
- } from "../chunk-E7AASGCN.js";
8
+ } from "../chunk-BUCUQ3WX.js";
9
9
  import "../chunk-MLKGABMK.js";
10
10
 
11
11
  // src/server/get-toc.ts
@@ -34,18 +34,34 @@ function flattenTree(tree) {
34
34
  return [node];
35
35
  });
36
36
  }
37
- function findNeighbour(tree, url) {
38
- const list = flattenTree(tree.children);
39
- for (let i = 0; i < list.length; i++) {
40
- if (list[i].url === url) {
41
- return {
42
- next: list[i + 1],
43
- previous: list[i - 1]
44
- };
37
+ function findNeighbour(tree, url, options) {
38
+ const { separateRoot = true } = options ?? {};
39
+ const roots = separateRoot ? getPageTreeRoots(tree) : [tree];
40
+ const lists = roots.map((node) => flattenTree(node.children));
41
+ for (const list of lists) {
42
+ for (let i = 0; i < list.length; i++) {
43
+ if (list[i].url === url) {
44
+ return {
45
+ next: list[i + 1],
46
+ previous: list[i - 1]
47
+ };
48
+ }
45
49
  }
46
50
  }
47
51
  return {};
48
52
  }
53
+ function getPageTreeRoots(pageTree) {
54
+ const result = pageTree.children.flatMap((child) => {
55
+ if (child.type !== "folder") return [];
56
+ const roots = getPageTreeRoots(child);
57
+ if (child.root) {
58
+ roots.push(child);
59
+ }
60
+ return roots;
61
+ });
62
+ if (!("type" in pageTree)) result.push(pageTree);
63
+ return result;
64
+ }
49
65
  function separatePageTree(pageTree) {
50
66
  return pageTree.children.flatMap((child) => {
51
67
  if (child.type !== "folder") return [];
@@ -162,6 +178,7 @@ export {
162
178
  findNeighbour,
163
179
  flattenTree,
164
180
  getGithubLastEdit,
181
+ getPageTreeRoots,
165
182
  getTableOfContents,
166
183
  highlight,
167
184
  separatePageTree
@@ -1,28 +1,49 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { I as I18nConfig } from '../config-inq6kP6y.js';
3
- import { R as Root, I as Item, F as Folder$1, S as Separator } from '../page-tree-CfT5zlWh.js';
3
+ import { R as Root, I as Item, F as Folder$1, S as Separator } from '../page-tree-9q98UqWL.js';
4
4
 
5
5
  interface FileInfo {
6
6
  /**
7
- * The locale extension of file
7
+ * locale extension from the last second `.`, like `.en`
8
+ *
9
+ * empty string if no locale
10
+ */
11
+ locale: string;
12
+ /**
13
+ * File path without extension
14
+ *
15
+ * @deprecated obtain it with `join(dirname, name)`
8
16
  */
9
- locale?: string;
17
+ flattenedPath: string;
10
18
  /**
11
19
  * Original path of file
12
20
  */
13
21
  path: string;
14
22
  /**
15
- * File path without extension
23
+ * File name without locale and extension
16
24
  */
17
- flattenedPath: string;
25
+ name: string;
18
26
  /**
19
- * File name without locale and extension
27
+ * file extension from the last `.`, like `.md`
28
+ *
29
+ * empty string if no file extension
30
+ */
31
+ ext: string;
32
+ dirname: string;
33
+ }
34
+ interface FolderInfo {
35
+ /**
36
+ * Original path of folder
37
+ */
38
+ path: string;
39
+ /**
40
+ * folder name
20
41
  */
21
42
  name: string;
22
43
  dirname: string;
23
44
  }
24
45
  declare function parseFilePath(path: string): FileInfo;
25
- declare function parseFolderPath(path: string): FileInfo;
46
+ declare function parseFolderPath(path: string): FolderInfo;
26
47
 
27
48
  interface LoadOptions {
28
49
  transformers?: Transformer[];
@@ -102,7 +123,16 @@ interface LanguageEntry<Data = PageData> {
102
123
  }
103
124
  interface LoaderOutput<Config extends LoaderConfig> {
104
125
  pageTree: Config['i18n'] extends true ? Record<string, Root> : Root;
105
- getPageTree(locale?: string): Root;
126
+ getPageTree: (locale?: string) => Root;
127
+ getPageByHref: (href: string, options?: {
128
+ /**
129
+ * resolve relative file paths in `href` from specified dirname, must be a virtual path.
130
+ */
131
+ dir?: string;
132
+ }) => {
133
+ page: Page<Config['source']['pageData']>;
134
+ hash?: string;
135
+ } | undefined;
106
136
  _i18n?: I18nConfig;
107
137
  /**
108
138
  * Get list of pages from language, empty if language hasn't specified
@@ -141,6 +171,7 @@ interface MetaData {
141
171
  interface PageData {
142
172
  icon?: string | undefined;
143
173
  title?: string;
174
+ description?: string | undefined;
144
175
  }
145
176
  type InferPageType<Utils extends LoaderOutput<any>> = Utils extends LoaderOutput<infer Config> ? Page<Config['source']['pageData']> : never;
146
177
  type InferMetaType<Utils extends LoaderOutput<any>> = Utils extends LoaderOutput<infer Config> ? Meta<Config['source']['metaData']> : never;
@@ -164,7 +195,7 @@ interface PageFile {
164
195
  }
165
196
  type File = MetaFile | PageFile;
166
197
  interface Folder {
167
- file: FileInfo;
198
+ file: FolderInfo;
168
199
  children: (File | Folder)[];
169
200
  }
170
201
  /**
@@ -229,4 +260,4 @@ interface PageTreeBuilder {
229
260
  }
230
261
  declare function createPageTreeBuilder(): PageTreeBuilder;
231
262
 
232
- export { type BuildPageTreeOptions, type BuildPageTreeOptionsWithI18n, type FileInfo, fileSystem as FileSystem, type InferMetaType, type InferPageType, type LanguageEntry, type LoadOptions, type LoaderConfig, type LoaderOptions, type LoaderOutput, type Meta, type MetaData, type Page, type PageData, type PageTreeBuilder, type Source, type SourceConfig, type Transformer, type UrlFn, type VirtualFile, createGetUrl, createPageTreeBuilder, getSlugs, loadFiles, loader, parseFilePath, parseFolderPath };
263
+ export { type BuildPageTreeOptions, type BuildPageTreeOptionsWithI18n, type FileInfo, fileSystem as FileSystem, type FolderInfo, type InferMetaType, type InferPageType, type LanguageEntry, type LoadOptions, type LoaderConfig, type LoaderOptions, type LoaderOutput, type Meta, type MetaData, type Page, type PageData, type PageTreeBuilder, type Source, type SourceConfig, type Transformer, type UrlFn, type VirtualFile, createGetUrl, createPageTreeBuilder, getSlugs, loadFiles, loader, parseFilePath, parseFolderPath };
@@ -1,8 +1,8 @@
1
1
  import {
2
- resolvePath,
2
+ joinPath,
3
3
  slash,
4
4
  splitPath
5
- } from "../chunk-SHGL6VBO.js";
5
+ } from "../chunk-QTVCCXFT.js";
6
6
  import {
7
7
  __export
8
8
  } from "../chunk-MLKGABMK.js";
@@ -24,8 +24,14 @@ function buildAll(nodes, ctx, skipIndex) {
24
24
  for (const node of [...nodes].sort(
25
25
  (a, b) => a.file.name.localeCompare(b.file.name)
26
26
  )) {
27
- if (isPageFile(node) && !node.file.locale) {
28
- const treeNode = buildFileNode(node, ctx);
27
+ if (isPageFile(node)) {
28
+ if (node.file.locale.length > 0 && node.file.locale.slice(1) !== ctx.defaultLanguage)
29
+ continue;
30
+ const localized = ctx.storage.read(
31
+ joinPath(node.file.dirname, `${node.file.name}.${ctx.lang}`),
32
+ "page"
33
+ );
34
+ const treeNode = buildFileNode(localized ?? node, ctx);
29
35
  if (node.file.name === "index") {
30
36
  if (!skipIndex) output.unshift(treeNode);
31
37
  continue;
@@ -33,10 +39,11 @@ function buildAll(nodes, ctx, skipIndex) {
33
39
  output.push(treeNode);
34
40
  }
35
41
  if ("children" in node) {
36
- if (node.children.length === 1 && node.children[0].file.name === "index" && isPageFile(node.children[0])) {
37
- output.push(buildFileNode(node.children[0], ctx));
42
+ const folder = buildFolderNode(node, false, ctx);
43
+ if (folder.children.length === 0 && folder.index) {
44
+ output.push(folder.index);
38
45
  } else {
39
- folders.push(buildFolderNode(node, false, ctx));
46
+ folders.push(folder);
40
47
  }
41
48
  }
42
49
  }
@@ -75,8 +82,11 @@ function resolveFolderItem(folder, item, ctx, idx, addedNodePaths) {
75
82
  } else if (isExtract) {
76
83
  filename = item.slice(extractPrefix.length);
77
84
  }
78
- const path = resolvePath(folder.file.path, filename);
79
- const itemNode = ctx.storage.readDir(path) ?? ctx.storage.read(path, "page");
85
+ const path = joinPath(folder.file.path, filename);
86
+ let itemNode = ctx.storage.readDir(path);
87
+ if (!itemNode) {
88
+ itemNode = (ctx.lang ? ctx.storage.read(`${path}.${ctx.lang}`, "page") : null) ?? ctx.storage.read(`${path}.${ctx.defaultLanguage}`, "page") ?? ctx.storage.read(path, "page");
89
+ }
80
90
  if (!itemNode) return [];
81
91
  addedNodePaths.add(itemNode.file.path);
82
92
  if (isExcept) return [];
@@ -87,10 +97,10 @@ function resolveFolderItem(folder, item, ctx, idx, addedNodePaths) {
87
97
  return [buildFileNode(itemNode, ctx)];
88
98
  }
89
99
  function buildFolderNode(folder, isGlobalRoot, ctx) {
90
- const metaPath = resolvePath(folder.file.path, "meta");
91
- const meta = findLocalizedFile(metaPath, "meta", ctx) ?? ctx.storage.read(metaPath, "meta");
100
+ const metaPath = joinPath(folder.file.path, "meta");
101
+ const meta = ctx.storage.read(`${metaPath}.${ctx.lang}`, "meta") ?? ctx.storage.read(`${metaPath}.${ctx.defaultLanguage}`, "meta") ?? ctx.storage.read(metaPath, "meta");
92
102
  const indexFile = ctx.storage.read(
93
- resolvePath(folder.file.flattenedPath, "index"),
103
+ joinPath(folder.file.path, "index"),
94
104
  "page"
95
105
  );
96
106
  const metadata = meta?.data;
@@ -137,15 +147,15 @@ function buildFolderNode(folder, isGlobalRoot, ctx) {
137
147
  return ctx.options.attachFolder?.(node, folder, meta) ?? node;
138
148
  }
139
149
  function buildFileNode(file, ctx) {
140
- const localized = findLocalizedFile(file.file.flattenedPath, "page", ctx) ?? file;
141
150
  const item = {
142
- $id: localized.file.path,
151
+ $id: file.file.path,
143
152
  type: "page",
144
- name: localized.data.data.title ?? pathToName(localized.file.name),
145
- icon: ctx.options.resolveIcon?.(localized.data.data.icon),
146
- url: ctx.options.getUrl(localized.data.slugs, ctx.lang),
153
+ name: file.data.data.title ?? pathToName(file.file.name),
154
+ description: file.data.data.description,
155
+ icon: ctx.options.resolveIcon?.(file.data.data.icon),
156
+ url: ctx.options.getUrl(file.data.slugs, ctx.lang),
147
157
  $ref: !ctx.options.noRef ? {
148
- file: localized.file.path
158
+ file: file.file.path
149
159
  } : void 0
150
160
  };
151
161
  return ctx.options.attachFile?.(item, file) ?? item;
@@ -174,7 +184,7 @@ function createPageTreeBuilder() {
174
184
  options,
175
185
  builder: this,
176
186
  storage: options.storage,
177
- i18n
187
+ defaultLanguage: i18n.defaultLanguage
178
188
  });
179
189
  return [lang, tree];
180
190
  });
@@ -182,10 +192,6 @@ function createPageTreeBuilder() {
182
192
  }
183
193
  };
184
194
  }
185
- function findLocalizedFile(path, format, ctx) {
186
- if (!ctx.lang) return;
187
- return ctx.storage.read(`${path}.${ctx.lang}`, format);
188
- }
189
195
  function pathToName(name) {
190
196
  const result = [];
191
197
  for (const c of name) {
@@ -200,39 +206,40 @@ function pathToName(name) {
200
206
  function parseFilePath(path) {
201
207
  const segments = splitPath(slash(path));
202
208
  const dirname = segments.slice(0, -1).join("/");
203
- const base = segments.at(-1) ?? "";
204
- const dotIdx = base.lastIndexOf(".");
205
- const nameWithLocale = dotIdx !== -1 ? base.slice(0, dotIdx) : base;
206
- const flattenedPath = [dirname, nameWithLocale].filter((p) => p.length > 0).join("/");
207
- const [name, locale] = getLocale(nameWithLocale);
209
+ let name = segments.at(-1) ?? "";
210
+ let ext = "";
211
+ let locale = "";
212
+ let dotIdx = name.lastIndexOf(".");
213
+ if (dotIdx !== -1) {
214
+ ext = name.substring(dotIdx);
215
+ name = name.substring(0, dotIdx);
216
+ }
217
+ dotIdx = name.lastIndexOf(".");
218
+ if (dotIdx !== -1 && isLocale(name.substring(dotIdx))) {
219
+ locale = name.substring(dotIdx);
220
+ name = name.substring(0, dotIdx);
221
+ }
208
222
  return {
209
223
  dirname,
210
224
  name,
211
- flattenedPath,
212
225
  locale,
213
- path: segments.join("/")
226
+ path: segments.join("/"),
227
+ ext,
228
+ flattenedPath: [dirname, `${name}${locale}`].filter((p) => p.length > 0).join("/")
214
229
  };
215
230
  }
231
+ function isLocale(code) {
232
+ return code.length > 0 && !/\d+/.test(code);
233
+ }
216
234
  function parseFolderPath(path) {
217
235
  const segments = splitPath(slash(path));
218
236
  const base = segments.at(-1) ?? "";
219
- const [name, locale] = getLocale(base);
220
- const flattenedPath = segments.join("/");
221
237
  return {
222
238
  dirname: segments.slice(0, -1).join("/"),
223
- name,
224
- flattenedPath,
225
- locale,
226
- path: flattenedPath
239
+ name: base,
240
+ path: segments.join("/")
227
241
  };
228
242
  }
229
- function getLocale(name) {
230
- const sep = name.lastIndexOf(".");
231
- if (sep === -1) return [name];
232
- const locale = name.slice(sep + 1);
233
- if (/\d+/.exec(locale)) return [name];
234
- return [name.slice(0, sep), locale];
235
- }
236
243
  function normalizePath(path) {
237
244
  const segments = splitPath(slash(path));
238
245
  if (segments[0] === "." || segments[0] === "..")
@@ -276,7 +283,13 @@ var Storage = class {
276
283
  };
277
284
  this.makeDir(node.file.dirname);
278
285
  this.readDir(node.file.dirname)?.children.push(node);
279
- this.files.set(`${node.file.flattenedPath}.${node.format}`, node);
286
+ this.files.set(
287
+ joinPath(
288
+ node.file.dirname,
289
+ `${node.file.name}${node.file.locale}.${node.format}`
290
+ ),
291
+ node
292
+ );
280
293
  }
281
294
  list() {
282
295
  return [...this.files.values()];
@@ -334,17 +347,20 @@ function indexPages(storage, getUrl, i18n) {
334
347
  if (item.format === "meta")
335
348
  pathToFile.set(item.file.path, fileToMeta(item));
336
349
  if (item.format === "page") {
337
- const page = fileToPage(item, getUrl, item.file.locale);
350
+ if (item.file.locale.length > 0 && item.file.locale.slice(1) !== defaultLanguage)
351
+ continue;
352
+ const page = fileToPage(item, getUrl, defaultLanguage);
338
353
  pathToFile.set(item.file.path, page);
339
- if (item.file.locale) continue;
340
354
  map.set(`${defaultLanguage}.${page.slugs.join("/")}`, page);
341
355
  if (!i18n) continue;
356
+ const basePath = joinPath(item.file.dirname, item.file.name);
342
357
  for (const lang of i18n.languages) {
343
- const localized = storage.read(
344
- `${item.file.flattenedPath}.${lang}`,
345
- "page"
358
+ if (lang === defaultLanguage) continue;
359
+ const localizedPage = fileToPage(
360
+ storage.read(`${basePath}.${lang}`, "page") ?? item,
361
+ getUrl,
362
+ lang
346
363
  );
347
- const localizedPage = fileToPage(localized ?? item, getUrl, lang);
348
364
  map.set(`${lang}.${localizedPage.slugs.join("/")}`, localizedPage);
349
365
  }
350
366
  }
@@ -355,6 +371,7 @@ function indexPages(storage, getUrl, i18n) {
355
371
  };
356
372
  }
357
373
  function createGetUrl(baseUrl, i18n) {
374
+ const baseSlugs = baseUrl.split("/");
358
375
  return (slugs, locale) => {
359
376
  const hideLocale = i18n?.hideLocale ?? "never";
360
377
  let urlLocale;
@@ -363,7 +380,8 @@ function createGetUrl(baseUrl, i18n) {
363
380
  } else if (hideLocale === "default-locale" && locale !== i18n?.defaultLanguage) {
364
381
  urlLocale = locale;
365
382
  }
366
- const paths = urlLocale ? [urlLocale, ...baseUrl.split("/"), ...slugs] : [...baseUrl.split("/"), ...slugs];
383
+ const paths = [...baseSlugs, ...slugs];
384
+ if (urlLocale) paths.unshift(urlLocale);
367
385
  return `/${paths.filter((v) => v.length > 0).join("/")}`;
368
386
  };
369
387
  }
@@ -395,21 +413,50 @@ function createOutput(options) {
395
413
  );
396
414
  const walker = indexPages(storage, getUrl, options.i18n);
397
415
  const builder = createPageTreeBuilder();
398
- const pageTree = options.i18n === void 0 ? builder.build({
399
- storage,
400
- resolveIcon: options.icon,
401
- getUrl,
402
- ...options.pageTree
403
- }) : builder.buildI18n({
404
- storage,
405
- resolveIcon: options.icon,
406
- getUrl,
407
- i18n: options.i18n,
408
- ...options.pageTree
409
- });
416
+ let pageTree;
410
417
  return {
411
418
  _i18n: options.i18n,
412
- pageTree,
419
+ get pageTree() {
420
+ if (options.i18n) {
421
+ pageTree ??= builder.buildI18n({
422
+ storage,
423
+ resolveIcon: options.icon,
424
+ getUrl,
425
+ i18n: options.i18n,
426
+ ...options.pageTree
427
+ });
428
+ } else {
429
+ pageTree ??= builder.build({
430
+ storage,
431
+ resolveIcon: options.icon,
432
+ getUrl,
433
+ ...options.pageTree
434
+ });
435
+ }
436
+ return pageTree;
437
+ },
438
+ set pageTree(v) {
439
+ pageTree = v;
440
+ },
441
+ getPageByHref(href, { dir = "" } = {}) {
442
+ const pages = Array.from(walker.pages.values());
443
+ const [value, hash] = href.split("#", 2);
444
+ if (value.startsWith(".") && (value.endsWith(".md") || value.endsWith(".mdx"))) {
445
+ const hrefPath = joinPath(dir, value);
446
+ const target2 = pages.find((item) => item.file.path === hrefPath);
447
+ if (target2)
448
+ return {
449
+ page: target2,
450
+ hash
451
+ };
452
+ }
453
+ const target = pages.find((item) => item.url === value);
454
+ if (target)
455
+ return {
456
+ page: target,
457
+ hash
458
+ };
459
+ },
413
460
  getPages(language = options.i18n?.defaultLanguage ?? "") {
414
461
  const pages = [];
415
462
  for (const key of walker.pages.keys()) {
@@ -437,9 +484,9 @@ function createOutput(options) {
437
484
  },
438
485
  getPageTree(locale) {
439
486
  if (options.i18n) {
440
- return pageTree[locale ?? options.i18n.defaultLanguage];
487
+ return this.pageTree[locale ?? options.i18n.defaultLanguage];
441
488
  }
442
- return pageTree;
489
+ return this.pageTree;
443
490
  },
444
491
  getNodePage(node) {
445
492
  const ref = node.$ref?.file ?? node.$id;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  useShiki
3
- } from "../chunk-Q73QZKHO.js";
4
- import "../chunk-E7AASGCN.js";
3
+ } from "../chunk-62S4EN7J.js";
4
+ import "../chunk-BUCUQ3WX.js";
5
5
  import "../chunk-MLKGABMK.js";
6
6
  export {
7
7
  useShiki
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "15.0.18",
3
+ "version": "15.1.1",
4
4
  "description": "The library for building a documentation website in Next.js",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -114,12 +114,12 @@
114
114
  "@types/mdast": "^4.0.3",
115
115
  "@types/negotiator": "^0.6.3",
116
116
  "@types/node": "22.13.10",
117
- "@types/react": "^19.0.10",
117
+ "@types/react": "^19.0.11",
118
118
  "@types/react-dom": "^19.0.4",
119
119
  "algoliasearch": "4.24.0",
120
120
  "mdast-util-mdx-jsx": "^3.2.0",
121
121
  "mdast-util-mdxjs-esm": "^2.0.1",
122
- "next": "^15.2.2",
122
+ "next": "^15.2.3",
123
123
  "remark-mdx": "^3.1.0",
124
124
  "remark-rehype": "^11.1.1",
125
125
  "typescript": "^5.8.2",