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("", &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}