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