use colored::*;
use scraper::{Html, Node, Selector};
use std::fmt::Write as FmtWrite;
pub fn render_html_to_terminal(html_content: &str) {
let document = Html::parse_document(html_content);
if let Ok(formatted) = render_html_custom(&document) {
print!("{formatted}");
} else {
render_html_simple(html_content);
}
}
fn render_html_custom(document: &Html) -> Result<String, Box<dyn std::error::Error>> {
let mut output = String::new();
let body_selector = Selector::parse("body").unwrap();
let root = document
.select(&body_selector)
.next()
.map(|e| e.html())
.unwrap_or_else(|| document.html());
let body_doc = Html::parse_fragment(&root);
render_headings(&body_doc, &mut output)?;
render_paragraphs(&body_doc, &mut output)?;
render_lists(&body_doc, &mut output)?;
render_code_blocks(&body_doc, &mut output)?;
render_tables(&body_doc, &mut output)?;
Ok(output)
}
fn render_headings(document: &Html, output: &mut String) -> Result<(), Box<dyn std::error::Error>> {
for level in 1..=6 {
let selector_str = format!("h{level}");
let Ok(selector) = Selector::parse(&selector_str) else {
continue;
};
for element in document.select(&selector) {
let text = element.text().collect::<String>().trim().to_string();
if text.is_empty() {
continue;
}
writeln!(output)?;
match level {
1 => {
writeln!(output, "{}", text.bold().bright_blue().underline())?;
writeln!(output, "{}", "=".repeat(text.len()).bright_blue())?;
}
2 => {
writeln!(output, "{}", text.bold().bright_cyan())?;
writeln!(output, "{}", "-".repeat(text.len()).bright_cyan())?;
}
3 => {
writeln!(output, "{}", text.bold().green())?;
}
4 => {
writeln!(output, "{}", text.bold().yellow())?;
}
5 => {
writeln!(output, "{}", text.yellow())?;
}
6 => {
writeln!(output, "{}", text.dimmed())?;
}
_ => {}
}
writeln!(output)?;
}
}
Ok(())
}
fn render_paragraphs(
document: &Html,
output: &mut String,
) -> Result<(), Box<dyn std::error::Error>> {
let Ok(selector) = Selector::parse("p") else {
return Ok(());
};
for element in document.select(&selector) {
let text = extract_styled_text(&element);
if !text.trim().is_empty() {
writeln!(output, "{text}")?;
writeln!(output)?;
}
}
Ok(())
}
fn extract_styled_text(element: &scraper::ElementRef) -> String {
let mut result = String::new();
for node in element.children() {
match node.value() {
Node::Text(text) => {
result.push_str(text);
}
Node::Element(elem) => {
let elem_ref = scraper::ElementRef::wrap(node).unwrap();
let inner_text = elem_ref.text().collect::<String>();
match elem.name() {
"strong" | "b" => {
result.push_str(&format!("{}", inner_text.bold()));
}
"em" | "i" => {
result.push_str(&format!("{}", inner_text.italic()));
}
"code" => {
result.push_str(&format!(
"{}",
inner_text.bright_magenta().on_truecolor(50, 50, 50)
));
}
"a" => {
if let Some(href) = elem_ref.value().attr("href") {
result.push_str(&format!(
"{} {}",
inner_text.bright_blue().underline(),
format!("({href})").dimmed()
));
} else {
result.push_str(&inner_text);
}
}
_ => {
result.push_str(&extract_styled_text(&elem_ref));
}
}
}
_ => {}
}
}
result
}
fn render_lists(document: &Html, output: &mut String) -> Result<(), Box<dyn std::error::Error>> {
let Ok(ul_selector) = Selector::parse("ul") else {
return Ok(());
};
let Ok(li_selector) = Selector::parse("li") else {
return Ok(());
};
for ul in document.select(&ul_selector) {
writeln!(output)?;
for li in ul.select(&li_selector) {
let text = li.text().collect::<String>().trim().to_string();
if !text.is_empty() {
writeln!(output, " {} {}", "•".bright_green(), text)?;
}
}
writeln!(output)?;
}
let Ok(ol_selector) = Selector::parse("ol") else {
return Ok(());
};
for ol in document.select(&ol_selector) {
writeln!(output)?;
let mut counter = 1;
for li in ol.select(&li_selector) {
let text = li.text().collect::<String>().trim().to_string();
if !text.is_empty() {
writeln!(
output,
" {}. {}",
counter.to_string().bright_yellow(),
text
)?;
counter += 1;
}
}
writeln!(output)?;
}
Ok(())
}
fn render_code_blocks(
document: &Html,
output: &mut String,
) -> Result<(), Box<dyn std::error::Error>> {
let Ok(selector) = Selector::parse("pre") else {
return Ok(());
};
for pre in document.select(&selector) {
let code = pre.text().collect::<String>();
if code.trim().is_empty() {
continue;
}
writeln!(output)?;
writeln!(output, "{}", "```".dimmed())?;
for line in code.lines() {
writeln!(output, "{}", line.on_truecolor(40, 44, 52))?;
}
writeln!(output, "{}", "```".dimmed())?;
writeln!(output)?;
}
Ok(())
}
fn render_tables(document: &Html, output: &mut String) -> Result<(), Box<dyn std::error::Error>> {
let Ok(table_selector) = Selector::parse("table") else {
return Ok(());
};
let Ok(th_selector) = Selector::parse("th") else {
return Ok(());
};
let Ok(tr_selector) = Selector::parse("tr") else {
return Ok(());
};
let Ok(td_selector) = Selector::parse("td") else {
return Ok(());
};
for table in document.select(&table_selector) {
writeln!(output)?;
writeln!(output, "{}", "Table:".bold().cyan())?;
let mut headers = Vec::new();
for th in table.select(&th_selector) {
headers.push(th.text().collect::<String>().trim().to_string());
}
if !headers.is_empty() {
writeln!(output, " {}", headers.join(" | ").bold())?;
writeln!(output, " {}", "-".repeat(headers.len() * 10))?;
}
for tr in table.select(&tr_selector) {
let mut cells = Vec::new();
for td in tr.select(&td_selector) {
cells.push(td.text().collect::<String>().trim().to_string());
}
if !cells.is_empty() {
writeln!(output, " {}", cells.join(" | "))?;
}
}
writeln!(output)?;
}
Ok(())
}
fn render_html_simple(html_content: &str) {
let text = html2text::from_read(html_content.as_bytes(), 80);
println!("{text}");
}
pub fn render_html_plain(html_content: &str) {
let text = html2text::from_read(html_content.as_bytes(), 80);
println!("{text}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_rendering() {
let html = r#"
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<h1>Main Title</h1>
<h2>Subtitle</h2>
<p>This is a paragraph with <strong>bold</strong> and <em>italic</em> text.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<ol>
<li>First</li>
<li>Second</li>
</ol>
<pre><code>
fn main() {
println!("Hello!");
}
</code></pre>
<table>
<tr><th>Name</th><th>Age</th></tr>
<tr><td>Alice</td><td>30</td></tr>
<tr><td>Bob</td><td>25</td></tr>
</table>
</body>
</html>
"#;
render_html_to_terminal(html);
render_html_plain(html);
}
#[test]
fn test_simple_html() {
let html = "<h1>Title</h1><p>Content</p>";
render_html_to_terminal(html);
}
}