Sign in

    moondelta

    Cross-platform Quill Delta rich-text operations in MoonBit

    quill
    delta
    rich-text
    operational-transform
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    3

    No dependencies

    #MoonDelta

    MoonDelta is a MoonBit implementation of the Quill Delta rich-text document and change format. The library works on Native, JavaScript, Wasm, and Wasm GC without an editor UI or an external runtime.

    #Supported operations

    • Parse and serialize Quill-style {"ops":[...]} JSON with text, embeds, formatting attributes, numeric and embed retains, and deletions. parse_json_compatible preserves the operation layout accepted by the JavaScript constructor; parse_json validates and normalizes it.
    • Build canonical operation lists with insert_text, insert_embed, retain, retain_embed, delete, push, chop, and concat.
    • Compose changes, apply them to full documents, invert changes for undo, transform concurrent changes, and transform cursor positions.
    • Diff documents with a MoonBit port of fast-diff 1.3.0, including Myers bisect and semantic cleanup; diff_at uses a UTF-16 cursor position in the original document, and diff_with_cursor_info accepts old and new selection ranges.
    • Visit operations with filter, for_each, map, partition, and reduce; use OpIterator for partial operations.
    • Visit rich-text lines with each_line, including delimiter attributes, custom delimiter, and early stop. lines returns line records.
    • Register type-specific EmbedHandler algorithms for composing, inverting, and transforming retain-embed payload updates, either globally with Delta::register_embed or in an explicit EmbedHandlers registry.
    • Maintain a bounded local History for undo and redo.

    Positions and lengths use UTF-16 code units, matching Quill. Diff treats Unicode scalar values and embeds as indivisible units.

    #Example

    let document = Delta::parse_json("{\"ops\":[{\"insert\":\"Hello world\\n\"}]}")
    let change = Delta::new().retain_plain(6).delete(5).insert_plain("MoonBit")
    let updated = document.apply(change)
    println(updated.stringify())
    // {"ops":[{"insert":"Hello MoonBit\n"}]}

    For a reusable document state:

    let history = History::new(document, 100)
    history.apply(change)
    ignore(history.undo())
    ignore(history.redo())

    Embedded payload updates require a registered handler for that embed type:

    fn counter_value(value : Json) -> Double {
    match value {
    Number(number, ..) => number
    _ => 0.0
    }
    }

    let handlers = EmbedHandlers::new()
    handlers.register(
    "counter",
    EmbedHandler::new(
    (base, update, _) =>
    Json::number(counter_value(base) + counter_value(update)),
    (update, _) => Json::number(-counter_value(update)),
    (_, other, _) => other,
    ),
    )
    let base = Delta::new().insert_embed(
    Json::object({ "counter": Json::number(5) }),
    empty_attributes(),
    )
    let increment = Delta::new().retain_embed(
    Json::object({ "counter": Json::number(2) }),
    empty_attributes(),
    )
    let changed = base.apply_with_handlers(increment, handlers)

    The runnable example is in cmd/main/main.mbt; the executable checks are in cmd/selftest/main.mbt.

    #Install and verify

    MoonDelta requires moonc 0.10.16 or newer. Check your installation with moon version --all.

    After publication to Mooncakes, install the library with:

    moon add xiaobai718-ab/moondelta

    On a supported MoonBit installation:

    moon check --target all --deny-warn moon build --target all --deny-warn moon test --target all --deny-warn moon run --target js cmd/main

    The first three commands check, build, and run 1547 tests on Native, JavaScript, Wasm, and Wasm GC. The example prints {"ops":[{"insert":"Hello MoonBit\n"}]}. GitHub Actions runs the same commands on every push and pull request.

    If a Windows installation fails while creating a \\?\ output path in a long workspace path, the direct compiler script checks the library and whitebox test sources on all four targets, then runs the JavaScript selftest (121 text-diff pairs, 410 concurrent-transform pairs, and focused API checks):

    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\verify.ps1

    To execute the whitebox tests despite that Windows path issue, the following script copies project source files to a short temporary directory, runs Native and JavaScript, and removes the copy. Pass -Targets wasm or -Targets wasm-gc for the other backends:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\test-short-path.ps1

    Regenerate the public MoonBit interface from the same short-path workspace:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\update-interface.ps1

    Before a release, run moon package --list to inspect the Mooncakes archive. The .moonignore file excludes generated files, scripts, and test fixtures from the published package while keeping them in the GitHub repository. Publishing uses moon publish after signing in with moon login.

    #Compatibility notes

    The implementation covers Quill Delta 5.1's typed operations with MoonBit naming. Delta::from_json validates and normalizes input; Delta::from_json_compatible and Delta::from_ops_raw preserve well-formed operation layout. A numeric diff_at hint is the caret position before an edit. diff_with_cursor_info accepts Quill-style old and new UTF-16 cursor ranges. The regression suite compares text, Unicode, cursor, compose, transform, and invert results with the upstream implementations. Rare Unicode bisect paths can choose a different valid edit script because MoonBit's StringView::to_owned does not preserve isolated UTF-16 surrogates on every backend. The library does not include rendering, a GUI, network synchronization, or CRDT. History is local undo/redo and does not rebase stored inverses over remote edits.

    The current feature-by-feature status and remaining parity work are listed in COMPATIBILITY.md.

    #License

    MoonDelta is Apache-2.0 licensed. Quill Delta's BSD-3-Clause and fast-diff's Apache-2.0 notices are reproduced in THIRD_PARTY_NOTICES.md.

    DeltaError

    pub suberror DeltaError {
    Invalid(String)
    } derive(
    Debug
    )

    Validation errors include the path in the source JSON whenever possible.

    CursorInfo

    pub(all) struct CursorInfo {
    old_range : CursorRange
    new_range : CursorRange
    }

    Before-and-after selection ranges accepted by Quill Delta's diff cursor option. A selected old range followed by a zero-length new range denotes replacement of the selection, with the new index at the resulting caret.

    CursorInfo::new

    fn CursorInfo::new(old_range : CursorRange, new_range : CursorRange) -> CursorInfo

    CursorRange

    pub(all) struct CursorRange {
    index : Int
    length : Int
    }

    A UTF-16 range in a document. The index and length must be nonnegative and lie within the document passed to diff_with_cursor_info.

    CursorRange::new

    fn CursorRange::new(index : Int, length : Int) -> CursorRange

    Delta

    pub(all) struct Delta {
    ops : Array[Op]
    } derive(Eq,
    Debug
    )

    A sequence of rich-text editing operations. Builders normalize adjacent operations, put inserts before deletes at the same position, and discard operations with zero length.

    Delta::apply

    fn Delta::apply(self : Delta, change : Delta) -> Delta raise DeltaError

    Apply a change to a full insert-only document.

    Delta::apply_with_handlers

    fn Delta::apply_with_handlers(self : Delta, change : Delta, handlers : EmbedHandlers) -> Delta raise DeltaError

    Delta::change_length

    fn Delta::change_length(self : Delta) -> Int

    Delta::chop

    fn Delta::chop(self : Delta) -> Delta

    A trailing unformatted retain has no observable effect on a change.

    Delta::compose

    fn Delta::compose(self : Delta, next : Delta) -> Delta raise DeltaError

    Apply next after this Delta. A document composed with a change yields a new document; composing two changes yields one equivalent change.

    Delta::compose_with_handlers

    fn Delta::compose_with_handlers(self : Delta, next : Delta, handlers : EmbedHandlers) -> Delta raise DeltaError

    Compose using registered, type-specific embed payload algorithms.

    Delta::concat

    fn Delta::concat(self : Delta, other : Delta) -> Delta

    Delta::count

    fn Delta::count(self : Delta) -> Int

    Delta::delete

    fn Delta::delete(self : Delta, length : Int) -> Delta

    Delta::diff

    fn Delta::diff(self : Delta, target : Delta) -> Delta raise DeltaError

    Produce the edits that turn one complete document into another. Myers diff compares Unicode scalar values and indivisible embeds; offsets and lengths in the resulting Delta are measured in UTF-16 code units.

    Delta::diff_at

    fn Delta::diff_at(self : Delta, target : Delta, cursor : Int) -> Delta raise DeltaError

    Diff with the cursor convention used by fast-diff: first try an edit immediately before the old caret, then an edit immediately after it. The hint is ignored when neither splice produces the target text.

    Delta::diff_with_cursor_info

    fn Delta::diff_with_cursor_info(self : Delta, target : Delta, cursor : CursorInfo) -> Delta raise DeltaError

    Use Quill's selected-range cursor convention, or explicit old/new edit ranges when they align with otherwise unchanged content.

    Delta::each_line

    fn Delta::each_line(self : Delta, visit : (Delta, Json, Int) -> Bool, newline? : String) -> Unit raise DeltaError

    Visit lines while operations remain inserts. The callback receives the line without its delimiter, delimiter attributes, and a zero-based line number. Returning false stops traversal. A non-insert stops iteration; any preceding complete lines have already been visited.

    Delta::equal

    fn Delta::equal(Delta, Delta) -> Bool

    Delta::filter

    fn Delta::filter(self : Delta, predicate : (Op, Int) -> Bool) -> Array[Op]

    Return operations accepted by a predicate, preserving original order.

    Delta::for_each

    fn Delta::for_each(self : Delta, visit : (Op, Int) -> Unit) -> Unit

    Visit every operation with its zero-based operation index.

    Delta::from_json

    fn Delta::from_json(value : Json) -> Delta raise DeltaError

    Delta::from_json_compatible

    fn Delta::from_json_compatible(value : Json) -> Delta raise DeltaError

    Parse a Quill Delta JSON value while preserving operation order, empty text operations, and multi-key embed objects accepted by the JavaScript constructor. Unknown JSON fields cannot be represented by typed Op values and are ignored.

    Delta::from_ops

    fn Delta::from_ops(ops : Array[Op]) -> Delta

    Delta::from_ops_raw

    fn Delta::from_ops_raw(operations : Array[Op]) -> Delta

    Construct a Delta from operations without normalizing their order or merging adjacent operations, matching Quill Delta's array constructor. Callers must supply well-formed operations. The array is shared, as in the JavaScript constructor.

    Delta::insert_embed

    fn Delta::insert_embed(self : Delta, embed : Json, attributes : Json) -> Delta

    Delta::insert_plain

    fn Delta::insert_plain(self : Delta, text : String) -> Delta

    Delta::insert_text

    fn Delta::insert_text(self : Delta, text : String, attributes : Json) -> Delta

    Delta::invert

    fn Delta::invert(self : Delta, base : Delta) -> Delta raise DeltaError

    Create a change that reverses this change against the supplied full base document. The base is required because deleted content is absent from a Delta change.

    Delta::invert_with_handlers

    fn Delta::invert_with_handlers(self : Delta, base : Delta, handlers : EmbedHandlers) -> Delta raise DeltaError

    Invert a change with registered algorithms for embed payload updates.

    Delta::is_document

    fn Delta::is_document(self : Delta) -> Bool

    Delta::is_empty

    fn Delta::is_empty(self : Delta) -> Bool

    Delta::iterator

    fn Delta::iterator(self : Delta) -> OpIterator

    Return a cursor over a copy of the operation list.

    Delta::length

    fn Delta::length(self : Delta) -> Int

    Delta::line_count

    fn Delta::line_count(self : Delta) -> Int raise DeltaError

    Delta::lines

    fn Delta::lines(self : Delta) -> Array[DeltaLine] raise DeltaError

    Split an insert-only document into rich-text lines. The final unterminated line is included when it has content. An empty document has no lines.

    Delta::map

    fn[T] Delta::map(self : Delta, convert : (Op, Int) -> T) -> Array[T]

    Map operations to arbitrary values, passing the operation index.

    Delta::new

    fn Delta::new() -> Delta

    Delta::not_equal

    fn Delta::not_equal(x : Delta, y : Delta) -> Bool

    Delta::operations

    fn Delta::operations(self : Delta) -> Array[Op]

    Delta::parse_json

    fn Delta::parse_json(source : String) -> Delta raise DeltaError

    Parse a Quill Delta JSON object or its bare operations array.

    Delta::parse_json_compatible

    fn Delta::parse_json_compatible(source : String) -> Delta raise DeltaError

    Delta::partition

    fn Delta::partition(self : Delta, predicate : (Op) -> Bool) -> (Array[Op], Array[Op])

    Split operations by a predicate. The first array contains accepted ops; both arrays preserve the order of their source operations.

    Delta::plain_text

    fn Delta::plain_text(self : Delta) -> String?

    Delta::push

    fn Delta::push(self : Delta, value : Op) -> Delta

    Append an operation while maintaining Quill's canonical operation order.

    Delta::reduce

    fn[T] Delta::reduce(self : Delta, initial : T, combine : (T, Op, Int) -> T) -> T

    Fold a Delta's operations, passing each operation index to the reducer.

    Delta::register_embed

    fn Delta::register_embed(embed_type : String, handler : EmbedHandler) -> Unit raise DeltaError

    Register the default handler for an embed type, as in Quill Delta's registerEmbed. Explicit registries remain available for isolated editors.

    Delta::retain

    fn Delta::retain(self : Delta, length : Int, attributes : Json) -> Delta

    Delta::retain_embed

    fn Delta::retain_embed(self : Delta, embed : Json, attributes : Json) -> Delta

    Delta::retain_plain

    fn Delta::retain_plain(self : Delta, length : Int) -> Delta

    Delta::slice

    fn Delta::slice(self : Delta, start : Int, end : Int) -> Delta

    Delta::slice_all

    fn Delta::slice_all(self : Delta) -> Delta

    Return the whole Delta as a separate value, corresponding to slice().

    Delta::slice_from

    fn Delta::slice_from(self : Delta, start : Int) -> Delta

    Slice from a UTF-16 offset through the end, corresponding to Quill's slice(start) overload.

    Delta::stringify

    fn Delta::stringify(self : Delta) -> String

    Delta::to_json

    fn Delta::to_json(self : Delta) -> Json

    Delta::to_repr

    Delta::transform

    fn Delta::transform(self : Delta, other : Delta, priority : Bool) -> Delta raise DeltaError

    Transform other against this Delta. With priority = true, this Delta wins when both changes insert at the same position or format the same key.

    Delta::transform_position

    fn Delta::transform_position(self : Delta, position : Int, priority : Bool) -> Int

    Rebase a UTF-16 cursor position through this change.

    Delta::transform_with_handlers

    fn Delta::transform_with_handlers(self : Delta, other : Delta, priority : Bool, handlers : EmbedHandlers) -> Delta raise DeltaError

    Transform concurrent changes using registered embed payload algorithms.

    Delta::unregister_embed

    fn Delta::unregister_embed(embed_type : String) -> Unit

    DeltaLine

    pub(all) struct DeltaLine {
    content : Delta
    attributes : Json
    start : Int
    terminated : Bool
    } derive(Eq,
    Debug
    )

    A rich-text line. content excludes the newline; formatting attached to the newline is returned separately as attributes (Quill block formats).

    DeltaLine::equal

    fn DeltaLine::equal(DeltaLine, DeltaLine) -> Bool

    DeltaLine::not_equal

    fn DeltaLine::not_equal(x : DeltaLine, y : DeltaLine) -> Bool

    EmbedHandler

    pub struct EmbedHandler {
    compose_value : (Json, Json, Bool) -> Json raise DeltaError
    invert_value : (Json, Json) -> Json raise DeltaError
    transform_value : (Json, Json, Bool) -> Json raise DeltaError
    }

    Algorithms for one Quill embed type. The values are the JSON values under the embed's single type key, not the outer { type: value } object.

    EmbedHandler::new

    fn EmbedHandler::new(compose : (Json, Json, Bool) -> Json raise DeltaError, invert : (Json, Json) -> Json raise DeltaError, transform : (Json, Json, Bool) -> Json raise DeltaError) -> EmbedHandler

    EmbedHandlers

    pub struct EmbedHandlers {
    handlers : Map[String, EmbedHandler]
    }

    An explicit registry keeps embed behavior deterministic across independent editors sharing a process. Pass it to the _with_handlers methods.

    EmbedHandlers::contains

    fn EmbedHandlers::contains(self : EmbedHandlers, embed_type : String) -> Bool

    EmbedHandlers::new

    EmbedHandlers::register

    fn EmbedHandlers::register(self : EmbedHandlers, embed_type : String, handler : EmbedHandler) -> Unit raise DeltaError

    EmbedHandlers::unregister

    fn EmbedHandlers::unregister(self : EmbedHandlers, embed_type : String) -> Unit

    History

    pub struct History {
    document : Delta
    undo_stack : Array[Delta]
    redo_stack : Array[Delta]
    revision : Int
    max_depth : Int
    }

    Local rich-text document with reversible changes. History stores inverse Deltas, so edits to embedded values have the same restrictions as invert.

    History::apply

    fn History::apply(self : History, change : Delta) -> Unit raise DeltaError

    History::can_redo

    fn History::can_redo(self : History) -> Bool

    History::can_undo

    fn History::can_undo(self : History) -> Bool

    History::document

    fn History::document(self : History) -> Delta

    History::new

    fn History::new(document : Delta, max_depth : Int) -> History raise DeltaError

    History::redo

    fn History::redo(self : History) -> Bool raise DeltaError

    Redo the most recently undone edit. Returns false when there is none.

    History::redo_depth

    fn History::redo_depth(self : History) -> Int

    History::revision

    fn History::revision(self : History) -> Int

    History::undo

    fn History::undo(self : History) -> Bool raise DeltaError

    Undo the most recent local edit. Returns false when there is nothing to do.

    History::undo_depth

    fn History::undo_depth(self : History) -> Int

    pub(all) enum Op {
    InsertText(String, Json)
    InsertEmbed(Json, Json)
    Retain(Int, Json)
    RetainEmbed(Json, Json)
    Delete(Int)
    } derive(Eq,
    Debug
    )

    A Quill Delta operation. Text positions are UTF-16 code-unit offsets. Attributes are JSON objects; an empty object means no formatting change.

    Op::attributes

    fn Op::attributes(self : Op) -> Json

    Op::equal

    fn Op::equal(Op, Op) -> Bool

    Op::kind

    fn Op::kind(self : Op) -> String

    Op::length

    fn Op::length(self : Op) -> Int

    Op::not_equal

    fn Op::not_equal(x : Op, y : Op) -> Bool

    Op::to_repr

    OpIterator

    pub struct OpIterator {
    ops : Array[Op]
    index : Int
    offset : Int
    }

    Internal cursor over operations. It splits text using UTF-16 offsets, the same unit used by Quill's JavaScript implementation.

    OpIterator::has_next

    fn OpIterator::has_next(self : OpIterator) -> Bool

    OpIterator::new

    fn OpIterator::new(ops : Array[Op]) -> OpIterator

    OpIterator::next

    fn OpIterator::next(self : OpIterator, requested : Int) -> Op

    OpIterator::next_full

    fn OpIterator::next_full(self : OpIterator) -> Op

    Consume the remaining part of the next operation.

    OpIterator::peek

    fn OpIterator::peek(self : OpIterator) -> Op?

    OpIterator::peek_kind

    fn OpIterator::peek_kind(self : OpIterator) -> String

    OpIterator::peek_length

    fn OpIterator::peek_length(self : OpIterator) -> Int

    OpIterator::rest

    fn OpIterator::rest(self : OpIterator) -> Array[Op]

    attributes_compose

    fn attributes_compose(left : Json, right : Json, keep_null : Bool) -> Json

    attributes_diff

    fn attributes_diff(left : Json, right : Json) -> Json

    attributes_invert

    fn attributes_invert(change : Json, base : Json) -> Json

    attributes_transform

    fn attributes_transform(left : Json, right : Json, priority : Bool) -> Json

    empty_attributes

    fn empty_attributes() -> Json

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io