diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8a798d8..3ad16e5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A list item whose marker is a picture starts its text where the page does in DOCX.** The + picture — a dot, an icon — was followed by a space, a little over two points where the page + leaves the `markerGap` asked for: `TealPulse`'s skills, highlights and certifications, a + 3.2pt dot and a 9.2pt gap, stood 6.3 to 6.7pt left of the page's in both editors, and + `OrangeOps`' 6pt in Word. A top-level item of a `hangingIndent(true)` list whose marker is + the list's and a picture alone is now followed by a tab to a stop the layout's measure past + the item's start — the picture's width and the gap — where the picture, its edges included, + clears that stop, as Word draws a picture at the size it is written; a marker of text, which + Word sets in its own widths, keeps its space. Those lines now stand within 0.1pt across. + Lines more than 2pt off across fall from 175 to 136 in Word, from 130 to 102 in LibreOffice + on Windows and from 427 to 388 on Linux. `ListBuilder.hangingIndent` and `markerGap` say what + DOCX now keeps. - **A page of its own draws its backgrounds from the body in DOCX, so LibreOffice no longer sets it lower.** LibreOffice gives a header a height of its own however little it holds, and the header carrying a page's backgrounds set every line of the seven sidebar CVs diff --git a/core/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java b/core/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java index 5c6c76c73..939d9efd1 100644 --- a/core/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java +++ b/core/src/main/java/com/demcha/compose/document/dsl/ListBuilder.java @@ -413,17 +413,19 @@ public ListBuilder continuationIndent(String continuationIndent) { * no marker — contributes no row, so its children hang at the level it would * have occupied rather than one deeper.

* - *

Fixed-layout only. This is geometry, and it applies to the - * backends that do their own layout — PDF and PPTX. The semantic DOCX + *

Fixed-layout, but for a drawn marker's gap. This is geometry, and + * it applies to the backends that do their own layout — PDF and PPTX. The semantic DOCX * export writes a Word paragraph per item and lets Word lay it out, so it - * keeps the marker in the item's text and is unchanged by this setting: the - * same paragraphs, the same text, the same nesting. Word positions content - * at absolute indents and has no way to be told "start the text one marker - * width plus a gap from here", so reproducing this geometry there would mean - * measuring the marker — which the semantic backend deliberately cannot do, - * since it depends on neither a font runtime nor a layout pass. A document - * exported both ways is therefore identical in content and nesting, and - * differs in how its wrapped lines line up.

+ * keeps the marker in the item's text: the same paragraphs, the same text, + * the same nesting. Word positions content at absolute indents and has no + * way to be told "start the text one marker width plus a gap from here", and + * sets a text marker in its own widths. Only a top-level item whose marker is + * the list's and a picture alone — a dot, an icon — has its first line's text + * placed where the layout puts it, a tab to the marker's width and the gap past + * the item's start, where the picture with its edges clears that stop: Word + * draws a picture at the size it is written. A document exported + * both ways is otherwise identical, and differs in how its wrapped lines line + * up.

* * @param hangingIndent whether items use marker/content geometry * @return this builder @@ -444,11 +446,11 @@ public ListBuilder hangingIndent(boolean hangingIndent) { *

Real geometry, never spaces. A markerless item takes no gap at all, * rather than an unexplained inset.

* - *

Fixed-layout only, for the reason given on - * {@link #hangingIndent(boolean)}: the semantic DOCX export does not lay text - * out and cannot place content a measured distance after a marker, so it - * ignores this value rather than approximating it with something that would - * render as a different number than the one asked for.

+ *

In the semantic DOCX export, for the reason given on + * {@link #hangingIndent(boolean)}, it places only the first line's text of a + * top-level item whose marker is a picture alone; after a marker of text it is + * a space, rather than an approximation that would render as a different + * number than the one asked for.

* * @param markerGap gap in points; {@code 0} is allowed * @return this builder diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index f60151119..009c90174 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -63,10 +63,10 @@ Payload records live in `core` under | Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) | |---|---|---|---| | Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, so a paragraph whose face puts the two half a point or more apart — Spectral's, not Lato's — has its text moved to the page's baseline in the same position, matched at its middle line (not yet a list item's or a table text cell's; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it | -| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ❌ ignored. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — identically whether the flag is set or not; content and nesting are unaffected. Word places content at absolute indents and has no relative-advance primitive, so honouring the gap would mean measuring the marker, which the semantic backend has no font runtime to do. Measured and rejected: a reserved-column approximation renders a different gap than the one configured, and misaligns outright for a marker wider than the column. Word numbering does not honour the gap either and does not claim to — the level's marker column is a stated constant (180 twips, plus 120 per nesting level), chosen near the single space the old text form used | +| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ⚠️ ignored, but for a drawn marker: a top-level item of a `hangingIndent(true)` list whose marker is a picture alone (a dot, an icon) has a tab to a stop where the layout starts its text, the picture's width and `markerGap` past the item's start, where the picture with its edges clears that stop, as Word draws a picture at the size it is written; its wrapped lines still start at the item's start. Otherwise `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — identically whether the flag is set or not; content and nesting are unaffected. Word places content at absolute indents and has no relative-advance primitive, so honouring the gap would mean measuring the marker, which the semantic backend has no font runtime to do. Measured and rejected: a reserved-column approximation renders a different gap than the one configured, and misaligns outright for a marker wider than the column. Word numbering does not honour the gap either and does not claim to — the level's marker column is a stated constant (180 twips, plus 120 per nesting level), chosen near the single space the old text form used | | Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding that widens the run on the page are not in the file, and the export records both. A `w:shd` fill is opaque, so a translucent chip is flattened first against what this export wrote underneath it — the paragraph's shading, the cell's, or the page — so the chip agrees with the file it is in, which on a white page is the colour the PDF shows. It stops being translucent, and that is recorded with the rest | | Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; a paragraph of one line of text in a Word paragraph of its own, with room above for its pictures' reach, keeps an exact line at the page's height of it, the pictures set in it where the page puts them in Word and what their ink reaches past it taken from the gaps around it, and in LibreOffice a lowered picture there stands higher and loses what passes the line's top; its description is the text it stands for or empty) | -| Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ⚠️ `DocxSemanticBackend.writeInlinePicture` + `DocxShapePictures` (a transparent PNG drawn by the shared `InlineSvgRasters` from the outline, fill and stroke — every outline kind, each layer centred in the run's box — placed as an inline picture is; the picture takes as far as the stroked ink reaches past the outline — half the stroke on an edge, more at a sharp corner's miter — and a pixel on each side, measured side by side, and is lowered by what it takes below, so no edge is cut and a shape takes that much more room in the line; a list marker that draws a disc is its picture, followed by a space) | +| Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ⚠️ `DocxSemanticBackend.writeInlinePicture` + `DocxShapePictures` (a transparent PNG drawn by the shared `InlineSvgRasters` from the outline, fill and stroke — every outline kind, each layer centred in the run's box — placed as an inline picture is; the picture takes as far as the stroked ink reaches past the outline — half the stroke on an edge, more at a sharp corner's miter — and a pixel on each side, measured side by side, and is lowered by what it takes below, so no edge is cut and a shape takes that much more room in the line; a list marker that draws a disc is its picture, followed by a tab to where the layout starts the item's text when the picture clears that stop, else by a space) | | Inline SVG (`ParagraphSvgSpan`) | ✅ `PdfParagraphFragmentRenderHandler` + `PdfPathPainter` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` + `PptxInlineSvgRasterizer` (simple layers stay native; arbitrary clips, exact dash/cap/join styles, and off-viewBox art use a transparent PNG fallback — drawn by the shared `InlineSvgRasters`; gradient paints use their primary colour) | ⚠️ `DocxSemanticBackend.writeInlinePicture` (always the transparent PNG: the same layers the layout resolves, through `InlineSvgLayers`, drawn by the raster the PPTX fallback uses — `InlineSvgRasters`, four pixels a point — and placed as an inline picture is; emoji included, so an emoji is a picture rather than a character, reported `APPROXIMATED`) | | Text an inline icon stands for — copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | ✅ `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF — it replaces glyphs, and a drawing has none. In a right-to-left line the glyph sits between the words it was written between and states its whole text; reading such a line back, PDFBox reverses the emoji one UTF-16 unit at a time and poppler reverses the code points of a ZWJ sequence or a U+FE0F pair, while pdf.js and MuPDF keep it whole (measured; a reader-side reversal of the glyph's text) | ❌ the icon is drawn and its text is not written | ⚠️ the icon is a picture whose description (`docPr/@descr`) is its text — read by a screen reader, but not a character a reader copies or searches | | Rectangle shape — fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | ✅ `PdfShapeFragmentRenderHandler` | ⚠️ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners — single-adjust `roundRect` preset — until custom geometry lands; uniform radii and side borders exact) | ⚠️ `DocxSemanticBackend.writeContainerChildren` — a `SectionNode` or `ContainerNode` carrying a fill, per-side borders or a uniform stroke is written as a one-cell table: the cell's shading is the fill behind everything inside, its borders are the container's at its full height, and its margins are the padding on all four sides, less half of each border, with the table half a border wider on each side, so the text and the border land where the page draws them. A panel with less space above it than its top border — such as one opening a table cell — takes what of the border neither that space nor its padding holds from the space above its first line, where that line has some, and where its row holds the page's height that height is less the borders Word draws outside it, since Word starts a cell's content below its top border or its top margin, whichever is wider (LibreOffice, where `MerchantInvoice`'s panel's content sets its height, draws it that border's width shorter). The table takes the width the layout placed the container at, plus a point of editor slack, and a `keepTogether()` panel the layout held on one page is a row that may not split. Panels, rows and tables inside it are tables in its cell; a table cell no style fills is written white, as the page draws it. One deviation: the corner radius is dropped (a cell is rectangular) with one warning per export. An unpainted container is not a table: its paragraphs are indented by every enclosing margin and padding, and its rows and tables get `w:tblInd`. A standalone `ShapeNode` that is only a fill, with no stroke, radius, gradient or transform, and no taller than Word's thickest border (12pt) — an `addDivider` — is a rule, written as a line's is; any other `ShapeNode`, standalone or laid over others, is drawn by `DocxDrawings` as a DrawingML shape anchored to the page behind the text, where the layout placed it — `rect`, or `roundRect` with the largest corner radius, in its fill and outline colours with their alpha, each side border a line along its edge; a gradient paint and a transform are not carried, and a shape with neither a fill colour nor an outline is dropped and reported. It is anchored in a body paragraph on its page (a table cell's only on a page with no other, which Word may print it clipped to, reported), stays where it is when the text is edited — except a drawing that is all a table cell holds, an icon beside its label (in a row of the flow, a layer stack or shape container that only draws, with no margins, on one page and painting nothing outside its box; in a composed cell, a layer stack of shapes with no margin or padding, the only drawing the cell holds, matched to the table's fragments inside the cell by kind and size), which is anchored in that cell's paragraph, held at its height and placed from its top and the cell's text column, so it moves with its row; a drawing holding a line stays on the page —, and is drawn in front of the text inside a filled panel unless it frames text or a picture there; reported `APPROXIMATED` | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index f89c5b5f6..32e4c5507 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -366,7 +366,11 @@ changing what was asked for: - **A markerless list.** Numbering always draws something and indents; a list that asked for neither would gain both. - **A drawn marker** — one made of runs, an icon or a disc. It has no Word list analogue, - so the item keeps the run path it already used. + so the item keeps the run path it already used. A top-level item of a + `hangingIndent(true)` list whose marker is a picture alone is followed by a tab to a stop + where the layout starts its text — the picture's width and `markerGap` past the item's + start, where the picture, its edges included, clears that stop — Word draws a picture at + the size it is written; any other marker by a space. - **A list whose siblings at one depth carry different markers.** A Word list definition names one marker per level, and silently replacing one of them with the other would be worse than writing both as text. @@ -700,12 +704,16 @@ tint it was flattened to. Recorded, like the other two. below. An outline no shape shows is reported as dropped. - **`hangingIndent(true)` → the ordinary list form.** A list that opts - into marker/content geometry exports exactly as one that did not: the - same Word list, the same levels, the same markers. Nothing is lost — - same items, same text, same nesting — but the marker column is the - level's own and `markerGap` has no effect here. - - This is a decision rather than an omission. Word places content at + into marker/content geometry exports as one that did not — the same Word + list, the same levels, the same markers, the same items, text and + nesting — but for one thing: after a top-level marker that is the list's and + a picture alone (see "What a list becomes"), the first line's text stands + where the layout puts it. Otherwise the marker column is the level's own and + `markerGap` has no effect here. + + That one exception rests on what the rest lacks: a picture is drawn at the + size it is written, and the layout has placed its text, so a tab stop there + is the page's distance. For a marker of text it is not: Word places content at absolute indents and has no way to be told "start the text one marker width plus a gap from here", so every mechanism that looks like it would — a hanging indent, a hanging indent with a tab stop, real Word diff --git a/docs/recipes/lists.md b/docs/recipes/lists.md index 7702fb565..0967edf62 100644 --- a/docs/recipes/lists.md +++ b/docs/recipes/lists.md @@ -94,13 +94,15 @@ inside the content column and leave the marker where it is. It is off by default and stays off; existing lists are untouched. -> **Fixed-layout only.** PDF and PPTX honour this. A DOCX list is a real -> Word list either way — same levels, same markers — but the marker column -> is the level's own rather than the gap configured here. The semantic DOCX -> export writes one Word paragraph per item and lets Word lay it out, so -> it keeps the marker in the text and exports the same either way — same -> paragraphs, same text, same nesting. See -> [DOCX export](docx-export.md). +> **Fixed-layout, but for a drawn marker's gap.** PDF and PPTX honour this. +> A DOCX list is a real Word list either way — same levels, same markers — +> but the marker column is the level's own rather than the gap configured +> here. The semantic DOCX export writes one Word paragraph per item and lets +> Word lay it out, so it keeps the marker in the text — same paragraphs, same +> text, same nesting. One exception: a top-level item whose marker is the +> list's and a picture alone (a dot, an icon) has its first line's text tabbed +> to where the layout puts it, the picture's width and the gap past the +> item's start. See [DOCX export](docx-export.md). ## Nested lists diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv index 1d3d532f8..71583eeb9 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -1001,17 +1001,17 @@ cv-orange_ops 0:e6da1125d5d5 -0.18 -0.37 marcus.bennett@email.com cv-orange_ops 0:6a3bd39f358d -0.18 -0.74 birmingham,westmidlands,uk cv-orange_ops 0:84308bea4540 -0.18 -1.02 linkedin cv-orange_ops 0:32c1cf49a2fe 0.03 0.12 skills -cv-orange_ops 0:8225698c0a01 -0.10 -5.82 warehouseoperationsmanagement -cv-orange_ops 0:fab5b1b3f07b -0.11 -5.82 teamleadership&development -cv-orange_ops 0:fe67bd8bcb96 -0.12 -5.82 inventorycontrol&accuracy -cv-orange_ops 0:52d5776e1f94 -0.14 -5.82 logistics&freightcoordination -cv-orange_ops 0:40c502892bf5 -0.15 -5.82 orderfulfilment&dispatch -cv-orange_ops 0:16d5a33ecc7e -0.16 -5.82 kpimonitoring&reporting -cv-orange_ops 0:01fc299910cc -0.18 -5.82 leanprocessimprovement -cv-orange_ops 0:d52f9933e794 -0.19 -5.82 health&safetycompliance -cv-orange_ops 0:2513b7af97dd -0.20 -5.82 wms&erpsystems -cv-orange_ops 0:034e1f3a8777 -0.22 -5.82 problemsolving&decisionmaking -cv-orange_ops 0:115925c1b96b -0.23 -5.82 stakeholdercommunication +cv-orange_ops 0:8225698c0a01 -0.10 0.13 warehouseoperationsmanagement +cv-orange_ops 0:fab5b1b3f07b -0.11 0.13 teamleadership&development +cv-orange_ops 0:fe67bd8bcb96 -0.12 0.13 inventorycontrol&accuracy +cv-orange_ops 0:52d5776e1f94 -0.14 0.13 logistics&freightcoordination +cv-orange_ops 0:40c502892bf5 -0.15 0.13 orderfulfilment&dispatch +cv-orange_ops 0:16d5a33ecc7e -0.16 0.13 kpimonitoring&reporting +cv-orange_ops 0:01fc299910cc -0.18 0.13 leanprocessimprovement +cv-orange_ops 0:d52f9933e794 -0.19 0.13 health&safetycompliance +cv-orange_ops 0:2513b7af97dd -0.20 0.13 wms&erpsystems +cv-orange_ops 0:034e1f3a8777 -0.22 0.13 problemsolving&decisionmaking +cv-orange_ops 0:115925c1b96b -0.23 0.13 stakeholdercommunication cv-orange_ops 0:6ee18b42fbf1 -0.20 0.12 achievements cv-orange_ops 0:8c76fa6dfb6b -0.17 0.06 15%productivityincrease cv-orange_ops 0:5ba0c9469ae2 -0.34 0.06 improvedwarehouseproductivity @@ -1474,18 +1474,18 @@ cv-teal_pulse 0:4ddba42ce115 0.93 0.14 isabella.moore@email.com cv-teal_pulse 0:a8acc3a90a7b 0.68 0.17 +447700900123 cv-teal_pulse 0:84308bea4540 0.83 0.16 linkedin cv-teal_pulse 0:32c1cf49a2fe -0.25 0.10 skills -cv-teal_pulse 0:9836c1fde1ab -0.09 -6.26 patientassessment -cv-teal_pulse 0:7344802265f6 -0.11 -6.26 careplanning -cv-teal_pulse 0:32b1d161b612 -0.13 -6.26 medicationadministration -cv-teal_pulse 0:b9fff349f0ab -0.15 -6.26 clinicaldocumentation -cv-teal_pulse 0:cc981996b30d -0.16 -6.26 infectionprevention -cv-teal_pulse 0:48d90339937f -0.18 -6.26 ivtherapy -cv-teal_pulse 0:a392ecf06df5 -0.20 -6.26 woundcare -cv-teal_pulse 0:7016cd40c2ef -0.21 -6.26 patienteducation -cv-teal_pulse 0:ff0a6cdd9508 -0.23 -6.26 safeguarding -cv-teal_pulse 0:7b6511893d61 -0.25 -6.26 teamcollaboration -cv-teal_pulse 0:d9ff32cb0736 -0.26 -6.26 dischargecoordination -cv-teal_pulse 0:ceb75137b9fb -0.28 -6.26 electronichealthrecords +cv-teal_pulse 0:9836c1fde1ab -0.09 0.08 patientassessment +cv-teal_pulse 0:7344802265f6 -0.11 0.08 careplanning +cv-teal_pulse 0:32b1d161b612 -0.13 0.08 medicationadministration +cv-teal_pulse 0:b9fff349f0ab -0.15 0.08 clinicaldocumentation +cv-teal_pulse 0:cc981996b30d -0.16 0.08 infectionprevention +cv-teal_pulse 0:48d90339937f -0.18 0.08 ivtherapy +cv-teal_pulse 0:a392ecf06df5 -0.20 0.08 woundcare +cv-teal_pulse 0:7016cd40c2ef -0.21 0.08 patienteducation +cv-teal_pulse 0:ff0a6cdd9508 -0.23 0.08 safeguarding +cv-teal_pulse 0:7b6511893d61 -0.25 0.08 teamcollaboration +cv-teal_pulse 0:d9ff32cb0736 -0.26 0.08 dischargecoordination +cv-teal_pulse 0:ceb75137b9fb -0.28 0.08 electronichealthrecords cv-teal_pulse 0:b944906ed84c -0.30 0.08 professionalsummary cv-teal_pulse 0:f8343b9099ef 0.65 0.06 compassionateregisterednursewith cv-teal_pulse 0:50347a4f84fc 0.64 0.06 high-qualitypatientcareacrossacu @@ -1495,29 +1495,29 @@ cv-teal_pulse 0:8d526cd52034 0.60 0.06 accurateclinicaldocumentation.kn cv-teal_pulse 0:4d61455b351b 0.59 0.06 communication,andpatient-centred cv-teal_pulse 0:c2f296ef30a3 -0.35 0.08 professionalexperience cv-teal_pulse 0:5cad9261fd60 -0.06 0.06 seniorstaffnurse—manchesterroyal -cv-teal_pulse 0:5149e8e5e6b6 -0.08 -6.56 deliverdirectnursingcareforadult -cv-teal_pulse 0:8dcc0d173718 -0.08 -6.56 coordinatecareplanswithdoctors,t -cv-teal_pulse 0:325626fb2f7a -0.09 -6.56 supervisejuniornursesandsupports -cv-teal_pulse 0:c2864de08768 -0.09 -6.56 improveddischargecoordinationand -cv-teal_pulse 0:3ac4bbda9488 -0.10 -6.56 maintainprecisedocumentationande +cv-teal_pulse 0:5149e8e5e6b6 -0.08 0.05 deliverdirectnursingcareforadult +cv-teal_pulse 0:8dcc0d173718 -0.08 0.05 coordinatecareplanswithdoctors,t +cv-teal_pulse 0:325626fb2f7a -0.09 0.05 supervisejuniornursesandsupports +cv-teal_pulse 0:c2864de08768 -0.09 0.05 improveddischargecoordinationand +cv-teal_pulse 0:3ac4bbda9488 -0.10 0.05 maintainprecisedocumentationande cv-teal_pulse 0:59a7aa760eea -0.07 0.06 staffnurse—salfordgeneralhospita -cv-teal_pulse 0:99bff63b0b23 -0.09 -6.56 providednursingcareacrossmedical -cv-teal_pulse 0:dee1c05c380e -0.09 -6.56 administeredmedications,monitore -cv-teal_pulse 0:a0cc1403feda -0.10 -6.56 supportedinfectioncontrolprocedu -cv-teal_pulse 0:25239222c011 -0.10 -6.56 builtstrongrapportwithpatientsan +cv-teal_pulse 0:99bff63b0b23 -0.09 0.05 providednursingcareacrossmedical +cv-teal_pulse 0:dee1c05c380e -0.09 0.05 administeredmedications,monitore +cv-teal_pulse 0:a0cc1403feda -0.10 0.05 supportedinfectioncontrolprocedu +cv-teal_pulse 0:25239222c011 -0.10 0.05 builtstrongrapportwithpatientsan cv-teal_pulse 0:f25d4331b19b -0.08 0.06 communitynurse—northcarecommunit -cv-teal_pulse 0:19eefb6b6368 -0.09 -6.56 deliveredhome-basednursingcarean -cv-teal_pulse 0:16412b3fd5d8 -0.10 -6.56 managedwoundcare,medicationsuppo -cv-teal_pulse 0:fb42708d13d3 -0.10 -6.56 workedcloselywithgps,socialservi +cv-teal_pulse 0:19eefb6b6368 -0.09 0.05 deliveredhome-basednursingcarean +cv-teal_pulse 0:16412b3fd5d8 -0.10 0.05 managedwoundcare,medicationsuppo +cv-teal_pulse 0:fb42708d13d3 -0.10 0.05 workedcloselywithgps,socialservi cv-teal_pulse 0:68bf0109c6f8 -0.26 0.11 education cv-teal_pulse 0:c1596da36d69 -0.21 0.10 bsc(hons)adultnursing cv-teal_pulse 0:47ce0218bbea -0.22 0.10 universityofmanchester cv-teal_pulse 0:1da060005bfc -0.25 0.10 2013–2016 cv-teal_pulse 0:e831beb04c00 -0.26 0.06 certifications -cv-teal_pulse 0:ec8bd1e014bb -0.19 -6.44 nmcregisterednurse -cv-teal_pulse 0:669f27afcccf -0.22 -6.44 immediatelifesupport(ils) -cv-teal_pulse 0:34b32be15830 -0.25 -6.44 venepunctureandcannulationcertif -cv-teal_pulse 0:32215e12a886 -0.28 -6.44 safeguardingadultslevel3 +cv-teal_pulse 0:ec8bd1e014bb -0.19 0.04 nmcregisterednurse +cv-teal_pulse 0:669f27afcccf -0.22 0.04 immediatelifesupport(ils) +cv-teal_pulse 0:34b32be15830 -0.25 0.04 venepunctureandcannulationcertif +cv-teal_pulse 0:32215e12a886 -0.28 0.04 safeguardingadultslevel3 cv-teal_pulse 0:676dfdd56ad2 -0.26 0.08 additionalinformation cv-teal_pulse 0:bcc890a69aa7 0.11 0.07 languages:english(native), cv-teal_pulse 0:de15ca9e3d9f 0.08 0.07 polish(conversational) diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv index 191858b45..05f39ac88 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -1380,18 +1380,18 @@ cv-teal_pulse 0:4ddba42ce115 0.93 0.09 isabella.moore@email.com cv-teal_pulse 0:a8acc3a90a7b 0.68 0.12 +447700900123 cv-teal_pulse 0:84308bea4540 0.83 0.12 linkedin cv-teal_pulse 0:32c1cf49a2fe -0.25 0.10 skills -cv-teal_pulse 0:9836c1fde1ab -0.09 -6.26 patientassessment -cv-teal_pulse 0:7344802265f6 -0.11 -6.26 careplanning -cv-teal_pulse 0:32b1d161b612 -0.13 -6.26 medicationadministration -cv-teal_pulse 0:b9fff349f0ab -0.15 -6.26 clinicaldocumentation -cv-teal_pulse 0:cc981996b30d -0.16 -6.26 infectionprevention -cv-teal_pulse 0:48d90339937f -0.18 -6.26 ivtherapy -cv-teal_pulse 0:a392ecf06df5 -0.20 -6.26 woundcare -cv-teal_pulse 0:7016cd40c2ef -0.21 -6.26 patienteducation -cv-teal_pulse 0:ff0a6cdd9508 -0.23 -6.26 safeguarding -cv-teal_pulse 0:7b6511893d61 -0.25 -6.26 teamcollaboration -cv-teal_pulse 0:d9ff32cb0736 -0.26 -6.26 dischargecoordination -cv-teal_pulse 0:ceb75137b9fb -0.28 -6.26 electronichealthrecords +cv-teal_pulse 0:9836c1fde1ab -0.09 0.08 patientassessment +cv-teal_pulse 0:7344802265f6 -0.11 0.08 careplanning +cv-teal_pulse 0:32b1d161b612 -0.13 0.08 medicationadministration +cv-teal_pulse 0:b9fff349f0ab -0.15 0.08 clinicaldocumentation +cv-teal_pulse 0:cc981996b30d -0.16 0.08 infectionprevention +cv-teal_pulse 0:48d90339937f -0.18 0.08 ivtherapy +cv-teal_pulse 0:a392ecf06df5 -0.20 0.08 woundcare +cv-teal_pulse 0:7016cd40c2ef -0.21 0.08 patienteducation +cv-teal_pulse 0:ff0a6cdd9508 -0.23 0.08 safeguarding +cv-teal_pulse 0:7b6511893d61 -0.25 0.08 teamcollaboration +cv-teal_pulse 0:d9ff32cb0736 -0.26 0.08 dischargecoordination +cv-teal_pulse 0:ceb75137b9fb -0.28 0.08 electronichealthrecords cv-teal_pulse 0:b944906ed84c -0.30 0.08 professionalsummary cv-teal_pulse 0:f8343b9099ef 0.65 0.06 compassionateregisterednursewith cv-teal_pulse 0:50347a4f84fc 0.64 0.06 high-qualitypatientcareacrossacu @@ -1401,29 +1401,29 @@ cv-teal_pulse 0:8d526cd52034 0.60 0.06 accurateclinicaldocumentation.kn cv-teal_pulse 0:4d61455b351b 0.59 0.06 communication,andpatient-centred cv-teal_pulse 0:c2f296ef30a3 -0.35 0.08 professionalexperience cv-teal_pulse 0:5cad9261fd60 -0.06 0.06 seniorstaffnurse—manchesterroyal -cv-teal_pulse 0:5149e8e5e6b6 -0.08 -6.60 deliverdirectnursingcareforadult -cv-teal_pulse 0:8dcc0d173718 -0.08 -6.60 coordinatecareplanswithdoctors,t -cv-teal_pulse 0:325626fb2f7a -0.09 -6.60 supervisejuniornursesandsupports -cv-teal_pulse 0:c2864de08768 -0.09 -6.60 improveddischargecoordinationand -cv-teal_pulse 0:3ac4bbda9488 -0.10 -6.60 maintainprecisedocumentationande +cv-teal_pulse 0:5149e8e5e6b6 -0.08 0.05 deliverdirectnursingcareforadult +cv-teal_pulse 0:8dcc0d173718 -0.08 0.05 coordinatecareplanswithdoctors,t +cv-teal_pulse 0:325626fb2f7a -0.09 0.05 supervisejuniornursesandsupports +cv-teal_pulse 0:c2864de08768 -0.09 0.05 improveddischargecoordinationand +cv-teal_pulse 0:3ac4bbda9488 -0.10 0.05 maintainprecisedocumentationande cv-teal_pulse 0:59a7aa760eea -0.07 0.06 staffnurse—salfordgeneralhospita -cv-teal_pulse 0:99bff63b0b23 -0.09 -6.60 providednursingcareacrossmedical -cv-teal_pulse 0:dee1c05c380e -0.09 -6.60 administeredmedications,monitore -cv-teal_pulse 0:a0cc1403feda -0.10 -6.60 supportedinfectioncontrolprocedu -cv-teal_pulse 0:25239222c011 -0.10 -6.60 builtstrongrapportwithpatientsan +cv-teal_pulse 0:99bff63b0b23 -0.09 0.05 providednursingcareacrossmedical +cv-teal_pulse 0:dee1c05c380e -0.09 0.05 administeredmedications,monitore +cv-teal_pulse 0:a0cc1403feda -0.10 0.05 supportedinfectioncontrolprocedu +cv-teal_pulse 0:25239222c011 -0.10 0.05 builtstrongrapportwithpatientsan cv-teal_pulse 0:f25d4331b19b -0.08 0.06 communitynurse—northcarecommunit -cv-teal_pulse 0:19eefb6b6368 -0.09 -6.60 deliveredhome-basednursingcarean -cv-teal_pulse 0:16412b3fd5d8 -0.10 -6.60 managedwoundcare,medicationsuppo -cv-teal_pulse 0:fb42708d13d3 -0.10 -6.60 workedcloselywithgps,socialservi +cv-teal_pulse 0:19eefb6b6368 -0.09 0.05 deliveredhome-basednursingcarean +cv-teal_pulse 0:16412b3fd5d8 -0.10 0.05 managedwoundcare,medicationsuppo +cv-teal_pulse 0:fb42708d13d3 -0.10 0.05 workedcloselywithgps,socialservi cv-teal_pulse 0:68bf0109c6f8 -0.26 0.11 education cv-teal_pulse 0:c1596da36d69 -0.21 0.10 bsc(hons)adultnursing cv-teal_pulse 0:47ce0218bbea -0.22 0.10 universityofmanchester cv-teal_pulse 0:1da060005bfc -0.25 0.10 2013–2016 cv-teal_pulse 0:e831beb04c00 -0.26 0.06 certifications -cv-teal_pulse 0:ec8bd1e014bb -0.19 -6.46 nmcregisterednurse -cv-teal_pulse 0:669f27afcccf -0.22 -6.46 immediatelifesupport(ils) -cv-teal_pulse 0:34b32be15830 -0.25 -6.46 venepunctureandcannulationcertif -cv-teal_pulse 0:32215e12a886 -0.28 -6.46 safeguardingadultslevel3 +cv-teal_pulse 0:ec8bd1e014bb -0.19 0.04 nmcregisterednurse +cv-teal_pulse 0:669f27afcccf -0.22 0.04 immediatelifesupport(ils) +cv-teal_pulse 0:34b32be15830 -0.25 0.04 venepunctureandcannulationcertif +cv-teal_pulse 0:32215e12a886 -0.28 0.04 safeguardingadultslevel3 cv-teal_pulse 0:676dfdd56ad2 -0.26 0.08 additionalinformation cv-teal_pulse 0:bcc890a69aa7 0.11 0.07 languages:english(native), cv-teal_pulse 0:de15ca9e3d9f 0.08 0.07 polish(conversational) diff --git a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv index b8a8ef34e..5e9cde8de 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -1001,17 +1001,17 @@ cv-orange_ops 0:e6da1125d5d5 -0.01 -0.62 marcus.bennett@email.com cv-orange_ops 0:6a3bd39f358d -0.01 -0.88 birmingham,westmidlands,uk cv-orange_ops 0:84308bea4540 -0.01 -1.08 linkedin cv-orange_ops 0:32c1cf49a2fe 0.11 0.03 skills -cv-orange_ops 0:8225698c0a01 -0.02 -5.98 warehouseoperationsmanagement -cv-orange_ops 0:fab5b1b3f07b -0.07 -5.98 teamleadership&development -cv-orange_ops 0:fe67bd8bcb96 0.00 -5.98 inventorycontrol&accuracy -cv-orange_ops 0:52d5776e1f94 -0.06 -5.98 logistics&freightcoordination -cv-orange_ops 0:40c502892bf5 -0.11 -5.98 orderfulfilment&dispatch -cv-orange_ops 0:16d5a33ecc7e -0.04 -5.98 kpimonitoring&reporting -cv-orange_ops 0:01fc299910cc -0.10 -5.98 leanprocessimprovement -cv-orange_ops 0:d52f9933e794 -0.15 -5.98 health&safetycompliance -cv-orange_ops 0:2513b7af97dd -0.08 -5.98 wms&erpsystems -cv-orange_ops 0:034e1f3a8777 -0.11 -5.98 problemsolving&decisionmaking -cv-orange_ops 0:115925c1b96b -0.16 -5.98 stakeholdercommunication +cv-orange_ops 0:8225698c0a01 -0.02 0.02 warehouseoperationsmanagement +cv-orange_ops 0:fab5b1b3f07b -0.07 0.02 teamleadership&development +cv-orange_ops 0:fe67bd8bcb96 0.00 0.02 inventorycontrol&accuracy +cv-orange_ops 0:52d5776e1f94 -0.06 0.02 logistics&freightcoordination +cv-orange_ops 0:40c502892bf5 -0.11 0.02 orderfulfilment&dispatch +cv-orange_ops 0:16d5a33ecc7e -0.04 0.02 kpimonitoring&reporting +cv-orange_ops 0:01fc299910cc -0.10 0.02 leanprocessimprovement +cv-orange_ops 0:d52f9933e794 -0.15 0.02 health&safetycompliance +cv-orange_ops 0:2513b7af97dd -0.08 0.02 wms&erpsystems +cv-orange_ops 0:034e1f3a8777 -0.11 0.02 problemsolving&decisionmaking +cv-orange_ops 0:115925c1b96b -0.16 0.02 stakeholdercommunication cv-orange_ops 0:6ee18b42fbf1 -0.12 0.03 achievements cv-orange_ops 0:8c76fa6dfb6b -0.10 0.06 15%productivityincrease cv-orange_ops 0:5ba0c9469ae2 -0.33 0.06 improvedwarehouseproductivity @@ -1473,18 +1473,18 @@ cv-teal_pulse 0:4ddba42ce115 1.14 0.08 isabella.moore@email.com cv-teal_pulse 0:a8acc3a90a7b 1.01 0.05 +447700900123 cv-teal_pulse 0:84308bea4540 1.12 0.11 linkedin cv-teal_pulse 0:32c1cf49a2fe -0.25 -0.04 skills -cv-teal_pulse 0:9836c1fde1ab -0.07 -6.58 patientassessment -cv-teal_pulse 0:7344802265f6 -0.07 -6.58 careplanning -cv-teal_pulse 0:32b1d161b612 -0.10 -6.58 medicationadministration -cv-teal_pulse 0:b9fff349f0ab -0.13 -6.58 clinicaldocumentation -cv-teal_pulse 0:cc981996b30d -0.03 -6.58 infectionprevention -cv-teal_pulse 0:48d90339937f -0.06 -6.58 ivtherapy -cv-teal_pulse 0:a392ecf06df5 -0.07 -6.58 woundcare -cv-teal_pulse 0:7016cd40c2ef -0.09 -6.58 patienteducation -cv-teal_pulse 0:ff0a6cdd9508 -0.12 -6.58 safeguarding -cv-teal_pulse 0:7b6511893d61 -0.15 -6.58 teamcollaboration -cv-teal_pulse 0:d9ff32cb0736 -0.17 -6.58 dischargecoordination -cv-teal_pulse 0:ceb75137b9fb -0.20 -6.58 electronichealthrecords +cv-teal_pulse 0:9836c1fde1ab -0.07 -0.10 patientassessment +cv-teal_pulse 0:7344802265f6 -0.07 -0.10 careplanning +cv-teal_pulse 0:32b1d161b612 -0.10 -0.10 medicationadministration +cv-teal_pulse 0:b9fff349f0ab -0.13 -0.10 clinicaldocumentation +cv-teal_pulse 0:cc981996b30d -0.03 -0.10 infectionprevention +cv-teal_pulse 0:48d90339937f -0.06 -0.10 ivtherapy +cv-teal_pulse 0:a392ecf06df5 -0.07 -0.10 woundcare +cv-teal_pulse 0:7016cd40c2ef -0.09 -0.10 patienteducation +cv-teal_pulse 0:ff0a6cdd9508 -0.12 -0.10 safeguarding +cv-teal_pulse 0:7b6511893d61 -0.15 -0.10 teamcollaboration +cv-teal_pulse 0:d9ff32cb0736 -0.17 -0.10 dischargecoordination +cv-teal_pulse 0:ceb75137b9fb -0.20 -0.10 electronichealthrecords cv-teal_pulse 0:b944906ed84c -0.27 0.13 professionalsummary cv-teal_pulse 0:f8343b9099ef 0.38 0.05 compassionateregisterednursewith cv-teal_pulse 0:50347a4f84fc 0.43 0.05 high-qualitypatientcareacrossacu @@ -1494,29 +1494,29 @@ cv-teal_pulse 0:8d526cd52034 0.36 0.05 accurateclinicaldocumentation.kn cv-teal_pulse 0:4d61455b351b 0.38 0.05 communication,andpatient-centred cv-teal_pulse 0:c2f296ef30a3 -0.19 0.13 professionalexperience cv-teal_pulse 0:5cad9261fd60 0.02 0.05 seniorstaffnurse—manchesterroyal -cv-teal_pulse 0:5149e8e5e6b6 0.11 -6.72 deliverdirectnursingcareforadult -cv-teal_pulse 0:8dcc0d173718 0.10 -6.72 coordinatecareplanswithdoctors,t -cv-teal_pulse 0:325626fb2f7a 0.08 -6.72 supervisejuniornursesandsupports -cv-teal_pulse 0:c2864de08768 0.06 -6.72 improveddischargecoordinationand -cv-teal_pulse 0:3ac4bbda9488 0.05 -6.72 maintainprecisedocumentationande +cv-teal_pulse 0:5149e8e5e6b6 0.11 0.00 deliverdirectnursingcareforadult +cv-teal_pulse 0:8dcc0d173718 0.10 0.00 coordinatecareplanswithdoctors,t +cv-teal_pulse 0:325626fb2f7a 0.08 0.00 supervisejuniornursesandsupports +cv-teal_pulse 0:c2864de08768 0.06 0.00 improveddischargecoordinationand +cv-teal_pulse 0:3ac4bbda9488 0.05 0.00 maintainprecisedocumentationande cv-teal_pulse 0:59a7aa760eea -0.02 0.05 staffnurse—salfordgeneralhospita -cv-teal_pulse 0:99bff63b0b23 0.05 -6.72 providednursingcareacrossmedical -cv-teal_pulse 0:dee1c05c380e 0.04 -6.72 administeredmedications,monitore -cv-teal_pulse 0:a0cc1403feda 0.02 -6.72 supportedinfectioncontrolprocedu -cv-teal_pulse 0:25239222c011 0.03 -6.72 builtstrongrapportwithpatientsan +cv-teal_pulse 0:99bff63b0b23 0.05 0.00 providednursingcareacrossmedical +cv-teal_pulse 0:dee1c05c380e 0.04 0.00 administeredmedications,monitore +cv-teal_pulse 0:a0cc1403feda 0.02 0.00 supportedinfectioncontrolprocedu +cv-teal_pulse 0:25239222c011 0.03 0.00 builtstrongrapportwithpatientsan cv-teal_pulse 0:f25d4331b19b -0.04 0.05 communitynurse—northcarecommunit -cv-teal_pulse 0:19eefb6b6368 0.04 -6.72 deliveredhome-basednursingcarean -cv-teal_pulse 0:16412b3fd5d8 0.02 -6.72 managedwoundcare,medicationsuppo -cv-teal_pulse 0:fb42708d13d3 0.13 -6.72 workedcloselywithgps,socialservi +cv-teal_pulse 0:19eefb6b6368 0.04 0.00 deliveredhome-basednursingcarean +cv-teal_pulse 0:16412b3fd5d8 0.02 0.00 managedwoundcare,medicationsuppo +cv-teal_pulse 0:fb42708d13d3 0.13 0.00 workedcloselywithgps,socialservi cv-teal_pulse 0:68bf0109c6f8 -0.19 -0.06 education cv-teal_pulse 0:c1596da36d69 -0.11 -0.04 bsc(hons)adultnursing cv-teal_pulse 0:47ce0218bbea -0.08 -0.04 universityofmanchester cv-teal_pulse 0:1da060005bfc -0.10 -0.04 2013–2016 cv-teal_pulse 0:e831beb04c00 -0.19 0.01 certifications -cv-teal_pulse 0:ec8bd1e014bb -0.11 -6.60 nmcregisterednurse -cv-teal_pulse 0:669f27afcccf 0.01 -6.60 immediatelifesupport(ils) -cv-teal_pulse 0:34b32be15830 -0.11 -6.60 venepunctureandcannulationcertif -cv-teal_pulse 0:32215e12a886 -0.12 -6.60 safeguardingadultslevel3 +cv-teal_pulse 0:ec8bd1e014bb -0.11 0.00 nmcregisterednurse +cv-teal_pulse 0:669f27afcccf 0.01 0.00 immediatelifesupport(ils) +cv-teal_pulse 0:34b32be15830 -0.11 0.00 venepunctureandcannulationcertif +cv-teal_pulse 0:32215e12a886 -0.12 0.00 safeguardingadultslevel3 cv-teal_pulse 0:676dfdd56ad2 -0.19 0.10 additionalinformation cv-teal_pulse 0:bcc890a69aa7 -0.11 0.14 languages:english(native), cv-teal_pulse 0:de15ca9e3d9f -0.02 0.14 polish(conversational) diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java index ecc2713a7..7e6b80e6b 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java @@ -691,6 +691,32 @@ java.util.Optional firstLine(DocumentNode node) { return java.util.Optional.empty(); } + /** + * How far right of its marker a list's first item's text starts, as the page sets it: the + * marker's width and the gap after it. A list whose marker the page draws is laid out as + * a fragment for the marker and one for the item's text beside it. + * + * @param list the list + * @return the distance in points, or empty when the list laid out no marker beside its text + */ + OptionalDouble markerToText(DocumentNode list) { + List own = fragmentsOf(list); + for (int index = 0; index + 1 < own.size(); index++) { + if (own.get(index).payload() instanceof ParagraphFragmentPayload marker + && own.get(index + 1).payload() instanceof ParagraphFragmentPayload text + && drawsOnly(marker) && !drawsOnly(text)) { + return OptionalDouble.of(own.get(index + 1).x() - own.get(index).x()); + } + } + return OptionalDouble.empty(); + } + + /** Whether a paragraph fragment holds nothing but drawings — a list's drawn marker. */ + private static boolean drawsOnly(ParagraphFragmentPayload paragraph) { + return !paragraph.lines().isEmpty() && paragraph.lines().stream().allMatch(line -> !line.spans().isEmpty() + && line.spans().stream().noneMatch(span -> span instanceof com.demcha.compose.document.layout.payloads.ParagraphTextSpan)); + } + /** * Where a node's first line of text starts, measured up from the foot of its page: the top * of the first fragment holding its lines, inside the paragraph's padding, where the page diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java index 90e0a0348..70b46271c 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java @@ -2905,7 +2905,7 @@ private void writeListItems(XWPFDocument document, // any row with runs in it is; its item is still just a label. writeRichListLine(document, list.textStyle(), list.marker(), com.demcha.compose.document.node.ListItem.of(normalized), 0, lineHeight, layout.firstLine(list), - layout.pathOf(list)); + layout.pathOf(list), layout.markerToText(list)); } else if (numId != null) { // Word draws the marker, so the text is the item and nothing else. writeListLine(document, list.textStyle(), normalized, 0, numId, lineHeight); @@ -2934,8 +2934,13 @@ private void writeNestedItem(XWPFDocument document, : com.demcha.compose.document.node.ListMarker.defaultForDepth(depth); java.util.OptionalDouble lineHeight = layout.lineHeight(list); if (item.isRich() || marker.isRich()) { + // A nested item stands after its depth's indent, and an item may carry a marker of + // its own: the list's measure of its first item's marker is only a top-level item's + // that carries the list's. + java.util.OptionalDouble markerToText = depth == 0 && marker.equals(list.marker()) + ? layout.markerToText(list) : java.util.OptionalDouble.empty(); writeRichListLine(document, list.textStyle(), marker, item, depth, lineHeight, layout.firstLine(list), - layout.pathOf(list)); + layout.pathOf(list), markerToText); } else if (numId != null) { writeListLine(document, list.textStyle(), item.label(), depth, numId, lineHeight); } else { @@ -3001,8 +3006,9 @@ private void indentListItemInside(XWPFParagraph para, int depth) { * *

This is the part of the opt-in marker/content list the semantic export * can reproduce, and it reproduces it exactly. The geometry — the measured - * marker column, the gap, the shared content origin — is unavailable here for - * the reason {@code hangingIndent} documents. Which piece of an item is bold + * marker column, the shared content origin — is not written, for the reason + * {@code hangingIndent} documents; the gap after a marker that is a picture + * alone is, as a tab to where the layout starts the text. Which piece of an item is bold * is not geometry: Word carries a style per run inside a paragraph, so * writing the item's plain reading in one face would be dropping something * Word can hold.

@@ -3011,7 +3017,12 @@ private void indentListItemInside(XWPFParagraph para, int depth) { * and face it was given, because those are run properties Word has. A marker * that draws a disc or an icon is written as the picture it draws, the way a * shape or an icon in a line is, rather than as a glyph the author did not ask - * for — and is followed by the same space as a text marker.

+ * for — and is followed by that tab, or, where the layout measured no gap or the + * picture reaches the stop, by the same space as a text marker.

+ * + * @param markerToText how far right of its marker the layout starts the item's text, or + * empty when that is not this item's — a nested item, or one with a + * marker of its own */ private void writeRichListLine(XWPFDocument document, DocumentTextStyle style, com.demcha.compose.document.node.ListMarker marker, @@ -3019,7 +3030,7 @@ private void writeRichListLine(XWPFDocument document, DocumentTextStyle style, int depth, java.util.OptionalDouble lineHeight, java.util.Optional line, - String path) { + String path, java.util.OptionalDouble markerToText) { warnDroppedInlineRuns(marker.runs(), path); warnDroppedInlineRuns(item.runs(), path); line = line.map(listLine -> itemLine(listLine, item.runs())); @@ -3033,16 +3044,30 @@ private void writeRichListLine(XWPFDocument document, DocumentTextStyle style, PictureReach pictures = PictureReach.NONE; if (marker.isRich()) { pictures = writeInlineTextRuns(para, style, marker.runs(), path, line); - // The gap after a marker is markerGap, which is geometry and so not - // available here; a space is what separates a marker from its item on - // the text path, and it separates them here for the same reason. - // A marker that drew a picture separates the same way; one whose picture had no - // data drew nothing, and gets no space either. - if (!com.demcha.compose.document.node.InlineRun.plainText(marker.runs()).isBlank() - || pictures.reach() > 0) { + // A space is what separates a marker of text from its item, as on the text path: + // Word sets the marker in its own widths. A marker that drew a picture is followed + // by a tab where the layout measured its gap; one whose picture had no data drew + // nothing, and gets no space either. + boolean drawnOnly = com.demcha.compose.document.node.InlineRun.plainText(marker.runs()).isBlank(); + if (!drawnOnly || pictures.reach() > 0) { XWPFRun gap = para.createRun(); applyStyle(gap, style); - gap.setText(" "); + // A picture alone: a blank run beside it is no text the page keeps — the layout + // drops it — and is a space Word does set, which the stop is not measured past. + boolean pictureAlone = marker.runs().stream().allMatch(run -> textOf(run) == null); + if (pictureAlone && markerToText.isPresent() + && markerToText.getAsDouble() > drawnWidth(para) + MARKER_TAB_CLEARANCE) { + // A marker that is a picture alone is drawn at the size it is written, so its + // text can stand where the page sets it, the marker's width and markerGap past + // it: a tab to a stop there. A space put TealPulse's skills and highlights + // 6.3 to 6.7pt left of the page's, the gap after their dots 9.2pt where a + // space is 2.5. Not where the picture, its edges included, reaches the stop: the tab + // would run on to Word's next default stop, half an inch on. + gap.addTab(); + tabTo(para, markerToText.getAsDouble()); + } else { + gap.setText(" "); + } } } if (item.isRich()) { @@ -3056,6 +3081,46 @@ private void writeRichListLine(XWPFDocument document, DocumentTextStyle style, styleTheMark(para, style); } + /** How far past a list marker's picture its tab stop must stand for the tab to reach it, in points. */ + private static final double MARKER_TAB_CLEARANCE = 0.1; + + /** How wide the pictures written inline in a paragraph so far are, in points. */ + private static double drawnWidth(XWPFParagraph para) { + double width = 0; + for (XWPFRun run : para.getRuns()) { + for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing drawing : run.getCTR().getDrawingList()) { + for (var inline : drawing.getInlineList()) { + if (inline.getExtent() != null) { + width += inline.getExtent().getCx() / (double) org.apache.poi.util.Units.EMU_PER_POINT; + } + } + } + } + return width; + } + + /** + * Sets a left tab stop a distance right of where a paragraph's first line starts: past its + * left indent and its first-line indent, as Word measures a tab stop from the column's edge. + * + * @param points how far right of the first line's start, in points + */ + private static void tabTo(XWPFParagraph para, double points) { + CTPPr properties = para.getCTP().isSetPPr() ? para.getCTP().getPPr() : para.getCTP().addNewPPr(); + long start = 0; + if (properties.isSetInd()) { + CTInd indent = properties.getInd(); + start = twipsOf(indent.isSetLeft() ? indent.getLeft() : null) + + twipsOf(indent.isSetFirstLine() ? indent.getFirstLine() : null) + - twipsOf(indent.isSetHanging() ? indent.getHanging() : null); + } + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTabs tabs = + properties.isSetTabs() ? properties.getTabs() : properties.addNewTabs(); + CTTabStop stop = tabs.addNewTab(); + stop.setVal(STTabJc.LEFT); + stop.setPos(BigInteger.valueOf(start + Math.round(points * POINT_TO_TWIP))); + } + /** * Appends one Word run per text-carrying inline run, each in its own style * and falling back to {@code style} when it has none — and, for a chip, on the diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxHangingIndentIsIgnoredTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxHangingIndentIsIgnoredTest.java index e854d3187..b7b649318 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxHangingIndentIsIgnoredTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxHangingIndentIsIgnoredTest.java @@ -19,8 +19,9 @@ /** * {@code hangingIndent} is fixed-layout geometry, and the semantic DOCX export does not - * lay text out — so it exports a list exactly the same way whether the flag is set or - * not. + * lay text out — so it exports a list of text markers exactly the same way whether the + * flag is set or not. (A top-level marker that is a picture alone is the exception, its + * first line's text tabbed to where the layout puts it: {@code DocxListMarkerGapTest}.) * *

That is a decision rather than an omission, and it was made against measurements. * Word places content at absolute indents and has no way to be told "start the text one @@ -42,7 +43,8 @@ * and it still does not honour it. What it buys is behaviour — Enter continues the list — * at the price of a marker column that is a stated constant rather than the configured * gap. So the decision this test pins is unchanged and its subject is narrower than it - * was: not "no numbering", but "the flag and the gap change nothing about the output".

+ * was: not "no numbering", but "the flag and the gap change nothing about the output" of a + * list of text markers.

* * @author Artem Demchyshyn */ diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListMarkerGapTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListMarkerGapTest.java new file mode 100644 index 000000000..63a0c7fc7 --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListMarkerGapTest.java @@ -0,0 +1,145 @@ +package com.demcha.compose.document.backend.semantic.docx; + +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A list item whose marker is a picture alone starts its text where the page does: the marker's + * width and {@code markerGap} past where the item starts. + * + *

The marker was followed by a space, a little over two points where the page leaves the + * gap asked for: {@code TealPulse}'s skills and highlights, a 3.2pt dot and a 9.2pt gap, stood + * 6.3 to 6.7pt left of the page's in both editors. Word draws a picture at the size it is written, so + * a tab to a stop there puts the text in place — where the picture, its edges included, clears + * the stop; a marker of text, which Word sets in its own widths, keeps its space.

+ */ +class DocxListMarkerGapTest { + + private static final double DOT = 3.4; + private static final double GAP = 9.56; + + @Test + void aDrawnMarkersItemStandsTheMarkerAndItsGapPastTheItemsStart() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addSection("Column", column -> column.padding(new DocumentInsets(0, 0, 0, 30)) + .addList(list -> list.name("Skills") + .marker(m -> m.dot(DOT, DocumentColor.rgb(0, 128, 128))) + .markerGap(GAP).hangingIndent(true).normalizeMarkers(false) + .addItem("Patient Assessment").addItem("Care Planning"))))) { + for (String item : new String[] {"Patient Assessment", "Care Planning"}) { + XWPFParagraph paragraph = item(document, item); + + assertThat(paragraph.getText()).isEqualTo("\t" + item); + assertThat(tabStop(paragraph)).as("past the column's 30pt indent, the dot and its gap") + .isEqualTo(leftIndent(paragraph) + Math.round((DOT + GAP) * 20)); + assertThat(leftIndent(paragraph)).as("the item starts at the column's indent").isEqualTo(600); + } + } + } + + @Test + void aMarkerOfTextKeepsItsSpace() throws Exception { + // Word sets a text marker in its own widths: a stop past it could fall short of it. + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addList(list -> list.marker(m -> m.color("•", DocumentColor.rgb(0, 128, 128))) + .markerGap(GAP).hangingIndent(true).addItem("Patient Assessment")))) { + XWPFParagraph paragraph = item(document, "Patient Assessment"); + + assertThat(paragraph.getText()).isEqualTo("• Patient Assessment"); + assertThat(paragraph.getCTP().getPPr() == null || !paragraph.getCTP().getPPr().isSetTabs()).isTrue(); + } + } + + @Test + void aDrawnMarkerWithNoGapToClearKeepsItsSpace() throws Exception { + // Its picture, edges and all, reaches past a stop at the dot's width: a tab there would + // run on to Word's next default stop, half an inch on. + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addList(list -> list.marker(m -> m.dot(4, DocumentColor.rgb(0, 128, 128))) + .markerGap(0).hangingIndent(true).addItem("Patient Assessment")))) { + XWPFParagraph paragraph = item(document, "Patient Assessment"); + + assertThat(paragraph.getText()).isEqualTo(" Patient Assessment"); + } + } + + @Test + void aDrawnMarkerWithABlankRunBesideItKeepsItsSpace() throws Exception { + // The layout drops the blank run and Word sets it: a stop measured past the dot alone + // could fall inside the space, and the tab run on half an inch. + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addList(list -> list.marker(m -> m.dot(4, DocumentColor.rgb(0, 128, 128)).plain(" ")) + .textStyle(com.demcha.compose.document.style.DocumentTextStyle.DEFAULT.withSize(14)) + .hangingIndent(true).addItem("Patient Assessment")))) { + XWPFParagraph paragraph = item(document, "Patient Assessment"); + + assertThat(paragraph.getText()).doesNotContain("\t"); + } + } + + @Test + void aTopLevelItemOfAListWithRichItemsStandsWhereThePageSetsItToo() throws Exception { + // Rich items make the list a tree, written item by item; a top-level one carrying the + // list's marker is placed as a flat one is. + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addRow("Columns", row -> row + .addSection("Side", side -> side.addList(list -> list + .marker(m -> m.dot(DOT, DocumentColor.rgb(0, 128, 128))) + .markerGap(GAP).hangingIndent(true) + .addItem(rich -> rich.bold("Label:").plain(" description")))) + .addSection("Main", main -> main.addParagraph("Main"))))) { + XWPFParagraph paragraph = item(document, "Label: description"); + + assertThat(paragraph.getText()).startsWith("\t"); + assertThat(tabStop(paragraph)).as("in a cell, from the cell's text edge") + .isEqualTo(leftIndent(paragraph) + Math.round((DOT + GAP) * 20)); + } + } + + @Test + void aNestedItemsDrawnMarkerKeepsItsSpace() throws Exception { + // A nested item stands after its depth's indent, which the list's measure of its first + // item's marker does not hold. + try (XWPFDocument document = DocxExports.withLayout(400, 300, 20, page -> page + .addList(list -> list.marker(m -> m.dot(DOT, DocumentColor.rgb(0, 128, 128))) + .markerFor(1, drawnDot()) + .markerGap(GAP).hangingIndent(true) + .addItem("Parent", parent -> parent.addItem("Child"))))) { + XWPFParagraph child = item(document, "Child"); + + assertThat(child.getRuns()).as("the child's marker is drawn too") + .anyMatch(run -> !run.getCTR().getDrawingList().isEmpty()); + assertThat(child.getText()).doesNotContain("\t"); + } + } + + /** A dot marker, as a list built with one carries it. */ + private static com.demcha.compose.document.node.ListMarker drawnDot() { + return new com.demcha.compose.document.dsl.ListBuilder() + .marker(m -> m.dot(DOT, DocumentColor.rgb(0, 128, 128))).addItem("x").build().marker(); + } + + private static XWPFParagraph item(XWPFDocument document, String text) { + java.util.List paragraphs = new java.util.ArrayList<>(document.getParagraphs()); + document.getTables().forEach(table -> table.getRows().forEach(row -> row.getTableCells() + .forEach(cell -> paragraphs.addAll(cell.getParagraphs())))); + return paragraphs.stream().filter(p -> p.getText().endsWith(text)).findFirst().orElseThrow(); + } + + private static long tabStop(XWPFParagraph paragraph) { + var stops = paragraph.getCTP().getPPr().getTabs().getTabArray(); + assertThat(stops).hasSize(1); + return DocxTwips.of(stops[0].getPos()); + } + + private static long leftIndent(XWPFParagraph paragraph) { + var properties = paragraph.getCTP().getPPr(); + return properties == null || !properties.isSetInd() || !properties.getInd().isSetLeft() + ? 0 : DocxTwips.of(properties.getInd().getLeft()); + } +} diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackendTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackendTest.java index 313bc76eb..98722fbb1 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackendTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackendTest.java @@ -207,12 +207,16 @@ void aDrawnListMarkerIsItsPictureAndATextMarkerKeepsItsOwnStyle() throws Excepti .toList(); assertThat(paragraphs).hasSize(2); - // A disc is the picture it draws — not a bullet nobody asked for — followed by - // the space a text marker gets, so the item does not run into it. + // A disc is the picture it draws — not a bullet nobody asked for — followed by a + // tab to where the page starts the item: the disc's width and markerGap past it. XWPFParagraph drawn = paragraphs.get(0); - assertThat(drawn.getText()).isEqualTo(" Drawn marker item"); + assertThat(drawn.getText()).isEqualTo("\tDrawn marker item"); assertThat(drawn.getRuns().stream().filter(run -> !run.getEmbeddedPictures().isEmpty())) .as("the disc").hasSize(1); + var stops = drawn.getCTP().getPPr().getTabs().getTabArray(); + assertThat(stops).hasSize(1); + assertThat(DocxTwips.of(stops[0].getPos())).as("the disc's width and the gap after it") + .isEqualTo(Math.round((4.0 + com.demcha.compose.document.node.ListNode.DEFAULT_MARKER_GAP) * 20)); // A marker written as text keeps the colour it was given, because a // run colour is something Word holds.