diff --git a/src/consts.rs b/src/consts.rs index 93c06dc..48c471b 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -6,6 +6,7 @@ pub const PREFIX_HEADER: &str = "=== "; pub const PREFIX_LIST_CONTINUATION: &str = " "; pub const PREFIX_PREFORMATTED: &str = "| "; pub const PREFIX_QUOTE: &str = "> "; +pub const PREFIX_TODO_ITEM: &str = "["; pub const MARKER_FENCED_FILETYPE_BACKTICK: &str = "```"; pub const MARKER_FENCED_FILETYPE_TILDE: &str = "~~~"; diff --git a/src/formatting.rs b/src/formatting.rs index 8812ef8..d9bef11 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -112,7 +112,7 @@ pub fn format_to_string(document: &Document) -> String { /// Finds a word boundary (i.e. whitespace after a word) nearest to the maximum line length. fn find_word_boundary(line: &FormattedLine, max_line_length: usize) -> Option { - let prefix_length = line.line_type.get_prefix().len(); + let prefix_length = line.line_type.get_prefix_length(); if let Some(split_pos) = line .contents .chars() diff --git a/src/lib.rs b/src/lib.rs index 91f4664..0e623e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,8 +139,11 @@ impl RawLine { self.raw.trim().is_empty() } - fn is_bullet_point(&self) -> bool { - LineType::from_raw(&self.trimmed) == LineType::ListBulletPoint + fn is_list_item(&self) -> bool { + match LineType::from_raw(&self.trimmed) { + LineType::ListBulletPoint | LineType::ListTodoItem => true, + _ => false, + } } fn is_header(&self) -> bool { @@ -183,8 +186,11 @@ impl FormattedLine { } fn is_list_item(&self) -> bool { - self.line_type == LineType::ListBulletPoint - || self.line_type == LineType::ListContinuousLine + match self.line_type { + LineType::ListBulletPoint | LineType::ListTodoItem => true, + LineType::ListContinuousLine => true, + _ => false, + } } fn num_indent(&self) -> usize { @@ -199,23 +205,28 @@ pub enum LineType { Header, /// Currently processing a list item that started with a '* ' ListBulletPoint, - /// Currently processing a wrapped line that is part of the previous ListBulletPoint + /// Currently processing a wrapped line that is part of a list started on an earlier line ListContinuousLine, - /// A line that starts with a '|' is considered to be preformatted, and can be longer than the - /// maximum line length. + /// An item on a TODO list + ListTodoItem, + /// A line that starts with a '|' is considered to be preformatted, and *can* be longer than + /// the maximum line length. Preformatted, /// A line that is prefixed with a '>' Quote, } impl LineType { - /// Note that this function cannot determine if a line is a 'continuation line' in a bullet + /// Detects the type of the given `line` by looking at its first characters. + /// Note that this function cannot determine if the line is a 'continuation line' in a bullet /// point list since that requires knowledge about the line preceding this one. fn from_raw(line: &str) -> Self { if line.starts_with(consts::PREFIX_HEADER) { LineType::Header } else if line.starts_with(consts::PREFIX_BULLET_POINT) { LineType::ListBulletPoint + } else if line.starts_with(consts::PREFIX_TODO_ITEM) { + LineType::ListTodoItem } else if line.starts_with(consts::MARKER_FENCED_FILETYPE_BACKTICK) || line.starts_with(consts::MARKER_FENCED_FILETYPE_TILDE) || line.starts_with(consts::PREFIX_PREFORMATTED) @@ -233,9 +244,18 @@ impl LineType { Self::Header => consts::PREFIX_HEADER, Self::ListBulletPoint => consts::PREFIX_BULLET_POINT, Self::ListContinuousLine => consts::PREFIX_LIST_CONTINUATION, + Self::ListTodoItem => consts::PREFIX_TODO_ITEM, Self::Preformatted => consts::PREFIX_PREFORMATTED, Self::Quote => consts::PREFIX_QUOTE, _ => "", } } + + fn get_prefix_length(&self) -> usize { + match self { + // '[ ] ' or '[x] ' + Self::ListTodoItem => 4, + _ => self.get_prefix().len(), + } + } } diff --git a/src/main.rs b/src/main.rs index 2d1b98b..3baa89e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,6 +107,15 @@ mod tests { assert_equal(&actual, &expected); } + #[test] + fn format_list_with_todo_items() { + let expected = read_file(Path::new("tests/todo_items.expected")).unwrap(); + let first_format = format_file(Path::new("tests/todo_items.input")); + let second_format = format(&first_format); + + assert_equal(&second_format, &expected); + } + #[test] fn wrapping_long_lines() { let expected = read_file(Path::new("tests/long_lines.expected")).unwrap(); diff --git a/src/parsing.rs b/src/parsing.rs index e7d7370..a416ad7 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -24,9 +24,9 @@ pub fn parse_document(contents: &str) -> Document { let header = FormattedLine::from_raw(raw_line, indent_level); document.add_block(Block::new(header)); - } else if raw_line.is_bullet_point() { - // This case means that the line is either the start of a new bullet point list, or the - // continuation of one. + } else if raw_line.is_list_item() { + // This case means that the line is either the start of a new list (bullet point or + // TODO items), or the continuation of one. let current_block = document.last_block_mut(); let indent_level = determine_new_bullet_point_indent(current_block, &raw_line); @@ -103,17 +103,21 @@ fn determine_new_header_indent(document: &Document, raw_line: &RawLine) -> usize } fn determine_new_bullet_point_indent(current_block: &Block, raw_line: &RawLine) -> usize { - assert!(raw_line.is_bullet_point()); + assert!(raw_line.is_list_item()); - if let Some(previous_bullet_point) = current_block.find_previous_of(LineType::ListBulletPoint) { - match previous_bullet_point + let previous_list_item = current_block + .find_previous_of(LineType::ListBulletPoint) + .or_else(|| current_block.find_previous_of(LineType::ListTodoItem)); + + if let Some(previous_list_item) = previous_list_item { + match previous_list_item .original_raw .num_indent .cmp(&raw_line.num_indent) { // List item is continuation of the bullet point list at the same level of // indenting. - Ordering::Equal => previous_bullet_point.indent_level, + Ordering::Equal => previous_list_item.indent_level, // List item is shifted one or more levels to the left compared to the previous // bullet point in the list. Find the first line (starting from the last line) @@ -125,7 +129,7 @@ fn determine_new_bullet_point_indent(current_block: &Block, raw_line: &RawLine) // List item is shifted right compared to the previous bullet point. Only one // level of indenting per line can be added per line. - Ordering::Less => previous_bullet_point.indent_level + 1, + Ordering::Less => previous_list_item.indent_level + 1, } } else if let Some(previous_text) = current_block.find_previous_of(LineType::Text) { previous_text.indent_level diff --git a/tests/todo_items.expected b/tests/todo_items.expected new file mode 100644 index 0000000..b19d996 --- /dev/null +++ b/tests/todo_items.expected @@ -0,0 +1,18 @@ +=== Header 1 + + [ ] Item 1.A + [x] Item 1.B + + === Header 1.1 + + [ ] Item 1.1.A + + === Header 1.2 + + [ ] Item 1.2.A + +=== Header 2 + + [ ] Item 2.A + [x] Item 2.A.A + [ ] Item 2.B diff --git a/tests/todo_items.input b/tests/todo_items.input new file mode 100644 index 0000000..91ef30a --- /dev/null +++ b/tests/todo_items.input @@ -0,0 +1,18 @@ +=== Header 1 + +[ ] Item 1.A +[x] Item 1.B + + === Header 1.1 + + [ ] Item 1.1.A + + === Header 1.2 + + [ ] Item 1.2.A + +=== Header 2 + + [ ] Item 2.A + [x] Item 2.A.A + [ ] Item 2.B