The page says ASCII, yet one Han character prints three numbers
A colleague wanted to turn a Chinese sentence into ASCII. The page handed back 228 184 173, three numbers; she pasted the same text again and got the same three. The ASCII table stops at 127, so a Han character should never have had an ASCII code to begin with. The second round was stranger: Hi went through the text-to-binary mode and came out as 01001000 01101001. She found that space in the middle annoying, moved the separator to the option that inserts nothing, pasted the string back in for decoding, and one character survived: i.
Read the text encoding converter page once and it falls into place: beneath the three modes there is one encoder plus three splitting rules, and the assumption that one character equals one code has never held. The trap is deepest for people who only write English, because plain Latin text really is one byte per character, so the two measurements agree every time and nothing hints that they are different things.
There is only one encoder, and it always emits UTF-8
Line 136 and line 160 both call new TextEncoder().encode(text). That API accepts no encoding argument and has exactly one output, UTF-8, so how many numbers a character prints is decided by how many bytes it occupies:
| Input | Characters counted by the page | UTF-8 bytes | Output under the ASCII label |
|---|---|---|---|
A | 1 | 1 | 65 |
中 | 1 | 3 | 228 184 173 |
fullwidth A | 1 | 3 | 239 188 161 |
Hi | 2 | 2 | 72 105 |
The binary tab behaves the same way: line 138 runs padStart(8, "0") so every byte is filled to 8 bits, while the decimal side on line 161 pads nothing, which is why 7 is written as 7. One Han character takes three groups, 24 bits in all, and with a space as the separator the output runs 26 characters long; that four-byte emoji is four groups, 32 bits, 35 characters.
Fig. 1: the label spelling ASCII码 sits on line 102 of the template, and everything it has ever produced is UTF-8 bytes. Lines 1180 through 1184 of config/tools.php add 5 more pieces of copy that promise the same word; of the 15 claims that can be checked here, 7 land on [agrees] and 8 on [conflicts].
With no separator, 100% of what the tool encodes cannot be undone
The separator only takes part in the forward join; the reverse direction always re-tokenises. The binary decoder on line 144 treats every character that is not 0 or 1 as whitespace, and the ASCII decoder on line 166 treats every character that is not a digit as whitespace. Nothing on that side is ever told how many bytes the original string had.
- With the space, comma and newline separators, 6 source strings round-trip byte for byte in both directions, which is 48 trips in total.
- Under 无分隔 the output is 100% irreversible:
Hicollapses into a singlei, because line 148 reads0100100001101001as one whole integer and18537mod 256 gives105, which is exactlyi. - Same option, 2 newlines written as
\n\nleave only one, and that mixed string (A,中, an emoji, fullwidthA,1) decodes to U+0000. The ASCII side refuses the whole string with解码失败:无效的ASCII码 228184173, a decode-failure message that echoes back the digits it could not split.
Fig. 2: reversible groups out of 6 source strings under the 4 separator options. The longer the string, the worse this gets: line 148 runs parseInt(parts[i], 2), which hands back a double, and the safe integer ceiling is 9007199254740991. Past it, even grabbing the last byte is wrong, because that mixed string of 96 bits comes out of mod 256 as 0 while its real last byte is 49.
One out-of-range value, two reverse paths, two policies
The ASCII decoder has an n > 255 check on line 170: a single hit abandons the whole string and echoes back the first out-of-range token, which is where 256, 257 and 65536 all land. The binary decoder on line 148 validates nothing at all; the value is pushed into a Uint8Array and truncated by the modulo, so 511 is stored as 255 and 300 as 44. That is why 111111111 becomes U+FFFD on the binary side and a plain error on the ASCII side.
The template writes 4 error exits and 3 of them turn out unreachable: TextDecoder is never given {fatal: true}, so an illegal byte sequence only produces U+FFFD and throws nothing, which makes both catch blocks dead code; and the minus sign is already eaten as a separator on line 166, so -5 decodes to U+0005. The generic conversion-failed message 转换失败: appears 0 times across the whole matrix.
Fullwidth and halfwidth: 94 code points change, 146 come back untouched
This option is not a general fullwidth-halfwidth converter; it is code-point arithmetic with two fixed offsets. The pattern behind fullwidth-to-halfwidth only covers U+FF00-U+FFEF, and what really gets rewritten are the 94 code points in U+FF01-U+FF5E; the other 146 in the same block come back unchanged, and neither 。 nor 、 is in the pattern at all.
The case most often mistaken for a bug is the fullwidth space: line 184 reads if (code === 0x3000) return " ";, but the pattern on line 182 does not include U+3000, so that branch never runs. Measured, ! A turns into ! A, the character count stays at 3, and the byte count falls from 9 to 5.
Halfwidth-to-fullwidth covers U+0020-U+007E, 95 code points in all, and line 194 singles out the space and maps it to U+3000. In both directions .length stays constant, so only the byte count moves: the 7 characters of ABC 12! weigh 21 bytes and become 7 characters of ABC 12! weighing 9, since those 3 bytes of U+3000 cannot be saved. Reasoning back from byte counts to an encoding is a separate matter, covered in telling GBK and UTF-8 apart.
There is no hexadecimal tab, and other encodings only come in corrupted
toString(16) appears 0 times in the template, and none of the three prefixes 0b, 0x or \u is ever added, so you never get e4 b8 ad; there is no branch for Base64 or for HTML entities either. Pasting another encoding in raises no error, it just drops the letters as if they were separators:
- The hex string
48 65 6C 6C 6Frun through the ASCII decoder yields0Aplus 3 U+0006 characters, and the URL-encoded form%48%65%6C%6C%6Fgives exactly the same result. \u4e2ddecodes to U+0004 U+0002; if you want Chinese written in\uform, that is what Chinese to Unicode covers.- A lone
228decodes to U+FFFD, because0xE4is a UTF-8 lead byte whose continuation bytes are missing; paste228 184 173back as one run and you get中. Under the ASCII label sits a UTF-8 decoder all along.
The counter counts code units, yet the input really has no limit
Line 229 fills the field labelled 输入字符 from .length, which is a count of UTF-16 code units: one emoji prints 输入字符:2, that mixed string prints 6 characters and 12 bytes, and its real code points number only 5.
Neither textarea, on line 37 or line 44, carries a maxlength, and there is no length gate in the JavaScript either: 10000 copies of A produce 29,999 characters of ASCII output and 89,999 characters of binary output, and pure computation on Node v24.19.0 takes 1.0 ms for one and 2.4 ms for the other; at 100000 characters it is 6.3 ms and 18.4 ms. Both directions come back identical byte for byte, with nothing truncated. How a browser lays that out cannot be measured on this machine, so keep those milliseconds as pure computation only.
Three questions that do not go away
Q: Can the text-to-ASCII mode be used on Chinese?
What you receive is not ASCII but the UTF-8 byte sequence of it. If you need strict ASCII, check first that every character sits under 127, which for English-only text is usually true and exactly why the label survives so long.
Q: How can I be sure the encoding still pastes back into the original text?
Choose space, comma or newline as the separator and stay away from the option that inserts nothing, where even the encoding produced by the tool itself cannot be restored. The reverse direction understands only this one format; paste in another encoding string and nothing is reported, it quietly becomes control characters.
Q: Why does the fullwidth space not convert to halfwidth?
Line 184 means to handle U+3000, but the pattern on line 182 only covers U+FF00-U+FFEF, so that branch is unreachable; going the other way, a halfwidth space is mapped by line 194 onto U+3000, so only one direction can ever be walked.