Skip to main content

tui_markdown/
options.rs

1//! Rendering configuration for tui-markdown.
2//!
3//! Options control the renderer's style sheet, image fallback content, and syntax-highlighting
4//! theme. [`Options`] is non-exhaustive, allowing new rendering choices to be added without
5//! breaking existing code.
6
7#[cfg(feature = "highlight-code")]
8use crate::CodeTheme;
9use crate::{DefaultStyleSheet, StyleSheet};
10
11/// Text used to represent Markdown images in rendered terminal output.
12///
13/// This option does not load or render image resources. It controls whether the text fallback
14/// contains the image description, destination, or both. [`AltText`](Self::AltText) is the
15/// default.
16///
17/// # Example
18///
19/// ```
20/// use tui_markdown::{from_str_with_options, ImageFallback, Options};
21///
22/// let options = Options::default().image_fallback(ImageFallback::AltTextAndUrl);
23/// let text = from_str_with_options("![Architecture diagram](diagram.png)", &options);
24///
25/// assert_eq!(
26///     text.to_string(),
27///     "[img] Architecture diagram (diagram.png)"
28/// );
29/// ```
30#[non_exhaustive]
31#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
32pub enum ImageFallback {
33    /// Show `[img]` followed by the description, or the destination when the description is empty.
34    #[default]
35    AltText,
36    /// Show `[img]` followed by the destination, ignoring the description.
37    Url,
38    /// Show `[img] {description} ({destination})`, omitting either value when it is empty.
39    AltTextAndUrl,
40}
41
42/// Rendering options for [`crate::from_str_with_options`].
43///
44/// `S` is the style sheet consulted while Markdown events are rendered. [`Options::default`] uses
45/// [`DefaultStyleSheet`]. Use [`Options::new`] to supply another [`StyleSheet`].
46/// [`StyleSheet::heading_marker`] and [`StyleSheet::code_block_fence`] customize or hide the
47/// corresponding presentation symbols.
48///
49/// # Example
50///
51/// ```
52/// use tui_markdown::Options;
53///
54/// let options = Options::default();
55///
56/// // or with a custom style sheet
57///
58/// use ratatui_core::style::{Style, Stylize};
59/// use tui_markdown::StyleSheet;
60///
61/// #[derive(Debug, Clone)]
62/// struct MyStyleSheet;
63///
64/// impl StyleSheet for MyStyleSheet {
65///     fn heading(&self, _level: u8) -> Style {
66///         Style::new().bold()
67///     }
68/// }
69///
70/// let options = Options::new(MyStyleSheet);
71/// ```
72#[derive(Debug, Clone)]
73#[non_exhaustive]
74pub struct Options<S: StyleSheet = DefaultStyleSheet> {
75    /// The [`StyleSheet`] implementation that will be consulted every time the renderer needs a
76    /// style or symbol choice.
77    pub(crate) styles: S,
78    /// The content to render in place of images.
79    pub(crate) image_fallback: ImageFallback,
80    /// Explicit syntax-highlighting theme for fenced code blocks.
81    ///
82    /// When absent, the renderer uses the shared built-in default.
83    #[cfg(feature = "highlight-code")]
84    code_theme: Option<CodeTheme>,
85}
86
87impl<S: StyleSheet> Options<S> {
88    /// Creates rendering options that use `styles`.
89    ///
90    /// Image fallback and syntax-highlighting settings retain their defaults.
91    pub fn new(styles: S) -> Self {
92        Self {
93            styles,
94            image_fallback: ImageFallback::default(),
95            #[cfg(feature = "highlight-code")]
96            code_theme: None,
97        }
98    }
99
100    /// Selects the text used to represent Markdown images.
101    ///
102    /// See [`ImageFallback`] for the exact output of each mode.
103    #[must_use]
104    pub fn image_fallback(mut self, image_fallback: ImageFallback) -> Self {
105        self.image_fallback = image_fallback;
106        self
107    }
108
109    /// Selects the syntax-highlighting theme for fenced code blocks.
110    ///
111    /// By default, no explicit theme is stored and the renderer borrows its shared
112    /// [`Base16OceanDark`](crate::BuiltinCodeTheme::Base16OceanDark) theme.
113    /// Pass a [`BuiltinCodeTheme`](crate::BuiltinCodeTheme) directly, or pass an owned
114    /// [`CodeTheme`]. Construct custom themes from TextMate source with
115    /// [`CodeTheme::from_textmate`](crate::CodeTheme::from_textmate), or load a TextMate file with
116    /// [`CodeTheme::from_file`](crate::CodeTheme::from_file). The selected theme applies when a
117    /// fenced code block names a recognized language.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use tui_markdown::{BuiltinCodeTheme, Options};
123    ///
124    /// let options = Options::default().code_theme(BuiltinCodeTheme::SolarizedDark);
125    /// ```
126    #[cfg(feature = "highlight-code")]
127    #[must_use]
128    pub fn code_theme(mut self, code_theme: impl Into<CodeTheme>) -> Self {
129        self.code_theme = Some(code_theme.into());
130        self
131    }
132
133    /// Returns the explicitly configured syntax-highlighting theme.
134    ///
135    /// Returns `None` when the renderer will use the shared
136    /// [`Base16OceanDark`](crate::BuiltinCodeTheme::Base16OceanDark) default.
137    #[cfg(feature = "highlight-code")]
138    #[must_use]
139    pub fn selected_code_theme(&self) -> Option<&CodeTheme> {
140        self.code_theme.as_ref()
141    }
142}
143
144impl Default for Options<DefaultStyleSheet> {
145    fn default() -> Self {
146        Self::new(DefaultStyleSheet)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use ratatui_core::style::Style;
153
154    use super::*;
155
156    #[test]
157    fn default() {
158        let options: Options = Default::default();
159        assert_eq!(
160            options.styles.heading(1),
161            Style::new().on_cyan().bold().underlined()
162        );
163    }
164
165    #[test]
166    fn custom_style_sheet() {
167        #[derive(Debug, Clone)]
168        struct CustomStyleSheet;
169
170        impl StyleSheet for CustomStyleSheet {
171            fn heading(&self, level: u8) -> Style {
172                match level {
173                    1 => Style::new().red().bold(),
174                    _ => Style::new().green(),
175                }
176            }
177        }
178
179        let options = Options {
180            styles: CustomStyleSheet,
181            image_fallback: ImageFallback::default(),
182            #[cfg(feature = "highlight-code")]
183            code_theme: None,
184        };
185
186        assert_eq!(options.styles.heading(1), Style::new().red().bold());
187        assert_eq!(options.styles.heading(2), Style::new().green());
188        assert_eq!(options.styles.code(), Style::new().white().on_black());
189        assert_eq!(options.styles.link(), Style::new().blue().underlined());
190        assert_eq!(options.styles.blockquote(), Style::new().green());
191        assert_eq!(options.styles.heading_meta(), Style::new().dim());
192        assert_eq!(options.styles.metadata_block(), Style::new().light_yellow());
193        assert_eq!(options.styles.image_alt(), Style::new().dim().italic());
194    }
195
196    #[test]
197    fn image_fallback_defaults_to_alt_text() {
198        let options = Options::default();
199
200        assert_eq!(options.image_fallback, ImageFallback::AltText);
201    }
202
203    #[test]
204    fn image_fallback_setter_updates_mode() {
205        let options = Options::default().image_fallback(ImageFallback::AltTextAndUrl);
206
207        assert_eq!(options.image_fallback, ImageFallback::AltTextAndUrl);
208    }
209
210    #[test]
211    #[cfg(feature = "highlight-code")]
212    fn default_has_no_explicit_code_theme() {
213        let options: Options = Options::default();
214
215        assert!(options.selected_code_theme().is_none());
216    }
217
218    #[test]
219    #[cfg(feature = "highlight-code")]
220    fn code_theme_selects_theme() {
221        let options = Options::default().code_theme(crate::BuiltinCodeTheme::SolarizedDark);
222
223        assert!(options.selected_code_theme().is_some());
224    }
225}