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/html.rs b/src/html.rs
index e8ac2303..421458f5 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -478,18 +478,27 @@ 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")?;
@@ -1043,17 +1052,20 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> {
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/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 {
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..fb9d4652 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -892,6 +892,23 @@ pub struct RenderOptions {
/// "
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");
+ ///
+ /// options.render.tasklist_classes = true;
+ /// assert_eq!(markdown_to_html(input, &options),
+ /// "\n");
+ /// ```
+ pub tasklist_classes: bool,
}
#[non_exhaustive]
@@ -2436,7 +2453,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 +2471,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 +2592,7 @@ fn parse_list_marker(
delimiter: ListDelimType::Period,
bullet_char: c,
tight: false,
+ is_task_list: false,
},
));
} else if isdigit(c) {
@@ -2620,6 +2648,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..a9ba5b30 100644
--- a/src/tests/fuzz.rs
+++ b/src/tests/fuzz.rs
@@ -14,6 +14,15 @@ fn tasklist() {
);
}
+#[test]
+fn tasklist_with_classes() {
+ html_opts!(
+ [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching],
+ "* [*]",
+ "\n",
+ );
+}
+
#[test]
fn table_nul() {
html_opts!(
diff --git a/src/tests/tasklist.rs b/src/tests/tasklist.rs
index dc148519..1404722f 100644
--- a/src/tests/tasklist.rs
+++ b/src/tests/tasklist.rs
@@ -51,6 +51,58 @@ fn tasklist() {
);
}
+#[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",
+ "\n",
+ "\n",
+ " Bird \n",
+ " McHale \n",
+ " Parish \n",
+ " \n",
+ "\n",
+ "\n"
+ ),
+ );
+}
+
#[test]
fn tasklist_relaxed_regression() {
html_opts!(
@@ -80,6 +132,35 @@ fn tasklist_relaxed_regression() {
);
}
+#[test]
+fn tasklist_with_classes_relaxed_regression() {
+ html_opts!(
+ [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching],
+ "* [!] Red\n",
+ concat!(
+ "\n"
+ ),
+ );
+
+ html_opts!(
+ [extension.tasklist, render.tasklist_classes],
+ "* [!] Red\n",
+ concat!("\n"),
+ );
+
+ html_opts!(
+ [extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching],
+ "* [!] Red\n",
+ concat!(
+ "\n"
+ ),
+ );
+}
+
#[test]
fn tasklist_32() {
html_opts!(
@@ -99,6 +180,25 @@ fn tasklist_32() {
);
}
+#[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"
+ ),
+ );
+}
+
#[test]
fn sourcepos() {
assert_ast_match!(
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)?;
}