Relevant source files
The following files were used as context for generating this wiki page:
- packages/core/src/mdx-plugins/remark-structure.ts
- packages/local-md/src/client.ts
- packages/core/src/mdx-plugins/remark-llms.runtime.ts
- packages/stf/src/index.ts
- packages/stf/src/lib/stf.tsx
- packages/core/src/source/client/index.tsx
- packages/story/src/utils/serialization.ts
- packages/twoslash/src/index.ts
- packages/core/src/content/md.ts
- packages/python/src/convert.ts
- packages/mdx/src/runtime/server.ts
- packages/doc-gen/src/remark-docgen.ts
- packages/core/src/mdx-plugins/remark-llms.ts
- packages/asyncapi/src/ui/index.tsx
- packages/api-docs/src/components/schema/client.tsx
- packages/openapi/src/ui/base.tsx
- packages/base-ui/src/components/codeblock.rsc.tsx
- packages/typescript/src/markdown.ts
- packages/core/src/mdx-plugins/stringifier.ts
- packages/python/src/components/index.tsx
- packages/mdx/src/loaders/mdx/remark-include.ts
- packages/preview/src/components/markdown.tsx
- packages/asyncapi/src/utils/schema.ts
- packages/sanity/registry/steps.component.tsx
- packages/radix-ui/src/components/codeblock.rsc.tsx
- packages/python/fumapy/mksource/document_module.py
- packages/local-md/src/md/renderer.ts
- packages/api-docs/src/schema/to-string.ts
- packages/content-collections/src/configuration.ts
- packages/asyncapi/src/utils/pages/to-text.ts
Overview
The Structured Text Format (stf) package provides a specialized data engine and reactive subscription model designed for interactive schema binding and state management within Fumadocs. It anchors client-side form controls and schema editors by maintaining nested hierarchical values, tracking field-level modifications, and publishing reactive update notifications to subscribed components.
Sources: packages/stf/src/index.ts:1-4, packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:190-237
Data Engine Architecture and Reactive State
Call-Chain Execution Walkthrough
The core data structures and listener subscription model in DataEngine manage state propagation and reactivity across hierarchical fields. React hooks bridge component rendering cycles with the underlying data instance by subscribing to field-level mutations.
Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-188, packages/stf/src/lib/stf.tsx:190-237
The traversal from a high-level component hook down to the core data instance flows through a deterministic sequence of function calls:
useObjectinitiates structured property inspection for a given field key.useFieldValuewraps the field access, setting up computation and change detection.useListenerestablishes the lifecycle bindings and registers event handlers.useDataEngineresolves the activeDataEngineinstance from the provided argument or React context.
Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-128, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258
Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-128, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258
Note
When instance is passed directly to useDataEngine, it bypasses the React context lookup entirely, supporting standalone or multi-engine configurations.
Sources: packages/stf/src/lib/stf.tsx:38-42
Cross-Boundary Payload Serialization and Deserialization
Payload Serialization and Recursive JSON Handling
To safely transmit complex data structures across boundaries, Fumadocs utilizes specialized serialization routines. The @ungap/structured-clone/json package provides deep parsing and stringification capabilities capable of handling recursive and JSON-compatible data models.
export function serialize(value: unknown): string {
return stringify(value);
}
export function deserialize(value: string): unknown {
return parse(value);
}Sources: packages/story/src/utils/serialization.ts:4-10
Note
The serialize and deserialize utility functions operate directly on unknown types, enabling transparent round-tripping of complex payloads without losing recursive references.
Sources: packages/story/src/utils/serialization.ts:1-10
Page Tree Deserialization and Client Hydration
Server-side loaders in Fumadocs produce serialized representations containing page tree configurations tagged with specific marker identifiers. The client inspects these payloads via isSerializedPageTree, verifying the engine identifier before running structural hydration.
function isSerializedPageTree(v: unknown): v is SerializedPageTree {
return (
typeof v === 'object' &&
v !== null &&
'$fumadocs_loader' in v &&
v.$fumadocs_loader === 'page-tree'
);
}Sources: packages/core/src/source/client/index.tsx:62-69
During deserialization, deserializePageTree processes the root node by visiting each item recursively to hydrate raw HTML strings for item names and icons into React elements using dangerouslySetInnerHTML.
export function deserializePageTree(serialized: SerializedPageTree): PageTree.Root {
const root = serialized.data as PageTree.Root;
visit(root, (item) => {
if ('icon' in item && typeof item.icon === 'string') {
item.icon = (
<span
dangerouslySetInnerHTML={{
__html: item.icon,
}}
/>
);
}
if (typeof item.name === 'string') {
item.name = (
<span
className="fd-page-tree-item-name"
dangerouslySetInnerHTML={{
__html: item.name,
}}
/>
);
}
});
return root;
}Sources: packages/core/src/source/client/index.tsx:14-39
Warning
Icon and name strings found within page tree items are transformed into raw markup wrapper spans via dangerouslySetInnerHTML; ensure server-provided strings are sanitized before serialization.
Sources: packages/core/src/source/client/index.tsx:16-35
The useFumadocsLoader hook memoizes the deserialization process across component lifecycles, scanning input properties and automatically decoding recognized loader formats while preserving all unrelated properties in the output object.
export function useFumadocsLoader<V>(serialized: V): Serialized<V> {
return useMemo(() => {
const out: Record<string, unknown> = {};
for (const k in serialized) {
const v = serialized[k];
if (isSerializedPageTree(v)) {
out[k] = deserializePageTree(v);
} else {
out[k] = v;
}
}
return out as Serialized<V>;
}, [serialized]);
}Sources: packages/core/src/source/client/index.tsx:47-60
Unified Stringification and Structural Transformations
Stringification Options and AST Customization
Fumadocs utilizes a unified stringifier configuration built on top of mdast-util-to-markdown and mdast-util-mdx to serialize Abstract Syntax Tree nodes into plain text or formatted markdown. The stringification engine accepts configuration options controlling element inclusion, attribute filtering, and custom node rendering.
| Option | Type | Default | Purpose |
|---|---|---|---|
filterElement | (node: Nodes) => boolean | 'children-only' | File, TypeTable, Callout, Card return true; other MDX elements return 'children-only'; all other nodes return true | Determines whether to include an element, drop it, or preserve only its children. |
filterMdxAttributes | (node, attribute) => boolean | undefined | Filters individual attributes on MDX flow and text elements during stringification. |
stringify | (node, parent, state, info, ctx) => string | undefined | undefined | Overrides the default stringification logic for specific nodes. |
Sources: packages/core/src/mdx-plugins/stringifier.ts:34-83, packages/core/src/mdx-plugins/stringifier.ts:94-114
Note
Nodes can carry extra metadata via node.data._stringify, which supports three values: 'children-only', { node } to replace the node entirely during serialization, or { text } to return a pre-computed string form directly.
Sources: packages/core/src/mdx-plugins/stringifier.ts:18-28, packages/core/src/mdx-plugins/stringifier.ts:126-135
Placeholder Rendering and Docgen Transformers
The renderPlaceholder function evaluates control character placeholders delimited by null bytes (\0) within markdown or text strings, asynchronously resolving structured JSON payloads and rendering child nodes recursively.
export async function renderPlaceholder(
text: string,
renderers: Record<string, (data: PlaceholderData) => Awaitable<string>>,
): Promise<string> {
let out = '';
let idx = 0;
for (const match of text.matchAll(regex)) {
out += text.slice(idx, match.index);
const inner = match[1];
try {
const data = JSON.parse(inner) as PlaceholderData;
const renderer = data.name && renderers[data.name];
if (data.children.trim()) {
data.children = await renderPlaceholder(data.children, renderers);
}
if (renderer) {
out += await renderer(data);
} else {
out += data.children;
}
} catch {
out += match[0];
}
idx = match.index + match[0].length;
}
out += text.slice(idx);
return out;Sources: packages/core/src/mdx-plugins/remark-llms.runtime.ts:12-42
Caution
If JSON parsing fails on the inner content of a \0...\0 placeholder, the parser catches the error and silently appends the raw match string back into the output without modification.
Sources: packages/core/src/mdx-plugins/remark-llms.runtime.ts:22-36
Additionally, remarkDocGen transforms code blocks matching specific docgen generators by visiting code nodes where the language is json and the meta string matches the doc-gen:<name> pattern.
export function remarkDocGen({ generators = [] }: RemarkDocGenOptions): Transformer<Root, Root> {
return async (tree, file) => {
generators.forEach((gen) => gen.onFile?.(tree, file));
const queue: Promise<void>[] = [];
visit(tree, 'code', (code, _, parent) => {
if (code.lang !== 'json' || !code.meta || !parent) return;
const matches = metaRegex.exec(code.meta);
if (!matches) return;
const name = matches[1];
const gen = generators.find((g) => g.name === name);
const run = async (): Promise<void> => {
const result = await gen?.run(JSON.parse(code.value), {
cwd: file.cwd,
path: file.path,
node: code,
});
const index = parent.children.findIndex((c) => c === code);
if (result && index !== -1) {
const items = Array.isArray(result)
? (result as BlockContent[])
: [result as BlockContent];
parent.children.splice(index, 1, ...items);
}
};
queue.push(run());
});
await Promise.all(queue);
};Sources: packages/doc-gen/src/remark-docgen.ts:31-66
Client Field Hooks and Form Bindings
React Context and Engine Integration
The structured text format framework supplies React bindings through StfProvider, useStf, and useDataEngine to expose data engines across component subtrees. StfProvider accepts an Stf instance containing a DataEngine and wraps child elements using React's context provider. useStf accepts configuration options with defaultValues and instantiates a memoized DataEngine.
const Context = createContext<Stf | null>(null);
export interface Stf {
dataEngine: DataEngine;
}
export function StfProvider({ value, children }: { value: Stf; children: ReactNode }) {
return <Context value={value}>{children}</Context>;
}
export function useStf(options: {
/**
* Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.
*/
defaultValues?: DefaultValue<Record<string, unknown>>;
}): Stf {
const optionsRef = useRef(options);
optionsRef.current = options;
return useMemo(
() => ({
dataEngine: new DataEngine(optionsRef.current.defaultValues),
}),
[],
);
}
export function useDataEngine(instance?: Stf | DataEngine) {
if (instance instanceof DataEngine) return instance;
if (instance) return instance.dataEngine;
return use(Context)!.dataEngine;
}Sources: packages/stf/src/lib/stf.tsx:11-42
Note
When useStf is called, the passed defaultValues object is modified in place during engine initialization; callers requiring original object preservation should pass a clone via structuredClone().
Sources: packages/stf/src/lib/stf.tsx:21-25
useDataEngine resolves an active DataEngine instance by inspecting an explicitly passed instance or falling back to the React context.
export function useDataEngine(instance?: Stf | DataEngine) {
if (instance instanceof DataEngine) return instance;
if (instance) return instance.dataEngine;
return use(Context)!.dataEngine;
}Sources: packages/stf/src/lib/stf.tsx:38-42
Field and Listener Hooks
Field bindings rely on useFieldValue and useListener to subscribe components to specific data engine paths, re-rendering when values initialize, update, or delete. useFieldValue accepts a field key, an optional stf reference, a defaultValue, a compute transformation function, and an isChanged comparator.
export function useFieldValue<V = unknown>(
key: FieldKey,
options: {
stf?: Stf | DataEngine;
defaultValue?: DefaultValue;
/**
* compute value from the actual field value.
*
* to re-compute on in-place updates (may happen on objects, arrays), you should clone the object here.
*/
compute?: (currentValue: unknown) => V;
/** determine whether the value/computed value is changed */
isChanged?: (prev: V, next: V) => boolean;
} = {},
) {
const { stf, compute = (v) => v as V, defaultValue, isChanged = (a, b) => a !== b } = options;
const engine = useDataEngine(stf);
const [value, setValue] = useState<V>(() =>
compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
);
const prevEngineRef = useRef(engine);
if (prevEngineRef.current !== engine) {
setValue(
compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
);
prevEngineRef.current = engine;
}
function onUpdate() {
const computed = compute(engine.get(key));
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
}
useListener({
field: key,
stf,
onInit: onUpdate,
onUpdate: onUpdate,
onDelete() {
const computed = compute(undefined);
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
},
});
return [value, (newValue: unknown) => engine.update(key, newValue)] as const;
}Sources: packages/stf/src/lib/stf.tsx:190-237
| Hook / Utility | Parameters | Return Value | Purpose |
|---|---|---|---|
useStf | options: { defaultValues?: DefaultValue<Record<string, unknown>> } | Stf | Creates a memoized Stf context object managing a new DataEngine. |
useDataEngine | instance?: Stf | DataEngine | DataEngine | Resolves the data engine instance from arguments or context. |
useFieldValue | key: FieldKey, options?: { stf, defaultValue, compute, isChanged } | [value, (newValue) => void] | Subscribes to a field path, computing values and returning a getter tuple with an updater. |
useListener | listener: DataEngineListener & { stf?: Stf | DataEngine } | void | Subscribes raw lifecycle listeners (onInit, onUpdate, onDelete) to a field key. |
Sources: packages/stf/src/lib/stf.tsx:21-36, packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258
Array and Object Collection Hooks
Collection controls are provided via useArray and useObject, mapping structured schema properties and array indices to interactive form bindings. useArray computes array item information objects and exposes methods to insert or remove items at specific indices.
export function useArray(
field: FieldKey,
options: { defaultValue?: DefaultValue<unknown[]> } = {},
) {
const engine = useDataEngine();
const { defaultValue } = options;
const [items] = useFieldValue(field, {
defaultValue,
compute(value) {
const items: ArrayItemInfo[] = [];
if (!Array.isArray(value)) return items;
for (let i = 0; i < value.length; i++) {
items.push({
field: [...field, i],
index: i,
});
}
return items;
},
isChanged(prev, next) {
return prev.length !== next.length;
},
});
return {
items,
insertItem(itemValue?: unknown) {
const value = engine.get(field);
const idx = Array.isArray(value) ? value.length : 0;
engine.init([...field, idx], itemValue);
},
removeItem(index: number) {
engine.delete([...field, index]);
},
};
}Sources: packages/stf/src/lib/stf.tsx:49-85
useObject inspects plain objects using defined properties, pattern properties, fallbacks, and lazy evaluation flags, returning an array of property item descriptors alongside append and delete handlers.
export function useObject<T>(
field: FieldKey,
options: {
defaultValue?: DefaultValue<object>;
/** ignore fixed properties unless defined */
lazy?: boolean;
properties: Record<string, T>;
patternProperties?: Record<string, T>;
fallback?: T;
},
) {
const {
properties: definedProps,
patternProperties: definedPatternProps = {},
defaultValue,
fallback,
lazy,
} = options;
const engine = useDataEngine();
const [objectKeys] = useFieldValue(field, {
defaultValue,
compute(currentValue) {
return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
},
isChanged(prev, next) {
return !deepEqual(prev, next);
},
});
const properties = useMemo(() => {
const properties: PropertyItemInfo<T>[] = [];
const unknownKeys = new Set(objectKeys);
for (const [key, prop] of Object.entries(definedProps)) {
if (lazy && !unknownKeys.has(key)) continue;
unknownKeys.delete(key);
properties.push({
kind: 'fixed',
field: [...field, key],
key,
info: prop,
});
}
for (const [pattern, prop] of Object.entries(definedPatternProps)) {
const regex = RegExp(pattern);
for (const key of unknownKeys) {
if (!key.match(regex)) continue;
unknownKeys.delete(key);
properties.push({
kind: 'pattern',
info: prop,
key,
pattern,
field: [...field, key],
});
}
}
if (fallback) {
for (const key of unknownKeys) {
properties.push({
kind: 'fallback',
field: [...field, key],
key,
info: fallback,
});
}
}
return properties;
}, [definedPatternProps, definedProps, fallback, field, lazy, objectKeys]);
return {
properties,
_objectKeys: objectKeys,
onAppend(name: string, value?: unknown) {
name = name.trim();
if (name.length === 0) return;Sources: packages/stf/src/lib/stf.tsx:101-188
Warning
When using useObject with lazy: true, fixed properties defined in properties are ignored unless they are already present as keys in the underlying object data.
Sources: packages/stf/src/lib/stf.tsx:101-135
Markdown Ingestion and Dynamic Parsing
Block and Inline Transforms
Fumadocs processes raw markdown and inline snippets through a unified pipeline combining remark, remark-gfm, remark-rehype, and hast-util-to-jsx-runtime. The createMarkdownRenderer function configures a processor that parses content into an mdast tree, runs transforms, and evaluates it into React elements via JSX runtimes.
Sources: packages/core/src/content/md.ts:30-48
const processor = remark()
.use(remarkPlugins)
.use(remarkRehype, remarkRehypeOptions)
.use(rehypePlugins);Sources: packages/core/src/content/md.ts:35-38
Specialized markdown processors adapt block structures for interactive contexts. For instance, live previews apply a custom rehypeWrapWords visitor that splits text and element nodes into individual word spans decorated with animation classes, while skipping pre block nodes.
Sources: packages/preview/src/components/markdown.tsx:24-54
Warning
The rehypeWrapWords transformer explicitly skips pre elements (node.tagName === 'pre') to prevent code block contents from being fractured into individual word spans.
Sources: packages/preview/src/components/markdown.tsx:27
Code blocks embedded within markdown are intercepted by preprocessors like MarkdownPre or Pre, which extract language names from class attributes (converting language-mdx to md) and forward stripped code content to specialized rendering components.
Sources: packages/preview/src/components/markdown.tsx:80-95, packages/asyncapi/src/ui/index.tsx:222-235
Twoslash Code Intelligence Integration
Code intelligence and type popup annotations are injected via transformerTwoslash, which wraps Shiki highlighters with a lazy-loaded Twoslasher instance configured with moduleResolution: 100 (ModuleResolutionKind.Bundler).
Sources: packages/twoslash/src/index.ts:31-52
return (cachedInstance ??= createTwoslasher({
...twoslashOptions,
compilerOptions: {
moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
baseUrl: undefined,
...twoslashOptions.compilerOptions,
},
}));
}Sources: packages/twoslash/src/index.ts:38-46
The renderer configures Hast node mappings for hover triggers, popup contents, and documentation boxes, translating JSDoc links ({@link ...}) during nested markdown parsing.
Sources: packages/twoslash/src/index.ts:60-107, packages/twoslash/src/index.ts:133-137
| Hast Element Mapping | Target Tag Name | Properties / Classes | Purpose |
|---|---|---|---|
hoverToken | Popup | — | Wraps inspected tokens requiring hover inspection. |
hoverPopup | PopupContent | class: 'nd-copy-ignore' | Container for type and documentation popups. |
popupDocs | div | class: 'prose twoslash-popup-docs' | Styles JSDoc documentation sections. |
popupTypes | div | class: 'twoslash shiki fd-codeblock prose-no-margin' | Renders type signature code blocks inside popups. |
nodesHighlight | span | class: 'highlighted-word twoslash-highlighted' | Highlights code tokens associated with type info. |
Sources: packages/twoslash/src/index.ts:61-105
Related
On This Page
OverviewData Engine Architecture and Reactive StateCall-Chain Execution WalkthroughCross-Boundary Payload Serialization and DeserializationPayload Serialization and Recursive JSON HandlingPage Tree Deserialization and Client HydrationUnified Stringification and Structural TransformationsStringification Options and AST CustomizationPlaceholder Rendering and Docgen TransformersClient Field Hooks and Form BindingsReact Context and Engine IntegrationField and Listener HooksArray and Object Collection HooksMarkdown Ingestion and Dynamic ParsingBlock and Inline TransformsTwoslash Code Intelligence IntegrationRelated