use super::*;
pub(super) fn link_display_inlines(inl: &Inline) -> Option<Vec<Inline>> {
match inl {
Inline::MdShortcutLink { display } | Inline::MdRefLink { display, .. } => {
Some(display.clone())
}
Inline::MdLink(raw) => {
let bytes = raw.as_bytes();
if bytes.first() == Some(&b'<') {
return None; }
let text_end = scan_delimited(bytes, 0, b'[', b']')?;
match bytes.get(text_end) {
Some(&b'(') => None, _ => Some(vec![Inline::Text(raw[1..text_end - 1].to_string())]),
}
}
_ => None,
}
}
pub(super) fn resolve_md_link(raw: &str) -> Option<String> {
let bytes = raw.as_bytes();
if bytes.first() == Some(&b'<') {
let inner = raw.strip_prefix('<')?.strip_suffix('>')?;
return Some(if autolink_has_uri_scheme(inner) {
url_atom(inner)
} else {
href_atom(inner, &format!("mailto:{inner}"))
});
}
let text_end = scan_delimited(bytes, 0, b'[', b']')?;
let text = &raw[1..text_end - 1];
match bytes.get(text_end) {
Some(&b'(') => {
let url_end = scan_delimited(bytes, text_end, b'(', b')')?;
(url_end == bytes.len()).then(|| {
inline_link_atom(
text,
&inline_link_destination(&raw[text_end + 1..url_end - 1]),
)
})
}
Some(&b'[') => {
let ref_end = scan_delimited(bytes, text_end, b'[', b']')?;
(ref_end == bytes.len()).then(|| ref_link_atom(text, &raw[text_end + 1..ref_end - 1]))
}
None => Some(shortcut_link_atom(text)),
_ => None,
}
}
pub(super) fn inline_link_dest(close: &str) -> String {
let content = close
.strip_prefix("](")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or("");
inline_link_destination(content)
}
fn inline_link_destination(content: &str) -> String {
let rest = content.trim_start();
let url = if let Some(r) = rest.strip_prefix('<') {
match r.find('>') {
Some(close) => &r[..close],
None => rest, }
} else {
let end = rest
.find(|c: char| c.is_ascii_whitespace())
.unwrap_or(rest.len());
&rest[..end]
};
decode_html_entities(url)
}
pub(super) fn inline_link_node_atom(url: &str, display: &[Inline], md: bool) -> String {
let display_text = inline_plain_text(display);
if url.is_empty() || norm_ws(url) == norm_ws(&display_text) {
return url_atom(&display_text);
}
let arg = grp_arg(&serialize_inlines(display, md));
format!("(\\href (VERB {}){})", encode_text(url), prefix_space(&arg))
}
pub(super) fn ref_link_node_atom(display: &[Inline], dest: &str) -> String {
let display_text = inline_plain_text(display);
if norm_ws(&display_text) == norm_ws(dest) {
return shortcut_link_atom(dest);
}
if display_has_macro(display) {
return link_over_display(display);
}
let (inner, is_code) = match display {
[Inline::MdCode(content)] => (content.clone(), true),
_ => (display_text, false),
};
code_wrap(
format!("(\\link {})", text_atom(&inner).unwrap_or_default()),
is_code,
)
}
fn display_has_macro(display: &[Inline]) -> bool {
display.iter().any(|inl| matches!(inl, Inline::Macro(_)))
}
fn link_over_display(display: &[Inline]) -> String {
let body = serialize_inlines(display, true).join(" ");
format!("(\\link {body})")
}
pub(super) fn shortcut_link_node_atom(display: &[Inline]) -> String {
match display {
[Inline::MdCode(content)] => shortcut_link_atom(&format!("`{content}`")),
_ if display_has_macro(display) => link_over_display(display),
_ => shortcut_link_atom(&inline_plain_text(display)),
}
}
pub(super) fn link_display_is_droppable(display: &[Inline]) -> bool {
if matches!(display, [Inline::MdCode(_)]) {
return false;
}
!display.iter().all(|inl| match inl {
Inline::Text(_) => true,
Inline::Macro(n) => !macro_arg_has_active_markdown(n),
_ => false,
})
}
fn macro_arg_has_active_markdown(node: &SyntaxNode) -> bool {
let head = macro_head(node);
let name = head.trim_start_matches('\\');
is_md_inline_text_macro(name)
&& macro_single_arg_content(node).is_some_and(|content| {
inlines_have_active_markdown(&resolve_macro_arg_inlines(&content))
})
}
fn inlines_have_active_markdown(inlines: &[Inline]) -> bool {
inlines.iter().any(|inl| match inl {
Inline::Text(_) => false,
Inline::Macro(n) => macro_arg_has_active_markdown(n),
_ => true,
})
}
pub(super) fn inline_plain_text(inlines: &[Inline]) -> String {
let mut s = String::new();
for inl in inlines {
match inl {
Inline::Text(t) => s.push_str(t),
Inline::MdCode(t) => s.push_str(t),
Inline::MdEmphasis { children, .. } => s.push_str(&inline_plain_text(children)),
Inline::MdInlineLink { display, .. } => s.push_str(&inline_plain_text(display)),
Inline::MdShortcutLink { display } => s.push_str(&inline_plain_text(display)),
_ => {}
}
}
s
}
pub(super) fn link_label_text(inlines: &[Inline]) -> String {
let mut s = String::new();
for inl in inlines {
match inl {
Inline::Text(t) => s.push_str(t),
Inline::MdCode(t) => s.push_str(t),
Inline::MdEmphasis { children, .. } => s.push_str(&link_label_text(children)),
Inline::MdInlineLink { display, .. } => s.push_str(&link_label_text(display)),
Inline::MdShortcutLink { display } => s.push_str(&link_label_text(display)),
Inline::Macro(n) => s.push_str(&n.text().to_string()),
_ => {}
}
}
s
}
fn inline_link_atom(text: &str, url: &str) -> String {
if url.is_empty() || norm_ws(url) == norm_ws(text) {
url_atom(text)
} else {
href_atom(text, url)
}
}
fn autolink_has_uri_scheme(inner: &str) -> bool {
let b = inner.as_bytes();
if !b.first().is_some_and(u8::is_ascii_alphabetic) {
return false;
}
let mut j = 1;
while j < b.len()
&& matches!(b[j], b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'.' | b'-')
{
j += 1;
}
(2..=32).contains(&j) && b.get(j) == Some(&b':')
}
fn url_atom(url: &str) -> String {
if url.is_empty() {
return "(\\url)".to_string();
}
format!("(\\url (VERB {}))", encode_text(url))
}
fn href_atom(text: &str, url: &str) -> String {
let mut atoms = vec![format!("(VERB {})", encode_text(url))];
if let Some(atom) = link_display_atom(text) {
atoms.push(atom);
}
format!("(\\href {})", atoms.join(" "))
}
fn link_display_atom(text: &str) -> Option<String> {
let (inner, is_code) = unwrap_code_span(text);
if is_code {
Some(md_code_atom(inner))
} else {
text_atom(text)
}
}
fn ref_link_atom(text: &str, dest: &str) -> String {
let (display, is_code) = unwrap_code_span(text);
if norm_ws(display) == norm_ws(dest) {
return shortcut_link_atom(dest);
}
code_wrap(
format!("(\\link {})", text_atom(display).unwrap_or_default()),
is_code,
)
}
pub(super) fn shortcut_link_atom(dest: &str) -> String {
let (dest, code_span) = unwrap_code_span(dest);
let is_code = code_span || dest.ends_with("()");
let (pkg, fun) = match dest.rsplit_once("::") {
Some((p, f)) => (Some(p), f),
None => (None, dest),
};
let s4 = dest.ends_with("-class");
let body = if s4 {
fun.strip_suffix("-class").unwrap_or(fun)
} else {
fun
};
let head = if s4 && pkg.is_none() {
"\\linkS4class"
} else {
"\\link"
};
let display = match pkg {
Some(p) => format!("{p}::{body}"),
None => body.to_string(),
};
code_wrap(
format!("({head} {})", text_atom(&display).unwrap_or_default()),
is_code,
)
}
pub(super) fn resolve_md_image(raw: &str) -> Option<String> {
let (url, title) = image_url_title(raw)?;
Some(figure_atom(&url, &title))
}
pub(super) fn resolve_md_image_demoted(raw: &str) -> Option<(&'static str, Vec<String>)> {
let (url, title) = image_url_title(raw)?;
let cond = match image_format(&url) {
ImageFormat::Html => "html",
ImageFormat::Pdf => "pdf",
ImageFormat::All => {
let mut args = vec![text_atom(&url).unwrap_or_default()];
if !title.is_empty() {
args.push(text_atom(&title).unwrap_or_default());
}
return Some(("figure", args));
}
};
Some((
"if",
vec![
format!("(TEXT {})", encode_text(cond)),
bare_figure_atom(&url, &title),
],
))
}
fn image_url_title(raw: &str) -> Option<(String, String)> {
let bytes = raw.as_bytes();
let alt_end = scan_delimited(bytes, 1, b'[', b']')?;
match bytes.get(alt_end) {
Some(&b'(') => {
let dest_end = scan_delimited(bytes, alt_end, b'(', b')')?;
if dest_end != bytes.len() {
return None;
}
let (url, title) = split_image_dest(&raw[alt_end + 1..dest_end - 1]);
Some((url.to_string(), title.to_string()))
}
Some(&b'[') => {
let ref_end = scan_delimited(bytes, alt_end, b'[', b']')?;
if ref_end != bytes.len() {
return None;
}
let label = &raw[alt_end + 1..ref_end - 1];
if label.is_empty() {
return None;
}
Some((synthesized_image_dest(label), String::new()))
}
None => Some((synthesized_image_dest(&raw[2..alt_end - 1]), String::new())),
_ => None,
}
}
pub(super) fn image_user_ref(raw: &str) -> Option<(&str, &str)> {
let bytes = raw.as_bytes();
let alt_end = scan_delimited(bytes, 1, b'[', b']')?;
let alt = &raw[1..alt_end]; match bytes.get(alt_end) {
Some(&b'[') => {
let ref_end = scan_delimited(bytes, alt_end, b'[', b']')?;
if ref_end != bytes.len() {
return None;
}
let label = &raw[alt_end + 1..ref_end - 1];
if label.is_empty() {
let alt_content = &raw[2..alt_end - 1];
return (!alt_content.is_empty()).then_some((alt_content, alt));
}
Some((label, alt))
}
None => {
let label = &raw[2..alt_end - 1];
(!label.is_empty()).then_some((label, alt))
}
_ => None,
}
}
pub(super) fn md_label_flatten(label: &str) -> String {
inline_plain_text(¶_to_inlines(&resolve_md_inline(label)))
}
fn synthesized_image_dest(label: &str) -> String {
format!("R:{}", url_encode(label))
}
fn split_image_dest(dest: &str) -> (&str, &str) {
let dest = dest.trim();
let (url, rest) = if dest.as_bytes().first() == Some(&b'<') {
match dest.find('>') {
Some(close) => (&dest[1..close], &dest[close + 1..]),
None => (dest, ""),
}
} else {
match dest.find(char::is_whitespace) {
Some(sp) => (&dest[..sp], &dest[sp..]),
None => (dest, ""),
}
};
(url, strip_title_delims(rest.trim()))
}
pub(super) fn strip_title_delims(s: &str) -> &str {
let b = s.as_bytes();
if b.len() >= 2
&& matches!(
(b[0], b[b.len() - 1]),
(b'"', b'"') | (b'\'', b'\'') | (b'(', b')')
)
{
&s[1..s.len() - 1]
} else {
s
}
}
fn figure_atom(url: &str, title: &str) -> String {
let figure = bare_figure_atom(url, title);
match image_format(url) {
ImageFormat::Html => format!("(\\if (TEXT {}) {figure})", encode_text("html")),
ImageFormat::Pdf => format!("(\\if (TEXT {}) {figure})", encode_text("pdf")),
ImageFormat::All => figure,
}
}
fn bare_figure_atom(url: &str, title: &str) -> String {
let mut args = vec![format!("(VERB {})", encode_text(url))];
if !title.is_empty() {
args.push(format!("(VERB {})", encode_text(title)));
}
format!("(\\figure {})", args.join(" "))
}
enum ImageFormat {
Html,
Pdf,
All,
}
fn image_format(url: &str) -> ImageFormat {
let lower = url.to_ascii_lowercase();
let has_dot_ext = |exts: &[&str]| {
exts.iter()
.any(|e| lower.strip_suffix(e).is_some_and(|p| p.ends_with('.')))
};
match (
has_dot_ext(&["jpg", "jpeg", "gif", "png", "svg"]),
has_dot_ext(&["jpg", "jpeg", "gif", "png", "pdf"]),
) {
(true, false) => ImageFormat::Html,
(false, true) => ImageFormat::Pdf,
_ => ImageFormat::All,
}
}
pub(super) fn scan_delimited(bytes: &[u8], start: usize, open: u8, close: u8) -> Option<usize> {
if bytes.get(start) != Some(&open) {
return None;
}
let mut depth = 0usize;
for (i, &b) in bytes.iter().enumerate().skip(start) {
if b == open {
depth += 1;
} else if b == close {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
}
None
}