Skip to main content

mdwright_format/
options.rs

1/// Formatter knobs.
2///
3/// Style knobs default to `Preserve` except GFM tables, which default
4/// to compact normal form. The structural-emit pipeline never consults
5/// style knobs; rewrites are applied by the formatter's rewrite-family
6/// pipeline. Structural defaults: `wrap = keep`, `trailing-newline =
7/// preserve`, `end-of-line = lf`, empty exclude list.
8#[derive(Debug, Clone)]
9pub struct FmtOptions {
10    wrap: Wrap,
11    wrap_strategy: WrapStrategy,
12    italic: ItalicStyle,
13    strong: StrongStyle,
14    list_marker: ListMarkerStyle,
15    list_continuation_indent: ListContinuationIndent,
16    ordered_list: OrderedListStyle,
17    table: TableStyle,
18    trailing_newline: TrailingNewline,
19    end_of_line: EndOfLine,
20    exclude_globs: Vec<String>,
21    link_def_placement: Placement,
22    link_def_style: LinkDefStyle,
23    footnote_placement: Placement,
24    preserve_frontmatter: bool,
25    thematic_break_style: ThematicStyle,
26    math: MathOptions,
27    heading_attrs: HeadingAttrsStyle,
28}
29
30/// Heading-attribute trailer emission policy.
31///
32/// `# Heading {#id .class key=val}` parses (with
33/// `Options::ENABLE_HEADING_ATTRIBUTES`) into a `Tag::Heading` carrying
34/// the parsed `id`, `classes`, and `attrs`. This knob decides how the
35/// formatter re-emits the trailer.
36///
37/// - [`Self::Preserve`] (default): emit the source trailer byte-verbatim
38///   between the rendered inline body and the line terminator. Matches
39///   the preserve-by-default ethos every other style knob defaults to.
40/// - [`Self::Canonicalise`]: emit `{#id .class₁ .class₂ k=val}` in a
41///   fixed order: id first, then classes in source order, then
42///   key=value pairs in source order. Matches mdformat-mkdocs canonical.
43#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
44pub enum HeadingAttrsStyle {
45    /// Emit the source trailer byte-verbatim.
46    #[default]
47    Preserve,
48    /// Emit the trailer in fixed canonical order: `#id`, then classes
49    /// (source order), then `key=value` pairs (source order).
50    Canonicalise,
51}
52
53/// Math canonicalisation configuration.
54///
55/// All fields are off by default. Math regions are opaque to
56/// `CommonMark`: pulldown-cmark parses their bytes as prose, so any
57/// whitespace change inside shifts the byte-level event stream. Authors
58/// who render math downstream (`KaTeX`, `MathJax`) opt in.
59#[derive(Copy, Clone, Debug, Default)]
60pub struct MathOptions {
61    /// Whether whole-block math regions (display `\[…\]` / `$$…$$`
62    /// and environments standing alone) are normalised.
63    pub normalise: bool,
64    /// How math regions are emitted for downstream renderers. See
65    /// [`MathRender`] for the modes; default is [`MathRender::None`]
66    /// (verbatim emission, today's behaviour).
67    pub render: MathRender,
68}
69
70/// Delimiter rewrite policy for math regions at emit time.
71///
72/// mdwright never typesets math itself; downstream renderers
73/// (`KaTeX`, `MathJax`, `mkdocs-material`'s math plugin) do that. The
74/// modes here determine the *shape* of the math regions in the
75/// formatted output so the downstream renderer recognises them.
76///
77/// - [`Self::None`] (default): pass math regions through verbatim;
78///   today's behaviour.
79/// - [`Self::CommonmarkKatex`]: same emission as `None`, but signals
80///   intent in build logs. The bracket/paren forms (`\[…\]`, `\(…\)`)
81///   and dollar forms (`$$…$$`, `$…$`) are both recognised by `KaTeX`
82///   and `MathJax` v3 auto-renderers without rewriting.
83/// - [`Self::Dollar`]: rewrite `\[ … \]` to `$$ … $$` and `\( … \)`
84///   to `$ … $` at emit time. LaTeX environments
85///   (`\begin{align*}…\end{align*}`) are not rewritten; there is no
86///   dollar form of an environment.
87#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
88pub enum MathRender {
89    /// Pass math regions through verbatim.
90    #[default]
91    None,
92    /// Pass through verbatim; greppable signal that downstream is
93    /// `KaTeX` or `MathJax` over `CommonMark`-shaped math.
94    CommonmarkKatex,
95    /// Rewrite backslash-bracket / backslash-paren math to dollar
96    /// form. Environments are left unchanged.
97    Dollar,
98}
99
100impl FmtOptions {
101    /// Wrap mode for prose paragraphs.
102    #[must_use]
103    pub fn wrap(&self) -> Wrap {
104        self.wrap
105    }
106
107    /// Paragraph wrap strategy.
108    #[must_use]
109    pub fn wrap_strategy(&self) -> WrapStrategy {
110        self.wrap_strategy
111    }
112
113    /// Italic delimiter normalisation policy.
114    #[must_use]
115    pub fn italic(&self) -> ItalicStyle {
116        self.italic
117    }
118
119    /// Strong-emphasis delimiter normalisation policy.
120    #[must_use]
121    pub fn strong(&self) -> StrongStyle {
122        self.strong
123    }
124
125    /// Unordered-list bullet normalisation policy.
126    #[must_use]
127    pub fn list_marker(&self) -> ListMarkerStyle {
128        self.list_marker
129    }
130
131    /// List continuation indentation used when wrapping list-item paragraphs.
132    #[must_use]
133    pub fn list_continuation_indent(&self) -> ListContinuationIndent {
134        self.list_continuation_indent
135    }
136
137    /// Ordered-list number normalisation policy.
138    #[must_use]
139    pub fn ordered_list(&self) -> OrderedListStyle {
140        self.ordered_list
141    }
142
143    /// GFM table canonicalisation policy.
144    #[must_use]
145    pub fn table(&self) -> TableStyle {
146        self.table
147    }
148
149    /// Trailing-newline policy at the document boundary.
150    #[must_use]
151    pub fn trailing_newline(&self) -> TrailingNewline {
152        self.trailing_newline
153    }
154
155    /// Line-ending normalisation policy.
156    #[must_use]
157    pub fn end_of_line(&self) -> EndOfLine {
158        self.end_of_line
159    }
160
161    /// Formatter-specific exclude globs (independent of `[lint]
162    /// exclude`).
163    #[must_use]
164    pub fn exclude_globs(&self) -> &[String] {
165        &self.exclude_globs
166    }
167
168    /// Where reference-link definitions are emitted: gathered + sorted
169    /// at end of document, or kept in source positions.
170    #[must_use]
171    pub fn link_def_placement(&self) -> Placement {
172        self.link_def_placement
173    }
174
175    /// Whether reference-link and inline-link destinations are emitted
176    /// bare or angle-bracketed.
177    #[must_use]
178    pub fn link_def_style(&self) -> LinkDefStyle {
179        self.link_def_style
180    }
181
182    /// Where footnote definitions are emitted. Populated for parity
183    /// with [`Self::link_def_placement`]; session 11 reads this.
184    #[must_use]
185    pub fn footnote_placement(&self) -> Placement {
186        self.footnote_placement
187    }
188
189    /// Whether to emit the document's frontmatter byte-verbatim in
190    /// the formatted output. `true` (the default) preserves YAML or
191    /// TOML frontmatter exactly; `false` strips it.
192    #[must_use]
193    pub fn preserve_frontmatter(&self) -> bool {
194        self.preserve_frontmatter
195    }
196
197    /// Thematic-break canonicalisation policy. Defaults to
198    /// [`ThematicStyle::Preserve`]. The structural-emit path does not
199    /// consult this; a later canonicalisation post-pass will.
200    #[must_use]
201    pub fn thematic_break_style(&self) -> ThematicStyle {
202        self.thematic_break_style
203    }
204
205    /// Math canonicalisation configuration. See [`MathOptions`] for the
206    /// reason every field defaults off.
207    #[must_use]
208    pub fn math(&self) -> MathOptions {
209        self.math
210    }
211
212    /// Override the math options. Returns the receiver for chaining.
213    #[must_use]
214    pub fn with_math(mut self, math: MathOptions) -> Self {
215        self.math = math;
216        self
217    }
218
219    /// Override only the math render policy. Composes with the
220    /// existing [`MathOptions`]; preserves `normalise`.
221    #[must_use]
222    pub fn with_math_render(mut self, render: MathRender) -> Self {
223        self.math.render = render;
224        self
225    }
226
227    /// Heading-attribute trailer emission policy.
228    #[must_use]
229    pub fn heading_attrs(&self) -> HeadingAttrsStyle {
230        self.heading_attrs
231    }
232
233    /// Override the heading-attribute style.
234    #[must_use]
235    pub fn with_heading_attrs(mut self, style: HeadingAttrsStyle) -> Self {
236        self.heading_attrs = style;
237        self
238    }
239
240    /// Resolve the italic delimiter to emit, given the byte the
241    /// source originally used (`b'*'` or `b'_'`). `Preserve` returns
242    /// `source_delim`; fixed styles return their own byte. Keeps the
243    /// `match` out of every render site.
244    #[must_use]
245    pub fn resolve_italic(&self, source_delim: u8) -> u8 {
246        match self.italic {
247            ItalicStyle::Asterisk => b'*',
248            ItalicStyle::Underscore => b'_',
249            ItalicStyle::Preserve => source_delim,
250        }
251    }
252
253    /// Override the wrap mode. Returns the receiver for chaining.
254    /// Used by callers that build [`FmtOptions`] programmatically
255    /// (benches, golden tests, `--wrap` overrides).
256    #[must_use]
257    pub fn with_wrap(mut self, wrap: Wrap) -> Self {
258        self.wrap = wrap;
259        self
260    }
261
262    /// Override the paragraph wrap strategy.
263    #[must_use]
264    pub fn with_wrap_strategy(mut self, strategy: WrapStrategy) -> Self {
265        self.wrap_strategy = strategy;
266        self
267    }
268
269    /// Override the italic style. Used by callers that build options
270    /// programmatically (property tests, CLI overrides).
271    #[must_use]
272    pub fn with_italic(mut self, italic: ItalicStyle) -> Self {
273        self.italic = italic;
274        self
275    }
276
277    /// Override the strong style.
278    #[must_use]
279    pub fn with_strong(mut self, strong: StrongStyle) -> Self {
280        self.strong = strong;
281        self
282    }
283
284    /// Override the unordered-list bullet style.
285    #[must_use]
286    pub fn with_list_marker(mut self, list_marker: ListMarkerStyle) -> Self {
287        self.list_marker = list_marker;
288        self
289    }
290
291    /// Override list continuation indentation for wrapped list items.
292    #[must_use]
293    pub fn with_list_continuation_indent(mut self, indent: ListContinuationIndent) -> Self {
294        self.list_continuation_indent = indent;
295        self
296    }
297
298    /// Override the ordered-list numbering policy.
299    #[must_use]
300    pub fn with_ordered_list(mut self, ordered_list: OrderedListStyle) -> Self {
301        self.ordered_list = ordered_list;
302        self
303    }
304
305    /// Override the GFM table canonicalisation policy.
306    #[must_use]
307    pub fn with_table(mut self, table: TableStyle) -> Self {
308        self.table = table;
309        self
310    }
311
312    /// Override the thematic-break canonicalisation policy.
313    #[must_use]
314    pub fn with_thematic_break(mut self, thematic_break: ThematicStyle) -> Self {
315        self.thematic_break_style = thematic_break;
316        self
317    }
318
319    /// Override the link-destination style.
320    #[must_use]
321    pub fn with_link_def_style(mut self, link_def_style: LinkDefStyle) -> Self {
322        self.link_def_style = link_def_style;
323        self
324    }
325
326    /// Resolve the unordered-list bullet to emit, given the byte the
327    /// source used (`b'-'`, `b'*'`, or `b'+'`).
328    #[must_use]
329    pub fn resolve_list_marker(&self, source_marker: u8) -> u8 {
330        match self.list_marker {
331            ListMarkerStyle::Dash => b'-',
332            ListMarkerStyle::Asterisk => b'*',
333            ListMarkerStyle::Plus => b'+',
334            ListMarkerStyle::Preserve => source_marker,
335        }
336    }
337
338    // ----- Canonicalisation post-pass targets ---------------------
339    //
340    // Each accessor below maps a style knob to `Some(target)` when the
341    // user opted into canonicalisation and `None` when the knob is
342    // `Preserve`. Rewrite-family producers are the only
343    // consumers; identity emit never reads these.
344
345    /// Italic-delimiter target byte. `None` keeps source bytes.
346    #[must_use]
347    pub(crate) fn italic_target_byte(&self) -> Option<u8> {
348        match self.italic {
349            ItalicStyle::Asterisk => Some(b'*'),
350            ItalicStyle::Underscore => Some(b'_'),
351            ItalicStyle::Preserve => None,
352        }
353    }
354
355    /// Strong-delimiter target byte. `None` keeps source bytes.
356    #[must_use]
357    pub(crate) fn strong_target_byte(&self) -> Option<u8> {
358        match self.strong {
359            StrongStyle::Asterisk => Some(b'*'),
360            StrongStyle::Underscore => Some(b'_'),
361            StrongStyle::Preserve => None,
362        }
363    }
364
365    /// Unordered-list bullet target byte. `None` keeps source bytes.
366    #[must_use]
367    pub(crate) fn list_marker_target_byte(&self) -> Option<u8> {
368        match self.list_marker {
369            ListMarkerStyle::Dash => Some(b'-'),
370            ListMarkerStyle::Asterisk => Some(b'*'),
371            ListMarkerStyle::Plus => Some(b'+'),
372            ListMarkerStyle::Preserve => None,
373        }
374    }
375
376    /// Ordered-list renumbering policy for the canonicalisation pass.
377    #[must_use]
378    pub(crate) fn ordered_list_target(&self) -> Option<OrderedListStyle> {
379        match self.ordered_list {
380            OrderedListStyle::Consistent | OrderedListStyle::One => Some(self.ordered_list),
381            OrderedListStyle::Preserve => None,
382        }
383    }
384
385    /// Thematic-break shape for the canonicalisation pass.
386    #[must_use]
387    pub(crate) fn thematic_target(&self) -> Option<ThematicStyle> {
388        match self.thematic_break_style {
389            ThematicStyle::Dash | ThematicStyle::Asterisk | ThematicStyle::Underscore | ThematicStyle::Underscore70 => {
390                Some(self.thematic_break_style)
391            }
392            ThematicStyle::Preserve => None,
393        }
394    }
395
396    /// `true` iff GFM tables should be normalised.
397    #[must_use]
398    pub(crate) fn should_normalise_tables(&self) -> bool {
399        !matches!(self.table, TableStyle::Preserve)
400    }
401
402    /// True iff a canonicalisation family other than tables is active.
403    ///
404    /// Tables default to compact normalisation, so callers use this to
405    /// preserve the identity fast path for table-free documents.
406    #[must_use]
407    pub(crate) fn has_non_table_canonicalisation(&self) -> bool {
408        self.italic_target_byte().is_some()
409            || self.strong_target_byte().is_some()
410            || self.list_marker_target_byte().is_some()
411            || self.thematic_target().is_some()
412            || self.ordered_list_target().is_some()
413            || self.link_def_target().is_some()
414            || matches!(self.heading_attrs, HeadingAttrsStyle::Canonicalise)
415            || matches!(self.math.render, MathRender::Dollar)
416            || self.math.normalise
417            || !self.preserve_frontmatter
418    }
419
420    /// Link-destination angle-bracket rewrite target. `None` keeps
421    /// source form per definition.
422    #[must_use]
423    pub(crate) fn link_def_target(&self) -> Option<LinkDefStyle> {
424        match self.link_def_style {
425            LinkDefStyle::Bare => Some(LinkDefStyle::Bare),
426            LinkDefStyle::Angle => Some(LinkDefStyle::Angle),
427            LinkDefStyle::Preserve => None,
428        }
429    }
430
431    /// True iff any style knob is set to a non-`Preserve` value. When
432    /// false, the canonicalisation pass is skipped entirely.
433    #[must_use]
434    pub(crate) fn has_any_canonicalisation(&self) -> bool {
435        self.should_normalise_tables() || self.has_non_table_canonicalisation()
436    }
437
438    /// Override the trailing-newline policy.
439    #[must_use]
440    pub fn with_trailing_newline(mut self, trailing_newline: TrailingNewline) -> Self {
441        self.trailing_newline = trailing_newline;
442        self
443    }
444
445    /// Override the end-of-line policy.
446    #[must_use]
447    pub fn with_end_of_line(mut self, end_of_line: EndOfLine) -> Self {
448        self.end_of_line = end_of_line;
449        self
450    }
451
452    /// Override formatter exclude globs.
453    #[must_use]
454    pub fn with_exclude_globs(mut self, exclude_globs: Vec<String>) -> Self {
455        self.exclude_globs = exclude_globs;
456        self
457    }
458
459    /// Override reference-definition placement.
460    #[must_use]
461    pub fn with_link_def_placement(mut self, placement: Placement) -> Self {
462        self.link_def_placement = placement;
463        self
464    }
465
466    /// Override footnote-definition placement.
467    #[must_use]
468    pub fn with_footnote_placement(mut self, placement: Placement) -> Self {
469        self.footnote_placement = placement;
470        self
471    }
472
473    /// Override frontmatter preservation.
474    #[must_use]
475    pub fn with_preserve_frontmatter(mut self, preserve_frontmatter: bool) -> Self {
476        self.preserve_frontmatter = preserve_frontmatter;
477        self
478    }
479}
480
481impl Default for FmtOptions {
482    fn default() -> Self {
483        Self {
484            wrap: Wrap::Keep,
485            wrap_strategy: WrapStrategy::Stable,
486            // Most style knobs default to Preserve so structural emit
487            // round-trips source bytes. GFM tables default to compact
488            // normal form because redundant cell padding is not useful
489            // source information.
490            italic: ItalicStyle::Preserve,
491            strong: StrongStyle::Preserve,
492            list_marker: ListMarkerStyle::Preserve,
493            list_continuation_indent: ListContinuationIndent::MarkerWidth,
494            ordered_list: OrderedListStyle::Preserve,
495            table: TableStyle::Compact,
496            trailing_newline: TrailingNewline::Preserve,
497            end_of_line: EndOfLine::Lf,
498            exclude_globs: Vec::new(),
499            link_def_placement: Placement::End,
500            link_def_style: LinkDefStyle::Preserve,
501            // Footnotes stay at their source position. Pulldown's HTML
502            // renderer emits the `<div class="footnote-definition">`
503            // block at the point of parsing, so moving definitions to
504            // the document tail under `Placement::End` would change the
505            // event stream.
506            footnote_placement: Placement::Preserve,
507            preserve_frontmatter: true,
508            thematic_break_style: ThematicStyle::Preserve,
509            math: MathOptions::default(),
510            heading_attrs: HeadingAttrsStyle::default(),
511        }
512    }
513}
514
515impl FmtOptions {
516    /// mdformat-compatible formatting profile where mdwright can
517    /// reproduce mdformat's spelling without weakening verification.
518    ///
519    /// mdformat 1.0 defaults to `wrap = keep`; callers that want a
520    /// column limit should still set [`Self::with_wrap`] explicitly.
521    #[must_use]
522    pub fn mdformat() -> Self {
523        Self::default()
524            .with_wrap_strategy(WrapStrategy::Stable)
525            .with_list_marker(ListMarkerStyle::Dash)
526            .with_list_continuation_indent(ListContinuationIndent::FourSpace)
527            .with_ordered_list(OrderedListStyle::One)
528            .with_thematic_break(ThematicStyle::Underscore70)
529            .with_table(TableStyle::Align)
530    }
531}
532
533/// Trailing-newline policy applied at the document boundary.
534///
535/// `Preserve` (the default) matches the source's trailing-newline
536/// shape: if the source ends with one or more `\n` bytes, the
537/// formatted output ends with exactly one `\n`; if the source has no
538/// trailing `\n`, the output has none either. This is what
539/// formatter validation needs to hold: pulldown-cmark's HTML render of
540/// `\t\x10` is `<pre><code>\x10</code></pre>` while its
541/// render of `\t\x10\n` is `<pre><code>\x10\n</code></pre>`; the
542/// trailing LF lives inside the code body for any document ending in
543/// an indented code block. An unconditional "ensure trailing `\n`"
544/// post-pass cannot avoid that class of HTML-divergence; `Preserve`
545/// avoids it by construction.
546///
547/// `Strip` drops every trailing `\n`. `Ensure` forces exactly one
548/// trailing `\n` (the pre-Preserve `trailing_newline = true`
549/// behaviour); kept under an explicit name so callers opt in to the
550/// foot-gun rather than getting it by default.
551#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
552pub enum TrailingNewline {
553    /// Match the source: one trailing `\n` iff the source had any.
554    #[default]
555    Preserve,
556    /// Drop every trailing `\n`.
557    Strip,
558    /// Force exactly one trailing `\n`, appending if absent.
559    Ensure,
560}
561
562/// Emission position for collected items (link reference definitions,
563/// footnote definitions). `End` (the default) gathers items and sorts
564/// them at the end of the document; `Preserve` keeps source order.
565#[derive(Copy, Clone, Debug, PartialEq, Eq)]
566pub enum Placement {
567    End,
568    Preserve,
569}
570
571/// Destination style for link reference definitions and inline links.
572///
573/// `Bare` emits `[label]: url`; `Angle` emits `[label]: <url>`;
574/// `Preserve` (the default) emits whichever form the source used for
575/// each destination.
576#[derive(Copy, Clone, Debug, PartialEq, Eq)]
577pub enum LinkDefStyle {
578    Bare,
579    Angle,
580    Preserve,
581}
582
583/// Prose-wrap mode.
584///
585/// `Keep` (default) and `No` both leave existing breaks alone; the
586/// distinction is whether the formatter is *allowed* to introduce new
587/// breaks later (`Keep` means "do nothing if already within budget",
588/// `No` means "never break"). `At(n)` wraps at column `n`.
589#[derive(Copy, Clone, Debug, PartialEq, Eq)]
590pub enum Wrap {
591    Keep,
592    No,
593    At(u32),
594}
595
596impl Wrap {
597    /// Effective column target. `Keep` and `No` both return
598    /// `u32::MAX`, signalling the wrap pass "do not introduce breaks".
599    #[must_use]
600    pub fn columns(self) -> u32 {
601        match self {
602            Self::Keep | Self::No => u32::MAX,
603            Self::At(n) => n,
604        }
605    }
606
607    /// Reduce the target column count by `n` for nested contexts whose
608    /// physical lines will be prefixed (blockquote `> `, list-item
609    /// marker, footnote indent). `Keep` and `No` are unaffected.
610    #[must_use]
611    pub fn shrink(self, n: u32) -> Self {
612        match self {
613            Self::At(c) => Self::At(c.saturating_sub(n).max(1)),
614            Self::Keep | Self::No => self,
615        }
616    }
617}
618
619/// Paragraph-wrap planner.
620#[derive(Copy, Clone, Debug, PartialEq, Eq)]
621pub enum WrapStrategy {
622    /// Reflow each hard-break-bounded run with mdformat-compatible soft breaks.
623    Stable,
624    /// Rebalance the whole paragraph with a squared-slack planner.
625    Balanced,
626}
627
628impl WrapStrategy {
629    /// TOML spelling for this strategy.
630    #[must_use]
631    pub fn as_str(self) -> &'static str {
632        match self {
633            Self::Stable => "stable",
634            Self::Balanced => "balanced",
635        }
636    }
637}
638
639/// Italic delimiter normalisation policy. Defaults to `Preserve`:
640/// structural emit preserves each run's source delimiter. Fixed
641/// variants are consumed only by the canonicalisation post-pass.
642#[derive(Copy, Clone, Debug, PartialEq, Eq)]
643pub enum ItalicStyle {
644    Asterisk,
645    Underscore,
646    Preserve,
647}
648
649/// Strong-emphasis delimiter normalisation policy. Defaults to
650/// `Preserve`. Independent of [`ItalicStyle`] so an author can
651/// canonicalise one without forcing the other.
652#[derive(Copy, Clone, Debug, PartialEq, Eq)]
653pub enum StrongStyle {
654    Asterisk,
655    Underscore,
656    Preserve,
657}
658
659/// Unordered-list bullet normalisation policy. Defaults to
660/// `Preserve`: structural emit keeps each list's source bullet.
661#[derive(Copy, Clone, Debug, PartialEq, Eq)]
662pub enum ListMarkerStyle {
663    Dash,
664    Asterisk,
665    Plus,
666    Preserve,
667}
668
669/// Continuation indentation for wrapped list-item paragraphs.
670#[derive(Copy, Clone, Debug, PartialEq, Eq)]
671pub enum ListContinuationIndent {
672    /// Continue at the marker width: `- text` wraps to `  text`.
673    MarkerWidth,
674    /// Continue with four spaces after the containing block prefix.
675    FourSpace,
676}
677
678/// Ordered-list number normalisation policy.
679///
680/// `One` rewrites markers to `1.` where verification preserves the
681/// list start. `Consistent` renumbers from the source first marker.
682/// `Preserve` keeps source numbering verbatim.
683#[derive(Copy, Clone, Debug, PartialEq, Eq)]
684pub enum OrderedListStyle {
685    /// Rewrite every item marker in the list to `1.`.
686    One,
687    /// Renumber items from the source list's first marker.
688    Consistent,
689    Preserve,
690}
691
692/// Thematic-break canonicalisation policy. Defaults to `Preserve`:
693/// structural emit echoes the source `---` / `***` / `___` line
694/// verbatim. The fixed variants exist for the future canonicalisation
695/// pass.
696#[derive(Copy, Clone, Debug, PartialEq, Eq)]
697pub enum ThematicStyle {
698    Dash,
699    Asterisk,
700    Underscore,
701    /// Rewrite the whole break line to mdformat's 70 underscores.
702    Underscore70,
703    Preserve,
704}
705
706impl ThematicStyle {
707    /// The repeated byte the thematic-break line is built from, when
708    /// the style names a single byte. `Preserve` returns `None`
709    /// because the byte to emit comes from the source itself.
710    #[must_use]
711    pub fn as_byte(self) -> Option<u8> {
712        match self {
713            Self::Dash => Some(b'-'),
714            Self::Asterisk => Some(b'*'),
715            Self::Underscore | Self::Underscore70 => Some(b'_'),
716            Self::Preserve => None,
717        }
718    }
719}
720
721/// GFM table canonicalisation policy.
722#[derive(Copy, Clone, Debug, PartialEq, Eq)]
723pub enum TableStyle {
724    /// Trim source cell padding and emit one conventional space on
725    /// each side of each cell.
726    Compact,
727    /// Pad cells and delimiter rows to display-width-aligned columns.
728    Align,
729    /// Keep source table spacing.
730    Preserve,
731}
732
733/// Line-ending policy. `Keep` adopts the first newline in the source.
734#[derive(Copy, Clone, Debug, PartialEq, Eq)]
735pub enum EndOfLine {
736    Lf,
737    Crlf,
738    Keep,
739}