tui/markdown/
source_lines.rs1use super::headings::{MarkdownHeading, parse_markdown_headings};
2use super::renderer::render_markdown_result;
3use crate::line::Line;
4use crate::rendering::render_context::ViewContext;
5use crate::style::Style;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SourceMarkdownLine {
9 pub source_line_no: usize,
10 pub line: Line,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SourceMarkdownRenderResult {
15 pub lines: Vec<SourceMarkdownLine>,
16 pub headings: Vec<MarkdownHeading>,
17}
18
19pub fn render_markdown_source_lines(text: &str, ctx: &ViewContext) -> SourceMarkdownRenderResult {
22 let raw_lines = text.split('\n').collect::<Vec<_>>();
23 SourceMarkdownRenderResult { lines: render_source_lines(&raw_lines, ctx), headings: parse_markdown_headings(text) }
24}
25
26fn render_source_lines(raw_lines: &[&str], ctx: &ViewContext) -> Vec<SourceMarkdownLine> {
27 let mut rendered = Vec::with_capacity(raw_lines.len());
28 let mut index = 0usize;
29
30 while index < raw_lines.len() {
31 let raw = raw_lines[index];
32 if let Some(fence) = FenceDelimiter::parse(raw) {
33 rendered.push(SourceMarkdownLine {
34 source_line_no: index + 1,
35 line: render_raw_line(raw, Style::fg(ctx.theme.muted())),
36 });
37 index += 1;
38
39 let code_start = index;
40 while index < raw_lines.len() && FenceDelimiter::parse(raw_lines[index]).is_none() {
41 index += 1;
42 }
43 let code_end = index;
44 rendered.extend(render_fenced_code_block(code_start, &raw_lines[code_start..code_end], &fence.lang, ctx));
45
46 if index < raw_lines.len() {
47 rendered.push(SourceMarkdownLine {
48 source_line_no: index + 1,
49 line: render_raw_line(raw_lines[index], Style::fg(ctx.theme.muted())),
50 });
51 index += 1;
52 }
53 } else {
54 rendered
55 .push(SourceMarkdownLine { source_line_no: index + 1, line: render_single_markdown_line(raw, ctx) });
56 index += 1;
57 }
58 }
59
60 rendered
61}
62
63fn render_single_markdown_line(raw: &str, ctx: &ViewContext) -> Line {
64 if raw.trim().is_empty() {
65 return Line::default();
66 }
67
68 if is_table_source_line(raw) {
69 return Line::new(raw);
70 }
71
72 render_markdown_result(raw, ctx)
73 .lines
74 .into_iter()
75 .next()
76 .map(|line| line.line)
77 .filter(|line| !line.is_empty())
78 .unwrap_or_else(|| Line::new(raw))
79}
80
81fn render_raw_line(raw: &str, style: Style) -> Line {
82 Line::with_style(raw, style)
83}
84
85fn render_fenced_code_block(
86 source_start_index: usize,
87 raw_lines: &[&str],
88 lang: &str,
89 ctx: &ViewContext,
90) -> Vec<SourceMarkdownLine> {
91 if raw_lines.is_empty() {
92 return Vec::new();
93 }
94
95 let code_text = raw_lines.join("\n");
96 let highlighted = ctx.highlighter().highlight(&code_text, lang, &ctx.theme);
97 let lines = if highlighted.len() == raw_lines.len() {
98 highlighted
99 } else {
100 raw_lines.iter().map(|raw| render_raw_line(raw, Style::fg(ctx.theme.code_fg()))).collect()
101 };
102
103 lines
104 .into_iter()
105 .enumerate()
106 .map(|(offset, line)| SourceMarkdownLine { source_line_no: source_start_index + offset + 1, line })
107 .collect()
108}
109
110fn is_table_source_line(raw: &str) -> bool {
111 let trimmed = raw.trim();
112 trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.matches('|').count() >= 2
113}
114
115struct FenceDelimiter {
116 lang: String,
117}
118
119impl FenceDelimiter {
120 fn parse(raw: &str) -> Option<Self> {
121 let trimmed = raw.trim_start();
122 let marker = if trimmed.starts_with("```") {
123 "```"
124 } else if trimmed.starts_with("~~~") {
125 "~~~"
126 } else {
127 return None;
128 };
129
130 let rest = trimmed.trim_start_matches(marker).trim();
131 Some(Self { lang: rest.split_whitespace().next().unwrap_or("").to_string() })
132 }
133}