editor

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package blocks defines the built-in `editor_blocks` dataset and its per-record CRDT handler. Each record is one block of an object's body — paragraph, heading, list item, code, etc. — atomic at the field level so concurrent edits to two different blocks (or to two different fields of the same block) converge without losing either.

Wire / storage shape of one block record:

{
  "id":     "<base58>" | "<base58>:<N>",  // auto-derived from
                                          // ChangeId; SDK suffixes
                                          // `:N` for the Nth (N>=1)
                                          // record in a multi-record
                                          // change. Markdown PUT
                                          // batches inserts, so any
                                          // 2+-block insert produces
                                          // these suffixed ids.
  "_ver":   { "id": "<VersionId of creating change>", ... },  // SDK-managed
  "type":   "paragraph" | "heading" | "list_item" |
            "check_list_item" | "code" | "quote" |
            "divider" | "html" | "table" | "image",
  "style":  { "level": 1..6,         // heading
              "ordered": true|false, // list_item
              "number":  10,         // ordered list_item; omitted when 1
              "checked": true|false, // check_list_item
              "lang":    "go" },     // code
  "text":   "**bold** inline markdown",   // INLINE only, no block syntax
  "nav":    { "parentId": "<blockId>"|"",
              "pos":      "<lexid>" }
}

Concurrency model: per-field LWW. Two clients touching the same `text` race; per-block atomicity means `$set text` and `$set style.level` from different writers converge under standard CRDT LWW with no merge logic required. Char-grain text CRDT is explicit out of scope for v1.

`text` is INLINE markdown only — bold, italic, inline code, links, strikethrough. Block-level syntax (heading hashes, list bullets, fences, quote `>` prefixes) lives in `type` + `style` instead so the UI can render blocks structurally without re-parsing. The markdown package re-renders blocks → markdown by prepending the type-specific prefix.

Index

Constants

View Source
const (
	FieldType  = "type"
	FieldStyle = "style"
	FieldText  = "text"
	FieldNav   = "nav"
)

Field keys on a block record. Literal strings — no content-addressable propIds — to keep registration hand-readable, matching the chat / markdown / nav patterns.

View Source
const (
	StyleLevel   = "level"   // heading
	StyleOrdered = "ordered" // list_item
	StyleNumber  = "number"  // ordered list_item: its number, omitted when 1
	StyleChecked = "checked" // check_list_item
	StyleLang    = "lang"    // code
)

Style sub-keys (under FieldStyle).

View Source
const (
	NavParentId = "parentId"
	NavPos      = "pos"
)

Nav sub-keys (under FieldNav). Local to this dataset — NOT the same namespace as the `nav` virtual built-in used for the object tree.

View Source
const (
	MaxTextBytes     = 64 * 1024 // ~64 KiB per block; larger than chat MaxTextBytes by intent
	MaxTypeBytes     = 64        // "check_list_item" is the longest known value
	MaxLangBytes     = 64
	MaxParentIdBytes = 256
	MaxPosBytes      = 256
)

Validation limits. Conservative; revisit if real usage hits them.

View Source
const (
	TypeParagraph     = "paragraph"
	TypeHeading       = "heading"
	TypeListItem      = "list_item"
	TypeCheckListItem = "check_list_item"
	TypeCode          = "code"
	TypeQuote         = "quote"
	TypeDivider       = "divider"
	TypeHTML          = "html"
	TypeTable         = "table"
	TypeImage         = "image"
)

Known block-type values. The handler accepts any non-empty string (forward-compat for clients introducing new block kinds), so this list is informational. Helpers in api.go and markdown.go switch on these values explicitly.

View Source
const Dataset = "editor_blocks"

Dataset is the per-object dataset that holds the block records.

View Source
const Module = "editor"

Module is the module slug a type names in a part's dataset declaration (`{"module": "editor"}`). The SDK instantiates the block handler once for the shared canonical collection (Dataset) and once per namespaced `<typeId>_<key>` instance a type declares.

View Source
const RootParentId = ""

RootParentId is the sentinel for "no parent — top-level block". Empty string keeps queries simple: `{"nav.parentId": ""}` lists the document's top-level blocks.

Variables

View Source
var ErrNotFound = errors.New("blocks: block not found")

ErrNotFound signals that a referenced blockId does not exist on the object's editor_blocks dataset. Distinct from space.ErrNotFound so callers can map cleanly to 404 blocks.not_found.

Functions

func AllocateRun

func AllocateRun(prev, next string, n int) ([]string, error)

AllocateRun returns n lexids that sort strictly between prev and next, in ascending order. Empty prev means "before everything"; empty next means "after everything"; both empty seeds from Middle(). Used by the markdown bulk-rewrite path.

func BlockLinks(spaceId, objectId, collection string, b Block) []index.LinkEntry

BlockLinks extracts one block's edges.

func Create

func Create(ctx context.Context, sp space.Space, objectId, collection string, in CreateInput) (space.ModifyResult, error)

Create issues one upsert with an empty record id so the SDK derives a stable id from the change CID (base58(xxh3-64(ChangeId))). Returns the raw space.ModifyResult — recordIds[0] is the derived block id, versionId correlates the write with the live event. Read the block back through /query with dataset=editor_blocks.

If in.Pos is empty, the server reads the parent's current max pos and allocates the next lexid past it. Concurrent inserts may collide on the same pos — that's OK for sibling ordering; the lexid alphabet has enough headroom for clients to re-rank later.

func Delete

func Delete(ctx context.Context, sp space.Space, objectId, collection, blockId string) (space.ModifyResult, error)

Delete tombstones one block by id. Sticky — re-creating a block with the same id would be rejected by the SDK's tombstone rule. Children of the deleted block aren't cascaded automatically; the caller (or the markdown bulk path) is responsible for cleaning up.

func MaxPos

func MaxPos(ctx context.Context, sp space.Space, objectId, collection, parentId string) (string, error)

MaxPos returns the highest nav.pos string among blocks with the given parentId, or "" when the parent has no children yet. Used by Create to allocate the default tail position.

func NewChunker

func NewChunker() *index.ModuleChunker

NewChunker constructs the editor module chunker: one chunker for every editor collection in a space — the canonical `editor_blocks` and each namespaced instance — reconciled per collection as coalesced windows (index.ModuleChunker + index.MultiReconciler).

func NewModule

func NewModule() handler.Module

NewModule returns the handler.Module to add to config.Config.Modules so the SDK serves every editor collection — the canonical `editor_blocks` shared by every type declaring `{"module": "editor", "shared": true}` and each namespaced instance — with the block handler.

cfg := config.Config{
    Modules: []handler.Module{ editor.NewModule(), chat.NewModule() },
    ...
}

func NextPos

func NextPos(prev string) string

NextPos returns a lexid that sorts strictly after `prev`. Empty `prev` returns Middle() — used to drop the first block into an empty parent with headroom on both sides. Mirrors anytype-heart's pattern (Middle on empty, Next(prev) on append).

func Patch

func Patch(ctx context.Context, sp space.Space, objectId, collection, blockId string, in PatchInput) (space.ModifyResult, error)

Patch applies the set/unset paths atomically against blockId. The block must already exist (no upsert) — Patch on a missing record returns ErrNotFound. Empty patch is a no-op: no change is produced, so the result carries recordIds=[blockId] with an empty versionId.

Each Set entry is one $set op against its dotted path; the path becomes the SDK's space.Op.Path and the JSON value (as json.RawMessage) gets unmarshalled into a Go-native value the SDK accepts. Unset entries become $unset ops, payload-less.

Types

type Block

type Block struct {
	Id    string         `json:"id"`
	Ver   map[string]any `json:"_ver,omitempty"`
	Type  string         `json:"type"`
	Style map[string]any `json:"style,omitempty"`
	Text  string         `json:"text,omitempty"`
	Nav   Nav            `json:"nav"`
}

Block is the wire / Go-side shape of one block, used by HTTP handlers, the markdown refactor, and callers.

func Get

func Get(ctx context.Context, sp space.Space, objectId, collection, blockId string) (Block, error)

Get fetches one block by id and returns its wire shape (with _ver). Wraps space.ErrNotFound as ErrNotFound so callers can map to 404.

func List

func List(ctx context.Context, sp space.Space, objectId, collection string) ([]Block, error)

List returns every block on the object's editor_blocks dataset in document order — depth-first, siblings sorted by nav.pos ascending. Meta fields (`_ver` etc.) are always present in query results, so callers receive `_ver` alongside payload fields (needed for the wire response and for client-side dedup).

Empty result for objects with no body blocks yet (the dataset is empty until the first create).

type CreateInput

type CreateInput struct {
	Type     string
	Style    map[string]any // any-shaped; serialized as JSON object
	Text     string
	ParentId string
	Pos      string // empty → server allocates next-after-max
}

CreateInput is the validated input to Create. Mirrors the wire body shape — fields default to safe values when omitted on the wire.

type Nav struct {
	ParentId string `json:"parentId"`
	Pos      string `json:"pos"`
}

Nav is the per-record sibling-ordering namespace. Distinct from the `nav` virtual built-in used for the cross-space object tree (same shape, different scope).

type PatchInput

type PatchInput struct {
	Set   map[string]json.RawMessage
	Unset []string
}

PatchInput is the validated input to Patch. Each entry in Set is one dotted path → JSON value $set; each entry in Unset is one dotted path to $unset. Empty Set + empty Unset is a no-op.

type Window

type Window struct {
	AnchorId string   // first member block id — the record-id anchor
	BlockIds []string // member block ids, in document order
	Text     string   // member texts joined by "\n", heading-led
	Title    string   // the leading heading's text (BM25F boost), if any
}

Window is one coalesced index unit: the concatenated text of a run of consecutive document-ordered blocks.

func Windows

func Windows(ordered []Block) []Window

Windows groups document-ordered blocks (as returned by List — a depth-first tree walk, siblings by nav.pos) into coalesced windows. A new window starts before every heading and whenever appending the next block's text would exceed windowBudgetBytes. The heading (or the run's first block) leads its window — cheap context that also lifts BM25 term frequency for heading words.

Blocks with empty text (dividers, images) still count as members for ordering but add no text; a window whose total text is empty is omitted (nothing to index).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL