diff --git a/Cargo.lock b/Cargo.lock
index ebb6c6a..221b5b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -540,6 +540,7 @@ dependencies = [
"comline-codegen-rust",
"comline-codegen-typescript",
"comline-core",
+ "comline-doc-markdown",
"comline-runtime-conformance",
"eyre",
"indicatif",
@@ -562,7 +563,7 @@ dependencies = [
[[package]]
name = "comline-codegen"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/generation?rev=0601f004302ebb93e6804732419ccf459902e84a#0601f004302ebb93e6804732419ccf459902e84a"
+source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06"
dependencies = [
"comline-core",
"eyre",
@@ -576,7 +577,7 @@ dependencies = [
[[package]]
name = "comline-codegen-rust"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/comline-rust?rev=6e8715ac126ab78316dd6a2712165d716351fab1#6e8715ac126ab78316dd6a2712165d716351fab1"
+source = "git+https://github.com/ComlineProject/comline-rust?rev=bef9a74c028d7a44b4dc13ed677d300e061b2147#bef9a74c028d7a44b4dc13ed677d300e061b2147"
dependencies = [
"comline-codegen",
"comline-core",
@@ -586,7 +587,7 @@ dependencies = [
[[package]]
name = "comline-codegen-typescript"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/comline-typescript?rev=470dc5bad2f7e36e1cc2f44e3e57fde864a42315#470dc5bad2f7e36e1cc2f44e3e57fde864a42315"
+source = "git+https://github.com/ComlineProject/comline-typescript?rev=47b54cc75b05bc52586d431d26ed4def316cf0c4#47b54cc75b05bc52586d431d26ed4def316cf0c4"
dependencies = [
"comline-codegen",
"comline-core",
@@ -596,7 +597,7 @@ dependencies = [
[[package]]
name = "comline-core"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64"
+source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f"
dependencies = [
"ariadne",
"blake3",
@@ -612,6 +613,7 @@ dependencies = [
"regex",
"rust-sitter",
"rust-sitter-tool",
+ "schemars",
"semver",
"serde",
"serde_derive",
@@ -624,7 +626,19 @@ dependencies = [
[[package]]
name = "comline-core-stdlib"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64"
+source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f"
+
+[[package]]
+name = "comline-doc-markdown"
+version = "0.1.0"
+source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06"
+dependencies = [
+ "comline-codegen",
+ "comline-core",
+ "eyre",
+ "serde",
+ "serde_json",
+]
[[package]]
name = "comline-runtime"
diff --git a/Cargo.toml b/Cargo.toml
index c6214f0..7c0494c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,15 +32,18 @@ eyre = "0.6.8"
# needs real I/O (git fetch, filesystem), so it's off by default for
# `comline-core`'s wasm32 consumers (the playground editor, the language
# server); the CLI is the one consumer that actually runs `comline build`.
-comline-core = { git = "https://github.com/ComlineProject/core", rev = "47066583002dbca27975827106f9e34761d0dc64", features = ["deps"] }
+comline-core = { git = "https://github.com/ComlineProject/core", rev = "076cae0c3a0639b0c083b61a94f9d4913e853b3f", features = ["deps"] }
# All three pinned to revs that agree on one `comline-codegen`, and bumped
# together: comline-rust and comline-typescript's codegen crates consume
# `comline-codegen`'s types, so the rev below must be byte-identical to the
# one *they* pin - Cargo only unifies a git dependency across identical rev
# strings, and two copies of `GenRequest` are two incompatible types.
-comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "0601f004302ebb93e6804732419ccf459902e84a" }
-comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "6e8715ac126ab78316dd6a2712165d716351fab1", optional = true }
-comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "470dc5bad2f7e36e1cc2f44e3e57fde864a42315", optional = true }
+comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" }
+# The official Markdown documentation renderer (`comline doc`), linked in
+# like the official generators. Same generation rev as `comline-codegen`.
+comline-doc-markdown = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" }
+comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "bef9a74c028d7a44b4dc13ed677d300e061b2147", optional = true }
+comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "47b54cc75b05bc52586d431d26ed4def316cf0c4", optional = true }
# `comline conformance`: the conformance runner (corpus loading, judging,
# profile proofs) and its case format, a public spec. Pinned by git rev.
comline-runtime-conformance = { git = "https://github.com/ComlineProject/runtime", rev = "6f8f73a247f0aa4ac8b7041b2fdc4b02bce0e0d6" }
diff --git a/docs/cli.md b/docs/cli.md
index ace90a1..9d3cbaa 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -20,6 +20,7 @@ This is the long-form reference. For a two-minute tour see the
- [`build`](#comline-build)
- [`generate`](#comline-generate)
- [`diff`](#comline-diff)
+ - [`doc`](#comline-doc)
- [`conformance`](#comline-conformance)
- [`clean`](#comline-clean)
- [`reset`](#comline-reset)
@@ -445,6 +446,60 @@ comline diff 0.0.1 HEAD
Fails with exit `2` if the project has never been built, and exit `1` if an
argument matches no stored version (the error lists the versions that exist).
+### `comline doc`
+
+```
+comline doc [--out
] [--version ] [--changelog-from ] [--format markdown|json]
+```
+
+Write the package's documentation. Nothing is written to `.comline/`.
+
+- **Pages** (`markdown`, the default, rendered by the official renderer linked
+ into `comline`): `index.md` (the package, its `//!` header, its modules), one
+ page per module (`types::user` is `types/user.md`) with its `//!` header and
+ every unit with its `///` docs, fields, arguments and each protocol's wire
+ summary, `changelog.md` with `--changelog-from`, and `nav.yml`, a `nav` for
+ mkdocs or zensical.
+- **`json`** writes the documentation model itself, `model.json`, in the shape
+ of core's `schemas/doc-model.json`.
+
+| Flag | Default | Meaning |
+|---|---|---|
+| `--out ` | `[doc] out`, else `docs/api` | where to write, relative to the project |
+| `--version ` | the working tree | document a built version (a version string, a commit hash, or `HEAD`) from its frozen IR, `//!` headers included |
+| `--changelog-from ` | `[doc] changelog_from` | add what changed from built version `` to the documented one |
+| `--format ` | `[doc] format`, else `markdown` | `markdown`, `json`, or another id the `[doc] renderer` offers |
+
+A built version keeps no source, so with `--version` its protocols have no wire
+summary. A version built before headers were stored has none.
+
+`comline.toml`'s `[doc]` table:
+
+```toml
+[doc]
+out = "docs/api"
+format = "markdown"
+renderer = "renderers/my-renderer.wasm" # optional: an external renderer
+timeout = 60 # seconds per call to `renderer`
+links = { std = "https://comlineproject.github.io/docs/std/" }
+changelog_from = "0.1.0"
+```
+
+- `renderer` is run exactly like an external `generator`: a `.wasm` under
+ wasmtime with no filesystem, environment or network, a native executable with
+ an empty environment, the same limits and path checks. It must list the
+ format in its `Describe` answer's `renderers`.
+- `links` maps a dependency (or `std`) to the base URL of its documentation. A
+ type from it links to `//#`; a dependency not listed is
+ plain text.
+
+```bash
+comline doc
+comline doc --out site/api --changelog-from 0.1.0
+comline doc --version 0.2.0
+comline doc --format json
+```
+
### `comline conformance`
```
diff --git a/src/cli.rs b/src/cli.rs
index be085a9..671ad0f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -100,6 +100,47 @@ Examples:
json: bool,
},
+ /// Write the package's documentation: Markdown pages, or the model as JSON
+ #[command(long_about = "\
+Write the package's documentation: an index page, one page per module (its \
+`//!` header, every unit with its `///` docs, fields, arguments and each \
+protocol's wire summary), a changelog page with `--changelog-from`, and an \
+mkdocs/zensical `nav.yml`. Read-only: nothing is written to `.comline/`.
+
+The working tree is documented unless `--version` names a built version (a \
+version string, a commit hash or `HEAD`), which is documented from its frozen \
+IR, `//!` headers included. A built version keeps no source, so its protocols \
+have no wire summary.
+
+`--format json` writes the documentation model itself (`model.json`), for \
+other tools. `comline.toml`'s `[doc]` table sets the defaults: `out` \
+(`docs/api`), `format` (`markdown`), `renderer` (an external renderer, run \
+like an external generator), `links` (dependency docs URLs) and \
+`changelog_from`.
+
+Examples:
+ comline doc
+ comline doc --out site/api --changelog-from 0.1.0
+ comline doc --version 0.2.0
+ comline doc --format json")]
+ Doc {
+ /// Output directory; default from `[doc] out`, else `docs/api`
+ #[arg(long, value_name = "DIR")]
+ out: Option,
+
+ /// Document this built version instead of the working tree
+ #[arg(long = "version", value_name = "VERSION")]
+ doc_version: Option,
+
+ /// Add a changelog from this built version to the documented one
+ #[arg(long, value_name = "VERSION")]
+ changelog_from: Option,
+
+ /// `markdown` (pages) or `json` (the model); default from `[doc] format`
+ #[arg(long, value_name = "FORMAT")]
+ format: Option,
+ },
+
/// Validate the project and write generated code for each configured target
#[command(long_about = "\
Validate the project (no version bump, no `.comline/` write), then write \
diff --git a/src/commands/doc.rs b/src/commands/doc.rs
new file mode 100644
index 0000000..ae40a6f
--- /dev/null
+++ b/src/commands/doc.rs
@@ -0,0 +1,173 @@
+//! `comline doc` — the package's documentation (core#145).
+//!
+//! Core builds the documentation model (`comline_core::doc`): from the
+//! compiled working tree, or from a built version's frozen IR and `//!`
+//! headers with `--version`. `--changelog-from` adds what changed since an
+//! earlier built version. `--format json` writes the model itself; any other
+//! format is a renderer's id, `markdown` being the official one, linked in
+//! like the official generators. `[doc] renderer` in `comline.toml` names an
+//! external renderer instead, run exactly like an external generator
+//! ([`crate::external`]: the same sandbox, limits and path checks).
+//!
+//! Like `check`, nothing is written to `.comline/`.
+
+use std::path::{Path, PathBuf};
+use std::time::Duration;
+
+use comline_codegen::config::{ComlineToml, DEFAULT_DOC_FORMAT, DEFAULT_DOC_OUT};
+use comline_codegen::protocol::{FileOut, RenderOptions, RenderRequest};
+use comline_codegen::Registry;
+use comline_core::doc::{self, DocModel, DOC_MODEL_VERSION};
+use comline_core::package::build::cas::objects::Tree;
+use comline_core::package::build::cas::ObjectStore;
+use comline_core::schema::ir::frozen::unit::FrozenUnit;
+use miette::{miette, IntoDiagnostic, Result, WrapErr};
+
+use crate::commands::{core_err, ensure_project};
+use crate::{external, history, ui};
+
+/// The command line's say, each overriding its `[doc]` key.
+#[derive(Debug, Default)]
+pub struct Flags {
+ pub out: Option,
+ pub version: Option,
+ pub changelog_from: Option,
+ pub format: Option,
+}
+
+/// The renderers linked into this binary.
+pub(crate) fn renderer_registry() -> Registry {
+ let mut registry = Registry::new();
+ comline_doc_markdown::register(&mut registry);
+ registry
+}
+
+pub fn run(work_dir: &Path, flags: Flags) -> Result<()> {
+ ensure_project(work_dir)?;
+ let cfg = ComlineToml::load(work_dir)?.doc;
+
+ let out = match (&flags.out, &cfg.out) {
+ (Some(out), _) => work_dir.join(out),
+ (None, Some(out)) => work_dir.join(out),
+ (None, None) => work_dir.join(DEFAULT_DOC_OUT),
+ };
+ let format = flags
+ .format
+ .or(cfg.format.clone())
+ .unwrap_or_else(|| DEFAULT_DOC_FORMAT.to_string());
+ let changelog_from = flags.changelog_from.or(cfg.changelog_from.clone());
+
+ let (mut model, schemas) = match &flags.version {
+ Some(spec) => frozen_model(work_dir, spec)?,
+ None => working_tree_model(work_dir)?,
+ };
+
+ if let Some(from) = &changelog_from {
+ let history = history::load(work_dir)?;
+ let commit = history::resolve(&history, from)?;
+ let store = ObjectStore::new(work_dir);
+ let old = history::load_schemas(&store, commit)?;
+ let to = model
+ .package
+ .version
+ .clone()
+ .unwrap_or_else(|| "working tree".to_string());
+ model
+ .changelog
+ .push(doc::changelog(&commit.version, &old, &to, &schemas));
+ }
+
+ let files = if format == "json" {
+ let json = serde_json::to_string_pretty(&model).into_diagnostic()?;
+ vec![FileOut {
+ path: "model.json".into(),
+ contents: format!("{json}\n").into_bytes(),
+ }]
+ } else {
+ let request = RenderRequest {
+ renderer: format.clone(),
+ model_version: DOC_MODEL_VERSION,
+ model: serde_json::to_string(&model).into_diagnostic()?,
+ options: RenderOptions {
+ links: cfg.links.clone(),
+ },
+ };
+ match &cfg.renderer {
+ Some(path) => {
+ let timeout = cfg
+ .timeout
+ .map(Duration::from_secs)
+ .unwrap_or(external::DEFAULT_TIMEOUT);
+ let renderer = external::External::open(&work_dir.join(path), timeout)?;
+ let description = renderer.describe()?;
+ renderer.render(&description, request)?
+ }
+ None => external::builtin_render(&renderer_registry(), request)?,
+ }
+ };
+
+ for file in &files {
+ let path = out.join(file.path_buf());
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to create `{}`", parent.display()))?;
+ }
+ std::fs::write(&path, &file.contents)
+ .into_diagnostic()
+ .wrap_err_with(|| format!("failed to write `{}`", path.display()))?;
+ }
+ ui::success(format!(
+ "Documented {} into {} ({} file(s))",
+ match &model.package.version {
+ Some(v) => format!("{} {v}", model.package.name),
+ None => model.package.name.clone(),
+ },
+ out.display(),
+ files.len()
+ ));
+ Ok(())
+}
+
+/// The working tree's model, and its frozen schemas for a changelog.
+fn working_tree_model(work_dir: &Path) -> Result<(DocModel, Vec>)> {
+ let spinner = ui::spinner("compiling schemas");
+ let outcome = comline_core::package::build::compile_package(work_dir);
+ ui::finish_spinner(spinner);
+ let context = outcome.map_err(|e| core_err("validation failed", e))?;
+ Ok((doc::model(&context, None), doc::frozen_schemas(&context)))
+}
+
+/// A built version's model, from its frozen IR and headers.
+fn frozen_model(work_dir: &Path, spec: &str) -> Result<(DocModel, Vec>)> {
+ let history = history::load(work_dir)?;
+ let commit = history::resolve(&history, spec)?;
+ let store = ObjectStore::new(work_dir);
+ let schemas = history::load_schemas(&store, commit)?;
+ let config = history::load_config(&store, commit)?;
+ // A built version's config doesn't always freeze the package's name;
+ // the working tree's is the same unless the package was renamed.
+ let name = comline_core::package::config::ir::frozen::namespace(&config)
+ .map(str::to_string)
+ .or_else(|| {
+ comline_core::package::build::compile_package_without_fetching(work_dir)
+ .ok()
+ .map(|context| context.config.name.value.to_string())
+ })
+ .or_else(|| {
+ work_dir
+ .canonicalize()
+ .ok()?
+ .file_name()
+ .map(|n| n.to_string_lossy().into_owned())
+ })
+ .ok_or_else(|| miette!("version `{spec}` has no package name"))?;
+ let root = store
+ .read(&commit.tree)
+ .and_then(|bytes| Tree::from_bytes(&bytes))
+ .map_err(|e| miette!("failed to read version `{spec}`: {e:#}"))?;
+ let headers = doc::read_docs(&store, &root)
+ .map_err(|e| miette!("failed to read version `{spec}`'s headers: {e:#}"))?;
+ let model = doc::model_frozen(&name, Some(commit.version.clone()), &schemas, headers);
+ Ok((model, schemas))
+}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index a2b43c4..9e93298 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -9,6 +9,7 @@ pub mod clean;
pub mod completions;
pub mod conformance;
pub mod diff;
+pub mod doc;
pub mod generate;
pub mod inspect;
pub mod new;
diff --git a/src/external.rs b/src/external.rs
index 63e6232..be1a3cb 100644
--- a/src/external.rs
+++ b/src/external.rs
@@ -26,8 +26,8 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use comline_codegen::protocol::{
- self, Description, FileOut, GenerateRequest, LanguageInfo, Request, Response, IR_FORMAT,
- PROTOCOL_VERSION,
+ self, Description, FileOut, GenerateRequest, LanguageInfo, RenderRequest, Request, Response,
+ IR_FORMAT, PROTOCOL_VERSION,
};
use comline_codegen::versions::LangVersion;
use comline_codegen::Registry;
@@ -133,7 +133,7 @@ impl External {
self.name
))
}
- Response::Generate(_) => {
+ Response::Generate(_) | Response::Render(_) => {
return Err(miette!(
"generator `{}` answered `Describe` with generated files",
self.name
@@ -169,6 +169,61 @@ impl External {
let response = self.call(&Request::generate(request))?;
files(&format!("generator `{}`", self.name), response)
}
+
+ /// Renders documentation with the renderer `request` names, which its
+ /// description must list. Checked like [`External::generate`]'s files.
+ pub fn render(
+ &self,
+ description: &Description,
+ request: RenderRequest,
+ ) -> Result> {
+ let who = format!("renderer `{}`", self.name);
+ check_renderer(&who, description, &request)?;
+ let response = self.call(&Request::render(request))?;
+ rendered(&who, response)
+ }
+}
+
+/// Refuses `request` before it's sent when `description` doesn't offer its
+/// renderer at its model version.
+fn check_renderer(who: &str, description: &Description, request: &RenderRequest) -> Result<()> {
+ let Some(info) = description
+ .renderers
+ .iter()
+ .find(|r| r.id == request.renderer)
+ else {
+ return Err(miette!(
+ "{who} doesn't render `{}`; it offers {}",
+ request.renderer,
+ list(description.renderers.iter().map(|r| format!("`{}`", r.id)))
+ ));
+ };
+ if !info.model_versions.contains(&request.model_version) {
+ return Err(miette!(
+ "{who} reads documentation model {}, and this comline writes model {}",
+ list(info.model_versions.iter()),
+ request.model_version
+ ));
+ }
+ Ok(())
+}
+
+/// A built-in renderer's answer to `request`, through the protocol in
+/// process, like [`builtin`].
+pub fn builtin_render(registry: &Registry, request: RenderRequest) -> Result> {
+ let who = format!("`{}` renderer", request.renderer);
+ check_renderer(&who, &protocol::describe(registry), &request)?;
+ let bytes = protocol::encode(&Request::render(request)).map_err(|e| miette!("{e}"))?;
+ rendered(&who, protocol::respond(registry, &bytes))
+}
+
+/// [`files`] for a `Render` answer.
+fn rendered(who: &str, response: Response) -> Result> {
+ match response {
+ Response::Render(generated) => files(who, Response::Generate(generated)),
+ Response::Generate(_) => Err(miette!("{who} answered `Render` with generated code")),
+ other => files(who, other),
+ }
}
/// `items` as `a, b, c`, or `none`.
@@ -261,6 +316,11 @@ fn files(who: &str, response: Response) -> Result> {
Response::Describe(_) => {
return Err(miette!("{who} answered `Generate` with a description"))
}
+ Response::Render(_) => {
+ return Err(miette!(
+ "{who} answered `Generate` with rendered documentation"
+ ))
+ }
};
for diagnostic in &generated.diagnostics {
crate::ui::warn(format!("{who}: {diagnostic}"));
diff --git a/src/main.rs b/src/main.rs
index e18c213..aa0e8a5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -119,6 +119,20 @@ fn run(cli: Cli) -> Result<()> {
Commands::Build { watch } => commands::build::run(&work_dir, watch),
Commands::Check => commands::check::run(&work_dir),
Commands::Inspect { json } => commands::inspect::run(&work_dir, json),
+ Commands::Doc {
+ out,
+ doc_version,
+ changelog_from,
+ format,
+ } => commands::doc::run(
+ &work_dir,
+ commands::doc::Flags {
+ out,
+ version: doc_version,
+ changelog_from,
+ format,
+ },
+ ),
Commands::Generate {
target,
out,
diff --git a/tests/cli/doc.rs b/tests/cli/doc.rs
new file mode 100644
index 0000000..4f74d8f
--- /dev/null
+++ b/tests/cli/doc.rs
@@ -0,0 +1,146 @@
+//! `comline doc`
+
+use std::fs;
+use std::path::Path;
+
+use predicates::prelude::*;
+
+use crate::util::*;
+
+fn package(dir: &Path, header: &str, schema: &str) {
+ fs::create_dir_all(dir.join("src")).unwrap();
+ fs::write(
+ dir.join("config.idp"),
+ format!("{header}congregation shop\nspecification_version = 1\n"),
+ )
+ .unwrap();
+ fs::write(dir.join("src/api.ids"), schema).unwrap();
+}
+
+const API: &str = "//! The shop's API.\n\n/// One order.\nstruct Order {\n /// What it costs.\n total: u64\n}\n\n\
+ /// Orders things.\nprotocol Orders {\n /// Place one.\n /// @order: what to place\n \
+ function place(order: Order) -> bool;\n}\n";
+
+fn read(path: &Path) -> String {
+ fs::read_to_string(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))
+}
+
+#[test]
+fn writes_markdown_pages_for_the_working_tree() {
+ let temp = tempfile::tempdir().unwrap();
+ let shop = temp.path().join("shop");
+ package(&shop, "//! A shop.\n\n", API);
+
+ comline_cmd()
+ .current_dir(&shop)
+ .arg("doc")
+ .assert()
+ .success()
+ .stderr(predicate::str::contains("Documented shop"));
+
+ let out = shop.join("docs/api");
+ let index = read(&out.join("index.md"));
+ assert!(index.starts_with("# shop\n\nA shop.\n"), "{index}");
+ assert!(index.contains("| [`api`](api.md) | The shop's API. |"));
+ let api = read(&out.join("api.md"));
+ assert!(
+ api.contains("| `total` | `u64` | What it costs. |"),
+ "{api}"
+ );
+ assert!(api.contains("| `order` | [`Order`](#order) | what to place |"));
+ assert!(api.contains("**On the wire**"));
+ assert!(read(&out.join("nav.yml")).contains(" - \"api\": api.md"));
+ assert!(!out.join("changelog.md").exists());
+}
+
+#[test]
+fn documents_a_built_version_with_its_frozen_headers_and_a_changelog() {
+ let temp = tempfile::tempdir().unwrap();
+ let shop = temp.path().join("shop");
+ package(&shop, "//! A shop.\n\n", API);
+ comline_cmd()
+ .current_dir(&shop)
+ .arg("build")
+ .assert()
+ .success();
+
+ // Change the working tree: the built version must not read it.
+ package(
+ &shop,
+ "",
+ &API.replace("//! The shop's API.\n\n", "").replace(
+ " total: u64\n",
+ " total: u64\n optional note: string\n",
+ ),
+ );
+ comline_cmd()
+ .current_dir(&shop)
+ .arg("build")
+ .assert()
+ .success();
+
+ comline_cmd()
+ .current_dir(&shop)
+ .args(["doc", "--version", "0.1.0-dev.1", "--out", "v1"])
+ .assert()
+ .success();
+ let index = read(&shop.join("v1/index.md"));
+ assert!(
+ index.starts_with("# shop 0.1.0-dev.1\n\nA shop.\n"),
+ "{index}"
+ );
+ let api = read(&shop.join("v1/api.md"));
+ assert!(api.starts_with("# `api`\n\nThe shop's API.\n"), "{api}");
+ assert!(!api.contains("note"));
+
+ comline_cmd()
+ .current_dir(&shop)
+ .args(["doc", "--changelog-from", "0.1.0-dev.1", "--out", "now"])
+ .assert()
+ .success();
+ let changelog = read(&shop.join("now/changelog.md"));
+ assert!(
+ changelog.contains("## 0.1.0-dev.1 to working tree"),
+ "{changelog}"
+ );
+ assert!(changelog.contains("field name: `note`"), "{changelog}");
+}
+
+#[test]
+fn json_writes_the_model_and_doc_settings_come_from_comline_toml() {
+ let temp = tempfile::tempdir().unwrap();
+ let shop = temp.path().join("shop");
+ package(&shop, "", API);
+ fs::write(
+ shop.join("comline.toml"),
+ "[doc]\nout = \"site\"\nformat = \"json\"\n",
+ )
+ .unwrap();
+
+ comline_cmd()
+ .current_dir(&shop)
+ .arg("doc")
+ .assert()
+ .success();
+ let model: serde_json::Value =
+ serde_json::from_str(&read(&shop.join("site/model.json"))).unwrap();
+ assert_eq!(model["model_version"], 1);
+ assert_eq!(model["package"]["name"], "shop");
+ assert_eq!(model["modules"][0]["docs"], "The shop's API.");
+}
+
+#[test]
+fn an_unknown_format_names_what_is_offered() {
+ let temp = tempfile::tempdir().unwrap();
+ let shop = temp.path().join("shop");
+ package(&shop, "", API);
+
+ comline_cmd()
+ .current_dir(&shop)
+ .args(["doc", "--format", "html"])
+ .assert()
+ .failure()
+ .stderr(predicate::str::contains(
+ "doesn't render `html`; it offers `markdown`",
+ ));
+}
diff --git a/tests/cli/external.rs b/tests/cli/external.rs
index c7ca8c8..ad2739f 100644
--- a/tests/cli/external.rs
+++ b/tests/cli/external.rs
@@ -286,3 +286,63 @@ fn a_language_the_generator_does_not_offer_is_refused() {
"generator `{file}` handles `rust` from `9.0.0` on, not `1.85.0`"
)));
}
+
+/// `[doc] renderer`: the official Markdown renderer run as an external one
+/// writes what the built-in writes.
+fn renderer_matches_builtin(kind: Kind) {
+ let temp = tempfile::tempdir().unwrap();
+ let project = fixture_project(temp.path());
+ comline_cmd()
+ .current_dir(&project)
+ .args(["doc", "--out", "builtin"])
+ .assert()
+ .success();
+ let file = install(&project, kind, "renderer", "comline-doc-markdown");
+ fs::write(
+ project.join("comline.toml"),
+ format!("[doc]\nrenderer = \"{file}\"\nout = \"external\"\n"),
+ )
+ .unwrap();
+ comline_cmd()
+ .current_dir(&project)
+ .arg("doc")
+ .assert()
+ .success();
+ let external = tree(&project.join("external"));
+ assert!(!external.is_empty());
+ assert!(
+ external == tree(&project.join("builtin")),
+ "{kind:?}: the external renderer's pages differ from the built-in's"
+ );
+}
+
+#[cfg(feature = "gen-wasm")]
+#[test]
+fn a_wasm_renderer_writes_what_the_builtin_does() {
+ renderer_matches_builtin(Kind::Wasm);
+}
+
+#[test]
+fn a_native_renderer_writes_what_the_builtin_does() {
+ renderer_matches_builtin(Kind::Native);
+}
+
+#[test]
+fn a_generator_that_offers_no_renderer_is_refused_as_one() {
+ let temp = tempfile::tempdir().unwrap();
+ let project = fixture_project(temp.path());
+ let file = install(&project, Kind::Native, "real", "comline-gen-rust");
+ fs::write(
+ project.join("comline.toml"),
+ format!("[doc]\nrenderer = \"{file}\"\n"),
+ )
+ .unwrap();
+ comline_cmd()
+ .current_dir(&project)
+ .arg("doc")
+ .assert()
+ .failure()
+ .stderr(predicate::str::contains(
+ "doesn't render `markdown`; it offers none",
+ ));
+}
diff --git a/tests/cli/main.rs b/tests/cli/main.rs
index c00249d..8f5fc10 100644
--- a/tests/cli/main.rs
+++ b/tests/cli/main.rs
@@ -13,6 +13,7 @@ mod check;
mod clean;
mod conformance;
mod diff;
+mod doc;
mod end_to_end;
mod external;
mod generate;
diff --git a/tests/fixtures/generators/Cargo.lock b/tests/fixtures/generators/Cargo.lock
index 05a057e..64dfc52 100644
--- a/tests/fixtures/generators/Cargo.lock
+++ b/tests/fixtures/generators/Cargo.lock
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "comline-codegen"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/generation?rev=0601f004302ebb93e6804732419ccf459902e84a#0601f004302ebb93e6804732419ccf459902e84a"
+source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06"
dependencies = [
"comline-core",
"eyre",
@@ -87,7 +87,7 @@ dependencies = [
[[package]]
name = "comline-codegen-rust"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/comline-rust?rev=6e8715ac126ab78316dd6a2712165d716351fab1#6e8715ac126ab78316dd6a2712165d716351fab1"
+source = "git+https://github.com/ComlineProject/comline-rust?rev=bef9a74c028d7a44b4dc13ed677d300e061b2147#bef9a74c028d7a44b4dc13ed677d300e061b2147"
dependencies = [
"comline-codegen",
"comline-core",
@@ -97,7 +97,7 @@ dependencies = [
[[package]]
name = "comline-core"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64"
+source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f"
dependencies = [
"blake3",
"ciborium",
@@ -114,7 +114,19 @@ dependencies = [
[[package]]
name = "comline-core-stdlib"
version = "0.1.0"
-source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64"
+source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f"
+
+[[package]]
+name = "comline-doc-markdown"
+version = "0.1.0"
+source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06"
+dependencies = [
+ "comline-codegen",
+ "comline-core",
+ "eyre",
+ "serde",
+ "serde_json",
+]
[[package]]
name = "comline-test-generators"
@@ -122,6 +134,7 @@ version = "0.0.0"
dependencies = [
"comline-codegen",
"comline-codegen-rust",
+ "comline-doc-markdown",
]
[[package]]
diff --git a/tests/fixtures/generators/Cargo.toml b/tests/fixtures/generators/Cargo.toml
index bda1f1b..f92c3de 100644
--- a/tests/fixtures/generators/Cargo.toml
+++ b/tests/fixtures/generators/Cargo.toml
@@ -12,5 +12,6 @@ publish = false
[dependencies]
# The same revs the CLI pins, so the real generator's output is comparable
# byte for byte with the CLI's built-in one.
-comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "0601f004302ebb93e6804732419ccf459902e84a" }
-comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "6e8715ac126ab78316dd6a2712165d716351fab1" }
+comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" }
+comline-doc-markdown = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" }
+comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "bef9a74c028d7a44b4dc13ed677d300e061b2147" }
diff --git a/tests/fixtures/generators/src/bin/hostile.rs b/tests/fixtures/generators/src/bin/hostile.rs
index 8c39c80..8ed611b 100644
--- a/tests/fixtures/generators/src/bin/hostile.rs
+++ b/tests/fixtures/generators/src/bin/hostile.rs
@@ -20,7 +20,7 @@ fn main() {
let request: Request = decode(&input).unwrap();
let response = match request.body {
RequestBody::Describe => describe(&mode),
- RequestBody::Generate(_) => generate(&mode),
+ RequestBody::Generate(_) | RequestBody::Render(_) => generate(&mode),
};
let mut out = std::io::stdout().lock();
out.write_all(&encode(&response).unwrap()).unwrap();
@@ -47,6 +47,7 @@ fn describe(mode: &str) -> Response {
}],
protocols: (protocol, protocol),
ir_formats: vec![IR_FORMAT],
+ renderers: vec![],
})
}
diff --git a/tests/fixtures/generators/src/bin/renderer.rs b/tests/fixtures/generators/src/bin/renderer.rs
new file mode 100644
index 0000000..0c09379
--- /dev/null
+++ b/tests/fixtures/generators/src/bin/renderer.rs
@@ -0,0 +1,10 @@
+//! The official Markdown renderer as an external one: what generation's
+//! `comline-doc-markdown` binary is.
+
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ let mut registry = comline_codegen::Registry::new();
+ comline_doc_markdown::register(&mut registry);
+ comline_codegen::protocol::serve_stdio(®istry)
+}