Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
hosts read.** The keys a schema declaration may carry, the refusal of an
unknown one with the path to it, and the mapping onto `Schema`, written
once. The WebAssembly host's walker was the only copy; it reads through the
crate now, and the command-line host will. Each host keeps its own reading
crate now, and so does the command-line host. Each host keeps its own reading
through a seven-method `Declaration` trait, so keys are refused before
values are converted: a hook written where none is allowed is refused as a
key, with the host's reason, not as an unconvertible function. Errors carry
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ assert_eq!(
`format(parse(s))` is idempotent, so a tool can rewrite a file in place, and
`parse(format(ast))` gives back the same tree, so formatting loses nothing.

## Command line

The same engine as a command, for a documentation repository that wants a CI
gate and for anyone with a Markdoc file to tidy:

```sh
cargo install --path crates/accent-proust-cli # the binary is `accent-proust`
accent-proust fmt --check docs/*.md
accent-proust validate --config schema.yaml --partials docs/partials docs/*.md
accent-proust render --config schema.yaml --var channel=stable docs/page.md
```

`fmt` reprints canonical source and, with `--check`, prints a diff and exits 1
if anything would change. `validate` reports `path:line:column: level[id]:
message` per error, or one JSON object per file with `--format json`, and
exits 1 on an error. `render`, `transform` and `parse` print HTML, the
renderable tree and the syntax tree. The configuration is a YAML or JSON file
in the same vocabulary the WebAssembly bindings read from an object, so a
schema declared for one host is accepted by the other; `--partials` is a
directory of files, which is the thing the browser cannot do. The crate's
[README](crates/accent-proust-cli/README.md) has the rest, exit codes
included.

## Bring your own CommonMark parser

The bundled tokenizer uses `pulldown-cmark`, behind the default
Expand Down Expand Up @@ -138,7 +161,7 @@ cargo test --test conformance -- --nocapture

## Contributing

The library's minimum supported Rust version is 1.82. Develop on stable, which
The library's minimum supported Rust version is 1.96. Develop on stable, which
the test suite needs. See [AGENT.md](AGENT.md) for the gates and the workflow.

## Licence
Expand Down
11 changes: 10 additions & 1 deletion crates/accent-proust-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ The library reads no files and decides no policy; a host does. This is the
second host beside the WebAssembly bindings, with one command per stage the
library exposes.

## Build
## Install

From a checkout of the repository:

```sh
cargo install --path crates/accent-proust-cli # puts `accent-proust` on PATH
accent-proust --help
```

Or build without installing:

```sh
cargo build -p accent-proust-cli --release
Expand Down
20 changes: 18 additions & 2 deletions crates/accent-proust-wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ keystroke needs. Call `config.free()` when you are done with it.
`nodes` takes the same shape as `tags`, keyed by Markdoc node type, so you can
change what a `heading` renders as.

The vocabulary -- which keys a declaration may carry, and what each means -- is
[`accent-proust-schema-config`](https://github.com/zoosky/accent-proust/tree/main/crates/accent-proust-schema-config),
shared with the [command line](https://github.com/zoosky/accent-proust/tree/main/crates/accent-proust-cli).
This host reads it from an object and the command line from a YAML or JSON
file, so a schema declared for one is accepted by the other: a JSON file the
command line reads is what `JSON.parse` hands `new Config` here.

### What crosses, and what does not

A schema is data and crosses whole: `render`, `children`, `attributes`,
Expand All @@ -188,7 +195,14 @@ A schema is data and crosses whole: `render`, `children`, `attributes`,

Attribute types are written as the strings `"String"`, `"Number"`, `"Boolean"`,
`"Object"`, `"Array"`. Markdoc uses the JavaScript constructors, and a
constructor is a function. An array of them is a union.
constructor is a function. An array of them is a union, one level deep: a
list inside the list is refused.

A schema object is read key by key, so a class instance or a proxy works as
well as an object literal. Only a value carried through whole -- an attribute's
`default`, a variable -- has to be a plain object, because a `Date` or a `Map`
would flatten to `{}` on the way. An explicitly `undefined` variable is
`null`, which is what `user: session?.user` means by it.

A hook is code, and code does not cross: `transform`, `validate`, a custom
attribute type, a `RegExp` in `matches`, and host-defined `functions`. **So the
Expand All @@ -204,7 +218,9 @@ expected String, Number, Boolean, Object, Array, or an array of those
```

A schema that half arrives is worse than one that does not, because the missing
half is invisible until an author trips over it.
half is invisible until an author trips over it. For the same reason a property
whose getter throws is refused as unreadable rather than read as absent: a
block that was written and then lost is that failure in another form.

## Not supported yet

Expand Down
7 changes: 4 additions & 3 deletions site/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ llms:
description: |
accent-proust is a Rust implementation of the Markdoc language: parse,
validate, transform, render, and format. It also compiles to WebAssembly
and is published to npm for use in a browser. These pages cover the Rust
API, the JavaScript API, the Markdoc language itself, the architecture,
and every declared divergence from upstream Markdoc.
and is published to npm for use in a browser, and ships as a command for
the shell. These pages cover the Rust API, the JavaScript API, the command
line, the Markdoc language itself, the architecture, and every declared
divergence from upstream Markdoc.
extra_links:
- title: "Source"
url: "https://github.com/zoosky/accent-proust"
Expand Down
20 changes: 18 additions & 2 deletions site/content/03.docs/02.javascript/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ keystroke needs. Call `config.free()` when you are done with it.
`nodes` takes the same shape as `tags`, keyed by Markdoc node type, so you can
change what a `heading` renders as.

The vocabulary -- which keys a declaration may carry, and what each means -- is
[`accent-proust-schema-config`](https://github.com/zoosky/accent-proust/tree/main/crates/accent-proust-schema-config),
shared with the [command line](/docs/cli). The browser reads it from an
object and the shell from a YAML or JSON file, so a schema declared for one
is accepted by the other: a JSON file the shell reads is what `JSON.parse`
hands `new Config` here.

### What crosses the boundary

```mermaid
Expand All @@ -218,7 +225,14 @@ A schema is data and crosses whole: `render`, `children`, `attributes`, `slots`,

Attribute types are written as the strings `"String"`, `"Number"`, `"Boolean"`,
`"Object"`, `"Array"`. Markdoc uses the JavaScript constructors, and a
constructor is a function. An array of them is a union.
constructor is a function. An array of them is a union, one level deep: a
list inside the list is refused.

A schema object is read key by key, so a class instance or a proxy works as
well as an object literal. Only a value carried through whole -- an attribute's
`default`, a variable -- has to be a plain object, because a `Date` or a `Map`
would flatten to `{}` on the way. An explicitly `undefined` variable is
`null`, which is what `user: session?.user` means by it.

### What does not

Expand All @@ -239,7 +253,9 @@ expected String, Number, Boolean, Object, Array, or an array of those
```

A schema that half arrives is worse than one that does not, because the missing
half is invisible until an author trips over it.
half is invisible until an author trips over it. For the same reason a property
whose getter throws is refused as unreadable rather than read as absent: a
block that was written and then lost is that failure in another form.

## Not supported yet

Expand Down
168 changes: 168 additions & 0 deletions site/content/03.docs/03.cli/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
title: Command line
template: docs
lead: >-
The same engine as a command: format Markdoc, validate it against a schema
file, and render it, with exit codes a CI pipeline can act on.
menu:
visible: true
order: 3
description: >-
The accent-proust command: fmt, validate, render, transform and parse, a
YAML or JSON schema file shared with the JavaScript bindings, partials from
a directory, typed variables, and the exit codes.
---

The library reads no files and decides no policy; a host does. `accent-proust`
is the host for a shell -- the command that makes a documentation repository
a CI gate, and the first place `{% partial %}` works without writing a Rust
program.

## Install

The crate is a workspace member, built from the repository:

```sh
cargo install --path crates/accent-proust-cli
accent-proust --help
```

## Commands

| Command | Does | Needs a schema file |
|---|---|---|
| `fmt` | Reprints Markdoc source in canonical form | No |
| `validate` | Reports what a schema says is wrong | For tags of your own |
| `render` | Prints HTML | For tags of your own |
| `transform` | Prints the renderable tree as JSON | For tags of your own |
| `parse` | Prints the syntax tree as JSON | No |

Every command reads the files named on its command line, or stdin when none
is named. A file that cannot be read is reported and the run goes on, so one
bad path does not hide the rest; the exit code says a path failed.

### `fmt`

```sh
accent-proust fmt docs/*.md # formatted source to stdout
accent-proust fmt --check docs/*.md # a unified diff per file that would change
accent-proust fmt --write docs/*.md # rewrite in place
cat page.md | accent-proust fmt # stdin to stdout
```

Spacing inside a tag is normalised; your own spellings are left alone, so
`__bold__` stays `__bold__`. `parse(format(ast))` returns the same tree, so
formatting loses nothing, and `fmt` reformats its own output until it stops
changing -- refusing, with exit 2, a document still changing after four
passes -- so write-then-check is clean. `--check` prints a
diff rather than a list, because a CI log reader wants to know what is wrong,
not only where. Line endings are written as LF; a CRLF file is named by
`--check` and rewritten by `--write`.

### `validate`

```sh
accent-proust validate --config schema.yaml docs/*.md
accent-proust validate --config schema.yaml --format json docs/page.md
```

One line per error, `path:line:column: level[id]: message`, lines and columns
counted from one and the column in characters, as an editor shows it. The
error ids are upstream Markdoc's, so tooling written against its codes reads
them unchanged.

`--format json` prints one object per input, one per line, carrying `file` and
its `errors` in the shape the [JavaScript bindings](/docs/javascript) return,
positions included -- `character` and `offset` in UTF-16 code units,
`byteOffset` in bytes -- so a consumer written against either host reads the
other.

Exit 1 means an error at level `error` or `critical`. A `warning`, `info` or
`debug` is printed and does not fail the run: that is how a schema ships a
rule it wants surfaced but not enforced yet.

### `render`, `transform` and `parse`

`render` prints HTML, inputs concatenated in order. `transform` prints the
renderable tree as JSON, one array per input, one per line, in the shape
upstream's renderers expect -- a tag is `{"$$mdtype": "Tag", "name",
"attributes", "children"}`. `parse` prints the syntax tree the same way,
every node in the field order of upstream's `Node` class, so the output is
what `JSON.stringify(Markdoc.parse(source))` gives, with two additions upstream
does not make: positions in the bindings' shape, and a `file` label on every
location. One value per line rather than one array for the run, because that
composes with `jq`.

## The schema file

```sh
accent-proust render \
--config schema.yaml \
--partials docs/partials \
--var version=3 --var channel=stable \
docs/page.md
```

`--config` is a YAML or JSON file declaring `tags`, `nodes` and `variables`,
in the vocabulary
[`accent-proust-schema-config`](https://github.com/zoosky/accent-proust/tree/main/crates/accent-proust-schema-config)
defines -- which keys a declaration may carry, what each means, and the
refusal of an unknown one with the path to it -- and the JavaScript
bindings' `Config` reads from an object. A schema declared for one host is
accepted by the other; a JSON file is the one spelling both read as it
stands, the shell from disk and the browser through `JSON.parse`:

```yaml
tags:
callout:
render: aside
attributes:
type:
type: String
required: true
matches: [note, warning]
variables:
channel: stable
```

An unknown key is refused with the path to it -- `config.tags.callout.validate`,
not "invalid schema". A hook cannot be written in a file: `transform` and
`validate` are code, and the refusal says to keep the hook in a Rust host.

> [!WARNING]
> **The command line is never stricter than a Rust host, only more
> convenient.** It sees what a tag declares and never a hook-level check. A
> schema whose real enforcement lives in a hook passes here and fails there.

`--partials DIR` reads every UTF-8 text file under the directory, at any depth,
and `{% partial file="sections/intro.md" /%}` finds it by that path. This is
the thing the browser cannot do, and the reason a command-line host exists.

`--var NAME=VALUE` declares a variable and overrides one the file declared.
`VALUE` is read as YAML by the same reader as the file, so the two can never
disagree about what `3` is:

| Written | Becomes |
|---|---|
| `--var count=3` | the number 3 |
| `--var debug=true` | the boolean true |
| `--var name=production` | the string `production` |
| `--var missing=null`, `--var missing=` | null |
| `--var 'version="3"'` | the string `3` |
| `--var 'tags=[a, b]'` | a list of two strings |

## Exit codes

| Code | Means |
|---|---|
| 0 | Success; for `fmt --check`, nothing would change |
| 1 | A document has a problem: a file that would change, an error at level `error` or `critical` |
| 2 | A usage error, a file that could not be read or written, a schema file that does not declare, or a document the formatter does not settle on |

1 and 2 are kept apart so that CI can tell "the docs are wrong" from "the tool
is misconfigured". They are different alerts.

## Reference

The crate's [README](https://github.com/zoosky/accent-proust/blob/main/crates/accent-proust-cli/README.md)
is the full reference, and `accent-proust <command> --help` prints every flag.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ lead: >-
ships built in, and where the language stops and your schema starts.
menu:
visible: true
order: 3
order: 4
description: >-
A reference for the Markdoc language as accent-proust implements it: tags,
annotations, attributes, variables, functions, partials, and the built-in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
title: Architecture
template: docs
lead: >-
Five pure stages, one trait seam, two responsibilities left outside, and two
ratchets that keep the port honest.
Five pure stages, three trait seams, a workspace of hosts around them, and
two ratchets that keep the port honest.
menu:
visible: true
order: 4
order: 5
description: >-
How accent-proust is put together: the pipeline stages, the Tokenizer seam,
what the crate deliberately refuses to do, the module layout mirroring
upstream, and the conformance and divergence ratchets.
How accent-proust is put together: the pipeline stages, the Tokenizer,
TagRenderer and SchemaSource seams, what the crate deliberately refuses to
do, the module layout mirroring upstream, the workspace of hosts, and the
conformance and divergence ratchets.
---

## The pipeline
Expand Down Expand Up @@ -159,13 +160,20 @@ Members are hosts:
| Member | What |
|---|---|
| `crates/accent-proust-wasm` | WebAssembly bindings for a browser or other JavaScript host. Ships to npm rather than crates.io, so it sets `publish = false` |
| `crates/accent-proust-cli` | The [command-line host](/docs/cli), a binary named `accent-proust`: `fmt`, `validate`, `render`, `transform` and `parse`. `publish = false` until it has a release cadence of its own |
| `crates/accent-proust-schema-config` | The declarative schema vocabulary both hosts read: the keys, the refusal of an unknown one with the path to it, and the mapping onto `Schema`. Not a host; what the hosts share |

A binding that carries the library across an ABI is a host in the same sense a
CMS is, so it gets a crate beside the library rather than a feature inside it --
the same reasoning that keeps `Tokenizer` a trait rather than an implementation.
`default-members = ["."]` holds a bare `cargo build`, `cargo test` and
`cargo clippy --all-targets` to the library alone, so no member can quietly join
the standalone, MSRV or conformance lanes.
CMS is, and so is a binary that reads files; each gets a crate beside the
library rather than a feature inside it -- the same reasoning that keeps
`Tokenizer` a trait rather than an implementation. The vocabulary crate is not
a host but what two hosts share, and it lives beside them so that a schema
declared for the browser is accepted by the shell -- one from an object, the
other from a file -- and the two cannot drift apart. `default-members = ["."]`
holds a bare `cargo build`,
`cargo test` and `cargo clippy --all-targets` to the library alone, so no
member can quietly join the standalone, MSRV or conformance lanes; each brings
its own CI job.

## The two ratchets

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ lead: >-
complete.
menu:
visible: true
order: 5
order: 6
description: >-
Every deliberate difference between accent-proust and upstream Markdoc
v0.5.9, grouped by cause: the CommonMark engine, deliberate limits, API
Expand Down
Loading