Skip to main content

base_markdown/markdown/
configure.rs

1// Released under the MIT License.
2// Copyright, 2024, by Bnchi.
3// Copyright, 2024, by Titus Wormer.
4// Copyright, 2026, by Samuel Williams.
5
6//! Configuration.
7//!
8//! JS equivalent: https://github.com/syntax-tree/mdast-util-to-markdown/blob/fd6a508/lib/types.js#L307.
9use crate::message::Message;
10use alloc::{format, string::String};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13/// Configuration for indent of lists.
14pub enum IndentOptions {
15    /// Depends on the item and its parent list: uses `IndentOptions::One` if
16    /// the item and list are tight and `IndentOptions::Tab` otherwise.
17    Mixed,
18    /// The size of the bullet plus one space.
19    One,
20    /// Tab stop.
21    Tab,
22}
23
24/// Configuration for soft line breaks in phrasing content.
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum LineWrapping {
27    /// Preserve soft line breaks from the syntax tree (the default).
28    #[default]
29    Preserve,
30    /// Replace soft line breaks with spaces.
31    ///
32    /// Explicit Markdown hard breaks, code, and block boundaries are preserved.
33    Unwrap,
34}
35
36/// Configuration.
37#[derive(Clone, Debug)]
38pub struct Options {
39    /// How to handle soft line breaks in text (default: `LineWrapping::Preserve`).
40    pub line_wrapping: LineWrapping,
41    /// Marker to use for bullets of items in unordered lists (`'*'`, `'+'`, or
42    /// `'-'`, default: `'-'`).
43    pub bullet: char,
44    /// Marker to use for bullets of items in ordered lists (`'.'` or `')'`,
45    /// default: `'.'`).
46    pub bullet_ordered: char,
47    /// Marker to use in certain cases where the primary bullet doesn’t work
48    /// (`'*'`, `'+'`, or `'-'`, default: `'-'` when bullet is `'*'`, `'*'`
49    /// otherwise).
50    pub bullet_other: char,
51    /// Whether to add the same number of number signs (`#`) at the end of an
52    /// ATX heading as the opening sequence (`bool`, default: `false`).
53    pub close_atx: bool,
54    /// Marker to use for emphasis (`'*'` or `'_'`, default: `'*'`).
55    pub emphasis: char,
56    /// Marker to use for fenced code (``'`'`` or `'~'`, default: ``'`'``).
57    pub fence: char,
58    /// Whether to use fenced code always (`bool`, default: `true`).
59    /// The default is to use fenced code if there is a language defined,
60    /// if the code is empty,
61    /// or if it starts or ends in blank lines.
62    pub fences: bool,
63    /// Whether to increment the counter of ordered lists items (`bool`,
64    /// default: `true`).
65    pub increment_list_marker: bool,
66    /// How to indent the content of list items (default: `IndentOptions::One`).
67    pub list_item_indent: IndentOptions,
68    /// Marker to use for titles (`'"'` or `"'"`, default: `'"'`).
69    pub quote: char,
70    /// Whether to always use resource links (`bool`, default: `false`).
71    /// The default is to use autolinks (`<https://example.com>`) when possible
72    /// and resource links (`[text](url)`) otherwise.
73    pub resource_link: bool,
74    /// Marker to use for thematic breaks (`'*'`, `'-'`, or `'_'`, default:
75    /// `'*'`).
76    pub rule: char,
77    /// Number of markers to use for thematic breaks (`u32`, default: `3`, min:
78    /// `3`).
79    pub rule_repetition: u32,
80    /// Whether to add spaces between markers in thematic breaks (`bool`,
81    /// default: `false`).
82    pub rule_spaces: bool,
83    /// Whether to use setext headings when possible (`bool`, default:
84    /// `false`).
85    /// The default is to always use ATX headings (`# heading`) instead of
86    /// setext headings (`heading\n=======`).
87    /// Setext headings cannot be used for empty headings or headings with a
88    /// rank of three or more.
89    pub setext: bool,
90    /// Whether to support math (text) with a single dollar (`bool`, default: `true`).
91    /// Single dollars work in Pandoc and many other places, but often interfere with “normal”
92    /// dollars in text.
93    /// If you turn this off, you can still use two or more dollars for text math.
94    pub single_dollar_text_math: bool,
95    /// Marker to use for strong (`'*'` or `'_'`, default: `'*'`).
96    pub strong: char,
97    /// Whether to join definitions without a blank line (`bool`, default:
98    /// `false`).
99    pub tight_definitions: bool,
100}
101
102impl Default for Options {
103    fn default() -> Self {
104        Self {
105            line_wrapping: LineWrapping::default(),
106            bullet: '-',
107            bullet_ordered: '.',
108            bullet_other: '-',
109            close_atx: false,
110            emphasis: '*',
111            fence: '`',
112            fences: true,
113            increment_list_marker: true,
114            list_item_indent: IndentOptions::One,
115            quote: '"',
116            resource_link: false,
117            rule: '*',
118            rule_repetition: 3,
119            rule_spaces: false,
120            setext: false,
121            single_dollar_text_math: true,
122            strong: '*',
123            tight_definitions: false,
124        }
125    }
126}
127
128impl Options {
129    pub(super) fn check_bullet(&self) -> Result<char, Message> {
130        let marker = self.bullet;
131
132        if marker != '*' && marker != '+' && marker != '-' {
133            return Err(invalid_option(
134                format!("Cannot serialize items with `{marker}` for `options.bullet`, expected `*`, `+`, or `-`"),
135                "unexpected-marker",
136            ));
137        }
138
139        Ok(marker)
140    }
141
142    pub(super) fn check_bullet_ordered(&self) -> Result<char, Message> {
143        let marker = self.bullet_ordered;
144
145        if marker != '.' && marker != ')' {
146            return Err(invalid_option(format!("Cannot serialize items with `{marker}` for `options.bullet_ordered`, expected `.` or `)`"), "unexpected-marker"));
147        }
148
149        Ok(marker)
150    }
151
152    pub(super) fn check_bullet_other(&self) -> Result<char, Message> {
153        let bullet = self.check_bullet()?;
154        let mut bullet_other = self.bullet_other;
155
156        if bullet != '*' {
157            bullet_other = '*';
158        }
159
160        if bullet_other != '*' && bullet_other != '+' && bullet_other != '-' {
161            return Err(invalid_option(format!("Cannot serialize items with `{bullet_other}` for `options.bullet_other`, expected `*`, `+`, or `-`"), "unexpected-marker"));
162        }
163
164        if bullet_other == bullet {
165            return Err(invalid_option(
166                format!("Expected `bullet` (`{bullet}`) and `bullet_other` (`{bullet_other}`) to be different"),
167                "bullet-match-bullet_other",
168            ));
169        }
170
171        Ok(bullet_other)
172    }
173
174    pub(super) fn check_emphasis(&self) -> Result<char, Message> {
175        let marker = self.emphasis;
176
177        if marker != '*' && marker != '_' {
178            return Err(invalid_option(
179                format!("Cannot serialize emphasis with `{marker}` for `options.emphasis`, expected `*`, or `_`"),
180                "unexpected-marker",
181            ));
182        }
183
184        Ok(marker)
185    }
186
187    pub(super) fn check_fence(&self) -> Result<char, Message> {
188        let marker = self.fence;
189
190        if marker != '`' && marker != '~' {
191            return Err(invalid_option(
192                format!("Cannot serialize code with `{marker}` for `options.fence`, expected `` ` `` or `~`"),
193                "unexpected-marker",
194            ));
195        }
196
197        Ok(marker)
198    }
199
200    pub(super) fn check_quote(&self) -> Result<char, Message> {
201        let marker = self.quote;
202
203        if marker != '"' && marker != '\'' {
204            return Err(invalid_option(
205                format!("Cannot serialize title with `{marker}`  for `options.quote`, expected `\"`, or `'`"),
206                "unexpected-marker",
207            ));
208        }
209
210        Ok(marker)
211    }
212
213    pub(super) fn check_rule(&self) -> Result<char, Message> {
214        let marker = self.rule;
215
216        if marker != '*' && marker != '-' && marker != '_' {
217            return Err(invalid_option(
218                format!("Cannot serialize rules with `{marker}` for `options.rule`, expected `*`, `-`, or `_`"),
219                "unexpected-marker",
220            ));
221        }
222
223        Ok(marker)
224    }
225
226    pub(super) fn check_rule_repetition(&self) -> Result<u32, Message> {
227        let repetition = self.rule_repetition;
228
229        if repetition < 3 {
230            return Err(invalid_option(format!("Cannot serialize rules with repetition `{repetition}` for `options.rule_repetition`, expected `3` or more"), "unexpected-marker"));
231        }
232
233        Ok(repetition)
234    }
235
236    pub(super) fn check_strong(&self) -> Result<char, Message> {
237        let marker = self.strong;
238
239        if marker != '*' && marker != '_' {
240            return Err(invalid_option(
241                format!("Cannot serialize strong with `{marker}` for `options.strong`, expected `*`, or `_`"),
242                "unexpected-marker",
243            ));
244        }
245
246        Ok(marker)
247    }
248}
249
250fn invalid_option(reason: String, rule_id: &'static str) -> Message {
251    Message {
252        span: None,
253        reason: reason.into(),
254        rule_id,
255        source: "mdast-util-to-markdown",
256    }
257}
258
259#[cfg(test)]
260mod tests;