fumadocs-core 16.5.4 → 16.6.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.
@@ -15,9 +15,8 @@ async function searchSimple(db, query, params = {}) {
15
15
  }
16
16
  })).hits.map((hit) => ({
17
17
  type: "page",
18
- content: hit.document.title,
18
+ content: highlighter.highlightMarkdown(hit.document.title),
19
19
  breadcrumbs: hit.document.breadcrumbs,
20
- contentWithHighlights: highlighter.highlight(hit.document.title),
21
20
  id: hit.document.url,
22
21
  url: hit.document.url
23
22
  }));
@@ -27,24 +26,24 @@ async function searchSimple(db, query, params = {}) {
27
26
  //#region src/search/orama/search/advanced.ts
28
27
  async function searchAdvanced(db, query, tag = [], { mode = "fulltext", ...override } = {}) {
29
28
  if (typeof tag === "string") tag = [tag];
30
- let params = {
29
+ const params = {
31
30
  ...override,
32
31
  mode,
33
32
  where: removeUndefined({
34
33
  tags: tag.length > 0 ? { containsAll: tag } : void 0,
35
34
  ...override.where
36
35
  }),
36
+ limit: 10,
37
37
  groupBy: {
38
38
  properties: ["page_id"],
39
39
  maxResult: 8,
40
40
  ...override.groupBy
41
41
  }
42
42
  };
43
- if (query.length > 0) params = {
44
- ...params,
43
+ if (query.length > 0) Object.assign(params, {
45
44
  term: query,
46
45
  properties: mode === "fulltext" ? ["content"] : ["content", "embeddings"]
47
- };
46
+ });
48
47
  const highlighter = createContentHighlighter(query);
49
48
  const result = await search(db, params);
50
49
  const list = [];
@@ -55,24 +54,22 @@ async function searchAdvanced(db, query, tag = [], { mode = "fulltext", ...overr
55
54
  list.push({
56
55
  id: pageId,
57
56
  type: "page",
58
- content: page.content,
57
+ content: highlighter.highlightMarkdown(page.content),
59
58
  breadcrumbs: page.breadcrumbs,
60
- contentWithHighlights: highlighter.highlight(page.content),
61
59
  url: page.url
62
60
  });
63
61
  for (const hit of item.result) {
64
62
  if (hit.document.type === "page") continue;
65
63
  list.push({
66
64
  id: hit.document.id.toString(),
67
- content: hit.document.content,
65
+ content: highlighter.highlightMarkdown(hit.document.content),
68
66
  breadcrumbs: hit.document.breadcrumbs,
69
- contentWithHighlights: highlighter.highlight(hit.document.content),
70
67
  type: hit.document.type,
71
68
  url: hit.document.url
72
69
  });
73
70
  }
74
71
  }
75
- return list;
72
+ return list.length > 80 ? list.slice(0, 80) : list;
76
73
  }
77
74
 
78
75
  //#endregion
@@ -38,7 +38,7 @@ async function searchDocs(query, { indexName, onSearch, client, locale, tag }) {
38
38
  return groupResults(result.results[0].hits).flatMap((hit) => {
39
39
  if (hit.type === "page") return {
40
40
  ...hit,
41
- contentWithHighlights: hit.contentWithHighlights ?? highlighter.highlight(hit.content)
41
+ content: highlighter.highlightMarkdown(hit.content)
42
42
  };
43
43
  return [];
44
44
  });
@@ -0,0 +1,30 @@
1
+ import * as react from "react";
2
+ import { FC } from "react";
3
+ import { Options } from "remark-rehype";
4
+ import { Compatible } from "vfile";
5
+ import { Components } from "hast-util-to-jsx-runtime";
6
+ import { PluggableList } from "unified";
7
+
8
+ //#region src/content/md.d.ts
9
+ interface MarkdownRendererOptions {
10
+ remarkPlugins?: PluggableList;
11
+ rehypePlugins?: PluggableList;
12
+ remarkRehypeOptions?: Options;
13
+ }
14
+ interface MarkdownRenderer {
15
+ Markdown: FC<MarkdownProps>;
16
+ MarkdownServer: FC<Omit<MarkdownProps, 'async'>>;
17
+ }
18
+ interface MarkdownProps {
19
+ async?: boolean;
20
+ components?: Components;
21
+ children: Compatible;
22
+ }
23
+ declare function createMarkdownRenderer({
24
+ rehypePlugins,
25
+ remarkPlugins,
26
+ remarkRehypeOptions
27
+ }?: MarkdownRendererOptions): MarkdownRenderer;
28
+ declare function Markdown(props: MarkdownProps & MarkdownRendererOptions): react.ReactNode | Promise<react.ReactNode>;
29
+ //#endregion
30
+ export { Markdown, MarkdownProps, MarkdownRenderer, MarkdownRendererOptions, createMarkdownRenderer };
@@ -0,0 +1,47 @@
1
+ import { use } from "react";
2
+ import * as JsxRuntime from "react/jsx-runtime";
3
+ import { remark } from "remark";
4
+ import remarkRehype from "remark-rehype";
5
+ import { VFile } from "vfile";
6
+ import { toJsxRuntime } from "hast-util-to-jsx-runtime";
7
+
8
+ //#region src/content/md.ts
9
+ function createMarkdownRenderer({ rehypePlugins = [], remarkPlugins = [], remarkRehypeOptions } = {}) {
10
+ const processor = remark().use(remarkPlugins).use(remarkRehype, remarkRehypeOptions).use(rehypePlugins);
11
+ const cache = {};
12
+ const promises = {};
13
+ function render(tree, file, props) {
14
+ return toJsxRuntime(tree, {
15
+ development: false,
16
+ filePath: file.path,
17
+ components: props.components,
18
+ ...JsxRuntime
19
+ });
20
+ }
21
+ function parse(file, _props) {
22
+ return processor.parse(file);
23
+ }
24
+ return {
25
+ Markdown(props) {
26
+ const { async = false, children } = props;
27
+ const file = new VFile(children);
28
+ const key = String(file.value);
29
+ if (async) {
30
+ promises[key] ??= processor.run(parse(file, props), file);
31
+ return render(use(promises[key]), file, props);
32
+ }
33
+ cache[key] ??= processor.runSync(parse(file, props), file);
34
+ return render(cache[key], file, props);
35
+ },
36
+ async MarkdownServer(props) {
37
+ const file = new VFile(props.children);
38
+ return render(await processor.run(parse(file, props), file), file, props);
39
+ }
40
+ };
41
+ }
42
+ function Markdown(props) {
43
+ return createMarkdownRenderer(props).MarkdownServer(props);
44
+ }
45
+
46
+ //#endregion
47
+ export { Markdown, createMarkdownRenderer };
@@ -3,7 +3,7 @@ import { r as transformerIcon, t as CodeBlockIcon } from "../transformer-icon-C8
3
3
  import { transformerTab } from "./rehype-code.core.js";
4
4
  import { RehypeCodeOptions, rehypeCode, rehypeCodeDefaultOptions } from "./rehype-code.js";
5
5
  import { RemarkImageOptions, remarkImage } from "./remark-image.js";
6
- import { StructureOptions, StructuredData, defaultStringify, remarkStructure, remarkStructureDefaultOptions, structure } from "./remark-structure.js";
6
+ import { StringifyOptions, StructureOptions, StructuredData, defaultStringifier, remarkStructure, remarkStructureDefaultOptions, structure } from "./remark-structure.js";
7
7
  import { RemarkHeadingOptions, remarkHeading } from "./remark-heading.js";
8
8
  import { RemarkAdmonitionOptions, remarkAdmonition } from "./remark-admonition.js";
9
9
  import { RemarkDirectiveAdmonitionOptions, remarkDirectiveAdmonition } from "./remark-directive-admonition.js";
@@ -15,4 +15,4 @@ import { CodeBlockAttributes, CodeBlockTabsOptions, generateCodeBlockTabs, parse
15
15
  import { RemarkMdxFilesOptions, remarkMdxFiles } from "./remark-mdx-files.js";
16
16
  import { RemarkMdxMermaidOptions, remarkMdxMermaid } from "./remark-mdx-mermaid.js";
17
17
  import { FeedbackBlockProps, RemarkFeedbackBlockOptions, remarkFeedbackBlock } from "./remark-feedback-block.js";
18
- export { CodeBlockAttributes, CodeBlockIcon, CodeBlockTabsOptions, FeedbackBlockProps, RehypeCodeOptions, RehypeTocOptions, RemarkAdmonitionOptions, RemarkCodeTabOptions, RemarkDirectiveAdmonitionOptions, RemarkFeedbackBlockOptions, RemarkGfmOptions, RemarkHeadingOptions, RemarkImageOptions, type RemarkMdxFilesOptions, RemarkMdxMermaidOptions, RemarkNpmOptions, RemarkStepsOptions, StructureOptions, StructuredData, defaultStringify, generateCodeBlockTabs, parseCodeBlockAttributes, rehypeCode, rehypeCodeDefaultOptions, rehypeToc, remarkAdmonition, remarkCodeTab, remarkDirectiveAdmonition, remarkFeedbackBlock, remarkGfm, remarkHeading, remarkImage, remarkMdxFiles, remarkMdxMermaid, remarkNpm, remarkSteps, remarkStructure, remarkStructureDefaultOptions, structure, transformerIcon, transformerTab };
18
+ export { CodeBlockAttributes, CodeBlockIcon, CodeBlockTabsOptions, FeedbackBlockProps, RehypeCodeOptions, RehypeTocOptions, RemarkAdmonitionOptions, RemarkCodeTabOptions, RemarkDirectiveAdmonitionOptions, RemarkFeedbackBlockOptions, RemarkGfmOptions, RemarkHeadingOptions, RemarkImageOptions, type RemarkMdxFilesOptions, RemarkMdxMermaidOptions, RemarkNpmOptions, RemarkStepsOptions, StringifyOptions, StructureOptions, StructuredData, defaultStringifier, generateCodeBlockTabs, parseCodeBlockAttributes, rehypeCode, rehypeCodeDefaultOptions, rehypeToc, remarkAdmonition, remarkCodeTab, remarkDirectiveAdmonition, remarkFeedbackBlock, remarkGfm, remarkHeading, remarkImage, remarkMdxFiles, remarkMdxMermaid, remarkNpm, remarkSteps, remarkStructure, remarkStructureDefaultOptions, structure, transformerIcon, transformerTab };
@@ -6,7 +6,7 @@ import { t as transformerIcon } from "../transformer-icon-BYedaeE8.js";
6
6
  import { transformerTab } from "./rehype-code.core.js";
7
7
  import { rehypeCode, rehypeCodeDefaultOptions } from "./rehype-code.js";
8
8
  import { remarkImage } from "./remark-image.js";
9
- import { defaultStringify, remarkStructure, remarkStructureDefaultOptions, structure } from "./remark-structure.js";
9
+ import { defaultStringifier, remarkStructure, remarkStructureDefaultOptions, structure } from "./remark-structure.js";
10
10
  import { remarkAdmonition } from "./remark-admonition.js";
11
11
  import { remarkDirectiveAdmonition } from "./remark-directive-admonition.js";
12
12
  import { rehypeToc } from "./rehype-toc.js";
@@ -17,4 +17,4 @@ import { remarkMdxFiles } from "./remark-mdx-files.js";
17
17
  import { remarkMdxMermaid } from "./remark-mdx-mermaid.js";
18
18
  import { remarkFeedbackBlock } from "./remark-feedback-block.js";
19
19
 
20
- export { defaultStringify, generateCodeBlockTabs, parseCodeBlockAttributes, rehypeCode, rehypeCodeDefaultOptions, rehypeToc, remarkAdmonition, remarkCodeTab, remarkDirectiveAdmonition, remarkFeedbackBlock, remarkGfm, remarkHeading, remarkImage, remarkMdxFiles, remarkMdxMermaid, remarkNpm, remarkSteps, remarkStructure, remarkStructureDefaultOptions, structure, transformerIcon, transformerTab };
20
+ export { defaultStringifier, generateCodeBlockTabs, parseCodeBlockAttributes, rehypeCode, rehypeCodeDefaultOptions, rehypeToc, remarkAdmonition, remarkCodeTab, remarkDirectiveAdmonition, remarkFeedbackBlock, remarkGfm, remarkHeading, remarkImage, remarkMdxFiles, remarkMdxMermaid, remarkNpm, remarkSteps, remarkStructure, remarkStructureDefaultOptions, structure, transformerIcon, transformerTab };
@@ -16,7 +16,7 @@ interface RemarkFeedbackBlockOptions {
16
16
  /**
17
17
  * determine how the node should be resolved into a feedback block.
18
18
  *
19
- * scan paragraph, list, and image nodes by default.
19
+ * default: skip MDX elements, convert paragraph, list item, and image nodes.
20
20
  *
21
21
  * @returns
22
22
  * - `true`: convert the node into a feedback block.
@@ -8,16 +8,24 @@ import { createHash } from "node:crypto";
8
8
  *
9
9
  * Note: the uniqueness is only guaranteed per MDX file/page.
10
10
  */
11
- function remarkFeedbackBlock({ generateHash = ({ body }) => createHash("md5").update(body).digest("hex").substring(0, 16), tagName = "FeedbackBlock", resolve = (node) => node.type === "paragraph" || node.type === "image" || node.type === "list", generateBody = true } = {}) {
11
+ function remarkFeedbackBlock({ generateHash = ({ body }) => createHash("md5").update(body).digest("hex").substring(0, 16), tagName = "FeedbackBlock", resolve = (node) => {
12
+ switch (node.type) {
13
+ case "mdxJsxFlowElement": return "skip";
14
+ case "paragraph":
15
+ case "image":
16
+ case "listItem": return true;
17
+ default: return false;
18
+ }
19
+ }, generateBody = true } = {}) {
12
20
  return (tree) => {
13
21
  const counts = /* @__PURE__ */ new Map();
14
22
  visit(tree, (node, index, parent) => {
15
- if (node.type === "root") return;
23
+ if (node.type === "root" || !parent || typeof index !== "number") return;
16
24
  const resolved = resolve(node);
17
25
  if (resolved === false) return;
18
26
  if (resolved === "skip") return "skip";
19
27
  const text = flattenNode(node).trim();
20
- if (text.length === 0 || !parent || typeof index !== "number") return;
28
+ if (text.length === 0) return;
21
29
  let id = generateHash({ body: text });
22
30
  const count = counts.get(id) ?? 0;
23
31
  if (count > 0) id = `${id}-${count}`;
@@ -4,49 +4,80 @@ import { Nodes, Root } from "mdast";
4
4
  import { MdxJsxAttribute, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement } from "mdast-util-mdx";
5
5
 
6
6
  //#region src/mdx-plugins/remark-structure.d.ts
7
- interface Heading$1 {
7
+ interface StructuredDataHeading {
8
8
  id: string;
9
9
  content: string;
10
10
  }
11
- interface Content {
11
+ interface StructuredDataContent {
12
12
  heading: string | undefined;
13
13
  content: string;
14
14
  }
15
15
  interface StructuredData {
16
- headings: Heading$1[];
16
+ headings: StructuredDataHeading[];
17
17
  /**
18
18
  * Refer to paragraphs, a heading may contain multiple contents as well
19
19
  */
20
- contents: Content[];
20
+ contents: StructuredDataContent[];
21
+ }
22
+ type Stringifier = (this: Processor, node: Nodes, ctx: StringifierContext) => string;
23
+ interface StringifierContext {
24
+ addContent: (...content: StructuredDataContent[]) => void;
21
25
  }
22
26
  interface StringifyOptions {
23
27
  /**
24
- * Determine whether the element itself should be stringified in content block, only stringify its attributes & children if `false`.
28
+ * Filter the elements to be included in the output:
29
+ *
30
+ * - `true`: include element & its children.
31
+ * - `children-only`: exclude element but keep its children.
32
+ * - `false`: exclude element & its children.
33
+ *
34
+ * Default:
35
+ *
36
+ * ```ts
37
+ * filterElement = (node) => {
38
+ * switch (node.type) {
39
+ * case 'mdxJsxFlowElement':
40
+ * case 'mdxJsxTextElement':
41
+ * switch (node.name) {
42
+ * case 'File':
43
+ * case 'TypeTable':
44
+ * case 'Callout':
45
+ * case 'Card':
46
+ * return true;
47
+ * }
48
+ * return 'children-only';
49
+ * }
25
50
  *
26
- * Always return `false` by default.
51
+ * return true;
52
+ * },
53
+ * ```
54
+ */
55
+ filterElement?: (node: Nodes) => boolean | 'children-only';
56
+ /**
57
+ * Filter the attributes to stringify.
27
58
  */
28
- filterMdxElements?: (node: MdxJsxFlowElement | MdxJsxTextElement) => boolean;
29
59
  filterMdxAttributes?: (node: MdxJsxFlowElement | MdxJsxTextElement, attribute: MdxJsxAttribute | MdxJsxExpressionAttribute) => boolean;
30
60
  }
31
- interface StructureOptions extends Omit<StringifyOptions, 'filterMdxAttributes'> {
61
+ interface StructureOptions {
32
62
  /**
33
63
  * MDAST node types to be scanned as a content block.
34
64
  *
35
- * If a node's type represents in this array, it will be converted into a single content block.
65
+ * If a node's type is listed in this array, it will be converted into a single content block.
36
66
  *
37
67
  * @defaultValue ['heading', 'paragraph', 'blockquote', 'tableCell', 'mdxJsxFlowElement']
38
68
  */
39
69
  types?: string[] | ((node: Nodes) => boolean);
40
70
  /**
41
- * stringify a given node & its children, you can use something like `mdast-util-to-markdown`.
71
+ * stringify text content from a MDAST node.
42
72
  */
43
- stringify?: (this: Processor, node: Nodes) => string;
73
+ stringify?: Stringifier | StringifyOptions;
44
74
  /**
45
- * By default, it will not index MDX attributes. You can define a list of MDX attributes to index, either:
75
+ * Whether the MDX element should be treated as a single content block, only effective if `types` has `mdxJsxFlowElement`.
46
76
  *
47
- * - an array of attribute names.
48
- * - a function that determines if attribute should be indexed.
77
+ * Default: return `true` if the element is a leaf node, otherwise `false`.
49
78
  */
79
+ mdxTypes?: (node: MdxJsxFlowElement | MdxJsxTextElement) => boolean;
80
+ /** @deprecated use `stringify.filterMdxAttributes` instead */
50
81
  allowedMdxAttributes?: string[] | ((node: MdxJsxFlowElement | MdxJsxTextElement, attribute: MdxJsxAttribute | MdxJsxExpressionAttribute) => boolean);
51
82
  /**
52
83
  * export as `structuredData` (if true) or specified variable name.
@@ -56,21 +87,28 @@ interface StructureOptions extends Omit<StringifyOptions, 'filterMdxAttributes'>
56
87
  declare module 'mdast' {
57
88
  interface Data {
58
89
  /**
59
- * [Fumadocs] Stringified form of node, `remarkStructure` uses it to generate search index.
90
+ * [Fumadocs: remark-structure] The stringified form of node for generating search index.
91
+ */
92
+ _string?: string | (() => string);
93
+ /**
94
+ * [Fumadocs: remark-structure] Items to add to the structured data.
60
95
  */
61
- _string?: string[];
96
+ structuredData?: {
97
+ contents: StructuredDataContent[];
98
+ };
62
99
  }
63
100
  }
64
101
  declare module 'vfile' {
65
102
  interface DataMap {
66
103
  /**
67
- * [Fumadocs] injected by `remarkStructure`
104
+ * [Fumadocs: remark-structure] output data.
68
105
  */
69
106
  structuredData: StructuredData;
70
107
  }
71
108
  }
72
109
  declare const remarkStructureDefaultOptions: {
73
110
  types: string[];
111
+ mdxTypes(node: MdxJsxFlowElement | MdxJsxTextElement): boolean;
74
112
  exportAs: false;
75
113
  };
76
114
  /**
@@ -80,15 +118,15 @@ declare const remarkStructureDefaultOptions: {
80
118
  */
81
119
  declare function remarkStructure(this: Processor, {
82
120
  types,
83
- stringify,
121
+ mdxTypes,
122
+ stringify: stringifyOptions,
84
123
  allowedMdxAttributes,
85
- exportAs,
86
- ...stringifyOptions
124
+ exportAs
87
125
  }?: StructureOptions): Transformer<Root, Root>;
88
126
  /**
89
127
  * Extract data from markdown/mdx content
90
128
  */
91
129
  declare function structure(content: string, remarkPlugins?: PluggableList, options?: StructureOptions): StructuredData;
92
- declare function defaultStringify(config?: Options & StringifyOptions): (this: Processor, node: Nodes) => string;
130
+ declare function defaultStringifier(config?: Options & StringifyOptions): Stringifier;
93
131
  //#endregion
94
- export { StructureOptions, StructuredData, defaultStringify, remarkStructure, remarkStructureDefaultOptions, structure };
132
+ export { StringifyOptions, StructureOptions, StructuredData, defaultStringifier, remarkStructure, remarkStructureDefaultOptions, structure };
@@ -1,8 +1,8 @@
1
- import { n as toMdxExport, t as flattenNode } from "../mdast-utils-gJMY143g.js";
1
+ import { n as toMdxExport } from "../mdast-utils-gJMY143g.js";
2
2
  import { remarkHeading } from "./remark-heading.js";
3
3
  import { remark } from "remark";
4
- import remarkGfm from "remark-gfm";
5
4
  import { visit } from "unist-util-visit";
5
+ import remarkGfm from "remark-gfm";
6
6
  import { toMarkdown } from "mdast-util-to-markdown";
7
7
 
8
8
  //#region src/mdx-plugins/remark-structure.ts
@@ -14,6 +14,9 @@ const remarkStructureDefaultOptions = {
14
14
  "tableCell",
15
15
  "mdxJsxFlowElement"
16
16
  ],
17
+ mdxTypes(node) {
18
+ return node.children.length === 0;
19
+ },
17
20
  exportAs: false
18
21
  };
19
22
  /**
@@ -21,17 +24,13 @@ const remarkStructureDefaultOptions = {
21
24
  *
22
25
  * By default, the output is stored into VFile (`vfile.data.structuredData`), you can specify `exportAs` to export it.
23
26
  */
24
- function remarkStructure({ types = remarkStructureDefaultOptions.types, stringify, allowedMdxAttributes, exportAs = remarkStructureDefaultOptions.exportAs, ...stringifyOptions } = {}) {
25
- if (Array.isArray(allowedMdxAttributes)) {
26
- const arr = allowedMdxAttributes;
27
- allowedMdxAttributes = (_node, attribute) => attribute.type === "mdxJsxAttribute" && arr.includes(attribute.name);
28
- }
27
+ function remarkStructure({ types = remarkStructureDefaultOptions.types, mdxTypes = remarkStructureDefaultOptions.mdxTypes, stringify: stringifyOptions, allowedMdxAttributes, exportAs = remarkStructureDefaultOptions.exportAs } = {}) {
29
28
  if (Array.isArray(types)) {
30
29
  const arr = types;
31
30
  types = (node) => arr.includes(node.type);
32
31
  }
33
- stringify ??= defaultStringify({
34
- filterMdxAttributes: allowedMdxAttributes,
32
+ const stringify = typeof stringifyOptions === "function" ? stringifyOptions : defaultStringifier({
33
+ filterMdxAttributes: Array.isArray(allowedMdxAttributes) ? (_node, attribute) => attribute.type === "mdxJsxAttribute" && allowedMdxAttributes.includes(attribute.name) : allowedMdxAttributes,
35
34
  ...stringifyOptions
36
35
  });
37
36
  return (tree, file) => {
@@ -47,24 +46,38 @@ function remarkStructure({ types = remarkStructureDefaultOptions.types, stringif
47
46
  data.contents.push(...frontmatter._openapi.structuredData.contents);
48
47
  }
49
48
  }
49
+ const stringifierCtx = { addContent(...content) {
50
+ for (const item of content) data.contents.push({
51
+ ...item,
52
+ heading: item.heading ?? lastHeading
53
+ });
54
+ } };
50
55
  visit(tree, (element) => {
51
- if (element.type === "root" || !types(element)) return;
52
- if (element.type === "heading") {
53
- element.data ||= {};
54
- element.data.hProperties ||= {};
55
- const id = element.data.hProperties.id;
56
- if (typeof id !== "string") {
57
- console.warn("[remark-structure] hProperties.id is missing in heading node, it is required to generate heading data. You can add remark-heading prior to remark-structure to generate heading IDs.");
56
+ if (!types(element)) return;
57
+ switch (element.type) {
58
+ case "root": return;
59
+ case "mdxJsxFlowElement":
60
+ case "mdxJsxTextElement":
61
+ if (!mdxTypes(element)) return;
62
+ break;
63
+ case "heading": {
64
+ element.data ||= {};
65
+ element.data.hProperties ||= {};
66
+ const id = element.data.hProperties.id;
67
+ if (typeof id !== "string") {
68
+ console.warn("[remark-structure] hProperties.id is missing in heading node, it is required to generate heading data. You can add remark-heading prior to remark-structure to generate heading IDs.");
69
+ return "skip";
70
+ }
71
+ const content = stringify.call(this, element, stringifierCtx).trim();
72
+ if (content.length > 0) data.headings.push({
73
+ id,
74
+ content
75
+ });
76
+ lastHeading = id;
58
77
  return "skip";
59
78
  }
60
- data.headings.push({
61
- id,
62
- content: flattenNode(element).trim()
63
- });
64
- lastHeading = id;
65
- return "skip";
66
79
  }
67
- const content = stringify.call(this, element).trim();
80
+ const content = stringify.call(this, element, stringifierCtx).trim();
68
81
  if (content.length > 0) data.contents.push({
69
82
  heading: lastHeading,
70
83
  content
@@ -81,57 +94,81 @@ function remarkStructure({ types = remarkStructureDefaultOptions.types, stringif
81
94
  function structure(content, remarkPlugins = [], options = {}) {
82
95
  return remark().use(remarkGfm).use(remarkPlugins).use(remarkHeading).use(remarkStructure, options).processSync(content).data.structuredData;
83
96
  }
84
- function defaultStringify(config = {}) {
85
- const { filterMdxAttributes = (node) => {
86
- switch (node.name) {
87
- case "TypeTable":
88
- case "Callout": return true;
89
- default: return false;
97
+ function defaultStringifier(config = {}) {
98
+ const { filterMdxAttributes, filterElement = (node) => {
99
+ switch (node.type) {
100
+ case "mdxJsxFlowElement":
101
+ case "mdxJsxTextElement":
102
+ switch (node.name) {
103
+ case "File":
104
+ case "TypeTable":
105
+ case "Callout":
106
+ case "Card": return true;
107
+ }
108
+ return "children-only";
90
109
  }
91
- }, filterMdxElements = () => false } = config;
92
- function modHandler(handler) {
93
- return function(node, ...rest) {
94
- if (node.data?._string) return node.data._string.join("\n");
110
+ return true;
111
+ } } = config;
112
+ function modHandler(handler, ctx) {
113
+ return function(node, parent, state, info) {
114
+ const { structuredData, _string } = node.data ?? {};
115
+ if (structuredData) ctx.addContent(...structuredData.contents);
116
+ if (_string) return typeof _string === "function" ? _string() : _string;
117
+ const visibility = filterElement(node);
118
+ if (visibility === false) return "";
95
119
  switch (node.type) {
96
120
  case "mdxJsxFlowElement":
97
121
  case "mdxJsxTextElement": {
98
- const filteredAttributes = node.attributes.filter((attr) => filterMdxAttributes(node, attr));
99
- if (!filterMdxElements(node)) {
100
- let attrStr = "";
101
- for (const attr of filteredAttributes) {
102
- const str = typeof attr.value === "string" ? attr.value : attr.value?.value;
103
- if (!str) continue;
104
- attrStr += attr.type === "mdxJsxAttribute" ? `(${attr.name}=${str}) ` : `(${str}) `;
105
- }
106
- if (node.children.length === 0) return attrStr.trimEnd();
107
- return attrStr + rest[1].handle({
108
- type: "root",
109
- children: node.children
110
- }, ...rest);
122
+ if (visibility === "children-only") return node.type === "mdxJsxTextElement" ? state.containerPhrasing(node, info) : state.containerFlow(node, info);
123
+ const stringifiedAttributes = [];
124
+ for (const attr of node.attributes) {
125
+ if (attr.type === "mdxJsxExpressionAttribute") continue;
126
+ if (filterMdxAttributes && !filterMdxAttributes(node, attr)) continue;
127
+ const str = typeof attr.value === "string" ? attr.value : attr.value?.value;
128
+ if (!str) continue;
129
+ stringifiedAttributes.push({
130
+ ...attr,
131
+ value: str
132
+ });
111
133
  }
112
134
  const temp = node.attributes;
113
- node.attributes = filteredAttributes;
114
- const s = handler(node, ...rest);
135
+ node.attributes = stringifiedAttributes;
136
+ const s = handler(node, parent, state, info);
115
137
  node.attributes = temp;
116
138
  return s;
117
139
  }
118
- default: return handler(node, ...rest);
140
+ default:
141
+ if (visibility === "children-only") return "children" in node ? state.containerFlow({
142
+ type: "root",
143
+ children: node.children
144
+ }, info) : "";
145
+ return handler(node, parent, state, info);
119
146
  }
120
147
  };
121
148
  }
122
149
  const handlers = {
123
- ...config.handlers,
150
+ link(node, _, state, info) {
151
+ return state.containerPhrasing(node, info);
152
+ },
153
+ heading(node, _, state, info) {
154
+ return state.containerPhrasing(node, info);
155
+ },
156
+ image() {
157
+ return "";
158
+ },
124
159
  _custom(node, _, state, info) {
125
160
  const handlers = state.handlers;
126
- for (const k in handlers) handlers[k] = modHandler(handlers[k]);
161
+ for (const k in handlers) handlers[k] = modHandler(handlers[k], node.ctx);
127
162
  return state.handle(node.root, void 0, state, info);
128
- }
163
+ },
164
+ ...config.handlers
129
165
  };
130
- return function(root) {
166
+ return function(root, ctx) {
131
167
  const defaultExtensions = this.data("toMarkdownExtensions") ?? [];
132
168
  return toMarkdown({
133
169
  type: "_custom",
134
- root
170
+ root,
171
+ ctx
135
172
  }, {
136
173
  ...this.data("settings"),
137
174
  ...config,
@@ -142,4 +179,4 @@ function defaultStringify(config = {}) {
142
179
  }
143
180
 
144
181
  //#endregion
145
- export { defaultStringify, remarkStructure, remarkStructureDefaultOptions, structure };
182
+ export { defaultStringifier, remarkStructure, remarkStructureDefaultOptions, structure };
@@ -23,23 +23,22 @@ async function searchDocs(query, options) {
23
23
  list.push({
24
24
  id: hit.id,
25
25
  type: "page",
26
- content: doc.title,
27
- contentWithHighlights: highlighter.highlight(doc.title),
26
+ content: highlighter.highlightMarkdown(doc.title),
28
27
  url: doc.path
29
28
  }, {
30
29
  id: "page" + hit.id,
31
30
  type: "text",
32
- content: doc.content,
33
- contentWithHighlights: highlighter.highlight(doc.content),
31
+ content: highlighter.highlightMarkdown(doc.content),
34
32
  url: doc.path
35
33
  });
36
34
  }
37
35
  return list;
38
36
  }
39
- const params = {
37
+ const result = await client.search({
40
38
  datasources: [],
41
39
  ...extraParams,
42
40
  term: query,
41
+ limit: 10,
43
42
  where: removeUndefined({
44
43
  tag,
45
44
  ...extraParams?.where
@@ -49,8 +48,7 @@ async function searchDocs(query, options) {
49
48
  max_results: 7,
50
49
  ...extraParams?.groupBy
51
50
  }
52
- };
53
- const result = await client.search(params);
51
+ });
54
52
  if (!result || !result.groups) return list;
55
53
  for (const item of result.groups) {
56
54
  let addedHead = false;
@@ -60,23 +58,21 @@ async function searchDocs(query, options) {
60
58
  list.push({
61
59
  id: doc.page_id,
62
60
  type: "page",
63
- content: doc.title,
61
+ content: highlighter.highlightMarkdown(doc.title),
64
62
  breadcrumbs: doc.breadcrumbs,
65
- contentWithHighlights: highlighter.highlight(doc.title),
66
63
  url: doc.url
67
64
  });
68
65
  addedHead = true;
69
66
  }
70
67
  list.push({
71
68
  id: doc.id,
72
- content: doc.content,
73
- contentWithHighlights: highlighter.highlight(doc.content),
69
+ content: highlighter.highlightMarkdown(doc.content),
74
70
  type: doc.content === doc.section ? "heading" : "text",
75
71
  url: doc.section_id ? `${doc.url}#${doc.section_id}` : doc.url
76
72
  });
77
73
  }
78
74
  }
79
- return list;
75
+ return list.length > 80 ? list.slice(0, 80) : list;
80
76
  }
81
77
 
82
78
  //#endregion
@@ -22,14 +22,12 @@ async function searchDocs(query, options) {
22
22
  list.push({
23
23
  id: hit.id,
24
24
  type: "page",
25
- content: doc.title,
26
- contentWithHighlights: highlighter.highlight(doc.title),
25
+ content: highlighter.highlightMarkdown(doc.title),
27
26
  url: doc.path
28
27
  }, {
29
28
  id: "page" + hit.id,
30
29
  type: "text",
31
- content: doc.content,
32
- contentWithHighlights: highlighter.highlight(doc.content),
30
+ content: highlighter.highlightMarkdown(doc.content),
33
31
  url: doc.path
34
32
  });
35
33
  }
@@ -58,17 +56,15 @@ async function searchDocs(query, options) {
58
56
  list.push({
59
57
  id: doc.page_id,
60
58
  type: "page",
61
- content: doc.title,
59
+ content: highlighter.highlightMarkdown(doc.title),
62
60
  breadcrumbs: doc.breadcrumbs,
63
- contentWithHighlights: highlighter.highlight(doc.title),
64
61
  url: doc.url
65
62
  });
66
63
  addedHead = true;
67
64
  }
68
65
  list.push({
69
66
  id: doc.id,
70
- content: doc.content,
71
- contentWithHighlights: highlighter.highlight(doc.content),
67
+ content: highlighter.highlightMarkdown(doc.content),
72
68
  type: doc.content === doc.section ? "heading" : "text",
73
69
  url: doc.section_id ? `${doc.url}#${doc.section_id}` : doc.url
74
70
  });
@@ -58,15 +58,15 @@ function useDocsSearch(clientOptions, deps) {
58
58
  return fetchDocs(debouncedValue, client);
59
59
  }
60
60
  case "algolia": {
61
- const { searchDocs } = await import("../algolia-Cx9CqUgE.js");
61
+ const { searchDocs } = await import("../algolia-CfKKhsrI.js");
62
62
  return searchDocs(debouncedValue, client);
63
63
  }
64
64
  case "orama-cloud": {
65
- const { searchDocs } = await import("../orama-cloud-CmYuZkvC.js");
65
+ const { searchDocs } = await import("../orama-cloud-cgTJNLo0.js");
66
66
  return searchDocs(debouncedValue, client);
67
67
  }
68
68
  case "orama-cloud-legacy": {
69
- const { searchDocs } = await import("../orama-cloud-legacy-D-cNaYCM.js");
69
+ const { searchDocs } = await import("../orama-cloud-legacy-Caf8mcU9.js");
70
70
  return searchDocs(debouncedValue, client);
71
71
  }
72
72
  case "mixedbread": {
@@ -74,7 +74,7 @@ function useDocsSearch(clientOptions, deps) {
74
74
  return search(debouncedValue, client);
75
75
  }
76
76
  case "static": {
77
- const { search } = await import("../static-BQOJvapl.js");
77
+ const { search } = await import("../static-BUXJwBmr.js");
78
78
  return search(debouncedValue, client);
79
79
  }
80
80
  default: throw new Error("unknown search client");
@@ -10,6 +10,9 @@ interface SortedResult<Content = string> {
10
10
  * breadcrumbs to be displayed on UI
11
11
  */
12
12
  breadcrumbs?: Content[];
13
+ /**
14
+ * @deprecated it is now included in `content` as Markdown using `<mark />`.
15
+ */
13
16
  contentWithHighlights?: HighlightedText<Content>[];
14
17
  }
15
18
  type ReactSortedResult = SortedResult<ReactNode>;
@@ -22,6 +25,10 @@ interface HighlightedText<Content = string> {
22
25
  }
23
26
  declare function createContentHighlighter(query: string | RegExp): {
24
27
  highlight(content: string): HighlightedText[];
28
+ /**
29
+ * @param content - Markdown, it assumes the content is already sanitized & safe, no escape is performed.
30
+ */
31
+ highlightMarkdown(content: string): string;
25
32
  };
26
33
  //#endregion
27
34
  export { HighlightedText, ReactSortedResult, SortedResult, createContentHighlighter };
@@ -1,3 +1,6 @@
1
+ import { remark } from "remark";
2
+ import { visit } from "unist-util-visit";
3
+
1
4
  //#region src/search/index.ts
2
5
  function escapeRegExp(input) {
3
6
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -11,32 +14,57 @@ function buildRegexFromQuery(q) {
11
14
  return new RegExp(`(${escaped})`, "gi");
12
15
  }
13
16
  function createContentHighlighter(query) {
17
+ let processor = remark();
14
18
  const regex = typeof query === "string" ? buildRegexFromQuery(query) : query;
15
- return { highlight(content) {
16
- if (!regex) return [{
17
- type: "text",
18
- content
19
- }];
20
- const out = [];
21
- let i = 0;
22
- for (const match of content.matchAll(regex)) {
23
- if (i < match.index) out.push({
19
+ if (regex) processor = processor.use(remarkHighlight, regex);
20
+ return {
21
+ highlight(content) {
22
+ if (!regex) return [{
24
23
  type: "text",
25
- content: content.substring(i, match.index)
26
- });
27
- out.push({
24
+ content
25
+ }];
26
+ const out = [];
27
+ let i = 0;
28
+ for (const match of content.matchAll(regex)) {
29
+ if (i < match.index) out.push({
30
+ type: "text",
31
+ content: content.substring(i, match.index)
32
+ });
33
+ out.push({
34
+ type: "text",
35
+ content: match[0],
36
+ styles: { highlight: true }
37
+ });
38
+ i = match.index + match[0].length;
39
+ }
40
+ if (i < content.length) out.push({
28
41
  type: "text",
29
- content: match[0],
30
- styles: { highlight: true }
42
+ content: content.substring(i)
31
43
  });
32
- i = match.index + match[0].length;
44
+ return out;
45
+ },
46
+ highlightMarkdown(content) {
47
+ if (!regex) return content;
48
+ return String(processor.processSync(content).value);
33
49
  }
34
- if (i < content.length) out.push({
35
- type: "text",
36
- content: content.substring(i)
50
+ };
51
+ }
52
+ function remarkHighlight(regex) {
53
+ return (tree) => {
54
+ visit(tree, "text", (node) => {
55
+ let out = "";
56
+ const content = node.value;
57
+ let i = 0;
58
+ for (const match of content.matchAll(regex)) {
59
+ if (i < match.index) out += content.substring(i, match.index);
60
+ out += `<mark>${match[0]}</mark>`;
61
+ i = match.index + match[0].length;
62
+ }
63
+ if (i < content.length) out += content.substring(i);
64
+ node.type = "html";
65
+ node.value = out;
37
66
  });
38
- return out;
39
- } };
67
+ };
40
68
  }
41
69
 
42
70
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { r as findPath } from "../utils-Bc53B3CJ.js";
2
2
  import { createContentHighlighter } from "./index.js";
3
3
  import { t as createEndpoint } from "../create-endpoint-9PZc4Cmz.js";
4
- import { n as searchSimple, t as searchAdvanced } from "../advanced-3GdE_Q4d.js";
4
+ import { n as searchSimple, t as searchAdvanced } from "../advanced-ZOIuXvBJ.js";
5
5
  import { r as extname, t as basename } from "../path-CfJghBXy.js";
6
6
  import { create, insertMultiple, save } from "@orama/orama";
7
7
 
@@ -187,6 +187,7 @@ const STEMMERS = {
187
187
  tamil: "ta",
188
188
  turkish: "tr",
189
189
  ukrainian: "uk",
190
+ vietnamese: "vi",
190
191
  sanskrit: "sk"
191
192
  };
192
193
 
@@ -1,4 +1,4 @@
1
- import { n as searchSimple, t as searchAdvanced } from "./advanced-3GdE_Q4d.js";
1
+ import { n as searchSimple, t as searchAdvanced } from "./advanced-ZOIuXvBJ.js";
2
2
  import { create, load } from "@orama/orama";
3
3
 
4
4
  //#region src/search/client/static.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "16.5.4",
3
+ "version": "16.6.0",
4
4
  "description": "The React.js library for building a documentation website",
5
5
  "keywords": [
6
6
  "Docs",
@@ -24,8 +24,8 @@
24
24
  "import": "./dist/toc.js"
25
25
  },
26
26
  "./content": {
27
- "types": "./dist/content/index.d.ts",
28
- "import": "./dist/content/index.js"
27
+ "types": "./dist/content/md.d.ts",
28
+ "import": "./dist/content/md.js"
29
29
  },
30
30
  "./content/*": {
31
31
  "types": "./dist/content/*.d.ts",
@@ -242,7 +242,7 @@
242
242
  "scripts": {
243
243
  "build": "tsdown",
244
244
  "clean": "rimraf dist",
245
- "dev": "tsdown --watch",
245
+ "dev": "tsdown --watch --clean false",
246
246
  "lint": "eslint .",
247
247
  "types:check": "tsc --noEmit"
248
248
  }
@@ -1,22 +0,0 @@
1
- import * as react from "react";
2
- import { ReactNode } from "react";
3
- import { Components } from "hast-util-to-jsx-runtime";
4
- import { Compatible } from "vfile";
5
- import { PluggableList } from "unified";
6
-
7
- //#region src/content/index.d.ts
8
- interface MarkdownProps {
9
- components?: Components;
10
- }
11
- declare function Markdown({
12
- children: content,
13
- remarkPlugins,
14
- rehypePlugins,
15
- ...options
16
- }: MarkdownProps & {
17
- remarkPlugins?: PluggableList;
18
- rehypePlugins?: PluggableList;
19
- children: Compatible;
20
- }): Promise<react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | (string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined)>;
21
- //#endregion
22
- export { Markdown, MarkdownProps };
@@ -1,23 +0,0 @@
1
- import * as JsxRuntime from "react/jsx-runtime";
2
- import { remark } from "remark";
3
- import remarkGfm from "remark-gfm";
4
- import remarkRehype from "remark-rehype";
5
- import { toJsxRuntime } from "hast-util-to-jsx-runtime";
6
-
7
- //#region src/content/index.ts
8
- function rehypeReact(options = {}) {
9
- this.compiler = (tree, file) => {
10
- return toJsxRuntime(tree, {
11
- development: false,
12
- filePath: file.path,
13
- ...JsxRuntime,
14
- ...options
15
- });
16
- };
17
- }
18
- async function Markdown({ children: content, remarkPlugins = [], rehypePlugins = [], ...options }) {
19
- return (await remark().use(remarkGfm).use(remarkPlugins).use(remarkRehype).use(rehypePlugins).use(rehypeReact, options).process(content)).result;
20
- }
21
-
22
- //#endregion
23
- export { Markdown };