use dom_query::Selection;
use crate::converter::convert_node;
use crate::options::MarkdownOptions;
use crate::utils::{escape_markdown_text, escape_url, get_tag_name, resolve_url};
pub(crate) fn convert_inline_content(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
depth: usize,
) {
convert_inline_content_impl(sel, output, options, false, depth);
}
pub(crate) fn convert_inline_content_impl(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
skip_lists: bool,
depth: usize,
) {
if let Some(node) = sel.nodes().first() {
for child in node.children() {
if child.is_text() {
let text = child.text();
if options.escape_special_chars {
let line_start = output.is_empty()
|| output.ends_with('\n');
output.push_str(&escape_markdown_text(&text, line_start));
} else {
output.push_str(&text);
}
} else if child.is_element() {
let child_sel = Selection::from(child);
let tag = get_tag_name(&child_sel);
if skip_lists && (tag == "ul" || tag == "ol") {
continue;
}
convert_node(&child_sel, output, options, depth);
}
}
}
}
pub(crate) fn convert_strong(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
depth: usize,
) {
let start_len = output.len();
output.push_str("**");
let content_start = output.len();
convert_inline_content(sel, output, options, depth);
if output.len() == content_start {
output.truncate(start_len);
} else {
output.push_str("**");
}
}
pub(crate) fn convert_emphasis(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
depth: usize,
) {
let start_len = output.len();
output.push('*');
let content_start = output.len();
convert_inline_content(sel, output, options, depth);
if output.len() == content_start {
output.truncate(start_len);
} else {
output.push('*');
}
}
pub(crate) fn convert_strikethrough(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
depth: usize,
) {
let start_len = output.len();
output.push_str("~~");
let content_start = output.len();
convert_inline_content(sel, output, options, depth);
if output.len() == content_start {
output.truncate(start_len);
} else {
output.push_str("~~");
}
}
pub(crate) fn convert_link(
sel: &Selection,
output: &mut String,
options: &MarkdownOptions,
depth: usize,
) {
let href = sel.attr("href").unwrap_or_default();
if href.is_empty() {
convert_inline_content(sel, output, options, depth);
return;
}
let resolved_url = resolve_url(&href, options);
output.push('[');
convert_inline_content(sel, output, options, depth);
output.push_str("](");
output.push_str(&escape_url(&resolved_url));
output.push(')');
}
pub(crate) fn convert_image(sel: &Selection, output: &mut String, options: &MarkdownOptions) {
let src = sel.attr("src").unwrap_or_default();
if src.is_empty() {
return;
}
let alt = sel.attr("alt").unwrap_or_default();
let width = sel.attr("width");
let height = sel.attr("height");
let resolved_url = resolve_url(&src, options);
if options.commonmark && (width.is_some() || height.is_some()) {
output.push_str("<img src=\"");
output.push_str(&resolved_url);
output.push('"');
if !alt.is_empty() {
output.push_str(" alt=\"");
output.push_str(&escape_html_attr(&alt));
output.push('"');
}
if let Some(w) = width {
output.push_str(" width=\"");
output.push_str(&escape_html_attr(&w));
output.push('"');
}
if let Some(h) = height {
output.push_str(" height=\"");
output.push_str(&escape_html_attr(&h));
output.push('"');
}
output.push_str(" />");
} else {
output.push_str(";
output.push_str(&escape_url(&resolved_url));
output.push(')');
}
}
fn escape_html_attr(s: &str) -> String {
s.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">")
}
pub(crate) fn convert_inline_code(sel: &Selection, output: &mut String) {
let text = sel.text();
let content = text.as_ref();
if content.is_empty() {
return;
}
let max_backticks = count_max_consecutive_backticks(content);
if max_backticks == 0 {
output.push('`');
output.push_str(content);
output.push('`');
} else {
let delimiter = "`".repeat(max_backticks + 1);
output.push_str(&delimiter);
let needs_leading_space = content.starts_with('`');
let needs_trailing_space = content.ends_with('`');
if needs_leading_space {
output.push(' ');
}
output.push_str(content);
if needs_trailing_space {
output.push(' ');
}
output.push_str(&delimiter);
}
}
fn count_max_consecutive_backticks(s: &str) -> usize {
let mut max_count = 0;
let mut current_count = 0;
for c in s.chars() {
if c == '`' {
current_count += 1;
max_count = max_count.max(current_count);
} else {
current_count = 0;
}
}
max_count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_max_consecutive_backticks() {
assert_eq!(count_max_consecutive_backticks("hello"), 0);
assert_eq!(count_max_consecutive_backticks("hel`lo"), 1);
assert_eq!(count_max_consecutive_backticks("hel``lo"), 2);
assert_eq!(count_max_consecutive_backticks("`a`b``c"), 2);
assert_eq!(count_max_consecutive_backticks("```"), 3);
}
#[test]
fn test_escape_html_attr() {
assert_eq!(escape_html_attr("hello"), "hello");
assert_eq!(escape_html_attr("a & b"), "a & b");
assert_eq!(escape_html_attr("a \"quoted\""), "a "quoted"");
assert_eq!(escape_html_attr("<tag>"), "<tag>");
}
#[test]
fn test_escape_markdown_text() {
assert_eq!(escape_markdown_text("hello", true), "hello");
assert_eq!(escape_markdown_text("*bold*", true), r"\*bold\*");
assert_eq!(escape_markdown_text("[link]", true), r"\[link\]");
}
}