The editor's a-rule and LibreOffice's Internet Link style both outrank the
region CSS, so neither export lets the baked region color override a
non-plain link's paint: ODT leaves the color off the link run, DOCX paints
the link blue unless the run carries a direct color — the DOCX import
otherwise read its own link back as plain.
Both importers resolve the table's style name + look + grid position through
resolveTableCell: the region attr comes back on each cell, a bold region's
runs convert like headings (boldByDefault) and a run matching the region's
color loses that mark — the style's own text no longer lands as direct
formatting on reopen. A DOCX file carrying its own w:tblStylePr keeps the
baked areas instead; a style name not in the registry stays untouched.
The b:Sources part is also the citation style's only carrier, so a document
whose bibliography index sets a non-default style writes it even with no
source cited. The importer keeps a declared all-off w:tblLook ('') instead
of dropping it back to the default look.
The TOC importer no longer fabricates settings our own export mints: a right
stop at the column's end is the attr's default (null), and an entry template
resolving to the default style names no levelStyles entry. A custom-shape box
keeps its vertical text (the writing-mode read only ran for plain frames),
and a declared all-off tableLook ('') survives instead of reverting to the
default look.
- a continuous break that only encodes a columns region no longer
fabricates a section break or its own header/footer set
- a topBottom text box keeps its x offset (posOffset now written)
- cell margins read back in hundredth-cm, so twips don't show through
- a table margin's zero side stays the default instead of -0
- what a block's named style supplies (Title's centring, keep-with-next,
its size as a raised run's percent reference) is direct formatting no more
- a topBottom box keeps its left place instead of gaining a fabricated
centring; horizontal-pos right reads back as the box's wrapAlign
- a table margin's zero side stays the default instead of an explicit 0
An XE field's literal quote is backslash-escaped (Word's own escaping;
import already unescaped it) instead of flattened to an apostrophe. A
w:ins/w:del inside w:hyperlink imports (shared handleRevision closure).
Text boxes carry charStyle (w:rStyle), index entries, citations,
date/time fields and revision marks (w:delText for deletions);
dateTimeFieldParts/xeInstr are shared with the body path.
The deleted runs are hoisted through every serialization pass and spliced
into the tracked-changes registry as styled runs (as LibreOffice stores
them); import converts them back as real inline content. One point marker
per change instead of one per fragment. A deletion's marker also no longer
makes a formula beside it display (aloneInParagraph counts text:change).
applyComments now declares dc:/loext: only when absent — beside a revision
the duplicate xmlns:dc made content.xml invalid.
Word keeps a comment's resolved state as w15:done in
word/commentsExtended.xml, keyed by the w14:paraId of the comment
body's last paragraph. The docx package knows neither part, so a
post-pack pass stamps the paraId, mints the part with its content
type and relationship, and the importer reads it back.
The hand-serialized txbxContent emitted only heading style, indent and
alignment: spacing, line height, indents, direction, shading, borders,
links, comment ranges, bookmarks, cross-references, formulas, rubies
and image rotation were all dropped. A bookmark still open at a
paragraph's end lost its runs (missing flush), and the formula/ruby
run regexes could start at a box's drawing run and swallow the box
preamble — they now stay inside one run.
A text box's anchor paragraph nests its children, so the STY/PBX passes'
scan to the closing tag started at the anchor and stole the first box
child's spec (its indent, direction, shading and borders). Both passes
now match the sentinel right after the opening tag, where it is emitted.
The TEF fallback also accepts bookmark markers and comment annotations
inside a run (tempered to the annotation's first close tag, -end kept
distinct), instead of cutting the effect short at them.
A row holding only covered cells cannot exist in the editor and was
dropped on ODT import, but every rowspan reaching over it kept its
full count, leaving an inconsistent grid. Spans now shrink by the
dropped rows they covered.
The docx package cannot express a custom footnote/endnote mark, so the anchor
always went out as an automatic reference and the label was lost — and the
importer never read one either, so a Word file's custom mark leaked into the
note's body text. A post-pack pass now rewrites the anchor (flag + literal
run) and the note part's <w:footnoteRef/>; the importer reads the flag, takes
the run's text as the label and strips its repeat off the note body.
A DOCX numbering level keeps the first format it saw, so a second nested list
under the same parent with another number format or bullet char silently
rendered as the first one's (an attr-less nested list under an upper-roman
parent came back lower-alpha). The kind-mismatch fork now covers format and
bullet char too, and copies the shallower levels so the importer's cycle walk
still sees the ancestry.
Two applyTextEffects bugs: the fallback span for an effect-only run stopped at
the first element, so everything behind a tab/line-break fell out of the span
and lost the effect — the run is now terminated by an empty sentinel pair and
inline empties ride along. And a raised run (textPosition) was written as a
percentage of the default 12pt even inside a heading, so the importer, which
resolves against the block's real size, read a different height back.
The whole-block regexes in the sentinel passes (page breaks, paragraph
styles/boxes, text effects, whitespace collapse) matched a self-closing
<text:p/> too — [^>]* happily ends in a slash — and then swallowed everything
up to the next real closing tag, so an empty cell paragraph took the restyle
meant for a later paragraph's sentinel and that paragraph lost it. The
opening-tag pattern now rejects self-closing tags.
Both importers stamped the exporter's own note style back onto the note
(styleName "Footnote" from ODT, "footnote text" from DOCX), so an
editor-authored document accreted a foreign attr on every round trip and the
two formats disagreed. A style that only re-states the stock note look is now
suppressed like any other default-equal value; a customized one still lands.
applyPageBreaks required its sentinel directly after the opening tag, but a
custom paragraph's serialized text starts with the FSZ/STY/PBX payloads, so
the break was silently dropped. It also minted PB# names, colliding with
applyParagraphBoxes' PB# — the importer then resolved both paragraphs to one
definition, painting the break paragraph with a stranger's box props. Now the
sentinel matches anywhere in the block and the pass mints PBK# instead.
Both handled the key on the dropdown, but opening one leaves the focus on
its trigger button, which sits outside it — so Escape never arrived and the
popover stayed open. Both elements listen now.
Which importer ran was decided by the extension alone, so a .docx saved or
renamed as .odt (and the reverse) was reported as a broken file. A failed
read is retried as the other format; a file that is neither still reports
the error for the extension it carries.
Only a pure delete was turned into a deletion mark; typing or pasting over
a selection ran as it was, so the replaced text left the document unmarked
and rejecting the change lost the original for good. A one-step replacement
now marks its range deleted and puts the new content after it, as both word
processors do.
The document's content ended in an optional noteSection, so the content
match after the first child offered noteSection as its defaultType — and
that is what TipTap's clearNodes converts a block into. Every style command
on a top-level block past the first threw "Invalid content for node type
noteSection" and applied nothing. Spelling the same language as an
alternation puts a paragraph first again.
Clearing hfActive removes the live zone's DOM during Svelte's flush;
ProseMirror dispatches a blur transaction as it goes, and the hfTick
mutation in onTransaction then landed mid-render and threw, aborting the
rest of the batch. The tick is queued to the next microtask instead.
transformPasted fell back to a DEFAULT_EDITOR_FONT of 'Georgia' — the
comment said it had to match ToolbarExpanded's, which is Liberation Serif.
Text pasted where the caret carries no font mark got Georgia as direct
formatting, and pasting into a heading overrode its sans font. Only an
explicit font at the caret is carried over now.
Replace the hardcoded node if/else chain in describeTransaction with a
NODE_LABELS map covering image, text box, formula, page break, TOC,
fields, cross-reference, index/bibliography entries, ruby and columns;
foot-/endnotes labelled via their kind attr. MARK_LABELS gains link,
comment, bookmark, character style and tracked change.
exportPdf gains a test seam: a window.__edentextPdfSink takes the bytes
instead of doc.save(), whose download hangs in headless Chromium. The
smoke test clicks the real export menu over the imported corpus document
and asserts %PDF magic and a plausible size. The test browser is pinned
to en-US so the labels it clicks are deterministic across machines.
Found by the cross-format fuzz leg. importDocx baked the resolved cell
chain in as direct formatting even where it equals what an unset attr
already renders (0/0 spacing; single line height over a single default) —
every re-imported own file grew spacing attrs on every cell paragraph.
And a numbering level kept the first list kind it saw, so a nested bullet
list beside a nested ordered one came back ordered; a kind conflict now
forks its own numbering reference, as the ODT side mints a list style.
Ctrl+S always built .odt bytes and wrote them into whatever handle was
open — including a handle obtained by opening a .docx, silently
overwriting it with ODF content under its old name. Track the loaded
format and route Save through the matching exporter and handle; the
title's extension label now reflects it instead of a hardcoded .odt.
Save As stays .odt-only, so it switches the format back.
Three defects in the cell/list paragraph style pipe, found by fuzzing:
ParaStyle had no indent/indentFirst/indentRight, so a cell paragraph's
indents never reached content.xml (list items keep skipping them — their
indent lives in the list style). The minted-style dedupe keyed on only
four of the style's fields, so two different styles could share one name;
both keys now serialize via paraStyleProps. applyListItemStyles' regex
missed items carrying text:start-value, shifting every later item's style.
Each of the ~30 post-processing passes re-zipped every entry (images
included) at level 6 just for the next pass's unzipSync. Intermediate
archives are now stored; zipFinal compresses once at the end, keeping
mimetype first/stored and storing already-compressed picture formats.
Measured ~10x on an image-heavy document (853ms -> 87ms).
Both products name the tool after what it is, which says nothing to a
reader who does not know the word. Every label is "Synonyms" now — ribbon,
context menu, tools menu, dialog — and "thesaurus" stays in the tooltip so
it is still findable by name. Keys and code keep the technical name.
The pass that reads the caret's word on open also writes it, so it was a
dependency of itself: it re-ran on every flush and cleared term and groups
just as a lookup landed. It runs untracked now, on `open` alone. The
dialog also shows the language it is asking and lets it be switched, as
both products do — a German word in an English document found nothing,
with nothing on screen saying why.
Ctrl+F7, Review ▸ Thesaurus and the context menu open one dialog: the
word at the caret, the sense groups it is in, click one to replace it,
and a box to look up any other word. The data is MyThes re-packed to one
group per line (de 3.0MB/37k groups, en 6.7MB/143k) — the format repeats
every group once per member, ten times the bytes. Loaded on first use for
the document's language and scanned, not indexed: ~4ms a lookup, where a
word→group index measured ~100MB of memory against the text's ~8MB.
View ▸ Multiple Pages (1–4): rows of pages filled left to right, scrolling
as one canvas, every page editable. Neither reference product stops at two —
LibreOffice takes a column count, Word fits what the window allows — and both
re-zoom for it, so a whole row is fitted when the count changes. A pane is a
grid cell clipped to one page, a view showing only a contiguous slice; two
rows of cells take turns as the upper one. Cost: columns x 2 live views, each
holding the whole document (measured: +94k nodes and +16ms per keystroke per
view at 177 pages).
A new document starts with it off and an import adopts the file's own
flag, so an editor left recording no longer marks the next document it
opens. Both formats carry it, probed against soffice: ODF on the change
registry (text:track-changes, written for the flag alone where nothing
is recorded yet), Word as w:trackRevisions in settings.xml — LibreOffice
writes and reads exactly those two, and reading w:trackChanges instead
would have found nothing.
The colour section only read the marks, so a colour no mark carries —
a decoration's, a stylesheet rule's, the page's own — was invisible in
the dump. `painted` adds the computed colour per rendered run with the
element that declares it, the page colour variables read at the editor's
own element, and the document stylesheet's colour rules.
With revision recording on, setContent inserted the whole file in one
transaction, so the recorder marked every run as the reader's own
insertion — the document then rendered underlined in the author colour
(#c69200) from the first character. Measured: 19 843 of 19 843 characters
on an 11-page import, 436 revision spans.
Both loads (new document, import) now carry the RECORDING meta; what is
typed after the open is still recorded, as in both word processors.
View ▸ Split (Ctrl+Alt+S): a second live view of the editor's state, stacked
below the first and editable the same way. The panes share a width, so the
second renders the pagination the first measures — pageBreaks, columnsFlow
and tabStops skip the pass on a split pane rather than run it twice.
Every widget decoration now builds its DOM in a function: two views render
one decoration set, and a shared node is taken back and forth between the
two documents until the tab locks up (measured on a two-page document).
A polygon, a polyline, a curve or a connector's elbow used to be dropped
with a warning; it now rides the box as an SVG path in the 0…100 box a
preset's points use, and the editor draws it. Read from all four dialects
the two products write (ODF points and svg:d, a non-primitive
enhanced-path, DrawingML custGeom, VML's reversed-case path), written
back as enhanced-path and custGeom — LibreOffice renders both exactly as
the editor draws them. An open outline is stroked, never filled.
Both come from the document's language, read off Intl: the symbol's side
and spacing, the short date's order and padding. Each minted data style
names that language — without it LibreOffice prints its own locale's
separators (probed: a German document's 1.234,00 € came out $1,234.00).
A date is a serial day from LibreOffice's day 0, 1899-12-30. Word's
formula dialog offers no date, so the DOCX leg uses the field's \@
picture. Also drops a document's name from three older comments.
The three attrs LibreOffice's Format… dialog is mostly about, on both
sides of the bar. They live together on one TextStyle mark and a mark of
one type replaces that type whole, so each run's own attrs are the base of
the new one — recolouring leaves the font alone.
F3 was bound twice from here on: it now expands an AutoText shortcut where
the caret has one, as it does in both word processors, and finds the next
match otherwise.
Word's Text Direction and LibreOffice's, on the frame's own toolbar: the
browser lays the vertical flow out, so the box needs nothing but the mode.
ODF keeps it in the frame style's *paragraph* properties — probed: in the
graphic properties LibreOffice drops it, there it keeps it and lays the box
out — and Word in the shape body's `vert`, the VML fallback's
`layout-flow:vertical` read too. Both survive a LibreOffice re-save.
The Formula dialog gains the number format Word's has: whole numbers, two
decimals, grouped thousands and the two percentages beside the general
one.
ODF keeps it on the cell *style* as a reference to a data style — odf-kit
shares one cell style across every cell, so a formatted cell gets a copy
of it — and LibreOffice writes both back unchanged. Word keeps it on the
field as its \# switch; LibreOffice's DOCX filter neither reads nor writes
that switch, so that leg holds through our own round trip.
Ruby, as both products' phonetic guide writes it: one element holding a
base and its reading, so the editor keeps it as one inline atom and the
browser sets it as a real <ruby>.
ODF gets <text:ruby> plus a minted ruby-family style (centred, above —
probed: LibreOffice writes that style back unchanged); Word a <w:ruby> run
from a post-pack pass, the package having none. Both legs verified through
a LibreOffice re-save, our .docx to .odt included.
LibreOffice's Tools ▸ AutoText, in the ribbon where Word keeps its Quick
Parts: store the selection under a name and a shortcut, insert it from the
library or by typing the shortcut and pressing F3 — LibreOffice's key for
it.
An entry is HTML parsed back through the schema, so it keeps its marks and
blocks; the library is app-level, as it is in both products, so no file
carries it.
LibreOffice's Word Completion, its defaults included: words from eight
letters up are collected as they are finished, and the first three
letters offer the rest. The offer is a widget decoration after the caret,
so it is outside the document until Enter takes it — nothing to undo when
it is not taken.
Only while typing: a bare caret move drops the offer, or an Enter meant to
split the paragraph would complete a word instead. The settings share the
AutoCorrect dialog, as they share LibreOffice's.
The ¶ toggle adds a format to each side: bold / italic / underline and a
paragraph style, as LibreOffice's Format… and its Paragraph Styles box.
A format narrows a text search to the runs carrying it, and with no term
the formatting is the search — every such run, or every paragraph in the
style, empty ones included.
A replacement with no text only reformats, which is how a document gets
restyled without retyping it; a heading style switches the node type over
the way setParagraphStyle does, so the extension takes the same sheet.
Both dialogs offer three sort keys and let you pick the type; only the
first key was offered, and the type was always the detected one. A later
key decides only where the ones before tie, and the sort stays stable.
Alphanumeric is the plain collation ("10" before "2") — that is what
choosing it over the detected type is for, so the two collators differ in
`numeric`. Sorted numerically, a cell that is no number sorts last either
way, as an empty cell does in a spreadsheet.
Cite by short name (LibreOffice's own), numbered, APA, MLA or Chicago:
the style sets the citation's label and the row's shape, and rides the
bibliography index node — ODF as the entry template LibreOffice
regenerates its rows from (verified through a re-save), Word as the
sources part's StyleName. Labels are derived and kept true by an
appendTransaction, so adding a citation renumbers the rest.
A section narrower than the reserved sheet now splits the difference
between its insets instead of hanging it all on the right, and every
per-page layer draws from the box's own left — sheet, header/footer, page
border and line numbers. The ribbon's Size menu gains the same per-section
block Orientation already had.