PHPackages                             kntnt/html-to-markdown - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. kntnt/html-to-markdown

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

kntnt/html-to-markdown
======================

A dependency-free PHP 8.4 library that converts HTML into GitHub Flavored Markdown. A faithful port of the Go library JohannesKaufmann/html-to-markdown (v2).

v0.1.3(1mo ago)0135↓90%MITPHPPHP &gt;=8.4CI passing

Since Jun 4Pushed 1w agoCompare

[ Source](https://github.com/Kntnt/kntnt-html-to-markdown)[ Packagist](https://packagist.org/packages/kntnt/html-to-markdown)[ RSS](/packages/kntnt-html-to-markdown/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (6)Versions (5)Used By (0)

kntnt/html-to-markdown
======================

[](#kntnthtml-to-markdown)

[![License](https://camo.githubusercontent.com/0acb0f3e13c5afa0b358853355a845be33c8ee2c2e2d567519a3e39c1bf35f90/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f4b6e746e742f6b6e746e742d68746d6c2d746f2d6d61726b646f776e)](LICENSE)[![PHP Version](https://camo.githubusercontent.com/0bda31ebdcee7e7a9c150822819b854a5a1c617174f9356871bcb671f2c6e392/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6b6e746e742f68746d6c2d746f2d6d61726b646f776e)](composer.json)[![Latest release](https://camo.githubusercontent.com/3f43b02de7c79b331e440a71eaa63088df7a507e0eb88424a7f65d8606419693/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6b6e746e742f68746d6c2d746f2d6d61726b646f776e)](https://packagist.org/packages/kntnt/html-to-markdown)[![CI](https://github.com/Kntnt/kntnt-html-to-markdown/actions/workflows/ci.yml/badge.svg)](https://github.com/Kntnt/kntnt-html-to-markdown/actions/workflows/ci.yml)

A dependency-free PHP 8.4 library that converts HTML into [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM). If you need to turn HTML into clean, predictable Markdown — to feed page content to an LLM, archive content portably, or render it elsewhere — and you want output that matches a battle-tested reference implementation, this is for you.

Description
-----------

[](#description)

`kntnt/html-to-markdown` is a faithful PHP port of the Go library [`JohannesKaufmann/html-to-markdown`](https://github.com/JohannesKaufmann/html-to-markdown) (v2). It ports the converter core and the `base`, `commonmark`, `strikethrough`, and `table` plugins, reproducing the Go library's output **byte-for-byte**: upstream's golden test fixtures are asserted character-for-character by the test suite.

It has **zero runtime dependencies**. Every dependency the Go library reaches for maps onto a PHP built-in — `Dom\HTMLDocument` for HTML5 parsing and CSS selectors and `mbstring` for text handling — with relative-URL resolution hand-ported from RFC 3986, so the package drops into any PHP 8.4 project without dragging in a dependency tree or risking version conflicts. It is used by other Kntnt projects via Composer.

### Key Features

[](#key-features)

- **Faithful port.** Output matches the Go library byte-for-byte wherever PHP's HTML5 parser and Go's `x/net/html` agree; upstream's golden fixtures are asserted in the test suite.
- **Zero runtime dependencies.** No Composer runtime packages — only the near-ubiquitous `ext-dom` and `ext-mbstring`.
- **GitHub Flavored Markdown.** Full CommonMark core plus GFM tables and strikethrough.
- **Tables done properly.** Pipe tables with alignment, `colspan`/`rowspan`, captions, header promotion, and presentation-table handling.
- **Smart, context-aware escaping.** Text that merely *looks* like Markdown stays literal, while real formatting elements become real Markdown.
- **Relative-URL resolution.** Resolve `href`/`src` against a base domain to produce absolute links and images.
- **Scoped conversion.** Include or exclude parts of the document with CSS selectors.
- **Configurable output.** Choose emphasis delimiters, heading style, code fences, list markers, and more.
- **Two entry points.** A one-line static facade for the common case, and a deep, extensible `Converter` for everything else.

### The problem

[](#the-problem)

More and more PHP applications need Markdown out of HTML, and the naive approaches all fail in their own way: stripping tags throws away structure, and hand-rolled regular expressions produce invalid or surprising Markdown. Getting it right is harder than it looks, and the subtlest part is escaping: a sentence that happens to contain `**` or a leading `#` must not silently turn into bold text or a heading when it round-trips through Markdown.

### How this library helps

[](#how-this-library-helps)

Rather than inventing yet another converter, this library ports a mature, widely used, well-tested one — `JohannesKaufmann/html-to-markdown` — and holds itself to that reference byte-for-byte. You get GitHub Flavored Markdown out of the box: headings, emphasis, links, images, code, blockquotes, lists, thematic breaks, hard breaks, comments, GFM tables, and strikethrough. Relative URLs can be resolved against a base domain, conversion can be scoped to part of a document, and escaping is handled by a two-phase, context-aware model that decides *per occurrence* whether a character needs a backslash. Because it has zero runtime dependencies, it installs cleanly into any PHP 8.4 codebase.

### Limitations

[](#limitations)

- **No task lists.** This is the one deliberate gap in GFM coverage: a checkbox list item (`…`) is rendered as a plain list item, not `- [ ]` / `- [x]`. Task lists are out of scope upstream too.
- **Autolinks and the tagfilter do not apply.** Both are *parsing* features that act on Markdown input; when the input is HTML there is nothing for them to do. See [Supported Markdown](#supported-markdown) for the details.
- **It is a converter, not a sanitizer.** It emits Markdown (and strips script/style/iframe-style tags in the process), but it is not designed or audited as a security boundary against hostile HTML. Sanitize untrusted input separately if that is your threat model.
- **PHP 8.4+ only.** The port uses modern language and standard-library features (notably `Dom\HTMLDocument`, the native HTML5 parser added in 8.4) and carries no back-compatibility shims for older PHP.

Requirements
------------

[](#requirements)

- PHP **8.4** or newer
- `ext-dom` and `ext-mbstring` (both bundled with virtually every PHP build)

Installation
------------

[](#installation)

```
composer require kntnt/html-to-markdown
```

No further configuration is required.

Usage
-----

[](#usage)

### The facade

[](#the-facade)

For the common case, the static facade mirrors upstream's `ConvertString`. It wires up the `base` and `commonmark` plugins and converts in one call:

```
use Kntnt\HtmlToMarkdown\HtmlToMarkdown;

$markdown = HtmlToMarkdown::convert('Bold Text');
// => **Bold Text**
```

Pass options as named arguments. To resolve relative URLs against a base domain:

```
$markdown = HtmlToMarkdown::convert(
    '',
    domain: 'https://example.com',
);
// => ![](https://example.com/assets/image.png)
```

The facade also accepts `includeSelector` and `excludeSelector` (see [Include / exclude selectors](#include--exclude-selectors)). For strikethrough, tables, or custom output styling, build a `Converter` directly.

### The converter

[](#the-converter)

For full control — strikethrough and tables, custom output options, or custom plugins — build a `Converter`. The `base` and `commonmark` plugins are the minimum needed for sensible output; add `strikethrough` and `table` as required:

```
use Kntnt\HtmlToMarkdown\Converter\Converter;
use Kntnt\HtmlToMarkdown\Converter\Options;
use Kntnt\HtmlToMarkdown\Plugin\Base\BasePlugin;
use Kntnt\HtmlToMarkdown\Plugin\Commonmark\CommonmarkPlugin;
use Kntnt\HtmlToMarkdown\Plugin\Strikethrough\StrikethroughPlugin;
use Kntnt\HtmlToMarkdown\Plugin\Table\TablePlugin;

$converter = new Converter(
    plugins: [
        new BasePlugin(),
        new CommonmarkPlugin(),
        new StrikethroughPlugin(),
        new TablePlugin(),
    ],
);

$markdown = $converter->convertString(
    'TitleA1',
    new Options(domain: 'https://example.com'),
);
```

A single converter is safe to reuse across many conversions: options that vary per request — the base domain and the include/exclude selectors — live on the `Options` object passed to `convertString()`, not on the converter itself. If you already hold a parsed DOM, `convertNode()` takes a `Dom\Node` instead of a string.

### Commonmark options

[](#commonmark-options)

The `CommonmarkPlugin` accepts the same options as upstream, as named constructor arguments:

```
new CommonmarkPlugin(
    emDelimiter: '_',          // default "*"
    strongDelimiter: '__',     // default "**"
    horizontalRule: '---',     // default "* * *"
    bulletListMarker: '+',     // default "-"
    codeBlockFence: '~~~',     // default "```"
    headingStyle: 'setext',    // default "atx"
);
```

### Include / exclude selectors

[](#include--exclude-selectors)

Restrict conversion to a subset of the document, or strip parts of it, using CSS selectors (resolved with `Dom\HTMLDocument::querySelectorAll`):

```
$converter->convertString($html, new Options(
    includeSelector: 'article',
    excludeSelector: 'nav, aside, .ad',
));
```

### Supported Markdown

[](#supported-markdown)

The output targets [GitHub Flavored Markdown](https://github.github.com/gfm/). GFM is CommonMark plus five extensions. Because this library converts *from* HTML rather than parsing Markdown, each extension means something slightly different here — the table below is how each is handled:

GFM extensionStatusNotesTables✅ ProducedPipe tables with alignment, `colspan`/`rowspan`, captions, header promotion, and presentation-table handling.Strikethrough✅ Produced``, ``, and `` → `~~…~~`.Task lists❌ Not producedThe one gap. A checkbox list item (`…`) is rendered as a plain list item, not `- [ ]` / `- [x]`.Autolinks— Not applicableAn autolink is a *parsing* feature: a GFM reader turns a bare `https://…` in Markdown text into a link. Converting the other way, there is nothing to do — a bare URL is emitted as text and any GFM renderer autolinks it. (URLs that are already `` or `` elements become normal Markdown links and images.)Tagfilter (disallowed raw HTML)— Not applicableThe tagfilter neutralizes dangerous raw tags (``, ``, ``, ``, ``, …) found in Markdown *input*. This converter already strips exactly those tags from the HTML and emits Markdown rather than passing raw HTML through, so there is nothing left for the filter to act on.Underneath those extensions, the full **CommonMark** core is supported: headings (ATX and Setext), bold/italic, links, images, inline and fenced code, blockquotes, ordered and unordered lists, thematic breaks, hard line breaks, and HTML comments.

In short: the output is GFM except for task lists. This matches the upstream Go library's boundaries — task lists are out of scope there too.

### Updating

[](#updating)

This is a Composer package, so updating is simply:

```
composer update kntnt/html-to-markdown
```

The project follows [Semantic Versioning](https://semver.org/), so patch and minor updates within a major version will not break your integration. See the [Changelog](#changelog) for what each release contains.

Frequently asked questions (FAQ)
--------------------------------

[](#frequently-asked-questions-faq)

#### Does it produce task lists?

[](#does-it-produce-task-lists)

No. Rendering a checkbox `` as `- [ ]` / `- [x]` is the single deliberate gap in GFM coverage; the item is emitted as a plain list item instead. This matches the upstream Go library, where task lists are also out of scope.

#### Why does it require PHP 8.4?

[](#why-does-it-require-php-84)

The port leans on modern PHP — most load-bearingly the native `Dom\HTMLDocument` HTML5 parser introduced in 8.4, which replaces Go's `x/net/html`. Keeping a single, modern floor avoids back-compatibility shims and keeps the code close to the Go original. (Relative-URL resolution is hand-ported from RFC 3986, so it needs no PHP-8.5-only `Uri\Rfc3986\Uri` extension.)

#### Is the output really identical to the Go library?

[](#is-the-output-really-identical-to-the-go-library)

Wherever PHP's HTML5 parser and Go's `x/net/html` agree, yes — upstream's golden fixtures are asserted byte-for-byte. The only legitimate source of divergence is a genuine difference between the two HTML5 parsers; any such case is documented in [`docs/architecture.md`](docs/architecture.md) and annotated at the affected fixture. As of the current release there are no such deviations.

#### Which upstream version does it track?

[](#which-upstream-version-does-it-track)

This release ports upstream **v2.5.2** (commit `290df46`). The exact pin is recorded in [`NOTICE.md`](NOTICE.md) and updated whenever the port is re-synced.

#### What about character encoding?

[](#what-about-character-encoding)

Input is expected to be UTF-8 already. The converter parses with `Dom\HTMLDocument::createFromString($html, …, 'UTF-8')`, which forces UTF-8 and does not detect or convert other charsets — the same contract as the upstream Go library, whose `html.Parse()` requires UTF-8. If your HTML is in another charset (for example `ISO-8859-1` or `Windows-1252`), decode it to UTF-8 first; otherwise replacement characters (`�`) may appear in the output. When fetching over HTTP, the `Content-Type` header gives the charset; from a file you may need to read it from a `` tag or sniff the bytes. `mb_convert_encoding()` from `ext-mbstring` performs the conversion.

#### Is it safe to run on untrusted HTML?

[](#is-it-safe-to-run-on-untrusted-html)

It is a converter, not a sanitizer. It strips script/style-type tags and emits Markdown rather than passing raw HTML through, but it is not designed as a security boundary. If you process hostile input, run it through a dedicated HTML sanitizer first.

Questions, bugs, and feature requests
-------------------------------------

[](#questions-bugs-and-feature-requests)

Have a usage question or something to discuss? Please use [Discussions](https://github.com/Kntnt/kntnt-html-to-markdown/discussions).

Found a bug or want to request a feature? Please [open an issue](https://github.com/Kntnt/kntnt-html-to-markdown/issues). Search the existing issues first to avoid duplicates.

Extending
---------

[](#extending)

The converter is built from plugins, and the same mechanism the built-in plugins use is available to you. A plugin is any class implementing the `Plugin` interface — a `name()` and an `init()` that registers behaviour — which you then pass to the `Converter` alongside the built-ins.

### The registration surface

[](#the-registration-surface)

Inside `init()`, a plugin registers behaviour through `$converter->register`:

MethodRegisters`preRenderer(fn, priority)`A DOM transform run before rendering (rewrite or remove nodes).`renderer(fn, priority)`A render handler tried per node until one returns `RenderStatus::Success`.`rendererFor(tag, type, fn, priority)`A render handler guarded by a tag name, declaring its `TagType`.`postRenderer(fn, priority)`A transform over the whole rendered Markdown string.`textTransformer(fn, priority)`A transform applied to each text node.`escapedChar(...chars)`Markdown-significant characters that must be escaped in text.`unEscaper(fn, priority)`A context check deciding, per occurrence, whether an escape survives.`tagType(tag, type, priority)`An explicit block/inline classification for a tag.`plugin(plugin)`Another plugin this one depends on.Handlers carry an integer **priority** — lower runs first — built from the `Priority` constants `EARLY` (100), `STANDARD` (500), and `LATE` (1000). Ties break on registration order, deterministically.

### A custom plugin

[](#a-custom-plugin)

A render handler receives the context, an output `Buffer`, and the current node, and returns a `RenderStatus`: `Success` if it handled the node, or `TryNext` to fall through to the next handler. This example renders `` to `==…==` (a non-standard highlight syntax, used here only to show the shape):

```
use Dom\Node;
use Kntnt\HtmlToMarkdown\Converter\Buffer;
use Kntnt\HtmlToMarkdown\Converter\Context;
use Kntnt\HtmlToMarkdown\Converter\Converter;
use Kntnt\HtmlToMarkdown\Converter\Plugin;
use Kntnt\HtmlToMarkdown\Converter\Priority;
use Kntnt\HtmlToMarkdown\Converter\RenderStatus;
use Kntnt\HtmlToMarkdown\Dom\Dom;

final class HighlightPlugin implements Plugin
{
    public function name(): string
    {
        return 'highlight';
    }

    public function init(Converter $converter): void
    {
        $converter->register->renderer($this->render(...), Priority::STANDARD);
    }

    private function render(Context $ctx, Buffer $w, Node $node): RenderStatus
    {
        if (Dom::nodeName($node) !== 'mark') {
            return RenderStatus::TryNext;
        }

        $inner = new Buffer();
        $ctx->renderChildNodes($ctx, $inner, $node);
        $w->write('==' . $inner->bytes() . '==');

        return RenderStatus::Success;
    }
}
```

Register it like any other plugin:

```
$converter = new Converter([
    new BasePlugin(),
    new CommonmarkPlugin(),
    new HighlightPlugin(),
]);
```

The full design — the three-phase render loop, the escaping model, the tag-type system, and the Go→PHP module map — is documented in [`docs/architecture.md`](docs/architecture.md). Read it before writing anything that touches escaping or whitespace.

Development
-----------

[](#development)

### Build from source

[](#build-from-source)

```
git clone https://github.com/Kntnt/kntnt-html-to-markdown.git
cd kntnt-html-to-markdown
composer install
```

There is nothing to compile — this is a pure PHP library. The Go reference is cloned into `.reference/` (git-ignored) and pinned to the tag recorded in [`NOTICE.md`](NOTICE.md); it is the specification the port is checked against.

### Build a release artefact

[](#build-a-release-artefact)

This package is distributed through [Packagist](https://packagist.org/packages/kntnt/html-to-markdown); a published Git tag is the release, and Composer builds the dist archive itself. The `.gitattributes` `export-ignore` rules keep that archive lean — tests, fixtures, CI config, and documentation are stripped, leaving only the runtime code and its licensing metadata. To reproduce the archive Composer would download:

```
git archive --format=tar.gz --prefix=kntnt-html-to-markdown/ -o kntnt-html-to-markdown.tar.gz HEAD
```

### Run tests

[](#run-tests)

The suite is data-driven and built on [Pest](https://pestphp.com/). Its backbone is the **golden-fixture** tests: upstream's `testdata` golden files are run through the exact converter wiring upstream uses and asserted byte-for-byte. Around them sit ported unit tables — the commonmark and table options and their validation errors, strikethrough cases, the facade, URL resolution and query encoding — plus a handful of edge cases. Static analysis and coding-style checks run alongside.

```
composer test        # Pest test suite
composer test:coverage  # Pest with code coverage (needs pcov or Xdebug)
composer stan        # PHPStan, level max
composer cs          # PHP-CS-Fixer, PSR-12 (dry run)
composer cs-fix      # PHP-CS-Fixer, apply fixes
```

All four run in CI on every push and pull request against PHP 8.4. New code is expected to keep the golden fixtures passing, stay green under PHPStan at level max, and conform to PSR-12.

### Technical documentation

[](#technical-documentation)

- [`docs/architecture.md`](docs/architecture.md) — the porting record: the full Go→PHP module map, the escaping model, the URL-resolution notes, and every bridged Go-vs-PHP implementation difference. Read this before changing the engine.
- [`docs/coding-standards.md`](docs/coding-standards.md) — the project's coding standard.
- [`CLAUDE.md`](CLAUDE.md) / [`AGENTS.md`](AGENTS.md) — entry points for AI coding agents working in the repository.
- [`NOTICE.md`](NOTICE.md) — the upstream pin and the full licensing lineage.

How you can contribute
----------------------

[](#how-you-can-contribute)

Contributions are welcome, large or small. You can help by [opening an issue](https://github.com/Kntnt/kntnt-html-to-markdown/issues) to report a bug or request a feature, by sending a pull request with a fix or improvement, or by improving the documentation. Because **fidelity to the upstream Go library is the contract**, code contributions are expected to keep the golden fixtures passing byte-for-byte; if a change would alter output, explain why, and never weaken the converter to paper over a difference. When in doubt, open a discussion first.

Acknowledgements
----------------

[](#acknowledgements)

This library stands on the work of others. Thanks to **Johannes Kaufmann** for [`html-to-markdown`](https://github.com/JohannesKaufmann/html-to-markdown), the Go library this is a port of; and to **Dom Christie** ([Turndown](https://github.com/mixmark-io/turndown)) and **Luc Thevenard** ([collapse-whitespace](https://github.com/wooorm/collapse-white-space)), whose whitespace-collapsing code lives on in the port's lineage. The full attribution chain is in [`NOTICE.md`](NOTICE.md).

License
-------

[](#license)

Released under the MIT License, with dual copyright: Johannes Kaufmann for the original Go library, and Thomas Barregren / Kntnt for the PHP port. The license text is in [`LICENSE`](LICENSE); the complete lineage and per-component attribution are in [`NOTICE.md`](NOTICE.md).

Changelog
---------

[](#changelog)

See [`CHANGELOG.md`](CHANGELOG.md) for the release history. The project follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance96

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

4

Last Release

32d ago

PHP version history (2 changes)v0.1.0PHP &gt;=8.5

v0.1.3PHP &gt;=8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/4eebd2a9ac416639b0ab9c50581ee228727ba8b824aaf1d08002ed000bc1fa78?d=identicon)[TBarregren](/maintainers/TBarregren)

---

Top Contributors

[![TBarregren](https://avatars.githubusercontent.com/u/261837?v=4)](https://github.com/TBarregren "TBarregren (18 commits)")

---

Tags

htmlconvertermarkdowngfmcommonmark

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/kntnt-html-to-markdown/health.svg)

```
[![Health](https://phpackages.com/badges/kntnt-html-to-markdown/health.svg)](https://phpackages.com/packages/kntnt-html-to-markdown)
```

###  Alternatives

[league/commonmark

Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)

3.0k437.5M1.1k](/packages/league-commonmark)[league/html-to-markdown

An HTML-to-markdown conversion helper for PHP

1.9k32.2M327](/packages/league-html-to-markdown)[cebe/markdown

A super fast, highly extensible markdown parser for PHP

1.0k34.1M150](/packages/cebe-markdown)[pixel418/markdownify

The HTML to Markdown converter for PHP

198876.3k8](/packages/pixel418-markdownify)[texy/texy

Texy converts plain text in easy to read Texy syntax into structurally valid (X)HTML. It supports adding of images, links, nested lists, tables and has full support for CSS. Texy supports hyphenation of long words (which reflects language rules), clickable emails and URL (emails are obfuscated against spambots), national typographic single and double quotation marks, ellipses, em dashes, dimension sign, nonbreakable spaces (e.g. in phone numbers), acronyms, arrows and many others. Texy code can optionally contain HTML tags.

161862.7k15](/packages/texy-texy)[3f/converter

A set of classes to translate a text from HTML to BBcode and from BBCode to Markdown.

29153.6k](/packages/3f-converter)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
