The six modes, and which one you want
People arrive here wanting one of six different things, and picking the wrong one is an easy way to make a mess you cannot undo. They differ in what they treat as structure worth keeping.
- Remove all line breaks — every break goes and the document becomes one continuous paragraph. Right for a quotation, a search box, or a field that must be one line. Wrong for anything with more than one paragraph, because you will not get them back.
- Keep paragraphs, unwrap lines — the mode above with one exception: a blank line means "new paragraph" and survives. This is what almost everyone actually wants after copying out of a PDF, and it is what this page opens in.
- Remove blank lines — the opposite job. Line structure is untouched; the empty lines between the lines go. Useful for lists that arrived double-spaced.
- Join lines with a separator — turns a column of lines into a delimited list. The separator is yours, and each line can be wrapped in SQL or JSON quoting on the way out.
- Tidy extra spaces — leaves every break where it is and normalises the horizontal whitespace instead: runs of spaces, tabs, and the no-break spaces a word processor scatters through justified text.
- Convert line endings — changes nothing but the bytes between lines. Converts a file to LF, CRLF or CR without touching a character of content.
Switching mode resets the options to that mode's defaults, deliberately. Carrying a "collapse the spaces" setting into a mode whose entire purpose is not to alter the lines would be a good way to lose data quietly.
What each mode does to each invisible character
This is the table that took the longest to get right, and it is not written by hand. Every
cell below was produced by running the tool on the input in the second column while this
page was being built, which means the table cannot describe behaviour the tool does not
have. The probe is deliberately minimal: the letter A, the character, the
letter B.
| Character | Input | Remove all line breaks | Keep paragraphs, unwrap lines | Remove blank lines | Join lines with a separator | Tidy extra spaces | Convert line endings |
|---|---|---|---|---|---|---|---|
| LF U+000A | A␊B | A·B | A·B | A␊B unchanged | A,·B | A␊B unchanged | A␊B unchanged |
| CRLF U+000D U+000A | A␍␊B | A·B | A·B | A␍␊B unchanged | A,·B | A␍␊B unchanged | A␊B |
| CR U+000D | A␍B | A·B | A·B | A␍B unchanged | A,·B | A␍B unchanged | A␊B |
| NEL U+0085 | A⟦NEL⟧B | A·B | A·B | A⟦NEL⟧B unchanged | A,·B | A⟦NEL⟧B unchanged | A␊B |
| LS U+2028 | A⟦LS⟧B | A·B | A·B | A⟦LS⟧B unchanged | A,·B | A⟦LS⟧B unchanged | A␊B |
| PS U+2029 | A⟦PS⟧B | A·B | A·B | A⟦PS⟧B unchanged | A,·B | A⟦PS⟧B unchanged | A␊B |
| NBSP U+00A0 | A⟦NBSP⟧B | A⟦NBSP⟧B unchanged | A⟦NBSP⟧B unchanged | A⟦NBSP⟧B unchanged | A⟦NBSP⟧B unchanged | A·B | A⟦NBSP⟧B unchanged |
| TAB U+0009 | A→B | A→B unchanged | A→B unchanged | A→B unchanged | A→B unchanged | A·B | A→B unchanged |
Three things fall straight out of it. Of the 8 characters, none is left alone by all six modes, and none is rewritten by all six — so "does this tool remove X" has no answer that is not also a question about which mode. 5 of them (CRLF, CR, NEL, LS, PS) are altered by 4 of the 6 modes, which is as high as the table goes. And the two that are not line breaks at all — NBSP and TAB — are touched by exactly one mode, Tidy extra spaces. That is the design, not an oversight: a no-break space is a space, and a mode that promised to leave your lines alone has no business rewriting the characters inside them.
Where each of these characters comes from
Knowing which byte you have is most of the diagnosis, because each one implicates a different application.
| Character | Name | Kind | Where it comes from |
|---|---|---|---|
| LF U+000A | LINE FEED control alias | Line break | The line ending of Unix, macOS, the web, JSON, and every string literal written "like\nthis". Text copied out of a code editor or a terminal is almost always LF. |
| CRLF U+000D U+000A | CARRIAGE RETURN + LINE FEED sequence | Line break | The Windows line ending, and the one the internet protocols specify: HTTP/1.1 header lines and CSV records under RFC 4180 both end with CRLF, and both specifications were read for this site. Anything saved by a Windows editor, or produced by PowerShell or a .bat file, arrives this way. |
| CR U+000D | CARRIAGE RETURN control alias | Line break | On its own, the line ending of the classic Mac OS (through Mac OS 9) and of some lab and point-of-sale equipment. Far more often it is the leftover half of a CRLF that a regex removed the LF from — damage a browser hides completely, and that terminals, editors and string comparisons do not. |
| NEL U+0085 | NEXT LINE control alias | Line break | A C1 control from ISO 6429, and the line ending of IBM mainframe EBCDIC text. It shows up in data exported from mainframe systems and in files transcoded from EBCDIC without newline mapping. Almost nothing on a PC displays it as a break, so the text looks like one long line in some programs and two lines in others. |
| LS U+2028 | LINE SEPARATOR name | Line break | Unicode’s own unambiguous "break the line here, same paragraph" character. Page-layout applications emit it for a soft line break, and it survives copy-paste out of them. Browsers render it as a line break; many plain-text editors do not. |
| PS U+2029 | PARAGRAPH SEPARATOR name | Line break | The paragraph-level partner of LINE SEPARATOR. Text pasted out of layout and DTP software can carry it in place of a blank line, which is why a "remove blank lines" pass sometimes has nothing to remove even though the text plainly has paragraphs. |
| NBSP U+00A0 | NO-BREAK SPACE name | Looks like a space | Not a line break at all — a space that forbids one. Word processors insert it between a number and its unit, HTML authors write it as , and PDF text extraction produces runs of it where the original was justified. It looks exactly like a space and is matched by almost no "collapse the spaces" regex. |
| TAB U+0009 | CHARACTER TABULATION control alias | Looks like a space | The column separator of TSV data and the indentation of most source code. Copying a table out of a spreadsheet or a PDF gives you tabs between the cells, and turning them into spaces destroys the column structure you were trying to keep. |
Nine one-line fixes that do not work
Everything on this page exists because the obvious solution — one regex, ten seconds — is wrong in ways that do not announce themselves. Below are 9 patterns taken from the shape of answers that circulate on forums, each run against an input that exposes what it misses. The output column is not an illustration. It is the string the pattern in the first column returned when this page was built.
Sorted by how they fail: 3 leave an orphan carriage return behind, 5 change nothing whatsoever, and 1 destroys structure it should have kept. The classification is computed from each pattern's own output, not assigned by an author.
| Pattern | In | Out | How it fails | What works |
|---|---|---|---|---|
text.replace(/\n/g, ' ') The answer at the top of every "remove line breaks in JavaScript" thread. | first␍␊second 66 69 72 73 74 0D 0A 73 65 63 6F 6E 64 | first␍·second 66 69 72 73 74 0D 20 73 65 63 6F 6E 64 | orphan CR. The file came from Windows, so each break was CR + LF. Only the LF matched. Every line now ends with a carriage return that nothing displays — the text looks joined in a browser and still breaks in a terminal, an editor, or a database column. | text.replace(/\r\n|[\r\n]/g, ' ') first·second |
text.replace(/\n/g, '') The same mistake with an empty replacement, which hides it better. | first␍␊second 66 69 72 73 74 0D 0A 73 65 63 6F 6E 64 | first␍second 66 69 72 73 74 0D 73 65 63 6F 6E 64 | orphan CR. Now the two words are welded together *and* an orphan CR sits between them. Byte count went down, so the operation looks like it worked. Paste this into a CSV and the row silently splits in some readers. | text.replace(/\r\n|[\r\n]/g, '') firstsecond |
text.split('\n') Splitting a Windows file into lines. | alpha␍␊beta␍␊gamma 61 6C 70 68 61 0D 0A 62 65 74 61 0D 0A 67 61 6D 6D 61 | [alpha␍][beta␍][gamma] 5B 61 6C 70 68 61 0D 5D 5B 62 65 74 61 0D 5D 5B 67 61 6D 6D 61 5D | orphan CR. Shown here with brackets around each piece. Every line except the last carries a trailing CR inside the brackets, so "alpha" never equals "alpha", lookups miss, and trimmed comparisons pass while untrimmed ones fail. | text.split(/\r\n|[\r\n]/) [alpha][beta][gamma] |
text.replace(/\s+/g, ' ') The "just normalise all whitespace" one-liner. | one⟦PS⟧two 6F 6E 65 E2 80 A9 74 77 6F | one·two 6F 6E 65 20 74 77 6F | structure destroyed. JavaScript's \s includes U+2029 PARAGRAPH SEPARATOR, so a paragraph boundary is quietly demoted to a space. This pattern is too greedy, not too narrow: it flattens structure you meant to keep, and it does the same to tabs inside TSV data. | text.replace(/[^\S\r\n\u0085\u2028\u2029]{2,}/g, ' ') one⟦PS⟧two |
text.replace(/\r\n|[\r\n]/g, ' ') A correct CRLF-aware pattern, meeting text pasted from a layout program. | one⟦LS⟧two 6F 6E 65 E2 80 A8 74 77 6F | one⟦LS⟧two 6F 6E 65 E2 80 A8 74 77 6F | no change at all. Nothing matched, and nothing changed. The break is U+2028 LINE SEPARATOR, which browsers render as a line break and this pattern does not mention. The user sees a line break, presses the button again, and concludes the tool is broken. | text.replace(/\r\n|[\r\n\u0085\u2028\u2029]/g, ' ') one·two |
text.replace(/\s+/g, ' ') The greedy whitespace pattern, meeting mainframe-exported text. | one⟦NEL⟧two 6F 6E 65 C2 85 74 77 6F | one⟦NEL⟧two 6F 6E 65 C2 85 74 77 6F | no change at all. The opposite failure to the row above: \s does not include U+0085 NEXT LINE. A pattern broad enough to eat paragraph separators is still too narrow for EBCDIC exports, which is a good reason to name the characters you mean instead of reaching for \s. | text.replace(/[\u0085\u2028\u2029]|\r\n|[\r\n]/g, ' ') one·two |
text.replace(/ +/g, ' ') Collapsing runs of spaces after a PDF paste. | total:⟦NBSP⟧⟦NBSP⟧⟦NBSP⟧42 74 6F 74 61 6C 3A C2 A0 C2 A0 C2 A0 34 32 | total:⟦NBSP⟧⟦NBSP⟧⟦NBSP⟧42 74 6F 74 61 6C 3A C2 A0 C2 A0 C2 A0 34 32 | no change at all. Three no-break spaces are not spaces. The literal " " in the pattern is U+0020 and matches none of them, so the gap stays exactly as wide as it was and the text still will not wrap where you want it to. | text.replace(/[\u00A0\u202F]/g, ' ').replace(/ {2,}/g, ' ') total:·42 |
text.replace(/\n\n/g, '\n') Removing blank lines from a Windows file. | alpha␍␊␍␊beta 61 6C 70 68 61 0D 0A 0D 0A 62 65 74 61 | alpha␍␊␍␊beta 61 6C 70 68 61 0D 0A 0D 0A 62 65 74 61 | no change at all. The two LFs are not adjacent — a CR sits between them — so the pattern never matches and the blank line stays. This one is easy to miss because it fails silently on exactly the files it was written for. | text.replace(/(\r\n|[\r\n]){2,}/g, '$1') alpha␍␊beta |
text.replace(/\R/g, ' ') A pattern copied from a PCRE or Perl answer, where \R means "any line break". | one␊two 6F 6E 65 0A 74 77 6F | one␊two 6F 6E 65 0A 74 77 6F | no change at all. JavaScript has no \R. In a non-unicode regex the escape simply degrades to the letter R, so the pattern matches a capital R and nothing else — no error, no match, no line breaks removed. Add the u flag and the same pattern throws a SyntaxError instead. | text.replace(/\r\n|[\r\n\u0085\u2028\u2029]/g, ' ') one·two |
The last row deserves a note, because its pattern does not mean in JavaScript what it means
where it was copied from.
\R means "any line break" in PCRE and Perl, and it is copied into JavaScript
constantly. JavaScript has no such escape. In an ordinary regex it degrades silently to the
letter R — tested here, and /\R/ does match the string
"R": true. Add the u flag and the
same pattern is a SyntaxError. So the pattern either does nothing or throws, and
neither outcome looks like "your line breaks were not the ones you thought".
The carriage returns left behind when you strip the newlines
Take three lines saved by a Windows editor: first, second,
third. On disk that is 66 69 72 73 74 0D 0A 73 65 63 6F 6E 64 0D 0A 74
68 69 72 64 — note the 0D 0A pairs. Now run the pattern from the first
row of the table above, text.replace(/\n/g, ' '). Each 0A becomes 20 and each
0D stays: … 74 0D 20 73 …. One byte swapped for another, so the
file is exactly the size it was — and it still contains a carriage return per line.
In a browser that renders as first second third and looks finished. Paste it into a
terminal and the cursor jumps to column zero mid-sentence. Store it in a database and
WHERE name = 'first' misses every row. Diff it against a clean copy and every
line shows as changed with no visible difference. Four symptoms, one cause, and nothing on
screen that connects them — which is why the defence has to be reading the bytes, and what
the reveal toggle is for.
How to use the reveal view
Both panels switch at once, and a line-ending badge still breaks the line, so the shape of
the text survives while the cause of every break is named. Runs of ordinary spaces at the end
of a line get a faint dot each — the thing that makes a diff noisy and a trim()
comparison pass while an untrimmed one fails.
The counters under the input do the same job in summary form: a badge appears for each character class actually present, with its count. A document whose badge strip shows CRLF alongside NBSP has already told you two things about where it came from before you have read a word of it — it was saved on Windows, and something in its history justified the text. The sample loaded above reports its own counts the same way, and they are the real ones for that text rather than an illustration.
What this tool will not do
- It cannot know which hyphen you meant. The rejoin option is a heuristic with a published score; no dictionary is consulted. Details and the measurement on the PDF page.
- It does not reflow to a column width. Removing breaks and inserting new ones at 72 characters is a different job with different rules for hyphens and quotes.
- It does not repair encoding damage. If your text arrived as
’where an apostrophe belongs, that happened before the line breaks did, and a break remover cannot undo it. - It does not preserve formatting. Everything here is plain text. Bold, italics and headings are gone by the time the text reaches the box.
Every job, and the page that covers it
- Remove line breaks from PDF text — the hard-wrap problem in its worst form, with a measured account of what the hyphen-rejoin option gets right and wrong.
- Remove line breaks in Excel and CSV — why a CSV has two kinds of line break that look identical, and how to strip one without destroying the other.
- Remove line breaks in Word documents — soft breaks, paragraph separators and the no-break spaces a word processor leaves in your clipboard.
- Remove blank lines — eleven candidate lines and three definitions of "blank" that disagree about nine of them.
- Join lines with a separator — turning a column of lines into a comma list, a SQL IN clause or a JSON array.
- CRLF vs LF — where the two-character line ending came from, and what git actually does to your files under each core.autocrlf setting.
- Convert line endings — the byte-level conversion table for LF, CRLF and CR, with the round-trip check.
- Remove extra spaces — all twenty invisible horizontal characters, and the one that an ordinary space-collapsing regex can see.