HTML Entity Encode / Decode
Encode and decode HTML entities — named, decimal and hex — without silently eating your markup.
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 © whose
meaning the browser knows by definition; the numeric forms
© and ©, 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 ' — never ', which HTML 4
parsers can silently drop. Beyond the five, named converts each
non-ASCII character to its readable name (© for ©,
½ 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 © decode without its semicolon while
&hellip does not, and what turns the famous
¬it; 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 € through
Ÿ are remapped by the spec to the characters those bytes
meant in Windows-1252: € is €,
“ 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> & 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.
�, 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.
// DOM-based decoding — runs the string through an HTML parser, which
// silently drops tags, comments and leading whitespace:
const fragment = '<b>bold</b> & 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> & more';
ta.value; // => '<b>bold</b> & more'
// 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 => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": ''',
}[c]));
escapeHtml('Tom & Jerry said café <i>hi</i>');
import html
html.escape("Tom & Jerry said café <i>hi</i>")
# 'Tom & Jerry said caf\xe9 <i>hi</i>'
html.unescape("&lt;i&gt;")
# '<i>' — one layer per call, exactly like a browser
html.unescape("“") # '\u201c' — the Windows-1252 range, not a control code
# python3 -c 'import html,sys; [print(html.unescape(l), end="") for l in sys.stdin]' printf '’tis the season & 😀\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 “ 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; where an ampersand should be?
Double encoding. & is the encoding of the five characters
&, 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 ' and never as '?
' is XML, not HTML 4, so an old parser can drop it.
' 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.