Skip to content

Commit

Permalink
Merge pull request #468 from nicoburns/tasklist-class
Browse files Browse the repository at this point in the history
Add `task-list-item` class to task list items
  • Loading branch information
kivikakk authored Oct 15, 2024
2 parents c364193 + 975d3bf commit 96a64d1
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 32 deletions.
1 change: 1 addition & 0 deletions fuzz/fuzz_targets/all_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 33 additions & 21 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"<ul")?;
self.render_sourcepos(node)?;
self.output.write_all(b">\n")?;
} else if nl.start == 1 {
self.output.write_all(b"<ol")?;
self.render_sourcepos(node)?;
self.output.write_all(b">\n")?;
} else {
self.output.write_all(b"<ol")?;
self.render_sourcepos(node)?;
writeln!(self.output, " start=\"{}\">", nl.start)?;
match nl.list_type {
ListType::Bullet => {
self.output.write_all(b"<ul")?;
if nl.is_task_list && self.options.render.tasklist_classes {
self.output.write_all(b" class=\"contains-task-list\"")?;
}
self.render_sourcepos(node)?;
self.output.write_all(b">\n")?;
}
ListType::Ordered => {
self.output.write_all(b"<ol")?;
if nl.is_task_list && self.options.render.tasklist_classes {
self.output.write_all(b" class=\"contains-task-list\"")?;
}
self.render_sourcepos(node)?;
if nl.start == 1 {
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"</ul>\n")?;
Expand Down Expand Up @@ -1043,17 +1052,20 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> {
if entering {
self.cr()?;
self.output.write_all(b"<li")?;
if self.options.render.tasklist_classes {
self.output.write_all(b" class=\"task-list-item\"")?;
}
self.render_sourcepos(node)?;
self.output.write_all(b">")?;
write!(
self.output,
"<input type=\"checkbox\" {}disabled=\"\" /> ",
if symbol.is_some() {
"checked=\"\" "
} else {
""
}
)?;
self.output.write_all(b"<input type=\"checkbox\"")?;
if self.options.render.tasklist_classes {
self.output
.write_all(b" class=\"task-list-item-checkbox\"")?;
}
if symbol.is_some() {
self.output.write_all(b" checked=\"\"")?;
}
self.output.write_all(b" disabled=\"\" /> ")?;
} else {
self.output.write_all(b"</li>\n")?;
}
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down Expand Up @@ -296,6 +300,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.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 {
Expand Down
3 changes: 3 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>` 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
Expand Down
33 changes: 31 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,23 @@ pub struct RenderOptions {
/// "<p><figure><img src=\"https://example.com/image.png\" alt=\"image\" title=\"this is an image\" /><figcaption>this is an image</figcaption></figure></p>\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),
/// "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> Foo</li>\n</ul>\n");
///
/// options.render.tasklist_classes = true;
/// assert_eq!(markdown_to_html(input, &options),
/// "<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Foo</li>\n</ul>\n");
/// ```
pub tasklist_classes: bool,
}

#[non_exhaustive]
Expand Down Expand Up @@ -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;
}

Expand All @@ -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<usize> {
Expand Down Expand Up @@ -2565,6 +2592,7 @@ fn parse_list_marker(
delimiter: ListDelimType::Period,
bullet_char: c,
tight: false,
is_task_list: false,
},
));
} else if isdigit(c) {
Expand Down Expand Up @@ -2620,6 +2648,7 @@ fn parse_list_marker(
},
bullet_char: 0,
tight: false,
is_task_list: false,
},
));
}
Expand Down
9 changes: 9 additions & 0 deletions src/tests/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ fn tasklist() {
);
}

#[test]
fn tasklist_with_classes() {
html_opts!(
[extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching],
"* [*]",
"<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> </li>\n</ul>\n",
);
}

#[test]
fn table_nul() {
html_opts!(
Expand Down
100 changes: 100 additions & 0 deletions src/tests/tasklist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"<!-- end list -->\n",
"1. [ ] Bird\n",
"2. [ ] McHale\n",
"3. [x] Parish\n",
"<!-- end list -->\n",
"* [ ] Red\n",
" * [x] Green\n",
" * [ ] Blue\n"
),
concat!(
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Red</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Green</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Blue</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Papayawhip</li>\n",
"</ul>\n",
"<!-- end list -->\n",
"<ol class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Bird</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> McHale</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Parish</li>\n",
"</ol>\n",
"<!-- end list -->\n",
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Red\n",
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Green\n",
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Blue</li>\n",
"</ul>\n",
"</li>\n",
"</ul>\n",
"</li>\n",
"</ul>\n"
),
);
}

#[test]
fn tasklist_relaxed_regression() {
html_opts!(
Expand Down Expand Up @@ -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!(
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Red</li>\n",
"</ul>\n"
),
);

html_opts!(
[extension.tasklist, render.tasklist_classes],
"* [!] Red\n",
concat!("<ul>\n", "<li>[!] Red</li>\n", "</ul>\n"),
);

html_opts!(
[extension.tasklist, render.tasklist_classes, parse.relaxed_tasklist_matching],
"* [!] Red\n",
concat!(
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> Red</li>\n",
"</ul>\n"
),
);
}

#[test]
fn tasklist_32() {
html_opts!(
Expand All @@ -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!(
"<ul class=\"contains-task-list\">\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> List item 1</li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> This list item is <strong>bold</strong></li>\n",
"<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked=\"\" disabled=\"\" /> There is some <code>code</code> here</li>\n",
"</ul>\n"
),
);
}

#[test]
fn sourcepos() {
assert_ast_match!(
Expand Down
24 changes: 15 additions & 9 deletions src/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
}
Expand Down

0 comments on commit 96a64d1

Please sign in to comment.