use std::collections::VecDeque;
use std::iter::Peekable;
use textwrap::{Options, wrap};
use unicode_width::UnicodeWidthStr as _;
use crate::{
Line, LineKind, Mapper, MarkdownLink,
link_tracker::TrackedUrl,
markdown::{
ListMarker, MdContainer, MdContent, MdIterator, MdSection, Modifier, Span, TableAlignment,
},
wrap::{wrap_md_spans, wrap_md_spans_lines},
};
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MdLineContainer {
Blockquote,
ListItem {
marker: ListMarker,
continuation: bool,
},
}
fn marker_width<M: Mapper>(marker: &ListMarker, mapper: &M) -> usize {
match marker {
ListMarker::Unordered(b) => mapper.unordered_bullet(*b).width(),
ListMarker::Ordered(n) => mapper.ordered_marker(*n).width(),
ListMarker::TaskChecked(b) => {
mapper.unordered_bullet(*b).width() + mapper.task_checked().width()
}
ListMarker::TaskUnchecked(b) => {
mapper.unordered_bullet(*b).width() + mapper.task_unchecked().width()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum BorderPosition {
Top,
HeaderSeparator,
Bottom,
}
pub struct LineIterator<'a, M: Mapper> {
inner: Peekable<MdIterator<'a>>,
width: u16,
mapper: &'a M,
pending_lines: VecDeque<Line>,
needs_blank: bool,
prev_nesting: Vec<MdContainer>,
prev_was_blank: bool,
prev_in_list: bool,
}
impl<'a, M: Mapper> LineIterator<'a, M> {
pub(crate) fn new(inner: MdIterator<'a>, width: u16, mapper: &'a M) -> Self {
LineIterator {
inner: inner.peekable(),
width,
mapper,
pending_lines: VecDeque::new(),
needs_blank: false,
prev_nesting: Vec::new(),
prev_was_blank: false,
prev_in_list: false,
}
}
fn process_next_section(&mut self) -> bool {
let section = match self.inner.next() {
Some(s) => s,
None => return false,
};
let in_list = section
.nesting
.iter()
.any(|c| matches!(c, MdContainer::ListItem(_)));
let is_blank_line = section.content.is_blank();
let container_type_matches = |a: &MdContainer, b: &MdContainer| -> bool {
matches!(
(a, b),
(MdContainer::List(_), MdContainer::List(_))
| (MdContainer::ListItem(_), MdContainer::ListItem(_))
| (MdContainer::Blockquote(_), MdContainer::Blockquote(_))
)
};
let is_type_prefix = |shorter: &[MdContainer], longer: &[MdContainer]| -> bool {
!shorter.is_empty()
&& shorter.len() < longer.len()
&& shorter
.iter()
.zip(longer.iter())
.all(|(a, b)| container_type_matches(a, b))
};
let nesting_change = is_type_prefix(&self.prev_nesting, §ion.nesting)
|| is_type_prefix(§ion.nesting, &self.prev_nesting);
let list_depth = |nesting: &[MdContainer]| -> usize {
nesting
.iter()
.filter(|c| matches!(c, MdContainer::List(_)))
.count()
};
let curr_list_depth = list_depth(§ion.nesting);
let prev_list_depth = list_depth(&self.prev_nesting);
let same_top_level_list =
if in_list && self.prev_in_list && curr_list_depth == 1 && prev_list_depth == 1 {
let curr_list = section
.nesting
.iter()
.find(|c| matches!(c, MdContainer::List(_)));
let prev_list = self
.prev_nesting
.iter()
.find(|c| matches!(c, MdContainer::List(_)));
curr_list == prev_list
} else {
false
};
let same_nested_context =
in_list && self.prev_in_list && curr_list_depth > 1 && prev_list_depth > 1;
let same_list_context = same_top_level_list || same_nested_context;
let exiting_to_new_top_level =
nesting_change && curr_list_depth == 1 && prev_list_depth > 1 && {
let curr_first_list = section
.nesting
.iter()
.find(|c| matches!(c, MdContainer::List(_)));
let prev_first_list = self
.prev_nesting
.iter()
.find(|c| matches!(c, MdContainer::List(_)));
curr_first_list != prev_first_list
};
let should_emit_blank = self.needs_blank
&& (!same_list_context || section.is_list_continuation)
&& !is_blank_line
&& !self.prev_was_blank
&& (!nesting_change || exiting_to_new_top_level);
if should_emit_blank {
self.pending_lines.push_back(Line {
spans: Vec::new(),
kind: LineKind::Blank,
urls: Vec::new(),
});
}
self.needs_blank = !matches!(section.content, MdContent::Header { .. });
self.prev_nesting.clone_from(§ion.nesting);
self.prev_was_blank = is_blank_line;
self.prev_in_list = in_list;
let lines = section_to_lines(self.width, §ion, self.mapper);
self.pending_lines.extend(lines);
true
}
}
impl<M: Mapper> Iterator for LineIterator<'_, M> {
type Item = Line;
fn next(&mut self) -> Option<Self::Item> {
if let Some(line) = self.pending_lines.pop_front() {
return Some(line);
}
while self.process_next_section() {
if let Some(line) = self.pending_lines.pop_front() {
return Some(line);
}
}
None
}
}
fn section_to_lines<M: Mapper>(width: u16, section: &MdSection, mapper: &M) -> Vec<Line> {
let nesting = convert_nesting(§ion.nesting, section.is_list_continuation);
match §ion.content {
MdContent::Paragraph(p) if p.is_empty() => {
vec![Line {
spans: Vec::new(),
kind: LineKind::Blank,
urls: Vec::new(),
}]
}
MdContent::Paragraph(p) => {
let decorated_spans = apply_decorators(p.spans.clone(), mapper);
let prefix_width: usize = nesting
.iter()
.map(|c| match c {
MdLineContainer::Blockquote => mapper.blockquote_bar().width(),
MdLineContainer::ListItem { marker, .. } => marker_width(marker, mapper),
})
.sum();
let wrapped_lines =
wrap_md_spans(width, decorated_spans, prefix_width, mapper.hide_urls());
wrapped_to_lines(wrapped_lines, nesting, mapper)
}
MdContent::Header { tier, text } => {
if mapper.has_text_size_protocol() {
let spans = vec![Span::from(text.clone())];
let (n, d) = match tier {
1 => (7, 7),
2 => (5, 6),
3 => (3, 4),
4 => (2, 3),
5 => (3, 5),
_ => (1, 3),
};
let scaled_width = width / 2 * d / n;
let wrapped = wrap_md_spans(scaled_width, spans, 0, false);
wrapped
.into_iter()
.map(|line| Line {
spans: line.spans,
kind: LineKind::Header(*tier),
urls: Vec::new(), })
.collect()
} else {
vec![Line {
spans: vec![Span::from(text.clone())],
kind: LineKind::Header(*tier),
urls: Vec::new(), }]
}
}
MdContent::CodeBlock { language, code } => {
code_block_to_lines(width, language, code, nesting, mapper)
}
MdContent::HorizontalRule => {
let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
let available = (width as usize).saturating_sub(prefix_width);
let mut spans = prefix_spans;
spans.push(Span::new(
mapper.horizontal_rule_char().repeat(available),
Modifier::HorizontalRule,
));
vec![Line {
spans,
kind: LineKind::HorizontalRule,
urls: Vec::new(),
}]
}
MdContent::Table {
header,
rows,
alignments,
} => table_to_lines(width, header, rows, alignments, nesting, mapper),
MdContent::Html { html } => html
.split("\n")
.map(|linestr| Line {
spans: vec![Span::from(linestr.to_owned())],
kind: LineKind::Paragraph,
urls: Vec::new(),
})
.collect(),
}
}
fn apply_decorators<M: Mapper>(spans: Vec<Span>, mapper: &M) -> Vec<Span> {
let mut result: Vec<Span> = Vec::with_capacity(spans.len() * 2);
let mut prev_emphasis = false;
let mut prev_strong = false;
let mut prev_code = false;
let mut prev_strikethrough = false;
for mut span in spans {
let has_emphasis = span.modifiers.contains(Modifier::Emphasis);
let has_strong = span.modifiers.contains(Modifier::StrongEmphasis);
let has_code = span.modifiers.contains(Modifier::Code);
let has_strikethrough = span.modifiers.contains(Modifier::Strikethrough);
let is_newline = span.modifiers.contains(Modifier::NewLine);
if is_newline {
if let Some(last) = result.last_mut() {
last.content.truncate(last.content.trim_end().len());
}
}
if prev_code && !has_code {
let close = mapper.code_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::CodeWrapper));
}
}
if prev_strikethrough && !has_strikethrough {
let close = mapper.strikethrough_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::StrikethroughWrapper));
}
}
if prev_strong && !has_strong {
let close = mapper.strong_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::StrongEmphasisWrapper));
}
}
if prev_emphasis && !has_emphasis {
let close = mapper.emphasis_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::EmphasisWrapper));
}
}
let mut newline_transferred = false;
if has_emphasis && !prev_emphasis {
let open = mapper.emphasis_open();
if !open.is_empty() {
let mods = if is_newline && !newline_transferred {
newline_transferred = true;
Modifier::EmphasisWrapper | Modifier::NewLine
} else {
Modifier::EmphasisWrapper
};
result.push(Span::new(open.to_owned(), mods));
}
}
if has_strong && !prev_strong {
let open = mapper.strong_open();
if !open.is_empty() {
let mods = if is_newline && !newline_transferred {
newline_transferred = true;
Modifier::StrongEmphasisWrapper | Modifier::NewLine
} else {
Modifier::StrongEmphasisWrapper
};
result.push(Span::new(open.to_owned(), mods));
}
}
if has_strikethrough && !prev_strikethrough {
let open = mapper.strikethrough_open();
if !open.is_empty() {
let mods = if is_newline && !newline_transferred {
newline_transferred = true;
Modifier::StrikethroughWrapper | Modifier::NewLine
} else {
Modifier::StrikethroughWrapper
};
result.push(Span::new(open.to_owned(), mods));
}
}
if has_code && !prev_code {
let open = mapper.code_open();
if !open.is_empty() {
let mods = if is_newline && !newline_transferred {
newline_transferred = true;
Modifier::CodeWrapper | Modifier::NewLine
} else {
Modifier::CodeWrapper
};
result.push(Span::new(open.to_owned(), mods));
}
}
if newline_transferred {
span.modifiers.remove(Modifier::NewLine);
}
if span.modifiers.contains(Modifier::LinkDescriptionWrapper) {
span.content = if span.content == "[" {
mapper.link_desc_open().to_owned()
} else {
mapper.link_desc_close().to_owned()
};
} else if span.modifiers.contains(Modifier::LinkURLWrapper) {
span.content = if span.content == "(" {
mapper.link_url_open().to_owned()
} else {
mapper.link_url_close().to_owned()
};
}
let hide = mapper.hide_urls()
&& (span.modifiers.contains(Modifier::LinkURLWrapper)
&& !(span.modifiers.contains(Modifier::BareLink)
|| span.modifiers.contains(Modifier::Image)));
if !hide {
result.push(span);
} else {
result.push(Span {
content: String::new(),
modifiers: span.modifiers,
});
}
prev_emphasis = has_emphasis;
prev_strong = has_strong;
prev_code = has_code;
prev_strikethrough = has_strikethrough;
}
if prev_code {
let close = mapper.code_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::CodeWrapper));
}
}
if prev_strikethrough {
let close = mapper.strikethrough_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::StrikethroughWrapper));
}
}
if prev_strong {
let close = mapper.strong_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::StrongEmphasisWrapper));
}
}
if prev_emphasis {
let close = mapper.emphasis_close();
if !close.is_empty() {
result.push(Span::new(close.to_owned(), Modifier::EmphasisWrapper));
}
}
result
}
fn nesting_to_prefix_spans<M: Mapper>(nesting: &[MdLineContainer], mapper: &M) -> Vec<Span> {
let mut spans = Vec::new();
let last_list_idx = nesting
.iter()
.rposition(|c| matches!(c, MdLineContainer::ListItem { .. }));
for (i, container) in nesting.iter().enumerate() {
match container {
MdLineContainer::Blockquote => {
spans.push(Span::new(
mapper.blockquote_bar().to_owned(),
Modifier::BlockquoteBar,
));
}
MdLineContainer::ListItem {
marker,
continuation,
} => {
if Some(i) == last_list_idx && !*continuation {
let marker_text = match marker {
ListMarker::Unordered(b) => mapper.unordered_bullet(*b).to_owned(),
ListMarker::Ordered(n) => mapper.ordered_marker(*n),
ListMarker::TaskChecked(b) => {
format!("{}{}", mapper.unordered_bullet(*b), mapper.task_checked())
}
ListMarker::TaskUnchecked(b) => {
format!("{}{}", mapper.unordered_bullet(*b), mapper.task_unchecked())
}
};
spans.push(Span::new(marker_text, Modifier::ListMarker));
} else {
let indent_width = marker_width(marker, mapper);
spans.push(Span::new(" ".repeat(indent_width), Modifier::empty()));
}
}
}
}
spans
}
fn convert_nesting(md_nesting: &[MdContainer], is_list_continuation: bool) -> Vec<MdLineContainer> {
let last_list_item_idx = md_nesting
.iter()
.rposition(|c| matches!(c, MdContainer::ListItem(_)));
md_nesting
.iter()
.enumerate()
.filter_map(|(idx, c)| match c {
MdContainer::Blockquote(_) => Some(MdLineContainer::Blockquote),
MdContainer::ListItem(marker) => {
let continuation = is_list_continuation && last_list_item_idx == Some(idx);
Some(MdLineContainer::ListItem {
marker: marker.clone(),
continuation,
})
}
MdContainer::List(_) => None, })
.collect()
}
fn code_block_to_lines<M: Mapper>(
width: u16,
language: &str,
code: &str,
nesting: Vec<MdLineContainer>,
mapper: &M,
) -> Vec<Line> {
let code_lines: Vec<&str> = code.lines().collect();
let num_lines = code_lines.len();
if num_lines == 0 {
return vec![];
}
let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
let available_width = (width as usize).saturating_sub(prefix_width).max(1);
let mut result = Vec::new();
for line in code_lines {
let line_width = line.width();
if line_width > available_width {
let options = Options::new(available_width)
.break_words(true)
.word_splitter(textwrap::word_splitters::WordSplitter::NoHyphenation);
let parts: Vec<_> = wrap(line, options).into_iter().collect();
for part in parts {
let content_width = part.width();
let padding = available_width.saturating_sub(content_width);
let mut spans = prefix_spans.clone();
spans.push(Span::new(part.into_owned(), Modifier::Code));
if padding > 0 {
spans.push(Span::new(" ".repeat(padding), Modifier::Code));
}
result.push(Line {
spans,
kind: LineKind::CodeBlock {
language: language.to_owned(),
},
urls: Vec::new(),
});
}
} else {
let padding = available_width.saturating_sub(line_width);
let mut spans = prefix_spans.clone();
spans.push(Span::new(line.to_owned(), Modifier::Code));
if padding > 0 {
spans.push(Span::new(" ".repeat(padding), Modifier::Code));
}
result.push(Line {
spans,
kind: LineKind::CodeBlock {
language: language.to_owned(),
},
urls: Vec::new(),
});
}
}
result
}
fn wrapped_to_lines<M: Mapper>(
wrapped_lines: Vec<Line>,
nesting: Vec<MdLineContainer>,
mapper: &M,
) -> Vec<Line> {
let mut lines = Vec::new();
for (line_idx, wrapped_line) in wrapped_lines.into_iter().enumerate() {
let has_content = wrapped_line
.spans
.iter()
.any(|s| !s.content.trim().is_empty());
if !has_content && wrapped_line.urls.is_empty() {
continue;
}
let line_nesting = if line_idx == 0 {
&nesting
} else {
&nesting
.iter()
.map(|c| match c {
MdLineContainer::Blockquote => MdLineContainer::Blockquote,
MdLineContainer::ListItem { marker, .. } => MdLineContainer::ListItem {
marker: marker.clone(),
continuation: true,
},
})
.collect()
};
let is_only_image = wrapped_line.spans.len() == 5
&& (wrapped_line.spans[0].modifiers == Modifier::Image
|| wrapped_line.spans[0].modifiers == Modifier::Image | Modifier::NewLine)
&& wrapped_line.spans[0].content == "!["
&& wrapped_line.spans[1].modifiers == (Modifier::Image | Modifier::LinkDescription)
&& wrapped_line.spans[2].modifiers == Modifier::Image
&& wrapped_line.spans[2].content == "]("
&& wrapped_line.spans[3].modifiers == (Modifier::Image | Modifier::LinkURL)
&& wrapped_line.spans[4].modifiers == Modifier::Image
&& wrapped_line.spans[4].content == ")";
let mut image_lines = Vec::new();
for tracked_url in &wrapped_line.urls {
if let TrackedUrl::Image { desc, url } = tracked_url {
let spans = vec![
Span::new(
"![".to_owned(),
Modifier::Image | Modifier::LinkDescriptionWrapper,
),
if is_only_image {
Span::new(desc.clone(), Modifier::Image | Modifier::LinkDescription)
} else {
Span::new(
"Loading...".to_owned(),
Modifier::Image | Modifier::LinkDescription,
)
},
Span::new(
"]".to_owned(),
Modifier::Image | Modifier::LinkDescriptionWrapper,
),
Span::new("(".to_owned(), Modifier::Image | Modifier::LinkURLWrapper),
Span::new(url.clone(), Modifier::Image | Modifier::LinkURL),
Span::new(")".to_owned(), Modifier::Image | Modifier::LinkURLWrapper),
];
image_lines.push(Line {
spans,
kind: LineKind::Image(MarkdownLink {
url: url.clone(),
description: desc.clone(),
}),
urls: vec![TrackedUrl::Image {
desc: desc.clone(),
url: url.clone(),
}],
});
}
}
if !is_only_image && !wrapped_line.spans.is_empty() {
let mut spans = nesting_to_prefix_spans(line_nesting, mapper);
spans.extend(wrapped_line.spans);
lines.push(Line {
spans,
kind: LineKind::Paragraph,
urls: wrapped_line.urls,
});
}
lines.extend(image_lines);
}
lines
}
fn table_to_lines<M: Mapper>(
width: u16,
header: &[Vec<Span>],
rows: &[Vec<Vec<Span>>],
alignments: &[TableAlignment],
nesting: Vec<MdLineContainer>,
mapper: &M,
) -> Vec<Line> {
let mut lines = Vec::new();
let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
let available_width = (width as usize).saturating_sub(prefix_width);
let num_cols = header.len();
if num_cols == 0 {
return lines;
}
let cell_width = |cell: &[Span]| -> usize { cell.iter().map(|n| n.content.width()).sum() };
let mut col_widths: Vec<usize> = header.iter().map(|c| cell_width(c)).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(cell_width(cell));
}
}
}
let col_widths: Vec<usize> = col_widths.iter().map(|w| w + 2).collect();
let table_width: usize = col_widths.iter().sum::<usize>() + num_cols + 1;
let col_widths: Vec<usize> = if table_width > available_width && available_width > num_cols + 1
{
let content_width = available_width - num_cols - 1;
let total_content: usize = col_widths.iter().sum();
col_widths
.iter()
.map(|w| (w * content_width / total_content).max(3))
.collect()
} else {
col_widths
};
let build_border = |position: BorderPosition| -> Line {
let (left, mid, right) = match position {
BorderPosition::Top => (
mapper.table_top_left(),
mapper.table_top_junction(),
mapper.table_top_right(),
),
BorderPosition::HeaderSeparator => (
mapper.table_left_junction(),
mapper.table_cross(),
mapper.table_right_junction(),
),
BorderPosition::Bottom => (
mapper.table_bottom_left(),
mapper.table_bottom_junction(),
mapper.table_bottom_right(),
),
};
let horizontal = mapper.table_horizontal();
let mut spans = prefix_spans.clone();
spans.push(Span::new(left.to_owned(), Modifier::TableBorder));
for (i, &col_w) in col_widths.iter().enumerate() {
spans.push(Span::new(horizontal.repeat(col_w), Modifier::TableBorder));
if i < num_cols - 1 {
spans.push(Span::new(mid.to_owned(), Modifier::TableBorder));
}
}
spans.push(Span::new(right.to_owned(), Modifier::TableBorder));
Line {
spans,
kind: LineKind::TableBorder,
urls: Vec::new(),
}
};
let build_row_lines = |row: &[Vec<Span>], is_header: bool| -> Vec<Line> {
let vertical = mapper.table_vertical();
let wrapped_cells: Vec<Vec<Vec<Span>>> = row
.iter()
.enumerate()
.map(|(i, cell)| {
let col_width = col_widths.get(i).copied().unwrap_or(3);
let inner_width = col_width.saturating_sub(2).max(1) as u16;
let decorated = apply_decorators(cell.clone(), mapper);
let wrapped = wrap_md_spans_lines(inner_width, decorated, mapper.hide_urls());
if wrapped.is_empty() {
vec![Vec::new()]
} else {
wrapped
}
})
.collect();
let max_lines = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
let mut result = Vec::new();
for line_idx in 0..max_lines {
let mut spans = prefix_spans.clone();
spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));
for (i, col_width) in col_widths.iter().enumerate() {
let alignment = alignments.get(i).copied().unwrap_or(TableAlignment::Left);
let cell_spans = wrapped_cells
.get(i)
.and_then(|c| c.get(line_idx))
.map_or(&[][..], |v| v.as_slice());
let content_width: usize = cell_spans.iter().map(|s| s.content.width()).sum();
let inner_width = col_width.saturating_sub(2);
let padding_total = inner_width.saturating_sub(content_width);
let (left_pad, right_pad) = match alignment {
TableAlignment::Center => {
(padding_total / 2, padding_total - padding_total / 2)
}
TableAlignment::Right => (padding_total, 0),
TableAlignment::Left => (0, padding_total),
};
spans.push(Span::new(
format!(" {}", " ".repeat(left_pad)),
Modifier::empty(),
));
spans.extend(cell_spans.iter().cloned());
spans.push(Span::new(
format!("{} ", " ".repeat(right_pad)),
Modifier::empty(),
));
spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));
}
for i in row.len()..num_cols {
let col_width = col_widths.get(i).copied().unwrap_or(3);
spans.push(Span::new(" ".repeat(col_width), Modifier::empty()));
spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));
}
result.push(Line {
spans,
kind: LineKind::TableRow { is_header },
urls: Vec::new(),
});
}
result
};
lines.push(build_border(BorderPosition::Top));
lines.extend(build_row_lines(header, true));
lines.push(build_border(BorderPosition::HeaderSeparator));
for row in rows {
lines.extend(build_row_lines(row, false));
}
lines.push(build_border(BorderPosition::Bottom));
lines
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::DefaultMapper;
use super::*;
use pretty_assertions::assert_eq;
use tree_sitter::Parser;
fn make_parser() -> Parser {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_md::LANGUAGE.into())
.unwrap();
parser
}
fn make_inline_parser() -> Parser {
let mut inline_parser = Parser::new();
inline_parser
.set_language(&tree_sitter_md::INLINE_LANGUAGE.into())
.unwrap();
inline_parser
}
#[test]
fn line_iterator_clean_links() {
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let source = "[link](http://example.com)\n";
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
struct HideUrlsMapper;
impl Mapper for HideUrlsMapper {
fn link_desc_open(&self) -> &str {
""
}
fn link_desc_close(&self) -> &str {
""
}
fn hide_urls(&self) -> bool {
true
}
}
let line_iter = LineIterator::new(iter, 80, &HideUrlsMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(
lines[0],
Line {
spans: vec![
Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
Span::with("link", Modifier::Link | Modifier::LinkDescription),
Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
Span::with("", Modifier::Link | Modifier::LinkURLWrapper,),
Span::with("http://example.com", Modifier::Link | Modifier::LinkURL),
Span::with("", Modifier::Link | Modifier::LinkURLWrapper,),
],
kind: LineKind::Paragraph,
urls: vec![TrackedUrl::link("http://example.com", 0, 4, 0)]
}
);
}
#[test]
fn line_iterator_clean_links_nested_image() {
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let source = "[](http://example.com)\n";
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
struct HideUrlsMapper;
impl Mapper for HideUrlsMapper {
fn link_desc_open(&self) -> &str {
""
}
fn link_desc_close(&self) -> &str {
""
}
fn hide_urls(&self) -> bool {
true
}
}
let line_iter = LineIterator::new(iter, 80, &HideUrlsMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(
lines[0],
Line {
spans: vec,
Span::with(
"http://example.com/img.png",
Modifier::Link
| Modifier::LinkDescription
| Modifier::Image
| Modifier::LinkURL,
),
Span::with(
")",
Modifier::Link | Modifier::LinkDescription | Modifier::Image
),
Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
Span::with("", Modifier::Link | Modifier::LinkURLWrapper),
Span::with("http://example.com", Modifier::Link | Modifier::LinkURL,),
Span::with("", Modifier::Link | Modifier::LinkURLWrapper),
],
kind: LineKind::Paragraph,
urls: vec![
TrackedUrl::image("image", "http://example.com/img.png"),
TrackedUrl::link("http://example.com", 0, 36, 0),
],
}
);
}
#[test]
fn bold_italics_cause_line_wrap_bugfix() {
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let source = r#"
I have searched far **and** wide but I have *yet* to come across a show that would leave me so emotionally invested and at the end devastated. The reasons for this I have yet to understand and this blog post is meant to explore them. Who knows where my keyboard will take us."#;
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
let line_iter = LineIterator::new(iter, 80, &DefaultMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(4, lines.len());
assert_eq!(
lines[0],
Line {
spans: vec![
Span::with("I have searched far ", Modifier::default()),
Span::with("**", Modifier::StrongEmphasisWrapper),
Span::with("and", Modifier::StrongEmphasis),
Span::with("**", Modifier::StrongEmphasisWrapper),
Span::with(" wide but I have ", Modifier::default()),
Span::with("*", Modifier::EmphasisWrapper),
Span::with("yet", Modifier::Emphasis),
Span::with("*", Modifier::EmphasisWrapper),
Span::with(" to come across a show that", Modifier::default()),
],
kind: LineKind::Paragraph,
urls: Vec::new(),
}
);
assert_eq!(
lines[1],
Line {
spans: vec![Span::with(
"would leave me so emotionally invested and at the end devastated. The reasons",
Modifier::default()
),],
kind: LineKind::Paragraph,
urls: Vec::new(),
}
);
assert_eq!(
lines[2],
Line {
spans: vec![Span::with(
"for this I have yet to understand and this blog post is meant to explore them.",
Modifier::default()
),],
kind: LineKind::Paragraph,
urls: Vec::new(),
}
);
assert_eq!(
lines[3],
Line {
spans: vec![Span::with(
"Who knows where my keyboard will take us.",
Modifier::default()
),],
kind: LineKind::Paragraph,
urls: Vec::new(),
}
);
}
#[test]
fn long_line_link_wrapping() {
let source = "blalalalallalabbalallalaa [](https://repology.org/project/mdfried/versions)";
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(
vec]",
"",
"(https://repology.org/project/mdfried/versions)",
],
Line::to_strings(&lines),
);
}
#[test]
#[ignore]
fn long_url_link_wrapping() {
let source = "[](https://repology.org/project/mdfried/versions)";
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(
vec]",
"",
"(https://repology.org/project/mdfried/versions)",
],
Line::to_strings(&lines),
);
}
#[test]
fn is_only_image() {
let source = "";
let mut parser = make_parser();
let mut inline_parser = make_inline_parser();
let tree = parser.parse(source, None).unwrap();
let iter = MdIterator::new(tree, &mut inline_parser, source);
let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
let lines: Vec<Line> = line_iter.collect();
assert_eq!(
vec"],
Line::to_strings(&lines),
);
}
}