TabOnly

HTML Entity Encode / Decode

Encode and decode HTML entities — named, decimal and hex — without silently eating your markup.

input
level
numeric
output · encoded · named · hex

the escaped string appears here

Three levels, one direction each way

An HTML entity decodes from, and encodes to, one of three spellings: a named reference like &copy; whose meaning the browser knows by definition; the numeric forms &#169; and &#xA9;, spelled in decimal or hex; and for encoding, a minimal set that touches only the five characters the HTML parser treats as syntax — & < > " '. The level control above picks which one you mean. Encoding always escapes those five, whichever level is active: a string of "named" output that left the & unescaped would be broken the moment it was embedded, because the bare ampersand is where every later reference starts. The apostrophe is always &#x27; — never &apos;, which HTML 4 parsers can silently drop. Beyond the five, named converts each non-ASCII character to its readable name (&copy; for ©, &half; for ½) and numeric to a &#x…; every parser understands.

Decoding is where the page earns its keep. The reference table below the tool is the same ~2,200-entry WHATWG set the spec publishes — shipped as data, checked in, never hand-typed — and the decoder walks it the way the tokenizer does: from an &, consume the longest name that matches, which is what makes &copy decode without its semicolon while &hellip does not, and what turns the famous &notit; into ¬it; rather than an error. Measured against the same inputs a browser renders, the decoder agrees with it on every vector in the grooming doc — including the cases where the obvious hand-rolled version is wrong.

Three places a naive decoder gets your content wrong

The Windows-1252 range. Numeric references &#128; through &#159; are remapped by the spec to the characters those bytes meant in Windows-1252: &#128; is , &#147; is ". Legacy CMS exports are full of these, and a decoder that calls String.fromCharCode returns invisible control characters. This page remaps, and flags each remap in the notes strip.

The data-destroying DOM tricks. Almost every incumbent decodes by parsing the string with DOMParser and reading textContent — and that is the one that eats your markup, plus your comments and your leading whitespace. Measured: <b>bold</b> &amp; more comes back as bold & more. You cannot debug an export by running it through the parser; that is precisely what deleted it. The decoder here is a text scan, not a parser: nothing is interpreted except the references themselves, and CRLF survives untouched.

Invalid references — or rather, the categories a browser quietly fudges. &#0;, a UTF-16 surrogate, and everything past U+10FFFF all render as U+FFFD, and this page says which class each one was instead of passing a silent replacement character through. An unknown name like &foo; is kept literally, exactly as a browser keeps it, and reported as such. Decoding has no failure state that should stop your paste from coming back; the notes strip is the error reporting. None of your paste ever leaves this tab, of course — the same egress policy as everywhere else on the site (verify it yourself).

Do it without this tool

The one-liners, and the two ways they bite. First the recipe most of the internet uses, with its measured result on a pasted fragment.

javascript · the recipe that eats your markup
// DOM-based decoding — runs the string through an HTML parser, which
// silently drops tags, comments and leading whitespace:
const fragment = '<b>bold</b> &amp; more';
new DOMParser().parseFromString(fragment, 'text/html').documentElement.textContent;
// => 'bold & more'   ← the markup is gone, without even a warning

// The textarea route is a text scan and keeps the markup:
const ta = document.createElement('textarea');
ta.innerHTML = '<b>bold</b> &amp; more';
ta.value; // => '<b>bold</b> & more'
javascript · a building block that cannot inject itself
// The minimal escape — everything HTML treats as syntax, own-context only.
// It makes the string inert in *one* context; see the OWASP sheet below
// before claiming it stops XSS.
const escapeHtml = (s) => s.replace(/[&<>"']/g, c => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;',
  '"': '&quot;', "'": '&#x27;',
}[c]));

escapeHtml('Tom & Jerry said café <i>hi</i>');
python · the standard library, both ways
import html

html.escape("Tom & Jerry said café <i>hi</i>")
# 'Tom &amp; Jerry said caf\xe9 &lt;i&gt;hi&lt;/i&gt;'

html.unescape("&amp;lt;i&amp;gt;")
# '&lt;i&gt;'  — one layer per call, exactly like a browser

html.unescape("&#147;")   # '\u201c' — the Windows-1252 range, not a control code
shell · decode a legacy export without leaving the pipe
# python3 -c 'import html,sys; [print(html.unescape(l), end="") for l in sys.stdin]'
printf '&#8217;tis the season &amp; &#x1F600;\n' | python3 -c 'import html,sys; [print(html.unescape(l), end="") for l in sys.stdin]'
# 'tis the season & 😀

The specs, which one says what

The named references are the HTML Standard, §13.2.5 — the published ~2,200-entry table (html.spec.whatwg.org/entities.json), including the 106 legacy no-semicolon forms and the 93 entries that map to two code points. Its numeric character reference rules define the Windows-1252 remap for 128–159 and the three U+FFFD classes, and they are what the decoder here implements line by line. The encoding side is OWASP's XSS Prevention Cheat Sheet, whose first rule is the difference between escaping and sanitising: output encoding is context specific, which is why this page offers minimal / named / numeric and never claims to make a fragment "safe" — that decision belongs to the reader, in the context they are writing for.

Two neighbours on the same workflow — escape this string for somewhere else: percent-encoding for URLs, and base64 for bytes and files.

FAQ

Why is &#147; a curly quote instead of a control character?

The HTML spec remaps numeric references 128–159 to Windows-1252 meanings — 147 is ", 128 is , 133 is — because that is what the bytes meant where the content was written. A String.fromCharCode(147) decoder returns an invisible control character instead. The differences are flagged one by one in the notes strip.

Why do my exported pages show &amp;amp; where an ampersand should be?

Double encoding. &amp; is the encoding of the five characters &amp;, which is itself the encoding of &; a second pass over an already-escaped string produces the doubled spelling. The page decodes one layer, detects that its own output would decode again, and offers the second pass.

Why encode an apostrophe as &#x27; and never as &apos;?

&apos; is XML, not HTML 4, so an old parser can drop it. &#x27; is the same apostrophe as a numeric reference every parser since the 1990s understands. The tool emits the numeric form and never the named one, and the minimal level documents why in its copy above.

Does escaping & < > " ' stop XSS?

Escaping the five is necessary for text-interpolation contexts and exactly right there; it is not a blanket guarantee. In an attribute, in a URL, in <script> or in CSS, different characters carry the danger. This page escapes for HTML text; the OWASP sheet linked above is the map for the rest.

Why did another decoder delete the tags out of my pasted fragment?

The decoder was a parser. DOMParser + textContent drops tags, comments and leading whitespace — measured <b>bold</b> becomes bold with no error. The scan here is pure text: only the references are interpreted, everything else survives byte for byte.

related tools