> numParas = new LinkedHashMap<>();
/**
*
@@ -416,8 +434,8 @@ public void generate(XmlObject xml) throws DocxGenerationException, XmlException
setupNumbering(doc, this.templateDoc);
setupStyles(doc, this.templateDoc);
constructDoc(doc, xml);
-
- FileOutputStream out = new FileOutputStream(outFile);
+ setNumbering(doc);
+ FileOutputStream out = new FileOutputStream(this.outFile);
doc.write(out);
doc.close();
}
@@ -482,7 +500,7 @@ private XWPFParagraph handleBody(
handleSection(doc, cursor.getObject());
} else if ("table".equals(tagName)) {
XWPFTable table = doc.createTable();
- makeTable(table, cursor.getObject());
+ makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX);
} else if ("object".equals(tagName)) {
// FIXME: This is currently unimplemented.
makeObject(doc, cursor);
@@ -1175,7 +1193,7 @@ private void makeHeaderFooter(XWPFHeaderFooter headerFooter, XmlObject xml) thro
makeParagraph(p, cursor);
} else if ("table".equals(tagName)) {
XWPFTable table = headerFooter.createTable(0, 0);
- makeTable(table, cursor.getObject());
+ makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX);
} else {
// There are other body-level things that could go in a footnote but
// we aren't worrying about them for now.
@@ -1210,7 +1228,15 @@ private HeaderFooterType getHeaderFooterType(XmlCursor cursor) {
* @throws Exception
*/
private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor) throws DocxGenerationException {
- return makeParagraph(p, cursor, null);
+ return makeParagraph(p, cursor, null, MAX_CONTENT_WIDTH_IN_PX);
+ }
+
+ /**
+ * @see DocxGenerator#makeParagraph(XWPFParagraph, XmlCursor)
+ * @param maxWidth Maximum content width in px
+ */
+ private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor, int maxWidth) throws DocxGenerationException {
+ return makeParagraph(p, cursor, null, maxWidth);
}
/**
@@ -1218,18 +1244,21 @@ private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor) throws Do
* @param para The Word paragraph to construct
* @param cursor Cursor pointing at the element the paragraph will reflect.
* @param additionalProperties Additional properties to add to the paragraph, i.e., from sections
+ * @param maxWidth Maximum content width in px
* @return Paragraph (should be same object as passed in).
* @throws Exception
*/
private XWPFParagraph makeParagraph(
XWPFParagraph para,
XmlCursor cursor,
- Map additionalProperties)
+ Map additionalProperties,
+ int maxWidth)
throws DocxGenerationException {
cursor.push();
String styleName = cursor.getAttributeText(DocxConstants.QNAME_STYLE_ATT);
String styleId = cursor.getAttributeText(DocxConstants.QNAME_STYLEID_ATT);
+ String numId = cursor.getAttributeText(DocxConstants.QNAME_NUMID_ATT);
if (null != styleName && null == styleId) {
// Look up the style by name:
@@ -1315,7 +1344,7 @@ private XWPFParagraph makeParagraph(
} else if ("hyperlink".equals(tagName)) {
makeHyperlink(para, cursor);
} else if ("image".equals(tagName)) {
- makeImage(para, cursor);
+ makeImage(para, cursor, maxWidth);
} else if ("object".equals(tagName)) {
makeObject(para, cursor);
} else if ("page-number-ref".equals(tagName)) {
@@ -1328,6 +1357,13 @@ private XWPFParagraph makeParagraph(
} while(cursor.toNextSibling());
}
cursor.pop();
+
+ if (numId != null && !numId.isEmpty()) {
+ // Store Numbering paragraph
+ List paras = this.numParas.computeIfAbsent(numId, k -> new ArrayList<>());
+ paras.add(para);
+ }
+
return para;
}
@@ -1612,7 +1648,7 @@ private void makeFootnote(XWPFParagraph para, XmlObject xml) throws DocxGenerati
makeParagraph(p, cursor);
} else if ("table".equals(tagName)) {
XWPFTable table = note.createTable();
- makeTable(table, cursor.getObject());
+ makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX);
} else {
// There are other body-level things that could go in a footnote but
// we aren't worrying about them for now.
@@ -1882,8 +1918,9 @@ private XWPFHyperlinkRun makeHyperlinkRun(
* Construct an image reference
* @param doc
* @param cursor
+ * @param maxWidth Maximum image width in px
*/
- private void makeImage(XWPFParagraph para, XmlCursor cursor) throws DocxGenerationException {
+ private void makeImage(XWPFParagraph para, XmlCursor cursor, int maxWidth) throws DocxGenerationException {
cursor.push();
// FIXME: This is all a bit scripty because of the need to get both the image data and
@@ -2044,6 +2081,13 @@ private void makeImage(XWPFParagraph para, XmlCursor cursor) throws DocxGenerati
height = (int)Math.round(intrinsicHeight * factor);
}
+ maxWidth = adjustMaxWidthIfIndentationExists(para, maxWidth);
+ if (width > maxWidth) {
+ double ratio = width / (double) height;
+ width = maxWidth;
+ height = (int) (maxWidth / ratio);
+ }
+
// At this point, the measurement is pixels. If the original specification
// was also pixels, we need to convert to inches and then back to pixels
// in order to apply the dots-per-inch value.
@@ -2154,23 +2198,39 @@ private void makeHyperlink(XWPFParagraph para, XmlCursor cursor) throws DocxGene
// Set the appropriate target:
+ XWPFHyperlinkRun hyperlinkRun;
+
if (href.startsWith("#")) {
// Just a fragment ID, must be to a bookmark
String bookmarkName = href.substring(1);
hyperlink.setAnchor(bookmarkName);
+ cursor.push();
+ hyperlinkRun = makeHyperlinkRun(hyperlink, cursor, para);
+ cursor.pop();
} else {
- // Create a relationship that targets the href and use the
- // relationship's ID on the hyperlink
- // It's not yet clear from the POI API how to create a new relationship for
- // use by an external hyperlink.
- // throw new NotImplementedException("Links to external resources not yet implemented.");
- }
+ // Add the link as External relationship
+ String id = para.getDocument().getPackagePart().addExternalRelationship(href, XWPFRelation.HYPERLINK.getRelation()).getId();
- cursor.push();
- XWPFHyperlinkRun hyperlinkRun = makeHyperlinkRun(hyperlink, cursor, para);
- cursor.pop();
- para.addRun(hyperlinkRun);
+ // Append the link and bind it to the relationship
+ hyperlink.setId(id);
+ // Create the linked text
+ String linkedText = cursor.getTextValue();
+ CTText ctText = CTText.Factory.newInstance();
+ ctText.setStringValue(linkedText);
+ CTR ctr = CTR.Factory.newInstance();
+ ctr.setTArray(new CTText[]{ctText});
+
+ // Create the formatting
+ CTRPr rpr = ctr.addNewRPr();
+ rpr.addNewRStyle().setVal("Hyperlink");
+
+ // Insert the linked text into the link
+ hyperlink.setRArray(new CTR[]{ctr});
+
+ hyperlinkRun = new XWPFHyperlinkRun(hyperlink, CTR.Factory.newInstance(), para);
+ }
+ para.addRun(hyperlinkRun);
}
/**
@@ -2218,9 +2278,10 @@ private void makeObject(XWPFDocument doc, XmlCursor cursor) throws DocxGeneratio
* Construct a table.
* @param table Table object to construct
* @param xml The <table> element
+ * @param maxWidth Maximum content width in px
* @throws DocxGenerationException
*/
- private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationException {
+ private void makeTable(XWPFTable table, XmlObject xml, int maxWidth) throws DocxGenerationException {
// If the column widths are absolute measurements they can be set on the grid,
// but if they are proportional, then they have to be set on at least the first
@@ -2323,6 +2384,10 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce
} while (cursor.toNextSibling());
}
+ setDefaultTableWidthIfNeeded(table);
+ setColumnsWidthInPercentages(colDefs, maxWidth);
+ addTableGridWithColumnsIfNeeded(table, colDefs);
+
// populate the rows and cells.
cursor = xml.newCursor();
@@ -2333,7 +2398,7 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce
RowSpanManager rowSpanManager = new RowSpanManager();
do {
// Process the rows
- XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults);
+ XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults, maxWidth);
row.setRepeatHeader(true);
} while(cursor.toNextSibling());
}
@@ -2347,7 +2412,7 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce
RowSpanManager rowSpanManager = new RowSpanManager();
do {
// Process the rows
- XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults);
+ XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults, maxWidth);
// Adjust row as needed.
row.getCtRow(); // For setting low-level properties.
} while(cursor.toNextSibling());
@@ -2710,6 +2775,7 @@ private STBorder.Enum stBorderType(XWPFBorderType borderType) {
* @param colDefs Column definitions
* @param rowSpanManager Manages setting vertical spanning across multiple rows.
* @param defaults Defaults inherited from the table (or elsewhere)
+ * @param maxWidth Maximum content width in px
* @return Constructed row object
* @throws DocxGenerationException
*/
@@ -2718,7 +2784,8 @@ private XWPFTableRow makeTableRow(
XmlObject xml,
TableColumnDefinitions colDefs,
RowSpanManager rowSpanManager,
- Map defaults)
+ Map defaults,
+ int maxWidth)
throws DocxGenerationException {
XmlCursor cursor = xml.newCursor();
XWPFTableRow row = table.createRow();
@@ -2878,7 +2945,16 @@ private XWPFTableRow makeTableRow(
while (hasMore) {
if (cursor.getName().equals(DocxConstants.QNAME_P_ELEM)) {
XWPFParagraph p = cell.addParagraph();
- makeParagraph(p, cursor);
+ int widthInPx = convertColumnWidthToPx(colDef, maxWidth);
+ if (colspan != null && !colspan.equals("1")) {
+ int colspanValue = Integer.parseInt(colspan);
+ for (int i = colDef.getColumnIndex() + 1; i < colDef.getColumnIndex() + colspanValue; i++) {
+ widthInPx += convertColumnWidthToPx(colDefs.get(i), maxWidth);
+ }
+ }
+ int cellLeftMargin = table.getCellMarginLeft() != 0 ? table.getCellMarginLeft() : DEFAULT_CELL_MARGIN;
+ int cellRightMargin = table.getCellMarginRight() != 0 ? table.getCellMarginRight() : DEFAULT_CELL_MARGIN;
+ makeParagraph(p, cursor, widthInPx - cellLeftMargin - cellRightMargin);
if (null != align) {
if ("JUSTIFY".equalsIgnoreCase(align)) {
// Issue 18: "BOTH" is the better match to "JUSTIFY"
@@ -2901,7 +2977,8 @@ private XWPFTableRow makeTableRow(
tblPr.addNewTblW();
XWPFTable nestedTable = new XWPFTable(ctTbl, cell);
- makeTable(nestedTable, cursor.getObject());
+ int widthInPx = convertColumnWidthToPx(colDef, maxWidth);
+ makeTable(nestedTable, cursor.getObject(), widthInPx);
// for some reason this inserts two tables, where the
// first one is empty. we need to remove that one.
@@ -2992,6 +3069,26 @@ private void setupStyles(XWPFDocument doc, XWPFDocument templateDoc) throws Docx
}
+ private void setNumbering(XWPFDocument doc) {
+ int i = 1;
+
+ for (List paras : this.numParas.values()) {
+
+ int newNum = this.maxNum + i;
+ BigInteger bgiNumId = BigInteger.valueOf(newNum);
+ for (XWPFParagraph numPara : paras) {
+ CTPPr ctpPr = numPara.getCTPPr();
+ CTNumPr ctNumPr = ctpPr.addNewNumPr();
+ CTDecimalNumber decNumId = CTDecimalNumber.Factory.newInstance();
+ decNumId.setVal(bgiNumId);
+ ctNumPr.setNumId(decNumId);
+ }
+ linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, bgiNumId);
+
+ i++;
+ }
+ }
+
private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws DocxGenerationException {
// Load the template's numbering definitions to the new document
@@ -3028,6 +3125,10 @@ private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws D
} while (num != null);
+ // Calculate max numbering number
+ Optional 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 +3215,197 @@ 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) {
+ 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 setColumnsWidthInPercentages(TableColumnDefinitions colDefs, int maxWidth) {
+ if (colDefs.getColumnDefinitions().isEmpty()) {
+ return;
+ }
+ int dotsPerInch = 72;
+ double percentagesLeftForAutoWidth = 100;
+ List columnsWithAutoWidth = new ArrayList<>();
+ for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
+ String specifiedWidth = colDef.getSpecifiedWidth();
+ if ("auto".equalsIgnoreCase(specifiedWidth)) {
+ columnsWithAutoWidth.add(colDef);
+ } else if (!"auto".equals(specifiedWidth) && !specifiedWidth.endsWith("%")) {
+ int px = Integer.parseInt(specifiedWidth);
+ double percentages = BigDecimal.valueOf(100.0 * px / maxWidth).setScale(4, RoundingMode.FLOOR).doubleValue();
+ percentagesLeftForAutoWidth -= percentages;
+ setColumnWidthInPercentages(colDef, percentages, dotsPerInch);
+ } else if (specifiedWidth.endsWith("%")) {
+ double percentages = Double.parseDouble(specifiedWidth.substring(0, specifiedWidth.length() - 1));
+ percentagesLeftForAutoWidth -= percentages;
+ }
+ }
+ if (!columnsWithAutoWidth.isEmpty()) {
+ int columnsCount = columnsWithAutoWidth.size();
+ double width = BigDecimal.valueOf(percentagesLeftForAutoWidth / columnsCount).setScale(4, RoundingMode.FLOOR).doubleValue();
+ columnsWithAutoWidth.forEach(it -> setColumnWidthInPercentages(it, width, dotsPerInch));
+ }
+ }
+
+ /**
+ *
+ * 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);
+ }
+
+ /**
+ *
+ * Convert column width to px
+ *
+ *
+ * @param columnDefinition table column definition
+ * @param maxWidth Maximum table width in px
+ * @return column width in px
+ */
+ public static int convertColumnWidthToPx(TableColumnDefinition columnDefinition, int maxWidth) {
+ String width = columnDefinition.getWidth();
+ if (width.endsWith("%")) {
+ return (int) (Double.parseDouble(width.substring(0, width.length() - 1)) * maxWidth / 100.0);
+ }
+ return Integer.parseInt(width);
+ }
+
+ /**
+ *
+ * Adjusts width if paragraph has style with indentation
+ *
+ *
+ * @param para XWPF paragraph
+ * @param maxWidth Maximum width in px
+ * @return adjusted width
+ */
+ public static int adjustMaxWidthIfIndentationExists(XWPFParagraph para, int maxWidth) {
+ String style = para.getStyle();
+ XWPFStyle xwpfStyle = para.getDocument().getStyles().getStyle(style);
+ Object leftIndentation = null;
+ if (xwpfStyle != null) {
+ CTPPrGeneral pPr = xwpfStyle.getCTStyle().getPPr();
+ if (pPr.getNumPr() != null) {
+ BigInteger numId = pPr.getNumPr().getNumId().getVal();
+ XWPFNumbering numbering = para.getDocument().getNumbering();
+ BigInteger abstractNumId = numbering.getNum(numId).getCTNum().getAbstractNumId().getVal();
+ CTAbstractNum abstractNum = numbering.getAbstractNum(abstractNumId).getCTAbstractNum();
+ leftIndentation = abstractNum.getLvlArray(0).getPPr().getInd().getLeft();
+ } else if (pPr.getInd() != null) {
+ leftIndentation = pPr.getInd().getLeft();
+ }
+ }
+ if (leftIndentation instanceof BigInteger) {
+ int scale = 20;
+ maxWidth -= ((BigInteger) leftIndentation).intValue() / scale;
+ }
+ return maxWidth;
+ }
+
+ private static void setColumnWidthInPercentages(TableColumnDefinition colDef, double width, int dotsPerInch) {
+ try {
+ colDef.setWidth(width + "%", dotsPerInch);
+ } catch (MeasurementException e) {
+ throw new RuntimeException("Error setting column width: " + width, e);
+ }
+ }
}
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..b4e1d44 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,44 @@
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.CTInd;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
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.TableColumnDefinition;
+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 +533,273 @@ 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_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 testSetColumnsWidthInPercentages__should_set_33_percentages_width() {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidthAuto();
+ colDefs.newColumnDef().setWidthAuto();
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentages(colDefs, 100);
+
+ // THEN
+ Assert.assertEquals("33.3333%", colDefs.get(0).getWidth());
+ Assert.assertEquals("33.3333%", colDefs.get(1).getWidth());
+ Assert.assertEquals("33.3333%", colDefs.get(2).getWidth());
+ }
+
+ @Test
+ public void testSetColumnsWidthInPercentages__should_not_change_width_empty_definitions() {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentages(colDefs, 0);
+
+ // THEN
+ Assert.assertTrue(colDefs.getColumnDefinitions().isEmpty());
+ }
+
+ @Test
+ public void testSetColumnsWidthInPercentages__should_change_auto_width_by_percentages() throws MeasurementException {
+ // GIVEN
+ TableColumnDefinitions colDefs = new TableColumnDefinitions();
+ colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH);
+ colDefs.newColumnDef().setWidthAuto();
+
+ // WHEN
+ DocxGenerator.setColumnsWidthInPercentages(colDefs, 100);
+
+ // THEN
+ Assert.assertEquals("30%", colDefs.get(0).getWidth());
+ Assert.assertEquals("70.0%", 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);
+ }
+
+ @Test
+ public void testConvertColumnWidthToPx__should_convert_if_percentages() throws MeasurementException {
+ // GIVEN
+ TableColumnDefinition columnDefinition = new TableColumnDefinition(Mockito.mock(TableColumnDefinitions.class));
+ columnDefinition.setWidth("31.7498%", DOTS_PER_INCH);
+
+ // WHEN
+ int result = DocxGenerator.convertColumnWidthToPx(columnDefinition, 123);
+
+ // THEN
+ Assert.assertEquals(39, result);
+ }
+
+ @Test
+ public void testConvertColumnWidthToPx__should_return_as_is_if_not_percentages() throws MeasurementException {
+ // GIVEN
+ TableColumnDefinition columnDefinition = new TableColumnDefinition(Mockito.mock(TableColumnDefinitions.class));
+ columnDefinition.setWidth("31", DOTS_PER_INCH);
+
+ // WHEN
+ int result = DocxGenerator.convertColumnWidthToPx(columnDefinition, 123);
+
+ // THEN
+ Assert.assertEquals(31, result);
+ }
+
+ @Test
+ public void testAdjustMaxWidthIfIndentationExists__should_adjust_width_if_style_with_num_section_exists() {
+ // GIVEN
+ XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS);
+ XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS);
+ CTPPrGeneral pPr = Mockito.mock(CTPPrGeneral.class);
+ CTNumPr numPr = Mockito.mock(CTNumPr.class);
+ CTDecimalNumber numId = Mockito.mock(CTDecimalNumber.class);
+ Mockito.when(style.getCTStyle().getPPr()).thenReturn(pPr);
+ Mockito.when(pPr.getNumPr()).thenReturn(numPr);
+ Mockito.when(numPr.getNumId()).thenReturn(numId);
+ Mockito.when(numId.getVal()).thenReturn(BigInteger.valueOf(11));
+ Mockito.when(paragraph.getStyle()).thenReturn("SomeStyle123");
+ Mockito.when(paragraph.getDocument().getStyles().getStyle("SomeStyle123")).thenReturn(style);
+ Mockito.when(paragraph.getDocument().getNumbering().getNum(BigInteger.valueOf(11)).getCTNum().getAbstractNumId().getVal()).thenReturn(BigInteger.valueOf(82));
+ Mockito.when(paragraph.getDocument().getNumbering().getAbstractNum(BigInteger.valueOf(82)).getCTAbstractNum().getLvlArray(0).getPPr().getInd().getLeft()).thenReturn(BigInteger.valueOf(360));
+
+ // WHEN
+ int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500);
+
+ // THEN
+ Assert.assertEquals(482, result);
+ }
+
+ @Test
+ public void testAdjustMaxWidthIfIndentationExists__should_adjust_width_if_style_with_ind_section_exists() {
+ // GIVEN
+ XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS);
+ XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS);
+ CTPPrGeneral pPr = Mockito.mock(CTPPrGeneral.class);
+ CTInd ind = Mockito.mock(CTInd.class);
+ Mockito.when(style.getCTStyle().getPPr()).thenReturn(pPr);
+ Mockito.when(paragraph.getStyle()).thenReturn("SomeStyle123");
+ Mockito.when(paragraph.getDocument().getStyles().getStyle("SomeStyle123")).thenReturn(style);
+ Mockito.when(pPr.getInd()).thenReturn(ind);
+ Mockito.when(ind.getLeft()).thenReturn(BigInteger.valueOf(720));
+
+ // WHEN
+ int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500);
+
+ // THEN
+ Assert.assertEquals(464, result);
+ }
+
+ @Test
+ public void testAdjustMaxWidthIfIndentationExists__should_not_adjust_width_if_no_style() {
+ // GIVEN
+ XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS);
+
+ // WHEN
+ int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500);
+
+ // THEN
+ Assert.assertEquals(500, result);
+ }
+
// ===== INTERNAL UTILITIES
private XWPFDocument convert(String infile, String outfile) throws Exception {