# Turn HTML into plain text

Shape / columns

A variant of [Clean up text](https://meridian.online/operations/clean-up-text).

Remove HTML tags from text and decode its character entities.

Page: <https://meridian.online/operations/clean-up-text/strip-html-from-text>

## Status

Runs as a SQL step.

arcform carries it as a `sql:` step today.

## The DuckDB step

remove HTML tags and decode named, decimal and hex entities in one pass

```sql
CREATE TEMP TABLE raw AS SELECT '<p>Tom &amp; <b>Jerry</b> &lt;3 &quot;cheese&quot;&nbsp;&#65;&#x42;&#x1F600; R&D &#38;amp; &#1114112;&#xD800;</p>' AS html;
-- The text is cut into entities and the stretches between them, and each piece is decoded once, so &#38;amp; gives &amp;
-- and not &. A numeric entity chr() cannot encode (above U+10FFFF, or a surrogate) is left as written.
CREATE TEMP TABLE result AS
SELECT array_to_string(list_transform(regexp_extract_all(regexp_replace(html, '<[^>]*>', '', 'g'), '&[#a-zA-Z0-9]+;|[^&]+|&'),
  lambda t: CASE
    WHEN t = '&lt;' THEN '<' WHEN t = '&gt;' THEN '>' WHEN t = '&quot;' THEN '"' WHEN t = '&nbsp;' THEN chr(160) WHEN t = '&amp;' THEN '&'
    WHEN regexp_matches(t, '^&#[0-9]+;$') AND TRY_CAST(t[3:length(t) - 1] AS BIGINT) BETWEEN 1 AND 1114111
         AND NOT TRY_CAST(t[3:length(t) - 1] AS BIGINT) BETWEEN 55296 AND 57343 THEN chr(t[3:length(t) - 1]::INT)
    WHEN regexp_matches(t, '^&#[xX][0-9a-fA-F]+;$') AND TRY_CAST('0x' || t[4:length(t) - 1] AS BIGINT) BETWEEN 1 AND 1114111
         AND NOT TRY_CAST('0x' || t[4:length(t) - 1] AS BIGINT) BETWEEN 55296 AND 57343 THEN chr(('0x' || t[4:length(t) - 1])::INT)
    ELSE t END), '') AS plain
FROM raw;
SELECT * FROM result;
SELECT CASE WHEN ((SELECT plain FROM result) = 'Tom & Jerry <3 "cheese"' || chr(160) || 'AB' || chr(128512) || ' R&D &amp; &#1114112;&#xD800;') THEN 'as expected' ELSE error('the HTML did not become plain text with each entity decoded once and the ones chr cannot encode left as written') END AS verdict;
```

The statement builds its own data.

It needs nothing installed beyond DuckDB itself.

## What other tools call it

Meridian's classification; an independent blind rating agreed on 95.7% of 324 rows (variant, version 3).

- Duckle: [`Strip HTML`](https://github.com/slothflowlabs/duckle/blob/f85f8960edfb6efbf4cb89d58a7b32e16df0e867/frontend/src/workflow-ui/palette-data.ts) (read 2026-09-25)
- Apache Hop: [`HTML 2 Text`](https://hop.apache.org/manual/latest/pipeline/transforms/html2text.html) (read 2026-09-27)
- OpenRefine: [`Unescape HTML entities`](https://openrefine.org/docs/manual/cellediting#unescape-html) (read 2026-09-27)
- RapidMiner Studio: [`Unescape HTML`](https://docs.rapidminer.com/latest/studio/operators/extensions/Web%20Mining/utility/unescape_html_attribute.html) (read 2026-09-27)
- RapidMiner Studio: [`Unescape HTML Document`](https://docs.rapidminer.com/latest/studio/operators/extensions/Web%20Mining/html_processing/unescape_html.html) (read 2026-09-27)
