White Space Remover / Cleaner
import React, { useEffect, useMemo, useState } from "react";
/*
Whitespace Cleaner — Single-file React + TypeScript component
- Tailwind CSS classes used for styling (no imports required)
- Features:
* Multiple cleaning modes: Trim, Collapse spaces, Remove all whitespace,
Normalize newlines, Remove zero-width and control characters
* Live preview and stats (characters, words, lines)
* Copy to clipboard, Download .txt, Undo (history), Clear
* Keyboard shortcuts: Ctrl+Enter = Clean, Ctrl+Z = Undo, Ctrl+S = Download
* Autosave input to localStorage
* Accessibility: labels, aria attributes
How to use:
- Drop into a React + Tailwind project (TypeScript). This is a single component file.
- It exports a default React component
Ideas for extending:
- Add server-side API for batch file processing
- Provide CLI version (Node/Go/Rust) for heavy files
- Add file upload and drag-drop
*/
type Mode =
| "trim"
| "collapse"
| "remove_all"
| "normalize_newlines"
| "remove_zero_width"
| "custom";
const STORAGE_KEY = "whitespace_cleaner_input_v1";
const HISTORY_KEY = "whitespace_cleaner_history_v1";
function normalizeUnicode(s: string) {
// Use NFC for common normalization (covers composed chars)
try {
return s.normalize("NFC");
} catch (e) {
return s;
}
}
function removeZeroWidthAndControls(s: string) {
// Remove zero-width joiner (\u200D), zero-width space, BOM, and other invisible controls
return s.replace(/\u200B|\u200C|\u200D|\uFEFF|\p{C}/gu, "");
}
function collapseSpaces(s: string) {
// Replace sequences of spaces and tabs with a single space
return s.replace(/[ \t\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g, " ");
}
function removeAllWhitespace(s: string) {
// Remove all spaces, tabs, newlines and other whitespace characters
return s.replace(/\s+/g, "");
}
function trimLines(s: string) {
// Trim leading and trailing whitespace on each line
return s
.split(/\r?\n/)
.map((l) => l.trim())
.join("\n");
}
function normalizeNewlines(s: string, newline: "\n" | "\r\n") {
// Convert CRLF, CR to desired newline and collapse repeated blank lines
const converted = s.replace(/\r\n|\r/g, "\n");
// collapse 3+ newlines to two
const collapsed = converted.replace(/\n{3,}/g, "\n\n");
return newline === "\r\n" ? collapsed.replace(/\n/g, "\r\n") : collapsed;
}
function statsOf(s: string) {
const characters = s.length;
const words = s.trim().length === 0 ? 0 : s.trim().split(/\s+/).length;
const lines = s.split(/\r?\n/).length;
return { characters, words, lines };
}
export default function WhitespaceCleaner(): JSX.Element {
const [input, setInput] = useState("");
const [mode, setMode] = useState("collapse");
const [normalizeNl, setNormalizeNl] = useState<"\n" | "\r\n">("\n");
const [removeZeroWidth, setRemoveZeroWidth] = useState(true);
const [history, setHistory] = useState([]);
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) setInput(saved);
const h = localStorage.getItem(HISTORY_KEY);
if (h) {
try {
setHistory(JSON.parse(h));
} catch {}
}
}, []);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, input);
}, [input]);
useEffect(() => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(-50)));
}, [history]);
useEffect(() => {
function handler(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "enter") {
e.preventDefault();
doClean();
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
e.preventDefault();
undo();
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
downloadOutput();
}
}
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [input, mode, normalizeNl, removeZeroWidth, history]);
const output = useMemo(() => {
let s = input;
s = normalizeUnicode(s);
if (removeZeroWidth) s = removeZeroWidthAndControls(s);
switch (mode) {
case "trim":
s = s.trim();
break;
case "collapse":
s = collapseSpaces(s);
s = trimLines(s);
break;
case "remove_all":
s = removeAllWhitespace(s);
break;
case "normalize_newlines":
s = normalizeNewlines(s, normalizeNl);
break;
case "remove_zero_width":
s = removeZeroWidthAndControls(s);
break;
case "custom":
// keep input as is — user can use custom regex in future
break;
}
return s;
}, [input, mode, normalizeNl, removeZeroWidth]);
const statsIn = statsOf(input);
const statsOut = statsOf(output);
function doClean() {
if (input === output) return;
setHistory((h) => [...h.slice(-49), input]);
setInput(output);
}
function undo() {
setHistory((h) => {
if (h.length === 0) return h;
const last = h[h.length - 1];
setInput(last);
return h.slice(0, -1);
});
}
function clearAll() {
setHistory((h) => [...h.slice(-49), input]);
setInput("");
}
function copyOutput() {
navigator.clipboard
.writeText(output)
.then(() => {
// small visual confirmation could be added
})
.catch(() => {});
}
function downloadOutput() {
const blob = new Blob([output], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "cleaned.txt";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function loadExample(kind: "small" | "large") {
const text =
kind === "small"
? " This is an example text.\nNew line.\n\n Many spaces.\n"
: Array.from({ length: 2000 }, (_, i) => `Line ${i + 1}: example text `).join("\n");
setInput(text);
}
return (
);
}
Whitespace Cleaner
Modern, fast whitespace removal and normalization tool.
