use super::{element::LINK, Block, Line, LinkTag, MetaTag, Word};
use crate::ui::scrollable::markdown::{
fallback::StyledLine,
heading::Headings,
region::{LinkedRegions, SelectedRegion},
};
use ratatui::style::{Color, Style};
use std::fmt;
use term_rustdoc::util::{hashmap, xformat, HashMap, XString};
use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
#[derive(Debug, Default)]
pub struct Blocks {
pub blocks: Vec<Block>,
pub links: Links,
}
impl fmt::Display for Blocks {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for line in &self.blocks {
writeln!(f, "{line}")?;
}
Ok(())
}
}
impl Blocks {
pub fn push(&mut self, block: Block) {
self.blocks.push(block);
}
pub fn links(&mut self) -> &mut Links {
&mut self.links
}
}
impl Blocks {
pub fn new() -> Blocks {
Blocks {
blocks: Vec::with_capacity(16),
links: Links {
heading: Vec::new(),
links: Vec::with_capacity(8),
footnotes: hashmap(1),
},
}
}
pub fn shrink_to_fit(&mut self) {
self.blocks.iter_mut().for_each(Block::shrink_to_fit);
self.blocks.shrink_to_fit();
self.links.heading.shrink_to_fit();
self.links.links.shrink_to_fit();
self.links.footnotes.shrink_to_fit();
}
pub fn write_styled_lines(&mut self, width: f64) -> Vec<StyledLine> {
let mut writer = WriteLines::new(width);
for block in &self.blocks {
writer.write_lines(block.lines());
if !block.links().is_empty() {
writer.write_empty_line();
for &idx in block.links() {
if let Some(word) = self.links.get_link(idx) {
let anchor = Word {
word: xformat!("[{idx}]:"),
style: LINK,
tag: MetaTag::Link(LinkTag::ReferenceLink(idx)),
trailling_whitespace: true,
};
let width = width as usize;
let mut link = &word.word[..];
if link.len() + anchor.word.len() + 1 > width {
writer.write_line(&[anchor]);
while !link.is_empty() {
let end = width.min(link.len());
let line = Word {
word: link[..end].into(),
style: LINK,
tag: MetaTag::Link(LinkTag::ReferenceLink(idx)),
trailling_whitespace: false,
};
writer.write_line(&[line]);
link = &link[end..];
}
} else {
writer.write_line(&[anchor, word]);
}
}
}
}
if !block.footnotes().is_empty() {
writer.write_empty_line();
for key in block.footnotes() {
if let Some(block) = self.links.get_footnote(key) {
writer.write_lines(block.lines());
}
}
}
writer.write_empty_line();
}
writer.split(self.links())
}
}
struct WriteLines {
lines: Vec<StyledLine>,
regions: LinkedRegions,
width: f64,
penalties: Penalties,
}
impl WriteLines {
fn new(width: f64) -> WriteLines {
WriteLines {
lines: Vec::with_capacity(128),
regions: LinkedRegions::new(),
width,
penalties: Penalties::default(),
}
}
fn write_lines(&mut self, lines: &[Line]) {
let width = self.width;
for line in lines {
match wrap_optimal_fit(line, &[width], &self.penalties) {
Ok(lines) => lines.into_iter().for_each(|l| self.write_line(l)),
Err(err) => error!("failed to wrap the line to width {width}:{err}\n{line:?} "),
};
}
}
fn write_line(&mut self, words: &[Word]) {
self.lines.push(Word::words_to_line(
words,
self.lines.len(),
&mut self.regions,
));
}
fn write_empty_line(&mut self) {
self.lines.push(StyledLine::new());
}
fn split(mut self, links: &mut Links) -> Vec<StyledLine> {
links.set_heading_regions(self.regions.take_headings());
self.lines
}
}
#[derive(Default, Debug)]
pub struct Links {
heading: Vec<(u8, XString, SelectedRegion)>,
links: Vec<XString>,
footnotes: HashMap<XString, Block>,
}
impl Links {
pub fn push_link(&mut self, link: XString) -> usize {
self.links
.iter()
.position(|l| *l == link)
.unwrap_or_else(|| {
let len = self.links.len();
self.links.push(link);
len
})
}
pub fn get_link(&self, idx: usize) -> Option<Word> {
self.links.get(idx).map(|link| Word {
word: link.clone(),
style: Style {
fg: Some(Color::Rgb(30, 144, 255)), ..Default::default()
},
tag: MetaTag::Link(LinkTag::ReferenceLink(idx)),
trailling_whitespace: false,
})
}
pub fn push_footnote(&mut self, key: &str, value: Block) {
if let Some(old) = self.footnotes.insert(key.into(), value) {
error!("Footnote definition `{key}` existed with the value {old:?}, but now covered.");
}
}
pub fn get_footnote(&self, key: &str) -> Option<&Block> {
self.footnotes.get(key)
}
pub fn push_heading(&mut self, level: u8, raw: &str) -> usize {
let id = self.heading.len();
self.heading
.push((level, raw.into(), SelectedRegion::default()));
id
}
pub fn set_heading_regions(&mut self, mut regions: Vec<(usize, SelectedRegion)>) {
regions.sort_unstable_by_key(|(idx, _)| *idx);
if regions.len() != self.heading.len() {
error!(
"regions of heading has {} ids {:?}\nbut headings in blocks has {} ids {:?}",
regions.len(),
regions.iter().map(|r| r.0).collect::<Vec<_>>(),
self.heading.len(),
self.heading.iter().map(|r| r.0).collect::<Vec<_>>(),
);
}
for (idx, region) in regions {
if let Some((_, _, old)) = self.heading.get_mut(idx) {
*old = region;
} else {
error!("the heading id {idx} from regions doesn't exist in Links");
}
}
}
pub fn to_heading(&self) -> Headings {
let Some(top) = self.heading.iter().map(|h| h.0).min() else {
return Headings::default();
};
let mut headings = Headings::with_capacity(self.heading.len());
for (level, text, region) in &self.heading {
let mut heading = XString::default();
(0..level.saturating_sub(top)).for_each(|_| heading.push_str(" "));
heading.push_str(text.trim_start().trim_start_matches('#').trim());
headings.push(heading, region.clone());
}
headings
}
}