feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<title>Word editor</title>
|
feat(docx-editor): Save as…, PDF export, and a mark of our own
Three gaps, one theme: the editor could produce a file but not decide where
it went, what format it was in, or look like anything in the dock.
**Save as…** opens a real file dialog, and the extension typed there picks
the format. Save then writes to that file instead of dropping another copy
in Downloads every time. The renderer never names a path: the dialog returns
an opaque token, and the add-on will only write to a path a dialog actually
returned. An extension page is the least trusted thing in the add-on, and
"write these bytes anywhere" is not a capability it needs.
**PDF** goes through Chromium's own print pipeline in a hidden window — the
same engine as Ctrl+P — on the paper size read out of the document's own
sectPr. For that to match what the user was looking at, the page's
typography had to stop living in editor.css, which the export window can't
reach: it moves to lib/doc-css.js and both surfaces read the one string. The
result embeds subsetted fonts, keeps images, and turns hyperlinks into real
PDF link annotations.
**The icon** is ours. Microsoft's Word mark is a trademark and borrowing it
to look official is not something a browser that talks about sovereignty
should do. icon.svg says "text document" in its own words — a turned corner,
a heading rule, body lines, a pilcrow badge in Silent Mode green — and
`npm run icons` derives the PNGs and addon.json's copy from it, so there is
one drawing rather than several that drift.
Also: the scratch folder follows the profile rename to extensions-data/ via
the api.dataDir the host now provides, instead of creating a stale
addons-data/ beside it.
2026-09-21 03:35:55 +02:00
|
|
|
|
<link rel="icon" href="icon.svg">
|
feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<link rel="stylesheet" href="editor.css">
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="topbar">
|
|
|
|
|
|
<button class="btn wide" id="file-new" title="Start a blank document">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M4 1.5h5l3 3V14.5H4z"/><path d="M9 1.5v3h3"/></svg>
|
|
|
|
|
|
<span>New</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn wide" id="file-open" title="Open a .docx file (Ctrl+O)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M1.5 4h4l1.5 2h7.5v7.5h-13z"/><path d="M1.5 4V2.5h5"/></svg>
|
|
|
|
|
|
<span>Open</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn wide primary" id="file-save" title="Save as .docx (Ctrl+S)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M8 2v8M4.5 6.5L8 10l3.5-3.5M2.5 13.5h11"/></svg>
|
|
|
|
|
|
<span>Save</span>
|
|
|
|
|
|
</button>
|
feat(docx-editor): Save as…, PDF export, and a mark of our own
Three gaps, one theme: the editor could produce a file but not decide where
it went, what format it was in, or look like anything in the dock.
**Save as…** opens a real file dialog, and the extension typed there picks
the format. Save then writes to that file instead of dropping another copy
in Downloads every time. The renderer never names a path: the dialog returns
an opaque token, and the add-on will only write to a path a dialog actually
returned. An extension page is the least trusted thing in the add-on, and
"write these bytes anywhere" is not a capability it needs.
**PDF** goes through Chromium's own print pipeline in a hidden window — the
same engine as Ctrl+P — on the paper size read out of the document's own
sectPr. For that to match what the user was looking at, the page's
typography had to stop living in editor.css, which the export window can't
reach: it moves to lib/doc-css.js and both surfaces read the one string. The
result embeds subsetted fonts, keeps images, and turns hyperlinks into real
PDF link annotations.
**The icon** is ours. Microsoft's Word mark is a trademark and borrowing it
to look official is not something a browser that talks about sovereignty
should do. icon.svg says "text document" in its own words — a turned corner,
a heading rule, body lines, a pilcrow badge in Silent Mode green — and
`npm run icons` derives the PNGs and addon.json's copy from it, so there is
one drawing rather than several that drift.
Also: the scratch folder follows the profile rename to extensions-data/ via
the api.dataDir the host now provides, instead of creating a stale
addons-data/ beside it.
2026-09-21 03:35:55 +02:00
|
|
|
|
<button class="btn wide" id="file-saveas" title="Save as… — choose a name, a folder, and .docx or .pdf (Ctrl+Shift+S)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M2.5 3.5h7l4 4v6.5a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-9.5a1 1 0 0 1 1-1z"/><path d="M5 3.5v3.5h5"/><path d="M5 14.5v-4h6v4"/></svg>
|
|
|
|
|
|
<span>Save as…</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn wide" id="file-pdf" title="Save a PDF to your downloads">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M4 1.5h5l3 3v10H4z"/><path d="M9 1.5v3h3"/></svg>
|
|
|
|
|
|
<span>PDF</span>
|
|
|
|
|
|
</button>
|
feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<button class="btn" id="file-print" title="Print (Ctrl+P)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M4.5 6V2.5h7V6"/><rect x="2.5" y="6" width="11" height="5"/><path d="M4.5 11v2.5h7V11"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<span class="docname" id="docname">Untitled document</span>
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="btn" id="about" title="About this editor, and what it can't do yet">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="6.2"/><path d="M8 7.2v4M8 4.9v.9" stroke-linecap="round"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn danger" id="discard" title="Close this tab">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
feat(docx-editor): documents are tabs, each with its own close button and menu
Opening a second .docx used to mean a second browser tab: a whole ribbon,
banner and footer repeated, with one ✕ at the far end of a row that also
held the file's name. The name looked like a tab and nothing about it
behaved like one.
Now the editor holds documents the way the browser holds pages. A strip
under the toolbar carries one tab per open document — icon, name, unsaved
dot, its own ✕ — plus a + to open another. Middle-click closes, Ctrl+W
closes, Ctrl+Tab cycles, and right-click (or the caret on the tab under the
pointer) drops a menu: Duplicate, Open in the default app, Show in folder,
Close others, Close. The gestures are the browser's because that is the tab
strip every user of this editor already knows.
Under it, one ProseMirror view is handed a different state per document
rather than one view per tab, and the module-level "current document"
variables are marshalled in and out on a switch. That keeps the change out
of every function that touches the current document, at the price of one
list — DOC_FIELDS in captureActive/adoptDoc — that has to stay complete. A
variable missed there leaks one document's state into another, which would
look like the editor corrupting a file, so it is called out in a comment.
Closing the last document closes the editor tab, the way closing a
browser's last tab closes the window; an empty ribbon staring at the user
is not a state worth having.
The add-on hands a newly opened document to the editor that is already up
and fronts it, falling back to opening a tab if no editor acknowledges
within 900ms — so a crashed or closed editor degrades to exactly the old
behaviour rather than swallowing the document.
2026-09-22 08:35:06 +02:00
|
|
|
|
<!-- Open documents. Modelled on the browser's own tab strip, because that
|
|
|
|
|
|
is the thing every user of this editor already knows how to operate:
|
|
|
|
|
|
click to switch, ✕ to close, right-click (or the caret on the active
|
|
|
|
|
|
one) for the rest. -->
|
|
|
|
|
|
<div class="doctabs" id="doctabs" role="tablist" aria-label="Open documents"></div>
|
|
|
|
|
|
|
feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<!-- Ribbon. Grouped button sets with a caption under each group — the
|
|
|
|
|
|
Office 2007 shape without the tab strip, which would hide half the
|
|
|
|
|
|
controls behind a click for no gain at this feature count. -->
|
|
|
|
|
|
<div class="ribbon" id="ribbon">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn" id="undo" title="Undo (Ctrl+Z)" disabled>
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 8c0-3 2-5 5-5s5 2 5 5-2 5-5 5"/><path d="M6 5L3 8l3 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="redo" title="Redo (Ctrl+Y)" disabled>
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M13 8c0-3-2-5-5-5S3 5 3 8s2 5 5 5"/><path d="M10 5l3 3-3 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">undo</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<select class="rsel" id="style-select" title="Paragraph style">
|
|
|
|
|
|
<option value="paragraph">Normal</option>
|
|
|
|
|
|
<option value="h1">Heading 1</option>
|
|
|
|
|
|
<option value="h2">Heading 2</option>
|
|
|
|
|
|
<option value="h3">Heading 3</option>
|
|
|
|
|
|
<option value="h4">Heading 4</option>
|
|
|
|
|
|
<option value="h5">Heading 5</option>
|
|
|
|
|
|
<option value="h6">Heading 6</option>
|
|
|
|
|
|
<option value="blockquote">Quote</option>
|
|
|
|
|
|
<option value="code_block">Code</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">style</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<select class="rsel" id="font-family" title="Font">
|
|
|
|
|
|
<option value="">(document font)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input class="rnum" id="font-size" type="number" min="4" max="400" step="0.5" title="Size in points" placeholder="11">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn" id="m-strong" title="Bold (Ctrl+B)"><b style="font:600 13px/1 Georgia,serif">B</b></button>
|
|
|
|
|
|
<button class="btn" id="m-em" title="Italic (Ctrl+I)"><i style="font:italic 13px/1 Georgia,serif">I</i></button>
|
|
|
|
|
|
<button class="btn" id="m-underline" title="Underline (Ctrl+U)"><span style="font:13px/1 Georgia,serif;text-decoration:underline">U</span></button>
|
|
|
|
|
|
<button class="btn" id="m-strike" title="Strikethrough"><span style="font:13px/1 Georgia,serif;text-decoration:line-through">S</span></button>
|
|
|
|
|
|
<button class="btn" id="m-sup" title="Superscript"><span style="font:12px/1 Georgia,serif">x<sup>2</sup></span></button>
|
|
|
|
|
|
<button class="btn" id="m-sub" title="Subscript"><span style="font:12px/1 Georgia,serif">x<sub>2</sub></span></button>
|
|
|
|
|
|
<button class="btn swatch-btn" id="m-color" title="Text colour">
|
|
|
|
|
|
<span style="font:600 12px/1 Georgia,serif">A</span><span class="bar" id="color-bar"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn swatch-btn" id="m-highlight" title="Highlight">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"><path d="M3 11l5-5 2 2-5 5H3z"/><path d="M9 5l2-2 2 2-2 2z"/></svg>
|
|
|
|
|
|
<span class="bar" id="hl-bar" style="background:#ffff00"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="m-clear" title="Clear formatting">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M4 3h8M7 3l-2 9M9.5 9.5l3.5 3.5M13 9.5L9.5 13"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">font</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn" id="a-left" title="Align left">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 4h12M2 7h8M2 10h12M2 13h7"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="a-center" title="Centre">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 4h12M4 7h8M2 10h12M4.5 13h7"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="a-right" title="Align right">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 4h12M6 7h8M2 10h12M7 13h7"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="a-justify" title="Justify">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 4h12M2 7h12M2 10h12M2 13h12"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<select class="rsel" id="line-height" title="Line spacing">
|
|
|
|
|
|
<option value="">Spacing</option>
|
|
|
|
|
|
<option value="1">Single</option>
|
|
|
|
|
|
<option value="1.15">1.15</option>
|
|
|
|
|
|
<option value="1.5">1.5</option>
|
|
|
|
|
|
<option value="2">Double</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn" id="indent-out" title="Decrease indent">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"><path d="M7 4h7M7 8h7M7 12h7M2 4v8M5 6L3 8l2 2"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="indent-in" title="Increase indent">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"><path d="M7 4h7M7 8h7M7 12h7M2 4v8M3 6l2 2-2 2"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="l-bullet" title="Bulleted list">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M6 4h8M6 8h8M6 12h8"/><circle cx="3" cy="4" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="8" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="12" r="1" fill="currentColor" stroke="none"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="l-ordered" title="Numbered list">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" fill="none"><path d="M6 4h8M6 8h8M6 12h8"/><text x="1" y="5.5" font-size="5" fill="currentColor" stroke="none">1</text><text x="1" y="9.5" font-size="5" fill="currentColor" stroke="none">2</text><text x="1" y="13.5" font-size="5" fill="currentColor" stroke="none">3</text></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<select class="rsel" id="list-format" title="Numbering format" style="max-width:96px">
|
|
|
|
|
|
<option value="decimal">1. 2. 3.</option>
|
|
|
|
|
|
<option value="lowerLetter">a. b. c.</option>
|
|
|
|
|
|
<option value="upperLetter">A. B. C.</option>
|
|
|
|
|
|
<option value="lowerRoman">i. ii. iii.</option>
|
|
|
|
|
|
<option value="upperRoman">I. II. III.</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">paragraph</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn" id="i-link" title="Link (Ctrl+K)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M6.5 9.5l3-3M7 4.5l1.5-1.5a2.5 2.5 0 013.5 3.5L10.5 8"/><path d="M9 11.5L7.5 13a2.5 2.5 0 01-3.5-3.5L5.5 8"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="i-image" title="Insert a picture">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="2" y="3" width="12" height="10" rx="1"/><circle cx="5.7" cy="6.3" r="1.1"/><path d="M2.6 11.6L6 8.6l2.3 2 2.2-2.2 2.9 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="i-table" title="Insert a table">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="2" y="3" width="12" height="10"/><path d="M2 6.3h12M2 9.7h12M6 3v10M10 3v10"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="i-rule" title="Horizontal rule">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 8h12"/><path d="M4 4.5h8M4 11.5h8" opacity=".35"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="i-pagebreak" title="Page break (Ctrl+Enter)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M4 2h8M4 14h8M2 8h12" stroke-dasharray="2 1.6"/><path d="M4 2v3M12 2v3M4 14v-3M12 14v-3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">insert</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rgroup" id="table-group" data-contextual="off">
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn wide" id="t-row-after" title="Insert a row below"><span>+ Row</span></button>
|
|
|
|
|
|
<button class="btn wide" id="t-col-after" title="Insert a column to the right"><span>+ Col</span></button>
|
|
|
|
|
|
<button class="btn wide" id="t-merge" title="Merge the selected cells"><span>Merge</span></button>
|
|
|
|
|
|
<button class="btn wide" id="t-split" title="Split the selected cell"><span>Split</span></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rrow">
|
|
|
|
|
|
<button class="btn wide" id="t-row-del" title="Delete this row"><span>− Row</span></button>
|
|
|
|
|
|
<button class="btn wide" id="t-col-del" title="Delete this column"><span>− Col</span></button>
|
|
|
|
|
|
<button class="btn wide" id="t-header" title="Toggle the header row"><span>Header</span></button>
|
|
|
|
|
|
<button class="btn wide danger" id="t-del" title="Delete the whole table"><span>Delete</span></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rlabel">table</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="banners" id="banners"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="board" id="board">
|
|
|
|
|
|
<div class="sheet" id="sheet">
|
|
|
|
|
|
<div class="empty-state" id="empty">Loading document…</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
<span class="stat" id="stat-words">0 words</span>
|
|
|
|
|
|
<span class="stat" id="stat-pages">1 page</span>
|
feat(docx-editor): the page fills the window, and two fonts ship with it
The page sat marooned in the middle of a wide window with dark space either
side of it. It is now drawn at the size the document actually claims — A4
stays A4, margins come from its own sectPr — and CSS `zoom` scales that to
fit, defaulting to Fit width with a control in the footer and Ctrl +/-/0.
Scaling rather than widening is deliberate. A page stretched to the window
would break every line somewhere different from where the printed page
breaks it, and an editor whose whole claim is that it shows you the document
should not lie about where the lines end. `zoom` also beats a transform
here: it affects layout, so the board scrolls correctly and ProseMirror's
coordinate maths keeps working.
Ubuntu and Fraunces now ship in fonts/, because Windows has neither and a
font offered in the ribbon that the machine lacks is a font the user picks
and then cannot see. Fetched once by `npm run fonts` and committed, never at
runtime: an extension in a browser built around not phoning home should not
ask a font CDN what a document looks like every time one is opened.
Two things had to be worked around. On file:// Chromium registers @font-face
rules and then refuses to fetch the files — the family appears in
document.fonts and every glyph still renders in the fallback — so the add-on
reads the woff2 and hands the page a stylesheet with them inlined as data
URLs. The PDF export needed the same treatment for a different reason: its
print window runs from a temp folder, where a relative url() resolves to
nothing, which would have quietly undone the one-stylesheet-for-both promise
that lib/doc-css.js exists to keep. If either path fails, the ribbon labels
those families "(not available)" rather than implying otherwise.
About 700 KB, most of it Ubuntu's Cyrillic and Greek — kept because the
documents this is used on are not all English. Licences ship alongside.
2026-09-22 19:12:56 +02:00
|
|
|
|
<span class="stat zoomer">
|
|
|
|
|
|
<button class="fbtn" id="zoom-out" title="Zoom out (Ctrl+-)">−</button>
|
|
|
|
|
|
<select class="fsel" id="zoom" title="How large the page is drawn">
|
|
|
|
|
|
<option value="fit">Fit width</option>
|
|
|
|
|
|
<option value="page">Whole page</option>
|
|
|
|
|
|
<option value="0.75">75%</option>
|
|
|
|
|
|
<option value="1">100%</option>
|
|
|
|
|
|
<option value="1.25">125%</option>
|
|
|
|
|
|
<option value="1.5">150%</option>
|
|
|
|
|
|
<option value="2">200%</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="fbtn" id="zoom-in" title="Zoom in (Ctrl++)">+</button>
|
|
|
|
|
|
</span>
|
feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<span class="stat"><button class="fbtn" id="open-folder">Open folder</button></span>
|
|
|
|
|
|
<span class="msg" id="msg"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<input type="file" id="file-input" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document" hidden>
|
|
|
|
|
|
<input type="file" id="image-input" accept="image/png,image/jpeg,image/gif,image/bmp" hidden>
|
|
|
|
|
|
<a id="download-link" style="display:none"></a>
|
|
|
|
|
|
|
|
|
|
|
|
<script src="vendor/docx-vendor.js"></script>
|
feat(docx-editor): Save as…, PDF export, and a mark of our own
Three gaps, one theme: the editor could produce a file but not decide where
it went, what format it was in, or look like anything in the dock.
**Save as…** opens a real file dialog, and the extension typed there picks
the format. Save then writes to that file instead of dropping another copy
in Downloads every time. The renderer never names a path: the dialog returns
an opaque token, and the add-on will only write to a path a dialog actually
returned. An extension page is the least trusted thing in the add-on, and
"write these bytes anywhere" is not a capability it needs.
**PDF** goes through Chromium's own print pipeline in a hidden window — the
same engine as Ctrl+P — on the paper size read out of the document's own
sectPr. For that to match what the user was looking at, the page's
typography had to stop living in editor.css, which the export window can't
reach: it moves to lib/doc-css.js and both surfaces read the one string. The
result embeds subsetted fonts, keeps images, and turns hyperlinks into real
PDF link annotations.
**The icon** is ours. Microsoft's Word mark is a trademark and borrowing it
to look official is not something a browser that talks about sovereignty
should do. icon.svg says "text document" in its own words — a turned corner,
a heading rule, body lines, a pilcrow badge in Silent Mode green — and
`npm run icons` derives the PNGs and addon.json's copy from it, so there is
one drawing rather than several that drift.
Also: the scratch folder follows the profile rename to extensions-data/ via
the api.dataDir the host now provides, instead of creating a stale
addons-data/ beside it.
2026-09-21 03:35:55 +02:00
|
|
|
|
<script src="lib/doc-css.js"></script>
|
feat(docx-editor): edit Word documents without quietly eating what Word put in them
A .docx editor is easy to write badly: read the file into HTML, let someone
edit it, write a fresh document back, and hand them a file that lost its
headers, its page size and half its formatting without ever saying so.
Three things keep this one honest.
The reader doesn't use mammoth's HTML. mammoth's converter is deliberately
semantic, and HTML has nowhere to put a run's colour or a paragraph's line
spacing, so it drops them — and those are controls this editor puts in the
ribbon. Taking its parsed document model instead means what the ribbon offers
is what the file can actually carry. Six properties mammoth's model didn't
keep are added by build-time patches, each asserting its anchor so an upgrade
that moves the code fails the build rather than shipping a lossy reader.
The writer rebuilds the body but carries the rest of the package across:
headers, footers, footnotes, endnotes, the document's own style catalogue,
its theme and its page setup, with relationship ids and content types
re-wired. Word features the editor can't model are still lost, so they are
detected when the file opens and named in a banner before anyone edits.
Tracked changes get their own gate. mammoth renders insertions as ordinary
text and drops deletions, so saving would accept every pending revision
without Word ever asking. Such a document opens read-only until the user
says that is what they want.
Verified over 66 real documents: 65 round-trip with an identical model and a
structurally valid package, the one exception being a 7 MB WMF picture, which
no browser can display and the writer cannot emit. Also driven end to end
through a real Theseus over CDP — sidebar, ribbon, typing, save, reopen.
2026-09-20 20:46:29 +02:00
|
|
|
|
<script src="lib/pkg.js"></script>
|
|
|
|
|
|
<script src="lib/schema.js"></script>
|
|
|
|
|
|
<script src="lib/read.js"></script>
|
|
|
|
|
|
<script src="lib/write.js"></script>
|
|
|
|
|
|
<script src="editor.js"></script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|