From dda16c753800ee5e0269dcbff20110898827c038 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Tue, 20 Oct 2026 12:52:23 +0100
Subject: [PATCH 1/2] fix(docx): keep the space above a spacer opening a stack
of one layer
A spacer the layout moves to a new page holds the space above it in
its line, but only outside every stack. A stack of one layer opening
with a spacer, moved to a new page, lost the space above its text:
measured in Word and LibreOffice, a stack with a 14pt margin and an
8pt spacer before its text stood 22.9pt high, the spacer written at
the foot of the page above.
The spacer's line now holds that space there, less its layer's offset,
which is named, and the spacer is kept with what follows it in the
stack, which the layout moves whole.
---
CHANGELOG.md | 10 +++
docs/recipes/docx-export.md | 2 +-
.../semantic/docx/DocxSemanticBackend.java | 31 ++++++-
.../docx/DocxSpaceAboveOnANewPageTest.java | 2 +
.../DocxStackOfOneLayerOnANewPageTest.java | 83 ++++++++++++++++++-
5 files changed, 122 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1045306a6..226736bb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,16 @@ follow semantic versioning; release dates are ISO 8601.
### Public API
+- **A DOCX export keeps the space above a spacer opening a stack of one layer on a new page.** A
+ stack of one layer that opens with a spacer, moved to a new page, lost the space above its text:
+ its own edges and the spacer's height. The spacer was written at the foot of the page above,
+ and the text under it at the new page's top.
+ Measured in Word and LibreOffice on Windows: a stack with a 14pt margin and an 8pt spacer before
+ its text stood 22.9pt high in both. Nothing was named.
+ - The spacer's line now holds the stack's edges, as a spacer in the flow holds a carried gap,
+ and the spacer is kept with what follows it in the stack. The text now stands 1.2pt higher
+ than the page sets it, in both, and stays on the page it was on.
+ - A layer's offset in its stack is named as before, and not held.
- **A DOCX export keeps the space above the first paragraph or picture of a stack of one layer the
layout moves to a new page.** A stack of one layer is written in the flow, and Word drops its
first paragraph's space above at the top of a page as any other's. That space was held only for
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index c89f5be46..6a00e5c06 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -76,7 +76,7 @@ creation date is real metadata.
| Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is |
| Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel |
| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph near it, a page can end between it and the next block, and in that same flow the report names the keep it loses |
-| Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included |
+| Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, which the layout moves to a new page whole: the space the layout leaves above it there, less its layer's offset, which is named, is held in its line. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included |
| Page breaks | Explicit Word page breaks |
Page geometry (size, margins and orientation — a page wider than it is tall is stated as
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 5cbba8b90..ae1c7ed55 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
@@ -18714,7 +18714,8 @@ private double spaceAboveOnItsPage(DocumentNode node) {
* space above it together are as tall as before, so Word breaks the page where it did.
*/
private void holdTheGapAboveInTheLine(XWPFParagraph para, DocumentNode node) {
- double onItsPage = spaceAboveOnItsPage(node);
+ // A layer's offset in its stack of one layer is named, not written, and not held either.
+ double onItsPage = spaceAboveOnItsPage(node) - layerOffsetNotWritten;
CTSpacing spacing = para.getCTP().isSetPPr() && para.getCTP().getPPr().isSetSpacing()
? para.getCTP().getPPr().getSpacing() : null;
if (!(onItsPage > 0.01) || spacing == null || !spacing.isSetBefore()
@@ -18730,6 +18731,21 @@ private void holdTheGapAboveInTheLine(XWPFParagraph para, DocumentNode node) {
spacing.setLine(BigInteger.valueOf(twipsOf(spacing.getLine()) + held));
}
+ /**
+ * Whether the layout placed anything after a node in the stack it is written in: after it, or
+ * after a container it closes on the way up to the stack, as a spacer ending a section the
+ * layer's text follows.
+ */
+ private boolean followedInItsStack(DocumentNode node) {
+ DocumentNode stack = openOverlays.peek();
+ for (DocumentNode at = node; at != null && at != stack; at = layout.parentOf(at)) {
+ if (layout.followedInItsParent(at)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Makes a paragraph that holds no text as short as a separator between tables. */
private static void holdToHairline(XWPFParagraph para) {
CTPPr properties = para.getCTP().isSetPPr() ? para.getCTP().getPPr() : para.getCTP().addNewPPr();
@@ -19022,7 +19038,9 @@ private void writeSpacer(XWPFDocument document, SpacerNode node) throws Exceptio
writeInPlaceOf(document, node);
return;
}
- boolean opensAPage = currentCell == null && overlayDepth == 0 && startsAPageOfItsOwn(node);
+ // In the flow, a stack of one layer's included: opening one on a new page, a stack with a
+ // 14pt margin and an 8pt spacer before its text stood 22pt high in Word and LibreOffice.
+ boolean opensAPage = currentCell == null && overlayDepth == oneLayerDepth && startsAPageOfItsOwn(node);
if (opensAPage) {
// Text hanging below the block before — a row's last line held to its icon — a pull
// out of it and a card's border below it stay on the page above with it, as at a
@@ -19041,6 +19059,15 @@ private void writeSpacer(XWPFDocument document, SpacerNode node) throws Exceptio
holdToHairline(para);
if (opensAPage) {
holdTheGapAboveInTheLine(para, node);
+ // A stack of one layer moves to its new page whole, the spacer with what follows it
+ // there: left alone, Word set the spacer at the foot of the page above and the text
+ // under it at the new page's top, 22pt high.
+ if (oneLayerDepth > 0 && followedInItsStack(node)) {
+ CTPPr properties = para.getCTP().getPPr();
+ if (!properties.isSetKeepNext()) {
+ properties.addNewKeepNext();
+ }
+ }
}
// The hairline is part of the height, as a separator's is of the gap it stands in: owed
// whole below it, every spacer stood a tenth of a point taller than the page's, and
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java
index a7e8f8887..ec3f29477 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java
@@ -52,6 +52,8 @@ void aSpacerTheGapBeforeCarriesToANewPageHoldsTheGapInItsLine() throws Exception
.isEqualTo(owed(CONTENT - 3));
assertThat(DocxTwips.of(spacing.getLine())).as("the gap, and the hairline's height")
.isEqualTo(Math.round(GAP * 20) + HAIRLINE);
+ assertThat(spacerOnThePageAfter(document).getCTP().getPPr().isSetKeepNext())
+ .as("in the flow it is not kept with what follows, as a stack of one layer's is").isFalse();
}
}
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
index ae65ac918..38c120106 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
@@ -23,14 +23,16 @@
import static org.assertj.core.api.Assertions.assertThat;
/**
- * The first paragraph or picture of a stack of one layer the layout moves to a new page keeps the
- * space the layout leaves above it there, in Word.
+ * The first paragraph, picture or spacer of a stack of one layer the layout moves to a new page
+ * keeps the space the layout leaves above it there, in Word.
*
* A stack of one layer is written in the flow, and Word drops its first paragraph's space
* above at the top of a page as any other's. Measured in Word 16 and LibreOffice on Windows, the
* first line of a paragraph with a 20pt margin opening such a stack on a new page stood 20pt
* high in both, a picture with one 20pt high, and one in a stack with a 14pt margin 14pt high.
- * Held in a line kept with it, as in the flow, each stood where the page sets it. A layer's
+ * Held in a line kept with it, as in the flow, each stood where the page sets it. A stack with a
+ * 14pt margin and an 8pt spacer before its text stood 22.9pt high, the spacer left at the foot of
+ * the page above; its line holding the margin, kept with the text, it stood 1.2pt high. A layer's
* offset in its stack is named and not written, and the line holds none of it.
*
* @author Artem Demchyshyn
@@ -161,6 +163,81 @@ void anOffsetEndsWithItsStack() throws Exception {
}
}
+ @Test
+ void aSpacerOpeningTheStackHoldsItsEdgeAndGoesWithWhatFollows() throws Exception {
+ // Measured: a stack with a 14pt margin and an 8pt spacer before its text stood 22pt high
+ // in Word and LibreOffice, the spacer left at the foot of the page above.
+ Consumer content = movedToANewPage(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .topLeft(new SectionBuilder().spacer(0, 8)
+ .addParagraph(p -> p.name("Opening").text("Opening")).build()));
+ assertThat(startPage(content, "Opening")).as("the premise: the stack opens the second page").isEqualTo(1);
+ try (XWPFDocument document = export(content)) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "Opening"));
+ CTSpacing spacing = spacing(spacer);
+
+ assertThat(DocxTwips.of(spacing.getLine())).as("the stack's margin, and the spacer's hairline")
+ .isEqualTo(Math.round(EDGE * 20) + HAIRLINE);
+ assertThat(spacer.getCTP().getPPr().isSetKeepNext()).as("kept with the text under it").isTrue();
+ }
+ }
+
+ @Test
+ void aSpacerEndingASectionTheLayersTextFollowsGoesWithIt() throws Exception {
+ try (XWPFDocument document = export(movedToANewPage(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .topLeft(new SectionBuilder().addSection(inner -> inner.spacer(0, 8))
+ .addParagraph("Opening").build())))) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "Opening"));
+
+ assertThat(spacer.getText()).isEmpty();
+ assertThat(spacer.getCTP().getPPr().isSetKeepNext()).as("kept with the text after its section").isTrue();
+ }
+ }
+
+ @Test
+ void aSpacerAloneInItsStackIsKeptWithNothing() throws Exception {
+ // What follows the stack is not the stack's: the spacer is not kept with it.
+ try (XWPFDocument document = export(page -> page
+ .spacer(0, CONTENT - 10)
+ .addLayerStack(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .topLeft(new SectionBuilder().spacer(0, 8).build()))
+ .addParagraph("After"))) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "After"));
+
+ assertThat(DocxTwips.of(spacing(spacer).getLine())).as("its edge held all the same")
+ .isEqualTo(Math.round(EDGE * 20) + HAIRLINE);
+ assertThat(spacer.getCTP().getPPr().isSetKeepNext()).as("nothing in its stack follows it").isFalse();
+ }
+ }
+
+ @Test
+ void aSpacerUnderAnOffsetHoldsNoneOfIt() throws Exception {
+ Consumer content = movedToANewPage(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .position(new SectionBuilder().spacer(0, 8).addParagraph("Opening").build(), 0, EDGE,
+ LayerAlign.TOP_LEFT));
+ try (XWPFDocument document = export(content)) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "Opening"));
+
+ assertThat(DocxTwips.of(spacing(spacer).getLine())).as("the stack's margin, not its layer's offset")
+ .isEqualTo(Math.round(EDGE * 20) + HAIRLINE);
+ }
+ assertThat(DocxExports.report(300, 200, 20, content).bySubject().get("layer offset"))
+ .as("the offset named").isNotNull();
+ }
+
+ @Test
+ void aSpacerOpeningABandIsLeftToTheBand() throws Exception {
+ Consumer content = movedToANewPage(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .topLeft(new SectionBuilder().spacer(0, 8).addParagraph(p -> p.name("Opening").text("Opening")).build())
+ .bottomRight(new ParagraphBuilder().text("Caption").build()));
+ assertThat(startPage(content, "Opening")).as("the premise: the band opens the second page").isEqualTo(1);
+ try (XWPFDocument document = export(content)) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "Opening"));
+
+ assertThat(DocxTwips.of(spacing(spacer).getLine())).as("its hairline alone").isEqualTo(HAIRLINE);
+ assertThat(spacer.getCTP().getPPr().isSetKeepNext()).isFalse();
+ }
+ }
+
@Test
void aPullOutOfTheBlockOnThePageAboveStaysThere() throws Exception {
// The paragraph above pulls what follows it 5pt up. On its page, Word sets no block over
From fb51c2ea1c04baa43943f0c71456c21240d6c6c5 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Tue, 20 Oct 2026 15:32:42 +0100
Subject: [PATCH 2/2] fix(docx): keep a spacer with what follows it in its
outermost stack
A spacer opening a stack of one layer inside another was kept only
with what follows it in the inner stack. The layout moves the outer
stack whole, so a spacer closing the inner one was not kept with the
outer stack's text after it, on the same page. It is now kept with
anything that follows it up to the outermost stack of one layer.
---
.../backend/semantic/docx/DocxSemanticBackend.java | 7 ++++---
.../docx/DocxStackOfOneLayerOnANewPageTest.java | 14 ++++++++++++++
2 files changed, 18 insertions(+), 3 deletions(-)
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 ae1c7ed55..f3ab1db9e 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
@@ -18732,12 +18732,13 @@ private void holdTheGapAboveInTheLine(XWPFParagraph para, DocumentNode node) {
}
/**
- * Whether the layout placed anything after a node in the stack it is written in: after it, or
- * after a container it closes on the way up to the stack, as a spacer ending a section the
+ * Whether the layout placed anything after a node in the stacks of one layer it is written in:
+ * after it, or after a container it closes on the way up to the outermost stack, which the
+ * layout moves to its page whole with everything in it, as a spacer ending a section the
* layer's text follows.
*/
private boolean followedInItsStack(DocumentNode node) {
- DocumentNode stack = openOverlays.peek();
+ DocumentNode stack = openOverlays.peekLast();
for (DocumentNode at = node; at != null && at != stack; at = layout.parentOf(at)) {
if (layout.followedInItsParent(at)) {
return true;
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
index 38c120106..baa50b357 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxStackOfOneLayerOnANewPageTest.java
@@ -193,6 +193,20 @@ void aSpacerEndingASectionTheLayersTextFollowsGoesWithIt() throws Exception {
}
}
+ @Test
+ void aSpacerClosingAStackInsideAStackGoesWithWhatTheOuterOneHolds() throws Exception {
+ // The layout moves the outer stack whole: the text after the inner one is on its page too.
+ try (XWPFDocument document = export(movedToANewPage(stack -> stack.margin(DocumentInsets.top(EDGE))
+ .topLeft(new SectionBuilder()
+ .addLayerStack(inner -> inner.topLeft(new SectionBuilder().spacer(0, 8).build()))
+ .addParagraph("Opening").build())))) {
+ XWPFParagraph spacer = paragraphBefore(document, paragraphWith(document, "Opening"));
+
+ assertThat(spacer.getText()).isEmpty();
+ assertThat(spacer.getCTP().getPPr().isSetKeepNext()).as("kept with the outer stack's text").isTrue();
+ }
+ }
+
@Test
void aSpacerAloneInItsStackIsKeptWithNothing() throws Exception {
// What follows the stack is not the stack's: the spacer is not kept with it.