mod code_block;
mod inline;
mod style;
mod table;
mod writer;
use pulldown_cmark::{Options, Parser};
use ratatui::text::{Line, Span};
use std::ops::Range;
use unicode_width::UnicodeWidthStr;
use writer::Writer;
pub fn render_markdown(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
) -> Vec<Line<'static>> {
render_markdown_with_text(input, default_code_lang, wrap_width).lines
}
pub(super) fn render_markdown_with_text(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
) -> RenderedMarkdown {
render_markdown_document(input, default_code_lang, wrap_width, |_| {
LinkDisplay::LabelAndDestination
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum LinkDisplay {
LabelOnly,
LabelAndDestination,
}
pub(super) fn render_markdown_document(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
link_display: impl Fn(&str) -> LinkDisplay,
) -> RenderedMarkdown {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(input, options);
let mut writer = Writer::new(
parser,
default_code_lang.map(|s| s.to_string()),
wrap_width,
&link_display,
);
writer.run();
let mut document = RenderedMarkdown {
lines: writer.lines,
text: writer.text,
links: writer.links,
};
document.project_links();
document
}
pub(super) struct RenderedMarkdown {
pub lines: Vec<Line<'static>>,
pub text: Vec<RenderedText>,
pub links: Vec<RenderedLink>,
}
pub(super) struct RenderedLink {
pub destination: String,
pub label: String,
pub text_ranges: Vec<LinkTextRange>,
pub fragments: Vec<LinkFragment>,
}
pub(super) struct LinkTextRange {
pub text_index: usize,
pub range: Range<usize>,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct LinkFragment {
pub line: usize,
pub byte_range: Range<usize>,
pub columns: Range<usize>,
}
impl RenderedMarkdown {
fn project_links(&mut self) {
for link in &mut self.links {
for label in &link.text_ranges {
for fragment in &self.text[label.text_index].fragments {
let start = label.range.start.max(fragment.source_range.start);
let end = label.range.end.min(fragment.source_range.end);
if start >= end {
continue;
}
let start = fragment.visual_start + start - fragment.source_range.start;
let end = fragment.visual_start + end - fragment.source_range.start;
let line = &self.lines[fragment.line];
link.fragments.push(LinkFragment {
line: fragment.line,
byte_range: start..end,
columns: display_column(line, start)..display_column(line, end),
});
}
}
}
}
}
fn display_column(line: &Line<'_>, mut byte_offset: usize) -> usize {
let mut columns = 0;
for span in &line.spans {
let bytes = byte_offset.min(span.content.len());
columns += UnicodeWidthStr::width(&span.content[..bytes]);
byte_offset -= bytes;
if byte_offset == 0 {
break;
}
}
columns
}
pub(super) struct RenderedText {
pub text: String,
pub fragments: Vec<TextFragment>,
}
pub(super) struct TextFragment {
pub line: usize,
pub source_range: Range<usize>,
pub visual_start: usize,
}
#[derive(Clone)]
struct InlineSpan {
span: Span<'static>,
link: Option<usize>,
}
#[cfg(test)]
mod tests;