diff --git a/src/markdown/lists.rs b/src/markdown/lists.rs index 68fd9d2..eb814f4 100644 --- a/src/markdown/lists.rs +++ b/src/markdown/lists.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::theme::MarkdownTheme; use ratatui::{ style::Style, @@ -9,6 +11,10 @@ use super::width::display_width; use super::wrapping::push_wrapped_prefixed_lines; use super::LastBlock; +pub(crate) const TASK_CHECKED: &str = "☑ "; +pub(crate) const TASK_CHECKED_ALT: &str = "☒ "; +pub(crate) const TASK_UNCHECKED: &str = "☐ "; + #[derive(Clone, Copy)] pub(super) enum ListKind { Unordered, @@ -41,16 +47,18 @@ pub(super) fn list_item_prefix( let depth = list_stack.len(); prefix.push(Span::raw(" ".repeat(depth.saturating_sub(1)))); - let marker = match item.checkbox { - Some(true) => "☑ ".to_string(), - Some(false) => "☐ ".to_string(), + let marker: Cow<'static, str> = match item.checkbox { + // Avoids U+2611 emoji rendering on Windows. + Some(true) if cfg!(target_os = "windows") => TASK_CHECKED_ALT.into(), + Some(true) => TASK_CHECKED.into(), + Some(false) => TASK_UNCHECKED.into(), None => match list_stack.last().copied().unwrap_or(ListKind::Unordered) { ListKind::Unordered => match depth { - 1 => "• ".to_string(), - 2 => "◦ ".to_string(), - _ => "▸ ".to_string(), + 1 => "• ".into(), + 2 => "◦ ".into(), + _ => "▸ ".into(), }, - ListKind::Ordered(n) => format!("{n}. "), + ListKind::Ordered(n) => format!("{n}. ").into(), }, }; item.continuation_indent = " ".repeat(depth.saturating_sub(1)).len() + display_width(&marker); diff --git a/src/markdown/mod.rs b/src/markdown/mod.rs index 98b83ee..b3e4564 100644 --- a/src/markdown/mod.rs +++ b/src/markdown/mod.rs @@ -48,6 +48,8 @@ use links::build_link_spans; use lists::{ end_item, end_list, flush_list_item_spans, start_item, start_list, ItemState, ListKind, }; +#[cfg(test)] +pub(crate) use lists::{TASK_CHECKED, TASK_CHECKED_ALT, TASK_UNCHECKED}; use markers::push_custom_marker_spans; use spans::{ handle_inline_style_event, inline_text_style, push_inline_code_span, push_inline_latex_span, diff --git a/src/tests/markdown_lists.rs b/src/tests/markdown_lists.rs index 166568a..954399a 100644 --- a/src/tests/markdown_lists.rs +++ b/src/tests/markdown_lists.rs @@ -1,5 +1,6 @@ use super::{rendered_non_empty_lines, test_assets, test_md_theme}; use crate::markdown::{parse_markdown, parse_markdown_with_width}; +use crate::markdown::{TASK_CHECKED, TASK_CHECKED_ALT, TASK_UNCHECKED}; use crate::*; #[test] @@ -224,6 +225,13 @@ fn wrapped_list_inline_code_keeps_left_padding_in_rendered_line() { ); } +#[test] +fn task_markers_have_uniform_width() { + assert_eq!(display_width(TASK_UNCHECKED), 2); + assert_eq!(display_width(TASK_CHECKED_ALT), 2); + assert_eq!(display_width(TASK_CHECKED), 2); +} + #[test] fn code_block_inside_list_item_is_indented_and_has_no_blank_gap_before() { let (ss, theme) = test_assets();