use ratatui_core::style::Style;
use ratatui_core::text::{Line, Span};
use unicode_segmentation::UnicodeSegmentation;
use crate::style::Theme;
use crate::term::hyperlink::BufferLink;
use crate::width::grapheme_cols;
use super::FencedBlockRenderer;
use super::image::{MarkdownImage, image_cell_size};
use super::item::{MdItem, RichSpan};
use super::table::render_table_linked;
pub(super) fn trim_spans(mut spans: Vec<RichSpan>) -> Vec<RichSpan> {
if let Some(first) = spans.first_mut() {
first.content = first.content.trim_start().to_string();
}
if let Some(last) = spans.last_mut() {
last.content = last.content.trim_end().to_string();
}
spans.retain(|s| !s.content.is_empty());
spans
}
pub(super) fn spans_cols(spans: &[RichSpan]) -> usize {
spans
.iter()
.map(|s| crate::width::str_cols(&s.content) as usize)
.sum()
}
pub(super) fn flatten_linked(
items: &[MdItem],
width: u16,
theme: &Theme,
block_renderer: Option<&dyn FencedBlockRenderer>,
) -> (Vec<Line<'static>>, Vec<BufferLink>) {
let mut images = Vec::new();
flatten_linked_into(items, width, theme, block_renderer, &mut images)
}
pub(super) fn flatten_into(
items: &[MdItem],
width: u16,
theme: &Theme,
block_renderer: Option<&dyn FencedBlockRenderer>,
images: &mut Vec<MarkdownImage>,
) -> Vec<Line<'static>> {
flatten_linked_into(items, width, theme, block_renderer, images).0
}
pub(super) fn flatten_linked_into(
items: &[MdItem],
width: u16,
theme: &Theme,
block_renderer: Option<&dyn FencedBlockRenderer>,
images: &mut Vec<MarkdownImage>,
) -> (Vec<Line<'static>>, Vec<BufferLink>) {
let mut out = Vec::new();
let mut links = Vec::new();
for item in items {
match item {
MdItem::Blank => out.push(Line::default()),
MdItem::Prose { spans, indent } => {
let avail = width.saturating_sub(*indent).max(1);
for (row, row_links) in wrap_rich(spans, avail) {
let line_idx = out.len() as u16;
let line = prefix_line(*indent, row);
for mut bl in row_links {
bl.line = line_idx;
bl.start_col = bl.start_col.saturating_add(*indent);
bl.end_col = bl.end_col.saturating_add(*indent);
links.push(bl);
}
out.push(line);
}
}
MdItem::CodeBlock {
language,
source,
fallback,
indent,
} => {
let avail = width.saturating_sub(*indent).max(1);
let rendered = block_renderer
.and_then(|renderer| renderer.render(language, source, avail, theme));
for line in rendered.as_ref().unwrap_or(fallback) {
out.push(prefix_rendered_line(*indent, line));
}
}
MdItem::Table { table, indent } => {
let avail = width.saturating_sub(*indent).max(1);
for (row, row_links) in render_table_linked(table, avail, theme) {
let line_idx = out.len() as u16;
let line = prefix_line(*indent, row);
for mut bl in row_links {
bl.line = line_idx;
bl.start_col = bl.start_col.saturating_add(*indent);
bl.end_col = bl.end_col.saturating_add(*indent);
links.push(bl);
}
out.push(line);
}
}
MdItem::Image { data, alt, indent } => {
let avail = width.saturating_sub(*indent).max(1);
let (cols, rows) = image_cell_size(data, avail);
images.push(MarkdownImage {
row: out.len().min(u16::MAX as usize) as u16,
indent: *indent,
cols,
rows,
data: data.clone(),
alt: alt.clone(),
});
for _ in 0..rows {
out.push(Line::default());
}
}
}
}
(out, links)
}
fn prefix_rendered_line(indent: u16, line: &Line<'static>) -> Line<'static> {
if indent == 0 {
return line.clone();
}
let mut spans = Vec::with_capacity(line.spans.len() + 1);
spans.push(Span::raw(" ".repeat(indent as usize)));
spans.extend(line.spans.iter().cloned());
Line::from(spans)
}
pub(super) fn prefix_line(indent: u16, mut spans: Vec<RichSpan>) -> Line<'static> {
if indent == 0 {
return Line::from(spans.into_iter().map(|s| s.to_span()).collect::<Vec<_>>());
}
let mut line = vec![Span::raw(" ".repeat(indent as usize))];
line.extend(spans.drain(..).map(|s| s.to_span()));
Line::from(line)
}
pub(super) fn wrap_rich(spans: &[RichSpan], width: u16) -> Vec<(Vec<RichSpan>, Vec<BufferLink>)> {
if width == 0 {
let links = link_runs(spans, 0);
return vec![(spans.to_vec(), links)];
}
let cells: Vec<(&str, Style, Option<&str>)> = spans
.iter()
.flat_map(|s| {
let href = s.href.as_deref();
s.content.graphemes(true).map(move |g| (g, s.style, href))
})
.collect();
let mut out: Vec<(Vec<RichSpan>, Vec<BufferLink>)> = Vec::new();
let mut cur: Vec<(&str, Style, Option<&str>)> = Vec::new();
let mut cur_w = 0u16;
let mut i = 0;
let n = cells.len();
let is_break = |g: &str| g.chars().all(char::is_whitespace);
while i < n {
if is_break(cells[i].0) {
i += 1;
continue;
}
let start = i;
let mut word_w = 0u16;
while i < n && !is_break(cells[i].0) {
word_w = word_w.saturating_add(grapheme_cols(cells[i].0));
i += 1;
}
let word = &cells[start..i];
let sep = u16::from(!cur.is_empty());
if word_w <= width && cur_w + sep + word_w <= width {
if sep == 1 {
let (_, st, href) = *cur.last().expect("sep implies non-empty cur");
cur.push((" ", st, href));
cur_w += 1;
}
cur.extend_from_slice(word);
cur_w += word_w;
} else if word_w <= width {
if !cur.is_empty() {
out.push(coalesce_rich(&cur));
cur.clear();
}
cur.extend_from_slice(word);
cur_w = word_w;
} else {
if !cur.is_empty() {
out.push(coalesce_rich(&cur));
cur.clear();
cur_w = 0;
}
for &cell in word {
let w = grapheme_cols(cell.0);
if cur_w + w > width && !cur.is_empty() {
out.push(coalesce_rich(&cur));
cur.clear();
cur_w = 0;
}
cur.push(cell);
cur_w += w;
}
}
}
if !cur.is_empty() {
out.push(coalesce_rich(&cur));
}
if out.is_empty() {
out.push((Vec::new(), Vec::new()));
}
out
}
pub(super) fn coalesce_rich(
cells: &[(&str, Style, Option<&str>)],
) -> (Vec<RichSpan>, Vec<BufferLink>) {
let mut spans: Vec<RichSpan> = Vec::new();
let mut buf = String::new();
let mut run_style: Option<Style> = None;
let mut run_href: Option<String> = None;
let flush = |spans: &mut Vec<RichSpan>,
buf: &mut String,
style: &mut Option<Style>,
href: &mut Option<String>| {
if let Some(st) = style.take() {
spans.push(RichSpan::styled(std::mem::take(buf), st, href.take()));
}
};
for &(g, st, href) in cells {
let href = href.map(str::to_string);
match (&run_style, &run_href) {
(Some(s), h) if *s == st && *h == href => buf.push_str(g),
_ => {
flush(&mut spans, &mut buf, &mut run_style, &mut run_href);
run_style = Some(st);
run_href = href;
buf.push_str(g);
}
}
}
flush(&mut spans, &mut buf, &mut run_style, &mut run_href);
let links = link_runs(&spans, 0);
(spans, links)
}
pub(super) fn link_runs(spans: &[RichSpan], col_offset: u16) -> Vec<BufferLink> {
let mut links = Vec::new();
let mut col = col_offset;
let mut i = 0;
while i < spans.len() {
let Some(url) = spans[i].href.clone() else {
col = col.saturating_add(crate::width::str_cols(&spans[i].content));
i += 1;
continue;
};
let start = col;
while i < spans.len() && spans[i].href.as_deref() == Some(url.as_str()) {
col = col.saturating_add(crate::width::str_cols(&spans[i].content));
i += 1;
}
if col > start {
links.push(BufferLink {
line: 0, start_col: start,
end_col: col,
url,
});
}
}
links
}