Skip to content

RkfEdit — the edit API ​

RkfEdit is the headless edit API of @wity-ai/rkf-core: the operations on an RKF document, pure over the document model (the object parse() returns and serialize() takes).

js
import { RkfEdit, splitBlocksByHeading } from '@wity-ai/rkf-core';

Every function changes the document (or card, or machine) it is given in place and returns what a caller needs to record or report — the created element, the removed element, the previous position. Nothing here knows about undo history, events, selection, rendering or playback; those belong to the caller.

Addressing ​

ThingAddressed by
Sectionid
Card or other child of a sectionindex within its section, or its stable _uid (findChildIndex)
Link between cards (connector)_uid
Block inside a cardid — stable, unique within its card
Drawing markid — stable, unique in the document
Machineid — unique among the document's machines
Machine var / gate / deriveid — its name in expressions, unique among the three
Machine transform / porta generated id

normalizeDoc(doc) brings a loaded document to this shape: every section gets a viewport; every child a _uid and a pos; legacy single-content cards a blocks array; every block, mark, transform and port an id; rows the canonical cell shape. It is idempotent and never changes content. An embed's or a machine's existing id is never changed.

Document and sections ​

createDoc() → doc                                   // one 'main' section
normalizeDoc(doc) → doc
findSection(doc, sectionId) → section | null
addSection(doc, label = 'New Section') → section
renameSection(doc, sectionId, label) → section | null    // throws on an empty name
removeSection(doc, sectionId) → section | null
setDecisionSpace(doc, xml) → doc                    // an opaque <wity-knowledge> string; a falsy value clears it

Cards and other children ​

getChild(doc, sectionId, childIndex) → child | null
findChildIndex(doc, sectionId, childUid) → index | -1
nextBlockPos(doc, sectionId) → { x, y, w }          // below the lowest existing card
addCard(doc, sectionId, { content?, blocks?, pos? }) → { child, childIndex } | null
addMachine(doc, sectionId) → { child, childIndex } | null
insertChild(doc, sectionId, childIndex, child) → boolean
removeChild(doc, sectionId, childIndex) → { child, connectors } | null   // links to/from it are removed too
moveChild(doc, sectionId, childIndex, { x, y }) → previous pos | null | undefined (not found)
resizeChild(doc, sectionId, childIndex, width) → { before, after } | null
setChildBlocks(doc, sectionId, childIndex, blocks) → previous blocks | null
setChildContent(doc, sectionId, childIndex, content) → boolean           // the legacy single `content` field
splitCard(doc, sectionId, childUid) → { removed: { child, childIndex, connectors }, added: [{ child, childIndex }] } | null

addCard takes blocks (the card's block list) or content (one text block). A card's width is at least MIN_BLOCK_WIDTH (120); the default is DEFAULT_BLOCK_WIDTH (320).

splitCard groups a card's blocks by heading (splitBlocksByHeading): at the shallowest heading level that occurs at least twice, with content before the first such heading folded into the first group. It returns null when there is nothing to split on.

Card style ​

CARD_STYLE_KEYS = ['borderColor', 'borderWidth', 'backgroundColor']
checkCardStyle(attrs) → attrs                       // throws on an unknown key or a wrong value
setChildStyle(doc, sectionId, childUid, attrs) → child | null

Colours are hex (#3b82f6); borderWidth is a number from 1 to 4. null or '' clears a key; a card whose style becomes empty has no style.

getConnectors(doc, sectionId) → connector[]
addConnector(doc, sectionId, { from, fromPort, to, toPort, color?, width?, data? }) → connector | null
updateConnector(doc, sectionId, connectorUid, attrs) → connector | null
removeConnector(doc, sectionId, connectorUid) → connector | null

from / to are card _uids. data is a free-form property bag.

Blocks inside a card ​

These take the container whose blocks list is edited — a card, or a row cell.

newBlock(kind, opts?) → block                       // opts: heading, list (text) · application (embed) · columns (row)
findBlock(container, blockId) → { list, index, block, row, cellIndex } | null     // looks inside rows
insertBlock(container, block, where?) → { block, index }
updateBlock(container, blockId, patch) → { block, before }
removeBlock(container, blockId) → { block, index, row, cellIndex }
moveBlock(container, blockId, where?) → { block, index }
splitTextBlock(container, blockId, { before, after }) → { block, created }
mergeIntoPrevious(container, blockId) → { into, removed, offset }
setBlocks(container, blocks) → previous list
walkBlocks(container) → iterator of { block, row, cellIndex }
normalizeBlocks(blocks) → blocks

where is one of { index }, { afterId }, { beforeId }, { rowId, cell, index? }; omitted means the end.

  • insertBlock copies the block, gives it an id if it has none (or a fresh one if its id is already used in the container).
  • updateBlock: undefined or null in the patch removes a field. kind, id, cells and blocks cannot be changed by an update.
  • splitTextBlock on an empty list item ends the list instead of adding a block.
  • These throw when the block (or row) does not exist.

Block kinds (BLOCK_KINDS): text, image, math, embed, machine, mermaid, row.

Rows ​

A row is a container:

js
{ kind: 'row', id, columns, cells: [ { blocks: [ … ] }, … ] }

Each cell holds its own ordered blocks. A cell cannot hold a row, embed or machine block, and is never left empty — removing its last block leaves an empty text block. The earlier shape, { kind: 'row', columns, blocks: [ … ] } (one block per cell), is read and upgraded without losing anything (normalizeRow).

setRowColumns(container, rowId, columns) → { row, columns, cells }
removeRowCell(container, rowId, cellIndex) → { row, cell, columns }
normalizeRow(row) → row
MAX_ROW_COLUMNS = 8

setRowColumns: growing pads empty cells; shrinking drops only trailing empty cells — content is never discarded. removeRowCell removes a cell with its content; a row keeps at least one cell.

Drawing marks ​

The shapes drawn on a section's canvas (its rkf-canvas child). Coordinates are in the section's canvas space, as card positions are.

ShapeRequiredOptional
linex1, y1, x2, y2color, width, dash
arrowx1, y1, x2, y2color, width
rectx, y, width, heightstroke, strokeWidth, fill, rx
circlecx, cy, rstroke, strokeWidth, fill
pathd (SVG path data)stroke, strokeWidth, fill
textcontent, x, ycolor, fontSize, fontWeight, anchor

Every shape also takes opacity (0–1). Colours are hex; fill may be 'none'.

MARK_SHAPES                                         // shape → { tag, required, optional }
newMark(shape, attrs) → mark                        // with an id; throws on a missing or wrong attribute
markShape(mark) → shape | null
findMark(doc, markId) → { section, index, mark } | null
setMarkAttrs(doc, markId, attrs) → { section, index, mark } | null   // null or '' removes an optional attribute
normalizeMarks(doc) → doc

getDrawingCanvas(doc, sectionId) → canvas | null    // created on first use
getMarks(doc, sectionId) → mark[]
addMark(doc, sectionId, mark) → boolean
updateMark(doc, sectionId, markIndex, attrs) → mark | null
removeMark(doc, sectionId, markIndex) → mark | null

Machines ​

A machine is a block inside a card (kind: 'machine') — always a top-level block of its card.

evaluate() and RkfPlayer run the machines that are children of a section. They do not look inside cards, so a caller that wants machine blocks to run places them among the section's children in the document it evaluates (leaving the stored document as it is).

PartShape
var{ id, type, init, elasticity?, elasticDefault? } — type: number | boolean | string
gate{ id, expr }
derive{ id, expr }
transform{ id, trigger, on?, sets: [{ var, expr, when? }] }
port{ id, dir, var, label?, type?, … }
listMachines(doc) → [{ section, child, machine }]
findMachine(doc, machineId) → { section, child, machine } | null
renameMachine(doc, machineId, newId) → { section, child, machine } | null
findMachinePart(machine, part, id) → { index, item } | null
setMachinePart(machine, part, { id, newId?, ...fields }) → { item, index, created, unknownRefs }
removeMachinePart(machine, part, id) → { item, usedBy } | null
normalizeMachine(machine) → machine

setMachinePart adds a part or changes one; only the fields given change. For a var / gate / derive, id is its name — a new name adds one, and newId renames it. For a transform / port, id is the generated id of an existing one — leave it out to add one. Nothing changes when a rule is broken; the error says which:

  • names are identifiers (letters, digits, _; not true, false or Math), and unique among a machine's vars, gates and derives;
  • expressions must parse;
  • derives and gates must not depend on each other in a circle;
  • an input port's settings must fit its type — continuous: min, max, step · choice: options, labels · boolean · clock: interval · increment: by. An output port takes only var and label.

An expression may refer to names outside its machine, so an unknown name is not refused: it is reported back as unknownRefs. removeMachinePart leaves whatever still refers to a removed var / gate / derive as it is, and lists it in usedBy.

Example ​

js
import { RkfEdit, serialize } from '@wity-ai/rkf-core';

const doc = RkfEdit.createDoc();
const { child: card } = RkfEdit.addCard(doc, 'main', { content: 'Hello' });

const { block: row } = RkfEdit.insertBlock(card, RkfEdit.newBlock('row', { columns: 2 }));
RkfEdit.insertBlock(card, RkfEdit.newBlock('mermaid'), { afterId: row.id });

const { block: machine } = RkfEdit.insertBlock(card, RkfEdit.newBlock('machine'));
RkfEdit.setMachinePart(machine, 'var', { id: 'x', init: 2 });
RkfEdit.setMachinePart(machine, 'derive', { id: 'y', expr: 'x * x' });

RkfEdit.addMark(doc, 'main', RkfEdit.newMark('rect', { x: 0, y: 0, width: 10, height: 5 }));
RkfEdit.setChildStyle(doc, 'main', card._uid, RkfEdit.checkCardStyle({ borderColor: '#3b82f6', borderWidth: 2 }));

const xml = serialize(doc);   // requires the graph module — see rkf-core Setup