Skip to main content
Version: Next

@editora/paste-cleaner

Clean what people paste into your editor. Content from Word, Google Docs, Google Sheets, Excel, Pages, TextEdit, web pages and VS Code comes out as plain semantic HTML with real lists, headings and tables, and Markdown or plain text gets structure too. Works with any editor; no dependencies; about 11 kB gzipped.

Install​

npm i @editora/paste-cleaner
import { cleanPaste } from '@editora/paste-cleaner';

const { html, source, warnings } = cleanPaste({ html: clipboardHtml, text: clipboardText });
// source: 'word' | 'word-online' | 'google-docs' | 'google-sheets' | 'excel' | 'apple' | 'web' | 'vscode' | 'markdown' | 'plain' ...

What it fixes​

FromProblemResult
WordLists are flat paragraphs with mso-list styles and fake · bulletsNested <ul>/<ol>, with bullet vs number from Word's own list definitions
Word<o:p>, Mso* classes, VML shapes, blank-line paragraphs, tracked changesRemoved; tracked changes accepted
WordImages point at file:///C:/.../clip_image001.pngDropped and reported in warnings / images
Word for the webEvery list item is its own listOne nested list
Google DocsThe whole paste is wrapped in <b style="font-weight:normal">; bold and italic are inline stylesWrapper removed; <strong>, <em>, <u>, <s>, <sup>, <sub>
Google DocsNested lists sit directly inside the parent list; links go through google.com/url?q=Proper nesting; the real link
Sheets, ExcelTables full of widths, number formats and stylesheet classesClean <table>; a bold first row becomes <thead>; right-aligned numbers stay right-aligned
Pages, TextEditHeadings are just bigger text set in a <style> blockReal headings
Web pagesEvery element carries computed inline stylesClean tags
VS CodeOne <div> per line with coloured spansOne <pre><code> block
Markdown## Heading, - lists, tables, code fences shown as literal textHeadings, nested lists, tables, code blocks, links

Everything is sanitized against an allow-list: no scripts, event handlers, javascript:/data: links, frames, forms, SVG or style sheets get through. Only href, src, alt, width/height, colspan/rowspan, start and (when kept) text-align and colours survive.

Any contenteditable​

import { attachPasteCleaner } from '@editora/paste-cleaner';

const cleaner = attachPasteCleaner(document.querySelector('#editor'), {
onPaste: ({ source, warnings }) => warnings.forEach(showToast),
uploadImage: async (blob) => (await upload(blob)).url, // optional: pasted screenshots and data: images
});

Pasting blocks into the middle of a paragraph splits it, and the first and last pasted paragraphs join the text around the caret. The browser's own undo keeps working. Shift+Ctrl/⌘+V pastes plain text.

Editora​

import { PasteCleanerPlugin } from '@editora/paste-cleaner/editora';

editor.setConfig({ plugins: ['heading', 'bold', 'list', 'table', 'history', PasteCleanerPlugin()] });

Use it instead of smartPaste. Each paste is one undo step.

Tiptap and ProseMirror​

Run pasted HTML through cleanPaste before the editor parses it:

new Editor({ editorProps: { transformPastedHTML: (html) => cleanPaste(html).html } }); // Tiptap
new EditorView(node, { state, transformPastedHTML: (html) => cleanPaste(html).html }); // ProseMirror

Other editors: call cleanPaste wherever they let you intercept pasted HTML.

Options​

OptionDefault
keepColorsfalseKeep text and highlight colours
keepAlignmenttrueKeep centred / right / justified text
keepTablestruefalse turns rows into paragraphs
keepImagestrue
allowDataImagestrueInline data:image/png... images (SVG is never allowed)
keepLinkstrue
inferHeadingstrueShort, large paragraphs become h1–h3 when the source styled headings by size
detectHeaderRowstrueA fully bold first table row becomes a header row
markdowntrueConvert plain text that is clearly Markdown
linkifytrueLink bare URLs in plain text
plainTextfalseParagraphs and line breaks only
parseDOMParserOn a server, HTML needs one from jsdom or linkedom: parse: (html) => new JSDOM(html).window.document. Plain text and Markdown need none

The result also has text (a plain-text version), inline (true for a phrase pasted mid-sentence), and images (every image with kept or the reason it was dropped).

Tested on​

The test suite cleans one clipboard sample per source. The Pages/TextEdit sample is a real capture (macOS textutil); the Word, Word for the web, Google Docs, Sheets, Excel, Chrome and VS Code samples are written to match those apps' documented clipboard markup. If a paste from your app comes out wrong, please open an issue with its text/html (any clipboard inspector shows it).

Demo​

From a clone of the Editora repository:

npm run build -w @editora/core && npm run build -w @editora/paste-cleaner
node packages/paste-cleaner/demo/server.mjs # http://localhost:5193