This is the full developer documentation for html-to-markdown # html-to-markdown > Turn messy, real-world HTML into clean Markdown, Djot, or plain text. Call one converter from Python, the browser, the CLI, and 13 more runtimes — and get identical output everywhere. ## Why html-to-markdown [Section titled “Why html-to-markdown”](#why-html-to-markdown) Handles messy, real-world HTML Unclosed tags, odd entities, nested tables, mixed encodings — you get clean output without losing content. Use it from your language, identical output One converter behind Python, TypeScript, Go, Ruby, Java, and 11 more runtimes. The same input gives the same result everywhere. Markdown, Djot, or plain text Pick the output you need with a single option. Everything else works the same. Get the metadata too Pull the title, Open Graph, Twitter Card, JSON-LD, links, and images in the same pass — on by default. Keep your tables structured Get HTML tables back as structured rows and cells, spans and headers included, alongside the rendered Markdown. Customize the conversion Hook into the conversion to skip, replace, or preserve any element — and pay nothing when you don’t. ## Language support [Section titled “Language support”](#language-support) | Language | Install | API Reference | | --------------------- | ------------------------------------------------------------ | ------------------------------------------- | | **Rust** | `cargo add html-to-markdown-rs` | [Reference](/reference/api-rust/) | | **Python** | `pip install html-to-markdown` | [Reference](/reference/api-python/) | | **TypeScript / Node** | `pnpm add @xberg-io/html-to-markdown` | [Reference](/reference/api-typescript/) | | **Go** | `go get github.com/xberg-io/html-to-markdown/packages/go/v3` | [Reference](/reference/api-go/) | | **Ruby** | `gem install html-to-markdown` | [Reference](/reference/api-ruby/) | | **PHP** | `pie install xberg-io/html-to-markdown` | [Reference](/reference/api-php/) | | **Java** | Maven `io.xberg:html-to-markdown` | [Reference](/reference/api-java/) | | **C#** | `dotnet add package XbergIo.HtmlToMarkdown` | [Reference](/reference/api-csharp/) | | **Elixir** | `{:html_to_markdown, "~> 3.8"}` | [Reference](/reference/api-elixir/) | | **R** | `install.packages("htmltomarkdown")` | [Reference](/reference/api-r/) | | **C (FFI)** | Shared library + header | [Reference](/reference/api-c/) | | **WebAssembly** | `pnpm add @xberg-io/html-to-markdown-wasm` | [Reference](/reference/api-wasm/) | | **Swift** | Swift Package `HtmlToMarkdown` | [Reference](/reference/api-swift/) | | **Dart** | `dart pub add h2m` | [Reference](/reference/api-dart/) | | **Kotlin Android** | Maven `io.xberg:html-to-markdown-android` | [Reference](/reference/api-kotlin-android/) | | **Zig** | Zig package `html_to_markdown_rs` | [Reference](/reference/api-zig/) | | **CLI** | `cargo install html-to-markdown-cli` | [CLI Guide](/cli/) | ## Quick example [Section titled “Quick example”](#quick-example) * Python main.py ```python from html_to_markdown import convert result = convert("

Hello

World

") print(result.content) print(result.metadata) ``` * TypeScript index.ts ```typescript import { convert } from "@xberg-io/html-to-markdown"; const result = convert("

Hello

World

"); console.log(result.content); console.log(result.metadata); ``` * Rust src/main.rs ```rust use html_to_markdown_rs::convert; fn main() -> Result<(), Box> { let result = convert("

Hello

World

", None)?; println!("{}", result.content.unwrap_or_default()); Ok(()) } ``` ## Part of Xberg.io [Section titled “Part of Xberg.io”](#part-of-xbergio) [Xberg](https://github.com/xberg-io/xberg)The open-source content-intelligence engine: text, tables, and metadata from 101 formats (115 file extensions), with OCR, transcription, and code intelligence. [Xberg Pro](https://xberg.io)A complete self-hosted content-intelligence backend in a single container. Commercial. [Xberg Enterprise](https://xberg.io)The distributed, governed content-intelligence platform, scaled on Kubernetes with team governance and support. Commercial. [crawlberg](https://github.com/xberg-io/crawlberg)Web crawling and scraping with HTML→Markdown and headless-Chrome fallback. [html-to-markdown](https://github.com/xberg-io/html-to-markdown)Fast, lossless HTML→Markdown engine. [liter-llm](https://github.com/xberg-io/liter-llm)Universal LLM API client with native bindings for 14 languages and 165 providers. [tree-sitter-language-pack](https://github.com/xberg-io/tree-sitter-language-pack)Tree-sitter grammars and code-intelligence primitives. [alef](https://github.com/xberg-io/alef)The polyglot binding generator that produces every per-language binding across the 5 polyglot repos. ## Explore the docs [Section titled “Explore the docs”](#explore-the-docs) [Get Started](/installation/)Install a binding and run your first \`convert()\` call. [Guides](/visitor/)Visitor pattern, table extraction, and error handling. [Concepts](/concepts/architecture/)Architecture, the conversion pipeline, and the plugin system. [Reference](/configuration/)Options reference, generated API docs, and per-language guides. [CLI](/cli/)Every conversion option as a command-line flag. [Migration](/migration/)Upgrading from earlier versions. ## Getting help [Section titled “Getting help”](#getting-help) * **Bugs & feature requests** — [Open an issue on GitHub](https://github.com/xberg-io/html-to-markdown/issues) * **Community chat** — [Join the Discord](https://discord.gg/xt9WY3GnKR) * **Contributing** — [Read the contributor guide](/contributing/) # API reference > Generated, binding-accurate API pages from the Rust source (alef docs). ## API reference [Section titled “API reference”](#api-reference) These pages are **generated** from the public Rust API surface. You can run `task alef:generate` (or `alef docs`) after changing types, options, or `alef.toml` so the Markdown under `docs/reference/` stays in sync. | Topic | Link | | ----------------------------------- | ------------------------------------------------------ | | All types and result shapes | [Types](/reference/types/) | | Options, metadata, and field tables | [Configuration (generated)](/reference/configuration/) | | Error / enum reference | [Error types (generated)](/reference/errors/) | ### Language APIs [Section titled “Language APIs”](#language-apis) | Language | Page | | ----------------- | ---------------------------------------------------- | | Rust | [api-rust](/reference/api-rust/) | | Python | [api-python](/reference/api-python/) | | TypeScript / Node | [api-typescript](/reference/api-typescript/) | | Go | [api-go](/reference/api-go/) | | Ruby | [api-ruby](/reference/api-ruby/) | | PHP | [api-php](/reference/api-php/) | | Java | [api-java](/reference/api-java/) | | C# | [api-csharp](/reference/api-csharp/) | | Elixir | [api-elixir](/reference/api-elixir/) | | R | [api-r](/reference/api-r/) | | Kotlin (Android) | [api-kotlin-android](/reference/api-kotlin-android/) | | Swift | [api-swift](/reference/api-swift/) | | Dart | [api-dart](/reference/api-dart/) | | Zig | [api-zig](/reference/api-zig/) | | C (FFI) | [api-c](/reference/api-c/) | | WebAssembly | [api-wasm](/reference/api-wasm/) | For how to *use* the API in your language, start with [Language guides](/language-guides/) and the narrative [Configuration](/configuration/) and [Error handling](/errors/) pages; this section is the exhaustive field-level reference. # Changelog All notable changes to html-to-markdown will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## \[Unreleased] [Section titled “\[Unreleased\]”](#unreleased) ### Added [Section titled “Added”](#added) * **`ConversionOptions::hidden_content`** chooses whether the output holds the text that a browser does not show at first, so a search index can hold an inactive tab panel or a collapsed answer. `drop` (the default) writes none of it, as before. `reachable` keeps the text a reader can get to in the page: an element with the `hidden` attribute or with inline `display: none`, `visibility: hidden` or `font-size: 0`, an element of an inline `` with `display="none"` or `visibility="hidden"`, and a declarative shadow root (a `bold` gave `**bold**` and now gives `bold` ([#828](https://github.com/xberg-io/html-to-markdown/issues/828)). * The fast converter keeps the text after a `` end tag with a form feed after the name ([#826](https://github.com/xberg-io/html-to-markdown/issues/826)). * A page is not empty when a structured data script has a slash or a form feed after `script` in its start tag and ` bar

` gave `foo bar` and now gives `foo bar` ([#827](https://github.com/xberg-io/html-to-markdown/issues/827)). * A `script`, `style` or `textarea` tag inside a bogus comment (``, ``, ``) starts no element: `

before

?>Shown?>

after

` gave `before` and `after` and now gives `?>**Shown**?>` between them. * A page with many `script` or `style` start tags with no end tag converts in linear time; the time was quadratic. In a release build 2,000 ``, ``, ``); each of these also gave an empty page. A type written with a character reference (`application/ld+json`) is read as structured data ([#818](https://github.com/xberg-io/html-to-markdown/issues/818)). * The options of a select list are separate words instead of one joined word. A select list with the options `Quickstart`, `Installation` and `Ruby 101` gave `QuickstartInstallationRuby 101` and now gives `Quickstart Installation Ruby 101`; a label, a select list and a text area in one form gave `NameOneTwoarea words` and now give `Name One Two area words`. Both converters give the same text for output, meter and progress elements. Such a control in an inline element (a label, a span, bold text, a custom element) stays in its line, as a browser shows it: `items` gave `Oneitems` and now gives `One items`, and `items` gave `One`, a blank line and `items`, and now gives `One items`. A block that holds such a control still ends the line after it. An input writes no text and adds no space: `

NameMail

` gives `NameMail`. Inside `pre` and code, a button, an output, a meter, a progress bar or a text area adds no space: `
beforeafter
` gives `beforeGoafter`. A select list there keeps its options as separate words ([#752](https://github.com/xberg-io/html-to-markdown/issues/752)). * The `label` attribute of an option group is no longer written. It is not text of the page: a browser shows it only inside the open list. A select list with the groups `Getting Started` (option `Quickstart`) and `Build` (option `Commands`) gave `**Getting Started**`, a line break, `Quickstart**Build**`, a line break and `Commands`, and now gives `Quickstart Commands` ([#776](https://github.com/xberg-io/html-to-markdown/issues/776)). * An element with no end tag ends where the HTML standard says: at the end tag of its parent, or at a start tag that cannot be its content. Before, a paragraph with no end tag stayed open after its parent ended, so each `

Note

` put the rest of the page two levels deeper, and after 30 such blocks the page was cut at the depth limit with a `depth_limit_exceeded` warning. A page of 80 such blocks and a footer gave 30 notes and no footer; it gives 80 notes and the footer, with no warning. This changes the default output for other pages that omit an end tag: `

one

tail` gave `onetail` and gives `one`, a blank line, `tail`. `

quoted

after

` gave `after` inside the quote and gives it after the quote. A paragraph after a table whose last cell has no end tag was written into that cell and is written after the table. `tail` gave an empty document and gives `two` and `tail`. `
ab
cd
` gave a table of one cell and gives both rows. A document with no `` wrote its title into the body, or wrote nothing with `extract_metadata = false`; it writes its body. `

abc after` gave `a(b(c after))` and gives `a(b)(c) after`. `

one

two

three` gave `## onetwothree` and gives two headings and a paragraph. `

one

two
three` gave a blank line between `one` and the quote and gives none, as the same input with `

` written always did. A page that ends with an element open converts as the same page with every end tag written: `

one

site

two

` gave `one`, `site` and `two`, and gives `one` and `two`, because a `
` directly in the body is a page header. `

one

late
` loses `late` by the same rule. A page that omits an end tag is parsed a second time, by the HTML tree builder. Each of 11 recorded Docusaurus pages of 33 to 143 KB takes 1.3 to 6.1 ms more (median 3.2 ms), which is 1.9 to 2.5 times as long, and the 70 recorded pages together take 290 ms in place of 252 ms. Pages with every end tag are not affected. The second parse is given up when it holds more than 512 elements open, which is the depth a browser builds. Such a page converts as it did before, so its omitted end tags are not repaired and it can still lose its tail. A repair that would lose text is not used: the text of the page is counted before and after the second parse, and a page that comes back with less text converts as it did before. A page that a `` replaces is such a page: the tree builder drops the text after ``, and the converter writes it. The omitted end tags of such a page are not repaired. A fragment with no `` tag keeps its top-level `
` when it is parsed a second time. Before, the second parse read every fragment as a document and dropped that `
` as a page header: `
h

one

` gave `**one**` and gives `h` and `**one**`, and `
h

one

tail` gives `h`, `one` and `tail`. The depth limit and its warning are unchanged for a page that really nests that deep. Past the depth limit, where the converter warns, the number of empty tables before the cut can differ; no text is lost. Two inputs are not repaired: an element that its parent ended and that gets its own end tag later (`

one

two

resttail`), and an unquoted attribute value that runs into an end tag (`
`) ([#772](https://github.com/xberg-io/html-to-markdown/issues/772)). * Markdown and Djot output write one space between the text of a `

` gave `All MethodsInstance Methods`. Where the source has white space, the output is the same. * An inline `` keeps its text and no longer adds the words `SVG Image`. Its text is its `aria-label`, the `` and `` of the graphic, its `` elements and the HTML in a ``, in document order. Style sheets, scripts, metadata and the content of `` and `` are not text, and a `` reference is not followed. A `` gives the child that a reader of English gets. An element with `display="none"` or `visibility="hidden"` gives nothing, as hidden text outside a graphic gives nothing. `aria-hidden="true"` removes the label, the title and the description. That text is the alt text of the image, and it is what `inline_data_media = alt_text_only`, a heading and plain text output write, with the escaping of any other text. A graphic with no text writes nothing there, and an image with an empty alt text when the payload is kept. An icon link no longer takes the style sheet of its graphic as its label ([#750](https://github.com/xberg-io/html-to-markdown/issues/750)). * A graphic in a heading is written as its text also with `extract_metadata = false` and `highlight_style = none`. With those options it was written as a `data:` image, because the fast converter has no rule for a heading. It now leaves a page with a graphic in a heading, with a hidden element in a graphic, or with a link that has a name and no text, to the full converter ([#766](https://github.com/xberg-io/html-to-markdown/issues/766)). * Two page shapes no longer take quadratic time in the pass that removes hidden elements: many tag starts with no end (`
t
a\nb\n
`, and the full and fast converters now write `"t\n\n a\n b"`. The spaces at the end of the last line of that code stay too ([#799](https://github.com/xberg-io/html-to-markdown/issues/799)). * Add `expand_abbreviations` and `include_blockquote_citations` options, enabled by default. * Keep one blank quote line between lists and blocks, preserving blank lines inside code. * Convert repeated unfinished tags, unclosed hidden elements, and inline list fragments with linear scans. * Repair a page that ends a paragraph again after the end tag of its parent, or whose head has no end tag, from the first parse with no second tokenizer pass; at 512 levels the repair stops and the input after that point is converted as written, past the node budget of the repair the whole page is converted as written, each with a `malformed_html` warning. * A line break in the source before an inline element is one space between two words, in every container. Before, the words on the two sides of a `` were joined, with an empty span and with a span that holds text. This changes the default output: `

hard to read\ndocstrings

` gave `hard to readdocstrings` and gives `hard to read docstrings`, `

one\ntwo three

` gave `onetwo three` and gives `one two three`, and `
  • one\ntwo
` gave `- onetwo` and gives `- one two`. Outside a paragraph the line break before any other inline element was a line break, and it is a space now, as a browser shows it: `
one\ny
` gave `one` and `**y**` on two lines and gives `one **y**`. A line break before a block stays a line break, and so does a line break before an inline element whose content starts with a block ([#778](https://github.com/xberg-io/html-to-markdown/issues/778)). * White space on the two sides of the start of an inline element is one space, and it goes outside the marks of the element: `

one ytwo

` gave `one **y**two` with two spaces and gives `one **y**two`. The same holds for ``, ``, ``, ``, ``, ``, ``, `` and ``. At the start of a line such an element starts with no space: `
a
x
` gave a space and `x` on the second line and gives `x`. * A line break is no space when an element or a comment stands between it and a zero-width space, as in a browser: the zero-width space is a place where a line can break, so the two parts are one word. `

one \n ​two

` gave `one`, a space, the zero-width space and `two`, and gives `one`, the zero-width space and `two`. The line break can be the last content of an element that only wraps text: `

one\n​two

` gave `one`, a space, the zero-width space and `two`, and gives `one`, the zero-width space and `two`. Two or more line breaks are one line break there: `

one\n\n​two

` gave two paragraphs and gives one word. A space that is not a line break stays. The converter keeps the space before a zero-width space only when the `style` attribute of the element between them, or of an element around them, sets `display` or `white-space`: `

one\n​two

` keeps the space, and `

one\n​two

` gives one word. A line break directly before the zero-width space in the same text is not changed: `

one\n​two

` keeps its line break. * A `