Coelho.Schema (coelho v0.1.0)

Copy Markdown View Source

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

t()

@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

default()

@spec default() :: t()

The schema Coelho ships with: paragraphs, headings, lists, quotes, code blocks, images, and the usual inline marks.

extend(schema, opts)

@spec extend(
  t(),
  keyword()
) :: t()

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.

instance_of?(schema, name, node_type)

@spec instance_of?(t(), atom(), atom()) :: boolean()

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.

mark_spec(schema, name)

@spec mark_spec(t(), atom()) :: Coelho.Schema.MarkSpec.t() | nil

Looks up a mark spec by name, returning nil when unknown.

new(opts)

@spec new(keyword()) :: t()

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.

node_spec(schema, name)

@spec node_spec(t(), atom()) :: Coelho.Schema.NodeSpec.t() | nil

Looks up a node spec by name, returning nil when unknown.

resolve_mark_name(schema, name)

@spec resolve_mark_name(t(), term()) :: {:ok, atom()} | :error

Resolves a mark type name coming from untrusted input.

resolve_node_name(schema, name)

@spec resolve_node_name(t(), term()) :: {:ok, atom()} | :error

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.

to_json(schema)

@spec to_json(t()) :: map()

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.