fix: a citation inside a content control keeps its text
Word wraps every citation in a w:sdt; the inline walk ignored the wrapper, so its runs never reached the paragraph. a fixture fixture lost every [7]-style reference with them, and now matches LibreOffice's page count.
This commit is contained in:
1 parent
3154a9b7a2
commit
2fe762a594
3 files changed
+21
-4
No files matched your search
@@ -11,6 +11,7 @@ The **Open .odt** button (`App.svelte`) parses an uploaded file with `importOdt(
|
||||
- **Default suppression** keeps round trips (editor → LibreOffice → editor) from accreting explicit attrs: values matching the editor's defaults (12pt body, `HEADING_STYLE_OVERRIDES` sizes/margins, **0 paragraph spacing**, list margin 0, Times New Roman/Liberation Serif, `#000000` text) are imported as `null`/no mark, with small tolerances for producer unit rounding (pt↔cm↔twips). This is why the editor's defaults must equal Word's/LibreOffice's (below) — an editor-only default is indistinguishable from a failed style resolution and silently lands in every imported document.
|
||||
- **Mapping:** `text:h` → heading (level clamped to `MAX_HEADING_LEVEL`), per-depth `text:list-level-style-*` defs decide bullet vs ordered and `listStyleType` (reverse lookup `orderedTypeFromFormat`), tables handle covered cells / `number-columns-repeated` / header rows / colspans / cell shading (`StyleResolver.cellBackgroundColor` → `backgroundColor`) / cell borders (`StyleResolver.cellBorders` + `borderAttrFromOdf` → the per-side border attrs) / table margins (`tableMargins` reads the table style's `fo:margin-*`, `style:width`/`style:rel-width` and `table:align` against `Ctx.contentWidthCm` — so a table narrower than the text width stays narrow instead of being stretched; DOCX does the same from `w:tblInd` + the `w:tblGrid`/`w:tblW` width), `text:s`/`text:tab`/`text:line-break` become spaces/`\t`/`hardBreak`, `draw:frame`/`draw:image` → an `image` node (`convertFrame`: bytes read from the zip's `Pictures/` entry into a data-URI, `svg:width`/`-height` cm → px, `svg:title` → `alt`, `draw:transform` rotate → `rotation`; a non-`as-char` anchor or a graphic style's `style:wrap` → floating `wrap` mode via `StyleResolver.graphicProps` — an **explicit `as-char` anchor is always inline**: LibreOffice's named Graphics style carries a `style:wrap` that would otherwise float every inherited frame), text fields keep their stored value. **Text boxes / shapes** (`convertDrawElement`): `draw:frame`+`draw:text-box` → a `textBox` node (`convertTextBoxFrame`: height = `fo:min-height` else `svg:height`; fill/stroke read straight from `graphicProps`' `draw:fill(-color)`/`draw:stroke`/`svg:stroke-*`, values equal to the editor defaults suppressed, none → explicit `null`); `draw:rect`/`draw:ellipse`/`draw:custom-shape` with a rect/round-rectangle/ellipse preset → a `textBox` with the matching `shapeKind` (`convertShape`; other presets → "Unsupported shapes were removed"). A box is a block node but its frame sits inside a `text:p`, so converted boxes ride `Ctx.pendingBlocks` and `convertBlocks` flushes them after the anchor paragraph (dropping our own export's empty anchor); in cells/other boxes their blocks are unwrapped in place with a warning. The DOCX importer mirrors all of this (`convertWpsShape` for DrawingML `wps:wsp` — probed before the image `a:blip` path, `convertPict` for legacy VML; Word's `mc:AlternateContent` uses only the `mc:Choice` branch so the VML fallback can't double-import).
|
||||
- **Image formats (`import/imageFormats.ts`, shared by both importers):** `<img>` only renders a fixed set of formats, so `imageDataUrl(bytes, path)` resolves the mime by extension **then** magic-number (mislabelled/extensionless entries still work) and returns `null` for anything unrenderable — the importer then skips it with a specific "format the browser can't display" warning instead of a broken `<img>`. A frame's multiple `<draw:image>` alternatives (ODF) and a Word `a:blip`'s `svgBlip` alternative are probed for the first *displayable* one, and `<draw:a>`-wrapped frames are unwrapped (the hyperlink dropped). **Non-web formats we can decode client-side** (currently **TIFF**) are converted to PNG up front by `convertUnsupportedImages(bytes)` — an async pass `applyImport` runs before the synchronous import, passing a `path → PNG data-URI` map (`ConvertedImages`) into `importOdt`/`importDocx` (default empty, so tests stay sync). The decoder (`utif2`) is a **dynamic `import()`** → its own lazy chunk, loaded only when a convertible image is actually present, so ordinary docs pay nothing. **SVM/WMF/EMF** have no JS decoder (SVM is StarOffice-proprietary) → skipped with the format warning; LibreOffice/WASM conversion was ruled out (no backend; WASM LO is ~300MB).
|
||||
- **Content controls** (`inlineChildren`, DOCX): a `w:sdt` inside a paragraph — Word wraps every citation and bibliography entry in one — is walked through to its `w:sdtContent`, or the runs inside it never reach the paragraph (the thesis fixture lost every `[7]`-style reference and broke its lines one page short).
|
||||
- **Formulas** (`convertFormulaFrame`): a `<draw:frame>` whose `<draw:object>` resolves to a sub-document with a `math:math` root (or holds one inline) → a `formula` node. Our own `<annotation encoding="application/x-tex">` is preferred; LibreOffice strips it, so the MathML is parsed back to LaTeX instead. **`display` comes from the paragraph** (`aloneInParagraph`), not the object — LibreOffice writes `display="block"` on every formula it re-saves. DOCX: `m:oMath`/`m:oMathPara` sit beside the `w:r` runs in their own namespace, so `convertInline` probes them ahead of its `namespaceURI !== W` guard (that guard silently dropped every Word formula). See `docs/architecture/formulas.md`.
|
||||
- **Graceful degradation** (collected as `warnings`, surfaced once): footnotes/comments dropped, hyperlinks flattened to text, nested tables flattened to paragraphs, non-`Pictures` drawings dropped, unreadable pictures skipped (each with a warning). Page size is normalized to A4. Floating/wrapped images are imported as left/right/top-bottom wrap (free x/y positions collapse to the nearest side; see `image.ts`).
|
||||
|
||||
|
||||
+12
-1
@@ -894,6 +894,17 @@ function snapPt(v: number): number {
|
||||
}
|
||||
|
||||
// ---- inline conversion (runs, marks, fields, images) -----------------------
|
||||
// Word wraps a citation, a bibliography entry or any content control in a w:sdt whose
|
||||
// content is ordinary inline content — walk through the wrapper, or the runs inside it
|
||||
// (a citation's whole visible text) never reach the paragraph.
|
||||
function inlineChildren(el: Element): Element[] {
|
||||
return Array.from(el.children).flatMap((c) => {
|
||||
if (c.namespaceURI !== W || c.localName !== 'sdt') return [c];
|
||||
const content = fc(c, 'sdtContent');
|
||||
return content ? inlineChildren(content) : [];
|
||||
});
|
||||
}
|
||||
|
||||
function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockDefaults, hfFields: boolean): Node[] {
|
||||
const out: Node[] = [];
|
||||
let fieldMode: 'none' | 'instr' | 'result' = 'none';
|
||||
@@ -1004,7 +1015,7 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD
|
||||
}
|
||||
};
|
||||
|
||||
for (const el of Array.from(p.children)) {
|
||||
for (const el of inlineChildren(p)) {
|
||||
// Math is its own namespace and sits beside the w:r runs, so it has to be picked
|
||||
// up before the w:-only guard below drops it.
|
||||
if (el.namespaceURI === OMML_NS && (el.localName === 'oMath' || el.localName === 'oMathPara')) {
|
||||
|
||||
@@ -87,8 +87,8 @@ editor (`global.css`), so give LibreOffice the same answer:
|
||||
What the three real-world fixtures — the contract (an IHK
|
||||
contract form), the summary (a student summary: bullets,
|
||||
wrapped pictures, one table) and the thesis (a 48-page thesis with charts,
|
||||
metafile figures and a German TOC) — still report. The thesis is at 48 pages against
|
||||
LibreOffice's 48, with 62 line-level differences, most of them the justification rule above:
|
||||
metafile figures and a German TOC) — still report. The thesis matches LibreOffice's 49 pages,
|
||||
with 51 line-level differences — most of them the two engine rules below, not layout:
|
||||
|
||||
- **A hoisted text box loses the vertical offset it was anchored by.** A frame's own
|
||||
offsets are drawn now (`docs/architecture/frames.md`), but a text box anchored inside a
|
||||
@@ -129,7 +129,12 @@ LibreOffice's 48, with 62 line-level differences, most of them the justification
|
||||
(1.909cm) instead. So reading the attribute back naively moves the markers of our own
|
||||
exports. Whatever rule that is has to be understood before the value is trusted.
|
||||
- Lines whose natural width lands within ~0.2mm of the right margin may take one word
|
||||
more or fewer than LibreOffice. Sub-0.1mm engine rounding, not a layout rule.
|
||||
more or fewer than LibreOffice. Sub-0.1mm engine rounding, not a layout rule — but it
|
||||
**accumulates**: measured on the thesis' page 2, matching word widths (11.86mm for
|
||||
`haben,` on both sides) had drifted 0.45mm apart by the 150th mm of the line, which is
|
||||
what decides whether the next word fits. LibreOffice quantizes each glyph advance where
|
||||
Chromium keeps it fractional, and nothing in CSS exposes that. Most of the thesis'
|
||||
remaining `lineBreak` reports are this.
|
||||
|
||||
## Findings that did not survive measurement
|
||||
|
||||
|
||||
Reference in new issue
Block a user