The picker lists .odt and .docx, and the chosen extension decides which exporter runs and which format the document keeps — no silent switch to .odt for a file that came in as .docx. Both save paths now build their bytes through one helper instead of repeating the argument list.
28 KiB
src/lib/export/
odt.ts (ODF), docx.ts (Word), pdf.ts (html2canvas + jsPDF), saveFile.ts.
Frames, tables and pagination have their own deep-dives in docs/architecture/.
A password set for the document is applied in saveFile.ts's two write primitives, after
every pass here and after the template conversion — see docs/architecture/encryption.md.
Save As offers both document formats in one picker (saveAsDocument), like the template
one: the bytes are built only once the extension is known, and that extension is what the
document round-trips as from then on. Without a picker it keeps the format it had.
saveFile.ts picks the path: showSaveFilePicker where there is one, else a download —
Gecko has none and Brave disables the whole File System Access API by default. The first
fallback download shows a one-time hint (edentext-download-hint) that the browser's
"always ask where to save" setting picks the location.
Heading defaults (HEADING_STYLE_OVERRIDES, styles/headings.ts)
- Headings follow LibreOffice too (
HEADING_STYLE_OVERRIDES): 18 / 16 / 14 / 13 / 12 / 12pt (levels 4 and 6 italic) with the Heading style's 0.423cm/0.212cm margins on every level, bold, and sans (HEADING_FONT= Arial; on screen the bundled@font-facemaps it to Liberation Sans, metric-identical, mirroring Liberation Serif → Times New Roman for the body). Levels 7–10 continue level 6 at 12pt (alternating italic): probed, LibreOffice writes those styles with no properties at all and resolves them from its own pool, so there is no file value to follow. HTML stops ath6, so 7–10 render as unknown elements thateditor.cssgivesdisplay: block. Levels 1–10 (MAX_HEADING_LEVEL=HEADING_STYLE_OVERRIDES.length); both constants live instyles/headings.tsand feed the importers, the TOC, the DOCX heading styles,utils/fontSize.ts, andeditor.css(kept in sync bytests/unit/font-size-display.test.ts). The importers no longer override a file's own heading formatting: they only suppress values that equal these defaults, exactly as for body text.
ODF export (odt.ts)
buildOdt(json, margins, orientation) (the DOM-free pipeline returning the .odt bytes; App.svelte writes them via the File System Access API or a download — see export/saveFile.ts) calls tiptapToOdt(json, opts) from odf-kit, then runs several content.xml / styles.xml post-processing passes (unzip → string-replace → re-zip via rezipOdt). The ~30 intermediate archives are stored, not deflated — only the next pass's unzipSync ever reads them, and deflating every entry per pass made the pipeline ~10× slower (measured 853ms → 87ms on a 3-image document). zipFinal produces the returned .odt: mimetype first and stored (ODF requirement), already-compressed picture formats (png/jpg/gif/webp) stored, the rest deflated once. odf-kit alone can't express everything the editor supports, so:
-
Custom node types (
injectCustomTypes): paragraphs/headings carryinglineHeight/textAlign/spacing attrs — or runs with afontWeighttextStyle attr, which odf-kit's native path drops — are renamed to__cust_p__/__cust_h__and emitted via odf-kit'sunknownNodeHandler(odf-kit ignores those node attrs otherwise). Tables are renamed to__cust_table__and built byexportTablewith an explicit cell border (odf-kit's native table path emits no borders → invisible in LibreOffice/Word). -
Hard breaks & tabs (
replaceHardBreaks/replaceTabs/applyInlineSentinels):hardBreaknodes becomeLBR-sentinel text and tab chars becomeTAB-sentinel text so both survive every odf-kit path (a literal\twould collapse to a space), then the sentinels are rewritten to<text:line-break/>/<text:tab/>in content.xml. -
Images (
replaceImages/applyImages): odf-kit's native image path rounds dimensions to whole cm and doesn't reach our cell path, so images bypass it.replaceImages(a pre-pass) swaps eachimagenode for anIMG-sentinel text run and collects the bytes (decoded data-URI) + cm geometry (px→cm viaround3); the sentinel rides every odf-kit path including table cells.applyImages(afterapplyCellBlocks) rewrites each sentinel to a<draw:frame text:anchor-type="as-char" svg:width/height><draw:image xlink:href="Pictures/imageN.png"/></draw:frame>, adds the binaryPictures/entries to the zip, and appends a<manifest:file-entry>per picture toMETA-INF/manifest.xml. Integer px ⇄ round3-cm ⇄ px is sub-pixel exact, so size is unchanged. A rotated image also gets adraw:transform="rotate(<rad>) translate(...)"(ODF rotate is CCW radians, ours CW degrees; the translate re-centres it on the unrotated box). A floating image (wrap !== 'inline') instead usestext:anchor-type="paragraph"+ a minted<style:style style:family="graphic">carryingstyle:wrap+style:horizontal-pos(image-left →wrap="right" horizontal-pos="left"; topBottom →wrap="none" horizontal-pos="center"; through →wrap="run-through"+style:run-through) injected into content.xml automatic-styles. -
Tables: column widths come from the editor's per-column weights (
tableColumnWidthsCm, summing exactly to the text width, withtable:align="margins"). Table margins: a table'smarginLeft/marginRightshrink that width and are written byapplyTableMarginsinto the table's automatic style (style:width+fo:margin-left/-right; odf-kit names table stylesTable1,Table2, … in document order, matching the descriptor listexportTablefills). odf-kit serializes each cell to a single<text:p>of runs;exportTableemits all of a cell's content into that paragraph separated by aSEGsentinel and records aCellBlock[]descriptor, thenapplyCellBlockssplits onSEGand rebuilds real<text:h>/<text:p>/<text:list>(incl. nested lists) with minted automatic paragraph styles. Merged cells:exportTablepasses each cell'scolspan/rowspanas odf-kitCellOptions.colSpan/rowSpan; odf-kit emitstable:number-columns/rows-spannedand auto-generates the<table:covered-table-cell>placeholders (which never match theapplyCellBlockscell regex, socellBlocksstays one-per-real-cell aligned). Import already reads spans/covered cells inconvertTable. Cell shading: a cell'sbackgroundColorattr is passed asCellOptions.backgroundColor(normalized), which odf-kit mints into the cell style asfo:background-color. Cell borders: the per-side border attrs (tableCellBorders.ts) are passed asCellOptions.borderTop/Right/Bottom/Left('none'or'<W>pt solid #RRGGBB') overriding the table-level default border per side. -
Text boxes / shapes (
replaceTextBoxes/applyTextBoxes): a box is inline, so it becomes two things.replaceTextBoxes(afterreplacePageBreaks, before the inline passes) recurses over every node's content likereplaceImagesand swaps each box for aTBX-sentinelP{i}run where it sits in its paragraph — which rides every odf-kit path, table cells included — while its own blocks are staged at top level betweenS{i}…E{i}marker paragraphs (U+E008), so every existing pass serializes them unchanged (custom attrs, list styles, inline sentinels, images) and document-order-indexed passes stay aligned. The staging is only a place to serialize in: where a box sits and where its content is written out are no longer the same spot, which is what lets a box in a cell work with no change to the cell machinery.applyTextBoxes(afterapplyImages) cuts eachS…Eregion out, keeps the serialized blocks, and writes<draw:frame><draw:text-box fo:min-height>(plain box;svg:heightalso emitted for non-auto-grow consumers) or<draw:custom-shape>+ preset<draw:enhanced-geometry>(utils/shapes.ts; seedocs/architecture/frames.md) over theP{i}sentinel, in whatever<text:p>the box really sits in. It mints aTbxFr{n}graphic style (fill/stroke/padding + the image wrap props;draw:auto-grow-height="true"for boxes, both auto-grows explicitly false for shapes or LibreOffice's autofit shrinks them to their text). A plain box's style chains tostyle:parent-style-name="Frame"— that parent is what makes LibreOffice read it as a Writer text frame; without it it is a drawing object whose text loses lists, nested images and borders (probed on a re-save). It is also why an as-char box's style spells outstyle:vertical-pos="top" style:vertical-rel="baseline": under that parent a frame naming no vertical position hangs below the line, where a style-less as-char frame (an image) stands on it. Frames takefo:background-color(written besidedraw:fill) andfo:border—draw:strokeis ignored there — and vertical text asstyle:writing-modein the graphic properties; a shape instead needs<style:paragraph-properties style:writing-mode="tb-rl"/>(both probed).rewriteStylesXmldeclares a minimalFramebase style (zeroed margins/padding/border, or the built-in leaks 2mm margins and a hairline). An in-line box has no horizontal position of its own — the paragraph it sits in aligns it — so nothing is minted for that; a band-wrapped one carrieswrapAlignin its graphic style. DOCX is the same pair:inlineToRunsdrops a marker run where the box sits, andapplyTextBoxesDocxswaps only that run, leaving the paragraph and the text beside it alone. -
A text box's own parts, DOCX (
applyTextBoxesDocx): thedocxpackage can't emit a DrawingML shape, so the box XML is a hand-built string — and its content needs parts the package never hears about. The pass holds the whole unzipped package, so it mints them itself: a picture becomesword/media/tbx{n}.{ext}plus aRelationshipabove the package's highestrId(Content_Types already declares the raster defaults), a list an<w:abstractNum>/<w:num>pair above its highest id, appended before the first<w:num>because every abstract definition has to precede them. The numbering ids only exist once the packed file is in hand, so the paragraphs carry aTXBX_NUMsentinel that the same pass substitutes. -
Templates (
template.ts): a.ott/.dotxis the built document with one label changed — ODF's package media type (inmimetypeand in the manifest, the mimetype entry staying first and stored) and OOXML's content type for/word/document.xml.saveAsTemplateoffers both in one picker, so the bytes are only built once the extension is known. -
Formulas (
replaceFormulas/applyFormulas): anMTH-sentinel run becomes an as-char<draw:frame><draw:object xlink:href="./Formula{n}"/>plus aFormula{n}/content.xmlsub-document (MathML root) and its two manifest entries. Nosvg:width/svg:height— a sized frame makes LibreOffice scale the object instead of typesetting it. DOCX emits<m:oMath>(or<m:oMathPara>when displayed) from the same post-pack pass as the text boxes. Seedocs/architecture/formulas.md. -
Right-to-left pages: ODF gets
style:writing-mode="rl-tb"on the page layout (rewriteStylesXml), DOCX a<w:bidi/>in everyw:sectPrfrom a post-pack pass (applyBidiDocx) — the package exposes only the paragraph-level flag. A paragraph's own direction (textDirection.ts) rides that same flag in DOCX and theParaStyle/PBX-spec pipe in ODF, so it reaches cells and list items as well as top-level blocks. -
Mirror margins (
applyMirrorMarginsDocx,export/docx.ts): thedocxpackage writes only the per-sectionw:pgMar, so Word's document-levelw:mirrorMarginsgoes intoword/settings.xmlin the same post-pack pass; ODF's flag ridesrewriteStylesXml. Both formats already hold left/right as the inner/outer pair, so only the flag has to travel. -
Bookmarks & cross-references (
replaceBookmarks/applyBookmarks): theBMS/BME/XRFsentinels are spliced into the run text rather than emitted by a run builder, so they ride every odf-kit path (cells, custom paragraphs, lists) with no per-emitter change; one post-pass rewrites them to<text:bookmark-start/>/<text:bookmark-end/>/<text:bookmark-ref>. DOCX needs no pass at all — thedocxpackage hasBookmark,SimpleField(REF/PAGEREF) andInternalHyperlink. Seedocs/architecture/formatting.md. -
Captions (
replaceSequenceFields/applySequenceFields): aSEQsentinel becomes a<text:sequence>carrying LibreOffice's owntext:formula="ooow:<name>+1"(what makes its reader recount on load) plus a<text:sequence-decl>per counter used; DOCX gets aSimpleFieldSEQ Figure \* ARABIC. Both keep the editor's resolved rank as the field's cached result. -
Tracked changes (
replaceRevisions/applyRevisions): ODF splits the pair. An insertion keeps its text inline and is bracketed byTCIsentinels →<text:change-start/>/<text:change-end/>; a deletion's text is cut out of the run and left as oneTCD→<text:change/>, the text itself going into the<text:tracked-changes>registryapplyRevisionsbuilds right after<office:text>(a<text:deletion>holds the deleted paragraphs, a<text:insertion>only its<office:change-info>). The registry's<dc:creator>/<dc:date>needensureDcNamespace— odf-kit declares nodc:prefix in content.xml. The registry also carries whether recording goes on (text:track-changes), so it is written for that alone when a document records but holds no change yet — LibreOffice writes the same empty element (probed). Word keeps that flag in settings.xml instead, which thedocxpackage exposes asfeatures.trackRevisions(its schema quote names the element). DOCX needs no string pass:InsertedTextRun/DeletedTextRunfrom thedocxpackage emitw:ins/w:del+w:delText, withdocRevisionIdnumbering them per document as it does comments. -
Indexes, DOCX (
indexFieldParagraphs): TOC/INDEX/BIBLIOGRAPHY are complex fields whose result is the editor's cached rows (indent per level, leader tab, pages) — Word for Mac never updates fields on open, so a result-less field rendered as no index at all.w:updateFieldsstill asks a capable reader to regenerate + hyperlink; the importer skips a field's own paragraphs and the node view rebuilds the rows live. A row carriesw:pStylewhere the index names a level style (levelStyles,import/CLAUDE.md) —usedStyleNamescounts those names, so the styles reach styles.xml — and only otherwise the 0.5cm-per-level indent. -
Bibliography (
replaceBibEntries/applyBibEntries,applyBibliographyDocx): aBIBsentinel becomes a<text:bibliography-mark>whose attributes are the whole source record and which wraps the text the citation shows (unlike an index mark, which is a point). The<text:bibliography>index writes an entry template per type actually cited, spelling out the same "key: author, title, year" the node view prints — LibreOffice regenerates the rows from it, so a template that said anything else would rewrite them. DOCX cites by tag (CITATION "key") and keeps the sources in a custom-XML part thedocxpackage knows nothing about, so the post-pack pass mints all four pieces itself:customXml/item1.xml(b:Sources), itsitemProps1.xml, both relationships and the properties part's content type. (Probed: LibreOffice's own DOCX export writes theCITATIONfield but no sources at all.) -
Ruby (
replaceRuby/applyRuby,applyRubyDocx): anRBYsentinel becomes a<text:ruby>holding both halves plus one mintedstyle:family="ruby"automatic style (centred, above — probed: LibreOffice writes that style straight back out, adding only its ownloext:mirror of the position). Word's is a<w:ruby>run thedocxpackage cannot emit, so a post-pack pass swaps the sentinel run for it; thew:rubyPrsizes are half-points of the 12pt body default, which is what LibreOffice's own export writes. -
Cell formulas (
applyCellFormulas,formulaCellContent): ODF puts the formula on the cell, so the pass walks<table:table-cellin the same document orderapplyCellBlocksdoes and writestable:formula="ooow:…"+ the cachedoffice:value. Theooow:is a namespace prefix, soensureOoowNamespacedeclares it — undeclared, LibreOffice reads it as part of the formula. Word instead keeps a field inside the cell, so the DOCX side swaps the cell's first paragraph for oneSimpleField— and the cell's number format rides that field's\#switch (\@for a date), where ODF puts it on the cell style (a minted data style plus a copy of the shared cell style that references it). The data style names the document's language, or LibreOffice prints its own locale's separators and currency symbol. The\#picture is written with that language's separators too (cellFormatCode): Word reads a picture with the reader's regional settings, so a German Word takes"0.00"'s dot as grouping and prints 84 as "084"; the importer reads a picture back by shape, whichever locale wrote it. Seedocs/architecture/tables.md. -
applyListItemStyles— per-item alignment/spacing/line-height for top-level lists (odf-kit's ListBuilder has no per-item options) via minted automatic styles. -
applyListIndents— a top-level list's whole-listindent(cm) added to itsL#list-style per-levelfo:margin-left/text:list-tab-stop-position(label-alignment mode ignores the paragraph margin, so the shift must live in the list style). Import reverses it inconvertList(level-1 margin minus the 1.27cm base). -
applyNestedListTypes— odf-kit emits nested lists as bare<text:list>sharing the top-level list style, so a nested list of a different kind/format (e.g. ordered inside bullets) gets its own minted 6-level list style. -
applyListStyleNames— a list carrying an unoverriddenlistStyleNameis repointed from itsL#automatic style to the named<text:list-style>applyNamedStyleswrites into styles.xml (its nested lists stay bare and inherit); runs after everyL#-keyed pass. The list collectors resolve attrs througheffectiveListLevel, so an overridden styled list keeps a fully resolvedL#clone;applyListLevelKindsruns first and switches anL#level element's species where the style's level kind differs from the node's (a<ul>depth the style numbers), format/char baked in — seesrc/lib/styles/CLAUDE.md. DOCX:Numbering.refForStyleshares one abstract per style (each list its ownw:numinstance, so numbering restarts) andapplyListStylesDocxinjects thew:styleLink+ thew:type="numbering"style's real numId post-pack — the library can express neither. A list with astartattr keeps a private clone (the shared abstract starts every instance at the style's start). Probed: LibreOffice ignores DOCX numbering styles both ways (its import mintsWWNum#, its export flattens to the look) — the identity is a Word capability; ODF carries it in both products. -
applyListStartValues— odf-kit drops an ordered list'sstartattr; a Word list continued across an intervening paragraph (same numId, split into separateorderedListnodes withstart> 1 by the importer) getstext:start-valueon its first<text:list-item>so numbering keeps counting instead of restarting at 1. -
applyTableRowHeights— dragged row heights →style:min-row-heightautomatic table-row styles. -
collapseRunWhitespace— strips the bare\nseparators odf-kit inserts between runs inside a paragraph (they'd otherwise collapse to spurious spaces mid-word). -
rewriteStylesXml—style:page-usage="mirrored"on the page layout when the margins mirror (the left/right pair is already the inner/outer one), default font Liberation Serif → Times New Roman, heading sizes/margins → the editor's values, Standard'sfo:margin-bottom→ 0 (odf-kit emits 0.212cm; every paragraph and list item inherits it, and the editor has no paragraph spacing — see below). -
normalizeColor— coerces colors to#RRGGBB(ODF requirement; rejects/normalizesrgb()and short hex). -
Schema conformance (guarded by
tests/schema-validation.test.ts; LibreOffice forgives all of this, Word's strict reader does not):applyOdfVersionstamps every ODF part 1.3 — the version LibreOffice writes, and the first withstyle:header-first— over odf-kit's 1.2.draw:imagecarries thexlink:type/show/actuatetrio (xlink:typeis mandatory besidexlink:href);text:time-valueis an xsd dateTime, never aPT…Sduration (the ODT importer still reads the legacy duration);index-entry-link-start/-endonly in TOC entry templates;text:dont-balance-text-columnsonstyle:section-properties. DOCX:orderDocxSettings(last pass) re-sortsw:settingschildren into the fixed CT_Settings sequence the prepend-passes scramble;w14:paraIdin comments.xml requiresmc:Ignorable="w14"on the root.tests/package-lint.test.tsguards the invariants the schemas cannot express (unique style ids, no dangling style/num/rel references, balanced ranges, manifest completeness). -
A section past the first spells its zones out, blank ones included. A
w:sectPrnaming now:headerReferenceis Word's "Link to Previous" and repeats the section above it — measured: a chapter's running head landed on the pages a blank section was meant for. Both zones write an empty part rather than none (spellOut,mkHeaders). -
A box spanning the text column keeps where it sits across it, banded or behind the text alike; only a side wrap has its side dictated by the wrap itself.
-
An index row's tab stop is written even with no page number running to it — it is where the row's leader is kept, and a stop no tab reaches draws nothing.
-
The note configuration is written whether or not a note exists (
applyNotePrDocx, outside thedocNoteIds.sizegate, as Word keeps its own in settings.xml): a document numbering its first footnote from 3 must still say so. -
A note writes the paragraph style it names (
noteStyleName, andusedStyleNamesdefines it): the stockFootnoteTextis 10pt, so a note whose file gives it the body size would shrink. Seedocs/architecture/notes.md. -
A heading writes its own
w:outlineLvlbeside itsw:pStyle: the style carries the look, the level says it is a heading at all, and a document setting its chapters in a style of its own leaves the importer nothing else to go by. -
SVG is rasterized on the way into a .docx (
rasterizeSvgImages, before the walk — the emitters that place pictures are synchronous): Word reads no SVG, and an EMF arrives as one too, so a vector picture is drawn to PNG at 2× its placed size. Off the main thread there is no canvas and the picture is skipped, as it always was. -
Factory style slots (
FACTORY_SLOTS,buildStyles): thedocxpackage always writes its own Title/Heading1–6 into styles.xml, so the registry's versions ridestyles.default.title/headingNinstead ofparagraphStyles— a second definition under the samew:styleIdmakes Word and LibreOffice drop thebasedOnchain (headings lose their sans/bold). Table and numbering styles are spliced post-pack (applyRawStylesDocx): passing them asimportedStylesmakes the package's Styles merge replace its whole factory set,w:docDefaults(default font/size/language) included.
The filename is derived from the first non-empty heading (max 50 chars, sanitized), falling back to document.odt.
Printing the review markup (reviewPrint.ts)
A printed document says what the screen says: a bar in the margin beside every changed or
commented block, and the comment bodies as a list after the document. The list is not a
nicety — a comment's text is nowhere on the page, so a bar alone could only say that one
exists. A resolved comment prints nowhere, and a document with no markup prints exactly as
before (no list, no CSS, no margin shift). The Review tab's Print markup toggle
(storage/printMarkup.svelte.ts) turns the whole thing off, as both products offer;
printPdf rebuilds its options field by field, so a new one has to be copied there too.
The display mode prints too (storage/markup, Word's "Display for review"): a page in
no markup prints as it reads. The two paths that rasterise the live .paper inherit its
attributes with the clone; printPdf builds its page from the document instead, so it
takes them as markupAttrs. printMarkup still governs the bar and the list beside it.
- The margin strip is paid for. The print engine clips to the page area, so a bar at a
negative offset is silently dropped — measured, not assumed.
printPdftakesBAR_STRIP_CMout of the left@pagemargin and gives it back as.paperpadding, which leaves the text column exactly where it was. - The two paths differ in resolution. The raster paths (
exportPdf,printRaster) clone the live.paper, so they getChangeBarLayer's own per-range bars for free —buildCloneonly drops the.activeweight, an editing state.printPdfrebuilds fromgenerateHTML, where no layout exists yet:markReviewBlocksmarks whole top-level blocks (CSS has no line box to hang a bar on, and only a top-level block's left edge is the text column's), so a long paragraph with a small change is marked whole. - An entry names its page, which with the quoted text is what makes it findable in a
printout nobody can click — the format LibreOffice prints its own end-of-document
comments in. Only the raster paths can say it (
withAnchorPages): they print the editor's own pagination, while the vector path re-paginates in the browser and its entry simply has no page. The page comes off onecycle, so mixed section paper is out — the same limitationEditor.svelte's own current-page readout has. - The list is a page, not a flow, in the raster paths. The live
.paperis an already paginated layout nothing can flow into, sorenderCommentPageslays the list out and rasters it on its own; a page starts at an entry boundary, since a raster sliced mid-line cuts the text in half. One entry taller than a page is clipped. The vector path just appends the section next to.tiptap— a sibling, or the editor's own heading and list rules would restyle it — and lets the browser paginate.
Header/footer export
- Export: a synthetic
__cust_hf__node routes tounknownNodeHandler, which calls odf-kit'ssetHeader/setFooter(applyHfRuns);hardBreak→LBR,pageNumber→addPageNumber,pageCount→PGCsentinel. A zone image is an as-char<draw:frame>, or — floating — a page-anchored one plus a mintedHfBg*graphic style (run-throughbehind the text, positioned from the page corner).applyHfPostProcess(on styles.xml) rewrites the sentinels to<text:line-break/>/<text:page-count>, applies the paragraph alignment to theHeader/Footerstyles, and converts geometry to Word's model: page margin = the zone's edge distance (headerDistanceCm/footerDistanceCm), header/footermin-height= body margin − distance, so the body still starts at the editor's margin.
Sentinel order matters — the passes assume it. Full list of sentinels in use:
LBR/TAB (inline), IMG, SEG (cell blocks), TBX U+E008 (text boxes),
U+E009 (columns), U+E00A (date/time fields), STY U+E00D (paragraph styles),
CST U+E00E (character styles), TEF U+E00F (text effects), PBX (paragraph boxes), SEC U+E010
(section breaks), LEAD U+E011 (a tab stop's leader, inside style:type), MTH U+E012 (formulas),
BMS/BME U+E013/U+E014 (a bookmark's range), XRF U+E015 (cross-references),
CHP U+E016 (the chapter field), FNT U+E017 (footnotes/endnotes),
CMS/CME U+E018/U+E019 (a comment's range), SEQ U+E01A (a caption's number),
TCI U+E01B (a tracked insertion's range), TCD U+E01C (where a tracked deletion was),
TXBX_NUM U+E01D (DOCX only: a numbering id a box's list has yet to be given),
IXE U+E01E (an alphabetical-index entry), BIB U+E01F (a citation),
NOHYP U+E020 (DOCX only: a paragraph whose w:pPr must gain w:suppressAutoHyphens),
RBY U+E021 (a ruby annotation), PLH U+E022 (a placeholder field — ODF
<text:placeholder>, DOCX a w:sdt content control tagged edentext-placeholder).