discord_md/
generate.rs

1//! Generates markdown text or plain text from an AST
2//!
3//! [`generate`](crate::generate) module provides [`ToMarkdownString`] trait, which provides methods
4//! to generate markdown text or plain text from an AST.
5//!
6//! Note that every struct that implements [`ToMarkdownString`] also implements [`Display`](std::fmt::Display).
7//! This means you can use [`to_string()`](std::string::ToString::to_string())
8//! instead of [`to_markdown_string()`](`ToMarkdownString::to_markdown_string()).
9//!
10//! # Example
11//!
12//! ```
13//! use discord_md::ast::*;
14//! use discord_md::generate::{ToMarkdownString, ToMarkdownStringOption};
15//!
16//! let ast = MarkdownDocument::new(vec![
17//!     MarkdownElement::Bold(Box::new(Bold::new("bold"))),
18//!     MarkdownElement::Plain(Box::new(Plain::new(" text")))
19//! ]);
20//!
21//! assert_eq!(ast.to_string(), "**bold** text");
22//! assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new()), "**bold** text");
23//! assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new().omit_format(true)), "bold text");
24//! ```
25
26use crate::ast::{
27    BlockQuote, Bold, ItalicsStar, ItalicsUnderscore, MarkdownDocument, MarkdownElement,
28    MarkdownElementCollection, MultiLineCode, OneLineCode, Plain, Spoiler, Strikethrough,
29    Underline,
30};
31
32/// Struct that allows to alter [`to_markdown_string()`](`ToMarkdownString::to_markdown_string())'s behaviour.
33/// # Example
34///
35/// ```
36/// use discord_md::ast::*;
37/// use discord_md::generate::{ToMarkdownString, ToMarkdownStringOption};
38///
39/// let ast = MarkdownDocument::new(vec![
40///     MarkdownElement::Spoiler(Box::new(Spoiler::new("spoiler"))),
41///     MarkdownElement::Plain(Box::new(Plain::new(" text "))),
42///     MarkdownElement::OneLineCode(Box::new(OneLineCode::new("code"))),
43/// ]);
44///
45/// assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new()), "||spoiler|| text `code`");
46/// assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new().omit_format(true)), "spoiler text code");
47/// assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new().omit_spoiler(true)), " text `code`");
48/// assert_eq!(ast.to_markdown_string(&ToMarkdownStringOption::new().omit_format(true).omit_one_line_code(true)), "spoiler text ");
49/// ```
50#[derive(Default)]
51#[non_exhaustive]
52pub struct ToMarkdownStringOption {
53    /// Omit markdown styling from the output
54    pub omit_format: bool,
55
56    /// Omit spoilers from the output
57    pub omit_spoiler: bool,
58
59    /// Omit inline codes from the output
60    pub omit_one_line_code: bool,
61
62    /// Omit multiline code blocks from the output
63    pub omit_multi_line_code: bool,
64}
65
66impl ToMarkdownStringOption {
67    pub fn new() -> Self {
68        Default::default()
69    }
70
71    pub fn omit_format(mut self, value: bool) -> Self {
72        self.omit_format = value;
73        self
74    }
75
76    pub fn omit_spoiler(mut self, value: bool) -> Self {
77        self.omit_spoiler = value;
78        self
79    }
80
81    pub fn omit_one_line_code(mut self, value: bool) -> Self {
82        self.omit_one_line_code = value;
83        self
84    }
85
86    pub fn omit_multi_line_code(mut self, value: bool) -> Self {
87        self.omit_multi_line_code = value;
88        self
89    }
90}
91
92/// A trait for converting a markdown component into a String.
93pub trait ToMarkdownString {
94    /// Returns the content of the component as markdown styled text.
95    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String;
96}
97
98impl ToMarkdownString for MarkdownDocument {
99    /// Returns the content of the document as markdown styled text.
100    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
101        self.content().to_markdown_string(option)
102    }
103}
104
105impl ToMarkdownString for MarkdownElementCollection {
106    /// Returns the content of the collection as markdown styled text.
107    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
108        self.get()
109            .iter()
110            .map(|c| c.to_markdown_string(option))
111            .collect::<String>()
112    }
113}
114
115impl ToMarkdownString for MarkdownElement {
116    /// Returns the content of the element as markdown styled text.
117    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
118        match self {
119            MarkdownElement::Plain(x) => x.to_markdown_string(option),
120            MarkdownElement::ItalicsStar(x) => x.to_markdown_string(option),
121            MarkdownElement::ItalicsUnderscore(x) => x.to_markdown_string(option),
122            MarkdownElement::Bold(x) => x.to_markdown_string(option),
123            MarkdownElement::Underline(x) => x.to_markdown_string(option),
124            MarkdownElement::Strikethrough(x) => x.to_markdown_string(option),
125            MarkdownElement::Spoiler(x) => x.to_markdown_string(option),
126            MarkdownElement::OneLineCode(x) => x.to_markdown_string(option),
127            MarkdownElement::MultiLineCode(x) => x.to_markdown_string(option),
128            MarkdownElement::BlockQuote(x) => x.to_markdown_string(option),
129        }
130    }
131}
132
133impl ToMarkdownString for Plain {
134    /// Returns the content of the plain text.
135    fn to_markdown_string(&self, _option: &ToMarkdownStringOption) -> String {
136        self.content().to_string()
137    }
138}
139
140impl ToMarkdownString for ItalicsStar {
141    /// Returns the content of italics text as markdown styled text.
142    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
143        let content = self.content().to_markdown_string(option);
144
145        if option.omit_format {
146            content
147        } else {
148            format!("*{}*", content)
149        }
150    }
151}
152
153impl ToMarkdownString for ItalicsUnderscore {
154    /// Returns the content of italics text as markdown styled text.
155    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
156        let content = self.content().to_markdown_string(option);
157
158        if option.omit_format {
159            content
160        } else {
161            format!("_{}_", content)
162        }
163    }
164}
165
166impl ToMarkdownString for Bold {
167    /// Returns the content of bold text as markdown styled text.
168    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
169        let content = self.content().to_markdown_string(option);
170
171        if option.omit_format {
172            content
173        } else {
174            format!("**{}**", content)
175        }
176    }
177}
178
179impl ToMarkdownString for Underline {
180    /// Returns the content of underline text as markdown styled text.
181    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
182        let content = self.content().to_markdown_string(option);
183
184        if option.omit_format {
185            content
186        } else {
187            format!("__{}__", content)
188        }
189    }
190}
191
192impl ToMarkdownString for Strikethrough {
193    /// Returns the content of strikethrough text as markdown styled text.
194    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
195        let content = self.content().to_markdown_string(option);
196
197        if option.omit_format {
198            content
199        } else {
200            format!("~~{}~~", content)
201        }
202    }
203}
204
205impl ToMarkdownString for Spoiler {
206    /// Returns the content of spoiler text as markdown styled text.
207    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
208        let content = self.content().to_markdown_string(option);
209
210        if option.omit_spoiler {
211            "".to_string()
212        } else if option.omit_format {
213            content
214        } else {
215            format!("||{}||", content)
216        }
217    }
218}
219
220impl ToMarkdownString for OneLineCode {
221    /// Returns the content of the inline code as markdown styled text.
222    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
223        let content = self.content().to_string();
224
225        if option.omit_one_line_code {
226            "".to_string()
227        } else if option.omit_format {
228            content
229        } else {
230            format!("`{}`", content)
231        }
232    }
233}
234
235impl ToMarkdownString for MultiLineCode {
236    /// Returns the content of the multiline code block as markdown styled text.
237    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
238        let content = self.content().to_string();
239
240        if option.omit_multi_line_code {
241            "".to_string()
242        } else if option.omit_format {
243            content
244        } else {
245            format!("```{}{}```", self.language().unwrap_or(""), content)
246        }
247    }
248}
249
250impl ToMarkdownString for BlockQuote {
251    /// Returns the content of the block quote as markdown styled text.
252    fn to_markdown_string(&self, option: &ToMarkdownStringOption) -> String {
253        let content = self.content().to_markdown_string(option);
254
255        if option.omit_format {
256            content
257        } else {
258            content
259                .split('\n')
260                .map(|line| format!("> {}", line))
261                .collect::<Vec<_>>()
262                .join("\n")
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    fn example_text() -> MarkdownElementCollection {
272        MarkdownElementCollection::new(vec![MarkdownElement::Plain(Box::new(Plain::new("text")))])
273    }
274
275    fn option_default() -> ToMarkdownStringOption {
276        ToMarkdownStringOption::new()
277    }
278
279    fn option_omit_format() -> ToMarkdownStringOption {
280        ToMarkdownStringOption::new().omit_format(true)
281    }
282
283    #[test]
284    fn test_document_to_string() {
285        let ast = MarkdownDocument::new(MarkdownElementCollection::new(vec![
286            MarkdownElement::Spoiler(Box::new(Spoiler::new(MarkdownElementCollection::new(
287                vec![MarkdownElement::Plain(Box::new(Plain::new("spoiler")))],
288            )))),
289            MarkdownElement::Plain(Box::new(Plain::new(" plain "))),
290            MarkdownElement::OneLineCode(Box::new(OneLineCode::new("code"))),
291        ]));
292
293        assert_eq!(
294            ast.to_markdown_string(&option_default()),
295            "||spoiler|| plain `code`"
296        );
297        assert_eq!(
298            ast.to_markdown_string(&option_omit_format()),
299            "spoiler plain code"
300        );
301        assert_eq!(
302            ast.to_markdown_string(&option_default().omit_spoiler(true)),
303            " plain `code`"
304        );
305        assert_eq!(
306            ast.to_markdown_string(&option_omit_format().omit_one_line_code(true)),
307            "spoiler plain "
308        );
309        assert_eq!(
310            ast.to_markdown_string(&option_default().omit_spoiler(true).omit_one_line_code(true)),
311            " plain "
312        );
313    }
314
315    #[test]
316    fn test_element_collection_to_string() {
317        let ast = MarkdownElementCollection::new(vec![
318            MarkdownElement::OneLineCode(Box::new(OneLineCode::new("code"))),
319            MarkdownElement::Plain(Box::new(Plain::new(" plain "))),
320            MarkdownElement::Underline(Box::new(Underline::new(MarkdownElementCollection::new(
321                vec![MarkdownElement::Bold(Box::new(Bold::new(
322                    MarkdownElementCollection::new(vec![MarkdownElement::Plain(Box::new(
323                        Plain::new("underline bold"),
324                    ))]),
325                )))],
326            )))),
327        ]);
328
329        assert_eq!(
330            ast.to_markdown_string(&option_default()),
331            "`code` plain __**underline bold**__"
332        );
333        assert_eq!(
334            ast.to_markdown_string(&option_omit_format()),
335            "code plain underline bold"
336        );
337        assert_eq!(
338            ast.to_markdown_string(&option_default().omit_one_line_code(true)),
339            " plain __**underline bold**__"
340        );
341        assert_eq!(
342            ast.to_markdown_string(&option_omit_format().omit_one_line_code(true)),
343            " plain underline bold"
344        );
345    }
346
347    #[test]
348    fn test_plain_to_string() {
349        let ast = Plain::new("plain text");
350
351        assert_eq!(ast.to_markdown_string(&option_default()), "plain text");
352        assert_eq!(ast.to_markdown_string(&option_omit_format()), "plain text");
353    }
354
355    #[test]
356    fn test_italics_star_to_string() {
357        assert_eq!(
358            ItalicsStar::new(example_text()).to_markdown_string(&option_default()),
359            "*text*"
360        );
361        assert_eq!(
362            ItalicsStar::new(example_text()).to_markdown_string(&option_omit_format()),
363            "text"
364        );
365    }
366
367    #[test]
368    fn test_italics_underscore_to_string() {
369        assert_eq!(
370            ItalicsUnderscore::new(example_text()).to_markdown_string(&option_default()),
371            "_text_"
372        );
373        assert_eq!(
374            ItalicsUnderscore::new(example_text()).to_markdown_string(&option_omit_format()),
375            "text"
376        );
377    }
378
379    #[test]
380    fn test_bold_to_string() {
381        assert_eq!(
382            Bold::new(example_text()).to_markdown_string(&option_default()),
383            "**text**"
384        );
385        assert_eq!(
386            Bold::new(example_text()).to_markdown_string(&option_omit_format()),
387            "text"
388        );
389    }
390
391    #[test]
392    fn test_underline_to_string() {
393        assert_eq!(
394            Underline::new(example_text()).to_markdown_string(&option_default()),
395            "__text__"
396        );
397        assert_eq!(
398            Underline::new(example_text()).to_markdown_string(&option_omit_format()),
399            "text"
400        );
401    }
402
403    #[test]
404    fn test_strikethrough_to_string() {
405        assert_eq!(
406            Strikethrough::new(example_text()).to_markdown_string(&option_default()),
407            "~~text~~"
408        );
409        assert_eq!(
410            Strikethrough::new(example_text()).to_markdown_string(&option_omit_format()),
411            "text"
412        );
413    }
414
415    #[test]
416    fn test_spoiler_to_string() {
417        assert_eq!(
418            Spoiler::new(example_text()).to_markdown_string(&option_default()),
419            "||text||"
420        );
421        assert_eq!(
422            Spoiler::new(example_text()).to_markdown_string(&option_omit_format()),
423            "text"
424        );
425        assert_eq!(
426            Spoiler::new(example_text()).to_markdown_string(&option_default().omit_spoiler(true)),
427            ""
428        );
429        assert_eq!(
430            Spoiler::new(example_text())
431                .to_markdown_string(&option_omit_format().omit_spoiler(true)),
432            ""
433        );
434    }
435
436    #[test]
437    fn test_one_line_code_to_string() {
438        assert_eq!(
439            OneLineCode::new("one line code").to_markdown_string(&option_default()),
440            "`one line code`"
441        );
442        assert_eq!(
443            OneLineCode::new("one line code").to_markdown_string(&option_omit_format()),
444            "one line code"
445        );
446        assert_eq!(
447            OneLineCode::new("one line code")
448                .to_markdown_string(&option_default().omit_one_line_code(true)),
449            ""
450        );
451        assert_eq!(
452            OneLineCode::new("one line code")
453                .to_markdown_string(&option_omit_format().omit_one_line_code(true)),
454            ""
455        );
456    }
457
458    #[test]
459    fn test_multi_line_code_to_string() {
460        assert_eq!(
461            MultiLineCode::new("\nmulti\nline\ncode\n", None).to_markdown_string(&option_default()),
462            "```\nmulti\nline\ncode\n```"
463        );
464        assert_eq!(
465            MultiLineCode::new("\nmulti\nline\ncode\n", None)
466                .to_markdown_string(&option_omit_format()),
467            "\nmulti\nline\ncode\n"
468        );
469
470        assert_eq!(
471            MultiLineCode::new(" multi\nline\ncode\n", None).to_markdown_string(&option_default()),
472            "``` multi\nline\ncode\n```"
473        );
474        assert_eq!(
475            MultiLineCode::new(" multi\nline\ncode\n", None)
476                .to_markdown_string(&option_omit_format()),
477            " multi\nline\ncode\n"
478        );
479
480        assert_eq!(
481            MultiLineCode::new("multi line code", None).to_markdown_string(&option_default()),
482            "```multi line code```"
483        );
484        assert_eq!(
485            MultiLineCode::new("multi line code", None).to_markdown_string(&option_omit_format()),
486            "multi line code"
487        );
488
489        assert_eq!(
490            MultiLineCode::new("\nmulti\nline\ncode\n", Some("js".to_string()))
491                .to_markdown_string(&option_default()),
492            "```js\nmulti\nline\ncode\n```"
493        );
494        assert_eq!(
495            MultiLineCode::new("\nmulti\nline\ncode\n", Some("js".to_string()))
496                .to_markdown_string(&option_omit_format()),
497            "\nmulti\nline\ncode\n"
498        );
499
500        assert_eq!(
501            MultiLineCode::new("\nmulti\nline\ncode\n", None)
502                .to_markdown_string(&option_default().omit_multi_line_code(true)),
503            ""
504        );
505        assert_eq!(
506            MultiLineCode::new("\nmulti\nline\ncode\n", None)
507                .to_markdown_string(&option_omit_format().omit_multi_line_code(true)),
508            ""
509        );
510    }
511
512    #[test]
513    fn test_block_quote_to_string() {
514        let test_case = || {
515            MarkdownElementCollection::new(vec![MarkdownElement::Plain(Box::new(Plain::new(
516                "block quote\ntext",
517            )))])
518        };
519
520        assert_eq!(
521            BlockQuote::new(test_case()).to_markdown_string(&option_default()),
522            "> block quote\n> text"
523        );
524        assert_eq!(
525            BlockQuote::new(test_case()).to_markdown_string(&option_omit_format()),
526            "block quote\ntext"
527        );
528    }
529}