Skip to content

Commit 6ceb32d

Browse files
authored
Merge pull request #156 from DemchaAV/fix/docx-list-export
fix(docx): lists no longer silently dropped from the semantic export
2 parents b17cad2 + 45a0cd1 commit 6ceb32d

4 files changed

Lines changed: 68 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ Entries land here as they merge.
8282
of orphaning its heading from the content below. Blocks taller than a page
8383
still flow. Default off — existing layouts are byte-identical.
8484

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

8796
- **Recipe coverage is complete.** Nine new cookbook pages close every gap the

docs/recipes/docx-export.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ render PDF carry no POI footprint.
4242
| Document node | DOCX output |
4343
|---|---|
4444
| Paragraphs | Word paragraphs with alignment, font, size, colour, bold/italic/underline; inline runs preserved |
45+
| Lists | Marker-prefixed paragraphs in the list's text style; nested items indent per depth and keep their own markers |
4546
| Tables | Word tables, one cell per cell |
4647
| Images | Embedded pictures at the node's declared size |
4748
| Rows | A one-row table, so editors keep the side-by-side layout (cell content limited to atomic children) |

src/main/java/com/demcha/compose/document/backend/semantic/DocxSemanticBackend.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ private void writeNode(XWPFDocument document, DocumentNode node) throws Exceptio
145145
writeShapeContainer(document, shapeContainer);
146146
} else if (node instanceof ChartNode chart) {
147147
writeChartFallback(document, chart);
148+
} else if (node instanceof com.demcha.compose.document.node.ListNode list) {
149+
writeList(document, list);
148150
} else if (node instanceof ContainerNode || node instanceof SectionNode) {
149151
for (DocumentNode child : node.children()) {
150152
writeNode(document, child);
@@ -155,6 +157,41 @@ private void writeNode(XWPFDocument document, DocumentNode node) throws Exceptio
155157
// should use the PDF fixed-layout backend.
156158
}
157159

160+
/**
161+
* Semantic list mapping: each item becomes a marker-prefixed paragraph in
162+
* the list's text style; nested items indent two spaces per depth and use
163+
* their own marker when one is set.
164+
*/
165+
private void writeList(XWPFDocument document,
166+
com.demcha.compose.document.node.ListNode list) {
167+
for (String item : list.items()) {
168+
writeListLine(document, list.textStyle(),
169+
list.marker().value() + " " + item, 0);
170+
}
171+
for (com.demcha.compose.document.node.ListItem item : list.nestedItems()) {
172+
writeNestedItem(document, list, item, 0);
173+
}
174+
}
175+
176+
private void writeNestedItem(XWPFDocument document,
177+
com.demcha.compose.document.node.ListNode list,
178+
com.demcha.compose.document.node.ListItem item,
179+
int depth) {
180+
String marker = item.marker() != null ? item.marker().value() : list.marker().value();
181+
writeListLine(document, list.textStyle(), marker + " " + item.label(), depth);
182+
for (com.demcha.compose.document.node.ListItem child : item.children()) {
183+
writeNestedItem(document, list, child, depth + 1);
184+
}
185+
}
186+
187+
private void writeListLine(XWPFDocument document, DocumentTextStyle style,
188+
String text, int depth) {
189+
XWPFParagraph para = document.createParagraph();
190+
XWPFRun run = para.createRun();
191+
applyStyle(run, style);
192+
run.setText(" ".repeat(depth) + text);
193+
}
194+
158195
/**
159196
* Semantic chart fallback: the semantic export has no layout pass, so the
160197
* chart's compiled vector geometry is unavailable here. The chart's

src/test/java/com/demcha/compose/document/backend/semantic/DocxSemanticBackendTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ void chartExportsAsItsDataTable() throws Exception {
6868
}
6969
}
7070

71+
@Test
72+
void listsExportAsMarkerPrefixedParagraphs() throws Exception {
73+
byte[] docxBytes;
74+
try (DocumentSession session = GraphCompose.document()
75+
.pageSize(595, 842)
76+
.margin(DocumentInsets.of(36))
77+
.create()) {
78+
session.dsl().pageFlow().name("Flow")
79+
.addList("First", "Second")
80+
.build();
81+
docxBytes = session.export(new DocxSemanticBackend());
82+
}
83+
84+
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docxBytes))) {
85+
List<String> texts = document.getParagraphs().stream()
86+
.map(XWPFParagraph::getText).toList();
87+
assertThat(texts).anyMatch(t -> t.endsWith("First") && t.length() > "First".length());
88+
assertThat(texts).anyMatch(t -> t.endsWith("Second"));
89+
}
90+
}
91+
7192
@Test
7293
void exportProducesDocxWithParagraphAndTableContent() throws Exception {
7394
byte[] docxBytes;

0 commit comments

Comments
 (0)