Word documents
Table of Contents
The docx plugin of getkalem/plugins opens Word documents as
themselves: the file is a package of WordprocessingML parts, and the
plugin reads, edits and writes those parts, never a conversion of
them. Kalem lays the paragraphs out itself, through the plugin API's
flow interface, and reviews the comments and tracked changes through
its annotations interface (see Plugins, "Viewers of flowing text").
The plugin is installed from the plugin list; its README and its work
list (plugins/docx/docx_todo.md) are kept with it.
1. The standard and the oracle
- The standard is ECMA-376 part 1, WordprocessingML, in its
transitional namespace (the strict one is read as it), with the
w14,w15andw16additions Word writes read and kept. - The oracle is Word 2016 and later (Microsoft 365): "as Word writes it" is how each edit is written, element order following the schema's sequences, which Word checks. Word cannot be driven from a script past its file access prompt, so it is checked by hand.
- LibreOffice Writer 7.3 is the second oracle, by hand too:
soffice --convert-to txtand--convert-to docxopen the edited files, show the edits and keep the tracked insertions and deletions with their authors. - The corpus has four files from three producers: python-docx over its default template, a package written part by part with every construct the plugin reads, and each of the two saved again by LibreOffice. The tests check that each saves back byte for byte, and what each edit changes.
2. Which files
.docx, .docm, .dotx and .dotm. A file with one of these
extensions that starts as a ZIP archive is the plugin's at once; one
that does not is tried as a document encrypted with a password to
open, which Word writes as a compound file. A file of another
extension is not the plugin's whatever it holds.
3. What is read
- The package through its relationships, never by a part's name: the body, the headers and footers, the footnotes and endnotes, the comments, the styles, the numbering, the settings and the theme. Every element is read with its byte span; an element the plugin does not know is kept with its span and shown by name.
- Styles as 17.7.2 puts them together: the document defaults, the
table style and its conditional formats as
w:tblLookturns them on, the numbering level, the paragraph style and itsbasedOnchain, the character style, then direct formatting; the toggle properties combined across style kinds as 17.7.3 says. Theme fonts and colors are resolved, a tint or shade computed as Word computes it. - Lists counted as Word counts them: instances of one abstract
definition counting together,
w:startOverride,w:lvlRestart, legal numbering, levels linked to styles; labels in every common number format, bullets in Symbol and Wingdings as the Unicode characters they draw. - Fields show their results, simple and complex, nested;
HYPERLINKfields andw:hyperlinkare links. Notes are numbered in order of reference asw:footnotePrandw:endnotePrsay. - Comments with their ranges, and tracked changes: insertions, deletions, moves, formatting changes of runs and paragraphs, and paragraph marks inserted or deleted, each with its author and date.
- A password to open ([MS-OFFCRYPTO]): agile encryption, Word 2010's and later's, is read and written, and Word 2007's standard encryption is read. Kalem asks for the password, hidden as typed.
4. What is shown
Word's web layout, without pages: a document of the editor whose lines are the paragraphs, in both editors.
- Each paragraph is a line, drawn with its runs' look: bold, italic, underline, strike, caps and small caps, raised and lowered text, color, highlight and typeface; a size in proportion to the body text's, so a heading is larger than the text around it. A list item has its label before it; a heading is in the outline.
- A tab, a line break, a page break and a column break are one character each in the paragraph's line; a picture, a note's mark and an object are one object character.
- A picture is drawn where it is, as large beside the text as in the
document, and no wider than the text: in a PNG, JPEG, GIF, BMP, TIFF
or WebP file, or an SVG drawing through the PNG Word keeps of it. A
floating picture is drawn in its paragraph's line. In the terminal a
picture alone on its line is drawn over as many rows as it takes when
the terminal draws pictures (kitty's, iTerm2's or sixel graphics) and
the cursor is elsewhere; otherwise, and for a picture in EMF or WMF
or one linked from outside the document, its alternative text shows,
[Picture: …]. - A table row whose cells hold one paragraph each is one line, its
cells apart by tabs and the line marked
▏at its start. A row with more in a cell shows a paragraph a line, the row's first marked▏and each further cell's first▏┆. A nested table's paragraphs are shown in the cell that holds it. - The first section's header comes before the body, marked
Header ▏, and its footer after it, markedFooter ▏. Footnotes and endnotes follow, each marked with its number. A text box follows the paragraph it is anchored in, marked▕. - A separator drawn as shapes (a line, a thin bar across the page,
Word's horizontal line) is a horizontal rule across the text; a line
among text shows as
──. Charts, other shapes, SmartArt, equations and embedded objects are shown by name, as placeholders. - The document's colors were chosen for a white page: on a dark theme, a color that would not read on the background (black text, a dark gray, a dark blue) is shown with its lightness turned over and its hue kept, as a word processor's dark mode shows it, and text on a highlight is drawn to read on the highlight. The file keeps its colors.
- Text a comment is on is highlighted; inserted text is underlined in green and deleted text struck through in red. The status bar says the comment at the cursor with its author, or who made the change there.
5. What is edited
Typing, Enter, Backspace and Delete edit the document as typing in Word does, in the body, a table cell, the header, the footer and the notes:
- Typed text goes into the run before it and takes its look; over a selection it takes the look of the selection's first character. A deletion shortens, empties or removes the runs it covers.
- Enter splits the paragraph, the elements open where it splits with
it; Enter at the end of a paragraph whose style names a next style
(a heading's,
Normal) gives the new paragraph that style. Backspace at a paragraph's start joins it to the one before; a deletion across paragraphs is one edit. - Each of Kalem's edits is one step of the document's own history: Undo and Redo go through it, and undoing everything saves the file as it was opened, byte for byte.
- In a document that tracks changes, typed text is written as an
insertion, deleted text kept as a deletion, and Enter and Backspace
mark the paragraph mark inserted or deleted, each with the author,
the date and an ID of its own. The author is the setting
user.name, else the system account's name.
The commands of the Format menu and the toolbar change the look of the selected text, or of the word at the cursor:
| Command | What it writes |
|---|---|
| Bold, Italic, Underline, Strike Through | w:b and w:bCs, w:i and w:iCs, w:u, w:strike in each run's w:rPr; off again, taken away, and written off (w:val"0", an underline =w:val"none"=) where a style still turns it on |
| Superscript, Subscript | w:vertAlign |
| Font, Font Size | w:rFonts (w:ascii and w:hAnsi, the theme's fonts that would win taken away), w:sz and w:szCs in half points |
| Text Color | w:color; Automatic takes it away |
| Highlight Color | w:highlight for the sixteen colors Word names, else a shading (w:shd) |
| Clear Formatting | The run's direct look taken away; its character style, language and the like kept |
| Paragraph Style | w:pStyle first in the paragraph's w:pPr; none for the default style, as Word writes Normal |
| Align Left, Center, Align Right, Justify | w:jc (left, center, right, both); taken away when the style aligns so already, written where it does not |
| Increase and Decrease Indent | A list item's level (w:ilvl), else w:ind's w:left by half an inch |
| Line Spacing, Space Before, Space After | w:spacing's w:line (auto, in 240ths of a line), w:before and w:after in twentieths of a point |
| Bullets, Numbering, Numbering Style | w:numPr: the list of the paragraph before continued when it is of the kind, else a new one, of the document's definition of the kind or of Word's own (added to numbering.xml, the part made with its relationship and content type when there is none); the paragraphs given the List Paragraph style, as Word does; taken out, w:numId 0 where a style makes them a list |
| Clear Paragraph Formatting | w:jc, w:ind, w:spacing and the like taken away; the style and the list kept |
A run is cut where the range starts or ends inside it; every property
goes where the schema's sequence puts it, since Word repairs a file
whose properties are out of order. In a document that tracks changes,
each run's old properties are kept in a w:rPrChange and each
paragraph's in a w:pPrChange: Accept Change keeps them, Reject
Change undoes a run's (a paragraph's is not rejected yet, and Reject
All Changes leaves it).
The commands of the Review category act on the annotations:
| Command | What it does |
|---|---|
| Next Change, Previous Change | Moves to the next or the previous tracked change |
| Accept Change | Keeps the change at the cursor: an insertion's text stays, a deletion's goes, a deleted paragraph mark joins its paragraphs (in the body only), a formatting change keeps the new look |
| Reject Change | Undoes it: an insertion's text goes, a deletion's text comes back, a run's formatting change restores the old look (a paragraph's is refused for now) |
| Accept All Changes, Reject All Changes | The same for every change, in every part of the document |
| Track Changes | Turns tracking on or off (w:trackRevisions in the settings) |
| Next Comment, Previous Comment | Moves to the next or the previous comment |
| New Comment (Ctrl+Alt+M) | Puts a comment on the selected text, in the body or a note: its range's start and end around the text, a run split where the range starts or ends inside it, a run with its reference after the end |
| Reply to Comment | Answers the comment at the cursor: a comment anchored on the same text and named as its answer; an answer to an answer answers the first comment, as in Word |
| Edit Comment | Changes the text of the comment at the cursor, a paragraph a line; the palette starts with its text. Its author, date and mark stay, and its last paragraph keeps its ID, so its answers and its done mark stay its own |
| Resolve Comment | Marks the thread at the cursor done, or open again: the mark is on its first comment, and its answers show resolved with it |
| Delete Comment | Takes the comment at the cursor away with its answers (an answer alone, when it is one): its range, its reference and the comment; the runs a range split stay split, as in Word |
What is not edited says why when it is tried: deleted text of a
tracked change, a field's result, a note's mark, a check box, text
drawn two ways in the file (mc:AlternateContent), a paragraph of a
text box, an edit across a table's cells, a join across a section
break, and every edit of a document whose protection is enforced,
with accepting and rejecting changes and Track Changes, as Word
refuses them (a document protected for comments takes comments, and
one protected for tracked changes keeps Track Changes on). A comment
in a header or a footer is refused, as Word keeps none there.
6. What is written
Only the paragraphs an edit touched are written again; every other
byte of the part, and every other entry of the ZIP archive with its
local header, is copied as it was. A save without edits returns the
file unchanged. Nothing is written that ECMA-376 does not define: no
rsid invented, no property Word would not write. A comment added
to a document without comments adds the comments part, with its
relationship and content type; an answer adds the extended comments
part (commentsExtended.xml) the same way when there is none, and its
entry names the answered comment's last paragraph by its w14:paraId,
which is given one when it has none. Resolving writes w15:done in
that part, made the same way. Deleting a comment takes its entries out
of the parts Word keeps beside the comments (commentsExtended.xml,
commentsIds.xml, commentsExtensible.xml); the comments part stays
when its last comment goes. A document opened with a
password is saved encrypted again with it.
7. Export and conversion
None. Save As writes the document in its own kind only: a name of
another kind (report.docm for a .docx, report.pdf) is refused
with the plugin's reason, and the document keeps its file.
8. Known differences
- No pages: the layout is Word's web layout, without pages, columns or the page's size.
- Only the first section's header and footer are shown, once.
- Pictures in EMF or WMF, Windows's own drawings, are not drawn; a picture's crop, rotation and effects are not applied.
- Comments are highlighted in the text, not shown in a margin.
- Ordinal and cardinal list labels are in English whatever the document's language.
- LibreOffice drops a paragraph mark inserted as a tracked change when it saves the document again; Word keeps it.
- LibreOffice 7.3 shows an answer as a comment of its own: it reads no threads. Word shows it under the comment it answers.
9. Not implemented
- Section breaks are not marked in the text.
- Character styles given; borders, shading, tabs and keeping lines together edited; pictures, links, tables and notes inserted.
- Text boxes edited.
- Word opening every edited file without a repair prompt, checked by hand.
- Pages and printing.
10. Limits and performance
Measured on 2026-10-09 on an Apple M1 Max, with the plugin's library in
a release build: a document of 20,000 paragraphs (4.4 MB of
document.xml) is opened and laid out as blocks in 0.18 s in 82 MB;
the save takes 7 ms (2026-10-10). The component is 2.0 MB as released
with its picture decoders, its functions' names kept for backtraces. Kalem fetches a document's items
from it 2,048 at a time when it opens one; the document of 20,000
paragraphs opens in Kalem in 0.34 s.
An edit does not walk the document again. The plugin keeps the body as
it walked it, and every sixteen elements of the body (a table is one)
where the walk was (the
lists' counts, the fields and comments open); an edit walks again from
the last such place before it until the walk is where it was before,
usually a few paragraphs on, and the paragraphs after it are those of
before. Kalem then fetches the items the plugin says changed (flow-3,
API 0.2.9) and lays out their lines again. Measured on 2026-10-10 with
the component in Kalem, a character typed in the middle of the
document of 20,000 paragraphs takes 1.4 ms from the keystroke to the
text shown (0.66 s before); Enter 4.6 ms, Backspace joining two
paragraphs 2.3 ms, Undo 3.5 ms.
11. Code and tests
- The plugin:
plugins/docxofgetkalem/plugins:document.rs(the document and its edits),story.rs,styles.rs,numbering.rs,props.rs,flow.rs(the view),edit.rs(runs rewritten),review.rs(changes accepted and rejected),comments.rs(comments added and answered),contract.rs(the flow and its annotations),viewer.rs(the plugin as Kalem's viewer); the Office layer it shares with the workbook plugin incrates/ooxml. - Its tests:
tests/docx.rs,tests/conformance.rs(the corpus saved back byte for byte),tests/comments.rs,tests/viewer.rs,tests/contract.rs; the corpus intests/corpus/, made bymake.py. - Kalem's half: the interfaces in
crates/kalem-plugin/wit/flow.witandannotations.wit, the contract incrates/kalem-viewer/src/flow.rs, the document of the editor incrates/kalem-core/src/flow.rs, the Review commands incrates/kalem-core/src/builtin.rs; its tests incrates/kalem-core/tests/flow.rs,crates/kalem-script/tests/flow.rsand both editors'tests/flow.rs, andcrates/kalem-cli/tests/flow_component.rs, which opens, edits and saves a real document through the plugin's component when given one.