discord_md/
lib.rs

1//! discord-md is a Rust library that provides parser and builder for Discord's markdown.
2//!
3//! # Installation
4//!
5//! Add the following to your `Cargo.toml` file:
6//!
7//! ```toml
8//! [dependencies]
9//! discord-md = "3.0.0"
10//! ```
11//!
12//! # Parsing
13//!
14//! [`parse`] parses a markdown document and returns an AST.
15//!
16//! ## Example
17//!
18//! ```
19//! use discord_md::ast::*;
20//! use discord_md::parse;
21//!
22//! let message = "You can write *italics text*, `*inline code*`, and more!";
23//!
24//! let ast = MarkdownDocument::new(vec![
25//!     MarkdownElement::Plain(Box::new(
26//!         Plain::new("You can write ")
27//!     )),
28//!     MarkdownElement::ItalicsStar(Box::new(
29//!         ItalicsStar::new(vec![
30//!             MarkdownElement::Plain(Box::new(
31//!                 Plain::new("italics text")
32//!             ))
33//!         ])
34//!     )),
35//!     MarkdownElement::Plain(Box::new(
36//!         Plain::new(", ")
37//!     )),
38//!     MarkdownElement::OneLineCode(Box::new(
39//!         OneLineCode::new("*inline code*")
40//!     )),
41//!     MarkdownElement::Plain(Box::new(
42//!         Plain::new(", and more!")
43//!     )),
44//! ]);
45//!
46//! assert_eq!(
47//!     parse(message),
48//!     ast
49//! );
50//! ```
51//!
52//! ```
53//! use discord_md::ast::*;
54//! use discord_md::parse;
55//!
56//! let message = "Of course __*nested* styles__ are supported!";
57//!
58//! let ast = MarkdownDocument::new(vec![
59//!     MarkdownElement::Plain(Box::new(
60//!         Plain::new("Of course ")
61//!     )),
62//!     MarkdownElement::Underline(Box::new(
63//!         Underline::new(vec![
64//!             MarkdownElement::ItalicsStar(Box::new(
65//!                 ItalicsStar::new(vec![
66//!                     MarkdownElement::Plain(Box::new(
67//!                         Plain::new("nested")
68//!                     ))
69//!                 ])
70//!             )),
71//!             MarkdownElement::Plain(Box::new(
72//!                 Plain::new(" styles")
73//!             )),
74//!         ])
75//!     )),
76//!     MarkdownElement::Plain(Box::new(
77//!         Plain::new(" are supported!")
78//!     )),
79//! ]);
80//!
81//! assert_eq!(
82//!     parse(message),
83//!     ast
84//! );
85//! ```
86//!
87//! ```
88//! use discord_md::ast::*;
89//! use discord_md::parse;
90//!
91//! let message = r#"```sh
92//! echo "Code block is _available_ too!"
93//! ```"#;
94//!
95//! let ast = MarkdownDocument::new(vec![
96//!     MarkdownElement::MultiLineCode(Box::new(
97//!         MultiLineCode::new(
98//!             "\necho \"Code block is _available_ too!\"\n",
99//!             Some("sh".to_string())
100//!         )
101//!     ))
102//! ]);
103//!
104//! assert_eq!(
105//!     parse(message),
106//!     ast
107//! );
108//! ```
109//!
110//! # Generating
111//!
112//! First, build an AST with [`builder`] module.
113//! Then call `to_string()` to generate markdown text from the AST.
114//!
115//! ## Example
116//!
117//! ```
118//! use discord_md::ast::MarkdownDocument;
119//! use discord_md::builder::*;
120//!
121//! let ast = MarkdownDocument::new(vec![
122//!     plain("generating "),
123//!     one_line_code("markdown"),
124//!     plain(" is "),
125//!     underline(vec![
126//!         bold("easy"),
127//!         plain(" and "),
128//!         bold("fun!"),
129//!     ]),
130//! ]);
131//!
132//! assert_eq!(
133//!     ast.to_string(),
134//!     "generating `markdown` is __**easy** and **fun!**__"
135//! );
136//! ```
137//!
138//! # Parser limitations
139//!
140//! The parser tries to mimic the behavior of the official Discord client's markdown parser, but it's not perfect.
141//! The following is the list of known limitations.
142//!
143//! - Block quotes are not parsed. `> ` will be treated as plain text.
144//! - Nested emphasis, like `*italics **bold italics** italics*`, may not be parsed properly.
145//! - Intraword emphasis may not be handled properly. The parser treats `foo_bar_baz` as emphasis, while Discord's parser does not.
146//! - Escaping sequence will be treated as plain text.
147
148pub mod ast;
149pub mod builder;
150pub mod generate;
151mod parser;
152
153use ast::MarkdownDocument;
154
155/// Parses a markdown document and returns AST.
156///
157/// # Example
158///
159/// ```
160/// use discord_md::ast::*;
161/// use discord_md::parse;
162///
163/// let message = "this **is** markdown.";
164///
165/// let ast = MarkdownDocument::new(vec![
166///     MarkdownElement::Plain(Box::new(
167///         Plain::new("this ")
168///     )),
169///     MarkdownElement::Bold(Box::new(
170///         Bold::new(vec![
171///             MarkdownElement::Plain(Box::new(
172///                 Plain::new("is")
173///             ))
174///         ])
175///     )),
176///     MarkdownElement::Plain(Box::new(
177///         Plain::new(" markdown.")
178///     )),
179/// ]);
180///
181/// assert_eq!(
182///     parse(message),
183///     ast
184/// );
185/// ```
186///
187/// # Limitations
188///
189/// The parser tries to mimic the behavior of the official Discord client's markdown parser, but it's not perfect.
190/// The following is the list of known limitations.
191///
192/// - Block quotes are not parsed. `> ` will be treated as plain text.
193/// - Nested emphasis, like `*italics **bold italics** italics*`, may not be parsed properly.
194/// - Intraword emphasis may not be handled properly. The parser treats `foo_bar_baz` as emphasis, while Discord's parser does not.
195/// - Escaping sequence will be treated as plain text.
196pub fn parse(msg: &str) -> MarkdownDocument {
197    // Since there are no invalid markdown document, parsing should never fails.
198    let (rest, doc) = parser::markdown_document(msg).unwrap();
199
200    // All input should be consumed.
201    assert!(rest.is_empty());
202
203    doc
204}
205
206#[cfg(test)]
207mod tests {
208    use super::ast::*;
209    use super::*;
210
211    #[test]
212    fn test_parse_1() {
213        let message = "*italics*, ||spoilers||, `*inline code*`";
214        assert_eq!(
215            parse(message),
216            MarkdownDocument::new(vec![
217                MarkdownElement::ItalicsStar(Box::new(ItalicsStar::new(vec![
218                    MarkdownElement::Plain(Box::new(Plain::new("italics")))
219                ]))),
220                MarkdownElement::Plain(Box::new(Plain::new(", "))),
221                MarkdownElement::Spoiler(Box::new(Spoiler::new(vec![MarkdownElement::Plain(
222                    Box::new(Plain::new("spoilers"))
223                )]))),
224                MarkdownElement::Plain(Box::new(Plain::new(", "))),
225                MarkdownElement::OneLineCode(Box::new(OneLineCode::new("*inline code*"))),
226            ])
227        );
228    }
229
230    #[test]
231    fn test_parse_2() {
232        let message = "__*nested* styles__ supported";
233        assert_eq!(
234            parse(message),
235            MarkdownDocument::new(vec![
236                MarkdownElement::Underline(Box::new(Underline::new(vec![
237                    MarkdownElement::ItalicsStar(Box::new(ItalicsStar::new(vec![
238                        MarkdownElement::Plain(Box::new(Plain::new("nested")))
239                    ]))),
240                    MarkdownElement::Plain(Box::new(Plain::new(" styles")))
241                ]))),
242                MarkdownElement::Plain(Box::new(Plain::new(" supported"))),
243            ])
244        );
245    }
246
247    #[test]
248    fn test_parse_3() {
249        let message = r#"
250```js
251const cond = a > b || c < d || e === f;
252```
253        "#
254        .trim();
255        assert_eq!(
256            parse(message),
257            MarkdownDocument::new(vec![MarkdownElement::MultiLineCode(Box::new(
258                MultiLineCode::new(
259                    "\nconst cond = a > b || c < d || e === f;\n",
260                    Some("js".to_string())
261                )
262            ))])
263        );
264    }
265}