1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
use std::path::PathBuf;
use super::{parse_file_or_stdin, select_view, write_or_stdout};
use usage::docs::markdown::{MarkdownRenderer, MarkdownTemplate};
use usage::error::UsageErr;
use usage_rs::Args;
/// Generate Markdown documentation from a usage spec
///
/// One page by default, or a page per command with --multi. Every part of the output comes
/// from a Tera template that --template can replace.
#[derive(Args)]
#[usage(alias = "md", effect = "read")]
pub struct Markdown {
/// A usage spec file, or a script with a usage shebang; "-" reads stdin
#[usage(short, long)]
file: PathBuf,
/// Render one spec-declared executable view
#[usage(long)]
view: Option<String>,
// /// Pass a usage spec in an argument instead of a file
// #[usage(short, long, required_unless = "--file", overrides = "--file")]
// spec: Option<String>,
/// Render each subcommand as a separate markdown file
#[usage(short, long, conflicts = "--out-file")]
multi: bool,
/// Escape HTML in markdown
#[usage(long)]
html_encode: bool,
/// Directory for the per-command pages (required with --multi)
#[usage(
long,
value_hint = usage_rs::ValueHint::DirPath,
requires = "--multi",
required_if = "--multi",
effect = "write"
)]
out_dir: Option<PathBuf>,
/// Output file path for single-file markdown generation, or "-" for stdout (default)
#[usage(
long,
value_hint = usage_rs::ValueHint::FilePath,
effect = "write"
)]
out_file: Option<PathBuf>,
/// Turn four-space indented blocks in help text into markdown code fences
///
/// `--replace-pre-with-code-fences` is the former name and still works. It was always a
/// misnomer: this has never looked at `<pre>` tags.
#[usage(
long = "indented-blocks-to-code-fences",
alias = "replace-pre-with-code-fences"
)]
indented_blocks_to_code_fences: bool,
/// Prefix for the links between pages, such as /cli/reference
#[usage(long)]
url_prefix: Option<String>,
/// Extension for page links (including the dot); empty means extensionless URLs
#[usage(long, default = ".md")]
link_extension: String,
/// Replace a built-in Tera template, as NAME=PATH; the names are spec, index, command, argument, flag, and config
#[usage(long)]
template: Vec<String>,
}
impl usage_rs::Run for Markdown {
type Output = usage::miette::Result<()>;
fn run(self) -> Self::Output {
// The banner belongs to every generated document, so build it in one place rather
// than once per output path.
let render = |md: &str| {
format!(
"<!-- @generated by usage-cli from usage spec -->\n{}\n",
md.trim()
)
};
// File-only, deliberately: every path this receives is a join onto `--out-dir`, so a
// `-` meaning stdout cannot turn up here.
let write = |path: &PathBuf, md: &str| -> usage::miette::Result<()> {
eprintln!("writing to {}", path.display());
super::write_file(path, render(md))?;
Ok(())
};
let spec = select_view(parse_file_or_stdin(&self.file)?, self.view.as_deref())?;
let mut ctx = MarkdownRenderer::new(spec.clone())
.with_link_extension(self.link_extension)
.with_html_encode(self.html_encode)
.with_indented_blocks_to_code_fences(self.indented_blocks_to_code_fences);
for value in &self.template {
let Some((name, path)) = value.split_once('=') else {
usage::miette::bail!("invalid template `{value}`; expected NAME=PATH");
};
let template = match name {
"spec" => MarkdownTemplate::Spec,
"index" => MarkdownTemplate::Index,
"command" => MarkdownTemplate::Command,
"argument" => MarkdownTemplate::Argument,
"flag" => MarkdownTemplate::Flag,
"config" => MarkdownTemplate::Config,
_ => usage::miette::bail!(
"unknown template `{name}`; expected spec, index, command, argument, flag, or config"
),
};
let path = PathBuf::from(path);
let source = std::fs::read_to_string(&path)
.map_err(|err| UsageErr::FileError(err, path.clone()))?;
ctx = ctx.with_template(template, source);
}
if let Some(url_prefix) = &self.url_prefix {
ctx = ctx.with_url_prefix(url_prefix);
}
if self.multi {
ctx = ctx.with_multi(true);
// Before anything is written, so a refusal does not leave a half-built directory.
//
// Refused rather than warned about: writing anyway destroys the command's own
// documentation, and a warning printed into a docs build is a warning nobody reads.
// No CLI in the fleet has a `configuration` command — they call it `config` — so
// this asks the one author who ever hits it to pick a name, rather than quietly
// losing a page for everybody who does not.
if !ctx.render_config()?.trim().is_empty()
&& let Some(cmd) = ctx.config_page_collision()
{
usage::miette::bail!(
"the `{cmd}` command's page and the settings page would both be written \
to {}; rename the command, or hide it, to generate both",
ctx.config_page()
);
}
let commands = spec.cmd.all_subcommands().into_iter().filter(|c| !c.hide);
for cmd in commands {
let md = ctx.render_cmd(cmd)?;
let dir = cmd
.full_cmd
.iter()
.take(cmd.full_cmd.len() - 1)
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("/");
let path = self
.out_dir
.as_ref()
.unwrap()
.join(dir)
.join(format!("{}.md", cmd.name));
write(&path, &md)?;
}
let md_idx = ctx.render_index()?;
let path_idx = self.out_dir.as_ref().unwrap().join("index.md");
write(&path_idx, &md_idx)?;
// Its own page, and only when there is something to put on it: a CLI with no
// settings should not gain an empty file in its docs directory.
let md_config = ctx.render_config()?;
if !md_config.trim().is_empty() {
// The same call the index links by, so the two cannot disagree about where the
// page went.
let path = self.out_dir.as_ref().unwrap().join(ctx.config_page());
write(&path, &md_config)?;
}
} else {
let md = ctx.render_spec()?;
write_or_stdout(self.out_file.as_deref(), &render(&md))?;
}
Ok(())
}
}