Turning HTML into plain text is four jobs, not one: remove the tags, drop the parts that are not text at all (scripts, styles and comments), turn block elements such as paragraphs and list items into line breaks, and decode the entities like &amp; and &#8217; back into characters, once, after the tags are gone. Most broken conversions skip one of those jobs. The words of two paragraphs run together, a line of JavaScript turns up in the middle of the text, &mdash; is still spelled out, or a stray < swallows the rest of a sentence.

If you just need the text now, paste the markup into the FindUtils HTML Tag Stripper. This post explains what each of the four jobs involves, with examples tested in PHP 8.4, so you know what to check in any converter.

Job 1: Remove the Tags, Without Gluing Words Together

The simplest stripper deletes everything from < to >. PHP's built-in strip_tags() does this with a small state machine, and it also removes HTML comments and PHP tags. Here is what it returns for a few inputs, run on PHP 8.4:

Inputstrip_tags() output
<p>color is blue</p><p>size is huge</p>color is bluesize is huge
<p>A</p><!-- note > here --><p>B</p>AB
<script>var x=1;</script><p>Hi</p>var x=1;Hi
<style>p{color:red}</style><p>Hi</p>p{color:red}Hi
<p>Fish &amp; chips &mdash; &#8217;</p>Fish &amp; chips &mdash; &#8217;

The first row is the most common complaint. A paragraph boundary is a line break to a reader, but in the source it is only a tag, so deleting the tag deletes the break. "blue" and "size" become one word. A search index then cannot find either word, and a word count is off by one.

Job 2: Drop Scripts, Styles and Comments Completely

Rows three and four show the second problem. Removing the <script> and <style> tags leaves their contents, which are code, not prose. A saved web page can carry more script than text, so a plain tag stripper can hand you a wall of JavaScript with a few sentences inside.

The fix is to remove these elements together with everything between their opening and closing tags, before the general tag removal. Comments need the same treatment. strip_tags() handled the comment above correctly, but a hand-written pattern that stops at the first > would end the comment early at note > and leave here --> in the text.

The browser's own text properties show the same split. MDN's textContent page explains that textContent returns the content of all elements, including <script> and <style>, while innerText only returns "human-readable" text, takes CSS into account, and skips hidden elements. If you convert inside a browser, innerText is usually closer to what a person sees, but reading it forces a layout, which is slower on large documents.

Job 3: Keep the Line Breaks That Block Elements Imply

To keep the shape of the text, a converter has to treat block elements differently from inline ones:

  • Block elements such as <p>, <div>, <h1> to <h6>, <li>, <tr>, <blockquote> and <pre> start a new line. So does <br>.
  • Inline elements such as <strong>, <em>, <a> and <span> are removed and their text stays in place, without adding a space. Adding a space here would break words like <b>F</b>indUtils.

Two cases need a decision rather than a rule. Table cells can become new lines (easy to read) or tabs (easy to paste into a spreadsheet). Text inside <pre> keeps its own spacing and line breaks in HTML, so a converter that squeezes all whitespace will flatten code samples. After the conversion, runs of blank lines and double spaces are normal, and a pass with the Whitespace Cleaner tidies them.

Job 4: Decode Entities Once, After Stripping

HTML writes some characters as entities: &amp; for &, &lt; for <, &nbsp; for a no-break space, and numeric forms like &#8217; for a curly apostrophe. As the last table row shows, strip_tags() leaves all of them encoded. PHP's html_entity_decode() turns them back into characters: with the ENT_QUOTES | ENT_HTML5 flags and UTF-8, the row becomes Fish & chips — ’.

The order matters, and so does doing it once:

  • Decode before stripping, and text turns into tags. A tutorial that says "type &lt;b&gt; for bold" means the reader should see <b>. Decode first, and the stripper sees a real <b> tag and deletes it, so the example vanishes.
  • Decode twice, and escaped text changes meaning. &amp;lt;b&amp;gt; is how a page shows the literal text &lt;b&gt;. One decode gives &lt;b&gt;, which is correct. A second decode gives <b>, which is not what the author wrote.
  • &nbsp; is not a normal space. The PHP manual notes that trim(html_entity_decode('&nbsp;')) does not return an empty string, because the decoded character is a no-break space (U+00A0), not the ASCII space that trim() removes. If your cleanup splits on spaces, decide whether to convert no-break spaces to ordinary ones.

To look up an entity you do not recognise, search for it in the HTML Entity Finder, which lists the named and numeric forms of each character.

The Stray < That Swallows Text

The PHP manual warns that because strip_tags() "does not actually validate the HTML, partial or broken tags can result in the removal of more text/data than expected." In testing, PHP 8.4 kept if a < b and c > d then intact, because a < followed by a space does not start a tag. But <p>x <3 y</p><p>next</p> came back as just x : everything after <3 was treated as the inside of a tag.

Real content contains these characters: maths, code snippets, "<3" in comments, arrows like <--. Correct HTML writes a literal < as &lt;, but text pasted from emails, chat exports and old CMS fields often does not. Whatever tool you use, compare the end of the output with the end of the input. If text is missing, look for a bare < just before the gap.

Stripping Tags Is Not Sanitising

It is tempting to run user input through a tag stripper and call it safe. The PHP manual says plainly that strip_tags() "should not be used to try to prevent XSS attacks" and that tags you allow keep all their attributes, including onmouseover. Plain text for a search index, a word count or a prompt is one job. Making user HTML safe to display is a different one, which needs escaping on output or a proper sanitiser.

Doing It in the Browser With FindUtils

The HTML Tag Stripper covers the four jobs for everyday pasting:

  1. Paste the HTML, or click Load sample to see an example.
  2. The plain text appears at once. Paragraphs, headings, list items, table rows and <br> become new lines, inline tags are removed with their text kept, and script and style blocks are dropped with their contents.
  3. The common entities (&amp;, &lt;, &gt;, &quot;, &#39; and &nbsp;) are decoded. Other named and numeric entities, such as &mdash; or &#8217;, are left as written. Look them up in the HTML Entity Finder or replace them with Find & Replace.
  4. Check the counters. Tags removed, Characters before, Characters after and Lines out show how much was taken away.
  5. Click Copy. Then run the Whitespace Cleaner if you want single spacing, or the Word Counter if you need a count.

If you want to keep headings, links and lists as structure rather than drop them, convert with HTML to Markdown instead.

A Checklist for Any HTML-to-Text Conversion

  • Script, style and comment content is gone, not just their tags.
  • Paragraphs and list items start on new lines, and no two words are glued together.
  • Entities are decoded exactly once, after the tags are removed.
  • No-break spaces are handled the way your next step expects.
  • The end of the output matches the end of the input, with no text lost to a stray <.
  • Tables come out as lines or tabs, whichever your destination needs.
  • The result is used as text, not trusted as safe HTML.

Need help with a project? The team behind FindUtils takes on freelance work through Upwork, including PHP bug fixes, a PHP 8 upgrade or new features for your website.

FAQ

Why are my words joined together after stripping HTML?

Two block elements sat side by side with no space between them in the source, for example </p><p>. Removing the tags removes the only thing separating the words. Use a converter that turns block elements into line breaks.

Does strip_tags decode & and other entities?

No. strip_tags() only removes tags, so &amp; and &#8217; stay as written. Run html_entity_decode() on the result, once, after stripping.

Why did JavaScript code end up in my plain text?

The stripper removed the <script> tags but kept what was between them. Script and style elements have to be removed together with their contents.

What is the difference between textContent and innerText?

According to MDN, textContent returns the text of every element, including scripts and styles, while innerText returns only human-readable text, respects CSS and skips hidden elements.

Is strip_tags safe against XSS?

No. The PHP manual warns against using it to prevent XSS. Escape text on output, or use a dedicated HTML sanitiser when you have to keep some tags.

Why is part of my text missing after stripping?

A bare < followed by a letter or number, such as <3, can be read as the start of a tag, so everything up to the next > is removed. Write it as &lt; in the source, or check the output against the input.

Sources