The knowledge behind the code: what each application puts on the clipboard, and what pastewash does with it.
- Parse (
tree.ts). The browser entry usesDOMParser, the Node entry parse5; both implement the HTML standard's parsing algorithm, and both are reduced to one small tree.- Comments go. So do
<head>,<style>,<script>and<template>, but the text of every<style>is kept, and so are<meta>names and the<html>element's attributes. - Word's conditional comments need no special handling.
<!--[if !supportLists]-->is a comment, and so is the downlevel form<![if !vml]>, so the markup between them is ordinary markup.
- Comments go. So do
- Recognise the source (
source.ts). Each application leaves a mark:- Google Docs:
id="docs-internal-guid-…"; - Google Sheets:
<google-sheets-html-origin>; - Word and Excel: Office namespaces on
<html>,ProgIdmeta andMsoclasses; - Word for the web:
OutlineElement/TextRunclasses andparaid; - LibreOffice and Apple: a
generatormeta.
- Google Docs:
- Read the CSS (
css.ts). Inlinestyleattributes, and<style>rules whose selectors are a tag, classes, or both, which is all these applications use. Specificity orders the rules; the style attribute comes last.- Matching ignores case. Word's HTML has no doctype, so browsers render it in quirks mode, where class names match without regard to case.
- Word's
@list l0:level1 { … }rules are kept separately: they say whether a list level is numbered.
- Walk (
walk.ts). Once through the tree:- Formatting is carried down as marks: bold, italic, underline, strike, sub, sup, code, link.
- Blocks become paragraphs, headings, list items, tables, quotes, code blocks and rules.
- Inline content outside any block gets a paragraph of its own; whitespace between blocks doesn't.
- Nest lists (
lists.ts). Every source's list items arrive as flat items with a level, and one algorithm nests them (below). - Finish (
finish.ts):- Collapse whitespace as a browser shows it.
- Give a space between two runs only the marks they share.
- Merge runs with the same marks.
- Drop what's left empty.
- Write (
serialize.ts). From a closed vocabulary only.- Marks are nested in one fixed order: link, strong, em, u, s, code, sub/sup. They're reopened only where they change, so the same text always comes out the same way.
- A table cell holding one paragraph loses the
<p>, and so does a list item's first paragraph when only nested lists follow it.
- Tags:
b strong→ bold;i em→ italic;u→ underline;s strike→ strike, anddeltoo outside Word;sup/sub;code kbd samp tt→ code;a[href]→ link, when its address is allowed.
- CSS:
font-weight600 and above, orbold, is bold. Below 600, ornormal, undoes bold, which is how Google Docs'<b style="font-weight:normal">wrapper stops being bold.font-stylelikewise sets or undoes italic.text-decorationadds underline and line-through.noneon an element only undoes its own<u>or<s>, as in CSS, where a parent's decoration still shows through.vertical-align: super/sub.- The
fontshorthand is read for weight, style and family; Word and Apple writefont: 7.0pt "Times New Roman".
- Monospace becomes code: a first font family of Courier, Consolas, Menlo, Monaco and the like, or anything with "Mono" in the name. Any other family undoes it, since
font-familyis inherited.<font face>counts, as LibreOffice uses it. - Headings aren't marked bold. Word and Google Docs style them through their tag, and
<strong>inside every heading would be noise.
Each list item becomes an Item with a level (1 = outermost), numbered or not, a key saying which list it belongs to, and optionally a start number and a checked state. Runs of consecutive items are nested by level:
- a deeper item opens a list inside the previous item;
- a shallower one closes lists;
- at the same level, a change from bullets to numbers, or a different key at the outermost level, starts a new list;
- levels that skip (1 then 3) nest one deeper, not two.
Where the level and key come from:
| Source | Level | Key | Numbered |
|---|---|---|---|
Plain nested <ul>/<ol> |
nesting depth | each list element | its tag |
| Google Docs | aria-level |
the outermost list | its tag, or list-style-type |
| Word for the web | data-aria-level |
data-listid |
its tag |
| Word | mso-list: lN levelM |
lN |
the @list lN:levelM rule's mso-level-number-format, or the marker |
| Word, filtered HTML | margin-left ÷ 36pt |
one for all | the marker |
Two places are subtle:
- A list directly inside another (
<ul><li>a</li><ul><li>b</li></ul></ul>, which is how Google Docs nests) puts its items among the outer list's items, a level deeper. A list inside an item's own content (<li>Intro<ul>…</ul>Outro</li>) stays there, in order, as a list of its own. Treating the second like the first moved "Outro" before the nested list. Random testing found it: the invariant that the output's text appears in the input in order caught it. - Word's markers. Unfiltered HTML puts the bullet or number in a span styled
mso-list: Ignore, which is skipped. "Filtered" HTML (Save as Web Page, Filtered) has nomso-liststyles at all, so a list item is a paragraph whose text starts with a short marker followed by Word's spacer, a span infont: 7.0pt "Times New Roman". The spacer appears in both kinds, which is what makes it reliable. Bullets are·in Symbol,oin Courier New, or§in Wingdings. Numbers like3.also give the list's start. Decimal lists have nomso-level-number-formatat all, since it's the default, so the marker decides.
Task lists: role="checkbox" with aria-checked (Google Docs, whose checkbox images, marked aria-roledescription="checkbox", are dropped), data-checked (pastewash's own output, so washing is idempotent), or a checkbox <input> (GitHub's rendering).
- Left out:
<v:…>VML, whose<img>fallback is real markup;- comments:
mso-element: comment-list,MsoCommentReference,msocomanchor; - footnote and endnote separators;
- hidden text (
mso-hide: all).
- Footnotes keep their text, including the
[1]references. Their#_ftn1links become plain text, like any fragment link. - Tracked changes are
<ins>/<del>withcite/datetime, ormsoIns/msoDelclasses. They're accepted by default, or rejected withtrackChanges: 'reject'. Outside Word, a<del>without those attributes is ordinary struck-through text. - Images:
file:///…/msohtmlclip1/…/clip_image002.pngexists only on the copier's machine. It's dropped and reported, so an editor can use the image data that was also on the clipboard. - Style classes:
MsoTitlebecomes<h1>. Headings are<h1>…<h6>already.
- The paste is wrapped in
<b style="font-weight:normal" id="docs-internal-guid-…">; the explicit weight undoes the bold. - Formatting is on spans:
font-weight:700,font-style:italic,text-decoration:underline,vertical-align:super. - Each list item's text is in a
<p role="presentation">inside the<li>. - A lone
<br>between blocks is an empty line. It's dropped, or kept as an empty paragraph withkeepEmptyParagraphs. - Links may arrive wrapped in
https://www.google.com/url?q=…, which is unwrapped. So is Outlook's*.safelinks.protection.outlook.com/?url=….
- Paragraphs are
<p class="Paragraph" paraid=…>. - Headings are
role="heading" aria-level=N. - Each paragraph ends with an
EOPspan holding a non-breaking space, which is trimmed like any trailing space. - Each list item tends to arrive in its own
<ul>/<ol>(with its ownstart) inside aListContainerWrapper.data-listidties them together, and only the first item'sstartcounts.
- Excel puts bold and italic in classes (
.xl65 { font-weight:700 }) and hides rows withdisplay:none. - Google Sheets puts styles on each cell, and in-cell links in
<a class="in-cell-link">. <colgroup>and<col>are dropped.
The output is written from scratch by the serializer, not filtered from the input:
- text and attribute values are escaped;
- only the listed elements and attributes are ever written;
- link schemes are checked against the allowed list after redirects are unwrapped;
- image sources must be
http(s):ordata:image/.
<script>, <style>, <svg>, <math>, <iframe>, <object>, <embed>, forms and media are dropped with their content. The security/xss fixture and the vocabulary invariant check this.
- Colours, fonts, sizes, alignment and indentation: an editor's own styles should decide them.
- Non-breaking spaces: they become ordinary spaces, since Word uses them for spacing and that's almost all of them.
- Empty list items, quotes and paragraphs, unless empty paragraphs are asked for.