use scraper::ElementRef;
use crate::context::Context;
use crate::converter::{Action, Rule};
use crate::dom;
use crate::options::{LinkReferenceStyle, LinkStyle};
#[derive(Debug, Clone, Copy)]
pub struct Link;
impl Rule for Link {
fn tags(&self) -> &'static [&'static str] {
&["a"]
}
fn apply(&self, content: &str, element: &ElementRef<'_>, ctx: &mut Context) -> Action {
let href = dom::attr(element, "href").unwrap_or("");
if href.is_empty() || href.trim() == "#" {
return Action::Replace(content.to_owned());
}
let absolute_href = ctx.resolve_url(href);
let escaped_content = escape_multiline(content);
let title = dom::attr(element, "title").map(|t| t.replace('\n', " ").replace('"', "\\\""));
let display = if escaped_content.trim().is_empty() {
let fallback = dom::attr(element, "title")
.or_else(|| dom::attr(element, "aria-label"))
.unwrap_or("")
.to_owned();
if fallback.is_empty() {
return Action::Replace(String::new());
}
fallback
} else {
escaped_content
};
let trimmed_display = display.trim();
if title.is_none()
&& trimmed_display.starts_with("")
}
LinkStyle::Referenced => {
build_reference_link(&display, &absolute_href, &title_part, ctx)
}
};
Action::Replace(dom::add_space_if_necessary(element, md))
}
}
fn build_reference_link(display: &str, href: &str, title_part: &str, ctx: &mut Context) -> String {
match ctx.options().link_reference_style {
LinkReferenceStyle::Full => {
let idx = ctx.link_index + 1;
ctx.push_reference(format!("[{idx}]: {href}{title_part}"));
format!("[{display}][{idx}]")
}
LinkReferenceStyle::Collapsed => {
ctx.push_reference(format!("[{display}]: {href}{title_part}"));
format!("[{display}][]")
}
LinkReferenceStyle::Shortcut => {
ctx.push_reference(format!("[{display}]: {href}{title_part}"));
format!("[{display}]")
}
}
}
fn escape_multiline(content: &str) -> String {
let trimmed = content.trim();
if !trimmed.contains('\n') {
return trimmed.to_owned();
}
trimmed.replace('\n', "\\\n")
}
fn extract_markdown_image_url(md: &str) -> Option<&str> {
let rest = md.strip_prefix("?;
let url_start = after_alt + 2;
let url_part = &rest[url_start..];
let end = url_part.find(')')?;
let url = url_part[..end].trim();
url.find([' ', '\t'])
.map_or(Some(url), |idx| Some(url[..idx].trim()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_multiline_no_newlines() {
assert_eq!(escape_multiline("hello"), "hello");
}
#[test]
fn escape_multiline_with_newlines() {
assert_eq!(escape_multiline("line1\nline2"), "line1\\\nline2");
}
#[test]
fn escape_multiline_trims_whitespace() {
assert_eq!(escape_multiline(" hello "), "hello");
}
#[test]
fn extract_image_url_basic() {
assert_eq!(
extract_markdown_image_url(""),
Some("https://example.com/img.png")
);
}
#[test]
fn extract_image_url_with_title() {
assert_eq!(
extract_markdown_image_url(r#""#),
Some("img.png")
);
}
#[test]
fn extract_image_url_empty_alt() {
assert_eq!(extract_markdown_image_url(""), Some("img.png"));
}
#[test]
fn extract_image_url_not_image() {
assert_eq!(extract_markdown_image_url("[text](url)"), None);
}
#[test]
fn extract_image_url_no_closing_paren() {
assert_eq!(extract_markdown_image_url(", None);
}
}