use std::cell::RefCell;
use std::rc::Rc;
use crate::base::{Point, Rgba};
use crate::layout::Style as LayoutStyle;
use crate::render::md::{self, Block, Marker, MdStyles};
use crate::render::rich::{RichLine, RichText, Span};
use crate::render::{Attrs, Style};
use crate::text::{CLikeLexer, DiffLexer, JsonLexer, YamlLexer};
use crate::theme::TokenSet;
use crate::ui::{Element, StyledCanvas};
use super::code::{code_token_color, data_rich_line, diff_rich_line};
#[path = "markdown_doc.rs"]
pub(crate) mod doc;
pub use doc::OutlineEntry;
#[path = "markdown_image.rs"]
pub(crate) mod imageflow;
#[path = "markdown_search.rs"]
pub(crate) mod search;
pub use search::MdSearchMatch;
pub(crate) struct Row {
pub(crate) line: RichLine,
pub(crate) indent: i32,
pub(crate) ground: Option<Rgba>,
pub(crate) quote: bool,
pub(crate) rule: bool,
pub(crate) image: Option<imageflow::MdImageSlice>,
}
type TypesetCache = Rc<RefCell<Option<(i32, Vec<Row>)>>>;
impl Row {
pub(crate) fn plain(line: RichLine) -> Row {
Row {
line,
indent: 0,
ground: None,
quote: false,
rule: false,
image: None,
}
}
}
pub struct MarkdownView {
source: String,
scroll_offset: i32,
layout: Option<LayoutStyle>,
highlights: Vec<MdSearchMatch>,
current_match: Option<usize>,
}
impl MarkdownView {
pub fn new(source: impl Into<String>) -> MarkdownView {
MarkdownView {
source: source.into(),
scroll_offset: 0,
layout: None,
highlights: Vec::new(),
current_match: None,
}
}
pub fn scroll_offset(mut self, rows: i32) -> MarkdownView {
self.scroll_offset = rows.max(0);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> MarkdownView {
self.layout = Some(layout);
self
}
pub fn highlights(mut self, matches: Vec<MdSearchMatch>, current: Option<usize>) -> Self {
self.highlights = matches;
self.current_match = current;
self
}
pub fn rows(source: &str, t: &TokenSet, width: i32) -> usize {
doc::layout_doc(source, t, width).rows.len()
}
pub fn outline(source: &str, t: &TokenSet) -> Vec<(u8, String)> {
md::parse(source, &md_styles(t))
.iter()
.filter_map(|b| match b {
Block::Heading { level, content } => Some((*level, content.plain())),
_ => None,
})
.collect()
}
pub fn outline_rows(source: &str, t: &TokenSet, width: i32) -> Vec<OutlineEntry> {
doc::outline_rows(source, t, width)
}
pub fn resolve_anchor(source: &str, t: &TokenSet, width: i32, anchor: &str) -> Option<usize> {
let want = anchor.strip_prefix('#').unwrap_or(anchor);
doc::outline_rows(source, t, width)
.into_iter()
.find(|e| e.heading.anchor_id == want)
.map(|e| e.row)
}
pub fn find(
source: &str,
t: &TokenSet,
width: i32,
query: &str,
case_insensitive: bool,
) -> Vec<MdSearchMatch> {
search::find_in_rows(
&doc::layout_doc(source, t, width).rows,
query,
case_insensitive,
)
}
pub fn view(self, cx: crate::reactive::Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(&t).build()
}
pub fn element(self, t: &TokenSet) -> Element {
let tokens = *t;
let offset = self.scroll_offset as usize;
let source = self.source;
let highlights = self.highlights;
let current = self.current_match;
let layout = self.layout.unwrap_or_else(|| {
LayoutStyle::default()
.grow(1.0)
.basis(crate::layout::Dimension::Cells(0))
});
let cache: TypesetCache = Rc::new(RefCell::new(None));
let measure_cache = Rc::clone(&cache);
let measure_source = source.clone();
Element::new()
.style(layout)
.measure(move |avail| {
if avail.w <= 1 {
return crate::base::Size::ZERO;
}
let mut slot = measure_cache.borrow_mut();
let rows = match &mut *slot {
Some((w, rows)) if *w == avail.w => rows,
slot => {
let rows = doc::layout_doc(&measure_source, &tokens, avail.w).rows;
&mut slot.insert((avail.w, rows)).1
}
};
let widest = rows
.iter()
.map(|r| r.indent + r.line.width())
.max()
.unwrap_or(0);
crate::base::Size::new(widest.min(avail.w), rows.len() as i32)
})
.draw(move |canvas, rect| {
if rect.w <= 1 || rect.h <= 0 {
return;
}
let mut slot = cache.borrow_mut();
let rows = match &mut *slot {
Some((w, rows)) if *w == rect.w => rows,
slot => {
let rows = doc::layout_doc(&source, &tokens, rect.w).rows;
&mut slot.insert((rect.w, rows)).1
}
};
let offset = offset.min(rows.len().saturating_sub(1));
draw_rows(canvas, rect, &tokens, &rows[offset..]);
if !highlights.is_empty() {
search::draw_highlights(
canvas,
rect,
&tokens,
rows,
offset,
&highlights,
current,
);
}
})
}
}
pub(crate) fn md_styles(t: &TokenSet) -> MdStyles {
MdStyles {
base: Style::EMPTY,
bold: Style::new().attrs(Attrs::BOLD),
italic: Style::new().attrs(Attrs::ITALIC),
code: Style::new().fg(t.text).bg(t.surface_raised),
link: Style::new().fg(t.link).attrs(Attrs::UNDERLINE),
heading: Style::new().attrs(Attrs::BOLD),
}
}
pub(crate) struct BlockTypesetter {
styles: MdStyles,
lexer: CLikeLexer,
diff: DiffLexer,
json: JsonLexer,
yaml: YamlLexer,
code_base: Style,
t: TokenSet,
}
impl BlockTypesetter {
pub(crate) fn new(t: &TokenSet) -> BlockTypesetter {
BlockTypesetter {
styles: md_styles(t),
lexer: CLikeLexer::default(),
diff: DiffLexer::new(),
json: JsonLexer::new(),
yaml: YamlLexer::new(),
code_base: Style::new().fg(t.text),
t: *t,
}
}
pub(crate) fn styles(&self) -> &MdStyles {
&self.styles
}
pub(crate) fn push_block(&self, out: &mut Vec<Row>, block: &Block, width: i32, separate: bool) {
let t = &self.t;
let blank = |rows: &mut Vec<Row>| {
if separate && !rows.is_empty() {
rows.push(Row::plain(RichLine::new()));
}
};
match block {
Block::Heading { level, content } => {
blank(out);
let ink = match level {
1 => Style::new().fg(t.accent).attrs(Attrs::BOLD),
2 => Style::new().fg(t.accent),
_ => Style::new().fg(t.text).attrs(Attrs::BOLD),
};
let mut line = RichLine::new();
if *level >= 3 {
line.push(Span::new(
format!("{} ", "#".repeat(*level as usize)),
Style::new().fg(t.text_faint),
));
}
for span in &content.spans {
line.push(Span::new(span.text.clone(), span.style.merge(ink)));
}
out.push(Row::plain(line));
if *level == 1 {
out.push(Row {
line: RichLine::new(),
indent: 0,
ground: None,
quote: false,
rule: true,
image: None,
});
}
}
Block::Paragraph(line) => {
blank(out);
for wrapped in wrap_line(line.clone(), width) {
out.push(Row::plain(wrapped));
}
}
Block::ListItem {
depth,
marker,
content,
} => {
let indent = 2 + *depth as i32 * 2;
let mut line = RichLine::new();
let marker_text = match marker {
Marker::Bullet => "โข ".to_string(),
Marker::Number(n) => format!("{n}. "),
};
line.push(Span::new(marker_text, Style::new().fg(t.accent_alt)));
for span in &content.spans {
line.push(span.clone());
}
for (i, wrapped) in wrap_line(line, width - indent).into_iter().enumerate() {
out.push(Row {
line: wrapped,
indent: indent + if i > 0 { 2 } else { 0 },
ground: None,
quote: false,
rule: false,
image: None,
});
}
}
Block::Blockquote(line) => {
blank(out);
let mut muted = RichLine::new();
for span in &line.spans {
let style = if span.style.fg.is_none() {
span.style.fg(t.text_muted)
} else {
span.style
};
muted.push(Span::new(span.text.clone(), style));
}
for wrapped in wrap_line(muted, width - 2) {
out.push(Row {
line: wrapped,
indent: 2,
ground: None,
quote: true,
rule: false,
image: None,
});
}
}
Block::CodeFence { lang, lines } => {
blank(out);
let diff_fence = DiffLexer::matches_lang(lang);
let json_fence = JsonLexer::matches_lang(lang);
let yaml_fence = YamlLexer::matches_lang(lang);
for code_line in lines {
let rich = if diff_fence {
diff_rich_line(code_line, &self.diff, self.code_base, t)
} else if json_fence {
data_rich_line(code_line, self.json.spans(code_line), self.code_base, t)
} else if yaml_fence {
data_rich_line(code_line, self.yaml.spans(code_line), self.code_base, t)
} else {
RichLine::from_highlighted(code_line, &self.lexer, self.code_base, |k| {
Style::new().fg(code_token_color(k, t))
})
};
let mut padded = RichLine::new();
padded.push(Span::new(" ", self.code_base));
for span in rich.spans {
padded.push(span);
}
out.push(Row {
line: padded,
indent: 1,
ground: Some(t.surface_raised),
quote: false,
rule: false,
image: None,
});
}
}
Block::Rule => {
blank(out);
out.push(Row {
line: RichLine::new(),
indent: 0,
ground: None,
quote: false,
rule: true,
image: None,
});
}
}
}
}
fn wrap_line(line: RichLine, width: i32) -> Vec<RichLine> {
RichText::from_lines(vec![line]).wrap(width.max(4)).lines
}
pub(crate) fn draw_rows(
canvas: &mut dyn StyledCanvas,
rect: crate::base::Rect,
t: &TokenSet,
rows: &[Row],
) {
for (i, row) in rows.iter().enumerate() {
let y = rect.y + i as i32;
if y >= rect.bottom() {
break;
}
if row.rule {
for x in rect.x..rect.right() {
canvas.put(Point::new(x, y), 'โ', t.border, Rgba::TRANSPARENT);
}
continue;
}
if let Some(slice) = &row.image {
imageflow::draw_image_row(canvas, rect, y, t, row.indent, slice);
continue;
}
if let Some(ground) = row.ground {
canvas.fill(
crate::base::Rect::new(rect.x, y, rect.w, 1),
' ',
t.text,
ground,
);
}
if row.quote {
canvas.put(Point::new(rect.x, y), 'โ', t.border, Rgba::TRANSPARENT);
}
let mut x = rect.x + row.indent;
for span in &row.line.spans {
let style = if span.style.fg.is_none() {
span.style.fg(t.text)
} else {
span.style
};
x += crate::widgets::richtext::print_span_clipped(
canvas,
x,
y,
rect.right(),
&span.text,
&style,
);
if x >= rect.right() {
break;
}
}
}
}
#[cfg(test)]
#[path = "markdown_scroll_tests.rs"]
mod scroll_composition_tests;
#[cfg(test)]
#[path = "markdown_view_tests.rs"]
mod tests;