markdown_it_tasklist/
lib.rs1use markdown_it::{
20 parser::{
21 core::CoreRule,
22 inline::{builtin::InlineParserRule, Text},
23 },
24 plugins::cmark::block::{
25 list::{BulletList, ListItem, OrderedList},
26 paragraph::Paragraph,
27 },
28 MarkdownIt, Node, NodeValue, Renderer,
29};
30use once_cell::sync::Lazy;
31use regex::Regex;
32
33pub fn add(md: &mut MarkdownIt) {
35 md.add_rule::<TasklistRule<false>>()
36 .after::<InlineParserRule>();
37}
38
39pub fn add_disabled(md: &mut MarkdownIt) {
41 md.add_rule::<TasklistRule<true>>()
42 .after::<InlineParserRule>();
43}
44
45#[derive(Debug)]
46pub struct TodoCheckbox {
47 pub checked: bool,
48 pub disabled: bool,
49}
50
51impl NodeValue for TodoCheckbox {
52 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
53 let mut attrs = node.attrs.clone();
54 attrs.push(("class", "task-list-item-checkbox".into()));
55 attrs.push(("type", "checkbox".into()));
56 if self.disabled {
57 attrs.push(("disabled", "".into()));
58 }
59 if self.checked {
60 attrs.push(("checked", "".into()));
61 }
62 fmt.self_close("input", &attrs);
63 }
64}
65
66struct TasklistRule<const DISABLED: bool>;
67
68static CHECKBOX_CHECKED_RE: Lazy<Regex> =
69 Lazy::new(|| Regex::new(r"^\[[xX]\][\s\t\n\v\f\r]").unwrap());
70static CHECKBOX_UNCHECKED_RE: Lazy<Regex> =
71 Lazy::new(|| Regex::new(r"^\[\s\][\s\t\n\v\f\r]").unwrap());
72
73impl<const DISABLED: bool> CoreRule for TasklistRule<DISABLED> {
74 fn run(root: &mut Node, _: &MarkdownIt) {
75 fn walk_recursive(node: &mut Node, disabled: bool) {
76 if node.is::<Paragraph>() {
77 return;
80 }
81 if node.is::<BulletList>() || node.is::<OrderedList>() {
82 let mut contains_task = false;
83 for item in node.children.iter_mut() {
84 if !item.is::<ListItem>() {
85 continue;
86 }
87 if let Some(child) = item.children.first_mut() {
88 let mut text_value = None;
90 if child.cast_mut::<Paragraph>().is_some() {
91 if let Some(child) = child.children.first_mut() {
92 if let Some(value) = child.cast_mut::<Text>() {
93 text_value = Some(value);
94 }
95 }
96 } else if let Some(value) = child.cast_mut::<Text>() {
97 text_value = Some(value);
98 }
99 if let Some(text) = text_value {
100 if CHECKBOX_UNCHECKED_RE.is_match(&text.content) {
102 contains_task = true;
103 text.content.replace_range(0..3, "");
104 item.attrs.push(("class", "task-list-item".into()));
105 item.children.insert(
106 0,
107 Node::new(TodoCheckbox {
108 checked: false,
109 disabled,
110 }),
111 );
112 } else if CHECKBOX_CHECKED_RE.is_match(&text.content) {
113 contains_task = true;
114 text.content.replace_range(0..3, "");
115 item.attrs.push(("class", "task-list-item".into()));
116 item.children.insert(
117 0,
118 Node::new(TodoCheckbox {
119 checked: true,
120 disabled,
121 }),
122 );
123 }
124 }
125 }
126 }
127 if contains_task {
128 node.attrs.push(("class", "contains-task-list".into()));
129 }
130 }
131 for n in node.children.iter_mut() {
132 stacker::maybe_grow(64 * 1024, 1024 * 1024, || {
133 walk_recursive(n, disabled);
134 });
135 }
136 }
137
138 walk_recursive(root, DISABLED);
139 }
140}