Skip to content
smrifat1411Public

About

Clean Microsoft Word clipboard HTML on paste and keep the equations editable. OMML and MathML to LaTeX. 3.6 kB, zero dependencies, works with Tiptap, ProseMirror, Lexical or plain contenteditable.

Topics

Resources

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

wordpaste

Clean Microsoft Word clipboard HTML — and keep the equations editable.

View on npm CI npm version minified and gzipped size zero dependencies MIT license

Tiptap ProseMirror Lexical JavaScript React Next.js Vue

Try the live playground →
Paste your own document into a real editor. Runs in your browser.


Paste from Word into a web editor and two things go wrong. You get a wall of invisible formatting, and every equation turns into a flat picture nobody can edit again.

wordpaste is one function. Clipboard HTML in, clean HTML out.

import { transformPastedHTML } from 'wordpaste';

You do not need to know where the paste came from. Word, LibreOffice, Outlook, Excel, Google Docs, Google Sheets — it handles all of them, and leaves ordinary HTML alone.

  • Equations survive as editable LaTeX instead of screenshots
  • Lists become real lists, nested, numbered by the browser again
  • 4.4 kB gzipped, zero dependencies, types included
  • One line to clean in any editor; equations render natively in Tiptap, elsewhere with a small math node of your own
  • JavaScript and TypeScript, no framework

Contents

Install

npm install wordpaste
# pnpm add wordpaste / yarn add wordpaste / bun add wordpaste

No build step? Import it straight from a CDN. Pin the version — an unpinned URL is served from a stale browser cache after a release.

<script type="module">
  import { transformPastedHTML } from 'https://esm.sh/wordpaste@0.13.0';
</script>

Use it

One line, wherever your editor lets you see a paste. Every example below is a single HTML file you can open and try.

Tiptap

▶ Run this example

import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import { transformPastedHTML } from 'wordpaste';

new Editor({
  element,
  extensions: [StarterKit],
  editorProps: { transformPastedHTML },
});

editorProps is a documented Tiptap option, typed as ProseMirror's EditorProps. There is no plugin or extension to write.

ProseMirror

▶ Run this example

The same prop, because Tiptap is built on ProseMirror.

import { EditorView } from 'prosemirror-view';
import { transformPastedHTML } from 'wordpaste';

new EditorView(element, { state, transformPastedHTML });

Lexical

▶ Run this example

Lexical has no equivalent prop, so claim the paste command:

import { PASTE_COMMAND, COMMAND_PRIORITY_HIGH, $getRoot, $insertNodes } from 'lexical';
import { $generateNodesFromDOM } from '@lexical/html';
import { transformPastedHTML } from 'wordpaste';

editor.registerCommand(
  PASTE_COMMAND,
  (event) => {
    const html = event.clipboardData?.getData('text/html');
    if (!html) return false;

    event.preventDefault();
    const dom = new DOMParser().parseFromString(transformPastedHTML(html), 'text/html');

    editor.update(() => {
      $getRoot().selectEnd();
      $insertNodes($generateNodesFromDOM(editor, dom));
    });
    return true;
  },
  COMMAND_PRIORITY_HIGH,
);

Quill

▶ Run this example

Quill 2 has no paste prop, so subclass its clipboard module. Quill's own formula format renders the equations; one matcher maps the span onto it.

import Quill from 'quill';
import { transformPastedHTML } from 'wordpaste';

const Clipboard = Quill.import('modules/clipboard');
const Delta = Quill.import('delta');

class WordClipboard extends Clipboard {
  onPaste(range, { html, text }) {
    super.onPaste(range, { html: html && transformPastedHTML(html), text });
  }
}
Quill.register('modules/clipboard', WordClipboard, true);

new Quill('#editor', {
  theme: 'snow',
  modules: {
    clipboard: {
      matchers: [['span[data-latex]', (node) => new Delta().insert({ formula: node.dataset.latex })]],
    },
  },
});

formula needs KaTeX on the page as window.katex. Block equations become inline; Quill has no display mode.

Vanilla JavaScript

▶ Run this example

No editor library. The native paste event carries the HTML:

import { transformPastedHTML } from 'wordpaste';

element.addEventListener('paste', (event) => {
  const html = event.clipboardData.getData('text/html');
  if (!html) return;

  event.preventDefault();
  document.execCommand('insertHTML', false, transformPastedHTML(html));
});

This inserts HTML directly, so sanitise it before you store it or show it to anyone else — see Security.

React, Next.js, Vue

▶ React · ▶ Vue

wordpaste has no UI and no state — it is a function, and the line that uses it is editorProps: { transformPastedHTML } in every framework. Only your editor's binding package changes.

// React — @tiptap/react
'use client';

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { transformPastedHTML } from 'wordpaste';

export function WordEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    editorProps: { transformPastedHTML },
  });

  return <EditorContent editor={editor} />;
}
<!-- Vue 3 — @tiptap/vue-3 -->
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';
import { transformPastedHTML } from 'wordpaste';

const editor = useEditor({
  extensions: [StarterKit],
  editorProps: { transformPastedHTML },
});
</script>

<template>
  <EditorContent :editor="editor" />
</template>

Next.js: importing wordpaste on the server is safe, but calling it there throws ReferenceError: DOMParser is not defined. So the editor component needs 'use client' — which it needs anyway, since paste is a browser event. You do not need next/dynamic or ssr: false.

To clean HTML on the server on purpose, supply a DOM first:

import { JSDOM } from 'jsdom';
globalThis.DOMParser = new JSDOM().window.DOMParser;

Other editors

Where each editor lets you see the clipboard HTML, and what it does with the equation span afterwards. Cleaning is one line everywhere. Rendering the maths is native in Tiptap; elsewhere it is a small node, blot or rule of your own.

Editor Where to call transformPastedHTML The equation span
TinyMCE paste_preprocess: (editor, args) => { args.content = transformPastedHTML(args.content); } kept, shown as its LaTeX text
Froala events: { 'paste.beforeCleanup': (html) => transformPastedHTML(html) } and wordPasteModal: false kept, shown as its LaTeX text
CKEditor 5 editor.editing.view.document.on('clipboardInput', (evt, data) => { if (typeof data.content === 'string') data.content = transformPastedHTML(data.content); }); and leave out PasteFromOffice, which would clean it again dropped unless General HTML Support allows span[data-type][data-latex]; a math plugin then renders it
Plate a plugin with inject: { plugins: { [HtmlPlugin.key]: { parser: { transformData: ({ data }) => transformPastedHTML(data) } } } }, the shape of Plate's own DocxPlugin, which you then leave out needs one deserializer rule per math plugin
BlockNote pasteHandler: ({ event, editor }) => { const html = event.clipboardData?.getData('text/html'); if (!html) return false; editor.pasteHTML(transformPastedHTML(html)); return true; } dropped; BlockNote has no math node
Milkdown ctx.update(editorViewOptionsCtx, (prev) => ({ ...prev, transformPastedHTML })), with @milkdown/plugin-clipboard 7.19.2 or later rename to Milkdown's data-type="math_inline" in a parse rule
Trix addEventListener('trix-before-paste', (e) => { if (e.paste.html) e.paste.html = transformPastedHTML(e.paste.html); }) dropped
Svelte, Angular you construct new Editor({ editorProps: { transformPastedHTML } }) yourself, so the Tiptap line is unchanged rendered by @tiptap/extension-mathematics
Markdown clean, then Turndown with a rule that turns [data-latex] into $…$ needs that rule

Showing the maths

An equation comes out like this:

<span data-type="inline-math" data-latex="\frac{a}{b}">\frac{a}{b}</span>

The LaTeX is in the attribute and in the text. So if you do nothing, you see the raw \frac{a}{b} on screen. That is deliberate — a visible clue beats a blank space you cannot debug.

To make it look like maths, pick one:

Tiptap — install @tiptap/extension-mathematics. The output above is already its markup, so it just works.

Anything else — render it with KaTeX or MathJax:

document.querySelectorAll('[data-latex]').forEach((el) => {
  katex.render(el.dataset.latex, el, {
    displayMode: el.dataset.type === 'block-math',
    throwOnError: false,
  });
});

What it handles

You copy from Formatting junk removed Equations kept editable
Microsoft Word Yes Yes — OMML
LibreOffice Writer Yes Yes — MathML
Google Docs Yes No
Outlook Yes n/a
Excel Yes n/a
Google Sheets Yes n/a
Anything else Colour only —

Real clipboard dumps in test/fixtures/ cover Microsoft Word 2007 for Windows, Word 15 for Mac, Excel 16 for Mac and Google Sheets in Chrome. Outlook, Google Docs, LibreOffice Writer and Calc, Word Online, Excel for Windows and Excel Online are not captured yet: untested, not unsupported.

Bold, italic, underline, super/subscript, links, lists, tables and text-align survive. Colour, highlight, font family and font size do not — that is the point, so the source document's design does not leak into your app.

Lists become real lists. Word does not paste a list as a list — every item is a <p> with the bullet or number sitting in the text as literal characters. Strip the styling naively and you keep "1." and "2." frozen in place, so reordering or inserting an item leaves the numbering wrong forever. wordpaste reads Word's markers before discarding them and rebuilds <ul>/<ol>, with nesting, the original sequence (1. a. i. I.) and a start when the list does not begin at 1.

Your editor has to want text-align. wordpaste emits style="text-align:center", but an editor drops any style its schema has no rule for. In Tiptap that means adding @tiptap/extension-text-align.

Google Docs needs its own handling. It wraps every paste in <b style="font-weight:normal"> — strip that style and the whole paste turns bold. It also stores bold and italic as inline styles rather than tags, so those become real <strong> and <em> before the fonts are dropped.

Its equations cannot be recovered by anyone: Docs puts them on the clipboard as images already. Word is the unusual one — it sends the picture and the real maths, which is the gap this package exploits.

Spreadsheets keep their cell formatting. Excel stores each cell's bold, italic, underline and alignment in a .xlNN class whose rule sits in a <style> block the editor never sees, so stripping the class loses the formatting. wordpaste reads the rules first and emits real tags. Google Sheets sends inline styles plus a <style> block, a <google-sheets-html-origin> wrapper, data-sheets-* bookkeeping and two <div>s around every link; all of that goes, while colspan, rowspan and the links stay.

Reading equations from a .docx

Everything above is about the clipboard. But the same OMML sits inside every .docx, in word/document.xml, and many tools that read a .docx — mammoth, docx-to-Markdown converters, document ingestion for RAG — drop the equations on the floor. ommlToLatex is the converter on its own, for that job.

import { ommlToLatex } from 'wordpaste';

ommlToLatex(
  '<m:oMath><m:f><m:num><m:r>a</m:r></m:num><m:den><m:r>b</m:r></m:den></m:f></m:oMath>',
);
// '\\frac{a}{b}'

String in, string out. It returns '' when the fragment cannot be parsed, so it never throws mid-document. It reads OMML only — LibreOffice's MathML is handled by transformPastedHTML, not here.

On Node there is no DOMParser. Supply one before the first call; that is the only setup:

import { JSDOM } from 'jsdom';
globalThis.DOMParser = new JSDOM().window.DOMParser;

It handles fractions (including \binom), sub-, super- and pre-scripts, radicals, delimiters, ∑ ∫ ∏, functions, limits, over/underlines, accents (\vec, \bar, \dot, \hat …), matrices and equation arrays. The full construct table, with a live converter, is on the OMML to LaTeX page.

Security

wordpaste is not a sanitiser. It removes formatting junk, not dangerous markup — <script>, <iframe>, inline onclick/onerror handlers and javascript: URLs pass straight through.

Inside Tiptap, ProseMirror or Lexical their schema drops all of that before rendering, so nothing more is needed. If you insert the output yourself with innerHTML or insertHTML, sanitise first:

import DOMPurify from 'dompurify';
import { transformPastedHTML } from 'wordpaste';

element.innerHTML = DOMPurify.sanitize(transformPastedHTML(html), {
  ADD_ATTR: ['data-latex', 'data-type'],
});

ADD_ATTR keeps the equation attributes, which DOMPurify strips by default. See SECURITY.md.

Limits

  • Images on the writer's disk are dropped. Word points at file:///C:/…, a dead link on the web. https: and data: images are kept. The real bytes arrive separately as clipboardData.files — uploading those is your app's job.
  • Browser first. It uses the native DOMParser; on Node, supply one from jsdom as shown under Reading equations from a .docx.
  • Not a .docx reader. transformPastedHTML handles what an editor puts on the clipboard. To get equations out of a file, unzip it yourself and hand the <m:oMath> fragments to ommlToLatex.

Advanced

One extra export for a case the main function does not cover. You will probably never need it.

hasWordMath(html): boolean

True when the paste carries recoverable equations. Use it to decide whether the OMML conversion is worth running.

Do not use it to skip pasted images. Word puts a screenshot of each equation on the clipboard next to the real figures, and nothing in clipboardData.files says which is which — so bailing out of a paste to avoid the screenshot silently drops the author's diagrams as well. Upload every pasted file: an extra picture of an equation is one click to delete, a missing figure is invisible until someone sits the exam.

Why this exists

Clean Office paste is a paid feature nearly everywhere:

Free Works outside its own editor Equations
TinyMCE PowerPaste No — paid subscriptions only No No
CKEditor paste-from-office-enhanced No — premium No No
CKEditor paste-from-office GPL or commercial No No
Tiptap Conversion No — from $49/mo No —
tinymce-word-paste-filter Yes Yes No
wordpaste Yes, MIT Yes Yes

Contributing

Bug reports are welcome — please include the raw clipboard HTML that reproduces it. Pull requests too; run npm test first.

New features are considered but not promised. This package is deliberately small, and staying small is the point.

Licence

MIT

About

Clean Microsoft Word clipboard HTML on paste and keep the equations editable. OMML and MathML to LaTeX. 3.6 kB, zero dependencies, works with Tiptap, ProseMirror, Lexical or plain contenteditable.

Topics

Resources

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages