Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/cm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,9 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
NodeValue::HtmlInline(ref literal) => self.format_html_inline(literal, entering),
NodeValue::Strong => self.format_strong(),
NodeValue::Emph => self.format_emph(node),
NodeValue::TaskItem(checked) => self.format_task_item(checked, entering),
NodeValue::TaskItem { checked, symbol } => {
self.format_task_item(checked, symbol, entering)
}
NodeValue::Strikethrough => self.format_strikethrough(),
NodeValue::Superscript => self.format_superscript(),
NodeValue::Link(ref nl) => return self.format_link(node, nl, entering),
Expand Down Expand Up @@ -596,13 +598,9 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
self.write_all(&[emph_delim]).unwrap();
}

fn format_task_item(&mut self, checked: bool, entering: bool) {
fn format_task_item(&mut self, _checked: bool, symbol: u8, entering: bool) {
if entering {
if checked {
write!(self, "[x] ").unwrap();
} else {
write!(self, "[ ] ").unwrap();
}
write!(self, "[{}] ", symbol as char).unwrap();
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ impl<'o> HtmlFormatter<'o> {
)?;
}
}
NodeValue::TaskItem(checked) => {
NodeValue::TaskItem { checked, .. } => {
if entering {
if checked {
self.output.write_all(
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ struct Cli {
#[arg(long)]
gfm: bool,

/// Enable relaxing which character is allowed in a tasklists.
#[arg(long)]
relaxed_tasklist_character: 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 @@ -203,6 +207,7 @@ fn main() -> Result<(), Box<dyn Error>> {
parse: ComrakParseOptions {
smart: cli.smart,
default_info_string: cli.default_info_string,
relaxed_tasklist_matching: cli.relaxed_tasklist_character,
},
render: ComrakRenderOptions {
hardbreaks: cli.hardbreaks,
Expand Down
10 changes: 7 additions & 3 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,13 @@ pub enum NodeValue {
/// in a document will be contained in a `Text` node.
Text(Vec<u8>),

/// **Inline**. [Task list item](https://github.github.com/gfm/#task-list-items-extension-). The
/// `bool` indicates whether it is checked or not.
TaskItem(bool),
/// **Inline**. [Task list item](https://github.github.com/gfm/#task-list-items-extension-).
TaskItem {
/// The `bool` `checked` indicates whether it is checked or not.
checked: bool,
/// The `symbol` that was used in the brackets to mark a task as `checked`.
symbol: u8,
},

/// **Inline**. A [soft line break](https://github.github.com/gfm/#soft-line-breaks). If
/// the `hardbreaks` option is set in `ComrakOptions` during formatting, it will be formatted
Expand Down
28 changes: 19 additions & 9 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use nodes::{
Ast, AstNode, ListDelimType, ListType, NodeCodeBlock, NodeDescriptionItem, NodeHeading,
NodeHtmlBlock, NodeList, NodeValue,
};
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use regex::bytes::{Regex, RegexBuilder};
use scanners;
use std::cell::RefCell;
Expand Down Expand Up @@ -341,6 +341,9 @@ pub struct ComrakParseOptions {
/// "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n");
/// ```
pub default_info_string: Option<String>,

/// Whether or not a simple `x` or `X` is used for tasklist or any other symbol is allowed.
pub relaxed_tasklist_matching: bool,
}

#[derive(Default, Debug, Clone, Copy)]
Expand Down Expand Up @@ -1649,17 +1652,18 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
}

fn process_tasklist(&mut self, node: &'a AstNode<'a>, text: &mut Vec<u8>) {
static TASKLIST: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\A(\s*\[([xX ])\])(?:\z|\s)").unwrap());
static TASKLIST: OnceCell<Regex> = OnceCell::new();
let r = TASKLIST.get_or_init(|| Regex::new(r"\A(\s*\[(.)\])(?:\z|\s)").unwrap());

let (active, end) = match TASKLIST.captures(text) {
let (symbol, end) = match r.captures(text) {
None => return,
Some(c) => (
c.get(2).unwrap().as_bytes() != b" ",
c.get(0).unwrap().end(),
),
Some(c) => (c.get(2).unwrap().as_bytes()[0], c.get(0).unwrap().end()),
};

if !self.options.parse.relaxed_tasklist_matching && !matches!(symbol, b' ' | b'x' | b'X') {
return;
}

let parent = node.parent().unwrap();
if node.previous_sibling().is_some() || parent.previous_sibling().is_some() {
return;
Expand All @@ -1676,7 +1680,13 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
}

*text = text[end..].to_vec();
let checkbox = inlines::make_inline(self.arena, NodeValue::TaskItem(active));
let checkbox = inlines::make_inline(
self.arena,
NodeValue::TaskItem {
checked: symbol != b' ',
symbol: symbol,
},
);
node.insert_before(checkbox);
}

Expand Down
42 changes: 40 additions & 2 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn fuzz_doesnt_crash(md: String) {
parse: ::ComrakParseOptions {
smart: true,
default_info_string: Some("Rust".to_string()),
relaxed_tasklist_matching: true,
},
render: ::ComrakRenderOptions {
hardbreaks: true,
Expand Down Expand Up @@ -758,11 +759,16 @@ fn tagfilter() {
#[test]
fn tasklist() {
html_opts!(
[render.unsafe_, extension.tasklist],
[
render.unsafe_,
extension.tasklist,
parse.relaxed_tasklist_matching
],
concat!(
"* [ ] Red\n",
"* [x] Green\n",
"* [ ] Blue\n",
"* [!] Papayawhip\n",
"<!-- end list -->\n",
"1. [ ] Bird\n",
"2. [ ] McHale\n",
Expand All @@ -777,6 +783,7 @@ fn tasklist() {
"<li><input type=\"checkbox\" disabled=\"\" /> Red</li>\n",
"<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Green</li>\n",
"<li><input type=\"checkbox\" disabled=\"\" /> Blue</li>\n",
"<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Papayawhip</li>\n",
"</ul>\n",
"<!-- end list -->\n",
"<ol>\n",
Expand All @@ -800,6 +807,35 @@ fn tasklist() {
);
}

#[test]
fn tasklist_relaxed_regression() {
html_opts!(
[extension.tasklist, parse.relaxed_tasklist_matching],
"* [!] Red\n",
concat!(
"<ul>\n",
"<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Red</li>\n",
"</ul>\n"
),
);

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

html_opts!(
[extension.tasklist, parse.relaxed_tasklist_matching],
"* [!] Red\n",
concat!(
"<ul>\n",
"<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Red</li>\n",
"</ul>\n"
),
);
}

#[test]
fn tasklist_32() {
html_opts!(
Expand Down Expand Up @@ -1293,6 +1329,7 @@ fn exercise_full_api<'a>() {
parse: ::ComrakParseOptions {
smart: false,
default_info_string: Some("abc".to_string()),
relaxed_tasklist_matching: true,
},
render: ::ComrakRenderOptions {
hardbreaks: false,
Expand Down Expand Up @@ -1389,8 +1426,9 @@ fn exercise_full_api<'a>() {
::nodes::NodeValue::Text(text) => {
let _: &Vec<u8> = text;
}
::nodes::NodeValue::TaskItem(checked) => {
::nodes::NodeValue::TaskItem { checked, symbol } => {
let _: &bool = checked;
let _: &u8 = symbol;
}
::nodes::NodeValue::SoftBreak => {}
::nodes::NodeValue::LineBreak => {}
Expand Down