Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export names what of a paragraph's pull up nothing above it gives back.** Word has no
space above a paragraph below zero. A paragraph's, a page reference's or a rule's top edge
below zero comes out of the space owed above it, and a paragraph's also off the foot of the one
line above it and the top of its own line, as far as each gives. What none gives was dropped
without a note.
- The note (`space above`) names the block's own pull, not a border below the block above,
which is named apart. In the flow, the block and what follows it stand that much lower. In
a table cell, the block stands up to that much lower, and where the cell sets its row's
height the row is that much taller.
- Not reckoned yet, and not named:
- the first block of a band's layer or of a row of columns' later layer, which resumes at the
page's distance from what is above it;
- a block opening a page in the flow: a section's first, one the layout moves to a new page,
or one after a page break;
- a side of an overlay's line pair, which takes its pull out of the space above alone.
- Measured in Word on Windows: `TealPulse`'s contact pairs are pulled 4.32pt up in a centred
row, a 2.16pt lift on the page, past what a line holding an icon gives. They stood 1.2pt
lower. Written as space below them, which shifts a centred paragraph as much, they rose
0.8pt, but their row grew past the page's and the page under it stood 2.2 to 2.8pt lower, so
the pull is named, not written.
- 5 paragraphs of the corpus's 62 documents gain the note: the 4 pairs and Obsidian's wordmark,
0.1pt. None of the DOCX changes.
- **A DOCX export names a panel's right border its table cell's edge would cut off.** Word draws
a nested table's right border outside the table and, on screen, cuts off what passes its cell's
edge. A bordered panel reaching that edge has its table written narrower, out of its right
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,19 @@ that line's letters leave room below them where Word draws them, three quarters
clear: the line written shorter, its text lowered by the four fifths of that Word's baseline
would rise. The rest it takes from the top of its own line, where it is one line of text, as
far as its letters leave room above them, its text seated where the page sets it; what neither
can give is not written. Text laid over the flow in text boxes owes no space, so its edges
move nothing.
can give is not written, and the report names the block's own pull (`space above`), a border
below the block above named apart. In the flow the block and what follows it stand that much
lower. In a table cell the page sets the cell's content that much shorter, so the block stands
up to that much lower, and where the cell sets its row's height the row is that much taller.
Not reckoned this way yet, and not named: the first block of a band's layer or of a row of
columns' later layer, which resumes at the page's distance from what is above it; a block
opening a page in the flow — a section's first, one the layout moves to a new page, one after a
page break; and a side of an overlay's line pair, which takes its pull out of the space above
alone. Measured: `TealPulse`'s
contact pairs, pulled 4.32pt up in a centred row past what a line holding an icon gives, a
2.16pt lift on the page, stood 1.2pt lower in Word; written as space below them instead, they
rose 0.8pt and grew their row past the page's, the page under it 2.2 to 2.8pt lower. Text laid
over the flow in text boxes owes no space, so its edges move nothing.

A negative bottom `margin` or `padding` — a paragraph's, an image's, a barcode's or a rule's —
pulls the next paragraph up into it the same way: it comes out of the space above that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
*/
private long blocksWritten;

/** {@link #blocksWritten} once the last page break was written: equal, nothing has been since. */
private long blocksAtTheLastBreak = -1;

/** Space the last body paragraph holds below itself, not yet written — see {@link #owePendingSpacingAfter}. */
private double pendingSpacingAfter;

Expand Down Expand Up @@ -1149,6 +1152,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
writing = null;
currentPage = 0;
lastEndPage = -1;
blocksAtTheLastBreak = -1;
topEdgeHeldAbove = null;
topEdgeHeld = 0;
topEdgeLine = null;
Expand Down Expand Up @@ -8981,7 +8985,10 @@ private static boolean drawn(CTBorder border) {
* {@link #newBodyParagraph} nets it against everything owed above, as the page sums it.
* Where that cannot give it all, a paragraph takes the rest off the foot of the line written
* just before it ({@link #takeFromTheLineAbove}), then off the top of its own line
* ({@link #riseInsideItsOwnLine}); what neither can give stays unwritten. Text laid over the flow owes no
* ({@link #riseInsideItsOwnLine}); what neither can give stays unwritten, and is named
* ({@link #nameAPullNotGivenBack}) where the space above it is the space owed here
* ({@link #spaceAboveWrittenElsewhere}). A side of an overlay's line pair is not this: it
* takes its pull out of the space above alone ({@link #writeLinePair}). Text laid over the flow owes no
* space ({@link #writeOverTheFlow}), so its edges move nothing.</p>
*/
private void standsIntoTheSpaceAbove(DocumentNode node) {
Expand All @@ -8995,7 +9002,55 @@ private void standsIntoTheSpaceAbove(DocumentNode node) {
riseInsideItsOwnLine(paragraph, shortBy);
}
}
// Only the block's own pull is its to name: what the space was short of before it is
// a container's edge above it, and a border below the block above is named apart
// (nameTheTailNotTaken), so it is not counted here.
double notGivenBack = Math.min(-edge, -(carriedSpacingBefore + pendingSpacingAfter));
if (notGivenBack > TAIL_CLEARANCE && !spaceAboveWrittenElsewhere(node)) {
nameAPullNotGivenBack(node, notGivenBack);
}
}
}

/**
* Whether the space Word writes above a block is not the space owed here, so a pull reckoned
* against it would be named wrong: a band's layer or a row of columns' later layer resumes at
* the page's distance from what is above it ({@link #resumeHere}), and a block opening a page in
* the flow — a section's first, one the layout moves to a new page, one after a page break —
* has the space above it held there or dropped, as Word drops it, with no block above it on
* the page. In a table cell the space owed is the cell's own, on whatever page.
*/
private boolean spaceAboveWrittenElsewhere(DocumentNode node) {
return !Double.isNaN(resumeSpacing) || opensItsPageInTheFlow(node)
|| currentCell == null && (startsAPageOfItsOwn(node) || lastEndPage < 0
|| blocksWritten == blocksAtTheLastBreak);
}

/**
* Names what of a block's top edge below zero nothing above it gives back, which Word does not
* write: a paragraph's space above is never below zero.
*
* <p>In the flow the block and what follows it stand that much lower, as under a pull of the
* block above ({@link #nameTheTailNotTaken}). In a table cell the page sets the cell's content
* that much shorter: the block stands up to that much lower, less where the cell sets its
* content at the middle or the foot, and where the cell sets its row's height the row is that
* much taller.
* {@code TealPulse}'s contact strip pulls each pair 4.32pt up in its centred row, a 2.16pt lift on
* the page, past the room its line holds above an icon's: in Word they stood 1.2pt lower. Written
* as space below them, which shifts a centred paragraph as much, the pairs rose 0.8pt and their
* row grew past the page's, the page under it 2.2 to 2.8pt lower (measured).</p>
*
* @param pull what of the pull is not given back, in points
*/
private void nameAPullNotGivenBack(DocumentNode node, double pull) {
if (currentCell == null) {
nameTheTailNotTaken(node, 0, 0, 0, pull);
return;
}
report.add(DocxExportReport.Severity.APPROXIMATED, "space above", layout.pathOf(node),
"its top edge below zero pulls it " + pointsOf(pull) + "pt further up than anything above it in "
+ "its table cell gives back, which Word does not write: it stands up to that much lower than the "
+ "page sets it, and where its cell sets its row's height, the row is that much taller");
}

/**
Expand Down Expand Up @@ -19868,6 +19923,7 @@ private void writePageBreak(XWPFDocument document) {
flushSpacingAfter();
XWPFParagraph para = document.createParagraph();
blocksWritten++;
blocksAtTheLastBreak = blocksWritten;
// It stands on the page the break ends, and carries what is drawn there when nothing else
// written on that page does (DocxDrawingAnchors.breakOn): a shape alone before a break
// was dropped, its note saying it was drawn.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ record Entry(Fate fate, String note) {
+ "first line takes, on the panel; below, what neither the space "
+ "below it nor its padding takes, on the block below "
+ "(space above); " + BORDER_WIDTH + "; any other is written";
// A paragraph's, a page reference's or a rule's own edge below zero is taken out of what is
// round it (standsIntoTheSpaceAbove); Word has no space above below zero.
private static final String PULLED_UP = "anywhere else, a top below zero out of the space above it and, a "
+ "paragraph's, the foot of the one line above and the top of its own "
+ "line, as far as they give; what none gives, named (space above)";
// An edge below zero pulls the next block up; Word sets no block over another.
private static final String PULL = "below zero below it, the pull the space under it and the next block's own "
+ "top edge do not give back, on the next block (space above); any other is "
Expand Down Expand Up @@ -152,6 +157,12 @@ record Entry(Fate fate, String note) {
static final String TOP_PULL = "a top below zero: on a picture, a barcode or a block written as a table, a "
+ "table, a row or a panel, not written; on another block, what the space above "
+ "it and the lines round it cannot give, not written; neither named";
static final String PULL_UNRECKONED = "a top below zero on the first block of a band's layer or of a row of "
+ "columns' later layer, which resumes at the page's distance from what is "
+ "above it; on a block opening a page in the flow, a section's first, one "
+ "the layout moves to a new page or one after a page break; or on a side "
+ "of an overlay's line pair, which takes it out of the space above alone: "
+ "not reckoned against the space Word writes there, not named";
static final String BOTTOM_PULL = "a bottom below zero on a list, a table, a row, a stack, a canvas, a shape "
+ "container in the flow or a block with no paint of its own: the next block "
+ "not pulled up by it, set that much lower, not named";
Expand Down Expand Up @@ -187,7 +198,7 @@ record Entry(Fate fate, String note) {

/** The known cases: a gap's note opens with one of them, or the ledger fails. */
private static final List<String> GAPS = List.of(TOP_PULL, BOTTOM_PULL, PADDING_BELOW_ZERO, SPACE_BELOW_AT_THE_FOOT,
CELL_PADDING, LIST_PICTURE, SEAT_UNREAD, ROW_COLUMNS_UNLAID, DRAWING_OVER_TEXT,
PULL_UNRECKONED, CELL_PADDING, LIST_PICTURE, SEAT_UNREAD, ROW_COLUMNS_UNLAID, DRAWING_OVER_TEXT,
COMPOSED_STACK_LAYER, LAYER_PAST_THE_EDGE, LAYER_UNLAID, PANEL_WIDER_THAN_ITS_CELL);

private static final Map<Class<?>, Map<String, Entry>> NODES = new LinkedHashMap<>();
Expand Down Expand Up @@ -281,8 +292,8 @@ record Entry(Fate fate, String note) {
"stroke:REPORTED:its translucency, as a rule's, flattened against the colour under it, and as a "
+ "rule, " + BORDER_WIDTH + "; drawn, its alpha is written; any other is written",
"linkTarget:REPORTED",
"bookmarkOptions:REPORTED", "padding:GAP:" + TOP_PULL + "; any other is written",
"margin:GAP:" + TOP_PULL + "; any other is written", "transform:REPORTED",
"bookmarkOptions:REPORTED", "padding:GAP:" + PULL_UNRECKONED + "; as a rule, " + PULLED_UP + "; any other is written",
"margin:GAP:" + PULL_UNRECKONED + "; as a rule, " + PULLED_UP + "; any other is written", "transform:REPORTED",
"dashPattern:REPORTED",
"anchor:REPORTED:drawn; a rule in the flow is bookmarked", "lineCap:REPORTED:a rule's other than butt, a border ending flat; drawn, it is written",
"fillWidth:WRITTEN", "keepWithNext:REPORTED:of a line drawn in the flow the page keeps blocks together in");
Expand Down Expand Up @@ -321,8 +332,8 @@ record Entry(Fate fate, String note) {
node(PageReferenceNode.class, "name:INERT",
"anchor:REPORTED:where the anchor has no bookmark, its number is written as text",
"textStyle:WRITTEN", "align:WRITTEN", "placeholderText:WRITTEN",
"padding:GAP:" + TOP_PULL + "; the sides its alignment sets it from",
"margin:GAP:" + TOP_PULL + "; the sides its alignment sets it from");
"padding:GAP:" + PULL_UNRECKONED + "; " + PULLED_UP + "; the sides its alignment sets it from",
"margin:GAP:" + PULL_UNRECKONED + "; " + PULLED_UP + "; the sides its alignment sets it from");
node(ParagraphNode.class, "name:INERT",
"text:REPORTED:where the page reads it as markdown, written as the page sets it where its lines "
+ "hold the pieces read so, a heading written taller than its line named; its marks written as letters "
Expand All @@ -339,7 +350,8 @@ record Entry(Fate fate, String note) {
"bookmarkOptions:REPORTED:a title that is not the text Word lists it by, a pair's whole line for "
+ "a side of one; a level past Word's ninth that shares it with another; the right side's of a "
+ "pair whose left holds the level",
"padding:GAP:" + TOP_PULL + "; " + PULL, "margin:GAP:" + TOP_PULL + "; " + PULL,
"padding:GAP:" + PULL_UNRECKONED + "; " + PULLED_UP + "; " + PULL,
"margin:GAP:" + PULL_UNRECKONED + "; " + PULLED_UP + "; " + PULL,
"autoSize:REPORTED:where the layout does not tell the size the page fits the text in the "
+ "paragraph's style to — no lines read, lines in sizes that do not say which is the "
+ "paragraph's, or one paragraph added at more than one place — the text written at its "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,14 +651,20 @@ void aHangATableBelowDoesNotTakeIsNamedOnTheTable() throws Exception {
@Test
void aParagraphPulledUpStandsBelowNoBorder() throws Exception {
// An edge below zero pulls a paragraph up, as TealPulse's contact pairs are in their
// strip: it is no border below a panel, and nothing is named for it.
// strip: it is no border below a panel. What of the pull nothing gives back is named as
// the pull it is, the border nowhere.
Exported exported = export(page -> page.addParagraph("Above")
.addParagraph(p -> p.text("Pulled").margin(new DocumentInsets(-4, 0, 0, 0)))
.addRow(row -> row.addParagraph(p -> p.text("Lifted").margin(new DocumentInsets(-4, 0, 0, 0)))
.addParagraph("Beside"))
.addParagraph("Below"));

assertThat(exported.report().bySubject()).doesNotContainKey("space above");
List<DocxExportReport.Note> notes = exported.report().bySubject().get("space above");
assertThat(notes).hasSize(2).allSatisfy(note -> assertThat(note.detail()).doesNotContain("border"));
assertThat(notes.get(0).path()).as("in the flow").doesNotContain("RowNode");
assertThat(notes.get(0).detail()).contains("over the block above it");
assertThat(notes.get(1).path()).as("in the row's cell").contains("RowNode");
assertThat(notes.get(1).detail()).startsWith("its top edge below zero pulls it 4pt further up");
}

@Test
Expand Down
Loading
Loading