1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! `Block::List` rendering.
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use crate::markdown::ast::{Block, ListItem};
use crate::markdown::renderer::Renderer;
impl<'t> Renderer<'t> {
pub(super) fn render_list(
&self,
ordered: bool,
start: Option<u64>,
items: &[ListItem],
out: &mut Vec<Line<'static>>,
indent_prefix: &str,
) {
// `marker_width` is the prefix printed before each item's first line; `nested_indent_width`
// is the leading whitespace before each nested block. The latter is `max(4, marker_width)`
// because source nesting uses 4 spaces, and rendering at the same width keeps the raw-mode
// view from showing the nested marker shifted; a marker wider than 4 grows the indent with
// it so those markers still align under their parent's content column.
let first_num = start.unwrap_or(1);
let last_num = first_num + items.len().saturating_sub(1) as u64;
let digit_width = last_num.to_string().len().max(1);
let marker_width = if ordered { digit_width + 2 } else { 2 };
let nested_indent_width = marker_width.max(4);
let child_indent_prefix = format!("{indent_prefix}{}", " ".repeat(nested_indent_width));
let mut counter = first_num;
for item in items {
// Loose-list spacing. These blanks stay 1:1 with the source lines the reveal maps
// against, so the count comes straight from `annotate_list_blanks`.
for _ in 0..item.blank_lines_before {
out.push(Line::raw(""));
}
// A task is a decorated bullet — the same marker plus the checkbox span below — so
// task items and plain bullets can coexist in one list.
let (marker, marker_style) = if ordered {
// Right-aligned in a `digit_width` slot so 10+ doesn't push its text out of
// alignment with the single-digit items above.
let s = format!(
"{indent_prefix}{counter:>digit_width$}. ",
digit_width = digit_width
);
counter += 1;
(s, self.theme.list_number)
} else {
let bullet_style = match item.task {
Some(true) => self.theme.task_checked,
Some(false) => self.theme.task_unchecked,
None => self.theme.list_bullet,
};
(format!("{indent_prefix}• "), bullet_style)
};
let task_prefix: Option<Span<'static>> = item.task.map(|checked| {
if checked {
Span::styled("[✓] ", self.theme.task_checked)
} else {
Span::styled("[ ] ", self.theme.task_unchecked)
}
});
// `task_strikethrough` keeps CROSSED_OUT opt-in, so a theme can mute completed text
// without striking it through.
let checked_text_style = if item.task == Some(true) {
if self.theme.task_strikethrough {
self.theme
.task_complete_text
.add_modifier(ratatui::style::Modifier::CROSSED_OUT)
} else {
self.theme.task_complete_text
}
} else {
Style::default()
};
// An empty item still emits its marker so the block produces at least one line.
if item.blocks.is_empty() {
let mut spans = vec![Span::styled(marker.clone(), marker_style)];
if let Some(tp) = task_prefix.clone() {
spans.push(tp);
}
out.push(Line::from(spans));
continue;
}
for (i, block) in item.blocks.iter().enumerate() {
if i == 0 {
match block {
Block::Paragraph { inlines } => {
let mut spans = vec![Span::styled(marker.clone(), marker_style)];
if let Some(tp) = task_prefix.clone() {
spans.push(tp);
}
spans.extend(self.render_inlines(inlines, checked_text_style));
out.push(Line::from(spans));
}
other => {
// A non-paragraph first block gets the marker on a line of its own.
let mut spans = vec![Span::styled(marker.clone(), marker_style)];
if let Some(tp) = task_prefix.clone() {
spans.push(tp);
}
out.push(Line::from(spans));
self.render_block(other, out, &child_indent_prefix, false);
}
}
} else {
// Later blocks take the child indent, so their text aligns with this item's
// text column.
self.render_block(block, out, &child_indent_prefix, false);
}
}
}
}
}