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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ Entries land here as they merge.
of orphaning its heading from the content below. Blocks taller than a page
still flow. Default off — existing layouts are byte-identical.

### Bug fixes

- **DOCX export no longer drops lists.** `DocxSemanticBackend` had no branch
for `ListNode`, so `addList(...)` content silently vanished from Word
exports. Lists now map to marker-prefixed paragraphs in the list's text
style, with nested items indented per depth and keeping their own markers.
(Found by the recipe fact-check: the docx-export recipe's "what is skipped"
list could not honestly be written without it.)

### Documentation

- **Recipe coverage is complete.** Nine new cookbook pages close every gap the
Expand Down
1 change: 1 addition & 0 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ render PDF carry no POI footprint.
| Document node | DOCX output |
|---|---|
| Paragraphs | Word paragraphs with alignment, font, size, colour, bold/italic/underline; inline runs preserved |
| Lists | Marker-prefixed paragraphs in the list's text style; nested items indent per depth and keep their own markers |
| Tables | Word tables, one cell per cell |
| Images | Embedded pictures at the node's declared size |
| Rows | A one-row table, so editors keep the side-by-side layout (cell content limited to atomic children) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ private void writeNode(XWPFDocument document, DocumentNode node) throws Exceptio
writeShapeContainer(document, shapeContainer);
} else if (node instanceof ChartNode chart) {
writeChartFallback(document, chart);
} else if (node instanceof com.demcha.compose.document.node.ListNode list) {
writeList(document, list);
} else if (node instanceof ContainerNode || node instanceof SectionNode) {
for (DocumentNode child : node.children()) {
writeNode(document, child);
Expand All @@ -155,6 +157,41 @@ private void writeNode(XWPFDocument document, DocumentNode node) throws Exceptio
// should use the PDF fixed-layout backend.
}

/**
* Semantic list mapping: each item becomes a marker-prefixed paragraph in
* the list's text style; nested items indent two spaces per depth and use
* their own marker when one is set.
*/
private void writeList(XWPFDocument document,
com.demcha.compose.document.node.ListNode list) {
for (String item : list.items()) {
writeListLine(document, list.textStyle(),
list.marker().value() + " " + item, 0);
}
for (com.demcha.compose.document.node.ListItem item : list.nestedItems()) {
writeNestedItem(document, list, item, 0);
}
}

private void writeNestedItem(XWPFDocument document,
com.demcha.compose.document.node.ListNode list,
com.demcha.compose.document.node.ListItem item,
int depth) {
String marker = item.marker() != null ? item.marker().value() : list.marker().value();
writeListLine(document, list.textStyle(), marker + " " + item.label(), depth);
for (com.demcha.compose.document.node.ListItem child : item.children()) {
writeNestedItem(document, list, child, depth + 1);
}
}

private void writeListLine(XWPFDocument document, DocumentTextStyle style,
String text, int depth) {
XWPFParagraph para = document.createParagraph();
XWPFRun run = para.createRun();
applyStyle(run, style);
run.setText(" ".repeat(depth) + text);
}

/**
* Semantic chart fallback: the semantic export has no layout pass, so the
* chart's compiled vector geometry is unavailable here. The chart's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ void chartExportsAsItsDataTable() throws Exception {
}
}

@Test
void listsExportAsMarkerPrefixedParagraphs() throws Exception {
byte[] docxBytes;
try (DocumentSession session = GraphCompose.document()
.pageSize(595, 842)
.margin(DocumentInsets.of(36))
.create()) {
session.dsl().pageFlow().name("Flow")
.addList("First", "Second")
.build();
docxBytes = session.export(new DocxSemanticBackend());
}

try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docxBytes))) {
List<String> texts = document.getParagraphs().stream()
.map(XWPFParagraph::getText).toList();
assertThat(texts).anyMatch(t -> t.endsWith("First") && t.length() > "First".length());
assertThat(texts).anyMatch(t -> t.endsWith("Second"));
}
}

@Test
void exportProducesDocxWithParagraphAndTableContent() throws Exception {
byte[] docxBytes;
Expand Down