1#![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]
2
3use crate::options::preprocessing::PreprocessingOptions;
6use crate::options::validation::{
7 CodeBlockStyle, HeadingStyle, HighlightStyle, LinkStyle, ListIndentType, NewlineStyle, OutputFormat, WhitespaceMode,
8};
9
10#[derive(Debug, Clone)]
26#[cfg_attr(
27 any(feature = "serde", feature = "metadata"),
28 derive(serde::Serialize, serde::Deserialize)
29)]
30#[cfg_attr(any(feature = "serde", feature = "metadata"), serde(default, deny_unknown_fields))]
31pub struct ConversionOptions {
32 pub heading_style: HeadingStyle,
34 pub list_indent_type: ListIndentType,
36 pub list_indent_width: usize,
38 pub bullets: String,
40 pub strong_em_symbol: char,
42 pub escape_asterisks: bool,
44 pub escape_underscores: bool,
46 pub escape_misc: bool,
48 pub escape_ascii: bool,
50 pub code_language: String,
52 pub autolinks: bool,
54 pub default_title: bool,
56 pub br_in_tables: bool,
58 pub highlight_style: HighlightStyle,
60 pub extract_metadata: bool,
62 pub whitespace_mode: WhitespaceMode,
64 pub strip_newlines: bool,
66 pub wrap: bool,
68 pub wrap_width: usize,
70 pub convert_as_inline: bool,
72 pub sub_symbol: String,
74 pub sup_symbol: String,
76 pub newline_style: NewlineStyle,
78 pub code_block_style: CodeBlockStyle,
80 pub keep_inline_images_in: Vec<String>,
82 pub preprocessing: PreprocessingOptions,
84 pub encoding: String,
86 pub debug: bool,
88 pub strip_tags: Vec<String>,
90 pub preserve_tags: Vec<String>,
92 pub skip_images: bool,
94 pub link_style: LinkStyle,
96 pub output_format: OutputFormat,
98 pub include_document_structure: bool,
100 pub extract_images: bool,
102 pub max_image_size: u64,
104 pub capture_svg: bool,
106 pub infer_dimensions: bool,
108}
109
110impl Default for ConversionOptions {
111 fn default() -> Self {
112 Self {
113 heading_style: HeadingStyle::default(),
114 list_indent_type: ListIndentType::default(),
115 list_indent_width: 2,
116 bullets: "-*+".to_string(),
117 strong_em_symbol: '*',
118 escape_asterisks: false,
119 escape_underscores: false,
120 escape_misc: false,
121 escape_ascii: false,
122 code_language: String::new(),
123 autolinks: true,
124 default_title: false,
125 br_in_tables: false,
126 highlight_style: HighlightStyle::default(),
127 extract_metadata: true,
128 whitespace_mode: WhitespaceMode::default(),
129 strip_newlines: false,
130 wrap: false,
131 wrap_width: 80,
132 convert_as_inline: false,
133 sub_symbol: String::new(),
134 sup_symbol: String::new(),
135 newline_style: NewlineStyle::Spaces,
136 code_block_style: CodeBlockStyle::default(),
137 keep_inline_images_in: Vec::new(),
138 preprocessing: PreprocessingOptions::default(),
139 encoding: "utf-8".to_string(),
140 debug: false,
141 strip_tags: Vec::new(),
142 preserve_tags: Vec::new(),
143 skip_images: false,
144 link_style: LinkStyle::default(),
145 output_format: OutputFormat::default(),
146 include_document_structure: false,
147 extract_images: false,
148 max_image_size: 5_242_880,
149 capture_svg: false,
150 infer_dimensions: true,
151 }
152 }
153}
154
155impl ConversionOptions {
156 #[must_use]
158 pub fn builder() -> ConversionOptionsBuilder {
159 ConversionOptionsBuilder(Self::default())
160 }
161}
162
163#[derive(Debug, Clone)]
169pub struct ConversionOptionsBuilder(ConversionOptions);
170
171macro_rules! builder_setter {
172 ($name:ident, $ty:ty) => {
173 #[must_use]
175 pub fn $name(mut self, value: $ty) -> Self {
176 self.0.$name = value;
177 self
178 }
179 };
180}
181
182macro_rules! builder_setter_into {
183 ($name:ident, $ty:ty) => {
184 #[must_use]
186 pub fn $name(mut self, value: impl Into<$ty>) -> Self {
187 self.0.$name = value.into();
188 self
189 }
190 };
191}
192
193impl ConversionOptionsBuilder {
194 builder_setter!(output_format, OutputFormat);
196 builder_setter!(include_document_structure, bool);
197 builder_setter!(extract_metadata, bool);
198 builder_setter!(extract_images, bool);
199
200 builder_setter!(heading_style, HeadingStyle);
202 builder_setter!(list_indent_type, ListIndentType);
203 builder_setter!(list_indent_width, usize);
204 builder_setter_into!(bullets, String);
205 builder_setter!(strong_em_symbol, char);
206 builder_setter!(code_block_style, CodeBlockStyle);
207 builder_setter!(newline_style, NewlineStyle);
208 builder_setter!(highlight_style, HighlightStyle);
209 builder_setter_into!(code_language, String);
210 builder_setter!(link_style, LinkStyle);
211 builder_setter!(autolinks, bool);
212 builder_setter!(default_title, bool);
213 builder_setter!(br_in_tables, bool);
214 builder_setter_into!(sub_symbol, String);
215 builder_setter_into!(sup_symbol, String);
216
217 builder_setter!(escape_asterisks, bool);
219 builder_setter!(escape_underscores, bool);
220 builder_setter!(escape_misc, bool);
221 builder_setter!(escape_ascii, bool);
222
223 builder_setter!(whitespace_mode, WhitespaceMode);
225 builder_setter!(strip_newlines, bool);
226 builder_setter!(wrap, bool);
227 builder_setter!(wrap_width, usize);
228
229 builder_setter!(convert_as_inline, bool);
231 builder_setter!(skip_images, bool);
232
233 #[must_use]
235 pub fn strip_tags(mut self, tags: Vec<String>) -> Self {
236 self.0.strip_tags = tags;
237 self
238 }
239
240 #[must_use]
242 pub fn preserve_tags(mut self, tags: Vec<String>) -> Self {
243 self.0.preserve_tags = tags;
244 self
245 }
246
247 #[must_use]
249 pub fn keep_inline_images_in(mut self, tags: Vec<String>) -> Self {
250 self.0.keep_inline_images_in = tags;
251 self
252 }
253
254 builder_setter!(max_image_size, u64);
256 builder_setter!(capture_svg, bool);
257 builder_setter!(infer_dimensions, bool);
258
259 #[must_use]
262 pub fn preprocessing(mut self, preprocessing: PreprocessingOptions) -> Self {
263 self.0.preprocessing = preprocessing;
264 self
265 }
266
267 builder_setter_into!(encoding, String);
269
270 builder_setter!(debug, bool);
272
273 #[must_use]
275 pub fn build(self) -> ConversionOptions {
276 self.0
277 }
278}
279
280use crate::options::preprocessing::PreprocessingOptionsUpdate;
283
284#[derive(Debug, Clone, Default)]
289#[cfg_attr(
290 any(feature = "serde", feature = "metadata"),
291 derive(serde::Serialize, serde::Deserialize)
292)]
293#[cfg_attr(any(feature = "serde", feature = "metadata"), serde(deny_unknown_fields))]
294pub struct ConversionOptionsUpdate {
295 pub heading_style: Option<HeadingStyle>,
297 pub list_indent_type: Option<ListIndentType>,
299 pub list_indent_width: Option<usize>,
301 pub bullets: Option<String>,
303 pub strong_em_symbol: Option<char>,
305 pub escape_asterisks: Option<bool>,
307 pub escape_underscores: Option<bool>,
309 pub escape_misc: Option<bool>,
311 pub escape_ascii: Option<bool>,
313 pub code_language: Option<String>,
315 pub autolinks: Option<bool>,
317 pub default_title: Option<bool>,
319 pub br_in_tables: Option<bool>,
321 pub highlight_style: Option<HighlightStyle>,
323 pub extract_metadata: Option<bool>,
325 pub whitespace_mode: Option<WhitespaceMode>,
327 pub strip_newlines: Option<bool>,
329 pub wrap: Option<bool>,
331 pub wrap_width: Option<usize>,
333 pub convert_as_inline: Option<bool>,
335 pub sub_symbol: Option<String>,
337 pub sup_symbol: Option<String>,
339 pub newline_style: Option<NewlineStyle>,
341 pub code_block_style: Option<CodeBlockStyle>,
343 pub keep_inline_images_in: Option<Vec<String>>,
345 pub preprocessing: Option<PreprocessingOptionsUpdate>,
347 pub encoding: Option<String>,
349 pub debug: Option<bool>,
351 pub strip_tags: Option<Vec<String>>,
353 pub preserve_tags: Option<Vec<String>>,
355 pub skip_images: Option<bool>,
357 pub link_style: Option<LinkStyle>,
359 pub output_format: Option<OutputFormat>,
361 pub include_document_structure: Option<bool>,
363 pub extract_images: Option<bool>,
365 pub max_image_size: Option<u64>,
367 pub capture_svg: Option<bool>,
369 pub infer_dimensions: Option<bool>,
371}
372
373impl ConversionOptions {
374 pub fn apply_update(&mut self, update: ConversionOptionsUpdate) {
376 macro_rules! apply {
377 ($field:ident) => {
378 if let Some(v) = update.$field {
379 self.$field = v;
380 }
381 };
382 }
383 apply!(heading_style);
384 apply!(list_indent_type);
385 apply!(list_indent_width);
386 apply!(bullets);
387 apply!(strong_em_symbol);
388 apply!(escape_asterisks);
389 apply!(escape_underscores);
390 apply!(escape_misc);
391 apply!(escape_ascii);
392 apply!(code_language);
393 apply!(autolinks);
394 apply!(default_title);
395 apply!(br_in_tables);
396 apply!(highlight_style);
397 apply!(extract_metadata);
398 apply!(whitespace_mode);
399 apply!(strip_newlines);
400 apply!(wrap);
401 apply!(wrap_width);
402 apply!(convert_as_inline);
403 apply!(sub_symbol);
404 apply!(sup_symbol);
405 apply!(newline_style);
406 apply!(code_block_style);
407 apply!(keep_inline_images_in);
408 apply!(encoding);
409 apply!(debug);
410 apply!(strip_tags);
411 apply!(preserve_tags);
412 apply!(skip_images);
413 apply!(link_style);
414 apply!(output_format);
415 apply!(include_document_structure);
416 apply!(extract_images);
417 apply!(max_image_size);
418 apply!(capture_svg);
419 apply!(infer_dimensions);
420 if let Some(preprocessing) = update.preprocessing {
421 self.preprocessing.apply_update(preprocessing);
422 }
423 }
424
425 #[must_use]
427 pub fn from_update(update: ConversionOptionsUpdate) -> Self {
428 let mut options = Self::default();
429 options.apply_update(update);
430 options
431 }
432}
433
434impl From<ConversionOptionsUpdate> for ConversionOptions {
435 fn from(update: ConversionOptionsUpdate) -> Self {
436 Self::from_update(update)
437 }
438}
439
440#[cfg(all(test, any(feature = "serde", feature = "metadata")))]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_conversion_options_serde() {
448 let options = ConversionOptions::builder()
449 .heading_style(HeadingStyle::AtxClosed)
450 .list_indent_width(4)
451 .bullets("*")
452 .escape_asterisks(true)
453 .whitespace_mode(WhitespaceMode::Strict)
454 .build();
455
456 let json = serde_json::to_string(&options).expect("Failed to serialize");
457 let deserialized: ConversionOptions = serde_json::from_str(&json).expect("Failed to deserialize");
458
459 assert_eq!(deserialized.list_indent_width, 4);
460 assert_eq!(deserialized.bullets, "*");
461 assert!(deserialized.escape_asterisks);
462 assert_eq!(deserialized.heading_style, HeadingStyle::AtxClosed);
463 assert_eq!(deserialized.whitespace_mode, WhitespaceMode::Strict);
464 }
465
466 #[test]
467 fn test_conversion_options_partial_deserialization() {
468 let partial_json = r#"{
469 "heading_style": "atxclosed",
470 "list_indent_width": 4,
471 "bullets": "*"
472 }"#;
473
474 let deserialized: ConversionOptions =
475 serde_json::from_str(partial_json).expect("Failed to deserialize partial JSON");
476
477 assert_eq!(deserialized.heading_style, HeadingStyle::AtxClosed);
478 assert_eq!(deserialized.list_indent_width, 4);
479 assert_eq!(deserialized.bullets, "*");
480 assert!(!deserialized.escape_asterisks);
481 assert!(!deserialized.escape_underscores);
482 assert_eq!(deserialized.list_indent_type, ListIndentType::Spaces);
483 }
484
485 #[test]
486 fn test_builder_pattern() {
487 let options = ConversionOptions::builder()
488 .heading_style(HeadingStyle::Underlined)
489 .wrap(true)
490 .wrap_width(100)
491 .include_document_structure(true)
492 .extract_images(true)
493 .build();
494
495 assert_eq!(options.heading_style, HeadingStyle::Underlined);
496 assert!(options.wrap);
497 assert_eq!(options.wrap_width, 100);
498 assert!(options.include_document_structure);
499 assert!(options.extract_images);
500 }
501}