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.
210 lines
12 KiB
HTML
210 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Word editor</title>
|
||
<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>
|
||
<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>
|
||
|
||
<!-- 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>
|
||
<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>
|
||
<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>
|