# `PhoenixKitEcommerce.HtmlToMarkdown`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ecommerce/blob/0.5.11/lib/phoenix_kit_ecommerce/html_to_markdown.ex#L1)

Converts HTML (as Shopify's `body_html` product field always is) into
Markdown.

The storefront that consumes synced products renders descriptions
through a Markdown component. By CommonMark rules, a block starting
with a raw `<p>` tag is treated as opaque raw HTML, so Markdown already
written inside Shopify's `body_html` (sellers routinely hand-write
`**bold**`/`- list` text inside `<p>` tags) is emitted byte-for-byte
instead of rendering. Converting `body_html` to Markdown at sync time
keeps stored descriptions renderable.

This is a small hand-rolled HTML -> Markdown transform rather than a
`LazyHTML`/`Floki`-based one: both are declared `only: :test` in
`mix.exs`, and this module runs on the Shopify sync write path in
every environment (not just `:test`) — pulling a test-only parser into
`:dev`/`:prod` would be a wider dependency-footprint change than this
fix calls for.

Supported tags: `p`, `br`, `h1`-`h6`, `ul`/`ol`/`li` (including lists
nested inside a `<li>`, rendered as an indented sub-list), `table`
(`thead`/`tbody`/`tfoot`/`tr`/`td`/`th`), `strong`/`b`, `em`/`i`, `a`,
`img`, plus a transparent `div` wrapper and HTML entity decoding
(`&nbsp;`, `&quot;`, `&#39;`, numeric character references).
`<script>`, `<style>`, `<noscript>` and `<template>` elements are
dropped entirely, content included, rather than leaking their raw
text. A bare `<`/`>` that isn't part of a real tag (e.g. "5 < 10") is
left as plain text instead of being parsed as a tag boundary.

The three markup-significant entities — `&lt;`, `&gt;` and `&amp;`
(and their numeric forms `&#60;`/`&#x3c;`, `&#62;`/`&#x3e;`,
`&#38;`/`&#x26;`, which are normalised to the named form) — are kept
ENCODED in emitted text. CommonMark decodes entities in text itself,
so the rendered result is identical, but decoding them here would turn
a seller's escaped `&lt;script&gt;` example into a raw HTML block in
the stored Markdown: `<script>` stripping above only ever sees real
tags, and the storefront's Markdown renderer emits raw HTML blocks
as-is, so under `shop_allow_raw_html_descriptions` the example would
execute. Keeping them encoded also keeps `convert/1` idempotent (a
second pass never sees a tag that wasn't there) and a literal
`&lt;b&gt;` example stays visible text rather than rendering bold.
Attribute values (`href`, `src`, `alt`) are decoded fully, since they
are URLs/plain strings, not Markdown text.

An `<a>` whose `href` scheme is not `http`, `https`, `mailto` or `tel`
(or a relative path) — `javascript:`, `data:`, `vbscript:`, … — is
emitted as its text alone, without the link: the href is copied
verbatim into `[text](href)` and the renderer would hand it to the
browser unchanged.
Text with no HTML tag at all is returned byte-for-byte unchanged, which
is what makes `convert/1` idempotent — converting an already-converted
(or always-plain) value is a no-op. Markdown already present in text
nodes (`**bold**`, `- item`) is never escaped, it is copied through
verbatim.

`<table>` becomes a GitHub-Flavored-Markdown pipe table (header row,
`---` separator, data rows) rather than dropping the structure — a
naive cell-concatenation would silently glue adjacent cells' text
together with no separator, which loses information a reader can't
recover. The header row is whichever row is inside `<thead>`, or the
first row containing a `<th>`, or — if neither marker is present —
the table's first row, promoted, so the output is always a valid
table; any other `<thead>`-tagged rows are folded into the body
instead of being dropped. Cell text has its own line breaks collapsed
to spaces and `|` escaped to `|`, since a table row is a single
Markdown line.

# `convert`

```elixir
@spec convert(String.t() | nil) :: String.t() | nil
```

Converts `html` to Markdown. Text that contains no HTML tag at all is
returned unchanged.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
