Commit Graph
74 Commits
Author SHA1 Message Date
Steffen Becker e2525237de feat: named list styles — model, editor resolution, ODF round-trip
Fourth style family (LibreOffice's Listenformatvorlagen): per-level model in
styles/listStyles.ts, five probed LO built-ins, listStyleName on the outermost
list, one effective-level resolver shared by the decoration walk and the export.
ODF writes the full named text:list-style into styles.xml and repoints
unoverridden lists at it (overrides keep the resolved automatic clone);
import maps named list styles back to the attr + registry, attr-free.
Probed: LO keeps the named reference and display-name across a re-save.
2026-08-25 20:38:15 +00:00
Steffen Becker 0753caa5a9 feat: placeholder fields (inline atom + ODF/DOCX round trip)
An inline atom drawn gray as ‹label›: a click selects the whole field,
typing replaces it, Tab/Shift-Tab jump between fields. Exports to ODF
<text:placeholder> (PLH sentinel U+E022) and a DOCX w:sdt content
control tagged edentext-placeholder; both importers read it back, the
export-minted gray stripped. Covered in roundtrip Leg 31 + fuzzDoc.
2026-08-24 16:16:24 +00:00
Steffen Becker a87e7a4ada fix: a heading's style name never lands on a plain paragraph
styleName was kept on split, so Enter at the end of a heading left a paragraph
carrying it: drawn like a heading, absent from the navigator's outline, exported
as text:p. It is no longer kept there. setParagraphStyle also missed the name
whenever it switched the node type — a following updateAttributes looks for the
new type in the pre-chain state — so it now rides along with setNode. Levels 2–6
never reached that command at all: the digit class built from MAX_HEADING_LEVEL
(10) read as [0-1], so Heading's own Mod-Alt-N took the key and set a level with
no style; that keymap is dropped, running ahead of every plugin binding.
2026-08-19 18:41:21 +02:00
Steffen Becker 8b1bef9706 fix: paragraph styles apply to every block, not just the first
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.
2026-08-16 02:18:55 +02:00
Steffen Becker 20e6dcad0e feat: a reading can be printed over its base text
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.
2026-08-15 10:40:52 +02:00
Steffen Becker 405b175e49 feat: AutoText keeps a block of text under a shortcut
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.
2026-08-15 10:33:16 +02:00
Steffen Becker ee801967f6 feat: words typed once are offered back
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.
2026-08-15 10:27:18 +02:00
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 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 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 eaaec541af feat: an alphabetical index, marked word by word
References ▸ Index entry marks a place with the term the index should
list — under a key of its own where it belongs elsewhere — and the Index
in the same menu lists each term once, sorted case-insensitively, with
every page it was marked on.

Both formats write a place, not a range, so the entry is an inline atom:
ODF text:alphabetical-index-mark plus text:alphabetical-index, Word XE
plus INDEX. LibreOffice reads our file back unchanged. The bibliography,
the other half of that gap, is still open.
2026-08-14 08:44:00 +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 5ee4eec5d7 feat: the editor records, shows and applies tracked changes
Two marks carry a revision with its author and date: an insertion is
marked text, a deletion is text kept and struck through. Consecutive
typing joins one change; accept/reject work at the cursor or on all.

ODF splits the pair — text:change-start/-end around an insertion, a
text:change where a deletion was, its text in the tracked-changes
registry. DOCX uses w:ins/w:del with w:delText. Both round-trip, and
accepting every change gives the document an unrecorded edit would.
2026-08-14 07:48:10 +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 55f27da855 feat: a single paragraph can run right to left
The block's own direction, set in the Paragraph dialog where LibreOffice
keeps it, so a quoted Hebrew or Arabic paragraph turns inside a left-to-right
document. Rides ODF style:writing-mode / Word w:bidi through cells and list
items as well as top-level blocks; the direction the page already has is
inheritance, so neither importer stamps it on every block.
2026-08-14 06:04:12 +02:00
Steffen Becker b2d1ddfb66 feat: comments, with a reviewing pane
The comment is a mark carrying its own author, date, body and resolved
flag — the bookmark pattern, so no second store and no schema node. The
pane lists them in document order and jumps to the text each marks.
ODF gets office:annotation + office:annotation-end and LibreOffice's
loext:resolved, DOCX the range pair plus word/comments.xml; a point
comment, which LibreOffice writes without a range, attaches to the run
before it. Ctrl+M no longer strips a comment, a bookmark or a link.
2026-08-14 01:23:54 +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 71c7b53281 feat: heading level 6, with LibreOffice's italic on 4 and 6
LibreOffice's style pool, probed: Heading 6 is 12pt bold italic and
Heading 4 italic too. MAX_HEADING_LEVEL drives the extension registry,
the shortcut plugin, the TOC depth and both importers' yardstick, so
only the constant table and the CSS selector lists had to grow.
Bookmarks/cross-references move to Implemented — they already are.
2026-08-14 00:38:49 +02:00
Steffen Becker 0c74fe94b1 feat: footnotes and endnotes can be inserted and edited
The anchor is an inline atom, the note text a block in the one noteSection
at the document end; a sync plugin keeps notes in anchor order, renumbers
them per class and drops the ones whose anchor is gone. Numbering, indent,
size and separator defaults are LibreOffice's, read out of a document it
saved. Endnotes still render in flow at the document end — lifting a
footnote to the foot of its page is the next step.
2026-08-13 09:22:17 +00:00
Steffen Becker a021bc42aa feat: a cell's content sits where its style puts it
ODF style:vertical-align (middle/bottom) on a table cell, which every
LibreOffice callout table uses for its Tip/Note label. Import and render
only: odf-kit dedupes cell styles by property, so writing it back needs a
pass of its own — noted in docs/architecture/tables.md.
2026-08-10 11:03:34 +00:00
Steffen Becker 78e35cef2c feat: a reference finds the text it points at, and follows it
Bookmarks are a mark, not a point: both formats store a range, and only a
range lets a REF recompute its text. A cross-reference caches its display
like a Word field and resolves live on each pagination settle — the
bookmark's text, or the page it renders on — keeping the cached value when
the target is gone. Ctrl+click jumps to it, internal #name links too.

ODF splices its sentinels into the run text so cells and custom paragraphs
need no separate emitter; DOCX needs no post-pass at all.
2026-08-09 13:56:22 +00:00
Steffen Becker ee56a348ee feat: formulas can be inserted and edited, and survive both formats
A formula node stores only its LaTeX source; the MathML on screen, the ODF
formula object and Word's OMML are derived from it, and both importers parse
foreign markup back into that source, so an imported formula stays editable.

Word documents were losing their formulas outright: m:oMath sits beside the w:r
runs in its own namespace, past convertInline's `namespaceURI !== W` guard.
2026-08-08 21:50:36 +00:00
Steffen Becker db0fe71cdb feat: a table keeps the cell margins its file gave it
The cell padding was a constant in three places that disagreed with each
other, so a narrow column lost 0.38cm it did not have: a bibliography
marker column of 0.71cm wrapped "[2] " onto three lines and every entry
grew with it, costing a fixture a whole page.

The table now carries its own margins (w:tblCellMar, direct or from the
table style; fo:padding on the ODF cell style) and both exports write
them back. A side within a twentieth of a millimetre of the default is
the default, or Word's 108 twips would tag every table.
2026-08-08 12:18:16 +00:00
Steffen Becker b61fadca48 feat: the character effects Word and LO carry beyond bold and italic
Letter case, the underline's shape and colour, a double strikethrough and a
freely raised run all had no home in the model, so every one of them was
dropped on import. They ride the marks now, render in CSS, and round-trip.

odf-kit's run formatting has a field for none of them, so a sentinel folds
them into the run's own automatic style — the route the character styles
already take, and it reaches every path odf-kit serializes.
2026-08-08 10:06:31 +00:00
Steffen Becker be96ee626c docs: apply rules for comments 2026-08-08 08:28:55 +00:00
Steffen Becker a8a4a4c32f feat: let a character style on the item's first portion reach the marker
The charStyle mark carries only a name, so resolving it needs the registry: the
plugin takes it as a `sheet` option (like ParagraphStyle), the exporters read
their own exportSheet. The style is the base, the portion's own marks stay on
top. The decoration pass now also re-runs on FORCE_PAGE_RECALC, so editing a
style repaints the markers.

The ODF marker style moves to styles.xml as a named style — LibreOffice ignores
a level's text:style-name when it resolves to an automatic style, which made the
whole ODF leg a no-op (probed both ways).
2026-08-07 20:27:07 +00:00
Steffen Becker 0b54f6cc44 feat: render a list marker bold when the item starts bold
A bold mark sits inside the item's paragraph and never reaches ::marker, so the
roman numerals of a numbered heading stayed light while their text was bold.
A plugin tags such items and editor.css weights the marker. The trigger is the
first text portion, which is the rule LibreOffice numbering itself uses.
2026-08-07 17:41:09 +00:00
Steffen Becker 7c09727cda docs: remove word as placeholder for general text editor 2026-08-07 18:08:20 +02:00
Steffen Becker 92d641f72f feat: honour a paragraph's own tab stops
CSS only has the fixed tab-size grid, so a tab that resolves to one of the
paragraph's stops is measured and given the exact advance as an inline margin.
A hanging indent implies a stop at the text position, as in Word.
2026-08-06 21:45:47 +00:00
Steffen Becker 1ed9f3d172 feat: table-styles 2026-08-05 19:01:25 +02:00
Steffen Becker 9483d598fc feat: keyboard shortcuts 2026-07-31 20:43:48 +02:00
Steffen Becker 3dcae6f640 feat: named character styles 2026-07-30 23:34:23 +02:00
Steffen Becker 603f4df905 feat: paragraph style manager ui 2026-07-30 19:58:47 +02:00
Steffen Becker ac9d6cdcff feat: named paragraph styles with inheritance 2026-07-28 23:53:00 +02:00
Steffen Becker 56f884a412 feat: dropdown for text styles 2026-07-27 20:35:55 +02:00
Steffen Becker f1ba14c948 feat: add paragraph frames and background 2026-07-19 16:34:23 +02:00
Steffen Becker 1fb81c389f feat: insert date/time 2026-07-08 23:21:07 +02:00
Steffen Becker de283adab5 feat: introduce columns 2026-07-08 19:17:16 +00:00
Steffen Becker 3b7e7606bb feat: bullet symbols 2026-07-04 17:14:54 +02:00
Steffen Becker 15b9386eb7 feat: add table frame menue 2026-07-04 00:00:44 +02:00
Steffen Becker 0aa9399f93 feat: textbox and shapes 2026-07-03 22:53:38 +02:00
Steffen Becker 86376ca7e3 fix: import correct font size of empty lines 2026-07-02 23:10:02 +02:00
Steffen Becker a5bb2deaa2 feat: table of content 2026-07-01 21:17:44 +02:00
Steffen Becker 418f152d4b feat: header row toggle 2026-06-28 12:23:41 +02:00
Steffen Becker 3cc2e65423 feat: table cells background/shading 2026-06-28 10:59:22 +02:00
Steffen Becker ec678ed495 feat: table cell splitting/merging 2026-06-28 10:30:00 +02:00
Steffen Becker 3b299809e1 feat: search and replace 2026-06-28 00:05:37 +02:00
Steffen Becker 98e7ee2458 feat: add hyperlinks 2026-06-27 19:55:56 +02:00
Steffen Becker f6d1348a43 feat: manual pagebreaks 2026-06-25 20:49:07 +02:00
Steffen Becker 8acae13b9a refactor: organize lib/editor 2026-06-24 23:34:24 +02:00