use ratatui_core::layout::Rect;
use ratatui_core::text::Line;
use crate::components::Image;
use crate::components::text::line_width;
use crate::geometry::Size;
use crate::highlight::{CodeHighlighter, Highlighter};
use crate::style::{StyleSheet, Theme};
use crate::surface::Surface;
use crate::term::hyperlink::{BufferLink, LinkPolicy, apply_buffer_links};
use crate::term::image::{ImageLayer, ImageSupport};
use crate::view::{RenderCtx, View};
use super::MarkdownBlockRenderer;
use super::flatten::flatten_linked_into;
use super::image::{ImageResolver, MarkdownImage};
use super::parse::parse_with;
pub struct Markdown<'a> {
source: String,
highlighter: CodeHighlighter<'a>,
block_renderers: Vec<&'a dyn MarkdownBlockRenderer>,
resolver: Option<&'a dyn ImageResolver>,
image_support: ImageSupport,
image_layer: Option<ImageLayer>,
link_policy: LinkPolicy,
}
impl<'a> Markdown<'a> {
pub fn new(source: impl Into<String>) -> Self {
Self {
source: source.into(),
highlighter: CodeHighlighter::Plain,
block_renderers: Vec::new(),
resolver: None,
image_support: ImageSupport::None,
image_layer: None,
link_policy: LinkPolicy::default(),
}
}
pub fn highlighter(mut self, highlighter: &'a dyn Highlighter) -> Self {
self.highlighter = CodeHighlighter::With(highlighter);
self
}
pub fn block_renderer(mut self, renderer: &'a dyn MarkdownBlockRenderer) -> Self {
self.block_renderers.push(renderer);
self
}
pub fn link_policy(mut self, policy: LinkPolicy) -> Self {
self.link_policy = policy;
self
}
pub fn images(
mut self,
resolver: &'a dyn ImageResolver,
support: ImageSupport,
layer: &ImageLayer,
) -> Self {
self.resolver = Some(resolver);
self.image_support = support;
self.image_layer = Some(layer.clone());
self
}
fn lines_images_links(
&self,
width: u16,
theme: &Theme,
sheet: &StyleSheet,
) -> (Vec<Line<'static>>, Vec<MarkdownImage>, Vec<BufferLink>) {
let items = parse_with(&self.source, theme, sheet, self.highlighter, self.resolver);
let mut images = Vec::new();
let (lines, links) = flatten_linked_into(
&items,
width,
theme,
sheet,
self.block_renderers.as_slice(),
&mut images,
);
(lines, images, links)
}
}
impl View for Markdown<'_> {
fn measure(&self, available: Size, ctx: &RenderCtx) -> Size {
let (lines, _, _) = self.lines_images_links(available.width, ctx.theme, &ctx.sheet);
let width = lines.iter().map(line_width).max().unwrap_or(0);
Size::new(width.min(available.width), lines.len() as u16)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
let (lines, images, links) = self.lines_images_links(area.width, ctx.theme, &ctx.sheet);
for (row, line) in lines.iter().enumerate() {
let y = area.y.saturating_add(row as u16);
if y >= area.bottom() {
break;
}
let mut x = area.x;
for span in &line.spans {
if x >= area.right() {
break;
}
x = surface.set_string(x, y, span.content.as_ref(), span.style);
}
}
apply_buffer_links(
surface.buffer_mut(),
ratatui_core::layout::Position {
x: area.x,
y: area.y,
},
&links,
self.link_policy,
);
for img in images {
let y = area.y.saturating_add(img.row);
let x = area.x.saturating_add(img.indent);
if y >= area.bottom() || x >= area.right() {
continue;
}
let rect = Rect {
x,
y,
width: img.cols.min(area.right() - x),
height: img.rows.min(area.bottom() - y),
};
let mut image = Image::new(img.data, img.cols, img.rows)
.support(self.image_support)
.alt(img.alt);
if let Some(layer) = &self.image_layer {
image = image.in_layer(layer);
}
image.render(rect, surface, ctx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{MarkdownBlock, MarkdownBlockContext, MarkdownBlockRenderer};
use crate::style::StyleBundle;
use crate::testing::{grid, render_with_sheet};
use ratatui_core::style::Color;
use ratatui_core::text::{Line, Span};
struct ContextBlock;
impl MarkdownBlockRenderer for ContextBlock {
fn render(
&self,
block: MarkdownBlock<'_>,
context: MarkdownBlockContext<'_>,
) -> Option<Vec<Line<'static>>> {
if !matches!(block, MarkdownBlock::Html { .. }) {
return None;
}
if context.theme.accent == Color::Cyan
&& context.sheet.heading.fg == Some(Color::Magenta)
{
Some(vec![
Line::from(Span::raw("alpha")),
Line::from(Span::raw("beta")),
Line::from(Span::raw("omega")),
])
} else {
Some(vec![Line::from(Span::raw("wrong context"))])
}
}
}
#[test]
fn measurement_and_render_share_the_active_theme_and_sheet() {
let theme = Theme {
accent: Color::Cyan,
..Theme::default()
};
let sheet = StyleSheet {
heading: StyleBundle::new().fg(Color::Magenta),
..StyleSheet::from_theme(&theme)
};
let renderer = ContextBlock;
let view = Markdown::new("<div>context</div>").block_renderer(&renderer);
let ctx = RenderCtx::new(&theme).with_sheet(sheet);
assert_eq!(view.measure(Size::new(10, 10), &ctx), Size::new(5, 3));
assert_eq!(
grid(&render_with_sheet(&view, 10, 3, &theme, sheet)),
"alpha \nbeta \nomega "
);
}
}