Skip to content
CorvidLabsPublic

About

🧊 Markdown with a Z axis. A plain-text format for documents with depth (time, layers, frames, space), with conformance-verified parsers in Swift, TypeScript, and Rust.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

165 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

3md

CI spec coverage Release License: MIT Live demo

Markdown with a Z axis. A .3md file is ordinary Markdown extended along one free axis: stack your content into planes and tell the reader what the depth means. Time for a daily planner. Frames for an animation. Layers for annotations. Space for a scene.

ThreeMD 2.2.1 is the current library. Tag v2.2.1 is 5411a9a, and the GitHub release was published on 2026-10-08. The text file is the .3md you edit. Binary stores those same frames as fields (.binary, payload kind 2). Kind 1 is deprecated. A file is parsed and saved at whatever size the process can hold. Linked file composition from 2.0.0 remains. npm serves 2.2.1. crates.io still serves threemd 2.1.0. The Homebrew formula threemd is 2.2.1. See Text file and binary, the 2.2.1 release notes, and linked file composition.

Browser viewer

Open the viewer & editor to read planes in Preview, rotate a sculpture in Cubes, edit a compatible grid in Slice, or change its source in Edit. Open a local file/folder, or paste a public GitHub repo, folder or file into the GitHub field.

Try it Open in the viewer
.3md sculpture Reusable canopy
Reading planes Layered notes β€” choose Preview
Uncompressed .3mdb, kind 2 The same canopy as binary
Self-contained binary composition Shared grove β€” choose an Inside entry
Public folder or repo Examples folder Β· Repository

The viewer guide covers older kind-1 binary, linked local folders, precise camera movement, Slice tools, downloads and current limits. LZFSE and Sculpt's default compact .3mdb need a readable or upstream uncompressed export. Cubes/Slice support bounded character grids; prose remains readable in Preview. GitHub linked folders and non-GitHub direct binary URLs have the documented loading gaps. Edits download as copies; the browser does not write back to a folder or GitHub.

Sculpt.3md

Sculpt.3md is the Mac app in this repository, at apps/sculpt. It paints a volume as translucent cubes or ASCII. Its package name stays Rook, and it builds against the ThreeMD library in this checkout. Default saves stay the app's compact .3mdb. That compact file is not the upstream ThreeMD binary standard. Readable .3md, and uncompressed ThreeMD binary, are explicit exports. The root package stays the cross-platform format library: Swift, TypeScript, and Rust.

Try the interactive demo (also on corvidlabs.xyz/3md), open the viewer & editor (paste any .3md and share a link), read the docs, or browse the curated animated gallery, or flip through the animated deck where the strongest examples appear as motion cards.

This repo eats its own dog food: selected project docs are also combined into one docs.3md (each Markdown file is a plane). GitHub can't preview .3md natively, so open all the docs in the 3md viewer and scrub through them.

The 3md interactive demo: planes stacked along the Z axis with a synced source view

Text file and binary is the bouncing dot: four frames, saved as the .3md you edit and as the binary file .binary writes. Kind 1 is deprecated. A file is parsed and saved at whatever size the process can hold.

---
3md: 0.1
axis: time
title: My Week
---
@plane z=0 label="Monday"
# Monday
- [ ] Standup

@plane z=1 label="Tuesday"
# Tuesday

This repository holds the format specification (SPEC.md), example documents (Examples/), and four libraries checked against one conformance suite (conformance/): ThreeMD, a cross-platform Swift parser; a TypeScript port in js/; a Rust crate in rust/; and a Godot 4 addon in gdscript/.

ThreeMD 2.0.0 adds general .3mdb storage, self-contained document composition, linked file composition, stable optional identities, atomic revision-checked edits and structured diagnostics in Swift, TypeScript and Rust. The uncompressed container and shared extension fixtures are portable. LZFSE is available only through Swift's conditional Apple backend; the ports return an explicit unsupported-backend error. These library APIs perform no file or network I/O. See the release notes and migration guide and editing capability matrix.

Cross-language file interchange is verified by a development gate: each Swift, TypeScript and Rust writer feeds every reader, for all nine pairings. It checks canonical text, uncompressed binary, composition references, linked-file bundles and imported edits. The interchange catalog states the cases, exact-byte checks and platform exceptions. This does not claim portable LZFSE or a shared JSON snapshot/patch transport.

The Godot addon reads and writes the same text, payload kind 1, and payload kind 2 documents. It also builds composition profiles, resolves linked files from bytes the caller supplies, and applies revision-checked document edits. LZFSE is refused. Parser, storage, composition, and editing scripts do not open files. ThreeMDFiles reads project paths a game has already chosen. Copy gdscript/addons/threemd into a project's addons folder and enable the plugin. On Godot 4.7 the plugin imports .3md and .3mdb as ThreeMDDocumentAsset resources and does not change the open scene. Planes stay data. gdscript/examples walks layers, a linked grove, both payload kinds, composition, edits, and a node map. Run fledge run gdscript for the headless suite. That suite is local. The verify lane above stays on the nine Swift, TypeScript, and Rust pairs until hosted CI installs Godot. With Godot 4 on PATH, fledge run gdscript-interchange adds the GDScript adapter and checks all sixteen writer/reader pairs.

Why

Markdown is two dimensional. Plenty of documents are not: a planner moves through time, an annotated contract has overlay layers, an ASCII animation is a stack of frames. 3md keeps Markdown's plain-text simplicity and adds one axis, with the author declaring what that axis means. Nothing comparable ships today; the closest prior art renders existing Markdown into 3D rather than giving the text a depth dimension of its own.

Text file and binary

Two saves of one document. The pictures in this section are the same frames both ways.

The text file is the .3md you edit. It is ordinary UTF-8. A frame is an @plane line plus that frame's Markdown. Open it in any editor.

Binary is that same document stored as fields. .binary writes it. Each frame keeps its number, its name, and its body, and the @plane line is not written again. The file begins with 40 bytes, 3mdbin and a short header, then the fields. This is payload kind 2.

Kind 1 is deprecated. It was the ThreeMD 2.0 binary file: the text file copied after that same 40-byte header. The frames are not stored as fields. Readers still open old kind 1 files. encodeTextContainer still writes one when a 2.0 reader must open the file. New files use .binary. The charts leave kind 1 out.

The bouncing dot's four frames twice. Left, the text file, each frame under an @plane line, 579 bytes. Right, the binary save, frame number and name, 516 bytes.

Examples/animation.3md is the bouncing dot. It crosses a dotted field, left to right, then back. The text file is 579 bytes. The binary file is 516 bytes, because the @plane lines are left out.

The bouncing dot. Each frame shows the text file on the left, including the o and dot lines, and the same lines stored as binary on the right.

The moving picture steps through the four frames. Left is the text file. Right is the binary save of that same frame.

The forgetting poem, eleven frames, then the week planner. Each frame is the text file on the left and the binary save on the right.

The second picture is two more files from Examples/. The poem drops one word at a time, then builds a shorter sentence back. Text file 940 bytes, binary 751. The week planner is seven days, Monday through Sunday: text file 797 bytes, binary 685. The words on the right are the body stored from the left.

One note at four sizes

Harbor is a day at the water: seven hours, and the text file is exactly 1,024 bytes. Binary of that same day is 922 bytes. The picture plays those seven hours, then the same kind of note at 10 MB, 100 MB, and 1 GB.

10 MB means 10 Γ— 1,024 Γ— 1,024 bytes. 100 MB and 1 GB are the next two steps, so 1 GB is 1,073,741,824 bytes, which is 1,048,576 times the harbor day. The three large files are not in the repository. Each of their frames starts with # Harbor and The light stays on the water., and that sentence repeats until the text file is the exact size.

Harbor, seven hours in a 1,024-byte text file beside the binary save, then the same note at 10 MB, 100 MB, and 1 GB.

Four cards for one note: 1,024 bytes, 10 MB, 100 MB, and 1 GB. Each card shows the text file and the binary save, and the read and write times for that file only.

Text file What it is Text bytes Binary bytes Text read Binary read Text write Binary write
1,024 bytes Harbor, seven hours 1,024 922 0.029 ms 0.005 ms 0.007 ms 0.003 ms
10 MB 10 frames of the sentence 10,485,760 10,485,627 4.863 ms 2.834 ms 1.613 ms 2.985 ms
100 MB 100 frames of the sentence 104,857,600 104,856,063 49.716 ms 29.156 ms 15.851 ms 29.252 ms
1 GB 1,024 frames of the sentence 1,073,741,824 1,073,725,480 464.691 ms 297.503 ms 389.117 ms 306.565 ms

One Rust 1.95.0 release process on an Apple M1 Ultra (macOS 26.5.2, 64 GB). Each time is the median of five calls after one warmup. This is not the speed gate. At 1,024 bytes the times are a few hundredths of a millisecond. At 1 GB, reading the text took 464.691 ms and reading the binary took 297.503 ms. Writing the text took 389.117 ms and writing the binary took 306.565 ms.

In this run, reading the binary was quicker at every size. Writing the binary was quicker for the 1,024-byte day and for the 1 GB file. At 10 MB and 100 MB, writing the text was quicker. Binary is 102 bytes smaller at 1,024 bytes, 133 bytes smaller at 10 MB, 1,537 bytes smaller at 100 MB, and 16,344 bytes smaller at 1 GB. A large frame is almost all that repeated sentence, so the two files end up nearly the same size. Both are parsed and saved.

The bars inside one card compare only with each other. A long blue bar means that card's slower time, not a bigger file. The 1 GB harbor file is 1,024 long frames. It is not the cube of short lines further down, and a long line is quicker to read than a million short ones.

flowchart LR
  source[".3md text file"] --> parsed[Parse]
  parsed --> document[Document]
  document --> textOut["Text file"]
  document --> binary["Binary: the same frames as fields"]
Loading

A reader tells the two apart by the first bytes. A text file does not start with 3mdbin. A binary file does, and its kind byte is 2. A deprecated kind 1 file starts with the same magic and its kind byte is 1. It is opened as the text that was copied in.

flowchart TD
  bytes[Input bytes] --> magic{Starts with 3mdbin?}
  magic -->|no| asText[Read it as the text file]
  magic -->|yes| kind{Kind byte}
  kind -->|2| asBinary[Read the frames as fields]
  kind -->|1| legacy[Deprecated. Read the copied text.]
  kind -->|other| bad[unsupportedPayloadKind]
Loading

The 40-byte header at the start of a binary file. The kind byte is 2, which means the frames are stored as fields.

The text file has no header. Those 40 bytes are why a binary file is not just a renamed .3md. The kind byte is 2 for the field save.

Six files

Each pair is one file. Blue is the text file. Green is the binary save.

Six files. Each pair is the text file and the binary save. Grove's binary file is one byte larger than its text, 1,013 against 1,012. The poem's binary file is 751 bytes against 940 of text.

Document Text file Binary
canopy.3md 290 263
animation.3md 579 516
daily-planner.3md 797 685
shared-grove.3md 1,012 1,013
kinetic-erasure-poem.3md 940 751
dna-double-helix.3md 6,919 6,576
conways-game-of-life.3md 3,304 3,049

Open canopy and the two layers are small fenced pictures: a dot is open and a hash is a solid tile. The bouncing dot crosses four frames. The planner is Monday through Sunday. Grove is one JSON block: it stores the canopy once and places it three times, and it is the row where binary is one byte larger (1,013 against 1,012). The poem is the large drop: 751 bytes is 79.9% of 940. The helix is twenty dotted rungs, a hash for the backbone and letters for the bases. Conway is in the table and left off the picture. Its text file is 3,304 bytes and its binary save is 3,049. Open it and each generation is a small grid: a dot is an empty cell and o is a live cell.

Adding many files together

Three sets of files, text and binary. 293 examples: binary is 31,693 bytes smaller. sculpt-4096 is 4,096 layers of 32 by 20, not a 1024 cube.

How many of 293 example files have a binary save at each percent of the text size. 100% would match the text. Together the files are 97.3% of the text. A typical file is 97.4%. The tall bar is 108 files at 98%.

Input What it is Text bytes Binary Binary / text
293 example files The committed size list 1,178,967 1,147,274 0.973
synthetic-2000 One file: 2,000 planes of mixed Markdown 4,037,480 3,998,362 0.990
sculpt-4096, 32 by 20 One file: 4,096 layers of a 32 by 20 picture. Not a 1024 cube 3,031,345 2,934,090 0.968

Binary for the 293 files is 31,693 bytes smaller than the text (2.7%). Together those files are 97.3% of the text. That is the committed ratio, and it is the 97.3% in the picture. A typical file, the median, is 97.4%. The biggest saving is the poem, at 79.9%. The smallest saving is annotated-contract.3md, at 99.2%. Every file in the list is under 100%. The tall bar is 108 files at 98%. Those totals are the committed check in conformance/structured/sizes.json. The test g6_kind_2_sizes_match_sizes_json checks them. The two generated files are not committed. The sizes are.

Reading the text file vs reading the binary

This picture is a speed comparison, not another size comparison. Each row is one made-up document of the letter a. Blue is how long the text file took to read. Green is how long the binary file took.

The two bars in a row compare only with each other. Do not compare bar lengths down the picture. A full blue bar means "this row's text time," not "a big file."

Five documents. In each row the text file took about 5 to 7 times as long to read as the binary file. The largest row is about 50 MB: 104.403 ms for text, 19.287 ms for binary. One Rust run, not the speed gate.

Document Text bytes Binary bytes Text read Binary read
64 planes of 256 a 17,318 16,763 0.078 ms 0.012 ms
512 planes of 256 a 138,690 134,140 0.591 ms 0.091 ms
4,096 planes of 256 a 1,113,050 1,073,148 4.649 ms 0.690 ms
8 planes of 1 MB 8,388,760 8,388,715 17.322 ms 3.139 ms
8 planes of 6 MB, about 50 MB 50,331,800 50,331,763 104.403 ms 19.287 ms

One Rust 1.98.0 release process on an Apple M1 Ultra (macOS 26.5.2, 64 GB). Each time is the median of five calls after one warmup. The project speed gate is still open. It wants three processes per language, and these numbers are not that gate. At about 50 MB, binary is 37 bytes smaller than the text.

How big a file can be

There is no fixed size stop in the library. A document is parsed and saved when the process can hold it. These local runs are the ones that were actually saved:

  • A 1 GB cube, 1024 Γ— 1024 Γ— 1024 letters, in Swift, TypeScript, and Rust. All three wrote the same bytes.
  • The harbor note at 1,024 bytes, 10 MB, 100 MB, and 1 GB, in Rust. One note at four sizes shows that run.
  • One plane of 5 GB of the letter a, in Rust.

A 10 GB file has not been measured. The library does not refuse that size. The process has to be able to hold the file. CI does not allocate the 1 GB or 5 GB files.

Binary is not a compressor. A document made of 1 GB of letters stays about 1 GB as binary. The header is 40 bytes either way.

The default limit is the largest integer the language uses for a size:

Library Default limit
Swift Int.max
Rust usize::MAX
TypeScript Number.MAX_SAFE_INTEGER (9,007,199,254,740,991)

That one default covers the file, the decoded text, the number of lines, the number of planes, and each line or plane body. A caller can pass a smaller positive limit. Zero, a negative number, and a number JavaScript cannot hold exactly are invalidLimits. A caller who wants the old stop can pass 67,108,864 (64 MiB).

On a 32-bit process the language integer stops near 2 GB. That is the language. A 5 GB length does not fit in a signed 32-bit integer. A hostile file can still use all of the machine's memory, because the library reads and writes the document it is given.

A page of 1024 by 1024 letters

One plane, 1024 lines of 1024 letters a, measured with the Rust 1.98.0 release build (median of five reads after one warmup, one run, not the speed gate):

Save Bytes Read time
Text file 1,049,641 2.307 ms
Binary 1,049,654 0.384 ms

On that page binary is 13 bytes larger than the text, because a wall of one letter has almost no syntax to remove. It was still quicker to read in this run.

The same number of letters, split into 1024 planes of one 1024-letter line, does get smaller. The @plane lines go away: text 1,063,879 bytes, binary 1,054,706 bytes.

A cube of 1024 such pages is 1024 Γ— 1024 Γ— 1024 letters, which is 1,073,741,824 bytes before the @plane lines. One local run on this Apple M1 Ultra parsed that cube and saved it again in Swift, TypeScript, and Rust. All three wrote the same bytes: text 1,074,804,681, binary 1,074,796,532. Binary is 8,149 bytes smaller than the text. Rust 1.98.0 release wrote the text in 3.413 seconds and read it in 2.539 seconds, and wrote the binary in 0.408 seconds and read it in 0.406 seconds. That run is one process, not the speed gate.

The same Rust build also parsed and saved one plane of 5 GB of the letter a (5,368,709,120 bytes). The text file was 5,368,709,164 bytes. The binary file was 5,368,709,179 bytes, 15 bytes larger, because a wall of one letter has almost no syntax to remove. Writing the text took 23.501 seconds and reading it took 16.208 seconds. Writing the binary took 2.360 seconds and reading it took 2.348 seconds. That 5 GB plane is the largest file measured. A 10 GB file has not been run.

A one-plane body of 64 letters a is the tiny file that grows: text 106 bytes, binary 117 bytes.

Installation

Swift Package Manager

Add the package to your Package.swift:

.package(url: "https://github.com/CorvidLabs/3md", from: "2.2.1")

Then depend on the ThreeMD library product:

.product(name: "ThreeMD", package: "3md")

from: "2.2.1" resolves from tag v2.2.1. npm serves 2.2.1. crates.io still serves threemd 2.1.0. The Homebrew formula threemd is 2.2.1. Read the 2.1 migration notes before upgrading: a 2.0 reader rejects kind 2, and Rust's canonical number spelling changes for 92 powers of two. The 2.0 migration guide still covers the earlier lossy-serialization change. The 2.2.1 release notes cover the Godot load fix. The 2.2.0 release notes cover the Godot addon.

JavaScript / TypeScript

A TypeScript library lives in js/, alongside the <three-md> web component (@corvidlabs/three-md-element). All three implementations (Swift, TypeScript, and the Rust crate in rust/) are kept in sync by the shared conformance suite (conformance/).

The publication workflow publishes to the public npm registry. Install with:

bun add @corvidlabs/threemd

npm serves @corvidlabs/threemd 2.2.1 and @corvidlabs/three-md-element 2.2.1. If an older installation maps @corvidlabs to GitHub Packages, point that scope at the public npm registry. The web component keeps text rendering; the library exports the storage, composition, linked-file and editing APIs.

No install needed just to use it: try the hosted editor and viewer, or load the self-contained component bundle directly with <script type="module" src=".../three-md.js">.

import { danglingLinks, linkGraph, parse, serialize } from "@corvidlabs/threemd";

const document = parse(source);
console.log(document.axis); // "time"
console.log(danglingLinks(document)); // unresolved [[z=N]] references
console.log(linkGraph(document));     // compact source -> target edge list

// Round trips back to text:
const text = serialize(document);

Rust

The threemd crate's publication workflow targets crates.io. Install an available published version with:

cargo add threemd

crates.io serves threemd 2.1.0. The 2.2.1 crate publish failed because CRATES_IO_TOKEN is not configured. The crate manifest in this repository reads 2.2.1. The crate still pins unicode-normalization =0.1.25. Its serde/serde_json dependencies are development-only.

let document = threemd::parse(source)?;
println!("{}", document.axis); // "time"

Godot

Copy gdscript/addons/threemd into the game's addons folder and enable ThreeMD in Project Settings. The plugin imports .3md and .3mdb files and does not change the open scene. A running game calls load() on that imported project path. The addon registers the loader for ThreeMDDocumentAsset, so the game does not add a second script. gdscript/examples/load_imported.gd does that for res://examples/grove/scene.3md and for the kind-2 scene.3mdb this addon writes beside it. The samples in gdscript/examples cover layers, linked files, both payload kinds, composition, and edits. The larger document set remains in Examples/.

var document = ThreeMDParser.parse(source)
var bytes = ThreeMDStorage.encode_binary(document)
var loaded = ThreeMDFiles.load_document("res://examples/grove/scene.3md")
var asset: ThreeMDDocumentAsset = load("res://examples/grove/scene.3md")

ThreeMDParser, ThreeMDStorage, ThreeMDComposition, ThreeMDFileComposition, and ThreeMDEditing are pure. ThreeMDFiles is the only library script that reads paths. The importer runs in the editor. A failure is a ThreeMDError with a code string, the same codes the other libraries use. These scripts run on Godot 4.7.2, the current stable release.

Library usage

ThreeMD 2.0.0 adds optional stable plane/reference identities, immutable revision-checked document/composition patches and structured diagnostics above the existing parser in Swift, TypeScript and Rust. General uncompressed binary, composition and linked-file APIs are implemented in all three. See the release scope and capability matrix. Releases before 2.0.0 do not contain these APIs.

import ThreeMD

let document = try Parser().parse(source)
print(document.axis)          // Axis(rawValue: "time")
for plane in document.planesByZ {
    print(plane.label ?? "", plane.body)
}
print(document.danglingLinks()) // unresolved [[z=N]] references
print(document.linkGraph())     // compact source -> target edge list

// Round trips back to text:
let text = Serializer().render(document)

Binary storage and reusable documents

The byte counts and the pictures are in Text file and binary. ThreeMD 2.0.0 provides bounded general-document storage and self-contained composition in Swift, TypeScript and Rust. The examples below use Swift; see EDITING-RELEASE.md for the TypeScript and Rust equivalents. The existing text parsers, command-line tool and hosted viewer retain their current text behavior.

import ThreeMD

let document = try Parser().parse(source)
let binary = try DocumentStorageCodec.encode(
    document, format: .binary(compression: .none)
)
let restored = try DocumentStorageCodec.decode(binary)

// Optional on platforms with Apple's Compression framework:
let compressed = try DocumentStorageCodec.encode(
    document, format: .binary(compression: .lzfse)
)

.binary writes the binary save: payload kind 2, the frames as fields, inside the version 1 container. It is often a little smaller than the text, and a page of repeated letters can be a few bytes larger. Decoding it does not parse the text file. Kind 1 is deprecated. encodeTextContainer still writes it, the text copied after the header, when a 2.0 reader must open the file. A 2.0 reader stops on kind 2 with unsupportedPayloadKind(2).

let legacy = try DocumentStorageCodec.encodeTextContainer(document)

The magic is 3mdbin\r\n. It is separate from Sculpt/Rook's older voxel-specific 3MDB container. The default storage limit is the largest integer the language can use. A caller can pass a lower positive limit. How big a file can be explains that, including a 32-bit process and a page of 1024 by 1024 letters. Unavailable compression, malformed headers, a non-positive limit, and cancellation produce errors. CRC detects corruption and does not authenticate content. The binary file uses that checksum.

Composition preserves a library of named documents and ordered references:

let root = Document(version: "0.1", axis: .layer, planes: [
    Plane(z: 0, body: "# Collection")
])
let composition = try DocumentComposition(rootID: "root", entries: [
    DocumentEntry(id: "root", document: root, references: [
        DocumentReference(targetID: "chapter", attributes: ["role": "first"]),
        DocumentReference(targetID: "chapter", attributes: ["role": "again"])
    ]),
    DocumentEntry(id: "chapter", document: document)
])
let text = try DocumentCompositionCodec.encode(composition)
let reloaded = try DocumentCompositionCodec.decode(text)

// The profile is itself a normal Document, so binary storage also works.
let profile = try DocumentCompositionCodec.document(for: composition)
let binaryComposition = try DocumentStorageCodec.encode(
    profile, format: .binary(compression: .none)
)

Each definition is saved once. All references resolve inside the supplied library, with checked IDs, targets, cycles, depth and work limits, including unused definitions. Documents keep their own axes; attributes have no built-in voxel or layout meaning. The library performs no file reads, URL resolution or automatic flattening. See SPEC.md for the binary layout and the composition profile.

The package keeps its existing deployment baseline. These synchronous APIs check cooperative task cancellation on macOS 10.15, iOS 13, tvOS 13, watchOS 6 and later, and supported non-Apple platforms. On earlier Apple runtimes that check is a no-op; storage and validation remain synchronous and available.

Actual canonical fixtures are in Examples/Extensions: readable canopy, portable binary canopy, readable shared grove, and portable binary shared grove. The fixture directory also includes LZFSE variants and a byte/hash manifest. The public Swift APIs generated all six documents and decoded them back to equal values. This demonstrates storage and references, without promising automatic character-to-model expansion. Gallery cards stay on text. The viewer page opens these uncompressed binaries and the composition profile.

Linked files

An ordinary document can name other 3md files in a 3md-files metadata ledger that maps single printable ASCII characters to relative filenames:

3md-files: "{\"1\":\"models/house.3md\",\"2\":\"models/tree.3md\"}"

The host reads the files it chooses and supplies their bytes. The library resolves the graph and can bundle it into the self-contained profile above:

let linked = try DocumentFileComposition.resolve(
    rootPath: "scene.3md",
    sources: [DocumentFileSource(path: "scene.3md", data: sceneBytes),
              DocumentFileSource(path: "models/house.3md", data: houseBytes),
              DocumentFileSource(path: "models/tree.3md", data: treeBytes)]
)
let portable = try DocumentCompositionCodec.encode(linked.composition)

Paths are project-relative POSIX paths that cannot escape the root. Missing files, cycles, invalid ledgers and exceeded limits are refused without partial results. See FILE-COMPOSITION.md, SPEC.md section 12.3 and the LinkedVillage example.

Command-line tool

The threemd CLI ships with the package and self-documents (run threemd --help). A path of - reads from standard input.

swift run threemd validate <file>   # parse a file; print "ok" or exit non-zero with the error
swift run threemd info <file>       # print version, axis, title, and each plane's position
swift run threemd links <file>      # list cross-plane links and dangling references
swift run threemd check-links <file> # exit non-zero when any [[z=N]] target is missing
swift run threemd html <file>       # render the document to HTML on stdout

validate, info, links, and check-links also accept --json for CI, editor integrations, and other tooling.

Format at a glance

  • A required --- frontmatter block declares 3md: (the version, and the file's magic marker), an optional axis:, an optional title:, and free metadata.
  • @plane z=... label="..." directives start planes; the Markdown between directives is the plane body.
  • A plain Markdown file with a 3md header and no directives is a valid one-plane document.

See SPEC.md for the full grammar and conformance rules.

Examples

The Examples/ directory holds the source example documents across 13 axis types - from medical charts, weather, and file transfers to games, maps, and animations. The gallery highlights the curated animated set. The viewer finds every text example in the tree, and it opens an uncompressed binary or a linked folder. A few source examples:

Want to see it work? docs/PROOF.md records how 3md was verified for machines (a blind AI authored valid 3md from the spec; the hosted text contract was checked in Swift, TypeScript, and Rust) and for people (plain, readable, diffable text).

Case studies

  • Planes as layers - a Godot 4.7 example maps planes onto nodes the game owns, and the addon does not spawn gameplay.
  • Load an imported grove - a game calls load() on an imported .3md and reads the title and plane labels.
  • Linked village - a glyph ledger names other files, and the library does not open them.
  • Shared grove - one embedded document is referenced by name, and the library does not read the filesystem.
  • Kind 2 canopy - the same document appears as text and as a kind-2 .3mdb. Gallery cards stay on text. The viewer page opens the uncompressed binary.
  • Conway frames - twenty-four stored generations, and the libraries do not run the rule.
  • Sunken Vault links - room exits are cross-plane links, and the libraries do not play the dungeon.

The index is docs/case-studies/README.md.

Development

This repo uses the CorvidLabs trust toolchain. Run the complete repository gate before calling a change done:

fledge trust verify

Trust validates the SpecSync contract, risk policy, and provenance posture, and composes the native fledge lanes run verify lane. That native lane runs the Swift format check, build, and tests plus the Rust crate, TypeScript parser parity, generated web-component bundle drift, and VS Code grammar tests. See AGENTS.md for the standing rules every contributor and agent follows. Browser UI tests are exposed separately with fledge lanes run ui. GDScript is a fourth library checked locally with fledge run gdscript when Godot 4.7.2 is installed, and hosted CI does not install Godot.

Each implementation has its own tests, and all three implementations run the shared 43-vector conformance suite in conformance/, which is the cross-implementation contract that keeps the parsers behaving identically.

Status

The 1.0 text grammar remains frozen. Specification 1.1 adds independently versioned binary storage, composition and linked file authoring without changing that grammar. ThreeMD 2.0.0, released on 2026-10-06, implements those extensions in Swift, TypeScript and Rust. The package version is separate from the format version. Specification 1.2 adds payload kind 2 inside that same container, and storage has no fixed size stop. ThreeMD 2.1.0 is tag v2.1.0, published on 2026-10-08. Sculpt.3md was nested on main after that tag. ThreeMD 2.2.0 is tag v2.2.0 at 54a6f30 and adds the Godot 4.7 addon. ThreeMD 2.2.1 is tag v2.2.1 at 5411a9a. A Godot 4.7 game can load() an imported .3md or .3mdb. npm serves 2.2.1. crates.io still serves threemd 2.1.0. The Homebrew formula threemd is 2.2.1. Older 3md: 0.1 documents remain valid: the parser is version-lenient and never rejects a document by its version string. See the 2.2.1 release notes.

License

MIT (c) CorvidLabs. See LICENSE.

About

🧊 Markdown with a Z axis. A plain-text format for documents with depth (time, layers, frames, space), with conformance-verified parsers in Swift, TypeScript, and Rust.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages