# reverse_markdown: turning imported HTML into readable Markdown

> The reverse_markdown gem converts HTML into Markdown through Nokogiri, with per-tag converters you can extend yourself. The project states plainly that a perfect conversion is impossible, and it ships a strict mode that raises on unknown tags so you notice the losses.

**xijo/reverse_markdown** — Ruby gem to convert html into markdown

- Repository: https://github.com/xijo/reverse_markdown
- Stars: 682 · Forks: 119
- Language: Ruby
- License: WTFPL
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/xijo-reverse-markdown

## A one-way converter for HTML that has to land in a Markdown pipeline

reverse_markdown exists for a specific migration problem: content that already lives as HTML and needs to become Markdown. The README gives the use case directly, describing the gem as useful if you want to import HTML into your Markdown based application. That is a narrower goal than a general document converter, and the project is honest about the gap it can close.

The stated reason for that limit is a language asymmetry. HTML is far more expressive than Markdown, supporting tables with merged cells, arbitrary nesting, inline styles and countless other features with no Markdown equivalent. No amount of code makes that lossless. The project's stated goal is good enough defaults for most common cases, and it says outright that it handles standard content well without attempting every edge case.

That framing matters for how you evaluate it. This is a migration helper, not a round trip format. If you expect to convert back and forth without loss, you are asking for something the README explicitly says is not possible. If you need a specific behaviour for a specific tag, the escape hatch is documented as writing custom converters in the project wiki.

Requirements are modest. The gem needs Nokogiri and Ruby 2.7.0 or higher, and it is published on RubyGems under the name reverse_markdown.

## Conversion is module based, so a missing tag is a class you write

The mechanism behind the gem is a set of converters, one per HTML tag, which is what makes the module based claim concrete. The README lists the tags handled out of the box: h1 through h6, p, em, strong, i, b, blockquote, code, img, a, hr, li, ol, ul, table, tr, th, td, br and figure. Nested lists are supported, and both inline and block code are handled, along with blockquotes.

The design consequence is that an unlisted tag is not a crash and not a silent omission. It is handled by a policy you set, which is the more interesting part of the API.

That policy is the unknown_tags option, and its four values define the trade-off precisely. The default, pass_through, includes the unknown tag completely in the result. drop removes the tag and its content. bypass ignores the tag but still tries to convert what is inside it. raise raises an error so you know about it.

The default is the forgiving one, which is convenient for a one-off import and risky for a pipeline where a dropped or raw-passthrough element could carry styling or scripts into your Markdown store. Setting :raise for the first pass over a corpus turns the gem from a best effort tool into a report of what your content actually contains.

## Converting your first document from Ruby or from the shell

Installing the gem is a single command, or a line in your Gemfile:

```sh
[sudo] gem install reverse_markdown
```

```ruby
gem 'reverse_markdown'
```

The library call takes either an HTML string or a Nokogiri document, and returns the Markdown:

```ruby
input  = '<strong>feelings</strong>'
result = ReverseMarkdown.convert input
```

The same conversion is available as a binary, which is the form to reach for in a shell pipeline:

```sh
$ reverse_markdown file.html > file.md
$ cat file.html | reverse_markdown > file.md
```

Two things to note about the shell usage. The binary reads standard input and writes standard output, so it composes with other Unix tools without an intermediate file, and the redirect in both examples is what produces the .md file. The README does not document options for the binary, so the configuration described below applies to the library API.

For a first run, converting one representative file by hand and reading the output line by line is the cheapest way to learn the gem's defaults.

## Per-call options and block level configuration cover the same four settings

Configuration comes in two forms, and the difference is scope rather than capability.

Passing options as a second argument to the conversion applies them to that call only:

```ruby
ReverseMarkdown.convert(input, unknown_tags: :raise, github_flavored: true)
```

The block style on an initializer applies to every conversion until the values are changed:

```ruby
ReverseMarkdown.config do |config|
  config.unknown_tags     = :bypass
  config.github_flavored  = true
  config.tag_border  = ''
end
```

Three options are documented. github_flavored defaults to false and switches to GitHub flavoured Markdown, though the README notes that yet only code blocks are supported, which is a narrower promise than the name suggests. tag_border defaults to a single space and controls whether whitespace is added at tag boundaries, with the alternative being no whitespace at all. unknown_tags is the policy described earlier.

The github_flavored caveat deserves emphasis because it is the kind of limitation that surfaces only after import. If your target system renders GitHub flavoured Markdown and your content includes tables or task lists, the flag will not produce those constructs, because the project only supports code blocks under it. Verify the output against your renderer rather than assuming the flag matches the target dialect.

## Unknown tags are the failure mode, and a strict first pass is the fix

The realistic failure of this kind of converter is not an exception on common HTML. It is a quietly wrong document.

Consider the failure paths. With the default pass_through, an unrecognised element keeps its raw markup in the output, so a piece of HTML could survive into a Markdown document and later be rendered as HTML again. With drop, the element and everything inside it vanishes, so a wrapper div that looked harmless to the author can take a paragraph with it. With bypass, the tag disappears but its content is converted, which changes the document structure. None of these raises an error, and each produces a document that looks plausible.

That is why the strict mode matters as a workflow rather than a setting. Running a corpus once with unknown_tags: :raise produces a list of exactly the tags your content uses that the gem does not map, and that list is the actual work list for your migration. You can then decide per tag whether a custom converter, a preprocessing step, or accepting the loss is right.

The second limitation is the language gap itself, and it is not fixable with configuration. Merged cells, arbitrary nesting and inline styles have no Markdown equivalent. For those cases the README points to writing custom converters, and the related projects list offers html_massage by Harlan T. Wood for converting regular sites into Markdown on top of reverse_markdown, which is a different starting point for messy pages.

## A permissive licence, a quiet release history, and a gem that others build on

The licence is WTFPL, which is about as permissive as software licences get. In practice that puts few obligations on you beyond keeping the licence text with the distribution. Confirm the specifics with whoever handles licensing in your organisation, since this is a summary rather than legal advice.

The repository has no GitHub releases. Change history lives in CHANGELOG.md, which the README links as the Change Log, and the last push was on 2026-09-10. The project is therefore worked on without a tagged release stream, which matters if you depend on it in a released product, because there is no version tag to pin a known good state to other than the version your Gemfile resolves.

The repository layout is conventional Ruby. A reverse_markdown.gemspec describes the gem, lib holds the implementation, bin holds the executable, spec holds the tests with an .rspec file and a Rakefile to run them, and a .tool-versions file records the Ruby version the project targets.

Two signals suggest this is treated as a stable dependency rather than a weekend project. The README credits a long list of named contributors, and the related projects section shows other gems built on it, including word-to-markdown by Ben Balter, which converts Word documents into Markdown using reverse_markdown. Being a building block for other tools is a stronger signal than any feature list.

## Conclusion

reverse_markdown suits teams importing HTML into a Markdown based system, such as a CMS migration or a documentation pipeline, where a close but imperfect rendering is acceptable. It does not suit content whose tables, merged cells or inline styles have to survive the round trip, because the project states those HTML features have no Markdown equivalent. Before adopting it, run your own documents with unknown_tags: :raise to find which tags the gem cannot map, and decide then whether a custom converter or the html_massage gem is the better fit for the messy cases.

## FAQ

### How do I install reverse_markdown?

Install the gem with gem install reverse_markdown, or add gem 'reverse_markdown' to your Gemfile. The gem requires Nokogiri and Ruby 2.7.0 or higher.

### How does reverse_markdown handle HTML tags it does not know?

The unknown_tags option decides that, with four values: pass_through keeps the tag in the output, drop removes the tag and its content, bypass ignores the tag but converts its content, and raise raises an error. The default is pass_through.

### Does reverse_markdown support GitHub flavoured Markdown?

There is a github_flavored option, default false, but the README states that yet only code blocks are supported under it.

### Can I convert HTML files from the command line with reverse_markdown?

Yes. The binary reads standard input and writes standard output, so you can run reverse_markdown file.html > file.md or pipe a file into it with cat file.html | reverse_markdown > file.md.

## Sources

- [Issues](https://github.com/xijo/reverse_markdown/issues)
- [License: WTFPL](https://github.com/xijo/reverse_markdown/blob/master/LICENSE)
- [README](https://github.com/xijo/reverse_markdown/blob/master/README.md)
- [xijo/reverse_markdown on GitHub](https://github.com/xijo/reverse_markdown)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/xijo-reverse-markdown
