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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export writes a page zone the page builds otherwise for the first page into the
first page's header or footer.** Word paginates, so a zone's content was built once, for a
page that was both the first and the last, and written on every page: a footer reading
`page.isFirst() ? "Original keep" : "Continuation sheet"` read "Original keep" on all of them.
The content is now built for the first page and for the pages after it —
`PageContext.unpaginated(boolean, boolean)`, new and `@Beta`, gives a semantic backend a
context for a kind of page that still refuses the numbers — and content built otherwise for
the first page becomes the section's title-page part and the ordinary one, each measured on a
page the layout draws it on where there is one. Word has no header or footer for the last
page: content built otherwise for it is written there as it is built for the pages between the
first and the last, and named in the report. Content that reads `page.number()` only for the pages after the first, which the
export did not build before, is now refused there as it is on the first; content that reads it
only on the last page, refused before, is written as it is built for the pages between and
named, unless the first page is the last.
- **A DOCX export sets a row and an alignment composed in a table cell where the page sets
them.** Content composed in a cell (`DocumentTableCell.node(...)`) has no place of its own in
the layout. A row the page divides by what its children measure — `SPACE_BETWEEN` and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
* }</pre>
*
* <p>The content function is called once per page with that page's
* {@link PageContext}, after pagination has settled. It must return a node; a
* zone that should be absent on some pages says so through
* {@link PageContext}, after pagination has settled — on a semantic export,
* where the format paginates, for the kinds of page it tells apart instead:
* the first, the last and the pages between
* ({@link PageContext#unpaginated(boolean, boolean)}). It must return
* a node; a zone that should be absent on some pages says so through
* {@code appliesTo} instead of returning {@code null}.</p>
*
* <p><strong>A zone is atomic.</strong> It is laid out into its declared
Expand Down Expand Up @@ -91,10 +94,12 @@ public final class DocumentPageZone {
* two are separate questions, so "no number on the cover, but keep the
* logo" is one zone with a predicate rather than two zones.
*
* <p>A predicate is a fixed-layout capability. A semantic export (DOCX)
* writes the zone once as a definition and the consuming format paginates,
* so there is no page to test it against: the zone lands on every page and
* the export logs a warning.</p>
* <p>A semantic export (DOCX) writes the zone as a definition and the
* consuming format paginates, so there is no page to test it against. It
* asks the predicate over sample pages instead: one that follows the kinds of
* page the format has a header or footer for — the first, even ones, the
* rest — lands on those, and one that picks pages within a kind (the last
* page) lands on every page, reported.</p>
*/
private final Predicate<PageContext> appliesTo;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.demcha.compose.document.output;

import com.demcha.compose.document.api.Beta;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.PageFieldKind;
import com.demcha.compose.document.node.PageFieldNode;
Expand Down Expand Up @@ -32,6 +33,10 @@
* survives export. One node type serves both lanes: a paginated render resolves
* it to the number, a semantic export maps it onto a live {@code PAGE} field.</p>
*
* <p>What a semantic export can tell apart is the kind of page — the first, the
* last, one between — so it builds the zone for a kind rather than a page, and
* {@link #isFirst()} still chooses a cover's line over the running one there.</p>
*
* @author Artem Demchyshyn
* @since 2.3.0
*/
Expand Down Expand Up @@ -70,10 +75,34 @@ public static PageContext paginated(int number, int total) {
* A context for a semantic export, where the consuming format paginates and
* the numbers are therefore unknown here.
*
* @return a context that hands out page fields rather than page numbers
* @return a context that hands out page fields rather than page numbers, the
* first page and the last
*/
public static PageContext unpaginated() {
return new PageContext(1, 1, false);
return unpaginated(true, true);
}

/**
* A context for a semantic export that builds a zone for a kind of page rather
* than a page — the first, the last or one between: Word, for one, has a header
* and footer for a section's first page and another for the pages after it.
*
* <p>The numbers stay unknown, as with {@link #unpaginated()};
* {@link #isFirst()} and {@link #isLast()} answer for the kind of page the
* zone is built for.</p>
*
* <p><b>Experimental</b> ({@code @Beta}) — see {@code docs/api-stability.md}.</p>
*
* @param first whether the zone is built for the first page
* @param last whether it is built for the last page
* @return a context that hands out page fields rather than page numbers
* @since 2.5.0
*/
@Beta
public static PageContext unpaginated(boolean first, boolean last) {
// Never read on a semantic export, the numbers only answer isFirst and isLast.
int number = first ? 1 : 2;
return new PageContext(number, last ? number : number + 1, false);
}

/**
Expand Down Expand Up @@ -112,8 +141,8 @@ public int total() {
/**
* Whether this is the first page.
*
* @return {@code true} on page 1; always {@code true} on a semantic export,
* where one zone definition serves every page
* @return {@code true} on page 1; on a semantic export, whether the zone is
* built for the kind of page the first one is ({@link #unpaginated(boolean, boolean)})
*/
public boolean isFirst() {
return number == 1;
Expand All @@ -122,8 +151,8 @@ public boolean isFirst() {
/**
* Whether this is the last page.
*
* @return {@code true} on the final page; always {@code true} on a semantic
* export, where one zone definition serves every page
* @return {@code true} on the final page; on a semantic export, whether the
* zone is built for the last page ({@link #unpaginated(boolean, boolean)})
*/
public boolean isLast() {
return number == total;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.demcha.compose.document.output;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* A semantic export's context answers for the kind of page a zone is built for and still
* refuses the numbers, which the consuming format sets.
*
* @author Artem Demchyshyn
*/
class PageContextTest {

@Test
void aSemanticContextAnswersForTheKindOfPageItIsBuiltFor() {
assertKind(PageContext.unpaginated(true, true), true, true);
assertKind(PageContext.unpaginated(true, false), true, false);
assertKind(PageContext.unpaginated(false, true), false, true);
assertKind(PageContext.unpaginated(false, false), false, false);
}

@Test
void theContextForNoPageInParticularIsTheFirstAndTheLast() {
assertKind(PageContext.unpaginated(), true, true);
}

@Test
void aSemanticContextOfAnyKindRefusesTheNumbers() {
PageContext later = PageContext.unpaginated(false, false);

assertThat(later.isPaginated()).isFalse();
assertThatThrownBy(later::number).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(later::total).isInstanceOf(UnsupportedOperationException.class);
}

private static void assertKind(PageContext page, boolean first, boolean last) {
assertThat(page.isFirst()).as("isFirst").isEqualTo(first);
assertThat(page.isLast()).as("isLast").isEqualTo(last);
assertThat(page.isPaginated()).isFalse();
}
}
7 changes: 7 additions & 0 deletions docs/api-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ matrix.
> shape of the opt-in that may still move rather than the behaviour. Both constructors
> are unaffected.
>
> **A page zone's context for a kind of page** is Experimental in 2.5.0:
> `PageContext.unpaginated(boolean, boolean)`, which a semantic backend builds a page zone
> with for a kind of page rather than a page — the first, the last or one between, as Word
> gives the first page a header and footer of its own — so `isFirst()` and `isLast()` answer
> for that kind while the numbers stay unknown. It is marked Experimental because the kinds it names are
> Word's: a second semantic format may need others. `unpaginated()` is unaffected.
>
> Seven members of the otherwise-Stable **PDF backend** also carry `@Beta`. The
> package is not Experimental — these are:
> `PdfFixedLayoutBackend.renderSections` / `writeSections`, the low-level seam
Expand Down
Loading
Loading