js.documents 1.89.0 → 1.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -564,7 +564,7 @@ The package is layered from generic primitives outward to the two conversion dir
564
564
  - **`src/odf-package/`** — the ODF-side counterpart to `src/opc/`: `manifest.ts` re-exports `odf.js`'s own manifest read/build/write/sync/validate functions (`odf.js` already owns `META-INF/manifest.xml` end to end — reading, deriving, writing, syncing, and validating it — unlike `ooxml.js`'s read-only OPC relationship handling) and adds exactly one thing of its own, `syncOdfManifest`: `odf.js`'s `buildManifest` synthesises a `manifest:file-entry` for every embedded sub-document directory it finds (any `"<dir>/content.xml"` prefix) but resolves that entry's media type by file EXTENSION, which a directory has none of, so it comes out empty unless a caller supplies an override. `syncOdfManifest` derives each one from what the sub-document actually is — the single element inside its own `office:body`, the same discriminant every `odf.js` reader keys on — and every part-mutating helper here syncs through it, so adding an image to a document that already embeds a formula cannot blank the formula object's own entry on the way past. `media.ts`'s `addImageMedia` inserts a binary image part under `Pictures/` (the real-world LibreOffice/OASIS convention, confirmed against `odf.js`'s own round-trip/manifest fixtures) — one step simpler than OOXML's own `addImageMedia` (`src/opc/media.ts`) since ODF references a media part directly by its package path (`xlink:href`) rather than through a relationship-ID indirection. `formula.ts`'s `addFormulaObject` is the newer sibling and a genuinely different shape of insertion: an embedded ODF formula is not a markup vocabulary inside the host `content.xml` the way OOXML's own OMML is, it is a WHOLE NESTED DOCUMENT stored under its own directory prefix in the same zip (`Object 1/content.xml`, an `office:document-content` > `office:body` > `office:math` > `math:math` tree), referenced from the host by a `draw:frame`/`draw:object` naming that directory — precisely what `odf.js`'s own `readOdfFormula` reads back, and what `readOdfEmbeddedFormula` (`src/odf/formula/read.ts`) resolves out of the outer package's flat parts record. The formula's own MathML nodes are written straight through with no translation and no re-serialisation, since `document-schema.js`'s `MathMlNode` and `odf.js`'s `XmlNode` are structurally identical; the `math:math` element declares the MathML namespace both as the `math:` prefix and as the default, so a prefixed tree (real LibreOffice output) and a bare one (what `src/omml/read.ts` recovers from an OOXML equation) are each genuinely namespaced. `OdpSlide.addImage`/`OdpShape` (`src/edit/odp/image.ts`) is `addImageMedia`'s real caller — and, through `src/edit/odg/*`'s wholesale reuse of `OdpShape` (see the `src/edit/` entry below), `OdgPage.addImage` too; `OdtBody.appendFormula` (via `src/edit/odt/formula.ts`) is `addFormulaObject`'s; `src/odb/read.ts` also reuses `manifest.ts`'s `readManifest` directly, to check `database/script`'s own manifest-declared media type before treating it as an HSQLDB script part.
565
565
  - **`src/edit/`** — the read-and-write editable model: live-view classes (`DocxEditor`/`DocxParagraph`/`DocxRun`/`DocxTable`, `PptxEditor`/`PptxSlide`/`PptxShape`, `OdtEditor`/`OdtParagraph`/`OdtRun`/`OdtTable`/`OdtList`, `OdpEditor`/`OdpSlide`/`OdpShape`, `OdsEditor`/`OdsSheet`/`OdsCell`, `OdgEditor`/`OdgPage`/`OdgBoxVector`/`OdgLineVector`/`OdgPathVector`) wrapping the actual `XmlElement` objects inside a decoded `Package`, plus `buildDocxPackage`/`buildPptxPackage`/`buildOdtPackage`/`buildOdpPackage`/`buildOdsPackage`/`buildOdgPackage` bridging a `ContentDocument` to a fresh package built entirely through those same primitives — `pdfToOdt`/`pdfToOdp`/`pdfToOds`/`pdfToOdg` each call the matching one. `DocxParagraph.appendOfficeMath` and `OdtBody.appendFormula` are the formula-writing primitives, and they are deliberately shaped differently because the two formats embed a formula in genuinely different ways: `appendOfficeMath` appends a real OMML display equation (`m:oMathPara` > `m:oMath`) built by `src/omml/write.ts` INLINE in the paragraph, while `appendFormula` writes a whole nested formula sub-document into the package (`src/odf-package/formula.ts`) and appends a `draw:frame`/`draw:object` referencing it. `buildDocxPackage`/`buildOdtPackage` use them to write an embedded formula as genuine, editable math in each format instead of a plain-text stand-in. `src/edit/odp/*` reuses `src/edit/odt/*`'s own paragraph/run/list/style-interning classes WHOLESALE rather than reimplementing them for presentations: a `draw:frame`'s `draw:text-box` holds the identical `text:p`/`text:span` content model `office:text` does, interned into the identical `content.xml` `office:automatic-styles` registry (`src/edit/odt/props.ts`'s `applyStyleChange`) — `OdpShape.appendParagraph`/`.paragraphs()`/`.addList()` return real `OdtParagraph`/`OdtList` instances, not odp-specific lookalikes. The genuinely new odp-specific work is `draw:page`/`draw:frame` mechanics (a slide is a `draw:page`, a shape's geometry is explicit `svg:x`/`svg:y`/`svg:width`/`svg:height` rather than pptx's placeholder-inheritance-heavy model) and rotation: `OdpShape.rotationDeg` is a genuine `draw:transform` setter built on `odf.js`'s own `applyOdfTransform`/`resolveOdfShapeGeometry` (`typed/shared/transform.ts`) — the write-side inverse of the exact function odf.js's own reader uses. `PptxShape.rotationDeg` (`src/edit/pptx/shape.ts`) is the DrawingML analogue, a plain `a:xfrm/@rot` attribute setter (60,000ths of a degree, clockwise, ECMA-376 20.1.7.6) needing no group-composition logic of its own, since `ooxml.js`'s own `composeShapeRotationDeg` already collapses to a bare passthrough of `xfrm.rotationDeg` for a top-level, ungrouped shape. That write side now lives in `src/edit/geometry.ts` (`buildTransformAttr`/`applyOdfGeometry`), a peer of the per-format edit directories rather than inside `odp/`, because `OdgBoxVector.rotationDeg`/`OdgPathVector.rotationDeg` need the identical machinery for `draw:rect`/`draw:ellipse`/`draw:path` — odf.js resolves all four element kinds through one `resolveOdfShapeGeometry`, so there is exactly one correct inverse of it. A table INSIDE a slide shape (not a document-level table) is now writable too: `OdpSlide.addTable` builds a `draw:frame` whose only child is a `table:table` directly (no `draw:text-box` wrapper) and reuses `OdtTable`/`buildTable` WHOLESALE for it, the same content-model-is-identical-wherever-it-lives argument `OdpShape`'s own paragraph/list reuse already rests on; `PptxSlide.addTable` (`src/edit/pptx/table.ts`) is the genuinely new DrawingML-side work, since a table shape lives in its own `p:graphicFrame` — a shape kind distinct from `p:sp`/`p:pic`, with its own frame on a direct `p:xfrm` child rather than nested in a `p:spPr` — and a DrawingML table's own merge model is a THIRD distinct convention from both docx's gridSpan-collapses-the-row scheme and ODF's covered-table-cell elements: every row always carries exactly as many `a:tc` as there are grid columns, and a covered cell is marked by a plain `hMerge`/`vMerge="1"` attribute on that same element, never an omitted or a differently-tagged one. `src/edit/ods/*` has no docx/pptx/odt/odp analogue to reuse for its core concern (cell addressing) but still reuses `src/edit/odt/*`'s style interning and `src/edit/odt/content.ts`'s `populateParagraph` for cell text content — `src/edit/ods/address.ts` is the write-side counterpart to `odf.js`'s own read-side `table:number-*-repeated`-aware cursor: setting a distant cell's value splits the covering repeated run in place at that one position rather than materialising every cell in between, exactly mirroring the read-side hazard `odf.js`'s own `typed/shared/a1.ts` already solved. `src/edit/ods/print-settings.ts` is the newest addition: `OdsSheet.printSettings`'s own getter/setter, mining `styles.xml`'s `office:automatic-styles`/`office:master-styles` directly (a part no other `src/edit/ods/*` module needed to touch before) rather than `content.xml` alone, reusing `odf.js`'s own exported `findStyleElement`/`resolvePageLayoutProperties`/`parsePageSize`/`parseMargins` for the read half and `src/edit/odt/automatic-styles.ts`'s `nextStyleName` (already generic over which `office:automatic-styles` element it scans) for the write half's own fresh-name minting. `src/edit/odg/*` reuses `OdpShape`/`buildTextBoxFrame`/`insertImageFrameMedia` WHOLESALE for `draw:frame` text/image content (a drawing page's `draw:frame` content model and geometry resolution — rotation included — are byte-for-byte identical to a presentation's, both resolved through `odf.js`'s own shared `readDrawFrame`), so there is no separate `OdgShape` class at all; the genuinely new work is the vector-primitive classes (a per-kind attribute vocabulary: `svg:x`/`y`/`width`/`height` for rect/ellipse/path, `svg:x1`/`y1`/`x2`/`y2` for a line) and their own fill/stroke, which needed a small, self-contained graphic-family style writer (`src/edit/odg/style.ts`) since `odf.js`'s own `StyleRegistry` recognises `'graphic'` as a style family but its `StylePropertiesSchema` only ever models text/paragraph formatting — it has no fill/stroke fields and never emits a `style:graphic-properties` element. A path vector's own `svg:d` is generated by `src/edit/odg/svg-path.ts`, the write-side inverse of `odf.js`'s own `typed/shared/path.ts` parser — always absolute, always space-separated commands, anchoring `svg:viewBox` at `"0 0 {widthPt} {heightPt}"` so the written numbers are the exact source `ContentPathPoint` values with no rescaling arithmetic either way (see Gotchas below for the cross-check against that exact parser). That vector writer is no longer odg-only: `buildVectorElement`/`appendVectorTo` (`src/edit/odg/vector.ts`) are the single dispatch point `OdgPage.addVector`, `OdpSlide.addVector`, and `OdtBody.appendVectors` all go through, so a `draw:rect`/`draw:ellipse`/`draw:line`/`draw:path` is built exactly one way whichever ODF document kind it lands in — the same wholesale-reuse argument `OdpShape`'s own paragraph/list reuse rests on. `src/edit/drawingml/vector.ts` is the OOXML half of the same idea and a peer of the per-format directories for the same reason `src/edit/geometry.ts` is: it holds everything inside a DrawingML shape-properties element, which docx and pptx express identically (`CT_ShapeProperties` is one type in both), leaving only the per-format wrapper to `src/edit/docx/vector.ts` (a page-anchored `w:drawing`/`wp:anchor` carrying a `wps:wsp`) and `src/edit/pptx/vector.ts` (a plain `p:sp`). See the vector write-side gotchas below for the geometry mapping and the anchoring choices each makes.
566
566
  - **`src/fonts/`** — source-embedded font extraction, and the registry composition every X → PDF conversion builds from it (see [Fonts](#fonts) above for the resolution order this produces). `obfuscation.ts` implements ECMA-376 Part 4, 2.8.1: `deriveFontKey` turns a `w:fontKey` GUID into the 16-byte XOR key — reading its 32 hex digits as byte pairs in REVERSE order, so `key[0]` is the GUID's LAST pair, verified against the specification's own worked example — and `deobfuscateEmbeddedFont` applies it twice across the part's first 32 bytes. One function covers docx and pptx both, by sniffing the leading sfnt signature FIRST and only deobfuscating bytes that are not already a recognisable font, rather than branching on source format: pptx's own `.fntdata` parts are stored clear and carry no font key at all, and a docx producer that stored a clear part stays readable too. `ooxml.ts` resolves `word/fontTable.xml` (or `ppt/presentation.xml`) through the package's own relationship graph rather than assuming a conventional path, reads each `w:embedRegular`/`w:embedBold`/`w:embedItalic`/`w:embedBoldItalic` (or `p:regular`/`p:bold`/`p:italic`/`p:boldItalic`) reference, and produces pdf-codec's `ProvidedFont` shape. `odf.ts` does the same for `style:font-face`'s `svg:font-face-src`/`svg:font-face-uri` — no relationship indirection, no obfuscation, and a face's weight/style taken from `loext:font-weight`/`loext:font-style` where a producer wrote them and from the font's OWN `OS/2` `fsSelection` bits where it did not (the better signal of the two: a `loext` attribute is a producer's claim about a file, `fsSelection` is that file's own declaration about itself). `registry.ts`'s `createDocumentFontRegistry` composes a source package plus any caller-supplied faces into a real `FontRegistry`, expressing the whole precedence chain as data (`sourceFonts` ahead of `fonts` ahead of the vendored substitutes) rather than as a branch. A face is deliberately never filtered by what the document actually uses: an embedded face is normally subsetted, so a character this package synthesises rather than reads can legitimately be absent from a face that is otherwise exactly right, and that is resolved per character by pdf-codec's own `onMissingGlyph`, not by dropping the whole face.
567
- - **`src/mathml/`** — a MathML presentation-layer typesetting engine, comparable in scope to pdf-codec's own standard-14 text-layout half — genuinely self-contained: no import from `model`, `pdf-codec`, or `odf.js` at all (not even `document-schema.js`), matching `src/layout/`'s own "pure conversion algorithm" isolation one tier further down. `nodes.ts` defines `MathMlNode`/`MathMlElement` as a local, structurally-compatible mirror of `odf.js`'s own `XmlNode` (the same "mirror the shape, don't import the package" trick `src/interop.test.ts` already proves holds between `ooxml.js` and `odf.js`), so `odf.js`'s `readOdfFormula`'s real return value type-checks against it with zero cast. `variant.ts` maps `mathvariant` to the Unicode Mathematical Alphanumeric Symbols block (Latin/Greek/digits, including the block's own well-known Letterlike-Symbols hole-fillers — italic small h, eleven Script/Fraktur/Double-struck capitals — generated directly from Unicode's own `UnicodeData.txt`, not transcribed by hand). `operators.ts` is a deliberately bounded operator dictionary (lspace/rspace/stretchy/largeop/movablelimits per operator), not the MathML3 spec's own multi-thousand-entry table. `layout.ts` is the recursive box-model engine itself (`mrow`/`mi`/`mn`/`mo`/`mtext`/`mspace`/`msub`/`msup`/`msubsup`/`munder`/`mover`/`munderover`/`mfrac`/`msqrt`/`mroot`/`mtable`/`mtr`/`mtd`/`mstyle`/`semantics`, plus a text-content fallback with a diagnostic for anything else), driven entirely by the injected `MathFontMetrics` port (`metrics.ts`) rather than any font-parsing code of its own — pdf-codec's own `math-font.ts` is the real implementation, consumed only through this structural port, never imported directly. `compose.ts`/`radical.ts`/`length.ts` are its own small geometry helpers (baseline-offset box placement, a hand-drawn hooked radical sign built from line segments rather than a bare glyph substitute, MathML length-unit parsing). `layout.ts` additionally stretches a row's own vertical fences through the same `MathFontMetrics` port (its `stretch` method resolves the font's OpenType MATH `MathVariants` data into positioned glyph IDs), emitting them as `MathAssembledGlyphs` items — the one item kind addressed by glyph ID rather than by Unicode text, because most of the glyphs such a construction names have no code point at all. Output is a flat `MathBox` (positioned glyph runs, rules, strokes, and assembled glyph placements, box-local top-left/y-down coordinates), passed with zero cast into pdf-codec's `writePdf({ formulas })` — see pdf-codec's own README for the structural-typing mechanism that makes this work across a package boundary with no shared class or branded type.
567
+ - **`src/mathml/`** — a MathML presentation-layer typesetting engine, comparable in scope to pdf-codec's own standard-14 text-layout half — genuinely self-contained: no import from `model`, `pdf-codec`, or `odf.js` at all (not even `document-schema.js`), matching `src/layout/`'s own "pure conversion algorithm" isolation one tier further down. `nodes.ts` defines `MathMlNode`/`MathMlElement` as a local, structurally-compatible mirror of `odf.js`'s own `XmlNode` (the same "mirror the shape, don't import the package" trick `src/interop.test.ts` already proves holds between `ooxml.js` and `odf.js`), so `odf.js`'s `readOdfFormula`'s real return value type-checks against it with zero cast. `variant.ts` maps `mathvariant` to the Unicode Mathematical Alphanumeric Symbols block (Latin/Greek/digits, including the block's own well-known Letterlike-Symbols hole-fillers — italic small h, eleven Script/Fraktur/Double-struck capitals — generated directly from Unicode's own `UnicodeData.txt`, not transcribed by hand). `operators.ts` is a deliberately bounded operator dictionary (lspace/rspace/stretchy/largeop/movablelimits per operator), not the MathML3 spec's own multi-thousand-entry table. `layout.ts` is the recursive box-model engine itself (`mrow`/`mi`/`mn`/`mo`/`mtext`/`mspace`/`msub`/`msup`/`msubsup`/`munder`/`mover`/`munderover`/`mfrac`/`msqrt`/`mroot`/`mtable`/`mtr`/`mtd`/`mstyle`/`semantics`, plus a text-content fallback with a diagnostic for anything else), driven entirely by the injected `MathFontMetrics` port (`metrics.ts`) rather than any font-parsing code of its own — pdf-codec's own `math-font.ts` is the real implementation, consumed only through this structural port, never imported directly. `compose.ts`/`radical.ts`/`length.ts` are its own small geometry helpers (baseline-offset box placement, the hand-drawn hooked radical sign `layout.ts` now uses only as a fallback when a font declares no √ MathVariants construction at all, MathML length-unit parsing). `layout.ts` additionally stretches a row's own vertical fences through the same `MathFontMetrics` port (its `stretch` method resolves the font's OpenType MATH `MathVariants` data into positioned glyph IDs), emitting them as `MathAssembledGlyphs` items — the one item kind addressed by glyph ID rather than by Unicode text, because most of the glyphs such a construction names have no code point at all. Output is a flat `MathBox` (positioned glyph runs, rules, strokes, and assembled glyph placements, box-local top-left/y-down coordinates), passed with zero cast into pdf-codec's `writePdf({ formulas })` — see pdf-codec's own README for the structural-typing mechanism that makes this work across a package boundary with no shared class or branded type.
568
568
  - **`src/omml/`** — the MathML ⇄ OMML (Office Math Markup Language, ECMA-376 Part 1 §22.1's own `m:` vocabulary) structural translator, both directions. `write.ts`'s `buildOfficeMath`/`buildOfficeMathParagraph` are the write side, the counterpart to `src/mathml/`'s own typesetting engine, covering the identical construct set deliberately, so a formula rendered to PDF and the same formula written into a docx degrade in exactly the same places rather than one being silently better than the other: each MathML construct maps onto its real OMML element (`mfrac` → `m:f`, `msqrt`/`mroot` → `m:rad` with `m:radPr/m:degHide` and the degree/radicand order reversed, `msub`/`msup`/`msubsup` → `m:sSub`/`m:sSup`/`m:sSubSup`, `munder`/`mover` → `m:limLow`/`m:limUpp` and `munderover` → the two nested, `mtable`/`mtr`/`mtd` → `m:m`/`m:mr`/`m:e` with per-column `m:mcs`/`m:mc` justification, and every token element → an `m:r`/`m:t` run whose `mathvariant` becomes OMML's own `m:scr` script + `m:sty` style pair). `read.ts`'s `readOfficeMath`/`collectOfficeMathElements` are the read side, the structural inverse of every one of those mappings, and read STRICTLY MORE than the writer writes — deliberately, since the writer only ever has to express what MathML can say while the reader has to cope with whatever Word itself authored: `m:d` (Word's representation of every parenthesised sub-expression), `m:nary` (a sum/product/integral with limits AND its own operand), `m:acc`, `m:bar`, `m:func`, and `m:sPre` each have one exact MathML inverse and no writer counterpart at all. Both directions emit no geometry, measure nothing, and load no font — this is a vocabulary translation, not a rendering. The directory lives outside `src/mathml/` for that directory's own isolation rule: `write.ts`'s whole output type (and `read.ts`'s whole input type) is `ooxml.js`'s `XmlElement`, and `src/mathml/` imports no package at all. `shared.ts` holds what neither direction owns: the `OmmlDiagnostic` shape both report through, the one `mathvariant` ⇄ `m:scr`/`m:sty` table each reads in its own direction, and `mi`'s own intrinsic-variant default. `buildDocxPackage` and `readDocxContent` are their real callers; a construct with no counterpart in the target vocabulary degrades on its own, with a diagnostic, exactly as `src/mathml/layout.ts`'s own `unsupported` fallback does for the PDF path.
569
569
  - **`src/ooxml/`** — resolves a `Package` into a `ContentDocument`: `docx/read.ts` and `pptx/read.ts` are now thin adapters over `ooxml.js`'s own `readDocx`/`readPptx`, wrapping their `{ metadata, sections }`/`{ metadata, slides }` result into `ContentDocument`'s `wordprocessing`/`presentation` shape. The docx style cascade (`docDefaults` → named-style `basedOn` chains → paragraph-mark run properties → character styles → direct formatting), the pptx placeholder → layout → master → theme inheritance cascade, and DrawingML geometry/colour resolution all now live upstream in `ooxml.js` itself, not in this package. `docx/formula.ts` is the one piece of genuinely local reading work left: a second, independent pass over the same `word/document.xml`, splicing every OOXML math equation `src/omml/read.ts` recovers into the sections `readDocx` produced — needed because `readDocx` has no `m:oMath` handling at all, exactly the way `src/odf/odt/read.ts` needs its own pass for a formula `odf.js`'s `readOdt` likewise does not read. Positioning is derived rather than approximated, and by a shorter route than the ODF side's own block-counting mirror needs: every `w:p` produces exactly one top-level `ContentParagraph` block and nothing else produces one, so the Nth `w:p` in the body IS the Nth paragraph-kind block. A `w:p` carrying nothing but its equation is CONSUMED by the formula block rather than emitted alongside it, which is what keeps a `docx → odt → docx` round trip from accumulating one blank paragraph per formula per hop. `docx/extras.ts`'s `readDocxExtras` is a second, independent re-projection of that same `readDocx` call, for the data `readDocxContent` genuinely cannot carry through `ContentDocument`'s section/block shape at all: comments, footnotes, headers/footers, and numbering (`abstractNum`/`num`) definitions. It calls `readDocx` a second time rather than being fused onto `readDocxContent`'s own return value — an accepted cost matching every other "each pipeline stage independently exported" pair in this codebase — and reuses `ooxml.js`'s own `Comment`/`Footnote`/`NumberingDefinitions` types directly rather than mirroring them locally.
570
570
  - **`src/odf/`** — the ODF-side counterpart to `src/ooxml/`, resolving an `odf.js` `Package` into a `ContentDocument`: `odt/read.ts`'s `readOdtContent` is a thin adapter over `odf.js`'s own `readOdt`, wrapping its `{ metadata, sections }` result into the identical `wordprocessing` shape `readDocxContent` produces — the concrete proof that odt and docx genuinely share one pivot and one layout engine. `odp/read.ts`'s `readOdpContent` is the same adapter over `odf.js`'s own `readOdp`, wrapping `{ metadata, slides }` into the identical `presentation` shape `readPptxContent` produces. `ods/read.ts`'s `readOdsContent` wraps `odf.js`'s `readOds`'s `{ metadata, sheets }` into the `spreadsheet` `ContentDocument` variant, and `odg/read.ts`'s `readOdgContent` wraps `odf.js`'s `readOdg`'s `{ metadata, pages }` into the `drawing` variant — `odg` still has no OOXML-side sibling adapter at all (no drawing-equivalent OOXML format this package reads); `ods` now does, `ooxml.js`'s own `readXlsxContent`/`buildXlsxPackage`, consumed directly by `src/convert/convert.ts`'s `odsToXlsx`/`xlsxToOds` bridge and by `src/codecs/registry.ts`'s own xlsx `content` codec (see the `src/codecs/` entry below — the latter is what lets `readDocumentMetadata`/`setDocumentMetadata`/`buildDocumentBytes` treat xlsx uniformly with the rest of `DocumentFormat`) but deliberately not re-exported from this package's own public surface, mirroring the `readDocx`/`readPptx` non-re-export choice above. `buildOdtPackage`/`buildOdpPackage`/`buildOdsPackage`/`buildOdgPackage` (`src/edit/{odt,odp,ods,odg}/content.ts`) each bridge a `ContentDocument` back to a fresh package built on that format's own live-view editor, closing the PDF → odt/odp/ods/odg direction (`pdfToOdt`/`pdfToOdp`/`pdfToOds`/`pdfToOdg` each call the matching one) — see the `pdfToOds` gotcha below for `buildOdsPackage`'s own printSettings-writing addition. `formula/read.ts`'s `readOdfFormulaContent`/`readOdfEmbeddedFormula` are the same thin-adapter pattern over `odf.js`'s own `readOdfFormulaDocument`, for a standalone `.odf` (the whole `'formula'`-kind `ContentDocument`) and an embedded sub-object (its bare `ContentFormula`) respectively — the latter reading the sub-object's own `content.xml` directly out of the outer package's flat `Package.parts` record, no separate unzip step needed; `formula/detect.ts`'s `collectFormulaFrames`/`collectSlideFormulaFrames` are genuinely new work with no `odf.js`-side equivalent at all — `odf.js`'s own `readDrawFrameContent` doesn't recognise a `draw:object`-bearing `draw:frame` yet, so `odt/read.ts` and `odp/read.ts` each run one of these as a second pass over the same package's raw `content.xml` to find and inject a formula's own embedded-object block. `collectFormulaFrames` is a deep walk (a frame directly in the container, one nested inside a `draw:g` group with that group's own `draw:transform` composed exactly as `walkDrawShapes` composes it, and one anchored inline inside a paragraph's own run content); `collectSlideFormulaFrames` replicates `odf.js`'s own `walkDrawShapes` traversal precisely so each formula's `ContentShape` index is derived rather than guessed. See the Gotchas entry below for where each detected formula's block actually lands.
@@ -672,7 +672,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
672
672
  - **A Firebird gbak backup stream's own wire format mixes two genuinely different byte-level encodings, confirmed only by testing against real bytes, not solely from reading the engine's source.** Every `rec_*`/`att_*` tag-and-attribute structure is little-endian ("VAX order", `isc_vax_integer`), one length-prefix byte per value; a row's own field-value sequence (once any RLE compression is peeled off) is standard RFC 1832 XDR — big-endian, every value (even a nominally 16-bit `SSHORT`) widened to a 4-byte-aligned unit, opaque byte runs zero-padded to the next 4-byte boundary. A genuine 64-bit-integer word-order bug (high 32 bits transmitted first, not low-first as `xdr_hyper`'s own in-memory `temp_long` array layout suggests on first reading) was caught exactly this way: a `DECIMAL(10,2)` column decoded to a nonsense value on the first real-fixture test run, not from a source-reading mistake that was obvious in advance.
673
673
  - **STIX Two Math is embedded as a whole, unmodified `CFF ` table rather than glyph-subsetted — pdf-codec's own font-embedding scope decision, not this package's.** See pdf-codec's own README for the full CFF-embedding scope statement.
674
674
  - **A stretchy fence in an `mrow` genuinely stretches to its content on the VERTICAL axis, via the font's own `MathVariants` data, for any `mo` the operator dictionary calls stretchy — matched now by a genuine HORIZONTAL stretch for an over/under-brace spanning its own base in `munder`/`mover`/`munderover` (see the next bullet).** `layoutRowChildren` (`src/mathml/layout.ts`) targets twice the larger of the row's own non-stretchy children's half-extents about the maths axis (MathML's default `symmetric` behaviour for a fence), asks the `MathFontMetrics.stretch` port for a construction reaching it, and emits a `MathAssembledGlyphs` item: one or more glyph IDs at explicitly computed positions, rather than the Unicode text a `MathGlyphRun` carries. It has to be glyph IDs — every pre-built larger variant in this font is unencoded, as are the radical's and the over-brace's assembly pieces, with only the bracket family's own pieces given code points (the U+239B–U+23AD block). What genuinely stretches today: parentheses, square brackets, curly braces, floor/ceiling, angle brackets, and the vertical-bar/norm pair, each drawn either from a larger pre-built variant or from a real multi-part assembly sized to the content. What does not, and why:
675
- - **`msqrt`/`mroot` radical signs** keep the hand-drawn hooked sign + vinculum `src/mathml/radical.ts` builds. This is not a `MathVariants` gap the font's own radical construction stretches perfectly well, and a bare stretchy `<mo>√</mo>` in an `mrow` does now use it but a radical *construct* needs a vinculum spanning its radicand's width, which no vertical glyph construction supplies, so switching it over means replacing the whole hand-built sign, not just its stem.
675
+ - **`msqrt`/`mroot` radical signs** render through the font's own vertical `MathVariants` construction (`base`/`variant`/`assembly`, sized to the radicand) plus a separately drawn vinculum rule spanning the radicand's width — the authentic radical silhouette the font designer drew, not the fixed-fraction hand-drawn approximation `src/mathml/radical.ts` builds. The construction's own top shelf and the vinculum rule overlap on the left segment (both solid at the bar height, so the overlap is invisible and the bar reads as one continuous line). That hand-drawn sign is now a FALLBACK, used only when `MathFontMetrics.stretch` returns `undefined` for a font backend that declares no radical construction at all so a different font still renders a real radical rather than vanishing.
676
676
  - **A multi-character `mo`** (`"||"`, an operator with combining marks) is never stretched: the font's `MathVariants` data is keyed per glyph, so there is no single construction to look up.
677
677
  - **An over/under-brace (`U+23DE`/`U+23DF`) spanning its own `munder`/`mover`/`munderover` base now genuinely stretches horizontally, via the identical `MathFontMetrics.stretch` port used for a vertical fence, just called with `axis: 'horizontal'` and a width target instead of a height one.** `layoutUnderOverChild` (`src/mathml/layout.ts`) is the call site: for each over/under script that `isStretchyOperator` accepts, it asks `stretchHorizontalOperator` to stretch to the base's own `widthPt`, computed independently for the over and under script (no shared or synchronised target — real `\overbrace{content}^{label}` semantics needs none), and falls back to the ordinary glyph run wherever the font declines (no horizontal `MathVariants` construction for that glyph, or the base is already wide enough that the construction's own base form already reaches it). `horizontallyStretchedBox` is `stretchedBox`'s horizontal-axis sibling, not a shared axis-branching function: its box width is the construction's own achieved `sizePt` (never `advanceWidthPt`, which for a horizontal assembly is only the widest individual glyph's own natural advance, far short of the whole span), and its ascent/descent come directly and unclamped from the construction's own real ink — one of the two is genuinely negative for both the over-brace and the under-brace, since each glyph's ink sits almost entirely on one side of its own drawing origin, and `layoutUnderOver` already stacks over/under content against the base's own edges using exactly that ascent/descent, with no further change needed on its part.
678
678
  - **A stretched fence's own glyphs have no ToUnicode mapping, so pdf-codec wraps them in an `/ActualText` span carrying the operator's real text.** Text extraction, search, and copy/paste still recover `(` from a six-piece assembled bracket. A fence the base glyph already covers is left as an ordinary `MathGlyphRun` rather than converted to glyph IDs, so a plain inline `(x + 1)` renders and extracts exactly as it did before any of this existed.
@@ -732,7 +732,7 @@ The prose below is authoritative; this table is a quick-reference summary of it,
732
732
 
733
733
  **docx/pptx/odt/odp/ods/odg → PDF** is a genuine layout render: the docx/odt flow/pagination engine and the pptx/odp direct-placement engine both produce real positioned text, images, tables, and (for docx/odt) numbered/bulleted lists, styled through the full cascade (theme fonts/colours, `basedOn` chains, placeholder inheritance for docx/pptx; `style:default-style`/`style:parent-style-name` chains for odt/odp). `odg` renders its vector primitives (rect/ellipse/line/path, the last emitted as real PDF `m`/`l`/`c`/`h` content-stream operators, not a polygon approximation of any curve) and reuses the pptx/odp direct-placement engine's own shape conversion for whatever text it also carries. It is a faithful **visual approximation**, not a pixel- or line-identical reproduction of what Word/PowerPoint/Writer/Impress/Draw would themselves render — how close depends on which typeface the document asks for and whether it embedded one, see the font-resolution gotcha above.
734
734
 
735
- **odf → PDF (`odfToPdf`), and a formula embedded inside odt/odp/ods,** render **faithful mathematical typesetting**, not a static image or a plain-text placeholder: real box-model layout (script/limit positioning, fraction/radical geometry with correct rule thickness, table column alignment, `mathvariant` → Mathematical Alphanumeric Symbols mapping) through the embedded STIX Two Math font, with genuine per-glyph metrics (advance width, italic correction, top-accent attachment) and font-wide layout constants (axis height, fraction/radical rule thickness and gaps, script shift amounts) parsed directly from that font's own `MATH` table — not approximated or hand-tuned. A vertical fence around a tall construct genuinely stretches too, assembled from the font's own `MathVariants` pieces and sized to what it wraps, rather than drawn at a fixed base size — and an over/under-brace spanning its own `munder`/`mover`/`munderover` base stretches horizontally on the identical basis, sized to that base's own width. The honest limits: `msqrt`/`mroot` still draw a hand-built radical sign rather than the font's own stretched one (a structural reason, not because the font data is unavailable see the Gotchas entries above); `mover`/`munder` centre geometrically rather than at the font's own declared accent-attachment point; and the operator dictionary and Greek `mathvariant` mapping each cover a deliberately bounded, common-case set rather than the full specification. For a formula anchored to a spreadsheet **cell**, position is genuinely resolved against that sheet's real column/row geometry (verified end to end against a real LibreOffice-authored `.ods`), and the rendered **size** comes from the same two-pass frame fit every engine uses (`formulaSizePtForFrame`) — see the Gotchas entry on it for the docx-OMML width-less fallback. **`pdfToOdf` (PDF → structured MathML) is not attempted, on either direction** — recovering a semantic operator tree (is this pair of glyphs a fraction, or a coincidentally stacked pair of ordinary characters? is a raised glyph a superscript, or just a smaller font size used for emphasis?) from nothing but positioned glyphs and paths is a categorically different, OCR-adjacent problem, with no geometry-reconstruction analogue anywhere else in this package: `reconstructWordprocessing`/`reconstructPresentation` recover paragraph/shape *structure* from geometry, never semantic *meaning* the way recognising a fraction would require.
735
+ **odf → PDF (`odfToPdf`), and a formula embedded inside odt/odp/ods,** render **faithful mathematical typesetting**, not a static image or a plain-text placeholder: real box-model layout (script/limit positioning, fraction/radical geometry with correct rule thickness, table column alignment, `mathvariant` → Mathematical Alphanumeric Symbols mapping) through the embedded STIX Two Math font, with genuine per-glyph metrics (advance width, italic correction, top-accent attachment) and font-wide layout constants (axis height, fraction/radical rule thickness and gaps, script shift amounts) parsed directly from that font's own `MATH` table — not approximated or hand-tuned. A vertical fence around a tall construct genuinely stretches too, assembled from the font's own `MathVariants` pieces and sized to what it wraps, rather than drawn at a fixed base size — and an over/under-brace spanning its own `munder`/`mover`/`munderover` base stretches horizontally on the identical basis, sized to that base's own width, and an `msqrt`/`mroot` radical sign renders through the font's own vertical construction (sized to its radicand) plus a separately drawn vinculum across the radicand's width. The honest limits: `mover`/`munder` centre geometrically rather than at the font's own declared accent-attachment point; and the operator dictionary and Greek `mathvariant` mapping each cover a deliberately bounded, common-case set rather than the full specification. For a formula anchored to a spreadsheet **cell**, position is genuinely resolved against that sheet's real column/row geometry (verified end to end against a real LibreOffice-authored `.ods`), and the rendered **size** comes from the same two-pass frame fit every engine uses (`formulaSizePtForFrame`) — see the Gotchas entry on it for the docx-OMML width-less fallback. **`pdfToOdf` (PDF → structured MathML) is not attempted, on either direction** — recovering a semantic operator tree (is this pair of glyphs a fraction, or a coincidentally stacked pair of ordinary characters? is a raised glyph a superscript, or just a smaller font size used for emphasis?) from nothing but positioned glyphs and paths is a categorically different, OCR-adjacent problem, with no geometry-reconstruction analogue anywhere else in this package: `reconstructWordprocessing`/`reconstructPresentation` recover paragraph/shape *structure* from geometry, never semantic *meaning* the way recognising a fraction would require.
736
736
 
737
737
  **PDF → docx/pptx/odt/odp** is necessarily a **best-effort reconstruction** from geometry: a PDF page is just positioned glyphs and images, with no semantic paragraph or shape structure to recover. Reading order, bold/italic/colour/font-size, and page/slide count are preserved; paragraph and text-block boundaries are inferred from baseline spacing and left-margin indentation, not recovered exactly. Two further kinds of content are recovered on top of that text, each on its own explicit terms: a real `ContentTable`, but **only** where a genuine drawn gridline lattice is detected, never from text alignment (which would be inventing structure, not recovering it); and a page's vector primitives, into a nested drawing document that currently reaches the `ContentDocument` pivot but not the output bytes. Both are covered in full by their own [Gotchas](#gotchas-and-quirks) entries.
738
738
 
@@ -377,22 +377,46 @@ function wrapRadical(radicand, index, ctx) {
377
377
  const signHeightPt = metrics.radicalExtraAscenderPt + ruleThicknessPt + gapPt + radicand.heightPt;
378
378
  const degreeWidthPt = index === void 0 ? 0 : metrics.radicalKernBeforeDegreePt + index.widthPt + metrics.radicalKernAfterDegreePt;
379
379
  const signOriginXPt = Math.max(0, degreeWidthPt);
380
- const sign = require_mathml_radical.buildRadicalSign(signOriginXPt, 0, signHeightPt, radicand.widthPt, ruleThicknessPt, ctx.color);
381
380
  const ascentPt = metrics.radicalExtraAscenderPt + ruleThicknessPt + gapPt + radicand.ascentPt;
382
381
  const descentPt = radicand.descentPt;
383
- const widthPt = signOriginXPt + sign.widthPt + radicand.widthPt;
384
- const items = [
385
- sign.hook,
386
- sign.vinculum,
387
- ...require_mathml_compose.placeChild(radicand, signOriginXPt + sign.widthPt, 0, ascentPt)
388
- ];
382
+ const items = [];
383
+ const stretched = ctx.metrics.stretch(8730, "vertical", signHeightPt, ctx.sizePt);
384
+ let signWidthPt;
385
+ if (stretched !== void 0) {
386
+ const placements = stretched.placements.map((placement) => ({
387
+ glyphId: placement.glyphId,
388
+ xPt: signOriginXPt,
389
+ yPt: stretched.inkAscentPt - placement.offsetPt
390
+ }));
391
+ items.push({
392
+ kind: "assembled-glyphs",
393
+ placements,
394
+ text: "√",
395
+ sizePt: ctx.sizePt,
396
+ color: ctx.color
397
+ });
398
+ items.push({
399
+ kind: "rule",
400
+ xPt: signOriginXPt,
401
+ yPt: metrics.radicalExtraAscenderPt,
402
+ widthPt: stretched.advanceWidthPt + radicand.widthPt,
403
+ heightPt: ruleThicknessPt,
404
+ color: ctx.color
405
+ });
406
+ signWidthPt = stretched.advanceWidthPt;
407
+ } else {
408
+ const sign = require_mathml_radical.buildRadicalSign(signOriginXPt, 0, signHeightPt, radicand.widthPt, ruleThicknessPt, ctx.color);
409
+ items.push(sign.hook, sign.vinculum);
410
+ signWidthPt = sign.widthPt;
411
+ }
412
+ items.push(...require_mathml_compose.placeChild(radicand, signOriginXPt + signWidthPt, 0, ascentPt));
389
413
  if (index !== void 0) {
390
414
  const raisePt = (ascentPt + descentPt) * metrics.radicalDegreeBottomRaisePercent / 100;
391
415
  const degreeBaselineFromTopPt = ascentPt + descentPt - raisePt - index.descentPt;
392
416
  items.push(...require_mathml_compose.shiftItems(index.items, metrics.radicalKernBeforeDegreePt, degreeBaselineFromTopPt - index.ascentPt));
393
417
  }
394
418
  return {
395
- widthPt,
419
+ widthPt: signOriginXPt + signWidthPt + radicand.widthPt,
396
420
  ascentPt,
397
421
  descentPt,
398
422
  heightPt: ascentPt + descentPt,
@@ -376,22 +376,46 @@ function wrapRadical(radicand, index, ctx) {
376
376
  const signHeightPt = metrics.radicalExtraAscenderPt + ruleThicknessPt + gapPt + radicand.heightPt;
377
377
  const degreeWidthPt = index === void 0 ? 0 : metrics.radicalKernBeforeDegreePt + index.widthPt + metrics.radicalKernAfterDegreePt;
378
378
  const signOriginXPt = Math.max(0, degreeWidthPt);
379
- const sign = buildRadicalSign(signOriginXPt, 0, signHeightPt, radicand.widthPt, ruleThicknessPt, ctx.color);
380
379
  const ascentPt = metrics.radicalExtraAscenderPt + ruleThicknessPt + gapPt + radicand.ascentPt;
381
380
  const descentPt = radicand.descentPt;
382
- const widthPt = signOriginXPt + sign.widthPt + radicand.widthPt;
383
- const items = [
384
- sign.hook,
385
- sign.vinculum,
386
- ...placeChild(radicand, signOriginXPt + sign.widthPt, 0, ascentPt)
387
- ];
381
+ const items = [];
382
+ const stretched = ctx.metrics.stretch(8730, "vertical", signHeightPt, ctx.sizePt);
383
+ let signWidthPt;
384
+ if (stretched !== void 0) {
385
+ const placements = stretched.placements.map((placement) => ({
386
+ glyphId: placement.glyphId,
387
+ xPt: signOriginXPt,
388
+ yPt: stretched.inkAscentPt - placement.offsetPt
389
+ }));
390
+ items.push({
391
+ kind: "assembled-glyphs",
392
+ placements,
393
+ text: "√",
394
+ sizePt: ctx.sizePt,
395
+ color: ctx.color
396
+ });
397
+ items.push({
398
+ kind: "rule",
399
+ xPt: signOriginXPt,
400
+ yPt: metrics.radicalExtraAscenderPt,
401
+ widthPt: stretched.advanceWidthPt + radicand.widthPt,
402
+ heightPt: ruleThicknessPt,
403
+ color: ctx.color
404
+ });
405
+ signWidthPt = stretched.advanceWidthPt;
406
+ } else {
407
+ const sign = buildRadicalSign(signOriginXPt, 0, signHeightPt, radicand.widthPt, ruleThicknessPt, ctx.color);
408
+ items.push(sign.hook, sign.vinculum);
409
+ signWidthPt = sign.widthPt;
410
+ }
411
+ items.push(...placeChild(radicand, signOriginXPt + signWidthPt, 0, ascentPt));
388
412
  if (index !== void 0) {
389
413
  const raisePt = (ascentPt + descentPt) * metrics.radicalDegreeBottomRaisePercent / 100;
390
414
  const degreeBaselineFromTopPt = ascentPt + descentPt - raisePt - index.descentPt;
391
415
  items.push(...shiftItems(index.items, metrics.radicalKernBeforeDegreePt, degreeBaselineFromTopPt - index.ascentPt));
392
416
  }
393
417
  return {
394
- widthPt,
418
+ widthPt: signOriginXPt + signWidthPt + radicand.widthPt,
395
419
  ascentPt,
396
420
  descentPt,
397
421
  heightPt: ascentPt + descentPt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.documents",
3
- "version": "1.89.0",
3
+ "version": "1.90.0",
4
4
  "description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {