-
Notifications
You must be signed in to change notification settings - Fork 2
Schema Customization
A schema is the document contract for a handle. It determines valid JSON and HTML, empty-document construction, commands, serialized output, viewer rendering, and collaboration snapshot compatibility. Choose it before creating a NativeEditorDocumentHandle; it cannot be changed for that handle later.
defaultSchema is the default for handles and viewers. It is the same object as prosemirrorSchema, uses ProseMirror snake_case list and void-node names, and retains codeBlock for code blocks. tiptapCompatibleSchema preserves the former camelCase list and void-node names for existing stored content.
| Preset | Lists | Void nodes |
|---|---|---|
defaultSchema / prosemirrorSchema
|
bullet_list, ordered_list, list_item
|
hard_break, horizontal_rule, image
|
tiptapCompatibleSchema |
bulletList, orderedList, listItem
|
hardBreak, horizontalRule, image
|
Both presets expose doc, paragraph, heading, blockquote, codeBlock, and text, plus the marks bold, italic, underline, strike, and link. A heading is represented publicly as { type: 'heading', attrs: { level: 1..6 } }; the schema projection maps it to the native h1 through h6 variants.
The old root export tiptapSchema was replaced by tiptapCompatibleSchema. Existing camelCase documents must pass that schema explicitly to every handle and viewer. New documents can omit schema or use defaultSchema.
Each compiled preset also has a keyed authoring form: defaultSchemaSpec, prosemirrorSchemaSpec, and tiptapCompatibleSchemaSpec.
Both presets now declare codeBlock.attrs.language with a default of null. Set it in JSON to record the code language; optional syntax coloring is configured through an addon, not through a different node type. See Code Syntax Highlighting.
Adding language changes the compiled schema fingerprint even for documents with no code blocks. Existing JSON can be loaded under the new schema after validating the content, but collaboration snapshots created with the old schema must not be treated as compatible. Migrate persisted content into a new compatible room/snapshot and decide how to handle old history, or keep the previous schema explicitly until every peer and stored snapshot is migrated. Coordinate custom attribute constraints the same way.
Do not rename stored node types and assume the result is equivalent. Node names, mark names, projections, attributes, and HTML rules are part of schema identity and the collaboration fingerprint.
defineSchema() accepts a ProseMirror-shaped SchemaSpec keyed by public JSON node and mark names and compiles it into the serializable SchemaDefinition used by native code.
import {
defineSchema,
resolveDocumentDescriptor,
} from '@apollohg/react-native-rich-text-editor';
const articleSchema = defineSchema({
nodes: {
article: { content: 'block+', role: 'doc' },
paragraph: {
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM: ['p', 0],
},
callout: {
content: 'inline*',
group: 'block',
attrs: { tone: { default: 'info' } },
parseDOM: [{ tag: 'aside' }],
toDOM: ['aside', 0],
},
text: { group: 'inline' },
},
marks: {
highlight: {
parseDOM: [{ tag: 'mark' }],
toDOM: ['mark', 0],
},
},
});
const articleDescriptor = resolveDocumentDescriptor(articleSchema);articleDescriptor.documentNodeName is article. Pass articleSchema to the handle and every RichTextViewer that renders its content.
| Field | Contract |
|---|---|
nodes |
Required record keyed by the public JSON node type. |
marks |
Optional record keyed by the public JSON mark type. |
atoms |
Optional AtomNodeDefinition[] composed into the compiled schema. See Custom Atom Nodes. |
| Field | Contract |
|---|---|
content |
ProseMirror-style content expression. Defaults to an empty leaf. |
group |
Space-separated content groups such as block or inline. |
attrs |
Declared persisted attributes. An AttrSpec without default is required. |
role |
Optional native semantic role. Common doc, paragraph, and text shapes are inferred; use heading for an attribute-projected heading. |
parseDOM |
Declarative input tag rules with optional fixed attrs. |
toDOM |
One static [tag] or [tag, 0] output, or an attribute-driven output projection. |
html |
NodeHtmlRules for lossless custom void-node attributes. |
isVoid |
Marks a node with no editable content. |
allowUndeclaredAttrs |
Explicit pass-through opt-in for undeclared JSON metadata. |
SchemaMarkSpec supports attrs, excludes, parseDOM, toDOM, and allowUndeclaredAttrs. Custom mark HTML output is restricted to inert supported tags: span, strong, em, u, s, code, a, sub, sup, and mark.
content supports node or group symbols, sequences, alternatives, parentheses, and ?, *, +, {n}, and {min,max} quantifiers. A valid schema has exactly one document role and one text role and can construct a valid empty document.
Use an AttributeDOMOutputSpec when one public JSON node type maps to several native HTML variants. The built-in heading is the canonical example:
const heading = {
content: 'inline*',
group: 'block',
role: 'heading' as const,
attrs: { level: { default: 1 } },
parseDOM: [1, 2, 3, 4, 5, 6].map((level) => ({
tag: `h${level}`,
attrs: { level },
})),
toDOM: {
switchOn: 'level',
cases: {
1: ['h1', 0],
2: ['h2', 0],
3: ['h3', 0],
4: ['h4', 0],
5: ['h5', 0],
6: ['h6', 0],
},
},
};The switch attribute must be declared and use one consistent scalar type. When parseDOM is supplied, its rules must map one-to-one to the output cases. The compiled native variants retain a JSON projection back to the public type and discriminator attribute.
The related public types are ParseDOMRule, DOMOutputSpec, AttributeDOMOutputSpec, and NodeJSONProjection.
NodeHtmlRules supports lossless attributes on custom void nodes:
interface NodeHtmlRules {
tag: string;
staticAttrs?: Record<string, string>;
attrMap?: Record<string, string>;
}staticAttrs must provide a discriminator that identifies the void node when parsing. attrMap maps every declared JSON attribute to a unique HTML attribute without colliding with the discriminator. Prefer defineAtomNode() for component-backed atoms because it validates safe tags, fills default data-* mappings, and rejects incomplete, ambiguous, or colliding rules. See Custom Atom Nodes.
SchemaDefinition, NodeSpec, and MarkSpec are the lower-level serializable forms sent to the native engine. defineSchema() produces them and should be the ordinary authoring path.
| Type | Important fields |
|---|---|
SchemaDefinition |
nodes: NodeSpec[], marks: MarkSpec[]
|
NodeSpec |
name, content, role, optional group, attrs, htmlTag, html, json, isVoid, allowUndeclaredAttrs
|
MarkSpec |
name, optional attrs, excludes, htmlTag, allowUndeclaredAttrs
|
NodeJSONProjection |
Public type and optional fixed attrs for a compiled native variant. |
AttrSpec |
Optional default, type, enum, min, and max; omitting default makes the attribute required. |
Attribute types are string, number, boolean, object, or array. Bounds require a compatible explicit type and apply to numeric values, Unicode string lengths, or array lengths. Enum values must share one JSON type. Constraints validate defaults, imports, and mutation candidates and contribute to the schema fingerprint. For inferred component and fragment types, see Custom Atom Nodes.
allowUndeclaredAttrs is an escape hatch, not a convenience setting. Use it only when arbitrary persisted metadata is a deliberate part of the node or mark contract. Atom definitions do not allow it.
Use extension helpers instead of copying shipped node specs:
import {
defaultSchema,
withAtomsSchema,
withImagesSchema,
withMentionsSchema,
} from '@apollohg/react-native-rich-text-editor';
const messageSchema = withAtomsSchema(
withMentionsSchema(withImagesSchema(defaultSchema)),
atomDefinitions
);withImagesSchema() and withMentionsSchema() return the input unchanged when it already has the standard image or mention node. The presets already include image. withAtomsSchema() similarly adds each definition once but rejects a conflicting node with the same name or ambiguous atom HTML rules.
The image helpers remain IMAGE_NODE_NAME, ImageNodeAttributes, imageNodeSpec(), withImagesSchema(), and buildImageFragmentJson(). The standard image has required src and nullable alt, title, width, and height attributes.
insertContentJson() accepts a document fragment. For a custom root, resolve its descriptor and reuse it:
const descriptor = resolveDocumentDescriptor(articleSchema);
const callout = buildDocumentFragmentJson(
[
{
type: 'callout',
attrs: { tone: 'warning' },
content: [{ type: 'text', text: 'Check the result.' }],
},
],
descriptor
);
const image = buildImageFragmentJson(
{ src: 'https://cdn.example.com/report.png', alt: 'Report chart' },
descriptor
);The helpers set the document-root type; they do not make an undeclared node valid.
- Create a handle with the final schema, fragment name, policy, and resource limits. An editor prop cannot override them.
- Use the same schema for stored content, viewers, and every participant in a collaboration room.
- A room snapshot records the schema fingerprint and fragment name. A changed schema requires an explicit content migration and compatible history strategy.
- Invalid or unconstructible schemas are rejected with schema errors; public handle or viewer creation does not silently substitute a fallback schema.
- Keep
maxSchemaNodesandmaxSchemaExpressionByteslarge enough for the schema while staying within the hard bounds in Production Limits and Errors.
For handle construction and schema exports, see Document API Reference.
React Native Rich Text Editor · Documentation · Migration Guide