<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/xberg-io/assets@v1/banner/readme-banner-dark.svg">
<img alt="Xberg" width="420" src="https://cdn.jsdelivr.net/gh/xberg-io/assets@v1/banner/readme-banner-light.svg">
</picture>
</p>
# html-to-markdown-rs
[](https://github.com/xberg-io/alef)
High-performance HTML to Markdown converter built with Rust.
This crate is the core engine compiled into the Python wheels, Ruby gem, Node.js NAPI bindings, WebAssembly package, and CLI, ensuring identical Markdown output across every language.
[](https://crates.io/crates/html-to-markdown-rs)
[](https://www.npmjs.com/package/@xberg-io/html-to-markdown)
[](https://pypi.org/project/html-to-markdown/)
[](https://rubygems.org/gems/html-to-markdown)
[](https://packagist.org/packages/xberg-io/html-to-markdown)
[](https://docs.rs/html-to-markdown-rs)
[](https://github.com/xberg-io/html-to-markdown/blob/main/LICENSE)
Fast, reliable HTML to Markdown conversion with full CommonMark compliance. Built with `html5ever` for correctness and a DOM-based filter for safe preprocessing.
## Installation
```toml
[dependencies]
html-to-markdown-rs = "3.0"
```
## Basic Usage
`convert()` returns a structured `ConversionResult` with the converted text, metadata, tables, and more:
```rust
use html_to_markdown_rs::convert;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let html = r#"
<html lang="en">
<head><title>Welcome</title></head>
<body>
<h1>Welcome</h1>
<p>This is <strong>fast</strong> conversion!</p>
<ul>
<li>Built with Rust</li>
<li>CommonMark compliant</li>
</ul>
</body>
</html>
"#;
let result = convert(html, None)?;
println!("{}", result.content.unwrap_or_default());
// `extract_metadata` is on by default, so `metadata` is always populated.
println!("Title: {:?}", result.metadata.document.title);
println!("Headers: {:?}", result.metadata.headers);
Ok(())
}
```
## Error Handling
Conversion returns a `Result<ConversionResult, ConversionError>`. Inputs that look like binary data are rejected with
`ConversionError::InvalidInput` to prevent runaway allocations. Table `colspan`/`rowspan` values are also clamped
internally to keep output sizes bounded.
## Configuration
### Builder Pattern
```rust
use html_to_markdown_rs::{
convert, ConversionOptions, HeadingStyle, CodeBlockStyle,
};
let options = ConversionOptions::builder()
.heading_style(HeadingStyle::Atx)
.list_indent_width(2)
.bullets("-")
.autolinks(true)
.wrap(true)
.wrap_width(80)
.build();
let result = convert(html, Some(options))?;
println!("{}", result.content.unwrap_or_default());
```
### Struct Literal
```rust
use html_to_markdown_rs::{
convert, ConversionOptions, HeadingStyle, ListIndentType,
};
let options = ConversionOptions {
heading_style: HeadingStyle::Atx,
list_indent_width: 2,
list_indent_type: ListIndentType::Spaces,
bullets: "-".to_string(),
strong_em_symbol: '*',
escape_asterisks: false,
escape_underscores: false,
newline_style: html_to_markdown_rs::NewlineStyle::Backslash,
code_block_style: html_to_markdown_rs::CodeBlockStyle::Backticks,
..Default::default()
};
let result = convert(html, Some(options))?;
println!("{}", result.content.unwrap_or_default());
```
### Preserving HTML Tags
The `preserve_tags` option allows you to keep specific HTML tags in their original form instead of converting them to Markdown:
```rust
use html_to_markdown_rs::{convert, ConversionOptions};
let html = r#"
<p>Before table</p>
<table class="data">
<tr><th>Name</th><th>Value</th></tr>
<tr><td>Item 1</td><td>100</td></tr>
</table>
<p>After table</p>
"#;
let options = ConversionOptions {
preserve_tags: vec!["table".to_string()],
..Default::default()
};
let result = convert(html, Some(options))?;
// result.content => "Before table\n\n<table class=\"data\">...</table>\n\nAfter table\n"
```
## Web Scraping with Preprocessing
```rust
use html_to_markdown_rs::{convert, ConversionOptions, PreprocessingOptions};
let mut options = ConversionOptions::default();
options.preprocessing.enabled = true;
options.preprocessing.preset = html_to_markdown_rs::PreprocessingPreset::Aggressive;
options.preprocessing.remove_navigation = true;
options.preprocessing.remove_forms = true;
let result = convert(scraped_html, Some(options))?;
println!("{}", result.content.unwrap_or_default());
```
## Metadata Extraction
Metadata extraction is on by default; `result.metadata` is always populated (set `.extract_metadata(false)` to skip the pass):
```rust
use html_to_markdown_rs::{convert, ConversionOptions};
let options = ConversionOptions::builder().extract_metadata(true).build();
let result = convert(html, Some(options))?;
let metadata = &result.metadata;
println!("Title: {:?}", metadata.document.title);
for header in &metadata.headers {
println!("H{}: {}", header.level, header.text);
}
for link in &metadata.links {
println!("Link: {} -> {}", link.text, link.href);
}
```
## Image Extraction
```rust
use html_to_markdown_rs::{convert, ConversionOptions};
let options = ConversionOptions::builder()
.extract_images(true)
.max_image_size(5 * 1024 * 1024) // 5 MB max
.infer_dimensions(true)
.build();
let result = convert(html, Some(options))?;
println!("{}", result.content.unwrap_or_default());
for img in &result.images {
println!(
"Image: {:?} ({:?}, {} bytes)",
img.filename,
img.format,
img.data.len()
);
}
```
## Table Extraction
Structured table data is exposed on `ConversionResult.tables`. It is collected alongside the
document tree, so it requires `include_document_structure(true)`:
```rust
use html_to_markdown_rs::{convert, ConversionOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let html = r#"
<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>
"#;
let options = ConversionOptions::builder().include_document_structure(true).build();
let result = convert(html, Some(options))?;
println!("{}", result.content.clone().unwrap_or_default());
for table in &result.tables {
println!("Table {}x{}:", table.grid.rows, table.grid.cols);
for cell in &table.grid.cells {
let kind = if cell.is_header { "Header" } else { "Cell" };
println!(" {kind} (r{},c{}): {}", cell.row, cell.col, cell.content);
}
}
Ok(())
}
```
## Custom Visitors
```rust
use std::sync::{Arc, Mutex};
use html_to_markdown_rs::{convert, ConversionOptions};
use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};
#[derive(Debug)]
struct NoImagesVisitor;
impl HtmlVisitor for NoImagesVisitor {
fn visit_image(
&mut self,
_ctx: &NodeContext,
_src: &str,
_alt: &str,
_title: Option<&str>,
) -> VisitResult {
VisitResult::Skip
}
}
let options = ConversionOptions::builder()
.visitor(Some(Arc::new(Mutex::new(NoImagesVisitor))))
.build();
let result = convert(html, Some(options))?;
println!("{}", result.content.unwrap_or_default());
```
## Other Language Bindings
This is the core Rust library. For other languages:
- **JavaScript/TypeScript**: [html-to-markdown-node](https://github.com/xberg-io/html-to-markdown/tree/main/crates/html-to-markdown-node) (NAPI-RS) or [html-to-markdown-wasm](https://github.com/xberg-io/html-to-markdown/tree/main/crates/html-to-markdown-wasm) (WebAssembly)
- **Python**: [html-to-markdown](https://github.com/xberg-io/html-to-markdown/tree/main/crates/html-to-markdown-py) (PyO3)
- **Dart/Flutter**: [h2m](https://pub.dev/packages/h2m) (official Dart binding) or [html_to_markdown_rust](https://pub.dev/packages/html_to_markdown_rust) (unofficial FFI wrapper via Hooks)
- **PHP**: [html-to-markdown](https://github.com/xberg-io/html-to-markdown/tree/main/crates/html-to-markdown-php) (PIE + Composer helpers)
- **Ruby**: [html-to-markdown](https://github.com/xberg-io/html-to-markdown/tree/main/packages/ruby) (Magnus + rb-sys)
- **CLI**: [html-to-markdown-cli](https://github.com/xberg-io/html-to-markdown/tree/main/crates/html-to-markdown-cli)
## Documentation
- [Full Documentation](https://docs.html-to-markdown.xberg.io)
- [API Reference](https://docs.rs/html-to-markdown-rs)
- [Documentation](https://docs.html-to-markdown.xberg.io/)
- [Contributing Guide](https://github.com/xberg-io/html-to-markdown/blob/main/CONTRIBUTING.md)
## Performance
10-30x faster than pure Python/JavaScript implementations, delivering 150-280 MB/s throughput.
## License
MIT