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;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MdRuleInk {
Token(crate::theme::TokenId),
Fixed(Rgba),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MdRuleWidth {
FullBleed,
Measure,
Inset(i32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MdRuleStyle {
pub ink: MdRuleInk,
pub width: MdRuleWidth,
pub space_before: i32,
pub space_after: i32,
}
impl Default for MdRuleStyle {
fn default() -> MdRuleStyle {
MdRuleStyle {
ink: MdRuleInk::Token(crate::theme::TokenId::Border),
width: MdRuleWidth::FullBleed,
space_before: 1,
space_after: 1,
}
}
}
impl MdRuleStyle {
pub fn ink(mut self, ink: MdRuleInk) -> MdRuleStyle {
self.ink = ink;
self
}
pub fn width(mut self, width: MdRuleWidth) -> MdRuleStyle {
self.width = width;
self
}
pub fn space(mut self, before: i32, after: i32) -> MdRuleStyle {
self.space_before = before.max(0);
self.space_after = after.max(0);
self
}
fn resolve(&self, t: &TokenSet, measure: i32) -> RuleRow {
let ink = match self.ink {
MdRuleInk::Token(id) => t.get(id),
MdRuleInk::Fixed(c) => c,
};
let (indent, extent) = match self.width {
MdRuleWidth::FullBleed => (0, None),
MdRuleWidth::Measure => (0, Some(measure.max(1))),
MdRuleWidth::Inset(cells) => {
let cells = cells.clamp(0, (measure - 1).max(0) / 2);
(cells, Some((measure - 2 * cells).max(1)))
}
};
RuleRow {
ink,
indent,
extent,
gap_after: self.space_after.max(0),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) struct RuleRow {
pub(crate) ink: Rgba,
pub(crate) indent: i32,
pub(crate) extent: Option<i32>,
pub(crate) gap_after: i32,
}
pub(crate) struct Row {
pub(crate) line: RichLine,
pub(crate) indent: i32,
pub(crate) ground: Option<Rgba>,
pub(crate) quote: bool,
pub(crate) rule: Option<RuleRow>,
pub(crate) image: Option<imageflow::MdImageSlice>,
pub(crate) fence: Option<FenceSlice>,
}
type TypesetCache = Rc<RefCell<WidthCache>>;
#[derive(Default)]
pub(crate) struct WidthCache {
slots: [Option<(i32, Vec<Row>)>; 2],
}
impl WidthCache {
fn rows_for(&mut self, width: i32, build: impl FnOnce() -> Vec<Row>) -> &[Row] {
match self
.slots
.iter()
.position(|s| matches!(s, Some((w, _)) if *w == width))
{
Some(0) => {}
Some(i) => self.slots.swap(0, i),
None => {
self.slots.swap(0, 1);
self.slots[0] = Some((width, build()));
}
}
match &self.slots[0] {
Some((_, rows)) => rows,
None => unreachable!("WidthCache::rows_for left slot 0 empty"),
}
}
}
impl Row {
pub(crate) fn plain(line: RichLine) -> Row {
Row {
line,
indent: 0,
ground: None,
quote: false,
rule: None,
image: None,
fence: None,
}
}
}
pub struct MarkdownView {
fence: Option<Rc<dyn FenceBlock>>,
source: String,
scroll_offset: i32,
layout: Option<LayoutStyle>,
highlights: Vec<MdSearchMatch>,
current_match: Option<usize>,
rule: MdRuleStyle,
}
impl MarkdownView {
pub fn new(source: impl Into<String>) -> MarkdownView {
MarkdownView {
fence: None,
source: source.into(),
scroll_offset: 0,
layout: None,
highlights: Vec::new(),
current_match: None,
rule: MdRuleStyle::default(),
}
}
pub fn rule_style(mut self, rule: MdRuleStyle) -> MarkdownView {
self.rule = rule;
self
}
pub fn fence_block(mut self, block: Rc<dyn FenceBlock>) -> MarkdownView {
self.fence = Some(block);
self
}
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 {
Self::rows_ruled(source, t, width, MdRuleStyle::default())
}
pub fn rows_ruled(source: &str, t: &TokenSet, width: i32, rule: MdRuleStyle) -> usize {
doc::layout_doc_ruled(source, t, width, rule).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> {
Self::outline_rows_ruled(source, t, width, MdRuleStyle::default())
}
pub fn outline_rows_ruled(
source: &str,
t: &TokenSet,
width: i32,
rule: MdRuleStyle,
) -> Vec<OutlineEntry> {
doc::outline_rows_ruled(source, t, width, rule)
}
pub fn resolve_anchor(source: &str, t: &TokenSet, width: i32, anchor: &str) -> Option<usize> {
Self::resolve_anchor_ruled(source, t, width, anchor, MdRuleStyle::default())
}
pub fn resolve_anchor_ruled(
source: &str,
t: &TokenSet,
width: i32,
anchor: &str,
rule: MdRuleStyle,
) -> Option<usize> {
let want = anchor.strip_prefix('#').unwrap_or(anchor);
doc::outline_rows_ruled(source, t, width, rule)
.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> {
Self::find_ruled(
source,
t,
width,
query,
case_insensitive,
MdRuleStyle::default(),
)
}
pub fn find_ruled(
source: &str,
t: &TokenSet,
width: i32,
query: &str,
case_insensitive: bool,
rule: MdRuleStyle,
) -> Vec<MdSearchMatch> {
search::find_in_rows(
&doc::layout_doc_ruled(source, t, width, rule).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(WidthCache::default()));
let measure_cache = Rc::clone(&cache);
let measure_source = source.clone();
let draw_fence = self.fence.clone();
let measure_fence = draw_fence.clone();
let rule = self.rule;
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 = slot.rows_for(avail.w, || {
doc::layout_doc_all(
&measure_source,
&tokens,
avail.w,
measure_fence.clone(),
rule,
)
.rows
});
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 = slot.rows_for(rect.w, || {
doc::layout_doc_all(&source, &tokens, rect.w, draw_fence.clone(), rule).rows
});
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 trait FenceBlock {
fn measure(&self, lang: &str, source: &str, width: i32) -> Option<i32>;
#[allow(clippy::too_many_arguments)]
fn draw_row(
&self,
lang: &str,
source: &str,
row: i32,
at: Point,
width: i32,
tokens: &TokenSet,
canvas: &mut dyn StyledCanvas,
);
}
#[derive(Clone)]
pub(crate) struct FenceSlice {
pub(crate) block: Rc<dyn FenceBlock>,
pub(crate) lang: String,
pub(crate) source: Rc<String>,
pub(crate) row: i32,
pub(crate) width: i32,
}
pub(crate) struct BlockTypesetter {
styles: MdStyles,
fence: Option<Rc<dyn FenceBlock>>,
lexer: CLikeLexer,
diff: DiffLexer,
json: JsonLexer,
yaml: YamlLexer,
code_base: Style,
t: TokenSet,
rule: MdRuleStyle,
}
impl BlockTypesetter {
pub(crate) fn new(t: &TokenSet) -> BlockTypesetter {
BlockTypesetter {
styles: md_styles(t),
fence: None,
lexer: CLikeLexer::default(),
diff: DiffLexer::new(),
json: JsonLexer::new(),
yaml: YamlLexer::new(),
code_base: Style::new().fg(t.text),
t: *t,
rule: MdRuleStyle::default(),
}
}
pub(crate) fn with_rule_style(mut self, rule: MdRuleStyle) -> BlockTypesetter {
self.rule = rule;
self
}
pub(crate) fn rule_style(&self) -> MdRuleStyle {
self.rule
}
pub(crate) fn separator_rows(&self, out: &[Row], separate: bool) -> usize {
if !separate || out.is_empty() {
return 0;
}
out.last()
.and_then(|r| r.rule)
.map_or(1, |r| r.gap_after.max(0) as usize)
}
pub(crate) fn with_fence_block(mut self, fence: Option<Rc<dyn FenceBlock>>) -> BlockTypesetter {
self.fence = fence;
self
}
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>| {
for _ in 0..self.separator_rows(rows, separate) {
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: Some(MdRuleStyle::default().resolve(t, width)),
image: None,
fence: 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: None,
image: None,
fence: 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: None,
image: None,
fence: None,
});
}
}
Block::CodeFence { lang, lines } => {
blank(out);
if let Some(fence) = &self.fence {
let source = lines.join("\n");
if let Some(rows) = fence.measure(lang, &source, width) {
let source = Rc::new(source);
for row in 0..rows.max(0) {
out.push(Row {
line: RichLine::new(),
indent: 0,
ground: None,
quote: false,
rule: None,
image: None,
fence: Some(FenceSlice {
block: Rc::clone(fence),
lang: lang.clone(),
source: Rc::clone(&source),
row,
width,
}),
});
}
return;
}
}
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: None,
image: None,
fence: None,
});
}
}
Block::Rule => {
if separate && !out.is_empty() {
for _ in 0..self.rule.space_before.max(0) {
out.push(Row::plain(RichLine::new()));
}
}
out.push(Row {
line: RichLine::new(),
indent: 0,
ground: None,
quote: false,
rule: Some(self.rule.resolve(t, width)),
image: None,
fence: 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 let Some(rule) = row.rule {
let start = rect.x + rule.indent.clamp(0, rect.w.max(0));
let end = match rule.extent {
Some(cells) => (start + cells).min(rect.right()),
None => rect.right(),
};
for x in start..end {
canvas.put(Point::new(x, y), '─', rule.ink, Rgba::TRANSPARENT);
}
continue;
}
if let Some(slice) = &row.image {
imageflow::draw_image_row(canvas, rect, y, t, row.indent, slice);
continue;
}
if let Some(slice) = &row.fence {
slice.block.draw_row(
&slice.lang,
&slice.source,
slice.row,
Point::new(rect.x, y),
slice.width.min(rect.w),
t,
canvas,
);
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;