Skip to main content

mdwright_config/
documentation.rs

1//! Human-facing documentation for the mdwright TOML schema.
2//!
3//! The configuration schema and the prose that explains it change
4//! together. Keeping the field catalogue in this crate gives the CLI
5//! initializer and the generated documentation one source of truth
6//! instead of two tables that can drift.
7
8/// Build the generated configuration reference page.
9#[must_use]
10pub fn render_reference_markdown() -> String {
11    let mut out = String::with_capacity(8192);
12    out.push_str(REFERENCE_PREAMBLE);
13    out.push_str("<!-- BEGIN GENERATED: do not edit. Regenerate by running `cargo xtask doc-config`. -->\n\n");
14    render_reference_section(&mut out, "[lint]", "lint.");
15    render_reference_section(&mut out, "[fmt]", "fmt.");
16    render_reference_section(&mut out, "[parse]", "parse.");
17    render_reference_section(&mut out, "[render]", "render.");
18    out.push_str("<!-- END GENERATED -->\n");
19    out
20}
21
22/// Build a documented standalone `.mdwright.toml` using the current
23/// defaults for every known option.
24#[must_use]
25pub fn render_default_toml() -> String {
26    let mut out = String::with_capacity(12_288);
27    out.push_str("# mdwright configuration\n");
28    out.push_str("#\n");
29    out.push_str("# Generated by `mdwright config init`. Every key below is set to\n");
30    out.push_str("# mdwright's current default. Edit or delete values to fit this project.\n");
31
32    let mut current_table = "";
33    for field in SCHEMA_FIELDS {
34        let Some((table, key)) = field.key.rsplit_once('.') else {
35            continue;
36        };
37        if table == current_table {
38            out.push('\n');
39        } else {
40            current_table = table;
41            out.push('\n');
42            out.push('[');
43            out.push_str(table);
44            out.push_str("]\n");
45        }
46        out.push_str("# Type: ");
47        out.push_str(field.ty);
48        out.push_str(".\n");
49        for line in field.description.lines() {
50            out.push_str("# ");
51            out.push_str(line);
52            out.push('\n');
53        }
54        out.push_str(key);
55        out.push_str(" = ");
56        out.push_str(field.default);
57        out.push('\n');
58    }
59
60    out
61}
62
63/// One row in the configuration reference and default-file template.
64struct FieldDoc {
65    /// Dotted TOML path, e.g. `"fmt.wrap"`.
66    key: &'static str,
67    /// Human-readable type, e.g. `"\"keep\" | \"no\" | int"`.
68    ty: &'static str,
69    /// Default value as it appears in TOML, e.g. `"\"keep\""`.
70    default: &'static str,
71    /// Prose description, one sentence.
72    description: &'static str,
73    /// CLI flag that overrides this key, or `None` for file-only knobs.
74    cli_override: Option<&'static str>,
75}
76
77const SCHEMA_FIELDS: &[FieldDoc] = &[
78    // ---- [lint] ------------------------------------------------------
79    FieldDoc {
80        key: "lint.preset",
81        ty: "\"default\" | \"all\" | \"none\"",
82        default: "\"default\"",
83        description: "Baseline lint rule set. Use `default` for curated defaults, `all` for every registered rule, or `none` with `lint.select` for an explicit set.",
84        cli_override: Some("--rules"),
85    },
86    FieldDoc {
87        key: "lint.select",
88        ty: "array of string",
89        default: "[]",
90        description: "Exact lint rule names to enable when `lint.preset = \"none\"`. Preset names are not valid rule names here.",
91        cli_override: Some("--rules"),
92    },
93    FieldDoc {
94        key: "lint.extend-select",
95        ty: "array of string",
96        default: "[]",
97        description: "Lint rule names to add on top of `lint.preset`.",
98        cli_override: Some("--rules"),
99    },
100    FieldDoc {
101        key: "lint.ignore",
102        ty: "array of string",
103        default: "[]",
104        description: "Lint rule names to remove after applying `lint.preset`, `lint.select`, and `lint.extend-select`.",
105        cli_override: Some("--rules"),
106    },
107    FieldDoc {
108        key: "lint.exclude",
109        ty: "array of string",
110        default: "[]",
111        description: "Gitignore-style patterns. Matching files are dropped from lint runs. Patterns are anchored to the directory containing the config file.",
112        cli_override: None,
113    },
114    FieldDoc {
115        key: "lint.info-strings.extra",
116        ty: "array of string",
117        default: "[]",
118        description: "Project-specific additions to the `info-string-typo` allowlist. The stdlib default allowlist still applies.",
119        cli_override: None,
120    },
121    // ---- [fmt] -------------------------------------------------------
122    FieldDoc {
123        key: "fmt.profile",
124        ty: "\"preserve\" | \"mdformat\"",
125        default: "\"preserve\"",
126        description: "Formatter style profile. `preserve` keeps mdwright's identity-oriented defaults; `mdformat` applies mdformat-compatible defaults where verified rewrites can preserve semantics. Explicit `[fmt]` keys override profile defaults.",
127        cli_override: None,
128    },
129    FieldDoc {
130        key: "fmt.wrap",
131        ty: "\"keep\" | \"no\" | int",
132        default: "\"keep\"",
133        description: "Wrap mode for prose paragraphs. `keep` leaves existing breaks alone; `no` forbids new breaks; an integer enforces that display-column budget for breakable lines in every formatter profile.",
134        cli_override: None,
135    },
136    FieldDoc {
137        key: "fmt.wrap-strategy",
138        ty: "\"stable\" | \"balanced\"",
139        default: "\"stable\"",
140        description: "Reflow strategy used when `fmt.wrap` is an integer. `stable` greedily fills soft-break runs and is the default; `balanced` rebalances paragraphs for more even line lengths.",
141        cli_override: None,
142    },
143    FieldDoc {
144        key: "fmt.italic",
145        ty: "\"asterisk\" | \"underscore\" | \"preserve\"",
146        default: "\"preserve\"",
147        description: "Italic delimiter canonicalisation. `preserve` leaves source bytes; `asterisk` or `underscore` opts into the post-pass rewrite. See [Style knobs](format/style.md).",
148        cli_override: None,
149    },
150    FieldDoc {
151        key: "fmt.strong",
152        ty: "\"asterisk\" | \"underscore\" | \"preserve\"",
153        default: "\"preserve\"",
154        description: "Strong-emphasis delimiter canonicalisation. Independent of `fmt.italic`: `*italic*` with `__strong__` is expressible.",
155        cli_override: None,
156    },
157    FieldDoc {
158        key: "fmt.list-marker",
159        ty: "\"dash\" | \"asterisk\" | \"plus\" | \"preserve\"",
160        default: "\"preserve\"",
161        description: "Unordered-list bullet canonicalisation. Each marker is rewritten through a marker-local fact and the family commits only after verification.",
162        cli_override: None,
163    },
164    FieldDoc {
165        key: "fmt.ordered-list",
166        ty: "\"one\" | \"consistent\" | \"preserve\"",
167        default: "\"preserve\"",
168        description: "Ordered-list number canonicalisation. `one` rewrites markers to `1.` only when verification preserves the list start; `consistent` renumbers each list from the source's first item; `preserve` keeps source numbering verbatim.",
169        cli_override: None,
170    },
171    FieldDoc {
172        key: "fmt.thematic-break",
173        ty: "\"dash\" | \"asterisk\" | \"underscore\" | \"underscore-70\" | \"preserve\"",
174        default: "\"preserve\"",
175        description: "Thematic-break canonicalisation. Fixed character modes preserve the source repeat count and spacing; `underscore-70` rewrites the whole break line to mdformat's 70 underscores.",
176        cli_override: None,
177    },
178    FieldDoc {
179        key: "fmt.trailing-newline",
180        ty: "\"preserve\" | \"strip\" | \"ensure\" | bool",
181        default: "\"preserve\"",
182        description: "Trailing-newline policy at the document boundary. `true` is accepted as a synonym for `ensure` and `false` for `strip`.",
183        cli_override: None,
184    },
185    FieldDoc {
186        key: "fmt.end-of-line",
187        ty: "\"lf\" | \"crlf\" | \"keep\"",
188        default: "\"lf\"",
189        description: "Line-ending normalisation. `keep` adopts the first newline seen in the source.",
190        cli_override: None,
191    },
192    FieldDoc {
193        key: "fmt.exclude",
194        ty: "array of string",
195        default: "[]",
196        description: "Formatter-specific exclude globs, independent of `[lint] exclude`.",
197        cli_override: None,
198    },
199    FieldDoc {
200        key: "fmt.heading-attrs",
201        ty: "\"preserve\" | \"canonicalise\"",
202        default: "\"preserve\"",
203        description: "ATX heading `{#id .class key=val}` trailer emission. `preserve` emits the source trailer byte-verbatim. `canonicalise` emits id first, then classes, then key-value pairs.",
204        cli_override: None,
205    },
206    // ---- [fmt.refs] --------------------------------------------------
207    FieldDoc {
208        key: "fmt.refs.placement",
209        ty: "\"end\" | \"preserve\"",
210        default: "\"end\"",
211        description: "Where reference-link definitions are emitted: gathered and sorted at the end of the document, or kept in source order.",
212        cli_override: None,
213    },
214    FieldDoc {
215        key: "fmt.refs.style",
216        ty: "\"bare\" | \"angle\" | \"preserve\"",
217        default: "\"preserve\"",
218        description: "Destination style for reference-link and inline-link URLs. `preserve` keeps each destination's source form; `bare` strips wrapping `<...>` where the bare form still parses; `angle` wraps every destination in `<...>`.",
219        cli_override: None,
220    },
221    // ---- [fmt.footnotes] --------------------------------------------
222    FieldDoc {
223        key: "fmt.footnotes.placement",
224        ty: "\"end\" | \"preserve\"",
225        default: "\"preserve\"",
226        description: "Where footnote definitions are emitted. Default is `preserve` because pulldown-cmark's HTML renderer ties footnote position to parse order; moving definitions would change the rendered HTML.",
227        cli_override: None,
228    },
229    // ---- [fmt.tables] -----------------------------------------------
230    FieldDoc {
231        key: "fmt.tables.style",
232        ty: "\"preserve\" | \"pad\"",
233        default: "\"preserve\"",
234        description: "GFM table spacing policy. `preserve` keeps source cell spacing; `pad` aligns cells and delimiter rows to mdformat-compatible widths when verification preserves semantics.",
235        cli_override: None,
236    },
237    // ---- [fmt.lists] -------------------------------------------------
238    FieldDoc {
239        key: "fmt.lists.continuation-indent",
240        ty: "\"marker-width\" | \"four-space\"",
241        default: "\"marker-width\"",
242        description: "Continuation indentation for wrapped list-item paragraphs. `marker-width` aligns to the source marker width; `four-space` matches mdformat's list continuation spelling.",
243        cli_override: None,
244    },
245    // ---- [fmt.frontmatter] ------------------------------------------
246    FieldDoc {
247        key: "fmt.frontmatter.preserve",
248        ty: "bool",
249        default: "true",
250        description: "Whether to emit document frontmatter byte-verbatim. `false` strips it.",
251        cli_override: None,
252    },
253    // ---- [fmt.math] --------------------------------------------------
254    FieldDoc {
255        key: "fmt.math.normalise",
256        ty: "bool",
257        default: "false",
258        description: "Whether whole-block math regions are normalised. Off by default because math bytes are opaque to CommonMark.",
259        cli_override: None,
260    },
261    FieldDoc {
262        key: "fmt.math.render",
263        ty: "\"none\" | \"commonmark-katex\" | \"dollar\"",
264        default: "\"none\"",
265        description: "Math delimiter rendering policy for downstream renderers. `none` preserves source math regions; `commonmark-katex` records intent without rewriting; `dollar` rewrites bracket and paren math to dollar delimiters.",
266        cli_override: None,
267    },
268    // ---- [parse.math] -----------------------------------------------
269    FieldDoc {
270        key: "parse.math.delimiters",
271        ty: "\"tex\" | \"github\"",
272        default: "\"tex\"",
273        description: "Math delimiter recognition policy. `tex` recognises `\\(...\\)`, `\\[...\\]`, and LaTeX environments; `github` also recognises `$...$` and `$$...$$`.",
274        cli_override: None,
275    },
276    // ---- [parse.extensions] -----------------------------------------
277    FieldDoc {
278        key: "parse.extensions.definition-lists",
279        ty: "bool",
280        default: "true",
281        description: "Recognise `Term\\n: definition\\n` definition lists. Turn off on non-mkdocs corpora to suppress recognition.",
282        cli_override: None,
283    },
284    FieldDoc {
285        key: "parse.extensions.abbreviation-lists",
286        ty: "bool",
287        default: "true",
288        description: "Recognise `*[ABBR]: definition` abbreviation declarations as a scan-and-preserve overlay. mdwright does not expand occurrences; the downstream renderer does.",
289        cli_override: None,
290    },
291    FieldDoc {
292        key: "parse.extensions.heading-attribute-lists",
293        ty: "bool",
294        default: "true",
295        description: "Recognise `# Heading {#id .class}` trailers via pulldown's heading-attribute extension. When off, the trailer reads as plain text in the heading body.",
296        cli_override: None,
297    },
298    FieldDoc {
299        key: "parse.extensions.block-attribute-lists",
300        ty: "bool",
301        default: "true",
302        description: "Recognise `{ .class }` on a line by itself after a non-empty block as a scan-and-preserve overlay. Inline attribute lists are out of scope.",
303        cli_override: None,
304    },
305    // ---- [parse.extensions.gfm] -------------------------------------
306    FieldDoc {
307        key: "parse.extensions.gfm.autolinks",
308        ty: "\"disabled\" | \"urls\" | \"urls-and-emails\"",
309        default: "\"urls-and-emails\"",
310        description: "Recognise GFM bare URL and email autolinks as document facts and render them as links. Use `urls` to leave bare emails as text or `disabled` for strict CommonMark-style text treatment.",
311        cli_override: None,
312    },
313    FieldDoc {
314        key: "parse.extensions.gfm.tagfilter",
315        ty: "bool",
316        default: "true",
317        description: "Apply GFM tagfiltering when rendering or building semantic signatures. This escapes the raw HTML tags that cmark-gfm filters, without rewriting source bytes.",
318        cli_override: None,
319    },
320    // ---- [parse.extensions.myst] ------------------------------------
321    FieldDoc {
322        key: "parse.extensions.myst.directive-containers",
323        ty: "bool",
324        default: "true",
325        description: "Recognise MyST `:::{name}` directive containers with `:KEY: value` options as a scan-and-preserve overlay. mdwright does not expand directives; downstream renderers do.",
326        cli_override: None,
327    },
328    FieldDoc {
329        key: "parse.extensions.myst.inline-roles",
330        ty: "bool",
331        default: "true",
332        description: "Recognise MyST `` {role}`payload` `` inline roles as a scan-and-preserve overlay inside paragraph text.",
333        cli_override: None,
334    },
335    FieldDoc {
336        key: "parse.extensions.myst.substitution-references",
337        ty: "bool",
338        default: "true",
339        description: "Recognise MyST `{{name}}` inline substitution references as a scan-and-preserve overlay. Declarations live in YAML frontmatter and round-trip through the frontmatter path.",
340        cli_override: None,
341    },
342    FieldDoc {
343        key: "parse.extensions.myst.comments",
344        ty: "bool",
345        default: "true",
346        description: "Recognise MyST `%` line comments at line-start as a scan-and-preserve overlay.",
347        cli_override: None,
348    },
349    // ---- [parse.extensions.pandoc] ----------------------------------
350    FieldDoc {
351        key: "parse.extensions.pandoc.fenced-divs",
352        ty: "bool",
353        default: "true",
354        description: "Recognise Pandoc `::: {.cls}` fenced div openers. The closer is a colon-only line of matching count.",
355        cli_override: None,
356    },
357    FieldDoc {
358        key: "parse.extensions.pandoc.short-form-divs",
359        ty: "bool",
360        default: "true",
361        description: "Recognise Pandoc `:::name` fenced div openers.",
362        cli_override: None,
363    },
364    FieldDoc {
365        key: "parse.extensions.pandoc.inline-attribute-spans",
366        ty: "bool",
367        default: "true",
368        description: "Recognise Pandoc `[content]{.cls}` inline attribute spans as a scan-and-preserve overlay.",
369        cli_override: None,
370    },
371    // ---- [render] ----------------------------------------------------
372    FieldDoc {
373        key: "render.profile",
374        ty: "\"pulldown\" | \"cmark-gfm\"",
375        default: "\"pulldown\"",
376        description: "HTML spelling profile for `mdwright render`. `pulldown` preserves the default renderer; `cmark-gfm` matches cmark-gfm spelling where parser semantics already agree.",
377        cli_override: Some("--render-profile"),
378    },
379];
380
381const REFERENCE_PREAMBLE: &str = r#"# Configuration
382
383mdwright reads configuration from (in precedence order):
384
3851. The file given via `--config PATH`.
3862. The nearest ancestor config discovered by walking upward from the
387   current directory. At each ancestor, candidates are tried in this
388   order: `.mdwright.toml`, `mdwright.toml`,
389   `pyproject.toml` containing a `[tool.mdwright]` table. The walk
390   stops at the filesystem root or at the first directory containing
391   `.git/` (the workspace boundary).
3923. Built-in defaults.
393
394A `pyproject.toml` *without* `[tool.mdwright]` does not stop the walk;
395discovery continues to the parent directory. A `.mdwright.toml` wins
396over a `pyproject.toml` in the same directory (matching ruff's
397"more-specific-name first" rule).
398
399Run `mdwright config init` to create a documented `.mdwright.toml`
400starter file with every option set to its default.
401
402## Single-file integration via `pyproject.toml`
403
404For projects that already use `pyproject.toml`, the entire mdwright
405configuration can live there under `[tool.mdwright]`:
406
407```toml
408# pyproject.toml
409[tool.mdwright]
410lint.preset = "default"
411lint.extend-select = ["latex-command"]
412
413[tool.mdwright.fmt]
414wrap = 100
415```
416
417## CLI overrides
418
419The following knobs accept CLI flags that take precedence over the
420config file:
421
422- `lint.preset`, `lint.select`, `lint.extend-select`, `lint.ignore`: `--rules`
423- `render.profile`: `mdwright render --render-profile`
424- `--no-suppress` toggles whether `<!-- mdwright: allow ... -->`
425  comments are honoured; there is no config-file equivalent.
426
427All `[fmt]` knobs are config-file-only.
428
429## Schema reference
430
431"#;
432
433fn render_reference_section(out: &mut String, heading: &str, prefix: &str) {
434    out.push_str("### `");
435    out.push_str(heading);
436    out.push_str("` and nested tables\n\n");
437    out.push_str("| Key | Type | Default | CLI override | Description |\n");
438    out.push_str("| --- | --- | --- | --- | --- |\n");
439    for field in SCHEMA_FIELDS {
440        if !field.key.starts_with(prefix) {
441            continue;
442        }
443        let cli = field.cli_override.unwrap_or("none");
444        let ty_escaped = field.ty.replace('|', "\\|");
445        out.push_str("| `");
446        out.push_str(field.key);
447        out.push_str("` | ");
448        out.push_str(&ty_escaped);
449        out.push_str(" | `");
450        out.push_str(field.default);
451        out.push_str("` | `");
452        out.push_str(cli);
453        out.push_str("` | ");
454        out.push_str(field.description);
455        out.push_str(" |\n");
456    }
457    out.push('\n');
458}