use crate::ImportedIconData;
pub(super) fn html_to_markdown(html: &str) -> String {
let body = html
.split_once("page-body")
.and_then(|(_, rest)| rest.split_once('>').map(|(_, rest)| rest))
.unwrap_or(html);
let mut text = html_images_to_markdown(body)
.replace("<br>", "\n")
.replace("<br/>", "\n")
.replace("<br />", "\n")
.replace("</p>", "\n\n")
.replace("</div>", "\n")
.replace("</li>", "\n");
text = strip_html_tags(&text);
html_unescape(&text).trim().to_string()
}
fn html_images_to_markdown(html: &str) -> String {
let mut output = String::with_capacity(html.len());
let mut rest = html;
loop {
let Some(img_start) = rest.find("<img") else {
output.push_str(rest);
break;
};
output.push_str(&rest[..img_start]);
let img_rest = &rest[img_start..];
let Some(tag_end) = img_rest.find('>') else {
output.push_str(img_rest);
break;
};
let tag = &img_rest[..=tag_end];
if let Some(src) = html_attr(tag, "src") {
output.push_str(&format!("\n\n"));
}
rest = &img_rest[tag_end + 1..];
}
output
}
fn html_attr(tag: &str, name: &str) -> Option<String> {
let pattern = format!("{name}=\"");
let (_, rest) = tag.split_once(&pattern)?;
let (value, _) = rest.split_once('"')?;
Some(html_unescape(value))
}
pub(super) fn extract_html_title(html: &str) -> Option<String> {
extract_html_class_text(html, "page-title")
.or_else(|| extract_html_tag_text(html, "h1"))
.map(|title| title.trim().to_string())
.filter(|title| !title.is_empty())
}
fn extract_html_class_text(html: &str, class_name: &str) -> Option<String> {
let marker = format!("class=\"{class_name}\"");
let (_, rest) = html.split_once(&marker)?;
let (_, rest) = rest.split_once('>')?;
let (content, _) = rest.split_once('<')?;
Some(html_unescape(content))
}
fn extract_html_tag_text(html: &str, tag_name: &str) -> Option<String> {
let marker = format!("<{tag_name}");
let (_, rest) = html.split_once(&marker)?;
let (_, rest) = rest.split_once('>')?;
let (content, _) = rest.split_once(&format!("</{tag_name}>"))?;
Some(html_unescape(&strip_html_tags(content)))
}
fn strip_html_tags(value: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut in_tag = false;
for ch in value.chars() {
match ch {
'<' => in_tag = true,
'>' => in_tag = false,
_ if !in_tag => output.push(ch),
_ => {}
}
}
output
}
fn html_unescape(value: &str) -> String {
value
.replace(" ", " ")
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
}
pub(super) fn extract_html_icon(html: &str) -> Option<ImportedIconData> {
let marker = "class=\"icon\"";
let (_, rest) = html.split_once(marker)?;
let (_, rest) = rest.split_once('>')?;
let (content, _) = rest.split_once('<')?;
let content = html_unescape(content).trim().to_string();
(!content.is_empty()).then_some(ImportedIconData {
kind: "emoji".to_string(),
unicode: content,
})
}
pub(super) fn split_leading_emoji(title: &str) -> (String, Option<String>) {
let trimmed = title.trim();
let Some(first) = trimmed.chars().next() else {
return (String::new(), None);
};
if is_emoji(first) {
let rest = trimmed[first.len_utf8()..].trim().to_string();
(rest, Some(first.to_string()))
} else {
(trimmed.to_string(), None)
}
}
fn is_emoji(ch: char) -> bool {
matches!(
ch as u32,
0x1F300..=0x1FAFF | 0x2600..=0x27BF
)
}