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

### Public API

- **A DOCX export's page-margin notes name every page they are about.**
- A note about several pages named the first of them only, and the later ones, which Word sets
with the margins in force too, went unnamed: `ObsidianInvoice` with its line items run onto
three pages named page 2, not page 3. The note on a block several pages start inside, on a run
of pages whose side margins differ, and on a section of several whose page count a Word
section would split now names each page, as "the top and bottom margins of pages 2 and 3
differ from page 1's", three or more in a row as "pages 2 to 4".
- A page-margin note about a page names it, `page N` or `section N, page N`, as the report's
contract says: the note on a section started where a page does, on a page after one whose
content reaches its foot, on a page no block starts and on a page's side margins carried
their section's path, which a document of one section has none of.
- The file is unchanged.

- **A DOCX export's report names what a table's cell is composed of by its cell.**
- The layout lays a composed cell's content out under its table and gives it no path of its
own, so every note about a node there carried none: 216 of the corpus's 1384 notes, a rounded
Expand All @@ -23,8 +36,9 @@ follow semantic versioning; release dates are ISO 8601.
one in the flow does: with its outline entry not written either, it gave two notes, and gives
one.
- The contract names the paths a note may carry that are not a node's: `page N`, and in a
document of several sections `section N` or `section N, page N`; a note about the whole
document or about a node of a page zone has none.
document of several sections `section N` or `section N, page N`. A note about the whole
document has none; one about a page zone or a node of one has none or, in a document of
several sections, names its section.
- The file is unchanged: the 62 corpus DOCX are byte for byte the same, and so is the text of
their notes. The 17 notes left without a path are about the whole document: the line that
closes it and a text footer reaching past the margin.
Expand Down
11 changes: 7 additions & 4 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ reference, which the export settles in the paragraph it wrote it in once the bod
page after one whose content reaches its foot, under a table or in its rows, starts none either,
as the line that would end the section has no room. Neither does a page of a
document of several sections whose header or footer counts its section's pages: Word would count
a section of its own apart. The report names each (`page margin`), on the block where there is
one, and Word sets that page with the margins in force.
a section of its own apart. The report names each (`page margin`): on the block where there is
one, in one note naming every page that starts inside it; on the section, naming each of its
pages, where its page count would split; and by the page, `page N`, otherwise. Word sets those
pages with the margins in force.

## Finding out what the export could not carry

Expand All @@ -224,8 +226,9 @@ authored for that row, from 0, not its column in the grid where a cell before it
or one above spans rows into its row — so `Invoice[0]/Lines[2]/row[3]/cell[1]/Total[0]` is the
`Total` node in the second cell authored for the fourth row. A note about no one node names what
it is about instead: `page N`, and in a document of several sections `section N` or
`section N, page N`. A note about the whole document, such as the line closing it, or about a
node of a page zone has none.
`section N, page N`. A note about the whole document, such as the line closing it, has none. One
about a page zone or a node of one has none either, or names its section in a document of
several.
`DROPPED` means the page draws it and the document does not carry
it; `APPROXIMATED` means it is in the document as the nearest thing Word owns — a panel
that keeps its fill and loses its rounded corners. Neither is an error: an export that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
*
* <p>A note about no one node says what it is about instead: {@code page N} for a page, and in a
* document of several sections {@code section N} for one of them or {@code section N, page N}. A
* note about the whole document, or about a node of a page zone, which the layout lays out apart
* from the body, has no path.</p>
* note about the whole document has no path. One about a page zone, or about a node of one, which
* the layout lays out apart from the body, has none either, or names its section in a document of
* several.</p>
*
* <p><b>Experimental</b> ({@code @Beta}) — see {@code docs/api-stability.md}.</p>
*
Expand Down Expand Up @@ -69,7 +70,8 @@ public enum Severity {
* @param subject what it was, in a few words — {@code "chart"}, {@code "corner radius"}
* @param path the authored node's path; a page's or a section's where the note is about one
* ({@code page N}, {@code section N}, {@code section N, page N}); or null where
* it belongs to the whole export or to a node of a page zone
* it belongs to the whole export; about a page zone or a node of one, null or,
* in a document of several sections, its section
* @param detail what happened and what it means for the document
*/
public record Note(Severity severity, String subject, String path, String detail) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,27 @@ static Plan plan(XWPFDocument document, int from, Map<IBodyElement, Owner> owner
if (!vary(layout)) {
return new Plan(opening, List.of(), zoned);
}
if (countsItsPages) {
// Word counts each section's own pages, and this section's page count would split.
List<Integer> differing = new ArrayList<>();
for (int page = 1; page < layout.pageCount(); page++) {
if (!sameTopAndBottom(layout.marginOf(page), opening)) {
differing.add(page);
}
}
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, section, notSetWith(differing, 0)
+ ": a Word section would count its own pages, where this section's header or footer counts all "
+ "of them");
return new Plan(opening, List.of(), zoned);
}
List<IBodyElement> all = document.getBodyElements();
List<IBodyElement> elements = new ArrayList<>(all.subList(Math.min(from, all.size()), all.size()));
List<Start> starts = new ArrayList<>();
Margin inForce = opening;
int inForceFrom = 0;
DocumentNode lastNamed = null;
int namedAt = -1;
List<Integer> namedPages = new ArrayList<>();
for (int page = 1; page < layout.pageCount(); page++) {
Margin margin = layout.marginOf(page);
if (sameTopAndBottom(margin, inForce)) {
Expand All @@ -179,13 +194,6 @@ static Plan plan(XWPFDocument document, int from, Map<IBodyElement, Owner> owner
String differ = "page " + (page + 1) + "'s top and bottom margins differ from page " + (inForceFrom + 1)
+ "'s";
String notWritten = differ + ", which Word sets it with: ";
if (countsItsPages) {
// Word counts each section's own pages, and this section's page count would split.
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, section, notWritten
+ "a Word section would count its own pages, where this section's header or footer counts "
+ "all of them");
return new Plan(opening, List.of(), zoned);
}
Owner inside = spanning(page, elements, owners);
XWPFTable table = inside == null ? null : tableOf(inside, elements, owners);
Carrier carrier = null;
Expand All @@ -199,23 +207,35 @@ static Plan plan(XWPFDocument document, int from, Map<IBodyElement, Owner> owner
: carrierBefore(at, elements, owners, separators);
}
if (carrier == null) {
if (inside == null || inside.node() != lastNamed) {
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN,
inside != null ? layout.pathOf(inside.node()) : section,
notWritten + (inside != null ? "page " + (page + 1) + " starts inside this block"
: "no block of the body starts page " + (page + 1))
if (inside == null) {
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, pagePath(section, page),
notWritten + "no block of the body starts page " + (page + 1)
+ ", where the export starts no Word section");
lastNamed = inside != null ? inside.node() : null;
lastNamed = null;
} else if (inside.node() != lastNamed) {
namedAt = report.size();
namedPages.clear();
namedPages.add(page);
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, layout.pathOf(inside.node()),
notSetWith(namedPages, inForceFrom) + ": page " + (page + 1) + " starts inside this "
+ "block, where the export starts no Word section");
lastNamed = inside.node();
} else {
// One note for the block, naming each page that starts inside it.
namedPages.add(page);
report.set(namedAt, DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN,
layout.pathOf(inside.node()), notSetWith(namedPages, inForceFrom)
+ ": each starts inside this block, where the export starts no Word section");
}
continue;
}
// A page with no content has room for a hairline.
double room = layout.lowestContentOn(page - 1) - inForce.bottom();
if (carrier.hairline() && room < ROOM_FOR_THE_END) {
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN,
table != null ? layout.pathOf(inside.node()) : section, notWritten + "page " + page
+ "'s content reaches its foot, where the paragraph that would end a Word section before page "
+ (page + 1) + " would open a page of its own");
table != null ? layout.pathOf(inside.node()) : pagePath(section, page),
notWritten + "page " + page + "'s content reaches its foot, where the paragraph that would "
+ "end a Word section before page " + (page + 1) + " would open a page of its own");
continue;
}
starts.add(new Start(page, margin, carrier));
Expand All @@ -226,9 +246,9 @@ static Plan plan(XWPFDocument document, int from, Map<IBodyElement, Owner> owner
? ", the part after it opening with its header rows" : "") + ", and its page break fixed "
+ "there, so rows an edit adds above it no longer run onto that page");
} else {
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, section, differ + ": written as a Word "
+ "section that starts where page " + (page + 1) + " does, its page break fixed there, so text "
+ "an edit adds above it no longer runs onto that page");
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, pagePath(section, page), differ
+ ": written as a Word section that starts where page " + (page + 1) + " does, its page break "
+ "fixed there, so text an edit adds above it no longer runs onto that page");
}
inForce = margin;
inForceFrom = page;
Expand All @@ -239,8 +259,8 @@ static Plan plan(XWPFDocument document, int from, Map<IBodyElement, Owner> owner
}

/**
* Names each run of pages whose side margins differ from the document's, once: they are not
* written, as the body is written against the document's width.
* Names each run of pages whose side margins differ from the document's, once, and each page of
* it: they are not written, as the body is written against the document's width.
*/
private static void nameTheSides(DocxLayoutMetrics layout, DocxExportReport.Builder report, String section,
Margin sides) {
Expand All @@ -249,13 +269,17 @@ private static void nameTheSides(DocxLayoutMetrics layout, DocxExportReport.Buil
}
for (int page = 0; page < layout.pageCount(); page++) {
Margin margin = layout.marginOf(page);
boolean differs = !sameSides(margin, sides);
boolean opensARun = page == 0 || !sameSides(margin, layout.marginOf(page - 1));
if (differs && opensARun) {
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, section, "page " + (page + 1)
+ "'s side margins differ from the document's and are not written: the body is written "
+ "against the document's width, and Word sets its text between the document's side margins");
if (sameSides(margin, sides)) {
continue;
}
List<Integer> run = new ArrayList<>(List.of(page));
while (page + 1 < layout.pageCount() && sameSides(layout.marginOf(page + 1), margin)) {
run.add(++page);
}
report.add(DocxExportReport.Severity.APPROXIMATED, PAGE_MARGIN, pagePath(section, run.get(0)),
(run.size() == 1 ? "page " + (run.get(0) + 1) + "'s side margins" : "the side margins of "
+ pages(run)) + " differ from the document's and are not written: the body is written "
+ "against the document's width, and Word sets its text between the document's side margins");
}
}

Expand Down Expand Up @@ -296,6 +320,46 @@ static void apply(XWPFDocument document, Plan plan) {
asALaterSection(body, opening, margin, plan.zoned());
}

/** What a note about one page of a section names: the page, after its section in a document of several. */
private static String pagePath(String section, int page) {
return (section == null ? "" : section + ", ") + "page " + (page + 1);
}

/**
* Some pages' top and bottom margins, and those Word sets them with: "page 3's top and bottom
* margins differ from page 1's, which Word sets it with", or "the top and bottom margins of pages
* 3 and 4 differ from page 1's, which Word sets them with".
*/
private static String notSetWith(List<Integer> pages, int inForceFrom) {
String from = " differ from page " + (inForceFrom + 1) + "'s, which Word sets ";
return pages.size() == 1 ? "page " + (pages.get(0) + 1) + "'s top and bottom margins" + from + "it with"
: "the top and bottom margins of " + pages(pages) + from + "them with";
}

/**
* Pages counted from 0, named from 1: "pages 2 and 3", "pages 2, 3 and 5", and three or more in a
* row as "pages 2 to 4".
*/
private static String pages(List<Integer> pages) {
List<String> named = new ArrayList<>();
for (int first = 0; first < pages.size(); ) {
int last = first;
while (last + 1 < pages.size() && pages.get(last + 1) == pages.get(last) + 1) {
last++;
}
if (last - first >= 2) {
named.add((pages.get(first) + 1) + " to " + (pages.get(last) + 1));
} else {
for (int page = first; page <= last; page++) {
named.add(String.valueOf(pages.get(page) + 1));
}
}
first = last + 1;
}
return "pages " + (named.size() == 1 ? named.get(0)
: String.join(", ", named.subList(0, named.size() - 1)) + " and " + named.get(named.size() - 1));
}

/** The last block written before a page that runs onto it, if one does. */
private static Owner spanning(int page, List<IBodyElement> elements, Map<IBodyElement, Owner> owners) {
Owner spanning = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ record Entry(Fate fate, String note) {
"fragments:WRITTEN:where the nodes' entries say",
"pageGeometry:REPORTED:a page's side margins, written as the document's, named where they differ; "
+ "top and bottom margins that differ from those Word sets a page with, where it starts inside a "
+ "block, after a page whose content reaches its foot under a table, or in a document of sections "
+ "block or where no block starts it, after a page whose content reaches its foot under a table, or in "
+ "a document of sections "
+ "whose header or footer counts a section's pages, named; any other is written, a later page's top "
+ "and bottom margins as a Word section starting where it does");

Expand Down
Loading
Loading