fumadocs-core 16.0.5 → 16.0.7

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.
@@ -2,13 +2,48 @@
2
2
  import Slugger from "github-slugger";
3
3
  import { visit } from "unist-util-visit";
4
4
 
5
- // src/mdx-plugins/remark-utils.ts
5
+ // src/mdx-plugins/mdast-utils.ts
6
+ import { valueToEstree } from "estree-util-value-to-estree";
6
7
  function flattenNode(node) {
7
8
  if ("children" in node)
8
9
  return node.children.map((child) => flattenNode(child)).join("");
9
10
  if ("value" in node) return node.value;
10
11
  return "";
11
12
  }
13
+ function toMdxExport(name, value) {
14
+ return {
15
+ type: "mdxjsEsm",
16
+ value: "",
17
+ data: {
18
+ estree: {
19
+ type: "Program",
20
+ sourceType: "module",
21
+ body: [
22
+ {
23
+ type: "ExportNamedDeclaration",
24
+ attributes: [],
25
+ specifiers: [],
26
+ source: null,
27
+ declaration: {
28
+ type: "VariableDeclaration",
29
+ kind: "let",
30
+ declarations: [
31
+ {
32
+ type: "VariableDeclarator",
33
+ id: {
34
+ type: "Identifier",
35
+ name
36
+ },
37
+ init: valueToEstree(value)
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ }
45
+ };
46
+ }
12
47
 
13
48
  // src/mdx-plugins/remark-heading.ts
14
49
  var slugger = new Slugger();
@@ -53,5 +88,6 @@ function remarkHeading({
53
88
 
54
89
  export {
55
90
  flattenNode,
91
+ toMdxExport,
56
92
  remarkHeading
57
93
  };
@@ -12,6 +12,7 @@ var Link2 = forwardRef(
12
12
  external = href.match(/^\w+:/) || // protocol relative URL
13
13
  href.startsWith("//"),
14
14
  prefetch,
15
+ children,
15
16
  ...props
16
17
  }, ref) => {
17
18
  if (external) {
@@ -23,11 +24,11 @@ var Link2 = forwardRef(
23
24
  rel: "noreferrer noopener",
24
25
  target: "_blank",
25
26
  ...props,
26
- children: props.children
27
+ children
27
28
  }
28
29
  );
29
30
  }
30
- return /* @__PURE__ */ jsx(Link, { ref, href, prefetch, ...props });
31
+ return /* @__PURE__ */ jsx(Link, { ref, href, prefetch, ...props, children });
31
32
  }
32
33
  );
33
34
  Link2.displayName = "Link";
@@ -1,8 +1,8 @@
1
1
  import { PluggableList } from 'unified';
2
2
  import { Compatible } from 'vfile';
3
3
  import { TOCItemType } from '../toc.js';
4
- import 'react';
5
4
  import 'react/jsx-runtime';
5
+ import 'react';
6
6
 
7
7
  /**
8
8
  * Get Table of Contents from markdown/mdx document (using remark)
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  remarkHeading
3
- } from "../chunk-QMATWJ5F.js";
3
+ } from "../chunk-JD3M54YF.js";
4
4
  import "../chunk-U67V476Y.js";
5
5
 
6
6
  // src/content/toc.ts
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  Link
4
- } from "./chunk-H2GMUTQG.js";
4
+ } from "./chunk-N2ZQXKIX.js";
5
5
  import {
6
6
  useParams
7
7
  } from "./chunk-BBP7MIO4.js";
package/dist/link.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  Link
4
- } from "./chunk-H2GMUTQG.js";
4
+ } from "./chunk-N2ZQXKIX.js";
5
5
  import "./chunk-BBP7MIO4.js";
6
6
  import "./chunk-U67V476Y.js";
7
7
  export {
@@ -4,7 +4,7 @@ import { RehypeShikiOptions } from '@shikijs/rehype';
4
4
  import { Processor, Transformer } from 'unified';
5
5
  import { ShikiTransformer } from 'shiki';
6
6
  import { Root as Root$1, BlockContent, Text } from 'mdast';
7
- export { a as StructureOptions, S as StructuredData, r as remarkStructure, s as structure } from '../remark-structure-DkCXCzpD.js';
7
+ export { a as StructureOptions, S as StructuredData, r as remarkStructure, s as structure } from '../remark-structure-BJxaB5zZ.js';
8
8
  export { a as RemarkCodeTabOptions, R as RemarkHeadingOptions, b as remarkCodeTab, r as remarkHeading } from '../remark-code-tab-DmyIyi6m.js';
9
9
  import { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
10
10
 
@@ -51,6 +51,12 @@ type RehypeCodeOptions = RehypeShikiOptions & {
51
51
  declare function rehypeCode(this: Processor, _options?: Partial<RehypeCodeOptions>): Transformer<Root, Root>;
52
52
  declare function transformerTab(): ShikiTransformer;
53
53
 
54
+ type ExternalImageOptions = {
55
+ /**
56
+ * timeout for fetching remote images (in milliseconds)
57
+ */
58
+ timeout?: number;
59
+ } | boolean;
54
60
  interface RemarkImageOptions {
55
61
  /**
56
62
  * Directory or base URL to resolve absolute image paths
@@ -91,7 +97,7 @@ interface RemarkImageOptions {
91
97
  *
92
98
  * @defaultValue true
93
99
  */
94
- external?: boolean;
100
+ external?: ExternalImageOptions;
95
101
  }
96
102
  /**
97
103
  * Turn images into Next.js Image compatible usage.
@@ -4,8 +4,9 @@ import {
4
4
  } from "../chunk-XN2LKXFZ.js";
5
5
  import {
6
6
  flattenNode,
7
- remarkHeading
8
- } from "../chunk-QMATWJ5F.js";
7
+ remarkHeading,
8
+ toMdxExport
9
+ } from "../chunk-JD3M54YF.js";
9
10
  import "../chunk-U67V476Y.js";
10
11
 
11
12
  // src/mdx-plugins/index.ts
@@ -426,8 +427,7 @@ function remarkImage({
426
427
  }
427
428
  return out;
428
429
  }
429
- if (src.type === "url" && !external) return;
430
- const size = await getImageSize(src).catch((e) => {
430
+ const size = await getImageSize(src, external).catch((e) => {
431
431
  throw new Error(
432
432
  `[Remark Image] Failed obtain image size for ${node.url} (public directory configured as ${publicDir})`,
433
433
  {
@@ -435,6 +435,7 @@ function remarkImage({
435
435
  }
436
436
  );
437
437
  });
438
+ if (!size) return;
438
439
  return {
439
440
  type: "mdxJsxFlowElement",
440
441
  name: "img",
@@ -550,9 +551,13 @@ function parseSrc(src, publicDir, dir) {
550
551
  file: path.join(dir, src)
551
552
  };
552
553
  }
553
- async function getImageSize(src) {
554
+ async function getImageSize(src, onExternal) {
554
555
  if (src.type === "file") return imageSizeFromFile(src.file);
555
- const res = await fetch(src.url);
556
+ if (onExternal === false) return;
557
+ const { timeout } = typeof onExternal === "object" ? onExternal : {};
558
+ const res = await fetch(src.url, {
559
+ signal: typeof timeout === "number" ? AbortSignal.timeout(timeout) : void 0
560
+ });
556
561
  if (!res.ok) {
557
562
  throw new Error(
558
563
  `[Remark Image] Failed to fetch ${src.url} (${res.status}): ${await res.text()}`
@@ -577,7 +582,8 @@ function remarkStructure({
577
582
  allowedMdxAttributes = (node) => {
578
583
  if (!node.name) return false;
579
584
  return ["TypeTable", "Callout"].includes(node.name);
580
- }
585
+ },
586
+ exportAs = false
581
587
  } = {}) {
582
588
  const slugger = new Slugger();
583
589
  if (Array.isArray(allowedMdxAttributes)) {
@@ -588,7 +594,7 @@ function remarkStructure({
588
594
  const arr = types;
589
595
  types = (node) => arr.includes(node.type);
590
596
  }
591
- return (node, file) => {
597
+ return (tree, file) => {
592
598
  slugger.reset();
593
599
  const data = { contents: [], headings: [] };
594
600
  let lastHeading;
@@ -599,9 +605,8 @@ function remarkStructure({
599
605
  data.contents.push(...frontmatter._openapi.structuredData.contents);
600
606
  }
601
607
  }
602
- visit2(node, (element) => {
603
- if (element.type === "root") return;
604
- if (!types(element)) return;
608
+ visit2(tree, (element) => {
609
+ if (element.type === "root" || !types(element)) return;
605
610
  if (element.type === "heading") {
606
611
  element.data ||= {};
607
612
  element.data.hProperties ||= {};
@@ -648,6 +653,14 @@ function remarkStructure({
648
653
  return "skip";
649
654
  });
650
655
  file.data.structuredData = data;
656
+ if (exportAs) {
657
+ tree.children.unshift(
658
+ toMdxExport(
659
+ typeof exportAs === "string" ? exportAs : "structuredData",
660
+ data
661
+ )
662
+ );
663
+ }
651
664
  };
652
665
  }
653
666
  function structure(content, remarkPlugins = [], options = {}) {
@@ -31,26 +31,31 @@ interface StructureOptions {
31
31
  * - a function that determines if attribute should be indexed.
32
32
  */
33
33
  allowedMdxAttributes?: string[] | ((node: MdxJsxFlowElement, attribute: MdxJsxAttribute | MdxJsxExpressionAttribute) => boolean);
34
+ /**
35
+ * export as `structuredData` or specified variable name.
36
+ */
37
+ exportAs?: string | boolean;
34
38
  }
35
39
  declare module 'mdast' {
36
40
  interface Data {
37
41
  /**
38
- * Get content of unserializable element
39
- *
40
- * Needed for `remarkStructure` to generate search index
42
+ * [Fumadocs] Get content of unserializable element, `remarkStructure` uses it to generate search index.
41
43
  */
42
44
  _string?: string[];
43
45
  }
44
46
  }
45
47
  declare module 'vfile' {
46
48
  interface DataMap {
49
+ /**
50
+ * [Fumadocs] injected by `remarkStructure`
51
+ */
47
52
  structuredData: StructuredData;
48
53
  }
49
54
  }
50
55
  /**
51
56
  * Attach structured data to VFile, you can access via `vfile.data.structuredData`.
52
57
  */
53
- declare function remarkStructure({ types, allowedMdxAttributes, }?: StructureOptions): Transformer<Root, Root>;
58
+ declare function remarkStructure({ types, allowedMdxAttributes, exportAs, }?: StructureOptions): Transformer<Root, Root>;
54
59
  /**
55
60
  * Extract data from markdown/mdx content
56
61
  */
@@ -1,5 +1,5 @@
1
1
  import { Algoliasearch } from 'algoliasearch';
2
- import { S as StructuredData } from '../remark-structure-DkCXCzpD.js';
2
+ import { S as StructuredData } from '../remark-structure-BJxaB5zZ.js';
3
3
  import 'mdast';
4
4
  import 'unified';
5
5
  import 'mdast-util-mdx-jsx';
@@ -1,5 +1,5 @@
1
1
  import { AnyOrama } from '@orama/orama';
2
- import '../remark-structure-DkCXCzpD.js';
2
+ import '../remark-structure-BJxaB5zZ.js';
3
3
  import { BaseIndex } from './algolia.js';
4
4
  import { LiteClient, SearchResponse } from 'algoliasearch/lite';
5
5
  import { OramaCloud, OramaCloudSearchParams } from '@orama/core';
@@ -1,4 +1,4 @@
1
- import { S as StructuredData } from '../remark-structure-DkCXCzpD.js';
1
+ import { S as StructuredData } from '../remark-structure-BJxaB5zZ.js';
2
2
  import '../remark-code-tab-DmyIyi6m.js';
3
3
  import { OramaCloud } from '@orama/core';
4
4
  import 'mdast';
@@ -1,5 +1,5 @@
1
1
  import { TypedDocument, Orama, Language, RawData, create, SearchParams } from '@orama/orama';
2
- import { S as StructuredData } from '../remark-structure-DkCXCzpD.js';
2
+ import { S as StructuredData } from '../remark-structure-BJxaB5zZ.js';
3
3
  import { SortedResult } from './index.js';
4
4
  export { HighlightedText, ReactSortedResult, createContentHighlighter } from './index.js';
5
5
  import { I18nConfig } from '../i18n/index.js';
package/dist/toc.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import * as react from 'react';
2
- import { ReactNode, RefObject, AnchorHTMLAttributes } from 'react';
3
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, RefObject, ComponentProps } from 'react';
4
3
 
5
4
  interface TOCItemType {
6
5
  title: ReactNode;
@@ -35,10 +34,10 @@ interface ScrollProviderProps {
35
34
  }
36
35
  declare function ScrollProvider({ containerRef, children, }: ScrollProviderProps): react_jsx_runtime.JSX.Element;
37
36
  declare function AnchorProvider({ toc, single, children, }: AnchorProviderProps): react_jsx_runtime.JSX.Element;
38
- interface TOCItemProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
37
+ interface TOCItemProps extends Omit<ComponentProps<'a'>, 'href'> {
39
38
  href: string;
40
39
  onActiveChange?: (v: boolean) => void;
41
40
  }
42
- declare const TOCItem: react.ForwardRefExoticComponent<TOCItemProps & react.RefAttributes<HTMLAnchorElement>>;
41
+ declare function TOCItem({ ref, onActiveChange, ...props }: TOCItemProps): react_jsx_runtime.JSX.Element;
43
42
 
44
43
  export { AnchorProvider, type AnchorProviderProps, ScrollProvider, type ScrollProviderProps, TOCItem, type TOCItemProps, type TOCItemType, type TableOfContents, useActiveAnchor, useActiveAnchors };
package/dist/toc.js CHANGED
@@ -1,11 +1,17 @@
1
1
  "use client";
2
- import {
3
- useOnChange
4
- } from "./chunk-EMWGTXSW.js";
5
2
  import "./chunk-U67V476Y.js";
6
3
 
7
4
  // src/toc.tsx
8
- import { createContext, forwardRef, useContext, useMemo, useRef } from "react";
5
+ import {
6
+ createContext,
7
+ useContext,
8
+ useEffect,
9
+ useEffectEvent,
10
+ useLayoutEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState
14
+ } from "react";
9
15
  import scrollIntoView from "scroll-into-view-if-needed";
10
16
 
11
17
  // src/utils/merge-refs.ts
@@ -14,60 +20,13 @@ function mergeRefs(...refs) {
14
20
  refs.forEach((ref) => {
15
21
  if (typeof ref === "function") {
16
22
  ref(value);
17
- } else if (ref !== null) {
23
+ } else if (ref != null) {
18
24
  ref.current = value;
19
25
  }
20
26
  });
21
27
  };
22
28
  }
23
29
 
24
- // src/utils/use-anchor-observer.ts
25
- import { useEffect, useState } from "react";
26
- function useAnchorObserver(watch, single) {
27
- const [activeAnchor, setActiveAnchor] = useState([]);
28
- useEffect(() => {
29
- let visible = [];
30
- const observer = new IntersectionObserver(
31
- (entries) => {
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);
37
- }
38
- }
39
- if (visible.length > 0) setActiveAnchor(visible);
40
- },
41
- {
42
- rootMargin: single ? "-80px 0% -70% 0%" : `-20px 0% -40% 0%`,
43
- threshold: 1
44
- }
45
- );
46
- function onScroll() {
47
- const element = document.scrollingElement;
48
- if (!element) return;
49
- const top = element.scrollTop;
50
- if (top <= 0 && single) setActiveAnchor(watch.slice(0, 1));
51
- else if (top + element.clientHeight >= element.scrollHeight - 6) {
52
- setActiveAnchor((active) => {
53
- return active.length > 0 && !single ? watch.slice(watch.indexOf(active[0])) : watch.slice(-1);
54
- });
55
- }
56
- }
57
- for (const heading of watch) {
58
- const element = document.getElementById(heading);
59
- if (element) observer.observe(element);
60
- }
61
- onScroll();
62
- window.addEventListener("scroll", onScroll);
63
- return () => {
64
- window.removeEventListener("scroll", onScroll);
65
- observer.disconnect();
66
- };
67
- }, [single, watch]);
68
- return single ? activeAnchor.slice(0, 1) : activeAnchor;
69
- }
70
-
71
30
  // src/toc.tsx
72
31
  import { jsx } from "react/jsx-runtime";
73
32
  var ActiveAnchorContext = createContext([]);
@@ -75,7 +34,7 @@ var ScrollContext = createContext({
75
34
  current: null
76
35
  });
77
36
  function useActiveAnchor() {
78
- return useContext(ActiveAnchorContext).at(-1);
37
+ return useContext(ActiveAnchorContext)[0];
79
38
  }
80
39
  function useActiveAnchors() {
81
40
  return useContext(ActiveAnchorContext);
@@ -96,31 +55,93 @@ function AnchorProvider({
96
55
  }, [toc]);
97
56
  return /* @__PURE__ */ jsx(ActiveAnchorContext.Provider, { value: useAnchorObserver(headings, single), children });
98
57
  }
99
- var TOCItem = forwardRef(
100
- ({ onActiveChange, ...props }, ref) => {
101
- const containerRef = useContext(ScrollContext);
102
- const anchors = useActiveAnchors();
103
- const anchorRef = useRef(null);
104
- const mergedRef = mergeRefs(anchorRef, ref);
105
- const isActive = anchors.includes(props.href.slice(1));
106
- useOnChange(isActive, (v) => {
107
- const element = anchorRef.current;
108
- if (!element) return;
109
- if (v && containerRef.current) {
110
- scrollIntoView(element, {
111
- behavior: "smooth",
112
- block: "center",
113
- inline: "center",
114
- scrollMode: "always",
115
- boundary: containerRef.current
116
- });
58
+ function TOCItem({
59
+ ref,
60
+ onActiveChange = () => null,
61
+ ...props
62
+ }) {
63
+ const containerRef = useContext(ScrollContext);
64
+ const anchorRef = useRef(null);
65
+ const activeOrder = useActiveAnchors().indexOf(props.href.slice(1));
66
+ const isActive = activeOrder !== -1;
67
+ const shouldScroll = activeOrder === 0;
68
+ const onActiveChangeEvent = useEffectEvent(onActiveChange);
69
+ useLayoutEffect(() => {
70
+ const anchor = anchorRef.current;
71
+ const container = containerRef.current;
72
+ if (container && anchor && shouldScroll)
73
+ scrollIntoView(anchor, {
74
+ behavior: "smooth",
75
+ block: "center",
76
+ inline: "center",
77
+ scrollMode: "always",
78
+ boundary: container
79
+ });
80
+ }, [containerRef, shouldScroll]);
81
+ useEffect(() => {
82
+ return () => onActiveChangeEvent(isActive);
83
+ }, [isActive]);
84
+ return /* @__PURE__ */ jsx("a", { ref: mergeRefs(anchorRef, ref), "data-active": isActive, ...props, children: props.children });
85
+ }
86
+ function useAnchorObserver(watch, single) {
87
+ const observerRef = useRef(null);
88
+ const [activeAnchor, setActiveAnchor] = useState(() => []);
89
+ const stateRef = useRef(null);
90
+ const onChange = useEffectEvent((entries) => {
91
+ stateRef.current ??= {
92
+ visible: /* @__PURE__ */ new Set()
93
+ };
94
+ const state = stateRef.current;
95
+ for (const entry of entries) {
96
+ if (entry.isIntersecting) {
97
+ state.visible.add(entry.target.id);
98
+ } else {
99
+ state.visible.delete(entry.target.id);
100
+ }
101
+ }
102
+ if (state.visible.size === 0) {
103
+ const viewTop = entries[0].rootBounds.top;
104
+ let fallback;
105
+ let min = -1;
106
+ for (const id of watch) {
107
+ const element = document.getElementById(id);
108
+ if (!element) continue;
109
+ const d = Math.abs(viewTop - element.getBoundingClientRect().top);
110
+ if (min === -1 || d < min) {
111
+ fallback = element;
112
+ min = d;
113
+ }
117
114
  }
118
- onActiveChange?.(v);
115
+ setActiveAnchor(fallback ? [fallback.id] : []);
116
+ } else {
117
+ const items = watch.filter((item) => state.visible.has(item));
118
+ setActiveAnchor(single ? items.slice(0, 1) : items);
119
+ }
120
+ });
121
+ useEffect(() => {
122
+ if (observerRef.current) return;
123
+ observerRef.current = new IntersectionObserver(onChange, {
124
+ rootMargin: "0px",
125
+ threshold: 0.98
119
126
  });
120
- return /* @__PURE__ */ jsx("a", { ref: mergedRef, "data-active": isActive, ...props, children: props.children });
121
- }
122
- );
123
- TOCItem.displayName = "TOCItem";
127
+ return () => {
128
+ observerRef.current?.disconnect();
129
+ observerRef.current = null;
130
+ };
131
+ }, []);
132
+ useEffect(() => {
133
+ const observer = observerRef.current;
134
+ if (!observer) return;
135
+ const elements = watch.flatMap(
136
+ (heading) => document.getElementById(heading) ?? []
137
+ );
138
+ for (const element of elements) observer.observe(element);
139
+ return () => {
140
+ for (const element of elements) observer.unobserve(element);
141
+ };
142
+ }, [watch]);
143
+ return activeAnchor;
144
+ }
124
145
  export {
125
146
  AnchorProvider,
126
147
  ScrollProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "16.0.5",
3
+ "version": "16.0.7",
4
4
  "description": "The library for building a documentation website in any React.js framework",
5
5
  "keywords": [
6
6
  "Fumadocs",
@@ -105,6 +105,7 @@
105
105
  "@orama/orama": "^3.1.16",
106
106
  "@shikijs/rehype": "^3.14.0",
107
107
  "@shikijs/transformers": "^3.14.0",
108
+ "estree-util-value-to-estree": "^3.5.0",
108
109
  "github-slugger": "^2.0.0",
109
110
  "hast-util-to-estree": "^3.1.3",
110
111
  "hast-util-to-jsx-runtime": "^2.3.6",
@@ -123,19 +124,19 @@
123
124
  "@mdx-js/mdx": "^3.1.1",
124
125
  "@mixedbread/sdk": "^0.35.1",
125
126
  "@orama/core": "^1.2.13",
126
- "@tanstack/react-router": "^1.133.32",
127
+ "@tanstack/react-router": "^1.133.36",
127
128
  "@types/estree-jsx": "^1.0.5",
128
129
  "@types/hast": "^3.0.4",
129
130
  "@types/mdast": "^4.0.4",
130
131
  "@types/negotiator": "^0.6.4",
131
- "@types/node": "24.9.1",
132
+ "@types/node": "24.9.2",
132
133
  "@types/react": "^19.2.2",
133
134
  "@types/react-dom": "^19.2.2",
134
135
  "algoliasearch": "5.41.0",
135
136
  "lucide-react": "^0.548.0",
136
137
  "mdast-util-mdx-jsx": "^3.2.0",
137
138
  "mdast-util-mdxjs-esm": "^2.0.1",
138
- "next": "16.0.0",
139
+ "next": "16.0.1",
139
140
  "react-router": "^7.9.4",
140
141
  "remark-mdx": "^3.1.1",
141
142
  "remove-markdown": "^0.6.2",