splitting on newline leaves an invisible carriage return, so the line that closes your parser never matches
encoding · case
Symptom
Section titled “Symptom”A line parser that works on every file you wrote yourself silently mis-parses a
file that came from Windows. The classic shape is a delimiter that never closes:
a code fence, a heredoc, a BEGIN/END block, a front-matter ---. Everything after
the opening delimiter is swallowed into a region that was supposed to end three
lines later.
Nothing errors. The text is right there in your editor, the closing line looks
identical to the one you are matching, and console.log prints it back looking
correct. Diff views, terminals, and log aggregators all render CR as nothing.
const text = "BEGIN\r\nbody\r\nEND\r\nafter";const lines = text.split("\n");lines[0] === "BEGIN"; // false <- the block never opensJSON.stringify(lines[0]); // '"BEGIN\r"'Same shape in Python:
"a\r\nb".split("\n")[0] == "a" # FalseAnd the anchored-regex version, which fools people who thought they had escaped it:
/^BEGIN$/.test(lines[0]) // false — $ matches before \n, not before \rYou get CRLF input without asking: git with core.autocrlf=true checks files out
that way, Notepad writes it, Set-Content and Out-File write it, and every
Windows process that prints line-by-line emits it.
Windows terminates lines with CRLF; POSIX with LF alone. Splitting on "\n"
consumes only the LF, so the CR stays glued to the end of the preceding element.
Every downstream exact comparison then fails against a character with no glyph.
Regex $ does not save you. In JavaScript multiline mode and in Python, $
matches before a trailing newline — not before a trailing CR — so /^x$/ is false
for "x\r" in exactly the same way === "x" is.
This is why the bug survives review: the failing value is invisible in every
medium a reviewer looks at. It shows up only under JSON.stringify, cat -A,
xxd, or an editor with “show invisibles” turned on.
Workaround
Section titled “Workaround”Split on the line break, not on the newline character:
const lines = text.split(/\r?\n/);If you must keep split("\n") — because you are streaming, or the offsets matter —
make every terminator-sensitive pattern tolerate the residue:
/^ *\r?$/.test(rest) // blank-line check/^(`{3,})([ \t]*\r?)$/.exec(line) // fence closePython’s str.splitlines() and Go’s bufio.Scanner handle both terminators
already. Reading a file as text with a runtime that normalizes is fine; the trap
is specifically hand-rolled splitting on "\n".
The corpus’s other encoding cases are about how bytes are written — BOMs, UTF-16, the CP949 codepage. This one is about how they are cut: the encoding is fine, the file is valid, and one invisible byte per line breaks equality.