A paragraph pasted out of a client’s Word document looks correct in the editor and wrong on the published page: slightly smaller, slightly greyer, set in a font that appears nowhere else on the site. No theme setting fixes it, because the problem is not in the theme. It is sitting in the post content, where the paste left a span carrying an inline font size and a Word class name, and an inline style beats your stylesheet every time.
Text copied out of a PDF fails differently. A PDF has no paragraphs, only lines of positioned glyphs, so every visual line end arrives as a real line break and a single sentence lands as four separate lines. Words hyphenated at the column edge arrive split down the middle: para- on one line, graph on the next.
Then there is the half of the problem you cannot see at all. Non breaking spaces from a chat app. A zero width joiner from an AI answer. A soft hyphen a word processor inserted years ago. A byte order mark that should have been consumed by whatever read the file. Each one occupies a position in the string, changes how a line wraps or whether a phrase search matches, and the editor draws it as nothing.
None of this is a bug. Word, Google Docs, PDF viewers and chat apps all put a rich and perfectly correct version of their own content on the clipboard. It is just a different format from the one you wanted, and WordPress stores what it is handed.
The tool below strips all of it. Paste, choose what to remove, copy the result out. Everything runs on the string inside the page: nothing is uploaded and no request is made, so a paste from an unpublished draft or a client contract never leaves your machine.
Text cleaner
Paste anything, pick what should go, press Clean. Nothing is uploaded: the text never leaves this browser tab.
Where the junk comes from
Word and Google Docs put HTML on the clipboard, not text. Word’s flavour carries class names it invented for its own styling model plus inline styles on every run of characters, so one sentence can arrive wrapped like this.
<p class="MsoNormal">
<span style="font-size:11.0pt;font-family:'Calibri',sans-serif">
The sentence you actually wanted.
</span>
</p>
Google Docs is tidier but not clean. It wraps the selection in a b element with font-weight:normal and a generated id, which is why bold sometimes refuses to apply to text pasted from Docs. The block editor knows about that particular wrapper and removes it, which tells you something useful: the normalisation is a list of known cases, not a general guarantee.
Web pages give you anchors, and anchors bring their query strings. Copy a sentence containing a link from a newsletter or a search result and the href arrives with utm_source, utm_campaign and sometimes a click tracker in front of the real address.
Chat apps contribute the quiet damage. Slack, WhatsApp, iMessage and the macOS text system substitute curly quotes as you type and insert non breaking spaces to stop short phrases wrapping. AI output has its own signature: real em dash characters rather than hyphens, a single ellipsis character rather than three dots, and now and then a zero width joiner left over from an emoji sequence someone edited.
It matters most when the text is heading somewhere with no tolerance for surprises. A filename. An alt attribute. A spreadsheet column that feeds a batch of generated images, where one bad cell becomes two hundred bad files.
The characters you cannot see
A non breaking space, U+00A0, renders exactly like a space and forbids a line break at that point. It is two bytes in UTF-8, C2 A0, and it is written as an entity. Paste a paragraph full of them into a narrow column and you get a line that refuses to wrap and pushes past its container. It also breaks any search that expects an ordinary space.
A zero width space, U+200B, renders as nothing and permits a break. It is used legitimately to suggest break points inside long URLs. Pasted into prose it is invisible sabotage: a search for “media library” will not match a phrase with a zero width space wedged between the two words, and neither will a database query, a redirect rule or a duplicate check.
A soft hyphen, U+00AD, is a conditional hyphen. It stays invisible until the renderer decides to break the line at that spot, at which point a hyphen appears. That is why a paragraph copied from a justified layout can sprout hyphens inside words after you paste it into a different column width. The character was always there. Only the line width changed.
The rest are variations on nothing. A zero width joiner, U+200D, exists to glue emoji parts into a single glyph; alone in a sentence it is pure noise. A word joiner, U+2060, is a zero width character that forbids a break. A byte order mark, U+FEFF, is a file header meant to tell a reader which encoding follows; when the reader does not consume it, it lands at the front of your content and shows as nothing or a stray box.
Mojibake, and where it’s comes from
A sequence of bytes is not text until something decides which table to read it with. UTF-8 encodes the right single quotation mark, U+2019, as three bytes: E2 80 99. Read those same three bytes one at a time through Windows-1252, the single byte table people still loosely call Latin-1, and each byte becomes its own character: â, €, ™. That is the whole of the famous it’s. Not corruption, a misreading.
The same arithmetic explains every variant. The ellipsis character, E2 80 A6, becomes …. The left double quotation mark, E2 80 9C, becomes “. A non breaking space, C2 A0, becomes  followed by a space, which is why a stray  keeps appearing in front of prices, units and degree signs. Once you recognise the †prefix you can read the rest by eye.
In WordPress the wrong table usually gets chosen in one of four places: a database dump exported as one charset and imported as another, a DB_CHARSET value in wp-config.php that no longer matches what the tables hold (modern installs ship utf8mb4), a plugin reading a CSV or a feed without declaring an encoding, or a response served without a charset in its Content-Type header so the browser guesses.
It also stacks. Save mojibake back through a UTF-8 pipeline and the wrong characters are encoded again, so ’ becomes ’. The repair is the reverse of the damage: encode the text back to Windows-1252, decode it as UTF-8, repeat while the result stays valid UTF-8. That is why a repair pass sometimes has to run more than once on the same paragraph.
What WordPress adds on top
The first thing to understand is that WordPress rewrites your punctuation on the way out. wptexturize(), in wp-includes/formatting.php, is attached to the_title, the_content, the_excerpt, comment_text and several more in wp-includes/default-filters.php. It is an output filter. The database keeps your straight quotes; the page shows curly ones. The substitutions are worth knowing precisely, because two of them surprise people.
- Three hyphens become an em dash.
- Two hyphens with a space on each side become an em dash.
- Two hyphens with no spaces become an en dash, unless they follow
xn, so internationalised domain names survive. - A single hyphen with a space on each side becomes an en dash. This is the one that catches people out: a plain typed hyphen between two words is silently promoted.
- Three dots become one ellipsis character, and
(tm)becomes a trademark sign. - Straight quotes become curly, with a long list of special cases for apostrophes in words like
'emand'tisand years such as'99.
The pass skips anything inside pre, code, kbd, style, script and tt, and inside the [code] shortcode. Those defaults are filterable through no_texturize_tags, and the whole thing can be switched off with the run_wptexturize filter. The practical consequence: converting curly quotes to straight ones in your source text does not stop curly quotes appearing on the published page. Usually that is what you want. What you do not want is to fight it without knowing it is there, which is one of the quieter items on the list of typography mistakes that give an amateur design away.
The second thing is that the block editor stores markup rather than re-deriving it. Its paste handler normalises a good deal: Word’s fake lists become real ones, stray divs are flattened, the Google Docs wrapper goes. What survives is written into post_content as literal HTML and is what your theme lives with from then on. Paste into a Classic block instead and nothing is normalised at all.
The third thing is that kses will not save you. WordPress runs wp_kses on post content only for users lacking the unfiltered_html capability, which on a single site install means everyone except administrators. Even for those users, class and style both sit in _wp_add_global_attributes() in wp-includes/kses.php, so <span style="font-size:11.0pt"> is on the allowed list by design. Word’s inline styles pass the filter cleanly.
The fourth thing is the important one. None of the invisible characters are markup. They are text. No paste handler, no kses pass and no texturize rule touches a zero width space, because to all of them it is an ordinary character in an ordinary string. The same goes for the fields around your posts: a trailing non breaking space pasted into an alt attribute is stored exactly as pasted, and since alt text lives in more than one place, you can end up cleaning it twice.
What to strip and what to keep
Removing every line break is almost always wrong. The option exists for real cases: a single form field, a filename, an alt attribute, one spreadsheet cell. Applied to prose it destroys the only structure the text has left. The PDF problem is not that there are line breaks, it is that the line breaks are in the wrong places.
What you want instead is to join lines inside a paragraph and leave the blank line between paragraphs alone. That is why joining and collapsing are separate operations: a blank line is a paragraph boundary and carries meaning, a lone line break inside a run of text is a wrapping artefact and carries none. Join across blank lines and four paragraphs become one wall of text that later editing never fully recovers.
Repairing split words carries the same shape of risk. It correctly rejoins para- and graph. It cannot know that well- and known were a genuine compound that happened to fall at a line end. Run it on PDF text, where nearly every line end hyphen is an artefact, and leave it off for text that already wraps correctly, where nearly every one is real.
Dropping duplicate lines is excellent on a list merged from two sources and ruinous on prose with legitimately repeated short lines. Note the order: duplicates go before the line joining, so with both switched on the raw lines are deduplicated first and the paragraphs rebuilt afterwards.
The two link options are not interchangeable. Unlinking keeps the words and drops the address, which suits a quoted sentence. Removing bare URLs deletes the address itself, which suits a pasted forum reply where the URL sits on its own line doing nothing.
Quote direction depends entirely on where the text is going. Convert curly to straight for anything headed into code, a filename, a CSV or a design tool with no smart quote logic of its own. Convert straight to curly for text that will not pass through a typographic filter later, such as a caption you are about to bake into an image, where the punctuation is part of the artwork. Accent normalisation follows the same logic: right for a slug, wrong for the body of a German or French article, where stripping diacritics changes the words.
How to check what you actually have
The cheapest instrument is the count. Characters, words and lines, before and after. If the character count drops by forty and nothing visible went missing, you removed forty invisible characters. If the line count falls from 180 to 12, you turned a PDF column back into twelve paragraphs. If the word count moves when you only meant to fix quotes, something joined that should not have.
Inside WordPress, the block editor’s code editor view spells out and shows every pasted span in plain sight. It will not reveal zero width characters, because there is nothing to reveal. Those need a search rather than a look.
# count every invisible character in a file, by kind
grep -oP '[x{00a0}x{00ad}x{200b}-x{200d}x{2060}x{feff}]' text.txt | sort | uniq -c
The same idea works against the posts table. Both queries below are read only, and both assume the default wp_ prefix, so substitute your own.
# published posts containing a non breaking space
wp db query "SELECT ID, post_title FROM wp_posts
WHERE post_status = 'publish'
AND post_content LIKE CONCAT('%', UNHEX('C2A0'), '%')
LIMIT 20;"
# published posts carrying the classic mojibake signature
wp db query "SELECT ID, post_title FROM wp_posts
WHERE post_content LIKE '%â€%'
LIMIT 20;"
Run the second one first. If it returns forty rows you have an encoding problem, not a paste problem, and fixing posts one at a time will not touch the cause.
Where a text cleaner stops
It cleans text, so formatting does not survive. Bold, italics, links and headings come out as words. That is the right trade for killing a Word paste, but it fixes the order of work: clean on the way in, then format in the editor. Clean afterwards and you format twice.
It does not repair a database. If your published posts already contain mojibake, an encoding repair fixes the paragraph in front of you, not the two hundred rows behind it. That job belongs to a corrected export and import, or to fixing the declared charset, and it deserves a backup first.
It has no find and replace and no regular expressions: every operation is a named transformation with a defined scope, which is deliberate. And it has no opinion about your language. German low quotes and French guillemets are typographic conventions, not encoding rules, and no general purpose cleaner should guess at them. The case modes are mechanical too, so neither knows that wp-config.php stays lower case.
Pasted text is two problems stacked on top of each other, and most people only ever notice the first. The visible one is structure: markup that came along for the ride, line breaks in the wrong places, links carrying campaign parameters. The invisible one is characters, and it is the one that quietly costs you, because a zero width space in a heading never announces itself. It just means a search does not match, a duplicate check misses, a line wraps somewhere absurd on a phone.
The fix is not vigilance, it is a boundary. Decide that text entering your site passes through one cleaning step, always, whatever it came from. The question then stops being “is this paste dirty” and becomes “what does this particular text need”. A PDF needs line joining and split word repair. An AI draft needs its dashes and ellipses normalised. A chat message needs its non breaking spaces turned back into ordinary ones. A Word document needs its markup taken away entirely.
After that, let WordPress do its own job. wptexturize supplies the curly quotes on output whether or not you supplied them on input, so the version you store can stay plain and predictable while the version readers see is properly typeset. Clean text in the database, correct typography on the page, and nothing invisible in between.