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

### Public API

- **A DOCX export keeps each anchor's bookmark its own, writes no control character the page
draws nothing for, sets a margin below zero out past its side, and says what a rounded panel
keeps.** A bookmark name a suffix made for one anchor — "Section-1" after "Section 1" took
`Section_1_2` — was given again to the anchor that cleans to it, "Section 1 2", so two links
went to one place; a name is now taken only once it is free. An anchor two nodes declare was a
bookmark on each, one name twice, where Word goes to the first and the page to the last: the
file now keeps the last it writes, and the report names the anchor — where the page's last is
a node the file writes no bookmark for too. A character the page leaves out — a control (a
bell, a null, a tab), a soft hyphen, a zero-width space, one of private use — was written, the
controls mostly as a question mark ("Total? due"), in runs, headers and footers, a badge's
initials, a list's marker and a picture's or a barcode's description; it is now left out too,
the line breaks and the direction marks and joiners kept. A margin below zero on a paragraph or
a list, and on the right of a block round them, was written as none, the text at the margin
where the page set it out past it; it is now an indent below zero in the flow. In a table
cell, a panel's included, the text stands as far out as the cell's text and no further, what
is held back named; a row's first child the page sets out past the row's left edge starts at
it, named. The corner-radius note on a rounded panel said it kept its fill where it had only a
border; it now says which it keeps.
- **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
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/recipes/docx-export.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.demcha.compose.document.backend.semantic.docx;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
* The name Word will accept for one of the document's own anchors.
Expand All @@ -29,6 +31,8 @@ final class DocxBookmarkNames {

private final Map<String, String> byAnchor = new HashMap<>();
private final Map<String, Integer> collisions = new HashMap<>();
/** Every name given, without case. */
private final Set<String> taken = new HashSet<>();
private int nextId;

/**
Expand Down Expand Up @@ -72,16 +76,24 @@ private String mint(String anchor) {

// Two anchors that clean to the same text are still two anchors. Without this the
// second one's bookmark would sit on the first one's name and every link to either
// would land in the same place.
// would land in the same place. A name a suffix makes is another anchor's own too:
// "Section-1" became "Section_1_2", which "Section 1 2" cleans to, so a name is taken
// only once it is free. Word reads bookmark names without case.
String key = base.toLowerCase(Locale.ROOT);
int seen = collisions.merge(key, 1, Integer::sum);
if (seen == 1) {
return base;
// The n-th anchor of a name is tried as name_n first, the first as the name itself.
int number = Math.max(seen, 2) - 1;
String name = seen == 1 ? base : suffixed(base, ++number);
while (!taken.add(name.toLowerCase(Locale.ROOT))) {
name = suffixed(base, ++number);
}
String suffix = "_" + seen;
String trimmed = base.length() + suffix.length() > MAX_LENGTH
? base.substring(0, MAX_LENGTH - suffix.length())
: base;
return trimmed + suffix;
return name;
}

/** A name with a number after it, cut to fit Word's length. */
private static String suffixed(String base, int number) {
String suffix = "_" + number;
return (base.length() + suffix.length() > MAX_LENGTH ? base.substring(0, MAX_LENGTH - suffix.length()) : base)
+ suffix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,31 @@ private Map<String, PlacedFragment> zoneFragments(int zoneIndex, int page,
return found;
}

/**
* How many nodes declare each anchor the layout registers: the page sends a link to the last.
* A node a repeated table header draws again is one node; a page zone's, for which the file
* holds no bookmark, is left out.
*
* @return each anchor's count of declaring nodes, empty without a layout
*/
Map<String, Integer> anchorsDeclared() {
Map<String, java.util.Set<String>> nodes = new HashMap<>();
for (Map.Entry<String, List<PlacedFragment>> entry : fragments.entrySet()) {
if (entry.getKey().startsWith("@page-zone")) {
continue;
}
for (PlacedFragment fragment : entry.getValue()) {
if (fragment.payload() instanceof com.demcha.compose.document.layout.payloads.AnchorMarkerPayload marker
&& !marker.anchor().isEmpty()) {
nodes.computeIfAbsent(marker.anchor(), key -> new java.util.HashSet<>()).add(entry.getKey());
}
}
}
Map<String, Integer> counts = new HashMap<>();
nodes.forEach((anchor, declaring) -> counts.put(anchor, declaring.size()));
return counts;
}

/**
* The first page a page zone is drawn on.
*
Expand Down
Loading
Loading