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}