From 8c4312fa631d7f31db06f2acb82fa9aba19a0c13 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 12 Sep 2024 13:57:24 +0100 Subject: [PATCH 1/3] Add classes to tasklists --- src/html.rs | 38 ++++++++++++++++++++++------------- src/nodes.rs | 3 +++ src/parser/mod.rs | 20 +++++++++++++++---- src/tests/fuzz.rs | 2 +- src/tests/tasklist.rs | 46 +++++++++++++++++++++---------------------- src/xml.rs | 24 +++++++++++++--------- 6 files changed, 82 insertions(+), 51 deletions(-) diff --git a/src/html.rs b/src/html.rs index e8ac2303..10a75d72 100644 --- a/src/html.rs +++ b/src/html.rs @@ -478,18 +478,28 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> { NodeValue::List(ref nl) => { if entering { self.cr()?; - if nl.list_type == ListType::Bullet { - self.output.write_all(b"\n")?; - } else if nl.start == 1 { - self.output.write_all(b"\n")?; - } else { - self.output.write_all(b"", nl.start)?; + + match nl.list_type { + ListType::Bullet => { + self.output.write_all(b"\n")?; + } + ListType::Ordered => { + self.output.write_all(b"\n")?; + } else { + writeln!(self.output, " start=\"{}\">", nl.start)?; + } + } } } else if nl.list_type == ListType::Bullet { self.output.write_all(b"\n")?; @@ -1042,12 +1052,12 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> { NodeValue::TaskItem(symbol) => { if entering { self.cr()?; - self.output.write_all(b"")?; write!( self.output, - " ", + " ", if symbol.is_some() { "checked=\"\" " } else { diff --git a/src/nodes.rs b/src/nodes.rs index 95b58a59..2051badd 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -297,6 +297,9 @@ pub struct NodeList { /// Whether the list is [tight](https://github.github.com/gfm/#tight), i.e. whether the /// paragraphs are wrapped in `

` tags when formatted as HTML. pub tight: bool, + + /// Whether the list contains tasks (checkbox items) + pub is_task_list: bool, } /// The metadata of a description list diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0b8f3171..79f40d41 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -223,8 +223,8 @@ pub struct ExtensionOptions { /// options.extension.tasklist = true; /// options.render.unsafe_ = true; /// assert_eq!(markdown_to_html("* [x] Done\n* [ ] Not done\n", &options), - /// "

    \n
  • Done
  • \n\ - ///
  • Not done
  • \n
\n"); + /// "
    \n
  • Done
  • \n\ + ///
  • Not done
  • \n
\n"); /// ``` pub tasklist: bool, @@ -2436,7 +2436,13 @@ impl<'a, 'o, 'c: 'o> Parser<'a, 'o, 'c> { return; } - if !node_matches!(parent.parent().unwrap(), NodeValue::Item(..)) { + let grandparent = parent.parent().unwrap(); + if !node_matches!(grandparent, NodeValue::Item(..)) { + return; + } + + let great_grandparent = grandparent.parent().unwrap(); + if !node_matches!(great_grandparent, NodeValue::List(..)) { return; } @@ -2448,8 +2454,12 @@ impl<'a, 'o, 'c: 'o> Parser<'a, 'o, 'c> { sourcepos.start.column += end; parent.data.borrow_mut().sourcepos.start.column += end; - parent.parent().unwrap().data.borrow_mut().value = + grandparent.data.borrow_mut().value = NodeValue::TaskItem(if symbol == ' ' { None } else { Some(symbol) }); + + if let NodeValue::List(ref mut list) = &mut great_grandparent.data.borrow_mut().value { + list.is_task_list = true; + } } fn parse_reference_inline(&mut self, content: &[u8]) -> Option { @@ -2565,6 +2575,7 @@ fn parse_list_marker( delimiter: ListDelimType::Period, bullet_char: c, tight: false, + is_task_list: false, }, )); } else if isdigit(c) { @@ -2620,6 +2631,7 @@ fn parse_list_marker( }, bullet_char: 0, tight: false, + is_task_list: false, }, )); } diff --git a/src/tests/fuzz.rs b/src/tests/fuzz.rs index 21223729..66dd1d3d 100644 --- a/src/tests/fuzz.rs +++ b/src/tests/fuzz.rs @@ -10,7 +10,7 @@ fn tasklist() { html_opts!( [extension.tasklist, parse.relaxed_tasklist_matching], "* [*]", - "
    \n
  • \n
\n", + "
    \n
  • \n
\n", ); } diff --git a/src/tests/tasklist.rs b/src/tests/tasklist.rs index dc148519..6246fa84 100644 --- a/src/tests/tasklist.rs +++ b/src/tests/tasklist.rs @@ -23,25 +23,25 @@ fn tasklist() { " * [ ] Blue\n" ), concat!( - "
    \n", - "
  • Red
  • \n", - "
  • Green
  • \n", - "
  • Blue
  • \n", - "
  • Papayawhip
  • \n", + "
      \n", + "
    • Red
    • \n", + "
    • Green
    • \n", + "
    • Blue
    • \n", + "
    • Papayawhip
    • \n", "
    \n", "\n", - "
      \n", - "
    1. Bird
    2. \n", - "
    3. McHale
    4. \n", - "
    5. Parish
    6. \n", + "
        \n", + "
      1. Bird
      2. \n", + "
      3. McHale
      4. \n", + "
      5. Parish
      6. \n", "
      \n", "\n", - "
        \n", - "
      • Red\n", - "
          \n", - "
        • Green\n", - "
            \n", - "
          • Blue
          • \n", + "
              \n", + "
            • Red\n", + "
                \n", + "
              • Green\n", + "
                  \n", + "
                • Blue
                • \n", "
                \n", "
              • \n", "
              \n", @@ -57,8 +57,8 @@ fn tasklist_relaxed_regression() { [extension.tasklist, parse.relaxed_tasklist_matching], "* [!] Red\n", concat!( - "
                \n", - "
              • Red
              • \n", + "
                  \n", + "
                • Red
                • \n", "
                \n" ), ); @@ -73,8 +73,8 @@ fn tasklist_relaxed_regression() { [extension.tasklist, parse.relaxed_tasklist_matching], "* [!] Red\n", concat!( - "
                  \n", - "
                • Red
                • \n", + "
                    \n", + "
                  • Red
                  • \n", "
                  \n" ), ); @@ -90,10 +90,10 @@ fn tasklist_32() { "- [x] There is some `code` here\n" ), concat!( - "
                    \n", - "
                  • List item 1
                  • \n", - "
                  • This list item is bold
                  • \n", - "
                  • There is some code here
                  • \n", + "
                      \n", + "
                    • List item 1
                    • \n", + "
                    • This list item is bold
                    • \n", + "
                    • There is some code here
                    • \n", "
                    \n" ), ); diff --git a/src/xml.rs b/src/xml.rs index cfc4603d..1afa5cf8 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -161,15 +161,21 @@ impl<'o, 'c> XmlFormatter<'o, 'c> { was_literal = true; } NodeValue::List(ref nl) => { - if nl.list_type == ListType::Bullet { - self.output.write_all(b" type=\"bullet\"")?; - } else { - write!( - self.output, - " type=\"ordered\" start=\"{}\" delim=\"{}\"", - nl.start, - nl.delimiter.xml_name() - )?; + match nl.list_type { + ListType::Bullet => { + self.output.write_all(b" type=\"bullet\"")?; + } + ListType::Ordered => { + write!( + self.output, + " type=\"ordered\" start=\"{}\" delim=\"{}\"", + nl.start, + nl.delimiter.xml_name() + )?; + } + } + if nl.is_task_list { + self.output.write_all(b" tasklist=\"true\"")?; } write!(self.output, " tight=\"{}\"", nl.tight)?; } From f0d3a3b05aab69217587781c9601da1b83a3f2c3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 12 Oct 2024 21:29:14 +0100 Subject: [PATCH 2/3] Hide tasklist classes behind an option --- src/html.rs | 28 ++++++------ src/parser/mod.rs | 21 ++++++++- src/tests/fuzz.rs | 9 ++++ src/tests/tasklist.rs | 104 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 145 insertions(+), 17 deletions(-) diff --git a/src/html.rs b/src/html.rs index 10a75d72..421458f5 100644 --- a/src/html.rs +++ b/src/html.rs @@ -478,11 +478,10 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> { NodeValue::List(ref nl) => { if entering { self.cr()?; - match nl.list_type { ListType::Bullet => { self.output.write_all(b" HtmlFormatter<'o, 'c> { } ListType::Ordered => { self.output.write_all(b" HtmlFormatter<'o, 'c> { NodeValue::TaskItem(symbol) => { if entering { self.cr()?; - self.output.write_all(b"
                  • ")?; - write!( - self.output, - " ", - if symbol.is_some() { - "checked=\"\" " - } else { - "" - } - )?; + self.output.write_all(b" ")?; } else { self.output.write_all(b"
                  • \n")?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 79f40d41..fb9d4652 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -223,8 +223,8 @@ pub struct ExtensionOptions { /// options.extension.tasklist = true; /// options.render.unsafe_ = true; /// assert_eq!(markdown_to_html("* [x] Done\n* [ ] Not done\n", &options), - /// "
                      \n
                    • Done
                    • \n\ - ///
                    • Not done
                    • \n
                    \n"); + /// "
                      \n
                    • Done
                    • \n\ + ///
                    • Not done
                    • \n
                    \n"); /// ``` pub tasklist: bool, @@ -892,6 +892,23 @@ pub struct RenderOptions { /// "

                    \"image\"
                    this is an image

                    \n"); /// ``` pub figure_with_caption: bool, + + /// Add classes to the output of the tasklist extension. This allows tasklists to be styled. + /// + /// ```rust + /// # use comrak::{markdown_to_html, Options}; + /// let mut options = Options::default(); + /// options.extension.tasklist = true; + /// let input = "- [ ] Foo"; + /// + /// assert_eq!(markdown_to_html(input, &options), + /// "
                      \n
                    • Foo
                    • \n
                    \n"); + /// + /// options.render.tasklist_classes = true; + /// assert_eq!(markdown_to_html(input, &options), + /// "
                      \n
                    • Foo
                    • \n
                    \n"); + /// ``` + pub tasklist_classes: bool, } #[non_exhaustive] diff --git a/src/tests/fuzz.rs b/src/tests/fuzz.rs index 66dd1d3d..a9ba5b30 100644 --- a/src/tests/fuzz.rs +++ b/src/tests/fuzz.rs @@ -10,6 +10,15 @@ fn tasklist() { html_opts!( [extension.tasklist, parse.relaxed_tasklist_matching], "* [*]", + "
                      \n
                    • \n
                    \n", + ); +} + +#[test] +fn tasklist_with_classes() { + html_opts!( + [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching], + "* [*]", "
                      \n
                    • \n
                    \n", ); } diff --git a/src/tests/tasklist.rs b/src/tests/tasklist.rs index 6246fa84..1404722f 100644 --- a/src/tests/tasklist.rs +++ b/src/tests/tasklist.rs @@ -22,6 +22,58 @@ fn tasklist() { " * [x] Green\n", " * [ ] Blue\n" ), + concat!( + "
                      \n", + "
                    • Red
                    • \n", + "
                    • Green
                    • \n", + "
                    • Blue
                    • \n", + "
                    • Papayawhip
                    • \n", + "
                    \n", + "\n", + "
                      \n", + "
                    1. Bird
                    2. \n", + "
                    3. McHale
                    4. \n", + "
                    5. Parish
                    6. \n", + "
                    \n", + "\n", + "
                      \n", + "
                    • Red\n", + "
                        \n", + "
                      • Green\n", + "
                          \n", + "
                        • Blue
                        • \n", + "
                        \n", + "
                      • \n", + "
                      \n", + "
                    • \n", + "
                    \n" + ), + ); +} + +#[test] +fn tasklist_with_classes() { + html_opts!( + [ + render.unsafe_, + extension.tasklist, + render.tasklist_classes, + parse.relaxed_tasklist_matching + ], + concat!( + "* [ ] Red\n", + "* [x] Green\n", + "* [ ] Blue\n", + "* [!] Papayawhip\n", + "\n", + "1. [ ] Bird\n", + "2. [ ] McHale\n", + "3. [x] Parish\n", + "\n", + "* [ ] Red\n", + " * [x] Green\n", + " * [ ] Blue\n" + ), concat!( "
                      \n", "
                    • Red
                    • \n", @@ -57,8 +109,8 @@ fn tasklist_relaxed_regression() { [extension.tasklist, parse.relaxed_tasklist_matching], "* [!] Red\n", concat!( - "
                        \n", - "
                      • Red
                      • \n", + "
                          \n", + "
                        • Red
                        • \n", "
                        \n" ), ); @@ -72,6 +124,35 @@ fn tasklist_relaxed_regression() { html_opts!( [extension.tasklist, parse.relaxed_tasklist_matching], "* [!] Red\n", + concat!( + "
                          \n", + "
                        • Red
                        • \n", + "
                        \n" + ), + ); +} + +#[test] +fn tasklist_with_classes_relaxed_regression() { + html_opts!( + [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching], + "* [!] Red\n", + concat!( + "
                          \n", + "
                        • Red
                        • \n", + "
                        \n" + ), + ); + + html_opts!( + [extension.tasklist, render.tasklist_classes], + "* [!] Red\n", + concat!("
                          \n", "
                        • [!] Red
                        • \n", "
                        \n"), + ); + + html_opts!( + [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching], + "* [!] Red\n", concat!( "
                          \n", "
                        • Red
                        • \n", @@ -89,6 +170,25 @@ fn tasklist_32() { "- [ ] This list item is **bold**\n", "- [x] There is some `code` here\n" ), + concat!( + "
                            \n", + "
                          • List item 1
                          • \n", + "
                          • This list item is bold
                          • \n", + "
                          • There is some code here
                          • \n", + "
                          \n" + ), + ); +} + +#[test] +fn tasklist_32_with_classes() { + html_opts!( + [render.unsafe_, extension.tasklist, render.tasklist_classes], + concat!( + "- [ ] List item 1\n", + "- [ ] This list item is **bold**\n", + "- [x] There is some `code` here\n" + ), concat!( "
                            \n", "
                          • List item 1
                          • \n", From 975d3bf2a33c9f820de43a38745b374268898ea4 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sun, 13 Oct 2024 20:04:23 +0100 Subject: [PATCH 3/3] Add tasklist_classes option to CLI --- fuzz/fuzz_targets/all_options.rs | 1 + src/main.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/fuzz/fuzz_targets/all_options.rs b/fuzz/fuzz_targets/all_options.rs index 131c932a..2af39c81 100644 --- a/fuzz/fuzz_targets/all_options.rs +++ b/fuzz/fuzz_targets/all_options.rs @@ -57,6 +57,7 @@ fuzz_target!(|s: &str| { render.ignore_empty_links = true; render.gfm_quirks = true; render.prefer_fenced = true; + render.tasklist_classes = true; markdown_to_html( s, diff --git a/src/main.rs b/src/main.rs index 41e1a3af..faa98190 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,10 @@ struct Cli { #[arg(long)] relaxed_autolinks: bool, + /// Output classes on tasklist elements so that they can be styled with CSS + #[arg(long)] + tasklist_classes: bool, + /// Default value for fenced code block's info strings if none is given #[arg(long, value_name = "INFO")] default_info_string: Option, @@ -296,6 +300,7 @@ fn main() -> Result<(), Box> { .ignore_setext(cli.ignore_setext) .ignore_empty_links(cli.ignore_empty_links) .gfm_quirks(cli.gfm_quirks || cli.gfm) + .tasklist_classes(cli.tasklist_classes) .build()?; let options = Options {