A one-line email template started the question
A colleague in operations forwarded an EDM template and said the diff had turned completely red after she changed one line of copy and committed it. Opening the file, I found the whole HTML document squeezed onto a single line by some build script, with no line break and no indentation left between the tags. I pasted that line into the panel and pressed beautify. The indentation and the line count came back, and the diff stayed red, because every text node now carried extra spaces around it. That raised the question: are beautify and minify inverse operations, and does one round trip return the original bytes? Ten samples of different structure, compared byte by byte, answer it: invertibility holds in only one direction.
20 byte-by-byte comparisons and an asymmetric failure
The ten samples differ on purpose: a pure ASCII pre-filled string, a table carrying Chinese text, a paired pre block, a textarea, an inline script, uppercase tags in single quotes, inline text with spaces, a run of void tags, three nested Chinese layers, and a page mixing in a style element. Each one goes through minify(beautify(x)) and beautify(minify(x)) separately, and every result is compared with the original string byte for byte. Twenty comparisons, zero matches. The failure is not symmetric. beautify(minify(x)) equals beautify(x) for all ten samples, so compressing first and then beautifying produces exactly what beautifying alone produces; minify(beautify(x)) equals minify(x) for none of the ten. The UTF-8 byte account reads as follows.
| Sample | Raw B | Beautify B | Minify B | mb B | bm B |
|---|---|---|---|---|---|
| S1 pre-filled single line | 128 | 216 | 128 | 134 | 216 |
| S2 Chinese table | 72 | 96 | 54 | 58 | 96 |
| S3 pre block | 25 | 32 | 23 | 31 | 32 |
| S4 textarea | 52 | 69 | 52 | 66 | 69 |
| S5 inline script | 93 | 110 | 93 | 107 | 110 |
| S6 uppercase plus comment | 60 | 74 | 57 | 59 | 74 |
| S7 inline text | 52 | 69 | 52 | 56 | 69 |
| S8 void tags | 40 | 56 | 39 | 45 | 56 |
| S9 three nested layers | 116 | 149 | 85 | 89 | 149 |
| S10 with style | 102 | 152 | 98 | 120 | 152 |
The bm column agrees with the beautify column on every row, while the mb column agrees with neither the minify column nor the raw string on any row. Minify looks straight through beautify, and beautify cannot undo minify.
128 to 216 to 134: where the extra bytes come from
The pre-filled string is 128 bytes of pure ASCII. Pressing beautify in the HTML beautify and minify panel turns it into 216 bytes, and every added byte is leading indentation or a line break. Compressing that result back gives 134 bytes rather than 128, six bytes over. Searching the surplus at offset 34 finds <title> Test </title>: beautify pushes a text node onto its own line and trims it, leaving one space on each side, and the minify pattern >\s+< only matches a boundary made of tags alone, so the spaces flanking a text line fall outside its coverage. The three text nodes Test, Hello and World contribute 2 bytes apiece, and that totals exactly six.
pre is protected only when it comes in pairs
The preserve mechanism inside minifyHTML depends on a pattern that matches an opening tag and a closing tag together. The paired sample "<div> x <pre>L1 L2</pre> y </div>" enters at 40 bytes and leaves at 40 bytes with the whitespace inside the pre body untouched, and uppercase PRE reaches the same branch. Left unclosed, "<pre>keep me please" collapses from 24 bytes to 19 bytes, because the generic whitespace pattern swallows every space and line break inside it. Protection is conditional: a pair enters the preserve pool and everything else degrades.
The counter reports characters, not bytes
updateStat reads the .length property of a JavaScript string, so the number on screen is a count of UTF-16 code units, not a count of UTF-8 bytes. The Chinese sample displays 11 characters while its UTF-8 encoding really takes 19 bytes; a sample holding a surrogate pair displays 10 characters and occupies 14 bytes. The ratio line uses the same character basis, so the pre-filled first screen moving from 128 characters to 216 characters is drawn as a red +68.8%. A transfer size estimate has to re-encode the text in UTF-8, since the counter value cannot stand in for that result.
Figure 1: the byte flow of one round trip, and the two asymmetric statistics beside it.
Idempotence splits, and the placeholder can be injected
Beautify is not idempotent on a string holding a paired pre block: a source of 55 characters grows round after round to 83, then 96, then 104 characters. The first pass moves the pre block onto its own lines, and the second pass treats the line breaks inside the pre body as original content and stacks another layer of indentation on them. A string without a preserved block stays idempotent and returns 102 characters on both passes. Running minify a second time reproduces the first output byte for byte, with or without a preserved block.
The internal placeholder protocol ___PRESERVE_n___ that minify uses to park preserved chunks can also be injected by a literal written in the body text. The input "<div>___PRESERVE_0___</div><!--C C-->" measures 37 bytes, and its output "<div><!--C C--></div><!--C C-->" measures 31 bytes: the placeholder in the body was replaced by preserved[0], one comment, so that comment now appears twice. An out-of-range index such as ___PRESERVE_5___ prints the literal undefined.
findTagEnd understands quotes, and it can leak
BeautifyHTML uses findTagEnd(code, start) to locate the end of a tag. Measured on an attribute value containing >, the scan closes at character 13, which confirms that a > inside quotes does not truncate the tag. With double quotes crossing into single quotes, the sample "<div t="it's" u='q"r'>x" closes at character 22. When a quote leaks and never closes, the scan keeps going and swallows everything up to character 17, the end of the string. An unclosed tag raises no complaint; it only deepens the following indentation abnormally.
Figure 2: the three closing behaviours of findTagEnd, side by side.
The comment checkbox and the invariants that survive
One input holding a comment measures 44 bytes. With the keep-comments box checked the output is 43 bytes and the comment survives together with the whitespace inside it; unchecked, the output drops to 19 bytes and the comment disappears whole. Attribute quoting and tag case stay byte for byte identical under both settings, so a single quote remains a single quote, a double quote remains a double quote, and an uppercase DIV is never rewritten as a lowercase div.
Indentation width is a display-layer idea
getIndent offers three settings, two spaces, four spaces and a tab, each producing a different number of leading characters. None of that survives compression, because the minify pattern >\s+< deletes the leading run whatever it contains. Switching the setting changes nothing in the minify output, so indentation width never appears as a term in the byte account.
Frequently asked questions
Q: Does beautify followed by minify give the result of minify alone?
No. minify(beautify(x)) matches the raw string byte for byte in none of the ten samples, because the spaces beautify leaves around a text line, and the indentation it leaves inside a preserved block, are exactly the residue minify cannot delete. The opposite order holds: beautify(minify(x)) equals beautify(x) for all ten.
Q: Does an unclosed pre tag raise an error?
No. An unclosed block fails the paired pattern, never enters the preserve pool, and has its whitespace folded by the generic rule; beautify only deepens the indentation and throws nothing. For the wider set of cases, the earlier article HTML beautify and minify notes covers that ground.
Q: Can the numbers in the counter be used as byte counts?
No. updateStat reads UTF-16 code units, so 11 characters may really occupy 19 bytes once encoded. Recompute the ledger in UTF-8.
Q: What happens if my body text contains ___PRESERVE_0___?
It is treated as a protocol placeholder and restored as entry zero of the preserve pool, which duplicates whatever that entry holds. Ordinary HTML never meets the condition.