A rich text schema: the set of node and mark types a document may use.
The schema is the single source of truth of a Coelho document. It is
declared once in Elixir, used server side to validate and render
documents, and exported with to_json/1 to build the matching
ProseMirror schema in the browser. A document the server would reject is
therefore a document the client could not have produced.
Declaring a schema
Coelho.Schema.new(
top_node: :doc,
nodes: [
doc: [content: "block+"],
paragraph: [content: "inline*", group: "block", render: {"p", []}],
text: [group: "inline", inline: true, text: true]
],
marks: [
bold: [render: {"strong", []}]
]
)Node and mark declaration order is preserved: ProseMirror resolves default types by position, so the first node of a group is its default.
A text node is injected automatically when the declaration omits it.
Summary
Functions
The schema Coelho ships with: paragraphs, headings, lists, quotes, code blocks, images, and the usual inline marks.
Adds nodes and marks to an existing schema.
Whether a node type answers to a name used in a content expression, either because it is that node, or because it belongs to that group.
Looks up a mark spec by name, returning nil when unknown.
Builds a schema from a declaration.
Looks up a node spec by name, returning nil when unknown.
Resolves a mark type name coming from untrusted input.
Resolves a node type name coming from untrusted input.
Exports the schema in the shape the browser side consumes to build the matching ProseMirror schema.
Types
@type t() :: %Coelho.Schema{ groups: %{optional(atom()) => MapSet.t(atom())}, mark_names: %{optional(String.t()) => atom()}, mark_order: [atom()], marks: %{optional(atom()) => Coelho.Schema.MarkSpec.t()}, node_names: %{optional(String.t()) => atom()}, node_order: [atom()], nodes: %{optional(atom()) => Coelho.Schema.NodeSpec.t()}, top_node: atom() }
Functions
@spec default() :: t()
The schema Coelho ships with: paragraphs, headings, lists, quotes, code blocks, images, and the usual inline marks.
Adds nodes and marks to an existing schema.
Most applications want the default schema and one thing of their own — a mention, an embed, a callout — and re-declaring the other fifteen nodes to get there would guarantee they drift.
Coelho.Schema.extend(Coelho.Schema.default(),
nodes: [
mention: [
group: "inline",
inline: true,
void: true,
attrs: [user_id: [required: true, validate: :integer], label: [default: nil, validate: {:nullable, :string}]],
render: &MyApp.RichText.render_mention/2
]
]
)Additions keep their declaration order, after what was already there. Redeclaring an existing name replaces it, which is how the default schema's rendering or attributes get adjusted without a fork.
Whether a node type answers to a name used in a content expression, either because it is that node, or because it belongs to that group.
@spec mark_spec(t(), atom()) :: Coelho.Schema.MarkSpec.t() | nil
Looks up a mark spec by name, returning nil when unknown.
Builds a schema from a declaration.
Raises ArgumentError when the declaration is inconsistent: an unparsable
content expression, a name no node or group answers to, an unknown mark in
a node's :marks list, or a missing top node. A schema is developer
authored, so an invalid one is a bug rather than a runtime condition.
@spec node_spec(t(), atom()) :: Coelho.Schema.NodeSpec.t() | nil
Looks up a node spec by name, returning nil when unknown.
Resolves a mark type name coming from untrusted input.
Resolves a node type name coming from untrusted input.
Never converts to an atom blindly: only names the schema already knows are resolved, so a hostile document cannot grow the atom table.
Exports the schema in the shape the browser side consumes to build the matching ProseMirror schema.
Nodes and marks are emitted as ordered pairs rather than objects: node order carries meaning in ProseMirror and map key order does not survive a round trip through Elixir.