NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #1831 most downloaded on PyPI
High-performance HTML to Markdown converter
Last release today
04 Oct 2026
Ships fairly regularly
a new release about every 9 days
Nearly every release is documented
notes for 59 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
198 releases · first in 2024
One column per month.
See CHANGELOG.md for v3.5.2 release notes.
See CHANGELOG.md for v3.5.2 release notes.
<!-- zig-fetch -->
Add to your build.zig.zon:
.dependencies = .{
.html-to-markdown-rs-zig = .{\n .url = \"https://github.com/kreuzberg-dev/html-to-markdown/releases/download/v3.5.2/html-to-markdown-rs-zig-v3.5.2.tar.gz\",\n .hash = \"html_to_markdown_rs-3.5.2-QtXyW34RAQCR-xKE80TXdB0wacI6n7na3yoAmNJ2t_sR\",\n },\n},\n```\n
bindings(csharp): trait-bridge facade methods now throw the per-binding HtmlToMarkdownRsException instead of an undefined KreuzbergException. The alef csharp backend's register/unregister facade method emitter was hardcoded to KreuzbergException (the kreuzberg core lib's exception class), causing every C# build to fail with CS0246: KreuzbergException could not be found. Fixed in alef 0.19.13 and pulled in by this regen.
bindings(swift): RustBridgeC.h placeholder now declares the RustStr C struct that SwiftBridgeCore.swift depends on. Without the typedef the Swift compiler reported cannot find type 'RustStr' in scope for every extension RustStr block before the full cargo build populated the real header. Fixed in alef 0.19.13.
bindings(swift): resolve Swift Package Manager "unsafe build flags" rejection for v3.5.2+. Swift 6.0+ strictly rejects packages with unsafeFlags in public products to prevent supply-chain code injection. The root Package.swift used by external consumers (via .package(url: "...", from: "...")) is now removed from git; it will be regenerated at release time with .binaryTarget pointing to pre-built xcframework/artifactbundle assets. For v3.5.2+, consumers will receive binary distribution (no Rust compilation required). For source builds, developers use cd packages/swift && swift build after cargo build -p html-to-markdown-rs-swift. The in-tree packages/swift/Package.swift retains unsafeFlags for local development. This fixes the blocking error for v3.5.1 external consumers attempting to depend on the package.
See CHANGELOG.md for full release notes.
See CHANGELOG.md for full release notes.
HtmlToMarkdown.convert(html, MyVisitor.new) against any object that responds to visit_* methods. The Elixir binding accepts HtmlToMarkdown.convert(html, %{visitor: %{"visit_link" => fn args -> ... end}}) with a function-keyed visitor map.<ul>/<ol> content is no longer triple-counted in Markdown output or the document structure collector.alef test-apps generate subcommand bundled into alef all, plus two new test_app channels: test_apps/homebrew/ and test_apps/php_ext/.publish-swift job removed (Swift Package Index auto-discovers tags within ~1h).actions/publish-hex bumped to v1.6.9 (generates Cargo.lock for native crates before mix hex.publish).bindings(ruby): expose ConversionOptions.visitor and wire the magnus visitor bridge end-to-end. The Ruby gem previously dropped VisitorHandle from codegen via [crates.ruby] exclude_types, so the documented HtmlToMarkdown.convert(html, MyVisitor.new) example silently ignored the visitor and emitted default markdown. Removed the exclusion — alef's magnus backend already implements the full visitor trait bridge, so the regen produces a working RbHtmlVisitorBridge that dispatches each visit_* callback via respond_to? + funcall and translates Ruby return values (:continue / :skip / :preserve_html / {custom: "..."}) into VisitResult variants. Closes #388.
bindings(elixir): expose ConversionOptions.visitor and wire the rustler visitor bridge end-to-end. Same fix as Ruby: removed VisitorHandle from [crates.elixir] exclude_types. The alef rustler backend already ships the bridge — a system thread runs the conversion, sends {:visitor_callback, ref_id, callback_name, args_json} to the caller, and blocks on HtmlToMarkdown.Native.visitor_reply/2 until the receive loop in HtmlToMarkdown.convert/2 dispatches the user's visitor map and replies. Visitor maps are keyed by callback name ("visit_link", "visit_text", …) with one-arity function values.
list: nested-list duplication in Markdown output and the document structure collector (PR #385, fixes kreuzberg#1004). push_list_item was previously called with output[item_start_pos..], which included the rendered Markdown of nested <ul>/<ol> children. Inner items' text was triple-counted: once as the item itself, once inside the parent item's text, and once as free text from the walker. A text_end_pos cursor now advances only past non-list children. Affects any ul > li > ul (or arbitrarily nested) shape.
alef.toml: dropped stale Ruby/Elixir vendor_mode = "core-only" overrides — alef now defaults source-build languages to vendor_mode = "registry" (the migrated install-from-crates.io flow), and the shared build-ruby-gem / build-elixir-hex actions enforce the same rewrite at workflow time. Overriding to core-only would silently vendor the core crate into packages/{ruby,elixir}/vendor/ and leave the manifest pointing at the workspace path, which fails to resolve on consumer machines.
crates/html-to-markdown-py/src/pyproject.toml: maturin manifest-path corrected to ../Cargo.toml (was ../../crates/html-to-markdown-py/Cargo.toml, which resolved to crates/crates/… from the pyproject's own directory). Local uv sync --upgrade / task upgrade now completes; the publish workflow builds via maturin build with explicit paths and was not affected.
packages/swift/rust/src/lib.rs: regenerated against alef 0.19.7 with the swift-bridge inbound trait phantom fix. The previous alef emitted Vec<Swift{Trait}BoxBox> in the extern "Rust" block (double-Box suffix), which swift-bridge-build rejected with "Type must be declared with type >". The inbound phantom is now omitted entirely — Swift{Trait}Box is an extern "Swift" type with no Rust-side struct backing it, and the Vec accessors are not consumed by the bindings we emit.
.task/languages/kotlin_android.yml + alef.toml [crates.update.kotlin_android]: the com.github.ben-manes.versions Gradle plugin (current pin 0.52.0, latest 0.53.0) throws java.util.ConcurrentModificationException under Gradle 9.5.1 on every :dependencyUpdates invocation; the plugin has not shipped a release since 2024-11. Both alef update --latest and task kotlin_android:upgrade previously failed the rest of the upgrade chain. Inert-echo the probe in both places until upstream lands a Gradle 9 fix or we swap to nl.littlerobots.version-catalog-update.
alef test-apps generate first-class subcommand, bundled into alef all as a discrete pipeline stage so registry-mode test_apps regenerate alongside the local-mode e2e suite. Per-stage stale-file sweep prevents either output dir (e2e/ vs test_apps/) from deleting the other's files (the bug that wiped half of test_apps/ when alef all ran in the previous topology).
Two new test_app channels emitted by the new subcommand: test_apps/homebrew/ (Brewfile + run_tests.sh + ffi_smoke.c — exercises both Homebrew formulae via brew bundle install and a pkg-config-linked C smoke test) and test_apps/php_ext/ (PIE-installed native ext driver — pie install xberg-io/html-to-markdown-ext then extension_loaded + convert smoke).
alef-generated build.gradle.kts for test_apps/kotlin_android/ — registry mode consumes the published dev.kreuzberg:html-to-markdown-android:3.5.x Maven artifact instead of the local workspace AAR.
Taskfile: e2e:smoke:homebrew and e2e:smoke:php_ext entries wired into the e2e:smoke:all aggregator.
.gitattributes: marks test_apps/** as linguist-generated=true (via the alef-scaffold change that now reads [e2e.registry].output).
docs/snippets/ruby/visitor/basic_visitor.md rewritten against the magnus-real API: visitor is a positional second argument to HtmlToMarkdown.convert, callbacks dispatch via respond_to? + funcall, return values are :continue / :skip / :preserve_html / { custom: "..." }, the ctx argument is a Hash with :node_type / :tag_name / :depth / etc. The previous snippet used a visitor: MyVisitor.new kwarg form and result[:content] accessor that don't match the gem's actual surface.
docs/snippets/elixir/visitor/basic_visitor.md rewritten against the rustler-real API: visitor is a %{"visit_*" => fn args -> ... end} map under the :visitor key of the options Hash; the bridge sends {:visitor_callback, ref_id, callback_name, args_json} messages and blocks on visitor_reply/2. The previous snippet used an aspirational use HtmlToMarkdown.Visitor behaviour form that alef does not generate.
publish-swift job removed from .github/workflows/publish.yaml. Swift Package Index has no central registry — packages are consumed directly from git tags, so the tag push IS the publish event. The previous check-spi-swift + publish-swift jobs only pinged SPI for fast re-indexing (SPI auto-discovers tags within ~1h without a ping). Cuts runner cost; SPI catches up automatically.
publish-zig retained — appends the tarball URL + SHA-256 to the GitHub Release notes via update-release-notes: true, which downstream consumers copy directly into build.zig.zon.
actions/publish-hex bumped to v1.6.9: generates Cargo.lock for every native/**/Cargo.toml before mix hex.publish. The lockfile is gitignored, so the publish action's fresh checkout had nothing to publish; this mirrors the build-elixir-hex fix from earlier in the v3.5.0 cycle.
task upgrade works end-to-end again after the Python and Kotlin Android fixes above.
See CHANGELOG.md for full release notes.
See CHANGELOG.md for full release notes.
First non-rc release in the 3.5.0 cycle. Promoted from v3.5.0-rc.3 after the publish workflow reached fully green (run 26393110006).
@kreuzberg/html-to-markdown-node-<target>).cargo generate-lockfile before mix hex.publish so Hex no longer fails on the gitignored NIF lockfile.-go- infix. Workaround for alef 0.19.6 packager collision with C FFI prefix; alef-side fix already on alef main.bindings: regenerated with alef 0.19.6. Node optional-dep package names now carry the @kreuzberg/ scope (@kreuzberg/html-to-markdown-node-<target>) so requireOptionalDependency() resolves the published per-platform packages instead of an unscoped name that does not exist on npm. test_apps/ restructured by alef to the new layout (per-language runners under test_apps/<lang>/{ffi,htm_test,run_tests} for C; legacy in-tree test files removed). Additional alef-emitter, swift-bridge, FFI param handling, and ahash-scaffold fixes carried through from the 0.19.x line.
ci(publish-hex): bump to xberg-io/actions@v1.6.9 — generate Cargo.lock before mix hex.publish. publish-hex@v1 runs a fresh actions/checkout, so the gitignored packages/elixir/native/html_to_markdown_nif/Cargo.lock is absent and mix hex.publish fails with Missing files: native/html_to_markdown_nif/Cargo.lock. v1.6.9 mirrors the build-elixir-hex fix and runs cargo generate-lockfile for every native/**/Cargo.toml before publishing. (v1 floating tag retagged.)
ci(publish): rename Go FFI tarballs to use -go- infix instead of -ffi-. alef 0.19.6's Go packager (alef publish package --lang go) emitted {crate}-ffi-v{version}-{platform}.tar.gz, colliding with the C FFI packager's prefix. check-registry asset-prefix probes and verify-release-assets pattern lists could not distinguish Go from C FFI tarballs, so verify-release-assets failed on the missing html-to-markdown-rs-go-*.tar.gz pattern. Workaround: rename -ffi-v → -go-v immediately after alef publish package --lang go in the workflow. The alef-side fix is already on alef main; the workaround can be dropped once the local alef pin moves to a release that ships it.
ci(actions/build-elixir-hex): generate Cargo.lock unconditionally after rewrite-native-deps. The fallback cargo generate-lockfile only ran on dry-run, but the lockfile is gitignored — real-release runs hit Missing files: native/html_to_markdown_nif/Cargo.lock at mix hex.build. Now runs in both modes. (xberg-io/actions v1.6.6, floating v1 retagged.)
ci(publish): split Dart pub.dev publishing into a workflow_dispatch flow so OIDC trusted publishing succeeds. pub.dev's OIDC verifier rejects tokens minted by release events (Authentication failed!) and only accepts push / workflow_dispatch. The publish workflow now assembles the Dart package as an artifact under the release-triggered run, then dispatches a separate publish-pubdev.yaml workflow (workflow_dispatch) that downloads the artifact and runs dart-lang/publish-pub@v1. The dispatched job inherits a token GitHub's OIDC provider mints with event_name == workflow_dispatch, which pub.dev's audit accepts.
ci(actions/homebrew-build-bottles): suppress brew config SIGPIPE under pipefail on arm64 Linux runners. The arm64 runner's /usr/bin/ldd writes more output than head -20 consumes; under set -o pipefail the broken-pipe propagated out of the early diagnostic block and aborted the script before any bottle work. Temporarily disables pipefail for the diagnostic stanza only — strict mode is restored before the build phase. (xberg-io/actions v1.6.7, floating v1 retagged.)
bindings: regenerated with alef 0.19.5. Picks up the Kotlin Android trait-bridge codegen fixes that caused the v3.5.0-rc.2 :compileReleaseKotlin failure (Unresolved reference 'HtmlToMarkdownRsBridge'): bridge_obj filename is now used for trait-bridge codegen so the file matches the object name; trait-bridge emission is skipped when the bridge function is excluded via kotlin_android.exclude_functions. Also picks up the alef 0.19.5 cumulative sweep: WASM emitter JSON-deserializes structured sub-config fields; swift-bridge restores JSON deserialization step in pre-call AHashMap binding; alef-emitter removes stray > after .collect::<Vec<Vec<String>>>(); PHP/Ruby/Elixir/Swift/Dart bridges emit pre-call AHashMap binding + ahash = "0.8" scaffold dep for Cow<'static, str> key map params; WASM wraps sanitized Vec<Vec<String>> fields with serde_wasm_bindgen::to_value(); WASM deduplicates input DTO struct generation across functions sharing the same config type; FFI preserves AHashMap<Cow<'static, str>, _> param types across the wrapper boundary; setup-defaults-ruby appends --add-checksums to default bundle install; scaffold-ffi injects workspace version into every internal workspace dependency so cargo publish accepts the FFI crate.
ci(publish): replace inline Homebrew formula updater with xberg-io/actions/publish-homebrew-source-formulas@v1. The publish-homebrew-formula job previously ran a 184-line scripts/publish/update-homebrew-formula.sh Bash heredoc that wrote html-to-markdown.rb + libhtml-to-markdown.rb from scratch (h2m is a dual-formula tap; the shared single-formula publish-homebrew@v1 doesn't apply). The new shared action does the same job from per-formula .rb.tmpl templates + a scripts/publish/homebrew.json manifest, downloading release assets via gh release download and substituting their SHA256s into ${cli_*_sha} / ${ffi_*_sha} placeholders. The script is deleted; the formula generation rules now live in version-controlled Ruby templates rather than a bash heredoc. The job's gate now also passes on dry_run == 'true' (was is_tag == 'true' only) so dry-run pipelines exercise the bottle pipeline downstream — the new action substitutes a zero-SHA placeholder for missing assets on dry-run.
list: Fix content duplication in Markdown output and the document structure collector when list items contain nested ul or ol children. item.rs previously captured the full rendered output of an <li> — including the rendered nested-list Markdown — as the item's text. A text_end_pos cursor now advances only past non-list children, so the structure collector records only the item's own text and the Markdown output for outer and mid items is not repeated. Affected: any ul > li > ul or ol > li > ol (arbitrarily nested) HTML structure. (#385)
ci(publish): skip publish-hex and homebrew-bottles on dry-run. Both jobs need real GitHub Release assets (generate-elixir-checksums downloads NIF tarballs; brew install --build-bottle downloads CLI/FFI source tarballs), but upload-release-assets@v1 only logs on dry-run — the release doesn't exist. Both jobs failed every dry-run after surviving every other stage. Gated their if: on dry_run != 'true'; real-release runs continue to exercise them.
ci(publish): replace inline Elixir Hex packaging with xberg-io/actions/build-elixir-hex@v1. The elixir-package job in .github/workflows/publish.yaml previously ran mix deps.get + mix hex.build inline with no path-dep rewrite and no Cargo.lock generation. Cargo.lock is gitignored, so on a fresh CI checkout mix hex.build failed at the files list check with Missing files: native/html_to_markdown_nif/Cargo.lock — the proximate blocker on v3.5.0-rc.2 dry-run after the Go FFI fix landed. The new shared action wraps rewrite-native-deps@v1 (default-on, dry-run-guarded) and falls back to cargo generate-lockfile on dry-run so the lockfile exists for mix hex.build even when the rewrite is skipped. Hex source-package builds now match the python-sdist / ruby-gem pattern (rewrite baked into the shared action; cannot be omitted).
ci(publish): build the Go FFI matrix on dry-runs too. .github/workflows/publish.yaml job go-ffi-libraries gated only on release_go == 'true' and the registry existence check, so on workflow_dispatch dry-runs it skipped entirely. Its downstream sibling upload-go-release already gates on is_tag || dry_run but waits for go-ffi-libraries.result == 'success', so no Go assets were ever uploaded to the dry-run release, and the terminal verify-assets gate failed with ✗ pattern NOT matched: html-to-markdown-rs-go-*.tar.gz on every recent retry (the immediate blocker on v3.5.0-rc.1 publish). Added the (is_tag == 'true' || dry_run == 'true') clause to go-ffi-libraries's if:, mirroring the kotlin-android-natives pattern (precedent: commit e00a56e1 "ci(publish): build Kotlin Android natives on dry_run too").
ci(e2e): pin erlef/setup-beam@v1.24.0 (was @v1.24 floating minor) to avoid silent action upgrades during the rc cycle.
ci(e2e/ruby): drop the explicit python3 scripts/ci/ruby/vendor-core-crate.py step from both ruby build jobs in .github/workflows/ci-e2e.yaml (committed earlier today as f23e6d458); the dead script itself is removed in f440af4fa. The shared xberg-io/actions/build-ruby-gem@v1 action now invokes rewrite-native-deps@v1 internally and vendors the core crate into packages/ruby/vendor/html-to-markdown/ (no -rs suffix). Running the local script first wrote packages/ruby/vendor/Cargo.toml with members = ["html-to-markdown-rs"] and copied the crate into vendor/html-to-markdown-rs/; the subsequent action then created a sibling vendor/html-to-markdown/ outside that members list, and cargo refused to build it with error inheriting 'lints' from workspace root manifest's 'workspace.lints' / 'workspace.lints' was not defined. The action is now the single source of truth for ruby vendoring.
ci(publish): pin actions/checkout@v5 on the homebrew-bottles matrix job. v6 hits an includeIf credential regression on the macos-15-intel runner that this matrix includes (same workaround liter-llm applies on its Homebrew matrix). Other jobs stay on @v6.
release: cut v3.5.0. Promoted from v3.5.0-rc.3 after the dry-run/republish cycle (publish run 26393110006) reached fully green (31 success / 0 failed / 56 skipped). Aligned every workspace manifest on 3.5.0 via alef sync-versions --set 3.5.0 + full alef generate regen.
release: cut v3.5.0-rc.1 release candidate. Aligned every workspace manifest (Cargo + npm + PyPI + Maven + Composer + Gemfile + Hex + pub.dev + Zig + R + Cocoa + nuget + Cargo.lock entries) on 3.5.0-rc.1 via alef sync-versions --set. Refreshed every per-language dependency tree to its current upstream pin (alef update --latest) and re-generated all bindings, READMEs, docs reference pages, and e2e suites against alef 0.18.1 so the regen artefacts on disk match the version pin baked into every binding manifest.
bindings: regenerated with alef 0.19.2. Picks up the cumulative v0.19.1 → v0.19.2 sweep: Swift codegen handles serde_rename_all = "lowercase" / "UPPERCASE" in unit enums and tagged Codable shapes (was silently emitting wrong casing); Swift now emits a custom Codable for serde-untagged data enums (the auto-derive previously used the wrong shape, surfacing as 19 runtime e2e failures); tagged-data enums route through JSONDecoder and Vec<Codable-enum> closure signatures are fixed. The trait-bridge codegen pipeline is rewired across all 14 language backends (rust/python/ts/node/wasm/ruby/php/go/c#/r/zig/elixir/dart/swift/java/kotlin-android) — super-trait lifecycle methods (name / version / initialize / shutdown) are driven by the IR instead of hardcoded literals, and canonical bridge-name helpers in Swift/Kotlin de-duplicate the prior near-misses. JNI no longer special-cases trait impl-name extraction; Kotlin Android accepts a bridge_class_name parameter so non-kreuzberg consumers (e.g. liter-llm) can opt out of the hardcoded KreuzbergBridge / kreuzberg:: references. Java's package_dir output_paths override now excludes setup/test/lint runs from .../src/main/java/ so they execute from packages/java/ as expected; JNI 0.22 compatibility was tightened (RuntimeMethodSignature parsing, borrow semantics, unsafe-block scoping, JString lifetime binding). Inherited from the v0.19.1 fix landed earlier today: magnus (Ruby) and rustler (Elixir) NodeContent::MetadataBlock reverse conversion now reconstructs Vec<(String, String)> from the sanitized Vec<Vec<String>> binding shape (the prior code didn't type-check and broke every Ruby build).
bindings: regenerated with alef 0.18.1. Picks up the v0.18.0 → v0.18.1 sweep: Java Builder #[serde(default)] non-optional fields use boxed nullable types so omitted JSON keys survive Jackson round-trip; PHP enum-variant accessor paths skipped before field validation; Python pyproject TOML arrays normalised to canonical pyproject-fmt shape; the Windows e2e runner no longer clobbers the inherited Path (case-insensitive env-key collision on std::process::Command::env); Ruby scaffold emits a Steepfile that ignores lib/<gem>/native.rb so Steep stops tripping on the Sorbet sigs the magnus backend deliberately emits; alef readme markdown normalisation matches rumdl-fmt MD012 (one blank max) so cold-regen READMEs no longer diverge from the alef all output; rubocop double-quote / %w[...] defaults emitted directly; Kotlin Android .gitkeep is empty (matches end-of-file-fixer); WASM optionalised non-Duration fields preserve core Default via the if-let wrapper; Box<str> round-trips through binding String via the new CoreWrapper::Box classifier; bare str resolves to TypeRef::String instead of falling through sanitize_type_ref; Zig test preamble installs SIG_IGN on SIGABRT so C++ destructor abort()s don't break the test-runner IPC; Java sealed-interface tagged-enum field defaults emit new EnumName.Variant() (records can't be statically referenced); PHP binding structs with custom core Default suppress the auto #[derive(Default)] and emit a delegating impl Default; doc-test import paths use the published crate name alef (post workspace-collapse); kotlin-android snapshot updated for vanniktech 0.36 (SourcesJar.Sources(), no-arg publishToMavenCentral()); Java e2e resolves per-fixture mock URLs from system properties; Java Vec<T>/Map<K,V> marshaling uses MAPPER.writerFor(constructCollectionType(...)) to preserve @JsonTypeInfo discriminators; PyO3 _to_rust_*_config aliases serde-renamed keys + final pyo3 calls use serde-renamed param names.
bindings: regenerated with alef 0.18.0. Workspace collapse + 0.17.36 → 0.18.0 cumulative codegen sweep. Notable fixes consumed in this regen: PyO3 replace_constructor_with_serde_rename skips the trait-bridge options field (visitor) from sorted_fields to avoid emitting it twice on has_default types with cfg-gated bridge fields (h2m all-features ConversionOptions::new previously failed rustc E0415: identifier 'visitor' is bound more than once); PHP e2e codegen sources the JSON key rename strategy from the language-effective serde_rename_all (camelCase by default) rather than the Rust core type's (which is None for h2m's ConversionOptions) so from_json reads the keys correctly into the binding struct's #[serde(rename_all = "camelCase")] — fixes 32 / 7-error PHP e2e failures; alef all repopulates current_gen_paths from the e2e cache manifest on a cache hit so the orphan-cleanup pass does not delete every previously-generated e2e file (157 deletes observed on first warm run); [crates.{node,wasm}.crate_dir] per-language override lets h2m point alef at its actual crates/html-to-markdown-{node,wasm} Rust crate dirs (without the -rs infix that the default formula assumed).
package READMEs: regenerated with cold alef readme. The 0.18.0 regen sweep used alef all whose hot-cache README pass omits the blank lines between consecutive ## section headers that the standalone alef readme pass emits cold. CI's Validate READMEs step (cold alef readme) caught the divergence; this commits the cold output. Same cosmetic alef bug as the prior dcd072a58 patch; tracked separately upstream.
bindings: regenerated with alef 0.17.36. Picks up the v0.17.18→v0.17.36 cumulative codegen sweep, including: Rustler from_json NIF shims gated on types with NIF wrappers (fixes Elixir NIF compile errors for ConversionOptionsUpdate, PreprocessingOptionsUpdate, NodeContext); Kotlin Android = PreprocessingOptions() synthesized defaults for non-nullable nested struct fields with Rust Default (fixes Jackson MissingKotlinParameterException on partial-options JSON, 98 → 0 failures); Kotlin sealed-class @field:JsonSerialize(as = …)/contentAs annotations; Kotlin @JsonIgnoreProperties(ignoreUnknown = true) + nullable default for #[serde(flatten)] fields; JNI crate_suffix = "-jni" build fix; Zig test_apps build.zig references binding module_name (not registry pkg_name) for root_source_file; Zig local-path dependencies in registry-mode test_apps; Dart pubspec single-caret version constraint; wasm package.json filenames use underscores (html_to_markdown_wasm.js); plus inherited 0.17.18-0.17.23 fixes (Swift visitor case continue, C# enum converters + nullable options, PHP with_visitor wither for trait-bridge fields, PyO3 streaming wrapper type identity, Go unresolved-Named fallback, Java sealed-interface display helpers, Ruby array literals, Dart positional-vs-named heuristic, R .alef_format_value wrapping, WASM camelCase input DTOs). Includes C visitor test suite (208 → 262 tests). The v0.17.25→v0.17.27 increment additionally brings: Rustler opaque-type NIF resource wrappers and enum-variant-field type collection (fixes cannot find type errors for types reachable only through enum variants); Kotlin Android LongMethod added to the generated @file:Suppress list, trait-interface emission skipped (no IDocumentExtractor/IRenderer redeclaration), Vec<u8> return types mapped to ByteArray, and integer-like float literals normalized; Java null (not "") builder default for Path fields and streaming adapters skipped; PHP #[php(name = …)] on facade static methods; Swift unwrap_or instead of unwrap_or_else in serde fallbacks (clippy -D warnings); WASM stops emitting the broken *Input config DTO; and the extractor skips underscore-prefixed pub fns (test-only helpers no longer leak into bindings or docs). The v0.17.28→v0.17.32 increment additionally brings: C FFI struct-field getters for nested struct and Option<T> fields now return a cloned, boxed value instead of null (htm_html_metadata_document, htm_conversion_result_document, htm_conversion_result_metadata); the Node index.d.ts/index.js are emitted by NAPI-RS at build time and no longer carry an alef generated-header; plus Swift visitor VisitResult, Java sealed-interface, and Ruby scaffold corrections. The v0.17.32→v0.17.34 increment additionally brings: a LICENSE file synced into every per-language package directory (pub.dev, RubyGems, and other registries require one); per-language READMEs that render only their own binding section instead of leaking every language's bullet; the PHP e2e harness no longer re-execs PHP with -n (PHPUnit keeps shared modules); the Ruby scaffold's .rubocop.yml excludes generated lib/**/*.rb; PHP streaming facade methods + snake_case adapter names; PyO3 keyword-escaping and signature defaults in serde-rename constructors; Java clear_fn (int, String) error constructor; and C/Zig/C# e2e codegen compile fixes. The v0.17.34→v0.17.35 increment additionally brings: PyO3 trait-bridge constructor signatures and dict-coercion exclude the synthetic bridge field (e.g. visitor) — fixes spurious visitor=... kwargs on generated Python constructors and stale dict round-trips; magnus RBS type aliases use lowercase identifiers (json_value) — invalid uppercase JsonValue was breaking steep check; ext-php-rs getters for Option<NonOpaqueNamed> explicitly .map(Into::into) instead of relying on the blanket IntoZval impl that did not unwrap the inner type; Dart e2e fixture codegen emits a default ConversionOptions() instance for absent required-positional options; Java options_type inherits from sibling language overrides; CLI --version syncs packages/zig/build.zig.zon; kotlin-android publishing switched to the vanniktech maven-publish plugin for Maven Central Portal. The v0.17.35→v0.17.36 increment additionally brings: the /* serde(default) */ placeholder marker (used as a required-suppression flag for C#) is filtered before emission in alef-codegen and alef-backend-java — generated Java now contains List.of() / false / 0 instead of /* serde(default) */, and generated magnus Rust uses the type's actual zero value (vec![] / false); kotlin-android .editorconfig disables ktlint's trailing-comma-on-call-site and -on-declaration-site rules (ktfmt strips them, ktlint demands them — the two were fighting on build.gradle.kts after the vanniktech migration); brew unsupported-call unsupported_in configuration; csharp variant-struct payload type fix; alef-e2e elixir Rustler NifTaggedEnum tuple emission for tagged-enum array args; publish workflow orders alef-scaffold ahead of backends.
Taskfile: add dart:e2e, swift:e2e, zig:e2e, kotlin_android:e2e convenience tasks so all 16 language bindings have a uniform per-language local e2e entry point matching the existing c:e2e / python:e2e / node:e2e / etc. pattern. Each delegates to alef test --e2e --lang <name>; the previously-existing e2e:test:<lang> tasks (which run against the published-package test_apps/ registry tree) stay alongside. Verified all 16 suites pass locally: c (262), csharp (262), dart (262), elixir, go, java, kotlin_android, node (262), php (262 — after the alef PHP rename fix), python (262), r (639), ruby (262), rust, swift (262), wasm (263), zig.
e2e/php: skip visitor fixtures in fixtures/edge-cases/visitor_errors.json — five visitor fixtures (visitor_custom_element_with_nesting, visitor_unknown_tag_preservation, visitor_deeply_nested_skip, visitor_element_start_skip_entire_subtree, visitor_element_end_modification) now carry "skip": { "languages": ["php"] } matching the rest of the visitor fixtures already in fixtures/visitor/*.json. The PHP ext-php-rs binding does not yet support callable visitor handles (no VisitorHandle::from_php_object() method), so these tests were emitting unbuildable test code. Generated PHP tests dropped from 262 to 208; suite is fully green.
e2e/kotlin_android: build the JNI crate in the e2e before-hook — the Kotlin/Android host-JVM e2e loads libhtm_jni (the html-to-markdown-rs-jni crate's [lib] name = "htm_jni" cdylib), not the C FFI dylib. The [crates.test.kotlin_android] before-hook in alef.toml was building html-to-markdown-ffi, so all 208 tests failed with UnsatisfiedLinkError: no htm_jni in java.library.path. It now builds html-to-markdown-rs-jni; the suite is fully green.
e2e/node: invoke the napi build via pnpm run build in the e2e before-hook — the [crates.test.node] before-hook ran a bare napi build, which is not on PATH in CI (the @napi-rs/cli binary lives in node_modules/.bin). The before-hook now runs pnpm install && pnpm run build so napi resolves from the crate's own package.json build script, fixing sh: 1: napi: not found.
core: fix panic slicing output at a non-UTF-8 char boundary — paragraph.rs captured content_start_pos = output.len() before appending a separator; a subsequent output.pop() in the whitespace-normalisation path of handle_span could shift the effective boundary one byte back, landing it mid-codepoint (e.g. inside U+25A0 ■). The structure-collector slice output[content_start_pos..] then panicked. Fixed by clamping with floor_char_boundary before slicing; the same clamp is now applied at the two analogous sites in figure.rs. Triggered by include_document_structure = true with a <pre> preceding block and a <span> whose first character is multibyte. (#380)
core: avoid stack overflow on documents with many unclosed list items — preprocessing now applies the HTML5 implicit-close rule for <li>, <dt>, and <dd> before the tl parser sees the document, so a 15k-item changelog with bare <li> tags (e.g. https://curl.se/changes.html) is parsed as siblings instead of a 448-deep chain. Eliminates a process abort (fatal runtime error: stack overflow) that bypassed Result::Err and std::panic::catch_unwind. (#379)
docs/installation.md and the per-package Python READMEs now document pip install --only-binary=:all: html-to-markdown and how to keep MSVC's link.exe ahead of GNU/Cygwin link on PATH when sdist fallback is unavoidable. (#378)NodeContent::MetadataBlock { entries } round-trips correctly as [[k,v],...] — the entries: Vec<(String, String)> field is now stored as JsValue in the wasm binding struct and serialized/deserialized via serde_wasm_bindgen instead of Vec<String>, preserving the nested-array wire format that serde produces for tuple vecs.compact_tables option — set compact_tables: true on ConversionOptions to emit GFM tables with no column padding. Cells are flushed to content width and separator rows use exactly --- per column, producing token-efficient output for RAG / LLM pipelines. Default false; existing output is unchanged.
Kotlin Android binding — dev.kreuzberg:html-to-markdown-android on Maven Central. Standalone Android library (AAR) with bundled libhtml_to_markdown_ffi.so for arm64-v8a and x86_64 ABIs; minSdk 21, compileSdk 35. JVM Kotlin users continue to consume the existing Java package (dev.kreuzberg:html-to-markdown) directly — Kotlin/JVM treats Java classes as native and Panama FFM is unavailable on Android, which is why Android needs its own package.
Swift binding — HtmlToMarkdown Swift Package on Swift Package Index. SPM-only (no CocoaPods); macOS 13+, iOS 16+; powered by swift-bridge.
Dart binding — h2m on pub.dev. Built with flutter_rust_bridge 2.12; supports Flutter Android/iOS targets plus server Dart on Linux/macOS/Windows. Package name h2m because html_to_markdown and html-to-markdown are taken on pub.dev.
Zig binding — published via GitHub Releases (build.zig.zon + tarball SHA-256 in release notes). Requires Zig 0.16+; links the existing html_to_markdown_ffi C library — no separate Rust bridge crate.
ffi: htm_visitor_handle_from_callbacks exports a vtable-style visitor-handle constructor for zig and other C consumers. Wraps a HtmVisitorCallbacks struct into the VisitorHandle shape expected by htm_conversion_options_builder_visitor.
html-to-markdown 3.4.0 — high-performance HTML to Markdown converter with a Rust core and polyglot bindings (Python, Node/TypeScript, Ruby, PHP, Go, J
html-to-markdown 3.4.0 — high-performance HTML to Markdown converter with a Rust core and polyglot bindings (Python, Node/TypeScript, Ruby, PHP, Go, Java, C#, Elixir, R, WebAssembly, C FFI).
| Language | Command |
|---|---|
| Rust | cargo add html-to-markdown-rs |
| Python | pip install html-to-markdown |
| Node / TS | npm install @kreuzberg/html-to-markdown |
| WASM | npm install @kreuzberg/html-to-markdown-wasm |
| Ruby | gem install html-to-markdown |
| PHP | pie install kreuzberg-dev/html-to-markdown-rs |
| Go | go get github.com/kreuzberg-dev/html-to-markdown/packages/go/v3 |
| Java | Maven Central: dev.kreuzberg:html-to-markdown:3.4.0 |
| C# | dotnet add package KreuzbergDev.HtmlToMarkdown |
| Elixir | {:html_to_markdown, "~> 3.4"} in mix.exs |
| R | install.packages("htmltomarkdown") |
| Homebrew (CLI) | brew install kreuzberg-dev/tap/html-to-markdown |
| Homebrew (lib) | brew install kreuzberg-dev/tap/libhtml-to-markdown |
html-to-markdown (CLI) and libhtml-to-markdown (FFI library + headers + pkg-config + CMake configs). Pre-built tarballs for macOS arm64/x86_64 and Linux arm64/x86_64; install with brew install kreuzberg-dev/tap/html-to-markdown.web, bundler, nodejs, deno) under @kreuzberg/html-to-markdown-wasm.KreuzbergDev.HtmlToMarkdown with native runtimes for linux-x64, linux-arm64, osx-x64, osx-arm64, win-x64, win-arm64.dev.kreuzberg:html-to-markdown bundling native libraries for the same six platforms via META-INF/native/<rid>/.rustler_precompiled NIFs for Linux + macOS (NIF 2.16/2.17 × 3 platforms); released artifacts download at first run.pie install kreuzberg-dev/html-to-markdown-rs no longer requires building from source.panic::catch_unwind instead of partial output + Rust backtrace.HtmlVisitor parity across all bindings — Python, Node/TypeScript, Ruby, PHP, Go, Java, C#, Elixir, R, and WASM all expose the visitor interface with visit_element_start/visit_text/visit_element_end and VisitResult::{Continue, Skip, Custom} semantics matching the Rust core.alef.toml + Rust source of truth, eliminating drift across the polyglot surface.OutputFormat::Plain ignored HtmlVisitor callbacks. The plain-text walker (crates/html-to-markdown/src/converter/plain_text.rs) ran the markdown pipeline first, then discarded its output and re-traversed the DOM via a visitor-less walk_plain, so VisitResult::Custom/Skip returned from visit_element_end/visit_text was silently dropped for Plain. Threaded a WalkState carrying the visitor through the plain walker so element/text hooks fire and their results are honoured.<img src> URLs not escaped, breaking CommonMark round-trip. crates/html-to-markdown/src/converter/handlers/image.rs emitted src raw, while <a href> already wrapped spaces/parens in angle brackets. Image renderer now uses the same three-branch escaping as links: empty → <>, contains space/newline → <URL>, unbalanced parens → \(/\) escaping.<td><p class='MsoNormal'>…</td> appears as the leading cell. The tl parser absorbs subsequent <td> and document content into the unclosed <p>, nesting the rest of the DOM inside the first table cell. Extended has_inline_block_misnest in converter/preprocessing_helpers.rs with a has_p_ancestor check that detects td/tr/th under <p> (structurally impossible in valid HTML) and triggers the existing html5ever repair path.</tagname\n> corrupted DOM and dropped content. JSX-style HTML (closing-tag > on the next line) caused the tl parser to leave elements unclosed, which silently absorbed siblings and dropped entire sections — affecting #127 (MW841 product headings missing from multilingual page), #143 (word-wrap merging nested link list items), and #121 (SPA menu nesting). New normalize_split_closing_tags preprocessing pass collapses such patterns to </tagname> before parsing, wired into all four preprocessing branches in converter/main.rs.max(3, col_width) dashes per column. * and _ are escaped in table cells regardless of escape_misc. Fixes the gh-140 fixture parity and produces CommonMark-conformant tables out of the box.astral-tl parser silently discarded every byte after <!-- /// ---> or any --[-]+> comment terminator. New normalize_bogus_comment_endings preprocessing pass rewrites such sequences to --> before parsing; wired into the html5ever-repair and inline-block-misnest fallback paths too.latest dist-tag. Pre-release versions (matching -(rc|beta|alpha|pre|dev)) now publish under the next dist-tag, so npm install @kreuzberg/html-to-markdown-node no longer pulls a 3.4.0-rc over a stable 3.3.x.from html_to_markdown import HeadingStyle raised TypeError. The package now re-exports the native PyO3 enums directly from _html_to_markdown and adds uppercase aliases (HeadingStyle.ATX, CodeBlockStyle.BACKTICKS) so both naming conventions satisfy ConversionOptions(heading_style=…).HtmlToMarkdown.convert(html, options) raised TypeError on every call with options. The wrapper passed a ConversionOptions object to the FFI, but the generated Rust function expects Option<String> JSON. Wrapper now serialises the options hash to JSON before crossing the FFI boundary.default-features = false Rust build broken. Bare #[serde(...)] and #[derive(Serialize, Deserialize)] on core types in src/types/{document,tables,result,warnings}.rs and src/options/conversion.rs are now feature-gated behind #[cfg_attr(feature = "serde", ...)]. CI now runs a cargo check --no-default-features matrix to prevent regressions.element_start/element_end events mispaired for hyphenated/namespaced custom tags. The repair_with_html5ever fallback re-parsed under HTML5 semantics, which discard XML-style self-closing on unknown elements. The repair path now pre-expands XML self-closing tags on non-void elements to explicit open+close pairs before the HTML5 parse.setVisitor() method added to ConversionOptions.result_is_r_list configured to suppress jsonlite double-wrapping of conversion results.pnpm-workspace.yaml declares onlyBuiltDependencies: [esbuild] and ignoredBuiltDependencies: [wasm-pack] for the new opt-in build script policy.org.jetbrains:annotations 26.0.0 → 26.1.0, plus updates across all language toolchains via task upgrade.Elixir visitor bridge — async thread-based visitor protocol (rustler 0.37).
.formatter.exs.maven-checkstyle-plugin with 120-char config.VisitResult.Continue — new VisitResult.Continue().convert export — restored missing function.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.3.0...v3.3.2
rustler::thread::spawn + OwnedEnv::send_and_clear + mpsc channels, replacing the impossible synchronous env.call() approach.SavedTerm, is_nil(), Pid::spawn_monitor, .encode() APIs with 0.37-compatible equivalents.map(Some)) and ambiguous From impl in generated _from methods.maven-checkstyle-plugin to pom.xml pointing to project checkstyle.xml (120-char limit), so mvn checkstyle:check uses our config instead of default Sun checks.Bundler::GemHelper.install_tasks name: for Bundler 4 compatibility.Java checkstyle — switched to 120-char line limit, added Spotless auto-formatting with Eclipse JDT formatter, added final params and javadoc to all ge
final params and javadoc to all generated code.list type collision — NodeContent::List variant no longer redefines Elixir's built-in list/0 type (now emits list_variant).serde — added serde with derive feature as direct dependency to the NIF crate.VisitResult.Continue — default visitor methods now use new VisitResult.Continue() instead of non-invocable VisitResult.Continue().convert export — restored the missing #[napi] pub fn convert function dropped during binding regeneration.Gemfile.lock.`exclude_selectors` option — CSS selector-based element exclusion. Unlike strip_tags (which removes the wrapper but keeps children), excluded elements
exclude_selectors option — CSS selector-based element exclusion. Unlike strip_tags (which removes the wrapper but keeps children), excluded elements and all descendants are dropped entirely. Supports any CSS selector: .class, #id, [attribute], compound selectors. Works in both markdown and plain text output modes.--preserve-tags, --skip-images, --max-depth for full ConversionOptions parity.exclude_selectors, ConversionResult.tables, and ConversionResult.warnings.VERSION constant. Gemspec now includes sig/**/*.alef-verify hook added to .pre-commit-config.yaml to check generated code freshness. CI installs alef v0.5.3 binary.<h1> inside <header> not exported (#321) — top-level <header> elements were unconditionally dropped during preprocessing; now only <header> with navigation hints (e.g. class="site-header", role="navigation") is removed.PreprocessingPreset not wired into preprocessing logic — the preset field on PreprocessingOptions was defined but never checked. Now Minimal/Standard/Aggressive presets have distinct behavior.remove_forms flag was dead code — <form> elements are now dropped when remove_forms: true and preset is Standard or Aggressive.<noscript> elements, and noise-hinted elements (cookie banners, ad containers).NodeContent type — binding wrapper now implements Default, Serialize, Deserialize via forwarding to core type, fixing compilation when DocumentNode (which contains NodeContent) derives these traits.dict[str, Any] for data enums — NodeContent, AnnotationKind, VisitResult now use TypedDicts with Literal discriminators instead of untyped dicts.__init__.py exports — all public types now exported from the package.ImageMetadata.dimensions — tuple type (u32, u32) correctly maps to Vec<u32> / number[] / []uint32 in all bindings via serde round-trip conversion./// on every line (was breaking cargo fmt).title, id, lang) correctly generate Option<&str> instead of &str.use std::ffi::... in trait bridge output.htm_convert stub — FFI function was always returning "Not implemented"; now delegates to core::convert(html, options, None).? syntax, fixed parameter syntax, NodeType mapping, variable shadowing, gofmt indentation.convert() TypeError (#319) — options type mismatch and wrong return type.from_update and from methods now emit safe return values instead of panic!().ConversionOptionsBuilder — fixed todo!() panics in opaque type delegation.autolinks default — replaced --autolinks with --no-autolinks so defaults match library.abi3-py310 stable ABI for PyO3 crate, so a single wheel works on Python 3.10 through 3.14+ without per-version builds.pub to pub(crate). API docs now document only the public API (down from 233 to 66 items).cargo fmt, cargo clippy, mypy, ruff, gofmt, dotnet format, mix format, steep check, rubocop, phpstan, biome).ci.yaml.Python type mismatch — convert() return type annotation now uses the public ConversionResult instead of _rust.ConversionResult, fixing Pylance type er
convert() return type annotation now uses the public ConversionResult instead of _rust.ConversionResult, fixing Pylance type errors when annotating with the re-exported type (#310).readme = "README.md" from binding crate Cargo.toml files (ffi, node, php, py, wasm) since no README exists for these internal crates (#309).^12.5 to ^13.1 across root, e2e, and test_apps.v3.14.2-1 to v3.9.0-1.v3.14.0 to v3.14.2.Elixir Hex package — fixed precompiled NIF pipeline: correct build output paths, removed Rust source from Hex files list, simplified build for rustler
rustler_precompiled.htm_conversion_options_from_json and related FFI functions.See CHANGELOG.md for full details.
rustler_precompiled.htm_conversion_options_from_json and related FFI functions.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.2.3...v3.2.3
Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.2.3...v3.2.3
htm_conversion_options_from_json, htm_preprocessing_options_from_json, and related to_json functions now generated. Fixed alef IR extraction to detect serde derives inside #[cfg_attr(...)] attributes.index.js with correct NAPI platform-aware loader (was referencing old html-to-markdown-rs.node binary name).github.com/xberg-io/html-to-markdown-go to monorepo path github.com/xberg-io/html-to-markdown/packages/go/v3.Rustler (compile-from-source) to RustlerPrecompiled with CI jobs for building and uploading platform-specific NIF binaries to GitHub releases.html-to-markdown-rs-ffi, html-to-markdown-rs-wasm).html-to-markdown_rb with wrong naming).Ruby binding compilation — fixed serde derive errors, Default trait conflicts, and deprecated Magnus API usage.
readme, keywords, categories, description fields.See CHANGELOG.md for full details.
magnus::exception::type_error() / runtime_error() with Ruby::exception_type_error() / Ruby::exception_runtime_error() (Magnus 0.7+ API).readme, keywords, categories, description fields to binding crate Cargo.toml files.Node.js Docker/cross-platform installs (#273) — platform-specific native packages now correctly published with optionalDependencies via NAPI prepublis
optionalDependencies via NAPI prepublish.WasmConversionOptions (not JsConversionOptions) via configurable type_prefix in alef.NodeContent::MetadataBlock type mismatch in binding-to-core conversion.pom.xml with kreuzberg (GPG plugin in main build, developer email, pluginManagement).From impls, glob import conflicts, cbindgen compatibility.compilers: [:rustler] directive for Rustler 0.34+.from_json/to_json functions — now generated for all serde-compatible types, fixing Java and Go bindings.pyproject.toml — corrected module-name and python-packages.docs/llms.txt metadata defaults corrected (#276).See CHANGELOG.md for full details.
Node.js Docker/cross-platform installs (#273) — platform-specific native packages (@kreuzberg/html-to-markdown-node-linux-x64-gnu, etc.) are now correctly published with optionalDependencies via NAPI prepublish, resolving cross-platform lockfile issues.
Homebrew formula (#304) — formula updated with correct source tarball SHA and bottle configuration.
Ruby gem build failure — fixed NodeContent::MetadataBlock type mismatch (Vec<(String, String)> → String) in binding-to-core conversion by deserializing sanitized fields from JSON.
Maven Central publish — aligned pom.xml with kreuzberg: GPG plugin in main build section, developer email, correct groupId (dev.kreuzberg), pluginManagement with pinned plugin versions.
All binding compilation failures — fixed private module path (convert_api) in generated bindings, missing From impls for function return types, glob import conflicts in PyO3/FFI backends, and cbindgen compatibility (removed const extern fn, updated to cbindgen 0.29).
Elixir NIF compilation — added compilers: [:rustler] ++ Mix.compilers() to mix.exs so Rustler compiles the NIF during mix compile.
FFI from_json/to_json functions — htm_conversion_options_from_json, htm_conversion_result_to_json, etc. now generated for all serde-compatible types, fixing Java (Panama FFM) and Go (cgo) bindings.
PHP e2e tests — fixed function call generation to use correct HtmlToMarkdownRs::convert() pattern.
Python pyproject.toml — corrected module-name and python-packages to match html-to-markdown pip package name.
docs/llms.txt metadata defaults corrected from false to true for extract_metadata, extract_document, extract_headers, extract_links, extract_images, extract_structured_data (#276).
WASM type prefix restored (#303) — WasmConversionOptions (not JsConversionOptions) via configurable type_prefix in alef. No breaking change for WASM users.
convert() silently truncates output at ~439 KB on certain large HTML inputs. Under investigation.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.2.0...v3.2.0
Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.2.0...v3.2.0
code_block_style default changed from Indented to Backticks — code blocks now use triple-backtick fences by default instead of 4-space indentation.bullets default changed from "-" to "-*+" — nested unordered lists now cycle through -, *, + at successive nesting levels.preprocessing.enabled default changed from false to true — HTML preprocessing (navigation removal, form stripping) is now on by default.camelCase to snake_case — affects JSON serialization/deserialization of the Rust core ConversionOptions struct (heading_style instead of headingStyle). Language bindings are not affected — each binding uses its language-native naming convention (camelCase for JS/TS/Java/C#, snake_case for Python/Ruby/Elixir/R).@kreuzberg/html-to-markdown to @kreuzberg/html-to-markdown-node.HtmlToMarkdown\ to Html\To\Markdown\Rs\. Main class renamed from HtmlToMarkdown to HtmlToMarkdownRs.dev.kreuzberg:html-to-markdown. Internal package namespace changed to dev.kreuzberg.htmltomarkdown.html_to_markdown_ to htm_ (e.g., htm_convert, htm_last_error_code). Header moved to include/html_to_markdown.h.HtmlToMarkdownConverter to HtmlToMarkdownRs.HtmlToMarkdownError, EmptyHtmlError, InvalidParserError, etc.) replaced with new hierarchy: ConversionError, ParseError, SanitizationError, ConfigError, IoError, InvalidInputError, PanicError, OtherError.ConversionOptionsBuilder is now public in the Rust API — use ConversionOptions::builder() for ergonomic option construction.TableData exported from crate root — no longer requires importing from submodules.docs/reference/api-{lang}.md pages for Python, TypeScript, Go, Java, C#, Ruby, PHP, Elixir, WASM, and C with full type mappings, signatures, and docstrings.alef.toml.fixtures/.AnnotationKind and NodeContent now implement Default.ConversionResult now derives Serialize/Deserialize.zensical.toml replaces mkdocs.yaml).alef readme from minijinja templates with inline configuration in alef.toml.task version:sync now uses alef binary for all operations (replaces sync_versions.py).crates/html-to-markdown-bindings-common — shared bindings helper crate replaced by alef codegen.tools/e2e-generator — replaced by alef e2e generate.tools/snippet-runner — snippet validation removed.scripts/generate_readme.py, scripts/sync_versions.py, scripts/readme_filters.py — replaced by alef.deny_unknown_fields added to serde option structs — invalid JSON fields now produce errors instead of being silently ignored.link_style field.Reference-style links: New link_style option ("inline" default, "reference") renders links as [text][1] with numbered [1]: url "title" definitions app
link_style option ("inline" default, "reference") renders links as [text][1] with numbered [1]: url "title" definitions appended at the end of the output. Supports URL+title deduplication, images (![alt][1]), and media elements (audio, video, iframe). Available across all bindings (Python, Node.js, WASM, PHP, CLI --link-style, FFI via JSON).Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.0.2...v3.1.0
Structure collector in tables: Suppressed StructureCollector calls for headings and lists inside table cells, preventing spurious document-structure n
StructureCollector calls for headings and lists inside table cells, preventing spurious document-structure nodes from table content.generate_id hash truncation and list item text extraction.document-structure feature gates that were no longer wired to any Cargo feature.StructureCollector calls for lists, images, and code blocks so document structure captures all block-level elements.str::floor_char_boundary (stable since Rust 1.91) with crate-local helper to maintain MSRV 1.85.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v3.0.1...v3.0.2
WASM TypeScript types: convert() now returns typed WasmConversionResult instead of any. All WasmConversionTable, WasmGridCell, WasmTableGrid, WasmConv
convert() now returns typed WasmConversionResult instead of any. All WasmConversionTable, WasmGridCell, WasmTableGrid, WasmConversionWarning, and WasmInlineImage interfaces are now emitted in generated .d.ts files. Added missing options fields (skipImages, outputFormat, includeDocumentStructure, extractImages, maxImageSize, captureSvg, inferDimensions). Fixes #265..pyi stub with package stub — added keyword-only (*) parameter markers and visitor parameter to convert().ConversionResult, ConversionOptions, and all nested type shapes. Wired stubs into composer.json PHPStan config.Single `convert()` API: One entry point across all 12 language bindings returning ConversionResult with content, document, metadata, tables, images, a
convert() API: One entry point across all 12 language bindings returning ConversionResult with content, document, metadata, tables, images, and warnings.ConversionResult type: Structured result with content (markdown/djot/plain), document (optional DocumentStructure), metadata (HtmlMetadata), tables (grid-based), images (inline image data), and warnings.DocumentStructure: Structured document tree with flat node array, index-based parent/child references, and TextAnnotation for inline formatting.includeDocumentStructure, extractImages, maxImageSize, captureSvg, inferDimensions, outputFormat (markdown/djot/plain).<q> element: Wraps content in quotation marks.<figure>/<figcaption> elements: Routed to semantic handler with caption separation.hidden attribute: Elements with hidden stripped before parsing.convert() returns ConversionResult instead of String in all bindings (Go, Java, C#, Node, Python, PHP, Ruby, Elixir, R, WASM, C FFI).ExtendedMetadata renamed to HtmlMetadata across all crates and bindings.Convert() returns *ConversionResult with Content, Metadata, Tables, Images, Warnings fields. Accepts optional JSON options via variadic parameter.TableGrid with GridCell) instead of flat cells [][]string.serde(deny_unknown_fields) on MetadataConfig, MetadataConfigUpdate, InlineImageConfigUpdate.convert_with_* functions: convert_with_metadata, convert_with_inline_images, convert_with_visitor (standalone), convert_with_tables, convert_with_async_visitor removed from public API. Single convert() replaces all.async-visitor Cargo feature, AsyncHtmlVisitor trait, async bridge/dispatch code).start_profiling/stop_profiling APIs).hocr module deleted. The hocr_spatial_tables option removed.convert_to_markdown() and markdownify() removed.hOCR support: The hocr_spatial_tables option and all hOCR-related APIs are deprecated and will be removed in v3. All hOCR functionality continues to w…
hocr_spatial_tables option and all hOCR-related APIs are deprecated and will be removed in v3. All hOCR functionality continues to work but emits deprecation warnings.@var annotations and is_array() runtime checks in ExtensionBridge.php. Removed redundant array_values() calls in ConversionOptions.php. Updated PHPStan baseline count for callable invocations.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.29.0...v2.30.0
hocr_spatial_tables option and all hOCR-related APIs are deprecated and will be removed in v3. All hOCR functionality continues to work but emits deprecation warnings. This is the final v2 release.@var annotations and is_array() runtime checks in ExtensionBridge.php that PHPStan flagged as always-true due to stub-defined return types. Removed redundant array_values() calls in ConversionOptions.php on properties already typed as list<string>. Updated PHPStan baseline count for callable invocations.Case-insensitive meta tag matching per HTML spec (fixes #251)
DC.* and DCTERMS.* meta tags mapped to structured fieldsnews_keywords, citation_keywords, DC.subject, topic, category, etc.full feature group added to core and all binding crates, enabled by defaultcargo-sort, checkmake, oxlint, typescript-typecheck)None — all changes are backward compatible.
See CHANGELOG.md for full details.
full feature group: Added a full feature to core crate and all binding crates (PHP, Python, Node, WASM, FFI, Elixir, bindings-common) that enables all available features. All bindings now default to full.DC.* and DCTERMS.* meta tags now map to dedicated DocumentMetadata fields (title, description, author, keywords). Other DC/DCTERMS fields stored in meta_tags with dc_/dcterms_ prefix.news_keywords, citation_keywords, DC.subject, DC.keywords, DCTERMS.subject, subject, topic, category, and classification meta tags.cargo-sort pre-commit hook: Added for consistent Cargo.toml key ordering.checkmake pre-commit hook: Added for Makefile linting.typescript-typecheck pre-commit hook: Added TypeScript type checking via tsc --noEmit.typecheck npm script: Added to packages/typescript/package.json.<meta name="Keywords"> and <meta name="DC.keywords"> are now correctly captured.convertWithTables() not found (#250): The visitor feature was not enabled by default in the PHP binding crate, causing html_to_markdown_convert_with_tables to be missing from the extension.["full"] (was ["metadata"]), enabling visitor support.["full"] (was []), enabling metadata, visitor, async-visitor, and inline-images.--memory-limit=512M to prevent OOM.test target: Added missing .PHONY: test target to FFI test Makefile.cargo-sort, checkmake, typescript-typecheck. Updated taplo-format to exclude Cargo.toml. Excluded Makefile.frag from checkmake.cargo-sort.pyproject-fmt.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.28.6...v2.28.6
Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.28.6...v2.28.6
vendor/ directory instead of rust-vendor/ for core crate vendoring.build-native-gem.rb for platform-specific pre-compiled gem builds, following kreuzberg patterns.dotnet list --outdated Python script to dotnet-outdated-tool for faster dependency updates.packages/r/configure.win tab indentation to match shfmt 2-space requirement.go-task/setup-task from v1 to v2, nick-fields/retry from v3 to v4.task update.Java visitor FFI: Fixed struct return type mismatch — replaced JAVA_LONG with proper StructLayout matching C HtmlToMarkdownVisitResult (24-byte struct
JAVA_LONG with proper StructLayout matching C HtmlToMarkdownVisitResult (24-byte struct: enum + 2 pointers)republish=trueINPUT_REF resolved to a branch name instead of the tag ref.<dl>/<dt>/<dd> with actual converter output (plain text, no Pandoc-style : prefix).Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.28.0...v2.28.1
Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.28.0...v2.28.1
HtmlToMarkdown.java, TableData.java, and TableExtractionResult.java.Table extraction API: New convert_with_tables function that extracts structured table data during HTML-to-Markdown conversion. Returns TableData struc
convert_with_tables function that extracts structured table data during HTML-to-Markdown conversion. Returns TableData structs containing cell contents as Vec<Vec<String>>, rendered markdown output, and per-row header flags. Uses the visitor pattern internally with a built-in TableCollector to capture table structure in a single pass. Available across all language bindings:
convert_with_tables(html, options, metadata_config) returning ConversionWithTablesconvert_with_tables(html, options, preprocessing, metadata_config) returning TableExtractionResultconvertWithTables(html, options?, metadataConfig?) returning TableExtractionHtmlToMarkdown.convert_with_tables(html, options, metadata_config) returning a HashHtmlToMarkdown::convertWithTables($html, $options, $metadataConfig) returning TableExtractionResultConvertWithTables(html) returning TableExtractionResultHtmlToMarkdown.convertWithTables(html) returning TableExtractionResultHtmlToMarkdownConverter.ConvertWithTables(html) returning TableExtractionResultHtmlToMarkdown.convert_with_tables(html, options, metadata_config) returning {:ok, content, tables, metadata}convert_with_tables(html, options, metadata_config) returning a listhtml_to_markdown_convert_with_tables(html, options_json, metadata_json) returning JSONconvertWithTables(html, options?, metadataConfig?) returning a JS objectOutputFormat::Plain was used with convert_with_tables, the plain text fast path returned before the visitor could extract table data, resulting in empty tables. The conversion pipeline now runs the full visitor walk before returning plain text content.Panic on block_content_start out of bounds: Fixed a crash (byte index N is out of bounds) in text node processing when inline handlers (e.g. , ) colle
byte index N is out of bounds) in text node processing when inline handlers (e.g. <strong>, <em>) collected children into a fresh buffer while inheriting a parent paragraph context. The block_content_start index pointed into the wrong buffer, causing a panic on certain HTML structures — notably <details> containing <p> with inline formatting. (Issues #216, #217)Plain text list items missing markers: and list items in OutputFormat::Plain were output without any bullet or number prefix. Now emits - for unordere
<ul> and <ol> list items in OutputFormat::Plain were output without any bullet or number prefix. Now emits - for unordered lists and sequential N. for ordered lists, respecting the start attribute on <ol>.Colon introduced into definition list text: elements inside were incorrectly prefixed with : (Pandoc definition list syntax), introducing spurious col
<dd> elements inside <dl> were incorrectly prefixed with : (Pandoc definition list syntax), introducing spurious colons into converted text. Standard Markdown and GFM do not support definition list syntax, so <dd> content is now output as plain blocks. (Issue #214, thanks @smoyerx)tests/test_apps/go/go.sum to match the v2.27.0 module version, fixing the CI Go lint job.Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.27.0...v2.27.1
Plain text output format — Set output_format to "plain" to strip all markup and return only visible text. This bypasses the full Markdown/Djot convers
output_format to "plain" to strip all markup and return only visible text. This bypasses the full Markdown/Djot conversion pipeline for maximum speed. Useful for search indexing, text extraction, and feeding content to LLMs.// Rust
let options = ConversionOptions { output_format: OutputFormat::Plain, ..Default::default() };
let plain = convert_html_with_options(html, &options)?;
# Python
from html_to_markdown import convert
plain = convert(html, output_format="plain")
// TypeScript / Node.js
import { convert } from "@kreuzberg/html-to-markdown-node";
const plain = convert(html, { outputFormat: "plain" });
Available across all 10+ language bindings: Rust, Python, Node.js, WASM, CLI, Ruby, PHP, Java, C#, Elixir, R, and Go.
Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.26.3...v2.27.0
OutputFormat::Plain option that strips all markup and returns only visible text content. Set output_format to "plain" (also accepts "plaintext" or "text"). This fast-path bypasses the full Markdown/Djot conversion pipeline — after DOM parsing, a lightweight text extractor walks the tree collecting only visible text with structural whitespace. Useful for search indexing, text extraction, and feeding content to LLMs.Subscript/superscript content silently dropped: When sub_symbol or sup_symbol was empty (the default), text inside and tags was discarded entirely — e
sub_symbol or sup_symbol was empty (the default), text inside <sub> and <sup> tags was discarded entirely — e.g. H<sub>2</sub>O produced HO instead of H2O.<a>…</a>\n<a>…</a>) were dropped, causing links and other inline markup to merge without a word boundary. Now collapses to a single space per HTML white-space normalization rules.Inconsistent whitespace before inline elements across paragraphs: Fixed a stateful bug where \n before , , , and other inline elements inside tags was
\n before <a>, <strong>, <em>, and other inline elements inside <p> tags was handled differently depending on the paragraph's position in the document. The second and subsequent paragraphs would drop the space before inline elements, producing text[link](url) instead of text [link](url). (Issue #212, thanks @haroldparis)Full Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.26.1...v2.26.2
convert_with_metadata: Fixed YAML frontmatter being prepended to Markdown output when using convert_with_metadata. Metadata is now returned exclusivel
convert_with_metadata. Metadata is now returned exclusively as a struct, preventing redundant content in the output string.RbSys::PackageNotFoundError) by adding workspace-level Cargo.toml for proper package discoveryhtml5ever version mismatch in publish scripts (0.36 → 0.38.0)convert_with_metadata output: convert_with_metadata no longer prepends YAML frontmatter to the markdown string. Since metadata is returned as a structured ExtendedMetadata object, embedding it in the content string was redundant and polluted the output.hOCR heading detection: Improved hierarchy logic to use font size (x_fsize) and bbox height as a proxy when detecting headings. Large-font paragraphs
x_fsize) and bbox height as a proxy when detecting headings. Large-font paragraphs now support longer text (up to 80 chars) and single-word headings. Added comprehensive test coverage for heading detection edge cases.Bun runtime support: Official support for Bun 1.2+ via Node-API compatibility. The existing NAPI-RS bindings work in Bun without changes.
markup5ever_rcdom: Brought the markup5ever_rcdom code (MIT/Apache-2.0) into the core crate as an internal rcdom module. This removes the external dependency on the "+unofficial" crate, eliminates the unused xml5ever transitive dependency, and removes the pinned version constraints.html5ever from 0.36.1 to 0.38.0 (now unpinned).pyo3 from 0.28.0 to 0.28.1.See CHANGELOG.md for full details.
markup5ever_rcdom: Brought the markup5ever_rcdom code (MIT/Apache-2.0) into the core crate as an internal rcdom module. This removes the external dependency on the "+unofficial" crate, eliminates the unused xml5ever transitive dependency, and removes the pinned html5ever/markup5ever_rcdom version constraints. See ATTRIBUTIONS.md for license details.html5ever from 0.36.1 to 0.38.0 (now unpinned).pyo3 from 0.28.0 to 0.28.1.Python bindings build: Added explicit #[pyclass(from_py_object)] on Python config wrapper classes to avoid PyO3 deprecation failures under -D warnings…
html5ever/markup5ever_rcdom versions to prevent trait-mismatch breakages during workspace dependency updates.#[pyclass(from_py_object)] on Python config wrapper classes to avoid PyO3 deprecation failures under -D warnings.multiple_crate_versions does not fail Node/WASM/FFI crate lint runs.See CHANGELOG.md for full details.
Subscript/superscript whitespace handling: Subscript and superscript tags now trim inner whitespace and place it outside delimiters, matching the beha
Reduced allocations in hot conversion paths: Return Cow from escape to avoid allocating on no-op paths, replace .repeat() with direct push loops in he
Cow<str> from escape to avoid allocating on no-op paths, replace .repeat() with direct push loops in heading/list/table/div/paragraph formatters, eliminate collect::<Vec>::join() in text dedentation, and use AHashMap for hOCR property maps.getrandom backend configuration from "js" to "wasm_js" for compatibility with getrandom 0.3.x.ahash workspace dependency replacement for standalone builds.See CHANGELOG.md for full details.
Definition list output is consistent across minified and spaced HTML, with proper indentation for multiline definitions. (issue #200)
<dl>/<dt>/<dd> output is consistent regardless of HTML whitespace/minification, and properly indent multiline definition content (issue #200).Automatically recovers UTF-16 HTML (including data without BOM) that was read via lossy UTF-8 decoding
[text](url) patterns in HTML attributesSee CHANGELOG.md for complete details.
# Python
pip install --upgrade html-to-markdown
# TypeScript/Node.js
npm install @kreuzberg/html-to-markdown-node@latest
# Ruby
gem update html-to-markdown
# PHP
composer update kreuzberg/html-to-markdown
...[text](url) patterns in href/src attributes, preventing caller-side URL join/parsing errors.TypeScript wrapper publishing: Fixed TypeScript wrapper dependency resolution by installing dependencies directly from npm registry instead of from wo
@kreuzberg/html-to-markdown-node is available from npm when building the TypeScript wrapper, eliminating the workspace resolution issues that caused previous build failures.Go FFI packaging: Fixed missing html_to_markdown.h header file in Go FFI archive tarballs, which caused go:generate installation to fail with "fatal e
html_to_markdown.h header file in Go FFI archive tarballs, which caused go:generate installation to fail with "fatal error: 'html_to_markdown.h' file not found". The header is now included in all platform archives (tar.gz and zip).--no-frozen-lockfile flag. The reinstall step after publishing Node packages now correctly updates workspace dependencies despite lockfile version mismatches.TypeScript wrapper publishing: Fixed TypeScript wrapper build failures by moving the build and publish steps into the same publish-node job. This elim
publish-node job. This eliminates npm CDN propagation delays that caused @kreuzberg/html-to-markdown to fail building because @kreuzberg/html-to-markdown-node wasn't available yet. Added workspace dependency reinstallation step to ensure pnpm correctly resolves the local package after publishing.go:generate install script that prevented automatic FFI library downloads:
go-ffi-{platform}.tar.gz to html-to-markdown-ffi-{version}-{platform}.tar.gzworkspace = true instead of vendored path reference, preventing Cargo workspace resolution failures during builds.go:generate workflow for automatic FFI library installation, including details about caching in ~/.html-to-markdown/ and alternative manual configuration.Go module versioning: Created 14 missing Go module tags (packages/go/v2.16.1, v2.19.1-v2.19.8, v2.20.1, v2.21.1, v2.22.1-v2.22.5) to ensure all versio
go get any version from v2.15.0 onwards.publish-typescript job to publish workflow to properly publish @kreuzberg/html-to-markdown TypeScript wrapper package to npm alongside the native Node.js bindings (@kreuzberg/html-to-markdown-node)..cargo-checksum.json files. Updated gemspec to include hidden files with File::FNM_DOTMATCH flag, and improved vendoring script to generate checksums correctly with --locked flag and proper cleanup.workspace = true instead of vendored path, preventing Cargo workspace resolution failures during builds.go:generate pattern following Kreuzberg approach. Added cmd/install package that automatically downloads platform-specific FFI libraries from GitHub releases and generates CGO flags. Users can now run go generate after installation instead of manually setting CGO_CFLAGS and CGO_LDFLAGS environment variables. FFI loader updated to check ~/.html-to-markdown/ for installed libraries.Djot output format support: New output_format option in ConversionOptions enables conversion to Djot lightweight markup language as an alternative to
output_format option in ConversionOptions enables conversion to Djot lightweight markup language as an alternative to Markdown. Djot uses different syntax for emphasis (_text_), strong (*text*), strikethrough ({-text-}), inserted ({+text+}), highlighted ({=text=}), subscript (~text~), and superscript (^text^).--output-format / -f flag to specify output format (markdown or djot)PyAsyncVisitorBridge::call_visitor_method_sync() now detects async methods via __await__ attribute and uses PYTHON_TASK_LOCALS event loop for proper async execution (issue #187)convert() wrapper method. Now correctly passes visitor to native convert_with_visitor function when provided (issue #187)async-visitor feature to include required tokio "sync" feature for Mutex supportFix platform standardization (4 supported platforms: macos-latest, windows-latest, ubuntu-latest, ubuntu-24.04-arm)
Bug fixes:
#[serde(default)] attribute to ConversionOptions struct to enable partial JSON deserialization. This allows deserializing JSON with only a subset of fields specified, using default values for missing fields. Fixes compatibility with language bindings (C#, Go, Java) that serialize partial configuration objects.Fixed Ruby gem build failure when installed via gem install or bundler
gem install or bundlerlints.workspace = true requires workspace context, but Ruby gems build standalone via rb_sys[lints.rust] and [lints.clippy] sections"2.22.1") to exact pin ("=2.22.2") to prevent older gems from pulling incompatible newer cratesFull Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.22.1...v2.22.2
lints.workspace = true (which requires workspace context) and added inline lint configuration. This resolves issue #181.html-to-markdown-rs dependency from loose semver ("2.x.x") to exact pin ("=2.22.2") to prevent older gems from pulling incompatible newer crate versions.sync_versions.py to preserve exact version pin prefix (=) when syncing Ruby gem dependencies.Java Maven Central publishing - Fixed Maven Central deployment by adding proper publish profile with central-publishing-maven-plugin configuration. Th
publish profile with central-publishing-maven-plugin configuration. The plugin is now correctly activated with -Ppublish flag and uses ossrh server credentials.https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.22.0...v2.22.1
Fixed C FFI visitor implementation to properly invoke visitor callbacks during HTML-to-Markdown conversion across all language bindings.
Fixed C FFI visitor implementation to properly invoke visitor callbacks during HTML-to-Markdown conversion across all language bindings.
convert_with_visitor in registry.rs - previously the visitor handle was created but discardedPhpVisitorBridge properly wired to Rust coreVisitorCallbackFactory: New class that creates Panama FFI upcall stubs for visitor callbacksHtmlToMarkdown.convertWithVisitor(): Public API method for converting HTML with a custom visitor| Binding | Tests |
|---|---|
| Rust FFI | 25/25 ✅ |
| C# | 42/42 ✅ |
| Java | 95/95 ✅ |
| PHP | 49/54 ✅ (5 incomplete) |
| Go | 25/29 ✅ (4 failing - see notes) |
Note: 4 Go tests fail due to visitor callbacks (visit_horizontal_rule, visit_definition_*, visit_figure_*, visit_details, visit_summary) that exist in the trait but are not yet invoked by the Rust core converter.
html_to_markdown_convert_with_visitor to properly use the visitor handle during conversion instead of discarding it. Previously the visitor was created but the plain convert() function was called instead of convert_with_visitor().PhpVisitorBridge to pass visitor to Rust core instead of ignoring the visitor parameter.VisitorCallbackFactory - New class that creates Panama FFI upcall stubs for visitor callbacks, enabling Java code to receive callbacks from the Rust core during conversion.HtmlToMarkdown.convertWithVisitor() - Public API method for converting HTML with a custom visitor implementation.Serde serialization support for ConversionOptions - Added Serialize and Deserialize traits to ConversionOptions, PreprocessingOptions, and all related
Serialize and Deserialize traits to ConversionOptions, PreprocessingOptions, and all related structs. Enables JSON serialization/deserialization with camelCase field naming and lowercase string enum representations.has_custom_element_tags to accurately detect only tag names with hyphensUpdated reqwest to 0.13.1: Migrated to new rustls defaults
Blockquote newline preservation: Fixed Issue #176 - Newlines were not preserved when block elements like were directly adjacent to elements
<strong> were directly adjacent to <blockquote> elements
Homebrew bottle CI debugging: Added verification steps to diagnose artifact upload/download issues
if-no-files-found: error to fail fast if bottle file not found during uploadFull changelog: https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CHANGELOG.md
WASM npm package publishing: Fixed Issue #172 - WASM package was published with only 3 files (LICENSE, package.json, README.md) instead of 25 files
WASM npm package publishing: Fixed Issue #172 - WASM package was published with only 3 files (LICENSE, package.json, README.md) instead of 25 files
npm publish.github/workflows/publish.yaml to unpack dist/, dist-node/, and dist-web/ directoriesFull Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.19.5...v2.19.6
Homebrew bottle naming: Fixed bottle filename format to match Homebrew convention
Homebrew bottle naming: Fixed bottle filename format to match Homebrew convention
html-to-markdown--2.19.x) to single-dash (html-to-markdown-2.19.x)brew installFull Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.19.4...v2.19.5
Homebrew formula publishing: Fixed publish workflow script that updates the Homebrew tap formula
Homebrew formula publishing: Fixed publish workflow script that updates the Homebrew tap formula
# bottle do instead of bottle do), preventing duplicate bottle blocks from accumulating on each releaseFull Changelog: https://github.com/kreuzberg-dev/html-to-markdown/compare/v2.19.3...v2.19.4
Table image processing: Fixed Issue #175 - images inside Blogger-style HTML tables (e.g., ) were being stripped during conversion. Enhanced table scan
<table class="tr-caption-container">) were being stripped during conversion. Enhanced table scanner to recognize images as content and properly process non-table elements like <a> and <img> that are direct children of table elements..npmignore to include dist/, dist-node/, and dist-web/ directories that were excluded by .gitignore during npm publish.sync_versions.py to synchronize both root composer.json and packages/php/composer.json.sync_versions.py script to update root composer.json for Packagist validationWASM npm package: Fixed missing .d.ts files in published package by updating files field with glob patterns (fixes #172)
.d.ts files in published package by updating files field with glob patterns (fixes #172)convert_html_to_markdown() to convert()@kreuzberg/html-to-markdownconvertHtmlToMarkdown() to convert()KreuzbergDev.HtmlToMarkdown package namecomposer.json to repository rootGPG_PASSPHRASE typo)Full changelog: https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CHANGELOG.md
Go formatting: Applied gofmt to packages/go/v2/htmltomarkdown/visitor.go to align constant declarations
gofmt to packages/go/v2/htmltomarkdown/visitor.go to align constant declarationsFull changelog: https://github.com/kreuzberg-dev/html-to-markdown/blob/main/CHANGELOG.md
Package namespace changed from html-to-markdown-node to @kreuzberg/html-to-markdown-node
npm (JavaScript/TypeScript):
html-to-markdown-node to @kreuzberg/html-to-markdown-nodehtml-to-markdown-wasm to @kreuzberg/html-to-markdown-wasmnpm install @kreuzberg/html-to-markdown-nodeimport { convert } from '@kreuzberg/html-to-markdown-node'Java:
io.github.goldziher to dev.kreuzberg<groupId>dev.kreuzberg</groupId>import dev.kreuzberg.htmltomarkdown.HtmlToMarkdown;C#/.NET:
Goldziher.HtmlToMarkdown to KreuzbergDev.HtmlToMarkdowndotnet add package KreuzbergDev.HtmlToMarkdownusing HtmlToMarkdown;Python, Ruby, PHP, Go, Elixir, Rust: No changes required.
<row>, <cell>, <graphic>) for academic publishing formatsSee the README for detailed migration instructions.
@kreuzberg scope for better organization and discoverability
html-to-markdown-node → @kreuzberg/html-to-markdown-nodehtml-to-markdown-wasm → @kreuzberg/html-to-markdown-wasmdev.kreuzberg package prefix instead of com.goldziher
KreuzbergDev namespace instead of Goldziher
KreuzbergDev.HtmlToMarkdownKreuzbergDev.HtmlToMarkdown namespace<row> elements for table rows with proper cell grouping and nesting<cell> elements with full attribute support including role="head" for header cells<graphic> elements for figure/image references within cells and content blocksResolved CI validation failures (mypy unused type:ignore comments)
Release v2.18.0
NodeContext provides element metadata: tag name, attributes, depth, parent tag, inline status, and sibling indexvisit_element_start and visit_element_end for complete controlconvert_with_async_visitor() function<script> tags as actual HTML elementsConversionOptionsHandle export in public API (GitHub issue #166)
ConversionOptionsHandle directly from the html_to_markdown packageOptionsHandle importchore(deps): bump actions/setup-java from 4 to 5 by @dependabot[bot] in https://github.com/Goldziher/html-to-markdown/pull/162
Full Changelog: https://github.com/Goldziher/html-to-markdown/compare/v2.16.0...v2.16.1
strip_newlines.feat(build): add profiling harness and workflow by @Goldziher in https://github.com/Goldziher/html-to-markdown/pull/160
Full Changelog: https://github.com/Goldziher/html-to-markdown/compare/v2.15.0...v2.16.0
Your coding agent can read these notes before it upgrades. Set up the MCP server →