Why a Pasted-Back String Stops Halfway
Paste an escaped string into this page's restore box and the usual outcome is not an error but a shorter string. The reason sits in fromUnicode (L126-150): four regexes queue up and each one sweeps the whole string, at L130 / L135 / L140 / L145. Step 1 accepts \u plus a {4,6} run of hex digits, step 2 accepts &#(\d+);, step 3 accepts &#x([0-9a-fA-F]+);, and step 4 accepts a bare backslash plus hex digits with one trailing \s?. Each step re-scans the whole string, so the last three steps read the characters the first three have just emitted. The intermediate states come out of a plain Node run with a fake DOM harness: the four step expressions were cut verbatim from the template and executed, and they agree with the in-page function on 96 inputs one by one. The rendering layer was not measured.
The relay needs at least 2 hops: \u0026#65; (10) becomes A (5) after step 1 and A after step 2; \0041 (9) has to wait for step 4 to finish; \u0026#x5c;0041 (15) uses all 3 hops and only works because step 3 is ordered before step 4. Widening the sample to 39 chained strings: step 1 changes something on 22 of them, after the first hop 30 are still matchable by the remaining three steps, after the second hop 22, after the third hop 13.
Figure 1: four boxes marking the step order and the hit counts.
A Pattern Belongs to the Step It Is Placed In
- Stuck at 5 code units:
A(10) - only step 3 can emitA, but step 2 has already run. - Stuck at 5 code units:
\u0026#38;#65;(14) lands here after step 2, and there is no fifth step to rescue it. - Stuck at 6 code units:
\u0041(10) emits\u0041, which step 4 cannot reach: it wants a backslash immediately followed by hex digits, andudoes not count.
The Greedy {4,6}: One A After a Chinese Character Breaks a Run
The encode side writes a fixed 4 hex digits and leaves no separator inside the escape, while the restore side allows up to 6. A character sitting right after an escape is read as the 5th digit whenever it is a hex letter, so 中A encodes to \u4e2dA (7) and returns as one code unit, U+E2DA: the arithmetic is 20013 folded into 320218 with remainder 58074. The same shape sends 工F through 24037 → 384607 → 56927, and 中文A (13) comes back as 中 plus U+587A. The counterexample is G, which is not a hex letter: 中G returns the original 2 code units.
The corpus is 1194 synthetic strings, 6 Chinese characters by 65 ASCII characters by 3 positions plus 24 surrogate-pair combinations, run through 6 tiers for 7164 round trips. hex/non-ascii passes 798/1194 = 66.83%, and every failure falls in the group whose neighbouring character is in 0-9a-fA-F: hexLetter 0/216 and digit 0/180, together 0/396; the other 774 pass 774/774. The strings are synthesized category by category, so the ratio is not a share of real text that breaks. The control tiers show the boundary: under scope=all every code unit is fixed length, 中A becomes \u4e2d\u0041 (12) and returns unchanged, and hex and hex-css each report 1194/1194 = 100.00%; the dec tier leans on the semicolon as its boundary and also reports 100.00% under both scopes. The difference between hex/non-ascii and hex/all is exactly those 396 strings.
Spelling Past Four Digits Is Truncated in Silence
An out-of-range code point gets no feedback; the value is taken modulo 65536 and that is the end of it. \u1F600 (7), 😀 (9) and 😀 (9) all three hand back the same U+F600, remainder 62976; \u1FFFFF (8) lands on U+FFFF, and � (15) emits only 1 code unit. The modulo of String.fromCharCode was probed at 8 further points, among them 128512 → 62976, 1000000 → 16960 and 2147483648 → 0. Spelling alone decides whether anything comes back: split into two halves, \uD83D\uDE00 (12) returns 2 code units, while the half-written \uD83D (6) leaves an orphan surrogate. Across 96 inputs nothing was ever thrown, and fromUnicode carries no try/catch and no toast.
The CSS Tier: One Space and a Run of Short Escapes
The \s? at the tail of step 4 swallows only 1 whitespace, and this tier loses something else on top of that. \4e2d \6587 (11) comes back as 2 code units with the space gone; \4e2d \6587 (12) still has 3 left. A tab and a full-width space each lose 1 the same way, and strings this page produced itself are treated the same: 中文 工具 turns into a string of 21 code units and comes back with 4. hex-css/non-ascii therefore drops to 744/1194 = 62.31%, and the punct category is left with 180/234. The reverse difference comes from the constant 4-digit output: because of padStart(4,'0') all 65408 code points from 128 to 65535 emit exactly 5 code units, so a short escape produced elsewhere never reaches the lower bound of {4,6}. \41 \42 (7) and \4e (3) stay untouched, while the two adjacent two-digit escapes \4142 (5) merge into one four-digit escape and restore as U+4142.
The Swap Button Is Not Undo
With 0 code units in the output box, swapText (L194-208) emits the toast 「无可交换的内容」 once and returns; the input, the mode, the result card, the stats and the selects, five places in all, do not move. When there is content it switches the mode on the side: in the to tier, 中文工具 gives 24 code units, after the swap the mode is from and the stats read 24 / 4, and swapping once more returns to the starting values. Two presses in a row do bring it back, but the press in between already changed the mode. The swallowed line never returns: in the from tier, \u4e2dA (7) yields a private-use code point of one code unit, and switching back to the to tier reads it as a fresh Chinese character and escapes it into \ue2da (6). Neither select value is ever cleared, since L93/L94 write only the display of the parent element, and clearAll clears four places but neither the mode nor the selects. Hidden effects, toast duration and whether the placeholder is truncated were not measured.
Checked Against the List Page Copy
The intro on lines 997-1011 of config/tools.php was judged entry by entry: 16 entries, 7 consistent, 2 conflicts, 7 partly consistent. Both conflicts sit on exclusive wording, a description of restore as accepting only \uXXXX (although A (6) and \4e2d (5) also decode) and a tip that says 4 hex digits against the {4,6} of the restore side. The conclusion stops at this page. The concept layer is already covered by the older Chinese Unicode converter article and is not repeated here; for tiers and options see the unicode-convert tool page, and for the format family see the text encode page.
FAQ
Q: why does an A after a Chinese character break the string?
The restore side allows up to 6 digits and the escape leaves no separator, so an A, being a hex letter, is merged in as the 5th digit and two characters turn into one code unit; swap in a G, which is not a hex letter, and the string comes back exactly as it went in.
Q: why does it neither raise an error nor warn me?
There is no failure branch at the string layer: parseInt uses however many digits it finds, an out-of-range value is taken modulo 65536, a half-written escape stays as written, and the 96 probe inputs threw nothing.
Q: how do I write it so it comes back unchanged?
Inside this page's tiers a boundary between escapes is enough, and scope=all and dec each give 1194/1194 = 100.00%. A code point past four digits has to be written in two halves: \uD83D\uDE00 (12) returns 2 code units, while \u1F600 (7) leaves one.