papyr

MoonBit document platform – Document model, Markdown rendering, and page generation

moonbit
markdown
documents
moon add f4ah6o/papyr@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
2 months ago
Downloads
29

Dependencies

README

#papyr.mbt

papyr.mbt is Papyr's canonical MoonBit implementation and the workspace that currently builds the docs site. The old f4ah6o/papyr TypeScript repository is kept only as migration reference material and will be archived after the MoonBit migration is verified.

MoonBit is the source of truth for new implementation work. TypeScript packages under packages/* remain as npm compatibility entrypoints for the current workspace, but new parser, formatter, adapter, projection, and rendering logic should be added to MoonBit first.

#What lives here

  • papyr.normalized-document.v0 and the decoder that accepts it.
  • A MoonBit core PapyrDocument API and validation middleware for authoring payloads.
  • MoonBit Markdown parse/serialize/format APIs for headings, paragraphs, inline marks, links, flat lists, and fenced code blocks.
  • MoonBit adapter scaffolding, starting with an in-memory document adapter.
  • Stable diagnostics JSON for decode and projection failures.
  • A CLI with compatibility commands for docs builds and the migrated markdown/json conversion surface.
  • Core document stack tool envelopes for validation, Markdown conversion, preview/search, and workspace/publication export over PapyrDocument.
  • tmpx-backed HTML rendering for static pages and partial fragments.
  • Optional mx-* helper emission for mhx-style progressive enhancement.
  • Fixtures, golden-style tests, and the frozen compatibility snapshot copied from the current papyr docs corpus.
  • The vendored docs workspace under apps/docs and packages/*.

#Acknowledgements

The readability refresh for apps/docs was informed by Mikan demo and the WebRTC SFU Sora documentation published through that demo. Papyr adapts those references around its own docs model: a constrained reading width, persistent document navigation, nearby search, a Markdown source route, and clear separation between rendered pages and source content.

#Ownership boundary

  • MoonBit owns document modeling, Markdown conversion, formatter behavior, adapters, normalized-document decoding, deterministic diagnostics, publication projection, artifact emission, and tmpx-backed HTML rendering.
  • JS/TS owns npm compatibility entrypoints, the React UI, playground routes, the Worker runtime, and Cloudflare or R2 integration until each surface is migrated or intentionally kept as runtime glue.

  • docs/compatibility/README.md freezes the current docs-site compatibility contract.
  • docs/build-content-bridge.md explains the JS parse -> MoonBit projection bridge used by apps/docs/scripts/build-content.ts.
  • docs/vendored-workspace.md explains why the docs app is vendored here and how to keep that workspace reproducible.
  • docs/post-parity-boundary.md records the next ownership step after the parity milestone.

#Vendored docs app

The current docs app and the TypeScript packages it depends on are vendored into this repository:

  • apps/docs
  • packages/*

This keeps docs-site parity work self-contained in papyr.mbt. A sibling checkout of f4ah6o/papyr must not be required at build time or runtime.

During the current migration milestone, papyr.mbt is the source of truth for the vendored docs workspace. Sync from upstream is manual and explicit. Do not overwrite MoonBit additions with old papyr code; port behavior into MoonBit packages and keep TypeScript logic only where it is required for package compatibility or browser/runtime integration.

#Workspace reproducibility

The vendored docs workspace is expected to build entirely inside this repository.

Use these commands as the baseline check:

pnpm install --frozen-lockfile pnpm run verify:vendored-workspace

These commands verify that the lockfile is enough to install the vendored workspace, package resolution stays inside papyr.mbt, and the docs app still typechecks and builds without checking out f4ah6o/papyr.

#Release / npm publish

Public packages/* are published to npm under the @f12o/papyr-* scope. packages/test-support stays private and is excluded from publish.

  • Versions use CalVer YYYY.M.PATCH, for example 2026.4.0.
  • All published packages move in lockstep to the same version.

#Bump package versions

pnpm bump 2026.M.X

#Manual publish (first release)

Use this first, from a workstation already logged in to npm with publish rights on the @f12o scope:

pnpm install --frozen-lockfile pnpm -r build pnpm publish:packages

#Trusted publishing (after npm setup)

Once each published package is configured on npmjs.com with a Trusted Publisher that points at GitHub repo f4ah6o/papyr.mbt and workflow file publish.yml, publish by pushing a tag:

git tag v2026.M.X git push origin main --follow-tags

This triggers .github/workflows/publish.yml, which uses GitHub OIDC trusted publishing with --provenance. No NPM_TOKEN secret is needed.

#Normalized document shape

{ "schemaVersion": "papyr.normalized-document.v0", "id": "docs/intro", "path": "docs/intro.md", "frontmatter": { "title": "Intro", "tags": ["guide"] }, "blocks": [ { "type": "heading", "depth": 1, "text": "Intro" }, { "type": "paragraph", "children": [ { "type": "text", "value": "Hello" } ] } ] }

#Core PapyrDocument contract

The core package exports PapyrDocument as the shared MoonBit authoring shape:

{ "id": "intro", "title": "Intro", "blocks": [ ["Heading", { "id": "b1", "level": 1, "content": [{ "text": "Intro", "marks": [] }] }] ], "meta": { "source": "markdown" } }

parse_papyr_document_json, validate_papyr_document_json, and validate_papyr_document return stable envelopes with ok, tool, document, and diagnostics. Validation rejects empty document ids, heading levels outside 1..6, non-canonical inline marks, link marks without href, and empty list items. App Server create/update paths validate optional document JSON before storage and keep the previous stored document on failure.

#CLI

The CLI is intentionally machine-readable. The same JSON envelopes are reused by the current subprocess bridge and a future WASM caller.

moon run src/cmd/main --target native -- markdown_to_document '# Intro' intro moon run src/cmd/main --target native -- validate_document '{"id":"intro","blocks":[]}'

#decode

decode reads one normalized document file or one normalized document array file. If no path is given, it reads the same JSON payload from stdin.

On success it prints:

{"ok":true,"command":"decode","documents":[...]}

On failure it prints:

{"ok":false,"message":"...","diagnostics":[...]}

Exit code 0 means decode succeeded. Exit code 1 means parse, validation, or usage failure.

#build-docs

build-docs expects positional arguments <raw-dir> <generated-at> [documents-envelope.json].

If documents-envelope.json is omitted, build-docs reads the current docs envelope from stdin:

{"documents":[...]}

#markdown-to-json

markdown-to-json <markdown> [document-id] parses the implemented MoonBit Markdown subset and prints a PapyrDocument JSON payload:

moon run src/cmd/main -- markdown-to-json '# Intro' intro

The current migration slice supports headings, paragraphs, bold/italic/code inline marks, links, flat ordered and unordered lists, fenced code blocks, and Papyr mirror fences for table, Mermaid, and Excalidraw blocks. Tables serialize as papyr-table fenced JSON, Mermaid blocks serialize as mermaid fences, and Excalidraw blocks serialize as papyr-excalidraw fenced JSON.

#json-to-markdown

json-to-markdown <papyr-document-json> reads the MoonBit PapyrDocument JSON shape and serializes the implemented Markdown subset:

moon run src/cmd/main -- json-to-markdown '{"id":"intro","title":"Intro","blocks":[["Heading",{"id":"b1","level":1,"content":[{"text":"Intro","marks":[]}]}]]}'

#format-markdown

format-markdown <markdown> parses and serializes the implemented subset. It is the MoonBit-first replacement surface for the old formatter package, with parity to be expanded in small tested slices.

#Document stack tool commands

The underscore commands return the shared tool envelope:

{"ok":true,"tool":"markdown_to_document","result":{},"diagnostics":[]}

Available commands are validate_document, markdown_to_document, document_to_markdown, format_markdown, preview_document, search_documents, export_publication, export_workspace, and export_papyr_bundle. validate_document and document_to_markdown accept a PapyrDocument JSON argument. preview_document, search_documents, export_publication, and export_workspace accept the command input object used by the bridge dispatcher. export_papyr_bundle accepts { "documents": [...],"generated_at": "..." } and emits a self-contained Papyr bundle JSON artifact. The canonical source remains the documents array of PapyrDocument JSON. The bundle also includes schema_version, artifact_kind, generated_at, markdown_mirrors, preview_html, and slides; slide artifacts are split at level-2 headings and include both slide document data and slide HTML. Markdown mirrors are export conveniences and are not the canonical source.

The generic tool <json> command accepts:

{"tool":"preview_document","input":{"document":{"id":"intro","blocks":[]}}}

build-docs and build-docs-source keep the existing artifact-bundle envelope used by the vendored docs app bridge. When MoonBit projection returns no files, the TypeScript build pipeline falls back to JS artifact synthesis.

#Local issue workflow

Work is tracked in repo-local Markdown files under issues/, not in GitHub Issues.

Active work stays in issues/. Blocked work moves to issues/pending/. Completed work moves to issues/closed/.

Issue files use the filename format YYYYMMDDTHHMMSSZ-<category>-gh-<number>-<short-description>.md and should be moved with git mv so history stays attached to the same file.

AGENTS.md contains the operational rules for pending, closed, reopen, and the required metadata fields.