fumadocs-core 13.2.1 → 13.3.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.
@@ -0,0 +1,67 @@
1
+ // src/i18n/middleware.ts
2
+ import { match as matchLocale } from "@formatjs/intl-localematcher";
3
+ import Negotiator from "negotiator";
4
+ import { NextResponse } from "next/server";
5
+ var COOKIE = "FD_LOCALE";
6
+ function getLocale(request, locales, defaultLanguage) {
7
+ const negotiatorHeaders = {};
8
+ request.headers.forEach((value, key) => negotiatorHeaders[key] = value);
9
+ const languages = new Negotiator({ headers: negotiatorHeaders }).languages(
10
+ locales
11
+ );
12
+ return matchLocale(languages, locales, defaultLanguage);
13
+ }
14
+ var defaultFormat = (locale, path) => {
15
+ return `/${locale}/${path}`;
16
+ };
17
+ function createI18nMiddleware({
18
+ languages,
19
+ defaultLanguage,
20
+ format = defaultFormat,
21
+ hideLocale = "never"
22
+ }) {
23
+ function getUrl(request, pathname, locale) {
24
+ if (!locale) {
25
+ return new URL(
26
+ pathname.startsWith("/") ? pathname : `/${pathname}`,
27
+ request.url
28
+ );
29
+ }
30
+ return new URL(
31
+ format(locale, pathname.startsWith("/") ? pathname.slice(1) : pathname),
32
+ request.url
33
+ );
34
+ }
35
+ return (request) => {
36
+ const { pathname } = request.nextUrl;
37
+ const pathLocale = languages.find(
38
+ (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
39
+ );
40
+ if (!pathLocale) {
41
+ if (hideLocale === "default-locale") {
42
+ return NextResponse.rewrite(getUrl(request, pathname, defaultLanguage));
43
+ }
44
+ const preferred = getLocale(request, languages, defaultLanguage);
45
+ if (hideLocale === "always") {
46
+ const locale = request.cookies.get(COOKIE)?.value ?? preferred;
47
+ return NextResponse.rewrite(getUrl(request, pathname, locale));
48
+ }
49
+ return NextResponse.redirect(getUrl(request, pathname, preferred));
50
+ }
51
+ if (hideLocale === "always") {
52
+ const path = pathname.slice(`/${pathLocale}`.length);
53
+ const res = NextResponse.redirect(getUrl(request, path));
54
+ res.cookies.set(COOKIE, pathLocale);
55
+ return res;
56
+ }
57
+ if (hideLocale === "default-locale" && pathLocale === defaultLanguage) {
58
+ const path = pathname.slice(`/${pathLocale}`.length);
59
+ return NextResponse.redirect(getUrl(request, path));
60
+ }
61
+ return NextResponse.next();
62
+ };
63
+ }
64
+
65
+ export {
66
+ createI18nMiddleware
67
+ };
@@ -20,6 +20,12 @@ function remarkHeading({
20
20
  return (root, file) => {
21
21
  const toc = [];
22
22
  slugger.reset();
23
+ if (file.data.frontmatter) {
24
+ const frontmatter = file.data.frontmatter;
25
+ if (frontmatter._openapi?.toc) {
26
+ toc.push(...frontmatter._openapi.toc);
27
+ }
28
+ }
23
29
  visit(root, "heading", (heading) => {
24
30
  heading.data ||= {};
25
31
  heading.data.hProperties ||= {};
@@ -0,0 +1,26 @@
1
+ interface I18nConfig {
2
+ /**
3
+ * Supported locale codes.
4
+ *
5
+ * A page tree will be built for each language.
6
+ */
7
+ languages: string[];
8
+ /**
9
+ * Default locale if not specified
10
+ */
11
+ defaultLanguage: string;
12
+ /**
13
+ * Don't show the locale prefix on URL.
14
+ *
15
+ * - `always`: Always hide the prefix
16
+ * - `default-locale`: Only hide the default locale
17
+ * - `never`: Never hide the prefix
18
+ *
19
+ * This API uses `NextResponse.rewrite`.
20
+ *
21
+ * @defaultValue 'never'
22
+ */
23
+ hideLocale?: 'always' | 'default-locale' | 'never';
24
+ }
25
+
26
+ export type { I18nConfig as I };
@@ -0,0 +1,12 @@
1
+ import { I as I18nConfig } from '../config-inq6kP6y.js';
2
+ import { NextMiddleware } from 'next/dist/server/web/types';
3
+
4
+ interface MiddlewareOptions extends I18nConfig {
5
+ /**
6
+ * A function that adds the locale prefix to path name
7
+ */
8
+ format?: (locale: string, path: string) => string;
9
+ }
10
+ declare function createI18nMiddleware({ languages, defaultLanguage, format, hideLocale, }: MiddlewareOptions): NextMiddleware;
11
+
12
+ export { I18nConfig, createI18nMiddleware };
@@ -0,0 +1,7 @@
1
+ import {
2
+ createI18nMiddleware
3
+ } from "../chunk-MXOJWF66.js";
4
+ import "../chunk-MLKGABMK.js";
5
+ export {
6
+ createI18nMiddleware
7
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  flattenNode,
3
3
  remarkHeading
4
- } from "../chunk-YKIM647L.js";
4
+ } from "../chunk-UQV4A7HQ.js";
5
5
  import {
6
6
  slash
7
7
  } from "../chunk-UWEEHUJV.js";
@@ -325,7 +325,10 @@ function remarkImage({
325
325
  const hasBlur = placeholder === "blur" && VALID_BLUR_EXT.some((ext) => src.endsWith(ext));
326
326
  importsToInject.push({
327
327
  variableName,
328
- importPath: slash(resolveSrc(src, publicDir))
328
+ importPath: slash(
329
+ // with imports, relative paths don't have to be absolute
330
+ src.startsWith("/") ? path.join(publicDir, src) : src
331
+ )
329
332
  });
330
333
  Object.assign(node, {
331
334
  type: "mdxJsxFlowElement",
@@ -417,6 +420,13 @@ function remarkStructure({
417
420
  slugger.reset();
418
421
  const data = { contents: [], headings: [] };
419
422
  let lastHeading = "";
423
+ if (file.data.frontmatter) {
424
+ const frontmatter = file.data.frontmatter;
425
+ if (frontmatter._openapi?.structuredData) {
426
+ data.headings.push(...frontmatter._openapi.structuredData.headings);
427
+ data.contents.push(...frontmatter._openapi.structuredData.contents);
428
+ }
429
+ }
420
430
  visit2(node, types, (element) => {
421
431
  if (element.type === "root") return;
422
432
  const content = flattenNode(element).trim();
@@ -1,31 +1,3 @@
1
- import { NextMiddleware } from 'next/dist/server/web/types';
2
-
3
- interface MiddlewareOptions {
4
- /**
5
- * Supported locale codes
6
- */
7
- languages: string[];
8
- /**
9
- * Default locale if not specified
10
- */
11
- defaultLanguage: string;
12
- /**
13
- * A function that adds the locale prefix to path name
14
- */
15
- format?: (locale: string, path: string) => string;
16
- /**
17
- * Don't show the locale prefix on URL.
18
- *
19
- * - `always`: Always hide the prefix
20
- * - `default-locale`: Only hide the default locale
21
- * - `never`: Never hide the prefix
22
- *
23
- * This API uses `NextResponse.rewrite`.
24
- *
25
- * @defaultValue 'never'
26
- */
27
- hideLocale?: 'always' | 'default-locale' | 'never';
28
- }
29
- declare function createI18nMiddleware({ languages, defaultLanguage, format, hideLocale, }: MiddlewareOptions): NextMiddleware;
30
-
31
- export { createI18nMiddleware };
1
+ export { createI18nMiddleware } from './i18n/index.js';
2
+ import './config-inq6kP6y.js';
3
+ import 'next/dist/server/web/types';
@@ -1,52 +1,7 @@
1
+ import {
2
+ createI18nMiddleware
3
+ } from "./chunk-MXOJWF66.js";
1
4
  import "./chunk-MLKGABMK.js";
2
-
3
- // src/middleware.ts
4
- import { match as matchLocale } from "@formatjs/intl-localematcher";
5
- import Negotiator from "negotiator";
6
- import { NextResponse } from "next/server";
7
- function getLocale(request, locales, defaultLanguage) {
8
- const negotiatorHeaders = {};
9
- request.headers.forEach((value, key) => negotiatorHeaders[key] = value);
10
- const languages = new Negotiator({ headers: negotiatorHeaders }).languages(
11
- locales
12
- );
13
- return matchLocale(languages, locales, defaultLanguage);
14
- }
15
- var defaultFormat = (locale, path) => {
16
- return `/${locale}/${path}`;
17
- };
18
- function createI18nMiddleware({
19
- languages,
20
- defaultLanguage,
21
- format = defaultFormat,
22
- hideLocale = "never"
23
- }) {
24
- function shouldHideLocale(locale) {
25
- return hideLocale === "always" || hideLocale === "default-locale" && locale === defaultLanguage;
26
- }
27
- return (request) => {
28
- const { pathname } = request.nextUrl;
29
- const pathLocale = languages.find(
30
- (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
31
- );
32
- if (!pathLocale) {
33
- const locale = getLocale(request, languages, defaultLanguage);
34
- let path = pathname;
35
- while (path.startsWith("/")) {
36
- path = path.slice(1);
37
- }
38
- const url = new URL(format(locale, path), request.url);
39
- return shouldHideLocale(locale) ? NextResponse.rewrite(url) : NextResponse.redirect(url);
40
- }
41
- if (hideLocale === "default-locale" && pathLocale === defaultLanguage) {
42
- const path = pathname.slice(`/${pathLocale}`.length);
43
- return NextResponse.redirect(
44
- new URL(path.startsWith("/") ? path : `/${path}`, request.url)
45
- );
46
- }
47
- return NextResponse.next();
48
- };
49
- }
50
5
  export {
51
6
  createI18nMiddleware
52
7
  };
@@ -1,9 +1,33 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { S as StructuredData } from '../remark-structure-Dj8oa5Ba.js';
3
3
  import { SortedResult } from './shared.js';
4
+ import { I as I18nConfig } from '../config-inq6kP6y.js';
4
5
  import 'mdast';
5
6
  import 'unified';
6
7
 
8
+ interface I18nSimpleOptions extends Omit<SimpleOptions, 'language' | 'indexes'> {
9
+ i18n: I18nConfig;
10
+ indexes: WithLocale<Index>[] | Dynamic<WithLocale<Index>>;
11
+ }
12
+ interface I18nAdvancedOptions extends Omit<AdvancedOptions, 'language' | 'indexes'> {
13
+ i18n: I18nConfig;
14
+ indexes: WithLocale<AdvancedIndex>[] | Dynamic<WithLocale<AdvancedIndex>>;
15
+ }
16
+ type WithLocale<T> = T & {
17
+ locale: string;
18
+ };
19
+ declare function createI18nSearchAPI$1<T extends 'simple' | 'advanced'>(type: T, options: T extends 'simple' ? I18nSimpleOptions : I18nAdvancedOptions): SearchAPI;
20
+
21
+ type ToI18n<T extends {
22
+ indexes: unknown;
23
+ }> = Omit<T, 'indexes' | 'language'> & {
24
+ indexes: ([language: string, indexes: T['indexes']] | {
25
+ language: string;
26
+ indexes: T['indexes'];
27
+ })[];
28
+ };
29
+ declare function createI18nSearchAPI<T extends 'simple' | 'advanced'>(type: T, options: T extends 'simple' ? ToI18n<SimpleOptions> : ToI18n<AdvancedOptions>): SearchAPI;
30
+
7
31
  interface SearchAPI {
8
32
  GET: (request: NextRequest) => Promise<NextResponse<SortedResult[]>>;
9
33
  search: (query: string, options?: {
@@ -29,16 +53,7 @@ interface AdvancedOptions {
29
53
  tag?: boolean;
30
54
  language?: string;
31
55
  }
32
- type ToI18n<T extends {
33
- indexes: unknown;
34
- }> = Omit<T, 'indexes' | 'language'> & {
35
- indexes: ([language: string, indexes: T['indexes']] | {
36
- language: string;
37
- indexes: T['indexes'];
38
- })[];
39
- };
40
56
  declare function createSearchAPI<T extends 'simple' | 'advanced'>(type: T, options: T extends 'simple' ? SimpleOptions : AdvancedOptions): SearchAPI;
41
- declare function createI18nSearchAPI<T extends 'simple' | 'advanced'>(type: T, options: T extends 'simple' ? ToI18n<SimpleOptions> : ToI18n<AdvancedOptions>): SearchAPI;
42
57
  interface Index {
43
58
  title: string;
44
59
  description?: string;
@@ -64,4 +79,4 @@ interface AdvancedIndex {
64
79
  }
65
80
  declare function initSearchAPIAdvanced({ indexes, language, tag, }: AdvancedOptions): SearchAPI;
66
81
 
67
- export { type AdvancedIndex, type Index, createI18nSearchAPI, createSearchAPI, initSearchAPI, initSearchAPIAdvanced };
82
+ export { type AdvancedIndex, type AdvancedOptions, type Dynamic, type Index, type SearchAPI, type SimpleOptions, createI18nSearchAPI, createI18nSearchAPI$1 as createI18nSearchAPIExperimental, createSearchAPI, initSearchAPI, initSearchAPIAdvanced };
@@ -2,8 +2,10 @@ import "../chunk-MLKGABMK.js";
2
2
 
3
3
  // src/search/server.ts
4
4
  import { Document } from "flexsearch";
5
+
6
+ // src/search/create-endpoint.ts
5
7
  import { NextResponse } from "next/server";
6
- function create(search) {
8
+ function createEndpoint(search) {
7
9
  return {
8
10
  search,
9
11
  async GET(request) {
@@ -18,13 +20,32 @@ function create(search) {
18
20
  }
19
21
  };
20
22
  }
21
- function createSearchAPI(type, options) {
22
- if (type === "simple") {
23
- return initSearchAPI(options);
24
- }
25
- return initSearchAPIAdvanced(options);
26
- }
23
+
24
+ // src/search/i18n-api.ts
27
25
  function createI18nSearchAPI(type, options) {
26
+ const map = /* @__PURE__ */ new Map();
27
+ return createEndpoint(async (query, searchOptions) => {
28
+ if (map.size === 0) {
29
+ const indexes = typeof options.indexes === "function" ? await options.indexes() : options.indexes;
30
+ for (const locale of options.i18n.languages) {
31
+ const api = createSearchAPI(type, {
32
+ ...options,
33
+ language: locale,
34
+ indexes: indexes.filter((index) => index.locale === locale)
35
+ });
36
+ map.set(locale, api);
37
+ }
38
+ }
39
+ const handler = map.get(
40
+ searchOptions?.locale ?? options.i18n.defaultLanguage
41
+ );
42
+ if (handler) return handler.search(query, searchOptions);
43
+ return [];
44
+ });
45
+ }
46
+
47
+ // src/search/legacy-i18n-api.ts
48
+ function createI18nSearchAPI2(type, options) {
28
49
  const map = /* @__PURE__ */ new Map();
29
50
  for (const entry of options.indexes) {
30
51
  const v = Array.isArray(entry) ? { language: entry[0], indexes: entry[1] } : entry;
@@ -38,7 +59,7 @@ function createI18nSearchAPI(type, options) {
38
59
  })
39
60
  );
40
61
  }
41
- return create(async (query, searchOptions) => {
62
+ return createEndpoint(async (query, searchOptions) => {
42
63
  if (searchOptions?.locale) {
43
64
  const handler = map.get(searchOptions.locale);
44
65
  if (handler) return handler.search(query, searchOptions);
@@ -46,6 +67,14 @@ function createI18nSearchAPI(type, options) {
46
67
  return [];
47
68
  });
48
69
  }
70
+
71
+ // src/search/server.ts
72
+ function createSearchAPI(type, options) {
73
+ if (type === "simple") {
74
+ return initSearchAPI(options);
75
+ }
76
+ return initSearchAPIAdvanced(options);
77
+ }
49
78
  function initSearchAPI({ indexes, language }) {
50
79
  const store = ["title", "url"];
51
80
  async function getDocument() {
@@ -99,7 +128,7 @@ function initSearchAPI({ indexes, language }) {
99
128
  return index;
100
129
  }
101
130
  const doc = getDocument();
102
- return create(async (query) => {
131
+ return createEndpoint(async (query) => {
103
132
  const results = (await doc).search(query, 5, {
104
133
  enrich: true,
105
134
  suggest: true
@@ -189,7 +218,7 @@ function initSearchAPIAdvanced({
189
218
  return index;
190
219
  }
191
220
  const doc = getDocument();
192
- return create(async (query, options) => {
221
+ return createEndpoint(async (query, options) => {
193
222
  const index = await doc;
194
223
  const results = index.search(query, 5, {
195
224
  enrich: true,
@@ -229,7 +258,8 @@ function initSearchAPIAdvanced({
229
258
  });
230
259
  }
231
260
  export {
232
- createI18nSearchAPI,
261
+ createI18nSearchAPI2 as createI18nSearchAPI,
262
+ createI18nSearchAPI as createI18nSearchAPIExperimental,
233
263
  createSearchAPI,
234
264
  initSearchAPI,
235
265
  initSearchAPIAdvanced
@@ -1,6 +1,7 @@
1
1
  export { a as TOCItemType, T as TableOfContents, g as getTableOfContents } from '../get-toc-CM4X3hbw.js';
2
2
  import { N as Node, I as Item, R as Root } from '../page-tree-BTCDMLTU.js';
3
3
  export { p as PageTree } from '../page-tree-BTCDMLTU.js';
4
+ export { SortedResult } from '../search/shared.js';
4
5
  import 'react';
5
6
 
6
7
  /**
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  remarkHeading
3
- } from "../chunk-YKIM647L.js";
3
+ } from "../chunk-UQV4A7HQ.js";
4
4
  import "../chunk-MLKGABMK.js";
5
5
 
6
6
  // src/server/get-toc.ts
package/dist/sidebar.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import "./chunk-MLKGABMK.js";
2
3
 
3
4
  // src/sidebar.tsx
@@ -1,4 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
+ import { I as I18nConfig } from '../config-inq6kP6y.js';
2
3
  import { R as Root, I as Item, F as Folder$1, S as Separator } from '../page-tree-BTCDMLTU.js';
3
4
 
4
5
  interface FileInfo {
@@ -49,7 +50,7 @@ interface SourceConfig {
49
50
  pageData: PageData;
50
51
  metaData: MetaData;
51
52
  }
52
- interface LoaderOptions extends Pick<BuildPageTreeOptionsWithI18n, 'languages' | 'defaultLanguage'> {
53
+ interface LoaderOptions {
53
54
  /**
54
55
  * @defaultValue `''`
55
56
  */
@@ -67,6 +68,24 @@ interface LoaderOptions extends Pick<BuildPageTreeOptionsWithI18n, 'languages' |
67
68
  * Additional options for page tree builder
68
69
  */
69
70
  pageTree?: Partial<Omit<BuildPageTreeOptions, 'storage' | 'getUrl'>>;
71
+ /**
72
+ * Configure i18n
73
+ */
74
+ i18n?: I18nConfig;
75
+ /**
76
+ * Accepted languages.
77
+ *
78
+ * A page tree will be built for each language.
79
+ *
80
+ * @deprecated Use `i18n` instead
81
+ */
82
+ languages?: string[];
83
+ /**
84
+ * Default locale when locale is not provided.
85
+ *
86
+ * @deprecated Use `i18n` instead
87
+ */
88
+ defaultLanguage?: string;
70
89
  }
71
90
  interface Source<Config extends SourceConfig> {
72
91
  /**
@@ -109,7 +128,7 @@ declare function getSlugs(info: FileInfo): string[];
109
128
  type InferSourceConfig<T> = T extends Source<infer Config> ? Config : never;
110
129
  declare function loader<Options extends LoaderOptions>(options: Options): LoaderOutput<{
111
130
  source: InferSourceConfig<Options['source']>;
112
- i18n: Options['languages'] extends string[] ? true : false;
131
+ i18n: Options['i18n'] extends I18nConfig ? true : Options['languages'] extends string[] ? true : false;
113
132
  }>;
114
133
 
115
134
  interface MetaData {
@@ -201,14 +220,7 @@ interface BuildPageTreeOptions {
201
220
  resolveIcon?: (icon: string | undefined) => ReactElement | undefined;
202
221
  }
203
222
  interface BuildPageTreeOptionsWithI18n extends BuildPageTreeOptions {
204
- /**
205
- * Build a page tree for each language
206
- */
207
- languages?: string[];
208
- /**
209
- * Hide the locale prefix from URLs if it is same as the specified default locale.
210
- */
211
- defaultLanguage?: string;
223
+ i18n: I18nConfig;
212
224
  }
213
225
  interface PageTreeBuilder {
214
226
  build: (options: BuildPageTreeOptions) => Root;
@@ -6,30 +6,33 @@ import {
6
6
  } from "../chunk-MLKGABMK.js";
7
7
 
8
8
  // src/source/path.ts
9
- import { parse } from "path";
10
9
  function parseFilePath(path) {
11
- const normalized = normalizePath(path);
12
- const parsed = parse(normalized);
13
- const flattenedPath = [parsed.dir, parsed.name].filter((p) => p.length > 0).join("/");
14
- const [name, locale] = parsed.name.split(".");
10
+ const segments = splitPath(slash(path));
11
+ const dirname = segments.slice(0, -1).join("/");
12
+ const base = segments.at(-1) ?? "";
13
+ const dotIdx = base.lastIndexOf(".");
14
+ const nameWithLocale = dotIdx !== -1 ? base.slice(0, dotIdx) : base;
15
+ const flattenedPath = [dirname, nameWithLocale].filter((p) => p.length > 0).join("/");
16
+ const [name, locale] = nameWithLocale.split(".");
15
17
  return {
16
- dirname: parsed.dir,
18
+ dirname,
17
19
  name,
18
20
  flattenedPath,
19
21
  locale,
20
- path: normalized
22
+ path: segments.join("/")
21
23
  };
22
24
  }
23
25
  function parseFolderPath(path) {
24
- const normalized = normalizePath(path);
25
- const parsed = parse(normalized);
26
- const [name, locale] = parsed.base.split(".");
26
+ const segments = splitPath(slash(path));
27
+ const base = segments.at(-1) ?? "";
28
+ const [name, locale] = base.split(".");
29
+ const flattenedPath = segments.join("/");
27
30
  return {
28
- dirname: parsed.dir,
31
+ dirname: segments.slice(0, -1).join("/"),
29
32
  name,
30
- flattenedPath: normalized,
33
+ flattenedPath,
31
34
  locale,
32
- path: normalized
35
+ path: flattenedPath
33
36
  };
34
37
  }
35
38
  function normalizePath(path) {
@@ -63,6 +66,8 @@ var group = /^\((?<name>.+)\)$/;
63
66
  var link = /^\[(?<text>.+)]\((?<url>.+)\)$/;
64
67
  var separator = /^---(?<name>.*?)---$/;
65
68
  var rest = "...";
69
+ var extractPrefix = "...";
70
+ var excludePrefix = "!";
66
71
  function buildAll(nodes, ctx, skipIndex) {
67
72
  const output = [];
68
73
  const folders = [];
@@ -106,12 +111,12 @@ function resolveFolderItem(folder, item, ctx, addedNodePaths) {
106
111
  };
107
112
  return [ctx.options.attachFile?.(node) ?? node];
108
113
  }
109
- const isExcept = item.startsWith("!"), isExtract = item.startsWith("...");
114
+ const isExcept = item.startsWith(excludePrefix), isExtract = item.startsWith(extractPrefix);
110
115
  let filename = item;
111
116
  if (isExcept) {
112
- filename = item.slice(1);
117
+ filename = item.slice(excludePrefix.length);
113
118
  } else if (isExtract) {
114
- filename = item.slice(3);
119
+ filename = item.slice(extractPrefix.length);
115
120
  }
116
121
  const path = resolvePath(folder.file.path, filename);
117
122
  const itemNode = ctx.storage.readDir(path) ?? ctx.storage.read(path, "page");
@@ -173,12 +178,19 @@ function buildFolderNode(folder, isGlobalRoot, ctx) {
173
178
  );
174
179
  }
175
180
  function buildFileNode(file, ctx) {
181
+ let urlLocale;
176
182
  const localized = findLocalizedFile(file.file.flattenedPath, "page", ctx) ?? file;
183
+ const hideLocale = ctx.i18n?.hideLocale ?? "never";
184
+ if (hideLocale === "never") {
185
+ urlLocale = ctx.lang;
186
+ } else if (hideLocale === "default-locale" && ctx.lang !== ctx.i18n?.defaultLanguage) {
187
+ urlLocale = ctx.lang;
188
+ }
177
189
  const item = {
178
190
  type: "page",
179
191
  name: localized.data.data.title,
180
192
  icon: ctx.options.resolveIcon?.(localized.data.data.icon),
181
- url: ctx.options.getUrl(localized.data.slugs, ctx.lang)
193
+ url: ctx.options.getUrl(localized.data.slugs, urlLocale)
182
194
  };
183
195
  return removeUndefined(ctx.options.attachFile?.(item, file) ?? item);
184
196
  }
@@ -199,13 +211,14 @@ function createPageTreeBuilder() {
199
211
  storage: options.storage
200
212
  });
201
213
  },
202
- buildI18n({ languages = [], defaultLanguage, ...options }) {
203
- const entries = languages.map((lang) => {
214
+ buildI18n({ i18n, ...options }) {
215
+ const entries = i18n.languages.map((lang) => {
204
216
  const tree = build({
205
- lang: lang === defaultLanguage ? void 0 : lang,
217
+ lang,
206
218
  options,
207
219
  builder: this,
208
- storage: options.storage
220
+ storage: options.storage,
221
+ i18n
209
222
  });
210
223
  return [lang, tree];
211
224
  });
@@ -324,7 +337,7 @@ function loadFiles(files, options) {
324
337
  }
325
338
 
326
339
  // src/source/loader.ts
327
- function buildPageMap(storage, languages, getUrl) {
340
+ function buildPageMap(storage, getUrl, languages = []) {
328
341
  const map = /* @__PURE__ */ new Map();
329
342
  const defaultMap = /* @__PURE__ */ new Map();
330
343
  map.set("", defaultMap);
@@ -361,19 +374,30 @@ function getSlugs(info) {
361
374
  );
362
375
  }
363
376
  function loader(options) {
377
+ if (options.languages) {
378
+ console.warn(
379
+ "Fumadocs: It's highly recommended to use `i18n` config instead of passing `languages` to loader."
380
+ );
381
+ return createOutput({
382
+ ...options,
383
+ i18n: {
384
+ languages: options.languages,
385
+ defaultLanguage: options.defaultLanguage
386
+ }
387
+ });
388
+ }
364
389
  return createOutput(options);
365
390
  }
366
391
  function createOutput({
367
392
  source,
368
393
  icon: resolveIcon,
369
- languages,
370
394
  rootDir = "",
371
395
  transformers,
372
396
  baseUrl = "/",
373
397
  slugs: slugsFn = getSlugs,
374
398
  url: getUrl = createGetUrl(baseUrl),
375
- defaultLanguage,
376
- pageTree: pageTreeOptions = {}
399
+ pageTree: pageTreeOptions = {},
400
+ i18n
377
401
  }) {
378
402
  const storage = loadFiles(
379
403
  typeof source.files === "function" ? source.files(rootDir) : source.files,
@@ -383,25 +407,24 @@ function createOutput({
383
407
  getSlugs: slugsFn
384
408
  }
385
409
  );
386
- const i18nMap = buildPageMap(storage, languages ?? [], getUrl);
410
+ const i18nMap = buildPageMap(storage, getUrl, i18n?.languages);
387
411
  const builder = createPageTreeBuilder();
388
- const pageTree = languages === void 0 ? builder.build({
412
+ const pageTree = i18n === void 0 ? builder.build({
389
413
  storage,
390
414
  resolveIcon,
391
415
  getUrl,
392
416
  ...pageTreeOptions
393
417
  }) : builder.buildI18n({
394
- languages,
395
418
  storage,
396
419
  resolveIcon,
397
420
  getUrl,
398
- defaultLanguage,
421
+ i18n,
399
422
  ...pageTreeOptions
400
423
  });
401
424
  return {
402
425
  pageTree,
403
426
  files: storage.list(),
404
- getPages(language = "") {
427
+ getPages(language = i18n?.defaultLanguage ?? "") {
405
428
  return Array.from(i18nMap.get(language)?.values() ?? []);
406
429
  },
407
430
  getLanguages() {
@@ -415,7 +438,7 @@ function createOutput({
415
438
  }
416
439
  return list;
417
440
  },
418
- getPage(slugs = [], language = "") {
441
+ getPage(slugs = [], language = i18n?.defaultLanguage ?? "") {
419
442
  return i18nMap.get(language)?.get(slugs.join("/"));
420
443
  }
421
444
  };
package/dist/toc.d.ts CHANGED
@@ -3,11 +3,21 @@ import { ReactNode, RefObject, AnchorHTMLAttributes } from 'react';
3
3
  import { T as TableOfContents } from './get-toc-CM4X3hbw.js';
4
4
 
5
5
  /**
6
- * The id of active anchor (doesn't include hash)
6
+ * The estimated active heading ID
7
7
  */
8
8
  declare function useActiveAnchor(): string | undefined;
9
+ /**
10
+ * The id of visible anchors
11
+ */
12
+ declare function useActiveAnchors(): string[];
9
13
  interface AnchorProviderProps {
10
14
  toc: TableOfContents;
15
+ /**
16
+ * Only accept one active item at most
17
+ *
18
+ * @defaultValue true
19
+ */
20
+ single?: boolean;
11
21
  children?: ReactNode;
12
22
  }
13
23
  interface ScrollProviderProps {
@@ -18,11 +28,11 @@ interface ScrollProviderProps {
18
28
  children?: ReactNode;
19
29
  }
20
30
  declare function ScrollProvider({ containerRef, children, }: ScrollProviderProps): React.ReactElement;
21
- declare function AnchorProvider({ toc, children, }: AnchorProviderProps): React.ReactElement;
31
+ declare function AnchorProvider({ toc, single, children, }: AnchorProviderProps): React.ReactElement;
22
32
  interface TOCItemProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
23
33
  href: string;
24
34
  onActiveChange?: (v: boolean) => void;
25
35
  }
26
36
  declare const TOCItem: react.ForwardRefExoticComponent<TOCItemProps & react.RefAttributes<HTMLAnchorElement>>;
27
37
 
28
- export { AnchorProvider, type AnchorProviderProps, ScrollProvider, type ScrollProviderProps, TOCItem, type TOCItemProps, useActiveAnchor };
38
+ export { AnchorProvider, type AnchorProviderProps, ScrollProvider, type ScrollProviderProps, TOCItem, type TOCItemProps, useActiveAnchor, useActiveAnchors };
package/dist/toc.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import {
2
3
  useOnChange
3
4
  } from "./chunk-KGMG4N3Y.js";
@@ -22,52 +23,59 @@ function mergeRefs(...refs) {
22
23
 
23
24
  // src/utils/use-anchor-observer.ts
24
25
  import { useEffect, useState } from "react";
25
- function useAnchorObserver(watch) {
26
- const [activeAnchor, setActiveAnchor] = useState();
26
+ function useAnchorObserver(watch, single) {
27
+ const [activeAnchor, setActiveAnchor] = useState([]);
27
28
  useEffect(() => {
29
+ let visible = [];
28
30
  const observer = new IntersectionObserver(
29
31
  (entries) => {
30
- setActiveAnchor((f) => {
31
- for (const entry of entries) {
32
- if (entry.isIntersecting) {
33
- return entry.target.id;
34
- }
32
+ for (const entry of entries) {
33
+ if (entry.isIntersecting && !visible.includes(entry.target.id)) {
34
+ visible = [...visible, entry.target.id];
35
+ } else if (!entry.isIntersecting && visible.includes(entry.target.id)) {
36
+ visible = visible.filter((v) => v !== entry.target.id);
35
37
  }
36
- return f ?? watch[0];
37
- });
38
+ }
39
+ if (visible.length > 0) setActiveAnchor(visible);
38
40
  },
39
- { rootMargin: `-80px 0% -78% 0%`, threshold: 1 }
41
+ {
42
+ rootMargin: single ? "-80px 0% -70% 0%" : `-20px 0% -40% 0%`,
43
+ threshold: 1
44
+ }
40
45
  );
41
- const scroll = () => {
46
+ function onScroll() {
42
47
  const element = document.scrollingElement;
43
48
  if (!element) return;
44
- if (element.scrollTop === 0) {
45
- setActiveAnchor(watch.at(0));
46
- } else if (element.scrollTop >= // assume you have a 10px margin
47
- element.scrollHeight - element.clientHeight - 10) {
48
- setActiveAnchor(watch.at(-1));
49
+ if (element.scrollTop === 0 && single) setActiveAnchor(watch.slice(0, 1));
50
+ else if (element.scrollTop + element.clientHeight >= element.scrollHeight - 6) {
51
+ setActiveAnchor((active) => {
52
+ const last = active.at(-1);
53
+ return last ? watch.slice(watch.indexOf(last)) : active;
54
+ });
49
55
  }
50
- };
51
- window.addEventListener("scroll", scroll);
56
+ }
52
57
  for (const heading of watch) {
53
58
  const element = document.getElementById(heading);
54
- if (element !== null) {
55
- observer.observe(element);
56
- }
59
+ if (element) observer.observe(element);
57
60
  }
61
+ onScroll();
62
+ window.addEventListener("scroll", onScroll);
58
63
  return () => {
59
- window.removeEventListener("scroll", scroll);
64
+ window.removeEventListener("scroll", onScroll);
60
65
  observer.disconnect();
61
66
  };
62
- }, [watch]);
63
- return activeAnchor;
67
+ }, [single, watch]);
68
+ return single ? activeAnchor.slice(0, 1) : activeAnchor;
64
69
  }
65
70
 
66
71
  // src/toc.tsx
67
72
  import { jsx } from "react/jsx-runtime";
68
- var ActiveAnchorContext = createContext(void 0);
73
+ var ActiveAnchorContext = createContext([]);
69
74
  var ScrollContext = createContext({ current: null });
70
75
  function useActiveAnchor() {
76
+ return useContext(ActiveAnchorContext).at(-1);
77
+ }
78
+ function useActiveAnchors() {
71
79
  return useContext(ActiveAnchorContext);
72
80
  }
73
81
  function ScrollProvider({
@@ -78,21 +86,21 @@ function ScrollProvider({
78
86
  }
79
87
  function AnchorProvider({
80
88
  toc,
89
+ single = true,
81
90
  children
82
91
  }) {
83
92
  const headings = useMemo(() => {
84
93
  return toc.map((item) => item.url.split("#")[1]);
85
94
  }, [toc]);
86
- const activeAnchor = useAnchorObserver(headings);
87
- return /* @__PURE__ */ jsx(ActiveAnchorContext.Provider, { value: activeAnchor, children });
95
+ return /* @__PURE__ */ jsx(ActiveAnchorContext.Provider, { value: useAnchorObserver(headings, single), children });
88
96
  }
89
97
  var TOCItem = forwardRef(
90
98
  ({ onActiveChange, ...props }, ref) => {
91
99
  const containerRef = useContext(ScrollContext);
92
- const activeAnchor = useActiveAnchor();
100
+ const anchors = useActiveAnchors();
93
101
  const anchorRef = useRef(null);
94
102
  const mergedRef = mergeRefs(anchorRef, ref);
95
- const isActive = activeAnchor === props.href.split("#")[1];
103
+ const isActive = anchors.includes(props.href.slice(1));
96
104
  useOnChange(isActive, (v) => {
97
105
  const element = anchorRef.current;
98
106
  if (!element) return;
@@ -115,5 +123,6 @@ export {
115
123
  AnchorProvider,
116
124
  ScrollProvider,
117
125
  TOCItem,
118
- useActiveAnchor
126
+ useActiveAnchor,
127
+ useActiveAnchors
119
128
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "13.2.1",
3
+ "version": "13.3.0",
4
4
  "description": "The library for building a documentation website in Next.js",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -67,6 +67,10 @@
67
67
  "./search-algolia/server": {
68
68
  "import": "./dist/search-algolia/server.js",
69
69
  "types": "./dist/search-algolia/server.d.ts"
70
+ },
71
+ "./i18n": {
72
+ "import": "./dist/i18n/index.js",
73
+ "types": "./dist/i18n/index.d.ts"
70
74
  }
71
75
  },
72
76
  "typesVersions": {
@@ -112,6 +116,9 @@
112
116
  ],
113
117
  "search-algolia/server": [
114
118
  "./dist/search-algolia/server.d.ts"
119
+ ],
120
+ "i18n": [
121
+ "./dist/i18n/index.d.ts"
115
122
  ]
116
123
  }
117
124
  },
@@ -120,8 +127,8 @@
120
127
  ],
121
128
  "dependencies": {
122
129
  "@formatjs/intl-localematcher": "^0.5.4",
123
- "@shikijs/rehype": "^1.12.1",
124
- "@shikijs/transformers": "^1.12.1",
130
+ "@shikijs/rehype": "^1.13.0",
131
+ "@shikijs/transformers": "^1.13.0",
125
132
  "flexsearch": "0.7.21",
126
133
  "github-slugger": "^2.0.0",
127
134
  "image-size": "^1.1.1",
@@ -132,7 +139,7 @@
132
139
  "remark-gfm": "^4.0.0",
133
140
  "remark-mdx": "^3.0.1",
134
141
  "scroll-into-view-if-needed": "^3.1.0",
135
- "shiki": "^1.12.1",
142
+ "shiki": "^1.13.0",
136
143
  "swr": "^2.2.5",
137
144
  "unist-util-visit": "^5.0.0"
138
145
  },
@@ -144,7 +151,7 @@
144
151
  "@types/hast": "^3.0.4",
145
152
  "@types/mdast": "^4.0.3",
146
153
  "@types/negotiator": "^0.6.3",
147
- "@types/node": "20.14.12",
154
+ "@types/node": "22.3.0",
148
155
  "@types/react": "^18.3.3",
149
156
  "@types/react-dom": "^18.3.0",
150
157
  "algoliasearch": "^4.24.0",