Codecs & canonical content

The codec seam

Kinogaki Core defines a container (a tree of path-addressed elements holding typed values, wired by connections) and a single seam, the codec, for translating that container to and from foreign formats. The native .prisma text and .prism binary live in Core; every other format is a codec. Each codec is small and self-contained, so Core stays a model with a clear boundary, not a tangle of file formats.

A codec is bidirectional, and both directions are fallible:

struct Codec {
  std::string name() const;
  std::optional<std::string> encode(const Document&) const;    // Document → bytes, or nullopt
  std::optional<Document> decode(std::string_view) const;      // bytes → Document, or nullopt
};

encode may decline a Document it cannot represent: asking a plain-text codec to write an arbitrary scene returns nothing rather than garbage. That fallibility is what lets the CLI dispatch blindly over a registry of codecs and still fail cleanly.

Canonical content

What a codec aims at matters as much as the seam. A codec does not translate a file straight into ad-hoc elements; it targets a canonical model for a kind of content, and that model is shared by every codec of its kind. The JSON, Markdown and HTML codecs all read and write the same document model. An image codec for PNG and one for OpenEXR would target the same image model.

That shared target is why a .png and an .exr would land in the same structure inside Prism, and why Prism can sit in the middle of a conversion as a neutral form:

kinogaki convert notes.md   notes.html     # markdown → document model → html
kinogaki convert data.json  data.prisma    # json → a navigable Prism document

Convert notes.md to notes.html and the two files parse to the identical document model (same headings, same lists, same links) because both codecs speak it. There is no per-pair adapter; there is one model and a codec on each side.

A worked example: Markdown → Prism → HTML

Watch a real file pass through. Start with a Markdown note:

# Notes

A paragraph with *emphasis*.

load it and the Markdown codec lands it on the document model, a tree of typed elements. A document's blocks and inline runs are ordered body content, so they're nameless elements: their order is their identity, and the file reads as the structure itself.

#include "kinogaki/Codecs.h"
using namespace kinogaki;

Document doc;
doc.load("notes.md");                  // Markdown → Document (Codec::Auto picks it from the extension)
std::string prisma = doc.toString();   // the document model, as .prisma ASCII
#prisma 4.0
def document "document" {
    def heading {
        int32 level = 1
        def text {
            str text = "Notes"
        }
    }
    def paragraph {
        def text {
            str text = "A paragraph with "
        }
        def emphasis {
            def text {
                str text = "emphasis"
            }
        }
        def text {
            str text = "."
        }
    }
}

That is a plain, diff-clean Prism document you can read, inspect, and edit: def heading {, def paragraph {, def text {, the hierarchy carrying the meaning. Save the same document as HTML and the HTML codec writes the same model, reading the shared structure straight through:

doc.save("notes.html", Codec::Html);
<h1>Notes</h1>
<p>A paragraph with <em>emphasis</em>.</p>

The heading, paragraph, emphasis, and text elements are the shared model both codecs speak: load through one, save through the other. (These docs are built this way: authored in Markdown, normalized through this model, rendered to the page you're reading.)

The content kinds

Each kind of content has one canonical model, and Prism's native shape is the scene/structured kind:

Animation lives on the time-sample axis cutting across all of these kinds: an animated GIF is the image kind plus time; a moving camera is the scene kind plus time.

All four kinds reduce to one substrate, which is why they share it. An image channel, a vertex position, a vector control point and a heading's text are all typed array values; layers and AOVs are named properties, the same mechanism that carries a material parameter. That shared substrate is why the canonical models can be shared at all.

A lossless contract

Every codec is held to a stated contract. A within-format round-trip is the identity: convert doc.md → doc.prisma → doc.md and you get your file back. Cross-format degradation is defined and tabulated, so every loss is documented. A construct a target format cannot express degrades in a documented way (an HTML-only island, say, becomes a verbatim raw block on the way to Markdown), so you always know what a conversion will and will not preserve.

The currently shipping codecs cover JSON, Markdown, HTML, SVG, plain text (as lines) and arbitrary blobs (as bytes), enough that anything you hand the CLI converts cleanly, and enough to build these docs entirely out of converted Markdown.