1mod code_blocks;
4mod links;
5mod parsing;
6mod tables;
7
8use crate::config::loader::SyntaxHighlightingConfig;
9use crate::ui::theme::{self, ThemeStyles};
10use anstyle::Style;
11use code_blocks::{
12 CodeBlockRenderEnv, CodeBlockState, finalize_unclosed_code_block, handle_code_block_event,
13};
14use parsing::{
15 LinkState, ListState, MarkdownContext, append_text, handle_end_tag, handle_start_tag,
16 inline_code_style, push_blank_line, trim_trailing_blank_lines,
17};
18use pulldown_cmark::{Event, Options, Parser};
19use tables::TableBuffer;
20use unicode_width::UnicodeWidthStr;
21
22pub use code_blocks::{
23 HighlightedSegment, highlight_code_to_ansi, highlight_code_to_segments, highlight_line_for_diff,
24};
25
26pub(crate) const LIST_INDENT_WIDTH: usize = 2;
27pub(crate) const CODE_LINE_NUMBER_MIN_WIDTH: usize = 3;
28
29#[derive(Clone, Debug)]
31pub struct MarkdownSegment {
32 pub style: Style,
33 pub text: String,
34 pub link_target: Option<String>,
35}
36
37impl MarkdownSegment {
38 pub(crate) fn new(style: Style, text: impl Into<String>) -> Self {
39 Self {
40 style,
41 text: text.into(),
42 link_target: None,
43 }
44 }
45
46 pub(crate) fn with_link(
47 style: Style,
48 text: impl Into<String>,
49 link_target: Option<String>,
50 ) -> Self {
51 Self {
52 style,
53 text: text.into(),
54 link_target,
55 }
56 }
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct MarkdownLine {
62 pub segments: Vec<MarkdownSegment>,
63}
64
65impl MarkdownLine {
66 pub(crate) fn push_segment(&mut self, style: Style, text: &str) {
67 self.push_segment_with_link(style, text, None);
68 }
69
70 pub(crate) fn push_segment_with_link(
71 &mut self,
72 style: Style,
73 text: &str,
74 link_target: Option<String>,
75 ) {
76 if text.is_empty() {
77 return;
78 }
79 if let Some(last) = self.segments.last_mut()
80 && last.style == style
81 && last.link_target == link_target
82 {
83 last.text.push_str(text);
84 return;
85 }
86 self.segments
87 .push(MarkdownSegment::with_link(style, text, link_target));
88 }
89
90 pub fn is_empty(&self) -> bool {
91 self.segments
92 .iter()
93 .all(|segment| segment.text.trim().is_empty())
94 }
95
96 pub(crate) fn width(&self) -> usize {
97 self.segments
98 .iter()
99 .map(|seg| UnicodeWidthStr::width(seg.text.as_str()))
100 .sum()
101 }
102}
103
104#[derive(Debug, Clone, Copy, Default)]
105pub struct RenderMarkdownOptions {
106 pub preserve_code_indentation: bool,
107 pub disable_code_block_table_reparse: bool,
108}
109
110pub fn render_markdown_to_lines(
112 source: &str,
113 base_style: Style,
114 theme_styles: &ThemeStyles,
115 highlight_config: Option<&SyntaxHighlightingConfig>,
116) -> Vec<MarkdownLine> {
117 render_markdown_to_lines_with_options(
118 source,
119 base_style,
120 theme_styles,
121 highlight_config,
122 RenderMarkdownOptions::default(),
123 )
124}
125
126pub fn render_markdown_to_lines_with_options(
127 source: &str,
128 base_style: Style,
129 theme_styles: &ThemeStyles,
130 highlight_config: Option<&SyntaxHighlightingConfig>,
131 render_options: RenderMarkdownOptions,
132) -> Vec<MarkdownLine> {
133 let parser_options = Options::ENABLE_STRIKETHROUGH
134 | Options::ENABLE_TABLES
135 | Options::ENABLE_TASKLISTS
136 | Options::ENABLE_FOOTNOTES;
137
138 let parser = Parser::new_ext(source, parser_options);
139
140 let mut lines = Vec::new();
141 let mut current_line = MarkdownLine::default();
142 let mut style_stack = vec![base_style];
143 let mut blockquote_depth = 0usize;
144 let mut list_stack: Vec<ListState> = Vec::new();
145 let mut list_continuation_prefix = String::new();
146 let mut pending_list_prefix: Option<String> = None;
147 let mut code_block: Option<CodeBlockState> = None;
148 let mut active_table: Option<TableBuffer> = None;
149 let mut link_state: Option<LinkState> = None;
150
151 for event in parser {
152 let mut code_block_env = code_block_render_env(
153 &mut lines,
154 &mut current_line,
155 blockquote_depth,
156 &list_continuation_prefix,
157 &mut pending_list_prefix,
158 base_style,
159 theme_styles,
160 highlight_config,
161 render_options,
162 );
163 if handle_code_block_event(&event, &mut code_block, &mut code_block_env) {
164 continue;
165 }
166
167 let mut ctx = MarkdownContext {
168 style_stack: &mut style_stack,
169 blockquote_depth: &mut blockquote_depth,
170 list_stack: &mut list_stack,
171 pending_list_prefix: &mut pending_list_prefix,
172 list_continuation_prefix: &mut list_continuation_prefix,
173 lines: &mut lines,
174 current_line: &mut current_line,
175 theme_styles,
176 base_style,
177 code_block: &mut code_block,
178 active_table: &mut active_table,
179 link_state: &mut link_state,
180 };
181
182 match event {
183 Event::Start(ref tag) => handle_start_tag(tag, &mut ctx),
184 Event::End(tag) => handle_end_tag(tag, &mut ctx),
185 Event::Text(text) => append_text(&text, &mut ctx),
186 Event::Code(code) => {
187 ctx.ensure_prefix();
188 ctx.current_line.push_segment_with_link(
189 inline_code_style(theme_styles, base_style),
190 &code,
191 ctx.active_link_target(),
192 );
193 }
194 Event::SoftBreak | Event::HardBreak => ctx.flush_line(),
195 Event::Rule => {
196 ctx.flush_line();
197 let mut line = MarkdownLine::default();
198 line.push_segment(base_style.dimmed(), &"―".repeat(32));
199 ctx.lines.push(line);
200 push_blank_line(ctx.lines);
201 }
202 Event::TaskListMarker(checked) => {
203 ctx.ensure_prefix();
204 ctx.current_line
205 .push_segment(base_style, if checked { "[x] " } else { "[ ] " });
206 }
207 Event::Html(html) | Event::InlineHtml(html) => append_text(&html, &mut ctx),
208 Event::FootnoteReference(r) => append_text(&format!("[^{}]", r), &mut ctx),
209 Event::InlineMath(m) => append_text(&format!("${}$", m), &mut ctx),
210 Event::DisplayMath(m) => append_text(&format!("$$\n{}\n$$", m), &mut ctx),
211 }
212 }
213
214 let mut code_block_env = code_block_render_env(
215 &mut lines,
216 &mut current_line,
217 blockquote_depth,
218 &list_continuation_prefix,
219 &mut pending_list_prefix,
220 base_style,
221 theme_styles,
222 highlight_config,
223 render_options,
224 );
225 finalize_unclosed_code_block(&mut code_block, &mut code_block_env);
226
227 if !current_line.segments.is_empty() {
228 lines.push(current_line);
229 }
230
231 trim_trailing_blank_lines(&mut lines);
232 lines
233}
234
235pub fn render_markdown(source: &str) -> Vec<MarkdownLine> {
237 let styles = theme::active_styles();
238 render_markdown_to_lines(source, Style::default(), &styles, None)
239}
240
241fn code_block_render_env<'a>(
242 lines: &'a mut Vec<MarkdownLine>,
243 current_line: &'a mut MarkdownLine,
244 blockquote_depth: usize,
245 list_continuation_prefix: &'a str,
246 pending_list_prefix: &'a mut Option<String>,
247 base_style: Style,
248 theme_styles: &'a ThemeStyles,
249 highlight_config: Option<&'a SyntaxHighlightingConfig>,
250 render_options: RenderMarkdownOptions,
251) -> CodeBlockRenderEnv<'a> {
252 CodeBlockRenderEnv {
253 lines,
254 current_line,
255 blockquote_depth,
256 list_continuation_prefix,
257 pending_list_prefix,
258 base_style,
259 theme_styles,
260 highlight_config,
261 render_options,
262 }
263}
264
265#[cfg(test)]
266mod tests;