use std::fs;
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html};
use crate::{Job, Result};
pub fn to_md(job: &Job) -> Result<()> {
let html = fs::read_to_string(job.input)?;
let md = html2md::parse_html(&html);
fs::write(job.output, md)?;
Ok(())
}
pub fn md_to_html(job: &Job) -> Result<()> {
let md = fs::read_to_string(job.input)?;
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
let parser = Parser::new_ext(&md, opts);
let mut out = String::new();
html::push_html(&mut out, parser);
fs::write(job.output, out)?;
Ok(())
}
pub fn html_to_txt(job: &Job) -> Result<()> {
let html = fs::read_to_string(job.input)?;
let text = strip_html(&html);
fs::write(job.output, text)?;
Ok(())
}
pub fn md_to_txt(job: &Job) -> Result<()> {
let md = fs::read_to_string(job.input)?;
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(&md, opts);
let mut out = String::new();
for event in parser {
match event {
Event::Text(t) | Event::Code(t) => out.push_str(&t),
Event::SoftBreak => out.push(' '),
Event::HardBreak => out.push('\n'),
Event::End(TagEnd::Paragraph | TagEnd::Heading(_) | TagEnd::Item) => {
out.push('\n');
}
Event::End(TagEnd::CodeBlock) => out.push('\n'),
Event::Start(Tag::Item) => out.push_str("- "),
_ => {}
}
}
fs::write(job.output, out)?;
Ok(())
}
pub fn txt_to_md(job: &Job) -> Result<()> {
fs::copy(job.input, job.output)?;
Ok(())
}
fn strip_html(html: &str) -> String {
let mut out = String::with_capacity(html.len());
let bytes = html.as_bytes();
let mut i = 0;
let mut in_script_or_style: Option<&str> = None;
while i < bytes.len() {
if let Some(tag) = in_script_or_style {
let close = format!("</{tag}");
if let Some(rel) = html[i..]
.to_ascii_lowercase()
.find(&close)
.and_then(|r| html[i + r..].find('>').map(|gt| r + gt + 1))
{
i += rel;
in_script_or_style = None;
} else {
break;
}
continue;
}
let b = bytes[i];
if b == b'<' {
let tag_start = i + 1;
let mut j = tag_start;
if j < bytes.len() && bytes[j] == b'/' {
j += 1;
}
let name_start = j;
while j < bytes.len()
&& (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'-')
{
j += 1;
}
let name = html[name_start..j].to_ascii_lowercase();
let end = html[i..]
.find('>')
.map(|rel| i + rel + 1)
.unwrap_or(bytes.len());
if name == "script" || name == "style" {
in_script_or_style =
Some(if name == "script" { "script" } else { "style" });
} else if matches!(
name.as_str(),
"p"
| "br"
| "div"
| "li"
| "tr"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
) {
out.push('\n');
}
i = end;
} else if b == b'&' {
if let Some(semi) = html[i..].find(';') {
let entity = &html[i + 1..i + semi];
let decoded = match entity {
"amp" => Some('&'),
"lt" => Some('<'),
"gt" => Some('>'),
"quot" => Some('"'),
"apos" => Some('\''),
"nbsp" => Some(' '),
s if s.starts_with("#x") || s.starts_with("#X") => {
u32::from_str_radix(&s[2..], 16)
.ok()
.and_then(char::from_u32)
}
s if s.starts_with('#') => {
s[1..].parse::<u32>().ok().and_then(char::from_u32)
}
_ => None,
};
if let Some(c) = decoded {
out.push(c);
i += semi + 1;
continue;
}
}
out.push(b as char);
i += 1;
} else {
out.push(b as char);
i += 1;
}
}
let mut collapsed = String::with_capacity(out.len());
let mut blank = 0;
for line in out.lines() {
let trimmed = line.trim_end();
if trimmed.is_empty() {
blank += 1;
if blank <= 1 {
collapsed.push('\n');
}
} else {
blank = 0;
collapsed.push_str(trimmed);
collapsed.push('\n');
}
}
collapsed
}