Skip to content

Migrating to v3

html-to-markdown began as a Python library — a fork of markdownify — focused on converting HTML to Markdown. Over time, the core was rewritten in Rust for performance, and generated packages were added for 15 language surfaces: Python, TypeScript, Ruby, Go, PHP, Java, C#, Elixir, R, C, WebAssembly, Swift, Dart, Kotlin Android, and Zig.

v3.0.0 rationalises the library’s API surface. Instead of multiple specialised functions, there is now a single convert() function that returns a structured ConversionResult. This simplifies the API while making all extraction capabilities available through one call.

hOCR (OCR-generated HTML) processing was removed from html-to-markdown and moved into Xberg, the document extraction library in the Xberg ecosystem. hOCR is a document-level concern — it belongs in the extraction pipeline, not in an html-to-markdown converter.

v2 had multiple functions for different extraction needs:

  • convert(html)String
  • convert_with_metadata(html, options, config)(String, Metadata)
  • convert_with_inline_images(html, options, config)Extraction
  • convert_with_tables(html, options)TablesResult
  • convert_with_visitor(html, options, visitor)String

v3 has one function:

  • convert(html, options?, visitor?)ConversionResult

ConversionResult contains all data:

  • content — the converted text (Markdown, Djot, or plain text)
  • metadata — HTML metadata (title, OG, links, images, structured data)
  • tables — extracted tables with cell data
  • document — structured document tree (when enabled)
  • images — extracted inline images where the binding exposes them (Rust and WASM when built with inline image support; generated native bindings may omit the Rust-only image payload)
  • warnings — non-fatal processing warnings
v2 v3
convert(html) convert(html).content
convert_with_metadata(html, opts, cfg) convert(html, opts).metadata
convert_with_inline_images(html, opts, cfg) convert(html, opts).images (set extract_images: true)
convert_with_tables(html, opts) convert(html, opts).tables
convert_with_visitor(html, opts, v) convert(html, opts, v).content
convert_to_string(html) convert(html).content
hocr_spatial_tables: true Use Xberg
ExtendedMetadata HtmlMetadata
start_profiling() Removed
markdownify(html) (Python) convert(html).content
// v2
let markdown = convert(html, None)?;
let (md, meta) = convert_with_metadata(html, None, cfg, None)?;
// v3
let result = convert(html, None)?;
let markdown = result.content.unwrap_or_default();
let metadata = result.metadata; // always available
# v2
markdown = convert(html)
markdown, metadata = convert_with_metadata(html)
# v3
result = convert(html)
markdown = result.content
metadata = result.metadata
// v2
const markdown = convert(html);
const { markdown, metadata } = convertWithMetadata(html);
// v3
const result = convert(html);
const markdown = result.content;
const metadata = result.metadata;
// v2
markdown, err := htmltomarkdown.Convert(html, nil)
markdown, meta, err := htmltomarkdown.ConvertWithMetadata(html, nil)
// v3
result, err := htmltomarkdown.Convert(html, nil)
markdown := result.Content
metadata := result.Metadata
# v2
markdown = HtmlToMarkdown.convert(html)
markdown, metadata = HtmlToMarkdown.convert_with_metadata(html)
# v3
result = HtmlToMarkdown.convert(html)
markdown = result.content
metadata = result.metadata
// v2
$markdown = HtmlToMarkdown\convert($html);
[$markdown, $metadata] = HtmlToMarkdown\convert_with_metadata($html);
// v3
$result = HtmlToMarkdown\HtmlToMarkdownApi::convert($html);
$markdown = $result->content;
$metadata = $result->getMetadata();
// v2
String markdown = HtmlToMarkdown.convert(html);
ConversionPair pair = HtmlToMarkdown.convertWithMetadata(html);
// v3
ConversionResult result = HtmlToMarkdown.convert(html);
String markdown = result.content();
HtmlMetadata metadata = result.metadata();
// v2
string markdown = HtmlToMarkdown.Convert(html);
(string md, Metadata meta) = HtmlToMarkdown.ConvertWithMetadata(html);
// v3
ConversionResult result = HtmlToMarkdownConverter.Convert(html, null);
string markdown = result.Content;
HtmlMetadata metadata = result.Metadata;
# v2
{:ok, markdown} = HtmlToMarkdown.convert(html)
{:ok, markdown, metadata} = HtmlToMarkdown.convert_with_metadata(html)
# v3
{:ok, result} = HtmlToMarkdown.convert(html)
markdown = result.content
metadata = result.metadata
# v2
markdown <- html_to_markdown(html)
result <- html_to_markdown_with_metadata(html)
# v3
result <- convert(html)
markdown <- result$content
metadata <- result$metadata
/* v2 */
char *markdown = htm_convert(html, NULL);
htm_free(markdown);
/* v3 */
HTMAlefHandle result = htm_convert(html, 0);
char *markdown = htm_conversion_result_content(result);
htm_free_string(markdown);
htm_conversion_result_free(result);

Separate from the v2 to v3 API change above, the C ABI itself changed. Objects no longer cross the boundary as opaque struct pointers. They are now HTMAlefHandle, a uint64_t handle into a registry owned by the library. The null handle is 0, so comparisons against NULL on these types become comparisons against 0.

Strings are unaffected: they are still char * and are still released with htm_free_string. The visitor callback struct HTMHtmVisitorCallbacks and the HTM_VISIT_* status codes are unchanged.

Only code that includes html_to_markdown.h and links the native library directly needs to change. The shipped language bindings are regenerated against the new ABI and their public APIs are the same.

/* before */
HTMConversionOptions *options = htm_conversion_options_from_json("{}");
HTMConversionResult *result = htm_convert(html, options);
if (result == NULL) {
fprintf(stderr, "%s\n", htm_last_error_context());
return 1;
}
char *markdown = htm_conversion_result_content(result);
htm_free_string(markdown);
htm_conversion_result_free(result);
htm_conversion_options_free(options);
/* after */
HTMAlefHandle options = htm_conversion_options_from_json("{}");
HTMAlefHandle result = htm_convert(html, options);
if (result == 0) {
fprintf(stderr, "%s\n", htm_last_error_context());
return 1;
}
char *markdown = htm_conversion_result_content(result); /* unchanged: still char* */
htm_free_string(markdown);
htm_conversion_result_free(result);
htm_conversion_options_free(options);

Where you declared T *, declare HTMAlefHandle. Where you compared against NULL, compare against 0. A bare truth test such as if (result) keeps working, because the null handle is zero.

Found a bug or mistake on this page?

If something here is wrong or out of date, open an issue on GitHub or contribute a fix via pull request.