Sign in

    notebooks

    Immutable Jupyter and DIB notebooks with shared navigation and traversal

    jupyter
    notebook
    nbformat
    dib
    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    24

    #Notebooks

    moonrockz/notebooks provides immutable notebook models and shared navigation and traversal. It lives in Moon-Lander and has no dependency on the framework module.

    The module, usage guides and notebook acceptance and law suites were migrated from Moonbase at fd5f9ca. The moonrockz/notebooks module name and package imports are unchanged.

    PackagePurpose
    moonrockz/notebooksOpen node contract, common node kinds, typed cursors, paths, walks and folds
    moonrockz/notebooks/nbformatJupyter v4 JSON, cells, outputs, attachments and metadata
    moonrockz/notebooks/dib.NET Interactive source notebooks, kernels, metadata and directives

    Add the module with moon add moonrockz/notebooks. In moon.pkg, import the packages you use:

    import {
    "moonrockz/notebooks" @notebooks,
    "moonrockz/notebooks/nbformat" @nbformat,
    "moonrockz/notebooks/dib" @dib,
    }

    All packages support wasm, wasm-gc, JavaScript and native targets. See the Jupyter API and DIB API for format-specific codecs and constraints.

    #Runnable usage guides

    The following guides contain complete examples with assertions. MoonBit compiles and runs their mbt check blocks as documentation tests, and mise run test includes them on all four targets.

    GuideRecipes
    Shared navigationCross-format queries, cursor moves, saved paths, prune/stop callbacks, folds, subtree roots and consumer-defined formats
    Jupyter notebooksReading, construction, rich outputs, attachments, immutable metadata, ID lookup, errors, extensions and edits
    DIB notebooksSelectors, aliases, construction, custom environments, metadata, directive navigation, errors and empty-cell normalization

    Run all usage guides and package tests on one target:

    mise exec -- moon test pkgs/notebooks pkgs/notebooks/nbformat pkgs/notebooks/dib --target js --deny-warn

    #Shared navigation with concrete payloads

    Each format has its own Notebook, Cell and Node values. Its Node implements the open @notebooks.Node trait:

    • children() returns immutable children in document order.
    • kind() classifies a notebook, cell, output, attachment or directive.
    • source() returns available source text, with None as the default.
    • metadata() returns node-local Metadata?, with None as the default.

    CellKind distinguishes code, Markdown, raw and unknown cells. The shared classification leaves the concrete format's payload intact. The traversal engine does not convert notebooks or discard format-specific data.

    Write a generic function once and call it with either format's root:

    fn[N : @notebooks.Node] cell_sources(root : N) -> ReadOnlyArray[String] {
    let sources = []
    @notebooks.walk(root, node => {
    if node.kind() is Cell(_) {
    if node.source() is Some(source) { sources.push(source) }
    }
    Continue
    })
    ReadOnlyArray::from_array(sources[:])
    }

    let jupyter = @nbformat.parse(jupyter_json)
    let interactive = @dib.parse(dib_source)
    let jupyter_sources = cell_sources(jupyter.root())
    let dib_sources = cell_sources(interactive.root())

    Cursor[N] retains the concrete node type N. For a Jupyter root, cursor.node() returns @nbformat.Node; for a DIB root, it returns @dib.Node. Pattern match that value to inspect outputs, attachment MIME bundles, kernel names or directive arguments. Both format packages expose book.cursor(), node.cursor(), node.at(path), walk, walk_cursor and book.fold as convenience methods backed by the shared engine.

    The trees differ where their formats differ. Jupyter cells have outputs or attachments as children. DIB cells have directives as children. A generic algorithm should use kind() to select the nodes it needs, rather than assume identical tree shapes.

    #Cursors, paths and traversal

    Construct a cursor with Cursor(root). Moves return a new position or None at a boundary. Use parent, first_child, last_child, next_sibling, previous_sibling and children. Saved positions retain their node and ancestors. depth() starts at zero; path() returns a NodePath of child indices. @notebooks.at(root, path) resolves it, or returns None for an invalid index. Paths are relative to their original traversal root and refer to child order, not JSON object keys or persistent cell IDs.

    walk(root, enter, leave~) supplies nodes. walk_cursor supplies positions. Both walk depth-first in pre-order. Continue visits children; SkipChildren skips descendants but still calls leave; Stop returns immediately without pending leave calls. cursor.walk retains the original paths and ancestors while walking that subtree. Starting a cursor from the node itself establishes a new root. Traversal uses an explicit stack.

    fold(root, init, f) and cursor.fold(init, f) thread an accumulator through the same pre-order, supplying cursor context. They visit every descendant. Use a walk when pruning or early termination is needed.

    #Additional notebook formats

    #MIME lookup capability

    MimeTyped is a separate, optional trait for cells that support MIME lookup. Its has_mime_type(cell, mime_type) method tests for a representation with that exact key. Implementing the trait indicates support for the query; a particular cell can still have no matching representation.

    Jupyter @nbformat.Cell implements MimeTyped for rich outputs and attachments. DIB cells omit this trait because the format has no stored MIME bundles. The generic helper requires the capability at compile time:

    let cells = @notebooks.cells_by_mime_type(jupyter.cells(), "image/png")
    // cells is ReadOnlyArray[@nbformat.Cell].

    The result preserves input order and concrete cell types. Calling this helper with DIB cells is a compile-time error. The trait is independent of Node, so consumer-defined cell types can implement it without adopting tree navigation. See the checked capability example.

    #Runtime cell inspection

    CellView is the shared, object-safe interface for inspecting individual cells. Both Jupyter and DIB cells implement it, so a ReadOnlyArray[&@notebooks.CellView] can hold a mixture of formats. It exposes kind(), source(), metadata() and mime_capability().

    mime_capability() returns Option[&@notebooks.MimeTyped]. Jupyter cells return Some, including cells with no stored representations. DIB cells return None. Query a returned capability with has_mime_type(mime_type) to distinguish a match from supported lookup with no match.

    Concrete cells expose the same accessor, so inspection also works without building a mixed collection. Consumer-defined cells implement CellView's kind and source methods. Metadata and MIME capability default to None; supporting consumers explicitly override mime_capability to return Some(self as &@notebooks.MimeTyped).

    See the checked mixed-collection example. Tree navigation continues to use each format's concrete Node and typed cursors. The cell view provides runtime capability inspection without changing that navigation contract.

    A consumer can implement @notebooks.Node on its own immutable tree and use the same cursors, path resolution, walks and folds. Children must be stable, ordered, finite and immutable. Neither this package nor existing formats need to change when another format implements the contract.

    This module does not convert between file formats. Reading and writing remain explicit operations on each format's notebook type, so saving a DIB notebook cannot silently discard Jupyter outputs or attachments.

    #Migration from moonbase-nbformat

    Replace the module dependency moonrockz/moonbase-nbformat with moonrockz/notebooks at the workspace version. Replace the package import moonrockz/moonbase-nbformat with moonrockz/notebooks/nbformat, keeping the @nbformat alias. Existing Jupyter codecs and convenience navigation methods remain available. Explicit cursor annotations now use @nbformat.Cursor[@nbformat.Node], or @notebooks.Cursor[@nbformat.Node]. Control and NodePath are shared types re-exported by both format packages.

    #Shared metadata

    Metadata stores an Ion struct using moonrockz/ion@0.6.0. Construct native metadata with Metadata::from_ion(value), or import an existing JSON object with Metadata(json). Metadata::empty() creates an empty struct. Top-level field names must have known text and be unique. Other shapes raise MetadataError::ExpectedObject; invalid keys raise InvalidKey.

    get(key) returns Option[@ion.Ion]; to_ion() exports the complete native value. Integers, decimals, timestamps, typed nulls, annotations and nested Ion values retain their types. Imports, lookups and exports defensively copy mutable containers and annotation arrays. A present null, including a typed null, is Some(value); an absent key is None. keys() returns immutable field names. is_empty() means no fields and no root annotations.

    get_json(key) and to_json() are explicit, fallible JSON adapters. They raise MetadataError::NotJson with a field path instead of dropping annotations, typed nulls, non-finite floats, timestamps, symbols, lob values, sexps or repeated nested struct fields. Exact integer and decimal spelling is retained in JSON numbers. Code that previously expected JSON from get should use get_json, or inspect the native value's kind().

    DIB accepts JSON, YAML, TOML and Ion metadata sections. Its default output uses JSON only when it preserves the entire Ion value, otherwise Ion. Jupyter continues to require JSON-compatible metadata and reports a field validation error for Ion-only values. See the native metadata example and DIB format rules.

    Node.metadata() returns the node's own attributes. Some(Metadata::empty()) means the node has an empty metadata container. None means the format defines no metadata container for that node. Metadata is never inherited. Use a cursor's parent() explicitly when a query needs ancestor attributes. Metadata remains an attribute, rather than a traversal child.

    NodeCommon metadata
    Jupyter notebook and every cell typeSome, including empty objects
    Jupyter display_data and execute_result outputsSome, including empty objects
    Jupyter stream, error and unknown output types; attachmentsNone
    DIB notebookSome, including an absent or empty #!meta section
    DIB cell and directiveNone

    Unknown output fields remain available through the specialized output payload. Both format packages re-export the same Metadata type. Jupyter notebook, cell and rich-output constructors accept it, and their accessors return it. Jupyter's JsonObject remains the type for extension fields and unknown payloads. Replace metadata arguments built with @nbformat.JsonObject(json) with @notebooks.Metadata(json) or @nbformat.Metadata(json). Existing @dib.Metadata(json) calls use the shared constructor and now raise MetadataError; DIB parsing continues to report FormatError::InvalidMetadata with the source line. Jupyter codecs retain field-path validation errors.

    CellView

    pub(open) trait CellView {
    fn kind(Self) -> CellKind
    fn source(Self) -> String?
    fn metadata(Self) -> Metadata? = _
    fn mime_capability(Self) -> &MimeTyped? = _
    }

    An immutable, format-independent cell view suitable for mixed collections. All methods are object-safe, so cells from different formats can be stored together as &CellView. Tree navigation remains on each format's Node type.

    MimeTyped

    pub(open) trait MimeTyped {
    fn has_mime_type(Self, String) -> Bool
    }

    An optional capability for querying the MIME representations of a cell. Implementing this trait means lookup is supported, even when a particular cell has no representations. Formats without MIME data should omit it.

    Node

    pub(open) trait Node {
    fn children(Self) -> ReadOnlyArray[Self]
    fn kind(Self) -> NodeKind
    fn source(Self) -> String? = _
    fn metadata(Self) -> Metadata? = _
    }

    A contract for a finite, immutable notebook tree with stable child order. The concrete Self type retains format-specific payloads. Implementations must not return children that alias mutable state or introduce cycles.

    MetadataError

    pub(all) suberror MetadataError {
    ExpectedObject
    InvalidKey(String)
    NotJson(path~ : String, reason~ : String)
    } derive(Eq,
    Debug
    )

    A metadata shape or format-conversion failure.

    MetadataError::equal

    MetadataError::message

    fn MetadataError::message(self : MetadataError) -> String

    MetadataError::not_equal

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

    CellKind

    pub(all) enum CellKind {
    Code
    Markdown
    Raw
    Unknown(String)
    } derive(Eq,
    Debug
    )

    The common role of a cell, independent of its file format. Unknown retains the original format's cell-type name.

    CellKind::equal

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

    CellKind::not_equal

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

    CellKind::to_repr

    Control

    pub(all) enum Control {
    Continue
    SkipChildren
    Stop
    } derive(Eq,
    Debug
    )

    A decision returned by an enter callback during depth-first traversal. Continue descends, SkipChildren prunes but still calls leave, and Stop returns immediately without calling any pending leave callbacks.

    Control::equal

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

    Control::not_equal

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

    Control::to_repr

    Cursor

    type Cursor[N]

    An immutable position in a tree of concrete nodes N. Moves return new positions; retaining a cursor saves its node, path and ancestors. Parent navigation never goes above the root passed to the constructor.

    Cursor::Cursor

    fn[N] Cursor::Cursor(root : N) -> Cursor[N]

    Establish a new navigation root at this node, with depth zero and path $. Constructing from a descendant discards its previous ancestor context.

    Cursor::children

    fn[N : Node] Cursor::children(self : Cursor[N]) -> ReadOnlyArray[Cursor[N]]

    Return immutable positions for all direct children in document order. Each position retains this cursor as its parent.

    Cursor::depth

    fn[N] Cursor::depth(self : Cursor[N]) -> Int

    Return the number of edges from this cursor's navigation root.

    Cursor::first_child

    fn[N : Node] Cursor::first_child(self : Cursor[N]) -> Cursor[N]?

    Move to the first child in document order, or return None for a leaf.

    Cursor::fold

    fn[N : Node, A] Cursor::fold(self : Cursor[N], init : A, f : (A, Cursor[N]) -> A) -> A

    Thread an accumulator through every node of this subtree in pre-order. Callbacks receive typed cursors with the original paths and ancestors. Use walk when pruning or early termination is needed.

    Cursor::last_child

    fn[N : Node] Cursor::last_child(self : Cursor[N]) -> Cursor[N]?

    Move to the last child in document order, or return None for a leaf.

    Cursor::next_sibling

    fn[N] Cursor::next_sibling(self : Cursor[N]) -> Cursor[N]?

    Move to the following sibling, retaining the same parent. Return None at the last sibling or at the navigation root.

    Cursor::node

    fn[N] Cursor::node(self : Cursor[N]) -> N

    Return the concrete node at this position without converting its payload.

    Cursor::parent

    fn[N] Cursor::parent(self : Cursor[N]) -> Cursor[N]?

    Return the saved parent position, or None at the navigation root.

    Cursor::path

    fn[N] Cursor::path(self : Cursor[N]) -> NodePath

    Build the path from this cursor's original root to its current node. The result remains stable when other cursors navigate the same tree.

    Cursor::previous_sibling

    fn[N] Cursor::previous_sibling(self : Cursor[N]) -> Cursor[N]?

    Move to the preceding sibling, retaining the same parent. Return None at the first sibling or at the navigation root.

    Cursor::walk

    fn[N : Node] Cursor::walk(self : Cursor[N], enter : (Cursor[N]) -> Control, leave? : (Cursor[N]) -> Unit) -> Unit

    Walk this subtree in depth-first pre-order, retaining original paths and ancestors. Call leave after descendants, including for a pruned node. Stop ends immediately without pending leaves. Saved positions and moves inside callbacks do not affect the walk. Uses an explicit stack.

    Metadata

    Immutable Ion metadata with unique, textual top-level field names. Import and export copy nested containers and annotations defensively.
    impl Eq for Metadata

    Metadata::Metadata

    fn Metadata::Metadata(value : Json) -> Metadata raise MetadataError

    Construct from a JSON object. Use from_ion for the richer Ion data model. This JSON input adapter retains the existing constructor's behavior.

    Metadata::empty

    fn Metadata::empty() -> Metadata

    Metadata::equal

    fn Metadata::equal(self : Metadata, other : Metadata) -> Bool

    Metadata::from_ion

    Require a non-null Ion struct with unique, textual top-level field names. Nested Ion values retain their types, annotations and repeated fields.

    Metadata::get

    fn Metadata::get(self : Metadata, key : String) ->
    Ion
    ?

    Look up a field as an independent Ion value. Typed nulls are present; an absent field is None. No format conversion is performed.

    Metadata::get_json

    fn Metadata::get_json(self : Metadata, key : String) -> Json? raise MetadataError

    Explicit JSON lookup. Raise NotJson if the selected value needs Ion's richer types; None still means the key is absent.

    Metadata::is_empty

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

    Metadata::keys

    fn Metadata::keys(self : Metadata) -> ReadOnlyArray[String]

    Metadata::not_equal

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

    Metadata::to_ion

    Export a defensive copy of the native Ion value, retaining every type.

    Metadata::to_json

    fn Metadata::to_json(self : Metadata) -> Json raise MetadataError

    Export independent JSON. Reject annotations, typed nulls, non-finite floats, symbols, timestamps, lob values, sexps and repeated struct fields. Integers and decimals retain their exact numeric spelling.

    Metadata::to_repr

    NodeKind

    pub(all) enum NodeKind {
    Notebook
    Cell(CellKind)
    Output(String)
    Attachment(String)
    Directive(String)
    } derive(Eq,
    Debug
    )

    The role of a node during shared traversal. Output variants carry the output type; attachments carry filenames; directives carry command names. Use the concrete node value to inspect format-specific payloads.

    NodeKind::equal

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

    NodeKind::not_equal

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

    NodeKind::to_repr

    NodePath

    type NodePath derive(Eq,
    Debug
    )

    An immutable sequence of zero-based child indices relative to a tree root. The empty path denotes that root. Paths describe traversal children, not JSON keys or persistent cell IDs; they may not resolve in another tree.

    NodePath::NodePath

    fn NodePath::NodePath(indices : ReadOnlyArray[Int]) -> NodePath

    Create a path from immutable child indices. Negative and out-of-range indices are permitted here and cause at to return None when resolved.

    test "doc: describe a child path" {
    let path = @notebooks.NodePath([1, 0])
    @expect.expect_all(s => {
    s.expect(path.indices()).to_equal([1, 0])
    s.expect(path.to_string()).to_equal("$[1][0]")
    s.expect(@notebooks.NodePath([]).to_string()).to_equal("$")
    })
    }

    NodePath::equal

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

    NodePath::indices

    fn NodePath::indices(self : NodePath) -> ReadOnlyArray[Int]

    Return the immutable child indices in root-to-node order.

    NodePath::not_equal

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

    NodePath::to_repr

    NodePath::to_string

    fn NodePath::to_string(self : NodePath) -> String

    Render a traversal path, such as $, $[0] or $[0][1].
    fn[N : Node] at(root : N, path : NodePath) -> N?

    Resolve a traversal path relative to root. Return None for any invalid child index, including a negative index. An empty path returns root.

    cells_by_mime_type

    fn[C : MimeTyped] cells_by_mime_type(cells : ReadOnlyArray[C], mime_type : String) -> ReadOnlyArray[C]

    Select MIME-capable cells in input order, retaining each matching input position once. The immutable result preserves the concrete cell type. The trait bound rejects unsupported cell types at compile time.

    fold

    fn[N : Node, A] fold(root : N, init : A, f : (A, Cursor[N]) -> A) -> A

    Thread an accumulator through every node in pre-order, establishing a new cursor root. The callback returns the next accumulator and receives the concrete node through its typed cursor. Use walk for pruning or stop.

    walk

    fn[N : Node] walk(root : N, enter : (N) -> Control, leave? : (N) -> Unit) -> Unit

    Walk concrete nodes in depth-first pre-order from a new root. Use walk_cursor when callbacks need parent, sibling or path context. SkipChildren still receives leave; Stop cancels pending leave callbacks.

    walk_cursor

    fn[N : Node] walk_cursor(root : N, enter : (Cursor[N]) -> Control, leave? : (Cursor[N]) -> Unit) -> Unit

    Walk from a new root, supplying typed cursor positions to callbacks. Enter runs before children and leave after them. SkipChildren still receives leave; Stop returns immediately without pending leave calls.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io