maxXwpfNum = numbering.getNums().stream().max(Comparator.comparing(o -> o.getCTNum().getNumId()));
+ this.maxNum = maxXwpfNum.map(xwpfNum -> xwpfNum.getCTNum().getNumId().intValue()).orElse(0);
+
} catch (Exception e) {
new DocxGenerationException(e.getClass().getSimpleName() + " Copying numbering definitions from template doc: " + e.getMessage(), e);
}
@@ -3114,5 +3177,139 @@ else if ("jpeg".equals(imgExtension) ||
return format;
}
+ /**
+ *
+ * Add table grid (w:tblGrid) and grid columns (w:gridCol) based on table column definitions.
+ *
+ *
+ * @param table XWPF table
+ * @param colDefs table column definitions
+ */
+ public static void addTableGridWithColumnsIfNeeded(XWPFTable table, TableColumnDefinitions colDefs) {
+ setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs);
+ CTTblGrid tblGrid = table.getCTTbl().getTblGrid();
+ if (tblGrid == null) {
+ tblGrid = table.getCTTbl().addNewTblGrid();
+ for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
+ String specifiedWidth = colDef.getSpecifiedWidth();
+ CTTblGridCol gridCol = tblGrid.addNewGridCol();
+ BigInteger gridColWidth;
+ // logic below has been copied from XWPFTable.setWidthValue
+ if (specifiedWidth.matches(XWPFTable.REGEX_PERCENTAGE)) {
+ String numberPart = specifiedWidth.substring(0, specifiedWidth.length() - 1);
+ double percentage = Double.parseDouble(numberPart) * 50;
+ long intValue = Math.round(percentage);
+ gridColWidth = BigInteger.valueOf(intValue);
+ } else if (specifiedWidth.matches("auto")) {
+ gridColWidth = BigInteger.ZERO;
+ } else {
+ gridColWidth = new BigInteger(specifiedWidth);
+ }
+ gridCol.setW(gridColWidth);
+ }
+ }
+ }
+
+ /**
+ *
+ * Set table width to 100% and change width type if needed for correct displaying in the both MS Word and LibreOffice
+ *
+ *
+ * @param table XWPF table
+ */
+ public static void setDefaultTableWidthIfNeeded(XWPFTable table) {
+ if (table.getWidthType() == TableWidthType.AUTO && table.getWidth() == 0) {
+ table.setWidth(DEFAULT_TABLE_WIDTH);
+ } else if (table.getWidthType() == TableWidthType.NIL) {
+ table.setWidthType(TableWidthType.PCT);
+ table.setWidth(DEFAULT_TABLE_WIDTH);
+ }
+ }
+
+ /**
+ *
+ * Set columns width in percentages depending on columns count, if all columns have auto width
+ *
+ *
+ * @param colDefs table column definitions
+ */
+ public static void setColumnsWidthInPercentagesIfAllHaveAutoWidth(TableColumnDefinitions colDefs) {
+ if (colDefs.getColumnDefinitions().isEmpty()) {
+ return;
+ }
+ boolean autoWidth = true;
+ for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
+ String specifiedWidth = colDef.getSpecifiedWidth();
+ if (!"auto".equalsIgnoreCase(specifiedWidth)) {
+ autoWidth = false;
+ break;
+ }
+ }
+ if (autoWidth) {
+ final int columnsCount = colDefs.getColumnDefinitions().size();
+ final int columnWidth = 100 / columnsCount;
+ final int dotsPerInch = 72;
+ for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
+ try {
+ colDef.setWidth(columnWidth + "%", dotsPerInch);
+ } catch (MeasurementException e) {
+ throw new RuntimeException("Error setting column width: " + columnWidth, e);
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * Link list numId to style abstractNumId and restart list items ordering from 1
+ *
+ *
+ * Assume all paras have the same style
+ *
+ *
+ * @param doc XWPF document
+ * @param paras list items
+ * @param numId list numId
+ */
+ public static void linkListNumIdToStyleAbstractIdAndRestartListLevels(XWPFDocument doc, List paras, BigInteger numId) {
+ String styleId = paras.get(0).getStyle();
+ XWPFStyle style = doc.getStyles().getStyle(styleId);
+ if (style != null) {
+ linkNumIdToStyleAbstractId(style, doc, numId);
+ } else {
+ doc.getNumbering().addNum(numId);
+ }
+ addLevelOverrideFromOneToNumId(doc, numId);
+ }
+
+ /**
+ *
+ * Link numId to style abstractNumId
+ *
+ *
+ * @param style XWPF style
+ * @param doc XWPF document
+ * @param numId list numId
+ */
+ private static void linkNumIdToStyleAbstractId(XWPFStyle style, XWPFDocument doc, BigInteger numId) {
+ BigInteger styleNumId = style.getCTStyle().getPPr().getNumPr().getNumId().getVal();
+ BigInteger abstractNumId = doc.getNumbering().getNum(styleNumId).getCTNum().getAbstractNumId().getVal();
+ doc.getNumbering().addNum(abstractNumId, numId);
+ }
+
+ /**
+ *
+ * Add level override from 1 to numId
+ *
+ *
+ * @param doc XWPF document
+ * @param numId list numId
+ */
+ private static void addLevelOverrideFromOneToNumId(XWPFDocument doc, BigInteger numId) {
+ CTNumLvl numLvl = doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride();
+ numLvl.setIlvl(BigInteger.ZERO);
+ CTDecimalNumber startOverride = numLvl.addNewStartOverride();
+ startOverride.setVal(BigInteger.ONE);
+ }
}
diff --git a/src/main/xsl/html2docx/baseProcessing.xsl b/src/main/xsl/html2docx/baseProcessing.xsl
index 22de534..c1ba5e7 100644
--- a/src/main/xsl/html2docx/baseProcessing.xsl
+++ b/src/main/xsl/html2docx/baseProcessing.xsl
@@ -141,10 +141,8 @@
-
-
-
-
+
+
diff --git a/src/main/xsl/html2docx/get-style-name.xsl b/src/main/xsl/html2docx/get-style-name.xsl
index 54be602..2fa7f1b 100644
--- a/src/main/xsl/html2docx/get-style-name.xsl
+++ b/src/main/xsl/html2docx/get-style-name.xsl
@@ -87,7 +87,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java
index c21a579..3f275ec 100644
--- a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java
+++ b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java
@@ -3,6 +3,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -10,6 +11,7 @@
import org.apache.poi.xwpf.usermodel.BodyElementType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.IRunElement;
+import org.apache.poi.xwpf.usermodel.TableWidthType;
import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFFooter;
@@ -20,28 +22,40 @@
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
+import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.Mockito;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType;
import org.wordinator.xml2docx.generator.DocxConstants;
import org.wordinator.xml2docx.generator.DocxGenerator;
+import org.wordinator.xml2docx.generator.MeasurementException;
+import org.wordinator.xml2docx.generator.TableColumnDefinitions;
import junit.framework.TestCase;
public class TestDocxGenerator extends TestCase {
public static final String DOTX_TEMPLATE_PATH = "docx/Test_Template.dotx";
+ private static final int DOTS_PER_INCH = 72;
@Test
public void testMakeDocx() throws Exception {
@@ -515,6 +529,215 @@ public void testNestedTableWidth() throws Exception {
assertEquals(BodyElementType.TABLE, elem.getElementType());
}
+ @Test
+ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_in_percentages() throws MeasurementException {
+ // GIVEN
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ CTTbl ctTbl = Mockito.mock(CTTbl.class);
+ CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class);
+ CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class);
+ CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class);
+ Mockito.when(table.getCTTbl()).thenReturn(ctTbl);
+ Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid);
+ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2);
+
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH);
+ colDefs.newColumnDef().setWidth("70%", DOTS_PER_INCH);
+
+ // WHEN
+ DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs);
+
+ // THEN
+ Mockito.verify(col1).setW(BigInteger.valueOf(1500));
+ Mockito.verify(col2).setW(BigInteger.valueOf(3500));
+ }
+
+ @Test
+ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_mixed_width() throws MeasurementException {
+ // GIVEN
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ CTTbl ctTbl = Mockito.mock(CTTbl.class);
+ CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class);
+ CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class);
+ CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class);
+ Mockito.when(table.getCTTbl()).thenReturn(ctTbl);
+ Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid);
+ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2);
+
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH);
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs);
+
+ // THEN
+ Mockito.verify(col1).setW(BigInteger.valueOf(1500));
+ Mockito.verify(col2).setW(BigInteger.ZERO);
+ }
+
+ @Test
+ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_auto_width() {
+ // GIVEN
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ CTTbl ctTbl = Mockito.mock(CTTbl.class);
+ CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class);
+ CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class);
+ CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class);
+ Mockito.when(table.getCTTbl()).thenReturn(ctTbl);
+ Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid);
+ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2);
+
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidthAuto();
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs);
+
+ // THEN
+ Mockito.verify(col1).setW(BigInteger.valueOf(2500));
+ Mockito.verify(col2).setW(BigInteger.valueOf(2500));
+ }
+
+ @Test
+ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_in_ints() throws MeasurementException {
+ // GIVEN
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ CTTbl ctTbl = Mockito.mock(CTTbl.class);
+ CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class);
+ CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class);
+ CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class);
+ Mockito.when(table.getCTTbl()).thenReturn(ctTbl);
+ Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid);
+ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2);
+
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidth("30", DOTS_PER_INCH);
+ colDefs.newColumnDef().setWidth("70", DOTS_PER_INCH);
+
+ // WHEN
+ DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs);
+
+ // THEN
+ Mockito.verify(col1).setW(BigInteger.valueOf(30));
+ Mockito.verify(col2).setW(BigInteger.valueOf(70));
+ }
+
+ @Test
+ public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_for_auto_width_type() {
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ Mockito.when(table.getWidthType()).thenReturn(TableWidthType.AUTO);
+ Mockito.when(table.getWidth()).thenReturn(0);
+ DocxGenerator.setDefaultTableWidthIfNeeded(table);
+ Mockito.verify(table).setWidth("100%");
+ }
+
+ @Test
+ public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_and_pct_type_for_nil_width_type() {
+ XWPFTable table = Mockito.mock(XWPFTable.class);
+ Mockito.when(table.getWidthType()).thenReturn(TableWidthType.NIL);
+ DocxGenerator.setDefaultTableWidthIfNeeded(table);
+ Mockito.verify(table).setWidthType(TableWidthType.PCT);
+ Mockito.verify(table).setWidth("100%");
+ }
+
+ @Test
+ public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_set_33_percentages_width() {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidthAuto();
+ colDefs.newColumnDef().setWidthAuto();
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs);
+
+ // THEN
+ Assert.assertEquals("33%", colDefs.get(0).getWidth());
+ Assert.assertEquals("33%", colDefs.get(1).getWidth());
+ Assert.assertEquals("33%", colDefs.get(2).getWidth());
+ }
+
+ @Test
+ public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_empty_definitions() {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs);
+
+ // THEN
+ Assert.assertTrue(colDefs.getColumnDefinitions().isEmpty());
+ }
+
+ @Test
+ public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_not_all_definitions_are_auto() throws MeasurementException {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH);
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs);
+
+ // THEN
+ Assert.assertEquals("30%", colDefs.get(0).getWidth());
+ Assert.assertEquals("auto", colDefs.get(1).getWidth());
+ }
+
+ @Test
+ public void testLinkListNumIdToStyleAbstractIdAndRestartListLevels__should_set_abstract_num_id_and_restart_list_ordering() {
+ // GIVEN
+ XWPFDocument doc = Mockito.mock(XWPFDocument.class, Answers.RETURNS_DEEP_STUBS);
+ XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class);
+ XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS);
+ CTNumLvl numLvl = Mockito.mock(CTNumLvl.class);
+ CTDecimalNumber startOverride = Mockito.mock(CTDecimalNumber.class);
+ BigInteger numId = BigInteger.valueOf(27);
+ BigInteger styleNumId = BigInteger.valueOf(89);
+ BigInteger styleAbstractNumId = BigInteger.valueOf(12);
+ Mockito.when(paragraph.getStyle()).thenReturn("FancyStyle123");
+ Mockito.when(doc.getStyles().getStyle("FancyStyle123")).thenReturn(style);
+ Mockito.when(doc.getNumbering().getNum(styleNumId).getCTNum().getAbstractNumId().getVal()).thenReturn(styleAbstractNumId);
+ Mockito.when(style.getCTStyle().getPPr().getNumPr().getNumId().getVal()).thenReturn(styleNumId);
+ Mockito.when(doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride()).thenReturn(numLvl);
+ Mockito.when(numLvl.addNewStartOverride()).thenReturn(startOverride);
+ List paras = Collections.singletonList(paragraph);
+
+ // WHEN
+ DocxGenerator.linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, numId);
+
+ // THEN
+ Mockito.verify(doc.getNumbering()).addNum(styleAbstractNumId, numId);
+ Mockito.verify(numLvl).setIlvl(BigInteger.ZERO);
+ Mockito.verify(startOverride).setVal(BigInteger.ONE);
+ }
+
+ @Test
+ public void testLinkListNumIdToStyleAbstractIdAndRestartListLevels__should_not_set_abstract_num_id_but_restart_list_ordering_if_style_does_not_exist() {
+ // GIVEN
+ XWPFDocument doc = Mockito.mock(XWPFDocument.class, Answers.RETURNS_DEEP_STUBS);
+ XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class);
+ CTNumLvl numLvl = Mockito.mock(CTNumLvl.class);
+ CTDecimalNumber startOverride = Mockito.mock(CTDecimalNumber.class);
+ BigInteger numId = BigInteger.valueOf(27);
+ Mockito.when(paragraph.getStyle()).thenReturn("FancyStyle123");
+ Mockito.when(doc.getStyles().getStyle("FancyStyle123")).thenReturn(null);
+ Mockito.when(doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride()).thenReturn(numLvl);
+ Mockito.when(numLvl.addNewStartOverride()).thenReturn(startOverride);
+ List paras = Collections.singletonList(paragraph);
+
+ // WHEN
+ DocxGenerator.linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, numId);
+
+ // THEN
+ Mockito.verify(doc.getNumbering()).addNum(numId);
+ Mockito.verify(numLvl).setIlvl(BigInteger.ZERO);
+ Mockito.verify(startOverride).setVal(BigInteger.ONE);
+ }
+
// ===== INTERNAL UTILITIES
private XWPFDocument convert(String infile, String outfile) throws Exception {