Sign in

    notebooks

    Immutable Jupyter and DIB notebooks with shared navigation and traversal

    jupyter
    notebook
    nbformat
    dib
    Download zip
    Author
    Version
    0.0.1
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    2

    Dependencies

    #Notebooks

    moonrockz/notebooks provides immutable notebook models and shared navigation and traversal. The module has no dependency on the other Moonbase modules.

    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

    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(json) requires a JSON object and raises MetadataError::ExpectedObject for another JSON type. Metadata::empty() creates an empty container. Use get(key), keys(), is_empty() and to_json() to inspect attributes. Imports, lookups and exports copy nested JSON objects and arrays, so callers cannot mutate the stored value. A present JSON null is Some(Null); an absent key is None. Key order is unspecified.

    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.

    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
    } derive(Eq,
    Debug
    )

    A failure to construct metadata from a non-object JSON value.

    MetadataError::equal

    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

    type Metadata derive(Eq,
    Debug
    )

    Immutable attributes belonging to one notebook node. Construction and exports deep-copy JSON so nested mutable values cannot change metadata. Format-specific constructors validate the meaning of reserved keys.

    Metadata::Metadata

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

    Require a JSON object and copy its entire value graph. Raise ExpectedObject for any other JSON type. Custom keys and JSON values are retained.

    Metadata::empty

    fn Metadata::empty() -> Metadata

    Create an empty metadata container without a fallible JSON conversion.

    Metadata::equal

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

    Metadata::get

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

    Look up an exact key, returning an independent JSON value or None. A present JSON null is Some(Null), distinct from an absent key.

    Metadata::is_empty

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

    Return whether this available metadata container has no attributes.

    Metadata::keys

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

    Return immutable keys without a sorting guarantee.

    Metadata::not_equal

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

    Metadata::to_json

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

    Export an independent JSON object that callers may mutate safely.

    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.

    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