Quillon.Schema (Quillon v0.3.0)

Copy Markdown View Source

Schema definitions for Quillon documents.

A schema defines the valid structure of a document:

  • Which node types are allowed
  • What children each node type can contain (content expressions)
  • Which marks can be applied to each node type
  • Required and optional attributes for nodes and marks
  • Mark behavior (inclusive, keep_on_split, excludes)

Example

schema = Quillon.Schema.default()

# Validate a document against the schema
{:ok, doc} = Quillon.Schema.Validator.validate(doc, schema)

Custom Schemas

custom = %Quillon.Schema{
  groups: %{
    block: [:paragraph, :heading, :code_block],
    inline: [:text]
  },
  nodes: %{
    document: %{content: "block+"},
    paragraph: %{content: "inline*", group: :block, marks: :all},
    heading: %{content: "inline*", group: :block, marks: [:bold, :italic]},
    code_block: %{content: nil, group: :block, marks: nil}
  },
  marks: %{
    bold: %{},
    italic: %{}
  }
}

Summary

Types

Specification for an attribute

Specification for a mark type

Specification for a node type

t()

A schema definition

Functions

Get allowed marks for a node type.

Returns the default schema for Quillon documents.

Get the types in a group.

Get the mark specification for a given type.

Get the node specification for a given type.

Check if a mark is allowed on a node type.

Check if a mark type exists in the schema.

Check if two marks conflict (via excludes).

Merge two schemas.

Check if a node type exists in the schema.

Types

attr_spec()

@type attr_spec() :: %{optional(:required) => boolean(), optional(:default) => any()}

Specification for an attribute

mark_spec()

@type mark_spec() :: %{
  optional(:inclusive) => boolean(),
  optional(:keep_on_split) => boolean(),
  optional(:excludes) => [atom()] | nil,
  optional(:attrs) => %{required(atom()) => attr_spec()}
}

Specification for a mark type

node_spec()

@type node_spec() :: %{
  optional(:content) => String.t() | nil,
  optional(:group) => atom() | nil,
  optional(:marks) => [atom()] | :all | nil,
  optional(:attrs) => %{required(atom()) => attr_spec()}
}

Specification for a node type

t()

@type t() :: %Quillon.Schema{
  groups: %{required(atom()) => [atom()]},
  marks: %{required(atom()) => mark_spec()},
  nodes: %{required(atom()) => node_spec()}
}

A schema definition

Functions

allowed_marks(schema, node_type)

@spec allowed_marks(t(), atom()) :: :all | [atom()] | nil

Get allowed marks for a node type.

Returns :all if all marks are allowed, a list of allowed mark types, or nil if no marks are allowed.

Examples

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.allowed_marks(schema, :paragraph)
:all

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.allowed_marks(schema, :divider)
nil

default()

@spec default() :: t()

Returns the default schema for Quillon documents.

The default schema includes:

  • Node types: document, paragraph, heading, divider, blockquote, callout, code_block, image, video, text
  • Marks: bold, italic, underline, strike, code, link
  • Groups: block, inline

Examples

iex> schema = Quillon.Schema.default()
iex> :blockquote in Map.keys(schema.nodes)
true

get_group(schema, name)

@spec get_group(t(), atom()) :: [atom()]

Get the types in a group.

Examples

iex> schema = Quillon.Schema.default()
iex> :paragraph in Quillon.Schema.get_group(schema, :block)
true

get_mark_spec(schema, type)

@spec get_mark_spec(t(), atom()) :: mark_spec() | nil

Get the mark specification for a given type.

Examples

iex> schema = Quillon.Schema.default()
iex> spec = Quillon.Schema.get_mark_spec(schema, :bold)
iex> spec.inclusive
true

get_node_spec(schema, type)

@spec get_node_spec(t(), atom()) :: node_spec() | nil

Get the node specification for a given type.

Examples

iex> schema = Quillon.Schema.default()
iex> spec = Quillon.Schema.get_node_spec(schema, :paragraph)
iex> spec.content
"inline*"

mark_allowed?(schema, node_type, mark_type)

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

Check if a mark is allowed on a node type.

Examples

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.mark_allowed?(schema, :paragraph, :bold)
true

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.mark_allowed?(schema, :divider, :bold)
false

mark_type?(schema, type)

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

Check if a mark type exists in the schema.

Examples

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.mark_type?(schema, :bold)
true

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.mark_type?(schema, :unknown)
false

marks_conflict?(schema, mark1, mark2)

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

Check if two marks conflict (via excludes).

Examples

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.marks_conflict?(schema, :code, :link)
true

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.marks_conflict?(schema, :bold, :italic)
false

merge(base, override)

@spec merge(t(), t()) :: t()

Merge two schemas.

Everything composes: an override declares only what it changes, and the rest of the base survives. Two consumers extending the same type or the same group coexist rather than unregistering each other's work.

  • Groups union their members.
  • Specs merge key by key, so an override supplying only attrs keeps the base's content, group and marks.
  • attrs merge entry by entry, so two consumers can each add their own.
  • An individual attr spec replaces, since it holds only required/default and is trivial to restate.

An explicit key still overwrites, which is how a type is narrowed: give content: nil to forbid children, or marks: [:bold] to restrict them. What a merge cannot do is remove a key the base declared - build the schema from scratch for that.

Examples

iex> base = Quillon.Schema.default()
iex> custom = %Quillon.Schema{marks: %{highlight: %{}}}
iex> merged = Quillon.Schema.merge(base, custom)
iex> Map.has_key?(merged.marks, :highlight)
true

Adding an attribute keeps the rest of the spec:

iex> plugin = %Quillon.Schema{nodes: %{paragraph: %{attrs: %{sticky: %{}}}}}
iex> merged = Quillon.Schema.merge(Quillon.Schema.default(), plugin)
iex> merged.nodes.paragraph.content
"inline*"
iex> Map.has_key?(merged.nodes.paragraph.attrs, :sticky) and
...>   Map.has_key?(merged.nodes.paragraph.attrs, :align)
true

An explicit key still narrows:

iex> plugin = %Quillon.Schema{nodes: %{paragraph: %{content: nil}}}
iex> merged = Quillon.Schema.merge(Quillon.Schema.default(), plugin)
iex> merged.nodes.paragraph.content
nil

A group extension keeps what was already there:

iex> base = Quillon.Schema.default()
iex> plugin = %Quillon.Schema{groups: %{block: [:aside]}}
iex> merged = Quillon.Schema.merge(base, plugin)
iex> :aside in merged.groups.block and :paragraph in merged.groups.block
true

Two plugins extending the same group coexist:

iex> a = %Quillon.Schema{groups: %{inline: [:sparkline]}}
iex> b = %Quillon.Schema{groups: %{inline: [:badge]}}
iex> merged = Quillon.Schema.default() |> Quillon.Schema.merge(a) |> Quillon.Schema.merge(b)
iex> Enum.sort(merged.groups.inline)
[:badge, :sparkline, :text]

node_type?(schema, type)

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

Check if a node type exists in the schema.

Examples

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.node_type?(schema, :paragraph)
true

iex> schema = Quillon.Schema.default()
iex> Quillon.Schema.node_type?(schema, :unknown)
false