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.
| Package | Purpose |
|---|---|
moonrockz/notebooks |
Open node contract, common node kinds, typed cursors, paths, walks and folds |
moonrockz/notebooks/nbformat |
Jupyter 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.
| Guide | Recipes |
|---|---|
| Shared navigation | Cross-format queries, cursor moves, saved paths, prune/stop callbacks, folds, subtree roots and consumer-defined formats |
| Jupyter notebooks | Reading, construction, rich outputs, attachments, immutable metadata, ID lookup, errors, extensions and edits |
| DIB notebooks | Selectors, 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, withNoneas the default.metadata()returns node-localMetadata?, withNoneas 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.
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.
| Node | Common metadata |
|---|---|
| Jupyter notebook and every cell type | Some, including empty objects |
| Jupyter display_data and execute_result outputs | Some, including empty objects |
| Jupyter stream, error and unknown output types; attachments | None |
| DIB notebook | Some, including an absent or empty #!meta section |
| DIB cell and directive | None |
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.