Commit Graph
111 Commits
Author SHA1 Message Date
Steffen Becker 2f087bc880 feat: Find & Replace searches and replaces formatting
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.
2026-08-15 10:19:48 +02:00
Steffen Becker dab1d662c9 feat: the table sort gets its three keys and a sort type
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.
2026-08-15 10:12:24 +02:00
Steffen Becker 24402dd372 feat: the bibliography gets a citation style
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.
2026-08-15 10:02:54 +02:00
Steffen Becker 5f2e7fcbcf feat: a section can restart the page numbering
Word's Start at, per section: ODF writes style:page-number on the
paragraph that switches master page, DOCX a w:pgNumType w:start. That
section is no longer exported as a continuous break — probed, LibreOffice
drops the start off a continuous one, and neither word processor honours
it there. The footer counts from the nearest restart at or above the page.
2026-08-15 09:49:20 +02:00
Steffen Becker 1c4dec49f3 feat: a reviewing pane, and a colour per reviewer
Every recorded change in document order beside the comments pane: author,
date, the text, accept or reject in place. A change split by a paragraph
boundary is one row, so acceptRevision/rejectRevision take an id. The
author colour is a decoration, since a mark cannot know the document's
other authors; it nests inside the mark's span and carries the under/
strikethrough with it. shortcutHint no longer throws on an id nothing
binds, which heading 7-10 had just made reachable.
2026-08-15 09:40:26 +02:00
Steffen Becker 147034730f feat: headings go to level 10, as both word processors do
HTML stops at h6, so 7-10 render as unknown elements editor.css makes
blocks; their built-in styles continue level 6 at 12pt, since LibreOffice
writes these styles with no properties and resolves them from its own
pool. Both importers read a two-digit level again (the style-name regexes
stopped at one digit). Verified through a LibreOffice re-save of both
formats and in the live editor.
2026-08-15 01:21:23 +02:00
Steffen Becker 4afd5cd1d5 feat: a paragraph can opt out of automatic hyphenation
LibreOffice's Format > Paragraph > Text Flow checkbox. Only "off"
travels (Word has no per-paragraph "on") and only under a document that
hyphenates, or it would say what is already true. ODF writes
fo:hyphenate="false" into the paragraph's text properties, DOCX gains
w:suppressAutoHyphens through a post-pack pass — the docx package
exposes no option for it. Both survive a LibreOffice re-save.
2026-08-15 01:09:55 +02:00
Steffen Becker bf564fbab4 fix: footnote counting per page/chapter and collecting at the end really work
Both were selectable in the dialog and changed nothing — they only rode the
file formats, so an imported document numbered and placed its notes wrong.
noteLabels now counts within a bucket (class, chapter, or the page pageBreaks
publishes through setNoteAnchorPages); a collected footnote leaves position:
absolute and opens its own page, as LibreOffice does. Counting and position
move to the footnote tab, the only place LibreOffice offers them.

The separator's own alignment moves the line alone — probed, notes stay left.
2026-08-14 23:11:10 +02:00
Steffen Becker 46165b82f7 fix: a caption is boxed to its frame, and the frame stays clickable
Two faults on one picture. A block after a float keeps its box over
the float — only its lines move — so the caption on top swallowed
every click meant for the picture; the frame is lifted above the
in-flow boxes. And a caption can only sit under a side-wrapped frame
if it is boxed to it, so its span in the column is measured and
becomes the caption's indent, which also drops it clear of the float.
Measured at 100% and 150%: caption under the picture, its width, in
all four wrap modes.
2026-08-14 22:40:42 +02:00
Steffen Becker 61ddf69d54 fix: a caption follows the frame's place, not the paragraph's
A picture with "above and below" wrap is a frame centred in the
column while its paragraph carries no alignment at all, so the
caption still went to the left margin. framePlacement reads the
place off the frame: centred, the side it is set against, or its
offset as the caption's indent. The debug dump gains a frames
section — every other one measures the flow downwards, and this
is a sideways question.
2026-08-14 22:40:42 +02:00
Steffen Becker c4a74f92b1 fix: a caption stands under its picture, not at the margin
It was inserted as a plain paragraph, so a centred or right-aligned
picture got its caption at the left edge of the text column.
LibreOffice frames picture and caption together; a loose paragraph
carries that over as the alignment of the block it captions.
Measured in the live editor: a centred 240px picture at 520…760
now has its caption text at 580…700.
2026-08-14 22:40:42 +02:00
Steffen Becker 3b24b14470 feat: lines and arrows, which both importers used to drop
Two endpoints rather than a box: no text, no fill, and the diagonal of
the frame with `flipV` picking which one. The three kinds share their
preset names, since the heads a file declares are what tell them apart.

ODF gives a line its own element and its heads a named marker, which
needs the draw: prefix declared in styles.xml — undeclared, the file
does not parse at all. Word keeps the frame and flips it.
2026-08-14 22:11:49 +02:00
Steffen Becker 147342655d feat: sort a table, sum a column, recognise a number
The formula rides the cell, as ODF writes it; Word's field syntax is what
is stored, since it is the wider language. `ooow:` in the ODF attribute
value is a namespace prefix — undeclared, LibreOffice reads it as part of
the formula and prints "Expression is faulty".

Sort takes one key and detects the type per cell where both dialogs make
you pick. Number recognition is off by default, as it is in LibreOffice,
and a formula parses a cell's text either way — exactly as there.
2026-08-14 22:11:49 +02:00
Steffen Becker 639fac90fd feat: a section can have its own paper, landscape amid portrait
The page grid was one repeating cycle, so every page had to be the same
size. It is now a table of height runs the placement pass grows as each
section's first page becomes known, and the sheets are one box per page
instead of a repeating gradient — .paper reserves the widest section's
paper, a narrower one takes the difference as a right inset.

Layout ▸ Orientation offers "this section"; the size stays document-wide
in the ribbon but round-trips per section from a file.
2026-08-14 10:36:52 +02:00
Steffen Becker c38320a108 feat: citations that carry their source, and a bibliography
The citation is an inline atom holding the whole record, which is what ODF
writes on text:bibliography-mark — so no database travels beside the file.
The list is a new index family: one row per source, in document order and
without page numbers, as LibreOffice prints one.

DOCX cites by tag and keeps the sources in a custom-XML part the docx
package knows nothing about, so the post-pack pass mints the part, its
properties, both relationships and the content type itself.
2026-08-14 10:16:59 +02:00
Steffen Becker c97a38ec77 feat: a Navigator pane lists the chapters and moves them
F5 (LibreOffice's key) opens an outline of the document's headings:
click one to jump, and move it up or down or promote/demote its level,
each carrying everything under the heading. Below it the tables,
pictures and bookmarks LibreOffice's Navigator also lists.

A move swaps with the adjacent chapter at the same level and stops at a
heading above it, so a subheading never reparents itself. The pane docks
right beside the comments one — the classic chrome's Tools chip owns the
left edge.
2026-08-14 08:32:30 +02:00
Steffen Becker 49e485038e feat: the document saves as a template, and the File menu remembers
A .ott/.dotx is the built document with one label changed — ODF's
package media type, Word's content type for the document part — so the
picker offers both and the chosen extension decides which exporter runs.

Opening or saving also records the file in a recent list. The name is in
localStorage so the menu draws at once; the handle that reopens it is in
IndexedDB beside it. A file that has moved, or whose permission the
browser drops on reload, is reported and dropped from the list.
2026-08-14 08:23:09 +02:00
Steffen Becker 6a313e5692 feat: pictures are autosaved to IndexedDB, not localStorage
A handful of photos is the whole ~5 MB quota, and past it the document
stopped saving. The autosaved JSON now keeps an `idb:<key>` per picture
and the bytes live in their own store; the key is a sampled content
hash, so a picture used twice is stored once and each save sweeps what
the document no longer names.

loadDocument is async as a result and Editor.svelte awaits it. Where
IndexedDB is unavailable the data-URIs stay inline exactly as before.
2026-08-14 08:17:06 +02:00
Steffen Becker 4cda289272 feat: a shape can be a triangle, a star or a block arrow
Twelve presets beside the three rectangular ones. Each outline is one
polygon in utils/shapes.ts, given once: the node view draws it as an SVG
path, the ODF export scales the same points into draw:enhanced-path, and
Word gets the preset's name and draws its own geometry.

A gallery in the box toolbar and the ribbon renders its tiles from that
same table, so a new shape needs no icon. Verified through LibreOffice
from both formats; a preset we can't draw still imports as a warning.
2026-08-14 08:02:08 +02:00
Steffen Becker 53aacd9f77 feat: captions that number themselves
A caption is a paragraph in the Caption style holding a sequence field —
one counter per category, in document order, renumbered as the document is
edited and on load (a file's cached numbers are unreadable wherever the
format is not arabic). Round-trips as an ODF text:sequence with
LibreOffice's own ooow:Illustration+1 formula and a Word SEQ field.
Caption style values probed off LibreOffice: 10pt italic, 0.212cm around.
2026-08-14 06:21:30 +02:00
Steffen Becker 40efc73f86 feat: Find & Replace by regular expression
A .* toggle beside match-case and whole-word takes the term as a
pattern; a pattern that will not compile simply finds nothing. Each
match keeps its captures, so the replacement expands $1…$9 and $&
the way LibreOffice does — Word has no equivalent.
2026-08-14 01:04:23 +02:00
Steffen Becker 8d8bb858de feat: AutoCorrect while typing
LibreOffice's rule set: typographic quotes per document language, its
dash matrix, its replacement table, sentence capitalization with an
abbreviation exception list, and TWo INitials. One handleTextInput
plugin listed last, so every input rule still fires first; the rule
table itself is a pure function under test. Auto-URL and auto-list were
already TipTap's and now answer to the same checkboxes.
2026-08-14 00:53:57 +02:00
Steffen Becker 06c5f2b12a fix: a cell paragraph drops the default style's space below it
Neither word processor passes it into a cell — LibreOffice puts cell text in
its Table Contents style, Word's table styles carry w:spacing after=0 — so the
space filled the cell and vertical alignment had no slack left to move the text
in, which read as the alignment being ignored. The space above is LibreOffice's
alone, so only the add model zeroes that too. Both exporters write the zeros
onto the paragraph, and only when the default style has spacing at all.
2026-08-13 23:39:31 +02:00
Steffen Becker 0da75ed456 refactor: the project is called edentext
Renames the product in the docs, the page title and the DOCX creator, and moves
the whole storage namespace from odf-editor- to edentext-. The prefix is a
per-origin namespace, so it stays the product name rather than a generic one.
2026-08-13 21:43:12 +02:00
Steffen Becker 6957f5682b fix: a style kerns where its own w:kern says so, not only the document's
Word kerns nothing unless w:kern names a size to start at, and the Office
theme declares it on Title alone — but TextProps.kerning stored only the
off state, and a style inherits its parent's, so a title under a document
that kerns nothing could never turn it back on. Its 26pt line then ran
0.36mm over the text width and wrapped a word onto a second line where
LibreOffice sets it in one (measured 151.64mm kerned against 151.98).
Both states are stored now, and both formats read and write them.
2026-08-13 19:15:28 +02:00
Steffen Becker 24d686232e fix: a table style's paragraph spacing no longer overrides the cell's own style
Word ranks a table style's w:pPr under the paragraph style, so a style zeroing
w:after does not take the space after off a Normal that declares one — probed
against soffice, which keeps 8pt there while its w:line=240 still beats
docDefaults' 276. Read as an override, every cell paragraph lost its spacing.
Spacing in a cell now layers docDefaults ← table style ← the paragraph's own
chain (baked in, since a cell carries no style name) ← direct w:pPr.

Corpus 934 → 900; the fixture it was measured on 66 → 34, its page count exact
for the first time (50 against 50, from 44).
2026-08-13 17:07:00 +02:00
Steffen Becker bbbe72bd22 feat: a note renders in the file's own style, marker included
The marker moves from a CSS ::before to a widget decoration: generated
content is no text node, so it reached neither the PDF nor a measurement.
The note also carries the paragraph style its file names, so font, size and
colour are the document's; only the hanging indent stays the editor's,
because the style model has no first-line half to pair with the margin.
Adds a unit test for the numbering and the settings, plus the architecture
doc and the two open parity findings.
2026-08-13 12:17:45 +00:00
Steffen Becker 9d494b2966 fix: a vertically merged cell keeps the edges of the rows it covers
A cell's grid box claimed one row, so a w:vMerge reaching the table's last
row sat inside the banded region instead of on its bottom edge — and a band
declaring insideH nil then dropped the line. The merge extent is counted
before the covered rows are read.

Measured against LibreOffice: the bottom rule runs the full width under a
merged first column, which the editor now draws too.
2026-08-13 08:09:12 +00:00
Steffen Becker 04d1fed5f0 fix: banded rows start on the first body row
The stripe began one row late: regionsAt counted the second body row as the
first band. Probed on a five-row fixture with a red band1Horz — LibreOffice
shades row 0 where the style paints no header region, and the row under the
header where it does.

The corpus cannot see this (no fixture uses the editor's own registry), so it
stayed at 739; checked by eye and by the unit test's inverted expectations. A
document with a table style assigned re-bands on its next open.
2026-08-13 07:06:13 +00:00
Steffen Becker c7e1f91d46 fix: a rule line a paragraph style declares is drawn
Only direct w:pPr/w:pBdr was read, so a Title style's line went missing and
everything under it sat 1.9mm high. The box now goes to the style registry,
where ParaProps' borders already render and round-trip.

Two thirds of that gap is the file's own w:pBdr w:space, carried as
ParaProps.borderPadding (ODF fo:padding) and applied to the ruled sides only —
padding the top as well pushes the paragraph's first line down.
Corpus 761 -> 739.
2026-08-13 00:39:10 +00:00
Steffen Becker 9d6aedb6ef feat: a Word table style paints the areas its file declares
w:tblStylePr — header row, banded rows, first/last column — was never read, so a
table styled in Word arrived as a bare grid. Import bakes each area into the cells
now (fill, borders, run marks): the file's own style has no registry entry to
re-derive regions from, and the bake is what a re-export carries.

A band's box is the whole banded region, so its insideH is the line between two
band rows; banding counts from the first body row, as in Word and LibreOffice.
Corpus 803 -> 795, page counts unchanged.
2026-08-12 22:30:02 +00:00
Steffen Becker 29667a6f0d feat: a right-to-left page fills its columns from the right
ODF style:writing-mode="rl-tb" and Word's section-level w:bidi were not
read, so a Hebrew two-column page put column 1 on the left where
LibreOffice puts it on the right — checked against pdftoppm of its own
PDF, since the harness bands both columns into one line and never gets
that far. It is a document setting now, applied as `dir` on the body
host, which is all CSS multi-column needs; it also gives bidi the base
direction the file means. Round-trips through both formats. The
header/footer band keeps its own direction, as LibreOffice lays it out.
2026-08-12 22:02:36 +00:00
Steffen Becker 096de84b82 feat: mirrored page margins, so an even page is the left-hand one
ODF style:page-usage="mirrored" and Word's w:mirrorMargins make the
declared left/right the inner/outer pair, which an even page swaps.
Read, rendered and written back in both formats; the body rides the
per-block inset a section's own margins already use, the header and
footer band moves with it. Measured on a mirrored two-column file:
its page 2 went from 12.7-184.1mm to 31.8-203.2, which is LibreOffice's
to 0.1mm, and its running head with it.
2026-08-12 21:50:25 +00:00
Steffen Becker 35f3c50541 fix: a column break lands on the line it was measured to
Three measurement bugs in columnsFlow, each worth a page on its own:
rects became lines by their top, so a raised run counted as a line of
its own (192 for 111); the split probed `left + 1`, which is a line's
*end* right-to-left, so it kept a word too many and the flow then shaved
one line per pass; and one median advance under-measured a paragraph
whose lines differ in height (2834px real against 2453 estimated), by
enough to budget 74 lines into room for 64. The chain used to flip
between one fragment and two forever; it now settles after one split.
2026-08-12 20:57:49 +00:00
Steffen Becker 9bfd579d65 fix: hidden text stays off the page, except the heading a running head reads
text:display="none" now drops the paragraph its style hides and the stored
value of a field that carries the attribute — LibreOffice lays out neither.
A hidden heading is the exception and still renders: it is the outline the
running head's chapter field reads, and dropping it blanked every head that
quotes one. The editor has no way to hold a block it does not draw.
2026-08-12 19:49:33 +00:00
Steffen Becker 99a14a1a7f fix: Hebrew and Arabic take the size their style declares for them
ODF carries western, asian and complex-script font properties side by side and
a word processor picks by the character's script; the importer read only the
western pair, so a complex-script run came out at the document default.
Decided per run — the finest split the model has — so a document with no
complex script is untouched, and our own export (all three written alike)
round-trips. Measured: a two-column page's line pitch 7.5mm -> 5.9 against
LibreOffice's 6.0.
2026-08-12 19:49:15 +00:00
Steffen Becker f7ee9546a4 refactor: what the selection formats becomes a shared reader
Font, size, mark colour and block attributes all answer the same question — the
one value the selection agrees on, blank when it mixes. One `uniform` walk now
serves all four, framework-free, so any chrome can read it inside its own
reactive wrapper.

The classic toolbar keeps its own copies for now: it is the baseline the ribbon
is compared against, and it should adopt these only once that comparison ends.
2026-08-12 01:22:52 +02:00
Steffen Becker 2a8f0675bc fix: Word's hollow bullet is the letter it is
A Courier New "o" was mapped to ◦ for looks. LibreOffice and Word both
draw the glyph the numbering names, so the nested lists of the gravity
summary now read like theirs.
2026-08-11 21:22:13 +00:00
Steffen Becker 95d174501d fix: a Word hyperlink keeps the look its file gives it
Word paints a link through its Hyperlink character style, never for
being one, so the editor's own blue overwrote both the file's #0000FF
and the plain black of a link carrying no style at all. The DOCX
importer marks those `plain` and the CSS leaves them alone; LibreOffice
paints every ODF text:a, so those keep the visual.
2026-08-11 20:52:12 +00:00
Steffen Becker 8db6214c11 fix: a CMYK image keeps the colours its file draws
Chromium colour-manages a CMYK JPEG through its embedded ICC profile
where LibreOffice and Word convert naively, so a fixture's
logo arrived dull teal on grey instead of vivid blue on black. The
importer drops the profile — measured back onto LibreOffice's own
values, and 655 KB smaller for that one logo.
2026-08-11 20:42:00 +00:00
Steffen Becker 361ce14864 feat: pair kerning follows the file, not the browser
A browser always kerns; ODF kerns unless told not to and Word kerns only
above the size w:kern names — nothing, in most files. Measured 5% on a
kerning-heavy string, enough to break a line a word early throughout every
Word document in the corpus. Carried on the style, since that is where
w:docDefaults and the default paragraph style put it.
2026-08-11 07:23:20 +00:00
Steffen Becker db8f4439b1 feat: a table that may not break between rows moves whole
ODF style:may-break-between-rows="false" — probed: LibreOffice leaves four
lines of a page empty rather than split such a table, and splits the identical
one when the attribute says "true". pageBreaks now emits a single atomic leaf
for the wrapper unless the table is taller than a page, where the rule cannot
be met. Imported, exported and round-tripped; Word has no equivalent.
2026-08-11 00:02:31 +00:00
Steffen Becker 13e026fb8b fix: a zone's own margins survive the ODF round trip
The Header/Footer style carries them, and the band that grows to hold them
is rebuilt from there on the way back in.
2026-08-10 21:31:38 +00:00
Steffen Becker af144362fe feat: an as-char frame sits where the file says it does
ODF style:vertical-pos/-rel on an inline frame, which every LibreOffice
formula image carries: baseline, character area, or a plain offset from the
baseline. The default moves from the line-box bottom to the baseline, so a
line with a picture in it keeps the descent below it, as LibreOffice and
Word both do.
2026-08-10 20:48:31 +00:00
Steffen Becker 819f51587e fix: direct formatting on a run beats its named character style
A character style is a stylesheet rule, direct formatting an inline style on the
run's span. The charStyle mark ranked below TextStyle, so its span sat *inside*
and the rule won over the ancestor's inline style — a 9pt style silently undid a
run's own 10pt. Priority 102 lifts it above TextStyle's 101.

One fixture: 259 → 172 parity issues, lineEnd 49 → 7.
2026-08-10 18:08:13 +00:00
Steffen Becker 3153c2f7cc feat: the running head names the chapter the page is in
A manual's footer carries text:chapter, which we imported as the string the
producer happened to cache — so every page of a fixture claimed the last chapter
of the book. It is now a live field: the layer resolves the last heading at
or above its outline level to have started by that page. Round-trips to ODF
text:chapter and Word STYLEREF; PDF keeps the cached name (Chrome's print
engine has no named strings).
2026-08-10 13:04:25 +00:00
Steffen Becker a8dcd9b897 feat: a cell's vertical alignment survives saving
Import read style:vertical-align but neither exporter wrote it, so a
mid-cell callout label jumped to the row's top on every save. odf-kit's
CellOptions and docx's TableCell both take it natively; DOCX import maps
w:vAlign in as well.
2026-08-10 12:51:58 +00:00
Steffen Becker aff3eaa812 fix: a named style's line spacing is the percentage the file declares
The style-registry path divided an ODF fo:line-height by 1.15 where the
block path, both DOCX paths and the export all treat it as the plain
percentage, so every paragraph whose spacing came from its style
rendered 15% too tight. Its suppression yardstick is the block's own
style too, so a 100% under a 115% style stays as direct formatting.
2026-08-09 20:43:34 +00:00
Steffen Becker d6bc656c83 feat: the space between two blocks follows the document's own model
LibreOffice adds the upper block's space-below to the lower one's
space-above, and drops the space-above where a page break put the block
at a page top — but for a Word document it takes only the larger of the
two (all probed against soffice). CSS margins can only collapse, so
space-before rides --space-before and editor.css renders it as padding
('add') or margin ('max'); the model is read from settings.xml, assumed
for DOCX, and written back on ODF export.
2026-08-09 16:53:13 +00:00
Steffen Becker af299d8b9f fix: a minted automatic style reaches a document that had none
odf-kit writes `<office:automatic-styles/>` self-closed when it minted
nothing, and every post-pass injected by replacing the closing tag — so
in such a document the page-break, spacing, list and text-effect styles
were dropped and only the style-name reference survived. One
injectAutomaticStyles helper now serves all of them, content.xml and
styles.xml alike.
2026-08-09 15:28:48 +00:00