use crate::ast::*;
use crate::printer::markdown_syntax_detector::is_safe_line_break_before;
use pretty::{Arena, DocAllocator, DocBuilder};
pub(crate) trait ToDocInline<'a> {
fn to_doc_inline(
&self,
allow_newlines: bool,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()>;
}
impl<'a> ToDocInline<'a> for Vec<Inline> {
fn to_doc_inline(
&self,
allow_newlines: bool,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
arena.concat(
self.iter()
.map(|inline| inline.to_doc_inline(allow_newlines, arena))
.collect::<Vec<_>>(),
)
}
}
impl<'a> ToDocInline<'a> for Inline {
fn to_doc_inline(
&self,
allow_newlines: bool,
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
match self {
Inline::Text(t) => {
let t = t.replace('\n', " ");
let words_or_spaces: Vec<_> = split_with_spaces(&t);
if !allow_newlines {
let words_or_spaces = words_or_spaces.into_iter().map(|v| match v {
Some(v) => arena.text(v.to_string()),
None => arena.space(),
});
arena.concat(words_or_spaces)
} else {
safe_text_layout(&words_or_spaces, arena)
}
}
Inline::LineBreak => arena.text(" \n"),
Inline::Code(code) => arena.text("`").append(code.clone()).append(arena.text("`")),
Inline::Html(html) => arena.text(html.clone()),
Inline::Emphasis(children) => arena
.text("*")
.append(children.to_doc_inline(allow_newlines, arena))
.append(arena.text("*")),
Inline::Strong(children) => arena
.text("**")
.append(children.to_doc_inline(allow_newlines, arena))
.append(arena.text("**")),
Inline::Strikethrough(children) => arena
.text("~~")
.append(children.to_doc_inline(allow_newlines, arena))
.append(arena.text("~~")),
Inline::Link(Link {
destination,
title,
children,
}) => {
let title = arena.text(link_title_suffix(title.as_ref()));
arena
.text("[")
.append(children.to_doc_inline(allow_newlines, arena))
.append(arena.text("]("))
.append(arena.text(link_destination_to_string(destination)))
.append(title)
.append(")")
}
Inline::Image(Image {
destination,
title,
alt,
}) => {
let title_part = link_title_suffix(title.as_ref());
arena
.text("
.append(arena.text(link_destination_to_string(destination)))
.append(arena.text(title_part))
.append(arena.text(")"))
}
Inline::Autolink(link) => arena.text(format!("<{link}>")),
Inline::FootnoteReference(label) => arena.text(format!("[^{label}]")),
Inline::Empty => arena.nil(),
Inline::LinkReference(v) => {
if v.label == v.text {
return arena
.text("[")
.append(v.label.to_doc_inline(allow_newlines, arena))
.append("]");
}
arena
.text("[")
.append(v.text.to_doc_inline(allow_newlines, arena))
.append("][")
.append(v.label.to_doc_inline(allow_newlines, arena))
.append(arena.text("]"))
}
}
}
}
pub(crate) fn link_destination_to_string(destination: &str) -> String {
if is_bare_destination(destination) {
return destination.to_owned();
}
let mut out = String::with_capacity(destination.len() + 2);
out.push('<');
for c in destination.chars() {
if c == '<' || c == '>' {
out.push('\\');
}
out.push(c);
}
out.push('>');
out
}
fn is_bare_destination(destination: &str) -> bool {
if destination.is_empty() {
return false;
}
let mut depth = 0usize;
let mut chars = destination.chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
if chars.next().is_none() {
return false;
}
}
'(' => depth += 1,
')' => match depth.checked_sub(1) {
Some(v) => depth = v,
None => return false,
},
c if c.is_whitespace() || c.is_control() || c == '<' => return false,
_ => {}
}
}
depth == 0
}
pub(crate) fn link_title_suffix(title: Option<&String>) -> String {
let Some(title) = title else {
return String::new();
};
let mut out = String::with_capacity(title.len() + 3);
out.push_str(" \"");
for c in title.chars() {
if c == '\\' || c == '"' {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
fn escape_image_alt(alt: &str) -> String {
let mut out = String::with_capacity(alt.len());
for c in alt.chars() {
if c == '\\' || c == ']' {
out.push('\\');
}
out.push(c);
}
out
}
fn split_with_spaces(s: &str) -> Vec<Option<&str>> {
let mut result = Vec::new();
let mut word_start: Option<usize> = None;
for (i, c) in s.char_indices() {
if c.is_whitespace() {
if let Some(start) = word_start {
result.push(Some(&s[start..i]));
word_start = None;
}
if result.last().is_none_or(|x| x.is_some()) {
result.push(None);
}
} else if word_start.is_none() {
word_start = Some(i);
}
}
if let Some(start) = word_start {
result.push(Some(&s[start..]));
}
result
}
fn safe_text_layout<'a>(
words_or_spaces: &[Option<&str>],
arena: &'a Arena<'a>,
) -> DocBuilder<'a, Arena<'a>, ()> {
if words_or_spaces.is_empty() {
return arena.nil();
}
let mut result = arena.nil();
let mut i = 0;
while i < words_or_spaces.len() {
match words_or_spaces[i] {
Some(word) => {
result = result.append(arena.text(word.to_string()));
i += 1;
}
None => {
let next_word = find_next_word(&words_or_spaces[i + 1..]);
let separator = if let Some(next_word) = next_word {
if is_safe_line_break_before(next_word, &[]) {
arena.softline()
} else {
arena.space()
}
} else {
arena.softline()
};
result = result.append(separator);
i += 1;
}
}
}
result
}
fn find_next_word<'a>(words_or_spaces: &'a [Option<&'a str>]) -> Option<&'a str> {
words_or_spaces.iter().flatten().next().copied()
}