System Guide
This guide provides a prose description of Wordgard's design and
functionality. For an item-by-item documentation of the programming
interface, see the reference manual. For more detailed
descriptions of specific parts of the system, try the
examples.

Wordgard is a rich text editor system with a focus on customizability.
Its intended use case is the editing of content that fits a specific
schema, rather than a generic
WYSIWYG or HTML editor. It
tries to provide a WYSIWYG-inspired interface, but describes content
and editing actions in terms of semantic concepts (headers, lists,
emphasis) instead of presentation concepts (font family, paragraph
indentation, bold text).
Introduction
The central thing that this library provides is a user interface
component, implemented in the Wordgard class. This component
displays an editor in a web document.
It also defines a set of abstractions for defining documents, editor
states, and editor actions, most of which can be used outside of the
browser.
To start using Wordgard, you need to install the "wordgard" package
from npm. The actual library is
made up of a handful of separate modules inside that package, with
names like "wordgard/editor" or "wordgard/doc". You import from
those to use the library. This code sets up a basic editor:
import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
import {history} from "wordgard/history"
let editor = Wordgard.create({
doc: `<p>Starting content</p>`,
config: [
fullSchema(),
history(),
menuBar()
],
parent: document.body
})
When doing non-trivial work with this library, it is very much
recommended to use TypeScript. The
pieces fit together in rather intricate ways, and having reliable
autocompletion and type errors is going to save you a lot of time.
The main concepts in this system are:
The custom document data structure used by the
editor. Though conversion to and from HTML is supported, the editor
stores its content using a custom immutable tree type.
Schemas specify what the content of the editor may
look like. The library comes with a bunch of predefined schema
elements (things like paragraphs, lists, links, images, emphasized
text), and you can define your own to
support other types of content.
The editor is configured by a collection of extensions. These
determine almost everything about its behavior, from the schema to
the visible UI elements to the way browser events are handled.
The editor state holds the configuration, current document,
selection, and any other values that need to be tracked to
represent the editing state. Extensions can include their own bits
of state. State changes happen through transactions, which are
objects containing a precise description of the changes.
The editor component displays an editor state in a browser
document, and helps translate user interaction into state
transactions.
Because the system is very open, and as much as possibly of its
functionality is written using the public programming interface, the
full API is rather large. For simple applications, you can get away
with using only a handful of concepts. But when you do need to do
ambitious custom things, you'll be able to.
This library makes enthusiastic use of TypeScript
namespaces
to nest related functionality. This may seem a bit unconventional at
times, but I find it helps avoid excessively long lists of imports,
and makes it easier to find things through autocompletion.
Documents
A Wordgard document is a tree, with nested structure like lists or
tables represented in the tree structure (a paragraph in a list
actually has that list as its parent node). Changing a document
involves creating a new tree, though that tree will generally share a
lot of nodes with the original document.
Value Type
Though the structure of the document tree superficially resembles that
of the browser DOM, it is used in a very different way. It uses value
semantics, meaning that a node object expresses a piece of document
structure, but does not have a significant identity. A given object
never changes, so when a node or its content is updated, a new object
is created. Similarly, it is possible for the same node object to
occur multiple times in a document. For these reasons, you cannot use
a given node's JavaScript object as a useful way to identify that
specific node. You should use a document offset
instead.
Nodes do not have a parent pointer. Since they will be reused in
changed versions of the document, they do not have a single stable
parent.
In the same vein, you cannot change a node. You can change a
document by applying a change to create a new document,
but there's no setAttribute or removeChild action to take on a
node in isolation.
Tree Structure
Wordgard defines two types of nodes:
Plots have content. These contain a part of the document, and
generally assign some meaning to it (such as it being a heading,
list, or table). The document itself is a plot.
Leaves are nodes without content. These represent things like
line breaks, images, and text.

Use of the exotic term "plot" was motivated by the fact that there's
no nice, short word in computer science tradition for internal tree
nodes (as opposed to external leaf nodes), and the metaphor of a
document consisting of plots with leaves in it pleasantly fits the
library's garden theme.
The two types of nodes are represented with different object types
(Plot and Leaf). Many systems give the two a largely
compatible interface, but I found that the difference between them was
often significant enough that forcing a clear separation avoids a lot
of category errors when working with nodes.
Each node has a type associated with it. This
describes whether it is a leaf or a plot, whether it is a block or
inline node, how to convert it to and from HTML, what content it
allows (if a plot), and so on. A node type may allow a parameter,
which is a value that is stored in the node. A heading plot may use
this to specify the heading level, for example, or an image leaf the
image URI.
On top of a type and a parameter, every node may store a set of
marks. These act a bit like additional parameters, except that they
are defined separately, outside of the node type. This is used for
things like inline styles (emphasis, links, superscript), text
alignment, image alt text, and so on. Like nodes, marks have a type and a parameter.
Text nodes are a special kind of leaf node. They have a fixed,
built-in leaf type, and their parameter is their
text content. Such nodes are automatically merged when two adjacent
ones occur with the same marks, so that stretches of text with the
same style are always a single leaf.
In this system, the term tag refers to a node's markup: a node type,
a parameter, and a set of marks. For a leaf, that is all the node is.
A plot is a plot tag plus an array of content nodes.
This diagram shows a sketch of Wordgard's node representation. The the
blue boxes plot tags (with their content below them), the purple boxes
leaves. White boxes are leaf or tag parameters, and orange boxes
represent marks.
Heading
2
Alignment: center
Text
"Document"
Emphasis
Underline
Image
"flower.jpg"
Alt: "monk's cress flower"
In the programming model, plots are objects of type Plot, with
tag and content properties. The content
contains other plots or Leaf nodes. Both leaves and plot tags
have type, param, and marks properties.
On nodes and tag/leaf objects, the isPlot/isLeaf properties can be used
to tell whether the object is a plot or leaf. In TypeScript, this
check will automatically narrow the type so that you can access its
leaf or plot-specific properties.
The library distinguishes between inline and block content, so every
node is marked as either being inline or being a block. A given plot's
content must either be all inline nodes or all block nodes. The term
textblock is used for block nodes with inline content (such as
paragraphs, headings, or code blocks).
The document representation is designed in such a way that a given
document has a single canonical representation. Unlike HTML's
free-form nesting of inline style tags, mark structure is flat, and
marks are ordered in a deterministic way. This makes it easier to
compare and reason about content.
Index System
To be able to refer to positions in the content, Wordgard uses an
index system that assigns a number to every position in the document.
It conceptually counts tokens from the start of the document, and
works like this:
The start of the document, right before the first content, is
position 0.
Each plot has an open token and a close token. So entering or
leaving a plot adds one to the index.
Each (UTF16) character in a text leaf counts as one token. So if a
paragraph at the start of the document contains the word “hi”,
position 1 is the start of the paragraph, position 2 is after the
“h”, position 3 after the “i”, and position 4 after the whole
paragraph.
Leaves that are not text leaves always count as a single token.
So if you have a document that, when expressed as HTML, would look
like this:
<blockquote><p>Text <img src="..."></p></blockquote>
The token sequence, with positions, looks like this:
0 1 2 3 4 5 6 7 8 9 10
<blockquote> <p> T e x t _ <img> </p> </blockquote>
Each node has a length property that
gives you the size of the entire node. Plots also have a contentLength property with the total length of their content.
For normal plots, their total length is their content length plus two.
The document plot is different—because its start/end tokens are not
part of the actual document, its length is equal to its content
length.
Interpreting such positions manually involves quite a lot of counting.
There is a resolved position abstraction that directly
gives you the context of a given document position. You get it by
calling doc.resolve(pos) and it will tell
you where you are in the position's parent node, where that parent
starts, what further ancestor nodes there are, what nodes sit directly
next to the position, and so on.
It is often useful to run through a document's nodes, or only the
nodes in a certain range. For that you can either call iterate, or create a resolved position and walk that
forward.
Changes
To modify a Wordgard document you create a change
set and then applying that, giving you the
changed document. The new and old document will usually share most of
their inner nodes.
Changes are specified with document positions. ChangeSet.create takes a collection of objects
that describe the changes (the same format is used when creating
editor transactions).
Assume the document is a paragraph with the word "sage". These are
some possible ways to make changes to it:
let delS = ChangeSet.create(doc, {from: 1, to: 2})
let addWord = ChangeSet.create(doc, {
from: 1,
insert: [Leaf.text("meadow ")]
})
let replace = ChangeSet.create(doc, {
from 1, to: 5,
insert: [Leaf.text("...")]
})
let multiple = ChangeSet.create(doc, [
{from: 1, insert: [Leaf.text("(")]},
{from: 5, insert: [Leaf.text(")")]}
])
A change spec can be a single change or an array of changes. It may
also contain existing change set objects. Content changes can specify
a start and optional end, and either delete that range, or insert
content by providing an array of tokens to insert at the given
position.
Tokens can be nodes. But sometimes you need to not just insert a whole
node, but end or start a plot at a specific position. For that reason,
tokens may also be plot open tokens, for which plot tag objects are
used, or a special Plot.End token to close the current plot.
For example, to create a paragraph break in the middle of the example
document, you could do this:
let paraBreak = ChangeSet.create(doc, {
from: 2,
insert: [Plot.End, Paragraph]
})
Where Paragraph is the plot tag for paragraph nodes. A set of
tokens is called a slice, which is also the type of
object used for replacements when iterating the changes in a set.
When specifying multiple changes at the same time, you do not need to
compensate change positions for the other changes. All positions are
specified in terms of the initial document. So for example the change
that inserted parentheses at positions 1 and 5 will put them around
the entire word, because that word spans 1 to 5 in the original
document.
It is also possible for changes to add or remove marks without
replacing the underlying content. When a change object has an add or remove property, it is a modifying (rather than replacing) change.
let makeStrong = ChangeSet.create(doc, {
from: 1, to: 5,
add: Strong
})
Where Strong is the mark for strong emphasis.
Change Correction
It is possible to create changes that would break document structure.
Examples include:
- Deleting the opening token of a paragraph without replacement.
- Putting an inline node where only block nodes are allowed, or vice
versa.
Often, you know that the type of change you're creating is valid. But
when you don't, it is possible to make ChangeSet.create check
and fix the change for you. There are two ways to do this:
Put a fit property on a specific
change. This gives ChangeSet.create permission to change
the range of the change, moving it out to cover plot open and
close tokens if that improves the fit, and activates a correction
mechanism that will make sure the document remains valid by
dropping changes or adding plot open/close tokens when necessary.
Wrapping a group of changes in a {correct: changes} object. That will first combine the given changes, and
then make sure the resulting change is valid. If you add a local true property to the object, the correction strategy will try to
keep its corrections as close to the changes as possible.
Mapping
Change sets support position mapping, which
adjusts a position in the old document to get the corresponding
position in the new document. This keeps the selection in the right
place as the document changes, and allows other positional data, such
as document decorations to be tracked across changes.
When content is inserted at the mapped position, whether you want
the new position to be before or after that content depends on the
kind of thing you are tracking. As second argument to mapPos allows
you to set an associativity for the mapping.
In some cases, you want to consider a position deleted and stop
tracking it when some content near it is deleted. The optional third
argument to mapPos allows you to provide a tracking mode,
instructing the method to return null instead of a new position when
either the element before, the element after, or the elements on both
sides of the position were deleted.
It is also possible to transform change
sets over each other. If you have two changes A and B that start from
the same document, you can transform B over A to create a version of B
that can be applied to the document created by A.
let a = ChangeSet.create(doc, {from: 1, insert: [Leaf.text("a")]})
let b = ChangeSet.create(doc, {from: 5, insert: [Leaf.text("b")]})
let b2 = b.transform(doc, a)
let doc2 = b2.apply(a.apply(doc))
It is possible for such a transformation to need to apply its own
corrections when the changes conflict in a way that would create an
invalid change. If you transform two changes across each other, the
library makes sure to apply the same correction in both so that if
you set the second argument (which determines the ordering of
conflicting inserts) to true in one and false in the other, the
result of applying them both after the other produces the same
document.
There's a shorthand function ChangeSet.transform to perform
this type of mutual transformation.
let {a: a2, b: b2} = ChangeSet.transform(doc, a, b)
assert(b2.apply(a.apply(doc)).eq(a2.apply(b.apply(doc))))
This functionality allows simple types of operational transformation.
It is also used when creating change sets or amending transactions to
move changes into the proper document coordinate system, and by the
undo history to combine undoable and non-undoable changes.
The Schema
A document schema lists a collection of node and mark
types that are allowed to appear in documents conforming to that
schema, and specifies their relations—which nodes may appear in which
plots, and which marks may appear on which tags. The document node
stores a reference to the schema, and ensures
that its content conforms to the schema on creation.
This means that your document (and editor) only ever contains content
elements that you explicitly allowed. Wordgard assumes you have a
specific set of constructs you want to allow, and defines your
document in terms of those constructs.
You configure your schema either by adding all its elements to an
editor configuration, and letting the editor state create it, or by
constructing it ahead of time, and passing in a document in that schema when creating
the state.
Plot Types
The elements that make up the schema are leaf, plot, and mark types.
The "wordgard/types" module provides a collection of basic schema
elements, but it is possible to define your own.
For example, this is what a definition for an "aside" plot might look
like:
const Aside = Plot.define("Aside", {
blockContent: Node.Group.Content,
group: Node.Group.Content,
shape: {element: "aside"}
})
Each node or mark type has a name string ("Aside" in this case),
which will be used in the document's JSON-serialized format, and its
toString output, which can be useful for debugging. These names must
be unique in a schema.
A plot needs to indicate what content it supports. In this case we
used the Content node group, which
indicates generic block-level content. Node groups are tags that can
be attached to nodes—you can see that we assign the same group to our
new plot—as a kind of category. The blockContent field accepts a node query, which
may be a group, a precise node type or tag, or a union or intersection
of other queries.
Each schema element must define an HTML/DOM shape. In this case, we simply want to wrap the plot's content in an
<aside> element, which we declare, and which allows the library to
derive both serialization and parsing logic for the plot. When there
are parameters to serialize, and DOM attributes to create and parse,
this needs to be a bit more elaborate. In some cases, you just want to
specify the format directly, and hand-write
the parse rules to match the element.
The Plot.define call above will create a plot
tag, not a type. Since this plot type takes no parameter, you'll
generally want to use it as a tag, rather than a type. To make this
convenient, the library allows you to pass in tags in many places
where it accepts types.
For plot types where you do need a parameter, you'd use Plot.Type.define instead of Plot.define, and get plot type as
result. Such type objects have an of method
that creates a tag from the type and a parameter.
This is what a plot type defining a speech bubble style with the name
of the character talking in the parameter might look like:
const SpeechBubble = Plot.Type.define<string>("SpeechBubble", {
inlineContent: true,
group: Node.Group.Content,
validate: "string",
defining: true,
shape: {
element: "speech-bubble",
attributes: param => ({"data-character": param}),
readElement: elt =>
elt.getAttribute("data-character") ?? parse.Reject
}
})
Even types defined with a parameter can specify a default parameter to use when there is no
explicit parameter available. Types defined with a default parameter
or as a singleton have a default property with their
default tag.
There's a number of other options available for plot types, such as
defining used above. I recommend looking
through the reference for Plot.Spec to learn about them.
Leaf Types
Leaves are defined in a very similar way. For example, this is how you
might define a dinosaur leaf type:
const Dinosaur = Leaf.Type.define<string>("Dinosaur", {
inline: true,
validate: "string",
shape: {
element: "wordgard-dinosaur",
attributes: name => ({"data-dino": name}),
readElement: elt => elt.getAttribute("data-dino")
},
selectable: true
})

Inline nodes, both plots and leaves, must set the inline property to true in their definition.
When a type has a parameter, it is recommended to define a validate property that either specifies the
valid type as a string, or provides a validation function. This helps
the JSON deserializer make sure that it's not creating nodes with
invalid parameters.
Because this leaf has a parameter, the shape defines both a way to
serialize it to an attribute, and a way to read it back from the
element when parsing.
The selectable flag tells the library that this leaf can be
selected. Cursoring through it will make the selection pause on the
node, and clicking it will select it.
Mark Types
Marks are defined in a way very similar to nodes, also providing both
Mark.define (for on/off things like Strong) and Mark.Type.define (for marks with parameters, like Link).
A mark shape may be either a wrapping element or an attribute. For
example, Strong wraps its target node in a <strong> element,
whereas ImageAlt sets its alt attribute. Such definitions
look like this:
export const Strong = Mark.define("Strong", {
rank: 60,
shape: {element: "strong"},
})
export const ImageAlt = Mark.Type.define<string>("ImageAlt", {
target: [Image, Figure, CaptionedFigure],
validate: "string",
shape: {attribute: "alt", value: 0, preferTarget: "img"}
})
Each mark can provide a rank to influence the
way it is ordered in mark sets relative to other
marks. This is important for marks that render a wrapping element
since it determines the nesting order of the elements when a single
node has multiple such marks.
An attribute shape that just stores the
parameter in the attribute as-is can indicate this with value: 0.
When a more involved transformation is necessary, it is again possible
to define custom functions to create and parse the attribute.
It is possible, in an attribute shape, to indicate a preferred target.
In the ImageAlt case, figures wrap the image in a <figure>
element, and images might also be configured to render additional
wrappers. But the alt attribute is meaningless when not on the
<img> element, so we need to target that, rather than the node's
outer element.
Another important property of a mark type is whether it is spanning. Some types of marks, such as image alt
text, refer to a specific node, and have nothing to do with the nodes
around that. Others, like strong emphasis, conceptually apply to a
stretch of content. When adjacent nodes all have the Strong
mark, the <strong> element should wrap them all, rather than each
individually. Text nodes can only have spanning marks, because they
are not really individual nodes in the sense of other nodes, but
rather stretches of characters that happen to be grouped because they
are adjacent to each other.
The definitions above don't have to explicitly set spanning because
it defaults to true for marks defined with an element shape, and false
for those with an attribute shape. But if you want to define a
non-spanning node with a wrapping attribute, you'll need to explicitly
set it.
There's some further fields in Mark.Spec that can configure a
mark's behavior. You can take a look whether they are useful for the
mark you're defining.
Schema Overrides
Plots types define their own content, and mark types define what nodes
they apply to. But since these attributes are about the way the
elements in a schema fit together, a specific schema may need to
adjust them. That's what schema overrides, the
last remaining type of schema element, are for.
You can use Schema.Override.plotContent to replace the content
query for a given plot, Schema.Override.markTarget to
configure what nodes a mark applies to, and Schema.Override.nodeGroup to change the set of groups a node belongs
to. Including the resulting objects in a schema configuration applies
them to that schema.
Such overrides are the reason that methods like matchNode, markAllowed, and canContain, which query these relations, exist on the schema,
rather than the node or mark type.
Editor State
The GardState class from "wordgard/state" implements the
object that holds the editor's state. Such objects are again
immutable, so a given state won't change, and you can compare a
previous state to a new state to see whether some aspect of it was
updated.
The main things the state tracks are the current document, the selection, and the editor
configuration. An editor has a current state that you access to get at these fields.
Creating an editor state can be done with GardState.create.
You provide it a starting document, an optional initial selection,
and a configuration, and it gives you a state. If the configuration
defines a schema, the document may be an HTML string or a piece of DOM
structure. If not, you have to pass in a document plot in order to
provide your schema.
let state = GardState.create({
doc: `<h1>Hi!</h1>`,
config: basicSchema()
})
To "reset" an editor state, for example when loading a new document
into your editor, you'll want to create a completely fresh state,
rather than trying to update your old state to the new document.
Transactions
Updating the state is done by creating a transaction, either via state.update or by
directly calling dispatch on an editor. Such a
transaction describes precisely what needs to be updated, and is made
available to any code that observes or handles editor changes, so that
such code has all the information it needs about the change.
The things stored in a selection are:
A set of document changes. Will hold
the empty set if the transaction makes no change.
An optional explicitly set selection.
If the selection isn't set, this holds null. newSelection will always hold the selection after the
transaction, whether explicitly set or mapped from the existing
selection.
A set of annotations, which are
metadata that can be attached to a transaction. Will include the
time at which the selection was created,
and often a user event tag.
A set of custom effects, which are
user-definable values that describe some additional effect the
transaction has, such as scrolling something into view, or opening
a dialog.
The new state created by the transaction is available in its state property.
Transactions are created with a Transaction.Spec object, which
allows you to provide these fields in a straightforward object literal.
let tr = state.update({
changes: {from: 10, insert: Image.of("bee.jpg")},
selection: {anchor: 11},
userEvent: "insert.bee",
scrollIntoView: true
})
The scrollIntoView field sets a flag on the transaction, telling the
editor to scroll the cursor into view after the transaction has been
dispatched. Most (but not all) editing actions will want to set it.
It is possible to register extensions that affect all transactions. A
transaction extender can change
individual transactions, which can be useful for adding metadata (like
annotations or effects) or fixing up undesired
document shapes. Transaction appenders
can inject additional transactions after a given transaction is
dispatched.
Selection
A Wordgard selection is an object that inherits from GardSelection. The "wordgard/state" module defines two types of
selections, and other code can define
custom types.
Each selection has an anchor (the
position at which its fixed point sits) and a head (the position of its movable side). The two
may be the same, for a cursor selection. There's also from/to properties to directly get the
lower or upper bound.
Text selections are the selection type used
for plain old selections. They may optionally store a set of marks to
apply to content inserted through that selection (which is used when
you, for example, toggle the emphasis mark while no text is selected).
Selection positions can be any point in the document, including
between blocks. Selections created by the library, whether for
keyboard cursor motion or pointer events or in response to changes,
tend to be 'normal' cursor selections, which are a subset of the
document positions, and include block positions only where they are
necessary to make some types of editing possible. See the nextNormalCursor method.
Node selections select a single selectable node. They have a node property that holds the node.
As an example of a custom selection the tables module defines a cell selection type that covers a rectangle of table
cells.
The editor state has a sel property that, for
convenience, holds a resolved version
of the selection, whose anchor/head/from/to
properties give you resolved positions.
Configuration
The "wordgard/state" module defines a versatile set of configuration
primitives.
The config field provided when
creating a state has the type GardState.Extension, which is
defined in the following, somewhat mysterious way:
type Extension = {extension: Extension} | Extension[]
The way to read this is that the library defines some built-in types
that count as extensions, via the object type, and an extension is a
single such predefined extension, a custom object with an extension in
its extension field, or a tree of arrays of extension values.
Being able to provide an extension property on an object type is
used throughout the system to make it possible to use things like
key bindings and input rules
directly in an editor configuration.
Since extensions often need a relative precedence to determine who
gets to override who, a configuration defines a full ordering of
extensions based on the position they had in the input extension tree,
plus their explicit precedence.
Explicit precedence is assigned by the functions in GardState.prec. Any extensions wrapped in a precedence get assigned
to that precedence, unless they override it with their own custom
precedence. In the final ordering, explicit precedence comes first,
and within a given explicit precedence, the order in the extension
tree determines relative precedence.
So if you have three key bindings for Ctrl-b in
your configuration tree, with the last one assigned to prec.high, that last one will get asked to
handle the key first, and if it declines, the other two are used in
order of appearance.
Extension modules often export their functionality as a
function—optionally taking some configuration object—which returns an
extension bundle that implements the functionality. Depending on the
type of extension and how well they can be used separately, it may
also be worthwhile to export the individual pieces separately, in case
the user wants to pick and match the precise extensions they need.
Facets
Because user code will often need the same primitives that library
code uses, Wordgard exposes ways to define your own extension points.
Such extension points are called facets, and
are also feature heavily in the library's own interface. They are
object that name the extension point, specify its type, allow code to
provide input values, and make it possible
to read the current value of the facet from a
state.
A facet has two type parameters: an input type
and an output type, where the output type defaults to an array of the
input type. When defining the facet you can provide your own combine function that takes an array
of inputs and computes an output value. A common pattern for
single-valued facets is to just take the highest-precedence input or,
if there are no inputs, a default value. Another useful pattern is to
have the function combine a set
of configuration objects into a single combined configuration.
Often facets only have static inputs, provided directly in the
configuration, and their output value is stable for a given
configuration. Such facets outputs are computed once, and kept for the
lifetime of the configuration. They occur no overhead during state
updates.
But it is also possible to define
dynamic inputs that depend on some other aspect of the state. Facets
with such inputs are recomputed whenever one of their inputs changes
(using a signal-like
mechanism for dependency tracking), but stay the same otherwise.
The library tries to keep facet output values stable as much as it
can, using comparison functions
to determine when outputs or inputs don't change. If a new output
compares equal to the old one, the old one is kept, so it is generally
safe to compare facet outputs by object identity.
Examples of facets are simple configuration options like GardState.readOnly, which determines whether an editor state is
read-only, collections like the facet that hold key bindings, and control facets like Panel.show and Decoration.Point.source that determine what
kind of UI elements are visible in the editor.
State Fields
Extensions may also define state fields. Like
dynamic facets, these live in any state for which they are configured,
and are updated as the state changes. Unlike facets, their update
happens with a reducer-like function, which is called with the field's old value and the
transaction on every update, and may opt to return the old value or a
changed value depending on the transaction.
Like anything that lives in the editor state, field content should be
immutable.
Fields are best used for persistent pieces of state. Examples of state
fields are the history tracked by the undo/redo
system, sets of document decorations that need to
persist across changes (you'll store them in a point
or range set and map them through changes), or
information like whether a given panel is currently open.
It is often useful to define state effects
to communicate from your transaction-dispatching code to your state
field update function.
Facets provide a convenience method for
registering a dynamic facet that uses a field's value.
Dynamic Configuration
While it is entirely possible for an editor configuration to be set at
the time where the editor is initialized and stay the same forever
after, you sometimes want to change your configuration without
throwing away your editor state. For example to load additional
extensions or to disable some feature.
There are three ways this can be done. GardState.reconfigure
is an effect type that will replace your entire configuration with a
new one. Then, GardState.appendConfig is an effect that will
add additional extensions at the end of your old configuration. This
can be useful for extensions that need to "inject" some configuration
when they are first activated. And finally, for fine-grained
reconfiguration, you use compartments.
A compartment tags part of your
initial configuration, and provides a way to replace exactly that
part. It may start empty, or later be emptied to remove some
extensions.
The way you use these is that you first define your own compartment, and include
that, with some extension in it, in your configuration using compartment.of(...). You can then
dispatch a transaction with the effect created by compartment.reconfigure(...) to
reconfigure your compartment.
When a configuration change changes the document schema, the library
will try to apply the new schema to the old document. You have to make
sure this is possible (the document contains no nodes or structure
that isn't valid in the new schema), or the library will raise an
exception.
Editor Component
All those foundations now finally allows us to get to the actual
editor. The Wordgard class from "wordgard/editor" is
responsible for rendering the document as a piece of editable browser
DOM, wiring up browser editing events to editing actions, and handling
editor state updates.
You create an editor with the Wordgard.create function. It
takes a few configuration parameters—such as an optional parent element to append the editor to, and an
editor state. For convenience, it is allowed to inline the options to
GardState.create directly into the object passed to Wordgard.create, and it will create the state for you.
const editor = Wordgard.create({
doc: `<p>Let's go</p>`,
config: [basicSchema(), history()],
parent: document.body
})
It is never a good idea to directly manipulate the editable content.
If you need to change the document, go through the transaction system.
If you want to display something in the document, use the
decoration system.
Updates
The most important method on a Wordgard editor is dispatch. You pass it a transaction or transaction spec, and it will apply that transaction
to its state and then update itself to reflect the new state.
Updates are only partially synchronous. The state property will immediately reflect the updated state. But the
actual DOM representation of the editor is only updated on the next
"flush", which is scheduled with
requestAnimationFrame
(and only if the editor is actually in the document). So a bunch of
rapid-fire transactions will not cause a sequence of unnecessary full
updates, but rather will be batched and processed together at the next
flush.
Regardless of this optimization, try to dispatch a single coherent
transaction, rather than a bunch of separate ones, whenever possible.
There is a updateListener hook that
is called after every update, which can be useful to have imperative
code listen in on editor activity.
Plugins
Editor plugins allow you to put a stateful
object in the editor component and have it get notified whenever the
editor updates. This is mostly useful for implementing extensions that
need to integrate closely with the DOM—for example, the built-in
tooltip and panel features are built with editor plugins.
You can use Wordgard.Plugin.define (or Wordgard.Plugin.fromClass) to define a plugin. When the editor state
has such a plugin in its configuration, the plugin is initialized on
editor creation and notified of anything that happens in the editor.
Its update method will be
called when the editor is flushed, right before the editor updates its
document. This is the point where the plugin may update any DOM
structure it manages.
To avoid layout thrashing (when the browser needs to repeatedly
recompute its layout because code alternates between reading layout
information and modifying the DOM), plugins that need access to the
document layout to update themselves should use the scheduleDOMRead and scheduleDOMWrite
methods.
A plugin update that, for example, needs to measure the size of a
tooltip should call scheduleDOMRead to do the reading, and if it
concludes that it needs to make more changes to the DOM, call
scheduleDOMWrite. When scheduled during a flush, scheduled reads and
writes will be immediately handled by the current flush, but grouped
by access type, to minimize the amount DOM layout computations.
DOM Queries
A Wordgard instance provides a number of methods for querying
DOM structure and layout. You can find out which document node
corresponds to a given DOM element with nodeFromDOM,
or go the other way with nodeDOM.
Again, though you can look DOM structure, that is only intended for
doing things like getting its client rectangles or comparing it to
some other element, not for manipulating the document.
You can find out where on the screen a given position is with coordsAtPos or figure out which position is under a set of
coordinates with posAtCoords. coordsForElement gives you the rectangle where the node or
character at the given position is displayed.
To compute layout-dependent cursor motion, there is a moveVertically method, that takes a selection and moves it
up or down. moveToLineBoundary tells you where the
line that a given selection is in starts or ends.
All such methods that require access to the DOM will force a flush
when they are called at a time where the editor isn't flushed. This is
generally not an issue, but it is something to keep in mind.
Styling
Wordgard uses a CSS-in-JS system to manage CSS rules. It is also
possible to style it with regular style sheets, but because both the
core editor and extensions need to be able to bundle their styles and
have them loaded automatically on demand in the right root without
burdening the user, the easiest way to define them is in your scripts.
To define a set of styles for your extension, you use Wordgard.styles, which returns an extension that causes the rules to
be loaded.
const myStyles = Wordgard.styles({
".my-button": { borderRadius: "5px" },
"&dark .my-button": { color: "white" },
"&light .my-button": { color: "black" }
})
The properties of the object passed to this function are selectors,
and the properties of the nested objects are CSS properties (with
support for camel-case). See the styling
example for a more detailed description of the
notation. You can use the &dark/&light selectors to define rules
that should only apply with a dark or light color scheme. These will
be replaced by generated class names that are present on the editor's
outer element when using that color scheme.
Rules without &dark or &light selectors get prefixed with another
generated class, to make sure they don't leak out of the editor. If
you need to refer directly to the editor's outer element (which is the
element that will have this class), you should use & in your
selector to refer to it, so that the added class selector ends up in
the right place.
A similar function Wordgard.theme exists to define a set of
styles that can optionally be added to a specific editor. Contrary to
plain styles which all share a scope class,
regular themes get their own scope class, so that the rules they
define won't affect editors in which they are not active.
The editor's structure looks something like this:
<wordgard-editor class="ͼ1 ͼ2">
<wg-panels class="wg-panels-top">
<wg-menubar role="toolbar" class="wg-panel">
</wg-menubar>
</wg-panels>
<wg-scroller>
<wg-content contenteditable="true" role="textbox">
<p></p>
</wg-content>
<wg-cursor-layer>
<wg-cursor class="wg-cursor-v"></wg-cursor>
</wg-cursor-layer>
</wg-scroller>
</wordgard-editor>
The editor has a wrapper element, which holds the generated prefix
classes. It is a column-direction flexbox that extensions can add
elements to. In the example above, the menu bar added
itself, via the panel system, to the top of the editor.
Then there's a scroller element. By default this doesn't actually
scroll, but it's what you can target with styles setting a height and
overflow if you want your editor to scroll.
Inside of that you have the actual editable element with the rendered
document. Because the editor draws its own cursor, the document is
overlaid by a <cursor-layer> element into which that cursor is
positioned.

Decorations
Wordgard provides several ways for extensions to influence the way the
document gets drawn. These are called “decorations”—they decorate the
rendered document, without changing the document data structure
itself.
The simplest form of decorations are “tag” decorations, which target a
specific node type, replacing its shape,
wrapping it in additional elements, or
drawing widgets next to it or at its
start or end. These work as extensions that, when present in an
editor's configuration, affect all nodes of the targeted type.
In situations where you just want to modify selected nodes or ranges
of content, you have to use a point or range set that places your decorations at a specific part of
the document.
These data structures associate a specific type of decoration (point or range decorations)
with a position or range in the document. You define an extension that
provides them to the editor component via a source facet, which causes the editor to use
them when drawing the document.
let blueNode = Decoration.Point.attributes({
style: "background: lightblue"
})
let source = Decoration.Point.source.of(state =>
PointSet.create([[0, blueNode]]))
That decoration turns the node at the start of the document blue. Of
course, a set that targets a static position in a changing document is
generally not of much use, so you'll usually want to store these sets
in a state field and use their map method to make them move along with document
changes in the field's update function. For small, cheap-to-compute
sets of decorations, it can also be workable to just generate them
every time the source function is called.
Point decorations can, apart from adding
attributes, place widgets in the
document, override the shape of a
specific node, or wrap it.
Range decorations can add an attribute or wrapper to a number of nodes in a range.
Like spanning marks, such wrappers may span multiple nodes.
Commands
When the editor component captures a
beforeinput
event with an input type of "insertLineBreak", it will dispatch the
insertLineBreak command. Many other native editing actions
also correspond to commands defined in
"wordgard/command".
Commands are also used in key bindings and menu buttons to specify the action to take for the key or
button. Extension modules that define additional editing functionality
will often export their own commands that implement user actions
related to that functionality.
A command is a function that takes an editor and a
parameter, and either performs the command as a side effect, returns
it as a transaction spec, or returns false
to indicate that it doesn't apply to the current editor state.
Though commands are just functions, they also act as tags that
identify some generic editing action. Extensions can provide
additional handlers for a given command which
will be run (in order of precedence) when the command is invoked with
Command.dispatch. This makes it possible to override a given
editing command, either in general or in specific situations, by
configuring your own custom handler.
For example, this handler overrides the enter command
(dispatched when enter is pressed) to, when in a paragraph with the
text "Abracadabra", to change the word to "Alakazam" instead of
splitting the paragraph.
const magicEnter = Command.handler(enter, wg => {
let block = wg.state.sel.head.textblockParent
if (!block || block.node.textContent() != "Abracadabra")
return false
return {
changes: {from: block.start, to: block.end,
insert: [Leaf.text("Alakazam")]},
scrollIntoView: true
}
})
A command that takes a parameter can be bound,
producing an object that can, just like a parameter-less command, be
passed to Command.dispatch to run the command with that
parameter.
Corrections
The way permitted plot content can be described in Wordgard is rather
simple—you provide a set of allowed
nodes and say whether the plot may be
empty. Those constraints are enforced on plot creation and change
application.
But sometimes you want to enforce more complicated invariants, such as
that tables must be rectangular (each row has the same number of
columns) or that sections must start with a heading. Wordgard
intentionally does not try to solve this at the content model level,
because experience with ProseMirror has
shown that...
writing document manipulation code in a way that never violates
complicated schema constraints is very hard.
such constraints often get in the way of multi-step editing actions
that the user is trying to perform, by disallowing the intermediate
document shape.
the automatic enforcing of such constraints can produce unexpected
and undesired document shapes.
For these reasons Wordgard intentionally defines a simpler, looser
document model. In cases where you do want to enforce additional
constraints, corrections are the abstraction to
support this. They allow you to register observer functions that look
at nodes of a given type every time they change or are introduced, and
optionally return an extension to the transaction to "fix" the node.
Custom fixing code can usually be smart about respecting the thing the
user may be trying to do to avoid being destructive or surprising
about it—though for some types of constraints this does require some
care.
This is an example of a constraint that makes sure the first block in
the document is a level 1 heading:
let ensureHeading = Correction.onChildList(Doc, ({node}) => {
let first = node.content[0].tag
if (first.is(Heading) && first.param == 1) return null
return {from: 0, insert: [Heading.of(1).create()]}
})
Such a correction will run whenever the child list of the Doc
node changes. There's also Correction.onContent, which will
run even if some content deep inside the node changes, and Correction.onMarks, which responds to mark set changes.
If you aren't sure that your starting document conforms to your
corrections, you can run a correction globally with its scan method. This returns a transaction when it
finds any necessary changes.
You'll often want to display some kind of buttons on an editor to
provide the user an obvious way to perform the editing actions that
the system supports.
Extensions may contribute their own menu items, so it is practical to
allow them to automatically insert menu content without further
configuration work. But in some situations, you want to control
precisely what appears in the menu. Wordgard's menu system tries to
bridge this tension.
Another question is how the menu items should be rendered. Some setups
will want their own custom display, integrated with the UI framework
they are using. Others are happy to use the built-in system. The
Wordgard menu definition system tries to support both.
Items
The menu model is made up of menu items, which can
be individual buttons or structuring elements like submenus or named
groups of items. Each item can declare a default parent. These items
can be added to a configuration, where they'll add themselves to the
Menu.Item.source facet.
When resolving your menu structure, you can
either just take whatever items are available in the top group, or provide your own Menu.Template, which either specifies the entire structure
explicitly or leaves some of the inner structure open to be
constructed by resolving parent links.
A menu button, at the minimum, defines what it looks like, what it does, and where it belongs. This very useless
button pops up an alert when you press it:
const myButton = Menu.Button.define({
label: "Press Me",
run: wg => { alert("!!"); return true },
parent: Menu.Group.commands,
rank: 99
})
The commands group is a top-level item
group that also holds things like the undo/redo buttons. An item's
rank determines where it sits relative to
other items in the same group.
Most buttons, at least at the top level, use an icon label instead of a text string. These are defined using SVG
path
strings, scaled to fit within a 100-by-100 picture. When using a
textual label, it is recommended to use a translatable phrase. When using an icon, you'll want to also define a
textual description, so that the
button is screen-reader accessible.
Submenus are defined in a way similar to buttons, but instead of
providing a run function, they act as a parent to some other items
(possibly further submenus), and pop up this list of inner items when
they are clicked or activated.
Both buttons and submenus can choose to hide or disable themselves by providing
a function that determines the item's status from the editor state.
Buttons can also be “active”, which
highlights them. This is used by, for example, inline mark buttons
whose mark is active at the cursor position.
A submenu that doesn't provide its own label can display its first
active child as its label, which can make it act like a drop-down
menu.
Menu groups just group a set of items, and make it
easier to organize bigger menus (like the top-level menu) or factor
out some parts into a smaller menu (such as a selection tooltip menu).
The library defines a few: the top-level menu,
a commands group for generic commands,
inline and block
groups for inline-level and block-level document manipulation, and an
insert group for buttons that insert various
types of nodes.
Finally, custom controls are a special kind
of button-like item that defers the way it looks (and responds to key
input) to custom code. It can be useful for including elements in the
menu that are a bit more complex than a plain button, such as for
example a color picker.
So when adding a menu to your editor, for example with menuBar, you can either just let it default to using the top group
and assembling a menu from all the items in the configuration, or
explicitly create a template by calling the template method on
groups and submenus. This simple menu just shows the undo buttons and
the block style dropdown containing whatever items the menu resolver finds for it.
import {Menu} from "wordgard/command"
import {undoButton, redoButton} from "wordgard/history"
const myMenu = Menu.Group.top.template(
undoButton,
redoButton,
Menu.Submenu.textblockStyle.template("..."))
In a menu template, the "..." marker tells the resolver to put any
items for that group of submenu that it finds in place of the marker.
Items that are specified explicitly in the template will not be
included again in this way. This kind of partial template allows you
to control the rough shape of a menu without explicitly listing every
single item.
Implementation
The menuBar extension from "wordgard/editor" implements a
straightforward, framework-less, keyboard accessible menu bar,
displayed as a panel at the top of the editor.
A custom menu, in order to be able to use the menu information
provided by existing editor extensions, would use the same items, but
display them in its own way (maybe in a tooltip, maybe as a React
component, whatever fits your situation). It should take an optional
menu template, and resolve that with the items
available via the menu item source facet.
When implementing your own menu, take care to make it accessible to
keyboard-only and screen-reader users.