Whole-document questions a host asks before it hands a document to the editor.
expressible/3 asks whether every block in a document sits at a place
the editor, mounted with a given palette, would itself have let an author
put it, and whether every slot the document still has to fill is one that
palette can fill. A host installing a document it did not author through
the editor - a stored master, a document built in code, one migrated from
an older palette - asks it once, and learns whether an author can go on
editing that document with the palette they will be handed.
expressible?/3 asks the same question and answers a boolean, for a
host that only needs the yes or no.
The same predicate the editor uses, asked at each block's own position
StatifierBlocks.Edit.Targets answers "may this block go here" for a gap
an author is about to drop into, with four rules (ADR-0005 decision 5):
the slot is declared, assignability accepts the block, the slot has room,
and the slot is not inside the block's own subtree. This module asks the
first three of those rules about a block that is already in the
document, at its own {parent_id, slot, index}. The fourth cannot fail
for a block already in a tree.
Rule 2 is kind admission only: whether the block's kinds are ones its
slot accepts (ADR-0003 decision 3). Its findings are read from
StatifierBlocks.Assignability.validate/3, which checks every block at
its own position once, and only its :kind_not_admitted findings are
reasons here.
A :type_mismatch - a read the environment at the block's position
contradicts - is not a reason. It is a validation finding, and the editor
does not refuse a drop for one: a slot is offered to a drag when any of its
gaps accepts the block, a drop at another gap is committed and flagged,
and the editor never blocks an edit for a validation reason (ADR-0005
decision 5). A document holding such a read is one an author could have
built in the editor, and can go on editing there.
Reasons
expressible/3 answers :ok, or {:no, reasons} with one reason per
thing the editor would refuse, in StatifierBlocks.Document.blocks/1
pre-order and, within one block, in the order the table lists them. Every
reason names the block an author has to look at second and the rule first:
| Reason | The rule |
|---|---|
{:unresolved, block_id, error} | the block's type does not resolve through the palette; error is StatifierBlocks.Palette.resolve/2's |
{:slot_not_declared, block_id, {parent_id, slot}} | the block sits in a slot its parent's type does not declare (rule 1) |
{:no_room, block_id, {parent_id, slot, index}} | the block sits past the first child of an :exactly_one or :zero_or_one slot (rule 3) |
{:not_admitted, block_id, finding} | the block's slot does not admit its kinds (rule 2); finding is the :kind_not_admitted member of StatifierBlocks.Assignability.finding/0 |
{:unfillable_slot, block_id, slot} | the block's :exactly_one or :at_least_one slot is empty, and no block type in the palette is admitted there |
A block whose parent does not resolve is not checked against rules 1 and
3 - there is no declared slot set to check against - and the parent's own
:unresolved reason is the one an author acts on.
What it does not ask
- Recipes.
:unfillable_slotasks the palette's block types, throughStatifierBlocks.Edit.Targets.accepted_types_at/5, not its recipes. - Profiles. A mount's
profilecan hide palette groups from an author; this function sees the palette it is handed. A host that mounts the editor with a narrowed palette passes that palette here. - Config validity, compile findings, or slot arity beyond room. An optional slot left empty, or a required slot left empty that the palette can fill, is something an author can finish in the editor.
Summary
Functions
:ok when every block in document sits where the editor, mounted with
palette, admits it, and every required slot that is still empty is one
some block type in palette could fill; otherwise {:no, reasons}.
true exactly when expressible/3 answers :ok for the same
document, palette and ctx; false when it answers {:no, reasons}.
Types
@type reason() :: {:unresolved, StatifierBlocks.Block.id(), term()} | {:slot_not_declared, StatifierBlocks.Block.id(), {StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name()}} | {:no_room, StatifierBlocks.Block.id(), StatifierBlocks.Assignability.target()} | {:not_admitted, StatifierBlocks.Block.id(), StatifierBlocks.Assignability.finding()} | {:unfillable_slot, StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name()}
Why a document is not expressible through a palette. See the moduledoc's table.
Functions
@spec expressible( StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), StatifierBlocks.Assignability.context() ) :: :ok | {:no, [reason()]}
:ok when every block in document sits where the editor, mounted with
palette, admits it, and every required slot that is still empty is one
some block type in palette could fill; otherwise {:no, reasons}.
ctx is the StatifierBlocks.Assignability.context/0 the assignability
check is asked with, defaulting to %{} the way
StatifierBlocks.Edit.Targets.admits_at?/5 defaults it. A host that
mounts the editor with a datamodel document passes the same context here
(StatifierBlocks.Assignability.context/1 builds it), so the two answer
the same question.
Examples
iex> alias StatifierBlocks.{Block, Document, Palette, Plan}
iex> wait = Block.new("core.wait", id: "blk_WAIT", config: %{"duration" => "48h"})
iex> root = Block.new("core.sequence", id: "blk_ROOT", slots: %{"body" => [wait]})
iex> Plan.expressible(Document.new(root, id: "doc_1"), Palette.core())
:ok
iex> Plan.expressible(Document.new(root, id: "doc_1"), Palette.new(%{"core.sequence" => StatifierBlocks.Core.Sequence}))
{:no, [{:unresolved, "blk_WAIT", {:unknown_block_type, "core.wait"}}]}
@spec expressible?( StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), StatifierBlocks.Assignability.context() ) :: boolean()
true exactly when expressible/3 answers :ok for the same
document, palette and ctx; false when it answers {:no, reasons}.
The reasons are dropped. A host that shows an author why a document
cannot be edited with a palette asks expressible/3 instead.
Examples
iex> alias StatifierBlocks.{Block, Document, Palette, Plan}
iex> wait = Block.new("core.wait", id: "blk_WAIT", config: %{"duration" => "48h"})
iex> root = Block.new("core.sequence", id: "blk_ROOT", slots: %{"body" => [wait]})
iex> Plan.expressible?(Document.new(root, id: "doc_1"), Palette.core())
true
iex> Plan.expressible?(Document.new(root, id: "doc_1"), Palette.new(%{"core.sequence" => StatifierBlocks.Core.Sequence}))
false