use std::boxed::Box;
use std::collections::HashMap;
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
use std::sync::LazyLock;
use regex::Regex;
use html5ever::driver::ParseOpts;
use html5ever::parse_document;
use html5ever::tendril::TendrilSink;
pub use markup5ever_rcdom::{Handle, NodeData, RcDom};
pub mod anchors;
pub mod codes;
pub mod common;
pub mod containers;
pub mod dummy;
pub mod headers;
pub mod iframes;
pub mod images;
pub mod lists;
pub mod markup5ever_rcdom;
pub mod paragraphs;
pub mod quotes;
pub mod styles;
pub mod tables;
use crate::anchors::AnchorHandler;
use crate::codes::CodeHandler;
use crate::containers::ContainerHandler;
use crate::dummy::DummyHandler;
use crate::dummy::HtmlCherryPickHandler;
use crate::dummy::IdentityHandler;
use crate::headers::HeaderHandler;
use crate::iframes::IframeHandler;
use crate::images::ImgHandler;
use crate::lists::ListHandler;
use crate::lists::ListItemHandler;
use crate::paragraphs::ParagraphHandler;
use crate::quotes::QuoteHandler;
use crate::styles::StyleHandler;
use crate::tables::TableHandler;
static EXCESSIVE_WHITESPACE_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new("\\s{2,}").unwrap()); static EMPTY_LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?m)^ +$").unwrap()); static EXCESSIVE_NEWLINE_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new("\\n{3,}").unwrap()); static TRAILING_SPACE_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?m)(\\S) $").unwrap()); static LEADING_NEWLINES_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new("^\\n+").unwrap()); static LAST_WHITESPACE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\s+$").unwrap()); static START_OF_LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new("(^|\\n) *$").unwrap()); static MARKDOWN_STARTONLY_KEYCHARS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)([=>+\-#])").unwrap()); static MARKDOWN_MIDDLE_KEYCHARS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[<>*\\_~]").unwrap());
pub fn parse_html_custom(
html: &str,
custom: &HashMap<String, Box<dyn TagHandlerFactory>>,
) -> String {
let dom = parse_document(RcDom::default(), ParseOpts::default())
.from_utf8()
.read_from(&mut html.as_bytes())
.unwrap();
let mut result = StructuredPrinter::default();
walk(&dom.document, &mut result, custom);
clean_markdown(&result.data)
}
pub fn parse_html(html: &str) -> String {
parse_html_custom(html, &HashMap::default())
}
pub fn parse_html_extended(html: &str) -> String {
let mut tag_factory: HashMap<String, Box<dyn TagHandlerFactory>> = HashMap::new();
tag_factory.insert(
String::from("span"),
Box::new(HtmlCherryPickHandler::default),
);
parse_html_custom(html, &tag_factory)
}
pub fn walk(
input: &Handle,
result: &mut StructuredPrinter,
custom: &HashMap<String, Box<dyn TagHandlerFactory>>,
) {
let mut handler: Box<dyn TagHandler> = Box::new(DummyHandler);
let mut tag_name = String::default();
match input.data {
NodeData::Document | NodeData::Doctype { .. } | NodeData::ProcessingInstruction { .. } => {}
NodeData::Text { ref contents } => {
let mut text = contents.borrow().to_string();
let inside_pre = result.parent_chain.iter().any(|tag| tag == "pre");
if inside_pre {
result.append_str(&text);
} else if !(text.trim().is_empty()
&& (result.data.ends_with('\n') || result.data.ends_with(' ')))
{
let inside_code = result.parent_chain.iter().any(|tag| tag == "code");
if !inside_code {
text = escape_markdown(result, &text);
}
let minified_text = EXCESSIVE_WHITESPACE_PATTERN.replace_all(&text, " ");
let minified_text = minified_text.trim_matches(|ch: char| ch == '\n' || ch == '\r');
result.append_str(minified_text);
}
}
NodeData::Comment { .. } => {} NodeData::Element { ref name, .. } => {
tag_name = name.local.to_string();
let inside_pre = result.parent_chain.iter().any(|tag| tag == "pre");
if inside_pre {
handler = Box::new(DummyHandler);
} else if custom.contains_key(&tag_name) {
let factory = custom.get(&tag_name).unwrap();
handler = factory.instantiate();
} else {
handler = match tag_name.as_ref() {
"div" | "section" | "header" | "footer" => Box::new(ContainerHandler),
"p" | "br" | "hr" => Box::new(ParagraphHandler::default()),
"q" | "cite" | "blockquote" => Box::new(QuoteHandler::default()),
"details" | "summary" => Box::new(HtmlCherryPickHandler::default()),
"b" | "i" | "s" | "strong" | "em" | "del" => Box::new(StyleHandler::default()),
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Box::new(HeaderHandler::default()),
"pre" | "code" => Box::new(CodeHandler::default()),
"img" => Box::new(ImgHandler::default()),
"a" => Box::new(AnchorHandler::default()),
"ol" | "ul" | "menu" => Box::new(ListHandler),
"li" => Box::new(ListItemHandler::default()),
"sub" | "sup" => Box::new(IdentityHandler),
"table" => Box::new(TableHandler),
"iframe" => Box::new(IframeHandler),
"html" | "head" | "body" => Box::new(DummyHandler),
_ => Box::new(DummyHandler),
};
}
}
}
handler.handle(input, result);
result.parent_chain.push(tag_name.to_string()); let current_depth = result.parent_chain.len();
result.siblings.insert(current_depth, vec![]);
for child in input.children.borrow().iter() {
if handler.skip_descendants() {
continue;
}
walk(child, result, custom);
if let NodeData::Element { ref name, .. } = child.data {
result
.siblings
.get_mut(¤t_depth)
.unwrap()
.push(name.local.to_string())
};
}
result.siblings.remove(¤t_depth);
result.parent_chain.pop();
handler.after_handle(result);
}
fn escape_markdown(result: &StructuredPrinter, text: &str) -> String {
let mut data = MARKDOWN_MIDDLE_KEYCHARS
.replace_all(text, "\\$0")
.to_string();
if START_OF_LINE_PATTERN.is_match(&result.data) {
data = MARKDOWN_STARTONLY_KEYCHARS
.replace(&data, "$1\\$2")
.to_string();
}
data
}
fn clean_markdown(text: &str) -> String {
let intermediate = EMPTY_LINE_PATTERN.replace_all(text, ""); let intermediate = EXCESSIVE_NEWLINE_PATTERN.replace_all(&intermediate, "\n\n"); let intermediate = TRAILING_SPACE_PATTERN.replace_all(&intermediate, "$1"); let intermediate = LEADING_NEWLINES_PATTERN.replace_all(&intermediate, ""); let intermediate = LAST_WHITESPACE_PATTERN.replace_all(&intermediate, "");
intermediate.into_owned()
}
#[derive(Debug, Default)]
pub struct StructuredPrinter {
pub parent_chain: Vec<String>,
pub siblings: HashMap<usize, Vec<String>>,
pub data: String,
}
impl StructuredPrinter {
pub fn insert_newline(&mut self) {
self.append_str("\n");
}
pub fn append_str(&mut self, it: &str) {
self.data.push_str(it);
}
pub fn insert_str(&mut self, pos: usize, it: &str) {
self.data.insert_str(pos, it);
}
}
pub trait TagHandlerFactory {
fn instantiate(&self) -> Box<dyn TagHandler>;
}
impl<F: Fn() -> T, T: TagHandler + 'static> TagHandlerFactory for F {
fn instantiate(&self) -> Box<dyn TagHandler> {
Box::new(self())
}
}
pub trait TagHandler {
fn handle(&mut self, tag: &Handle, printer: &mut StructuredPrinter);
fn after_handle(&mut self, printer: &mut StructuredPrinter);
fn skip_descendants(&self) -> bool {
false
}
}
#[unsafe(no_mangle)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn parse(html: *const c_char) -> *const c_char {
let in_html = unsafe { CStr::from_ptr(html) };
let out_md = parse_html(&in_html.to_string_lossy());
CString::new(out_md).unwrap().into_raw()
}
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
use super::parse_html;
use super::parse_html_extended;
use self::jni::JNIEnv;
use self::jni::objects::{JClass, JString};
use self::jni::sys::jstring;
#[no_mangle]
pub unsafe extern "C" fn Java_com_kanedias_html2md_Html2Markdown_parse(
mut env: JNIEnv,
_clazz: JClass,
html: JString,
) -> jstring {
let html_java: String = env
.get_string(html)
.expect("Couldn't get java string!")
.into();
let markdown = parse_html(&html_java);
let output = env
.new_string(markdown)
.expect("Couldn't create java string!");
output.into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_kanedias_html2md_Html2Markdown_parseExtended(
mut env: JNIEnv,
_clazz: JClass,
html: JString,
) -> jstring {
let html_java: String = env
.get_string(html)
.expect("Couldn't get java string!")
.into();
let markdown = parse_html_extended(&html_java);
let output = env
.new_string(markdown)
.expect("Couldn't create java string!");
output.into_raw()
}
}