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.
6.2 KiB
Formulas
Insert/edit a mathematical formula (editor/extensions/formula.ts, components/FormulaDialog.svelte),
round-tripping to an embedded ODF formula object and to Word's OMML.
One LaTeX string is the model
The formula node stores latex and a display flag — nothing else. Everything is derived:
latex ──parse──▶ AST ──▶ MathML (screen + .odt)
└──▶ OMML (.docx)
OMML ──parse──▶ AST ──▶ latex (.docx import)
MathML──parse──▶ AST ──▶ latex (.odt import)
Foreign markup is parsed into the AST and re-serialized, never passed through. That is why the
node view may set innerHTML — the MathML it renders is always our own output, so a foreign
annotation-xml can't smuggle in HTML. It also means an imported formula is always editable;
there is no read-only state to fall back to.
The price: a construct outside the AST degrades. Unknown OMML/MathML elements recurse into
their m:e/child rows, so content survives even where structure doesn't — the same trade
LibreOffice makes routing OMML through StarMath.
src/lib/math/: latex.ts (AST types, parser, serializer, symbol table), mathml.ts,
omml.ts. AST kinds: row, txt (i/n/o/f = identifier/number/operator/upright name),
frac, script, sqrt, fence, nary, acc, matrix, space.
The LaTeX subset
\frac, _/^, \sqrt[n]{}, \left…\right, the n-ary operators with limits, the accents,
matrix/pmatrix/bmatrix/Bmatrix/vmatrix/cases, the upright function names, \text{}
/\mathrm{}, greek + the common operators and relations, {} grouping, & / \\, and the
spacing macros. Not supported, deliberately: align environments, \substack, user macros,
color. An unknown macro degrades to \text{name} rather than vanishing.
Two invariants the tests pin (tests/unit/math.test.ts, tests/unit/docx-formula.test.ts):
astToLatex(parseLatex(x)) === xfor every stored source. Without it, opening an imported formula in the dialog and pressing Apply would silently rewrite it.- A literal
^or_in an imported run's text is escaped on the way out (Word documents contain typed-in code likecorr.^2), or re-parsing would read it as a script marker. - Whitespace inside an OMML run is dropped (
omml.tstokenize): math layout ignores it, and keeping it breaks the invariant above.
STIX Two Math is required, not decoration
MathML is typeset by the browser natively — no KaTeX/MathJax. But Chromium needs a font with
an OpenType MATH table: measured in this container with only the bundled Liberation/Carlito
faces installed, ∑ and ϕ render as nothing and ( ) stay glyph-height beside a
fraction. src/assets/fonts/STIXTwoMath-Regular.woff2 (OFL, the Fontsource latin subset —
the MATH table and the operators survive it) is bundled and wired in global.css;
editor.css puts it first in the math font stack. Word uses Cambria Math for the same job.
ODF
Export (export/odt.ts, sentinel MTH U+E012, pass applyFormulas right after applyImages):
a <draw:frame text:anchor-type="as-char" draw:style-name="MthFr"> holding
<draw:object xlink:href="./Formula{n}"/>, plus Formula{n}/content.xml — an ODF formula
sub-document whose root is the MathML — and two manifest entries (Formula{n}/ as
application/vnd.oasis.opendocument.formula, its content.xml as text/xml).
- No
svg:width/svg:heighton the frame, on purpose. With a size LibreOffice scales the object to fill it (a frame measured at the dialog's preview font size magnified the formula ~1.5×); without one it typesets at the natural size, matched to the body text. Verified by rendering throughsoffice. MthFrcarriesstyle:vertical-pos="middle" style:vertical-rel="text", what LibreOffice writes for its own formula frames.bottom/baselinewas tried and drops the formula below the line.- The LaTeX rides in
<annotation encoding="application/x-tex">so our own files re-import exactly. LibreOffice strips it and substitutes a StarMath annotation, so the fallback path (parse its MathML back to LaTeX) is the one that actually runs after a LO re-save — and it reproduces the source exactly (tests/lo-roundtrip.test.ts). displayis read off the paragraph, not the object (import/odt.tsaloneInParagraph): ODF has no inline/display flag, and LibreOffice writesdisplay="block"on every formula it re-saves, so trusting the attribute turned every inline formula into a displayed one.
DOCX
Export (export/docx.ts, sentinel MTH, post-pack pass applyFormulasDocx beside
applyTextBoxesDocx): the run holding the sentinel is replaced by <m:oMath>, or by
<m:oMathPara> when display — which is what centers it on its own line. The namespace is
declared inline on the m: element, like the wps/a namespaces in the text-box XML.
Import: m:oMath/m:oMathPara sit beside the w:r runs in their own namespace, so
convertInline picks them up before its namespaceURI !== W guard — that guard is why every
formula in a Word document was silently dropped before this feature. m:oMathPara →
display: true. Word stores no size for a formula, and neither do we.
A pasted formula is a picture, not a formula
Real documents mix both: one will carry OMML formulas and equations its author pasted
as raster images. Those import as image nodes and offer the image toolbar's wrap options on
click — in Word and LibreOffice too. Nothing short of OCR changes that, so a click that offers
wrap options instead of the formula dialog is the document's doing, not a broken import.
Testing note
npm run test:lo's formula leg needs the libreoffice-math package — LibreOffice is
modular and Writer alone cannot hold a formula object, so without it every formula is dropped
on load without a word (measured: converting a formula-bearing .docx with a Writer-only
install yields an .odt with zero formulas). The leg then fails on "both objects survive", which
looks like a broken export but is the missing component. Check dpkg -l | grep libreoffice-math before believing that failure. A desktop LibreOffice ships Math; only
stripped-down server/container installs don't.