underlined text
\n"); + /// ``` + pub underline: bool, + + /// Enables spoilers using double vertical bars + /// + /// ```md + /// Darth Vader is ||Luke's father|| + /// ``` + /// + /// ``` + /// # use comrak::{markdown_to_html, Options}; + /// let mut options = Options::default(); + /// options.extension.spoiler = true; + /// + /// assert_eq!(markdown_to_html("Darth Vader is ||Luke's father||", &options), + /// "Darth Vader is Luke's father
\n"); + /// ``` + pub spoiler: bool, + + /// Requires at least one space after a `>` character to generate a blockquote, + /// and restarts blockquote nesting across unique lines of input + /// + /// ```md + /// >implying implications + /// + /// > one + /// > > two + /// > three + /// ``` + /// + /// ``` + /// # use comrak::{markdown_to_html, Options}; + /// let mut options = Options::default(); + /// options.extension.greentext = true; + /// + /// assert_eq!(markdown_to_html(">implying implications", &options), + /// ">implying implications
\n"); + /// + /// assert_eq!(markdown_to_html("> one\n> > two\n> three", &options), + /// concat!( + /// "\n", + /// "\n")); + /// ``` + pub greentext: bool, } #[non_exhaustive] @@ -674,6 +735,37 @@ pub struct RenderOptions { /// "one
\n", + /// "\n\n", + /// "two
\nthree
\n", + /// "
Notify user @example
\n"); /// ``` pub escaped_char_spans: bool, + + /// Ignore setext headings in input. + /// + /// ```rust + /// # use comrak::{markdown_to_html, Options}; + /// let mut options = Options::default(); + /// let input = "setext heading\n---"; + /// + /// assert_eq!(markdown_to_html(input, &options), + /// "setext heading
\n[]()
\n"); + /// ``` + pub ignore_empty_links: bool, } #[non_exhaustive] @@ -1069,7 +1161,8 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { } } NodeValue::Table(..) => { - if !table::matches(&line[self.first_nonspace..]) { + if !table::matches(&line[self.first_nonspace..], self.options.extension.spoiler) + { return (false, container, should_continue); } continue; @@ -1099,6 +1192,17 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { (true, container, should_continue) } + fn is_not_greentext(&mut self, line: &[u8]) -> bool { + !self.options.extension.greentext || strings::is_space_or_tab(line[self.first_nonspace + 1]) + } + + fn setext_heading_line(&mut self, s: &[u8]) -> Optiontext text
\n[](https://example.com/evil-link-for-seo-spam)
\n", + ); + + html_opts!( + [render.ignore_empty_links], + "[ ](https://example.com/evil-link-for-seo-spam)", + "[ ](https://example.com/evil-link-for-seo-spam)
\n", + ); +} + +#[test] +fn empty_image_allowed() { + html_opts!( + [render.ignore_empty_links], + "![ ](https://example.com/evil-link-for-seo-spam)", + "\n", + ); +} + +#[test] +fn image_inside_link_allowed() { + html_opts!( + [render.ignore_empty_links], + "[![](https://example.com/image.png)](https://example.com/)", + "\n", + ); +} diff --git a/src/tests/greentext.rs b/src/tests/greentext.rs new file mode 100644 index 00000000..209d140f --- /dev/null +++ b/src/tests/greentext.rs @@ -0,0 +1,37 @@ +use super::*; + +#[test] +fn greentext_preserved() { + html_opts!( + [extension.greentext, render.hardbreaks], + ">implying\n>>implying", + ">implying
\n>>implying
\n\n1
\n
>
\n\n\n" + ); +} + +#[test] +fn unnest_quotes_on_line_end() { + html_opts!( + [extension.greentext], + "> 1\n> > 2\n> 1", + "2
\n
\n\n" + ); +} + +#[test] +fn unnest_quotes_on_line_end_commonmark() { + html_opts!( + [extension.greentext], + "> 1\n> > 2\n> \n> 1", + "1
\n\n\n2
\n1
\n
\n\n" + ); +} diff --git a/src/tests/spoiler.rs b/src/tests/spoiler.rs new file mode 100644 index 00000000..542debd0 --- /dev/null +++ b/src/tests/spoiler.rs @@ -0,0 +1,62 @@ +use super::*; + +#[test] +fn spoiler() { + html_opts!( + [extension.spoiler], + concat!("The ||dog dies at the end of Marley and Me||.\n"), + concat!( + "1
\n\n\n2
\n1
\n
The dog dies at the end of Marley and Me.
\n" + ), + ); +} + +#[test] +fn spoiler_in_table() { + html_opts!( + [extension.table, extension.spoiler], + concat!("Text | Result\n--- | ---\n`||some clever text||` | ||some clever text||\n"), + concat!( + "Text | \n", + "Result | \n", + "
---|---|
||some clever text|| | \n",
+ "some clever text | \n", + "
|should not be spoiler|\n", + "should be spoiler\n", + "|should be spoiler surrounded by pipes|
\n" + ), + ); +} + +#[test] +fn mismatched_spoilers() { + html_opts!( + [extension.spoiler], + concat!("|||this is a spoiler with pipe in front||\n||this is not a spoiler|\n||this is a spoiler with pipe after|||"), + concat!( + "|this is a spoiler with pipe in front\n", + "||this is not a spoiler|\n", + "this is a spoiler with pipe after|
\n" + ), + ); +} diff --git a/src/tests/underline.rs b/src/tests/underline.rs new file mode 100644 index 00000000..8efa9a9f --- /dev/null +++ b/src/tests/underline.rs @@ -0,0 +1,10 @@ +use super::*; + +#[test] +fn underline() { + html_opts!( + [extension.underline], + concat!("__underlined text__\n"), + concat!("underlined text
\n"), + ); +} diff --git a/src/xml.rs b/src/xml.rs index 3d2e3ebd..260382b1 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -278,6 +278,11 @@ impl<'o> XmlFormatter<'o> { self.escape(nl.url.as_bytes())?; self.output.write_all(b"\"")?; } + NodeValue::Underline => {} + NodeValue::SpoileredText => {} + NodeValue::EscapedTag(ref data) => { + self.output.write_all(data.as_bytes())?; + } } if node.first_child().is_some() {