diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmark.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmark.java index 9b2fe297c..0f3efadc7 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmark.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmark.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2024 Obeo. + * Copyright (c) 2017, 2025 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -12,11 +12,11 @@ package org.obeonetwork.m2doc.element; /** - * An bookmark that can be returned by services. + * A bookmark that can be returned by services. * * @author Yvan Lussaud */ -public interface MBookmark extends MElement { +public interface MBookmark extends MIdentifiable { /** * Gets the text to display. @@ -33,45 +33,4 @@ public interface MBookmark extends MElement { */ void setText(String text); - /** - * Gets the bookmark ID. - * - * @return the bookmark ID - */ - String getId(); - - /** - * Sets the bookmark ID. - * - * @param id - * the new bookmark ID - */ - void setId(String id); - - /** - * Tells if this is a reference to a {@link MBookmark} or a {@link MBookmark} declaration. - * - * @return true if this is a reference to a {@link MBookmark}, false if it's a {@link MBookmark} - * declaration - */ - boolean isReference(); - - /** - * Tells if this reference can be omitted when the bookmark declaration doesn't exists. - * - * @return true if this reference can be omitted when the bookmark declaration doesn't exists, false if an - * error - * should be raised. - */ - boolean isOptional(); - - /** - * Sets if this reference can be omitted when the bookmark declaration doesn't exists. - * - * @param optional - * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an - * error - */ - void setOptional(boolean optional); - } diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkCustomTextRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkCustomTextRef.java new file mode 100644 index 000000000..69b8ef4b3 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkCustomTextRef.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * A bookmark with a custom text reference (insert the custom text and reference the bookmark). + * + * @author Yvan Lussaud + */ +public interface MBookmarkCustomTextRef extends MBookmarkRef { + + /** + * Gets the text to display. + * + * @return the text to display + */ + String getText(); + + /** + * Sets the text to display. + * + * @param text + * the new text to display + */ + void setText(String text); + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkPageRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkPageRef.java new file mode 100644 index 000000000..9081400d0 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkPageRef.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * A bookmark page reference (insert the page number of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public interface MBookmarkPageRef extends MBookmarkRef { + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkRef.java new file mode 100644 index 000000000..422069925 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkRef.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * A bookmark reference. + * + * @author Yvan Lussaud + */ +public interface MBookmarkRef extends MIdentifiable { + + /** + * Tells if this reference can be omitted when the bookmark declaration doesn't exists. + * + * @return true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error + * should be raised. + */ + boolean isOptional(); + + /** + * Sets if this reference can be omitted when the bookmark declaration doesn't exists. + * + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error + */ + void setOptional(boolean optional); + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkSectionRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkSectionRef.java new file mode 100644 index 000000000..2172c0e57 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkSectionRef.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * A bookmark section reference (insert the section name of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public interface MBookmarkSectionRef extends MBookmarkRef { + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkTextRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkTextRef.java new file mode 100644 index 000000000..199794ee2 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MBookmarkTextRef.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * A bookmark text reference (insert the text of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public interface MBookmarkTextRef extends MBookmarkRef { + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MIdentifiable.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MIdentifiable.java new file mode 100644 index 000000000..a4f1a6b08 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/MIdentifiable.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element; + +/** + * Identifiable {@link MElement}. + * + * @author Yvan Lussaud + */ +public interface MIdentifiable extends MElement { + + /** + * Gets the ID. + * + * @return the ID + */ + String getId(); + + /** + * Sets the ID. + * + * @param id + * the new ID + */ + void setId(String id); + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/AbstractMBookmarkRef.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/AbstractMBookmarkRef.java new file mode 100644 index 000000000..cb835712d --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/AbstractMBookmarkRef.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element.impl; + +import org.obeonetwork.m2doc.element.MBookmarkRef; + +/** + * Abstract implementation of {@link MBookmarkRef}. + * + * @author Yvan Lussaud + */ +public class AbstractMBookmarkRef implements MBookmarkRef { + + /** + * The bookmark ID. + */ + private String id; + + /** + * Tells if this reference can be omitted when the bookmark declaration doesn't exists. + */ + private boolean optional; + + /** + * Constructor. + * + * @param id + * the bookmark id + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false + * otherwise + */ + public AbstractMBookmarkRef(String id, boolean optional) { + this.id = id; + this.optional = optional; + } + + @Override + public String getId() { + return id; + } + + @Override + public void setId(String id) { + this.id = id; + } + + @Override + public boolean isOptional() { + return optional; + } + + @Override + public void setOptional(boolean optional) { + this.optional = optional; + } +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkCustomTextRefImpl.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkCustomTextRefImpl.java new file mode 100644 index 000000000..58c2cc12e --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkCustomTextRefImpl.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element.impl; + +import org.obeonetwork.m2doc.element.MBookmarkCustomTextRef; + +/** + * A bookmark text reference (insert the text of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public class MBookmarkCustomTextRefImpl extends AbstractMBookmarkRef implements MBookmarkCustomTextRef { + + /** + * The text to display. + */ + private String text; + + /** + * Constructor. + * + * @param text + * the custom text + * @param id + * the bookmark id + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false + * otherwise + */ + public MBookmarkCustomTextRefImpl(String text, String id, boolean optional) { + super(id, optional); + this.text = text; + } + + @Override + public String getText() { + return text; + } + + @Override + public void setText(String text) { + this.text = text; + } + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkImpl.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkImpl.java index 361ce8286..7059c9ad8 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkImpl.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2024 Obeo. + * Copyright (c) 2017, 2025 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -30,32 +30,17 @@ public class MBookmarkImpl implements MBookmark { */ private String id; - /** - * true if this is a reference to a {@link MBookmarkImpl}, false if it's a {@link MBookmarkImpl} declaration. - */ - private final boolean reference; - - /** - * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an error - * should be raised. - */ - private boolean optional; - /** * Constructor. * * @param text * the text to display * @param id - * the id - * @param reference - * true if this is a reference to a {@link MBookmarkImpl}, false if it's a {@link MBookmarkImpl} - * declaration + * the bookmark id */ - public MBookmarkImpl(String text, String id, boolean reference) { + public MBookmarkImpl(String text, String id) { this.text = text; this.id = id; - this.reference = reference; } @Override @@ -78,19 +63,4 @@ public void setId(String id) { this.id = id; } - @Override - public boolean isReference() { - return reference; - } - - @Override - public boolean isOptional() { - return optional; - } - - @Override - public void setOptional(boolean optional) { - this.optional = optional; - } - } diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkPageRefImpl.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkPageRefImpl.java new file mode 100644 index 000000000..6670a3964 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkPageRefImpl.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element.impl; + +import org.obeonetwork.m2doc.element.MBookmarkPageRef; + +/** + * A bookmark page reference (insert the page number of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public class MBookmarkPageRefImpl extends AbstractMBookmarkRef implements MBookmarkPageRef { + + /** + * Constructor. + * + * @param id + * the bookmark id + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false + * otherwise + */ + public MBookmarkPageRefImpl(String id, boolean optional) { + super(id, optional); + } + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkSectionRefImpl.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkSectionRefImpl.java new file mode 100644 index 000000000..8538d4d97 --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkSectionRefImpl.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element.impl; + +import org.obeonetwork.m2doc.element.MBookmarkSectionRef; + +/** + * A bookmark section reference (insert the section name of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public class MBookmarkSectionRefImpl extends AbstractMBookmarkRef implements MBookmarkSectionRef { + + /** + * Constructor. + * + * @param id + * the bookmark id + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false + * otherwise + */ + public MBookmarkSectionRefImpl(String id, boolean optional) { + super(id, optional); + } + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkTextRefImpl.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkTextRefImpl.java new file mode 100644 index 000000000..a37596bdd --- /dev/null +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/element/impl/MBookmarkTextRefImpl.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Obeo. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Obeo - initial API and implementation + * + *******************************************************************************/ +package org.obeonetwork.m2doc.element.impl; + +import org.obeonetwork.m2doc.element.MBookmarkTextRef; + +/** + * A bookmark text reference (insert the text of the referenced bookmark). + * + * @author Yvan Lussaud + */ +public class MBookmarkTextRefImpl extends AbstractMBookmarkRef implements MBookmarkTextRef { + + /** + * Constructor. + * + * @param id + * the bookmark id + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false + * otherwise + */ + public MBookmarkTextRefImpl(String id, boolean optional) { + super(id, optional); + } + +} diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/BookmarkManager.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/BookmarkManager.java index fc6b8b611..72ea16e0c 100755 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/BookmarkManager.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/BookmarkManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2024 Obeo. + * Copyright (c) 2016, 2025 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -16,6 +16,8 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -25,14 +27,19 @@ import org.apache.poi.xwpf.usermodel.IRunBody; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; +import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.xb.xmlschema.SpaceAttribute.Space; import org.obeonetwork.m2doc.parser.ValidationMessageLevel; import org.obeonetwork.m2doc.util.M2DocUtils; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTMarkupRange; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType; +import org.w3c.dom.Node; /** * Manage bookmarks. @@ -41,6 +48,10 @@ */ public class BookmarkManager { + /** WordprocessingML namespace. */ + private static final String WORDPROCESSINGML_NAMESPACE = + "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + /** * The buffer size. */ @@ -51,6 +62,16 @@ public class BookmarkManager { */ private static final String REF_TAG = " REF %s \\h "; + /** + * The reference section. + */ + private static final String SECTION_REF_TAG = " REF %s \\n \\h "; + + /** + * The reference page. + */ + private static final String PAGE_REF_TAG = " PAGEREF %s \\h "; + /** * Known bookmarks so far. */ @@ -86,6 +107,386 @@ public class BookmarkManager { */ private final Map messagePositions = new HashMap<>(); + /** + * Native Word bookmark IDs remapped while reconstructed content is generated. + */ + private final Map nativeBookmarkIDs = new HashMap<>(); + + /** + * Copies native Word bookmark markers located before the given source run. + * Only markers between the beginning of the top-level paragraph child and + * the previous content-bearing sibling are copied. + * + * @param sourceRun + * the source run + * @param outputParagraph + * the generated paragraph + */ + public void copyNativeBookmarksBefore(XWPFRun sourceRun, XWPFParagraph outputParagraph) { + copyNativeBookmarkMarkers(sourceRun, outputParagraph, false); + } + + /** + * Copies native Word bookmark markers located after the given source run. + * Only markers up to the next content-bearing sibling are copied. + * + * @param sourceRun + * the source run + * @param outputParagraph + * the generated paragraph + */ + public void copyNativeBookmarksAfter(XWPFRun sourceRun, XWPFParagraph outputParagraph) { + copyNativeBookmarkMarkers(sourceRun, outputParagraph, true); + } + + /** + * Copies native bookmark markers from a paragraph that contains no runs or + * other content-bearing elements. Such paragraphs are used by Word as the + * starting point of bookmarks spanning a table row. + * + * @param sourceParagraph + * the source paragraph + * @param outputParagraph + * the generated paragraph + */ + public void copyNativeBookmarksFromEmptyParagraph(XWPFParagraph sourceParagraph, + XWPFParagraph outputParagraph) { + final CTP sourceCTP = sourceParagraph.getCTP(); + final Node paragraphNode = sourceCTP.getDomNode(); + + for (Node node = paragraphNode.getFirstChild(); node != null; node = node.getNextSibling()) { + if (isContentBearingElement(node)) { + return; + } + } + + for (Node node = paragraphNode.getFirstChild(); node != null; node = node.getNextSibling()) { + if (isBookmarkMarker(node)) { + copyNativeBookmarkMarker(sourceCTP, node, outputParagraph); + } + } + } + + /** + * Copies bookmark markers occurring before the first cell of a table row. + */ + public void copyNativeBookmarksBeforeRow(CTRow sourceRow, CTRow outputRow) { + final Node rowNode = sourceRow.getDomNode(); + for (Node node = rowNode.getFirstChild(); node != null; node = node.getNextSibling()) { + if (node.getNodeType() == Node.ELEMENT_NODE && "tc".equals(node.getLocalName())) { + break; + } + copyNativeRowBookmarkMarker(sourceRow, node, outputRow); + } + } + + /** + * Copies bookmark markers occurring after the last cell of a table row. + */ + public void copyNativeBookmarksAfterRow(CTRow sourceRow, CTRow outputRow) { + final Node rowNode = sourceRow.getDomNode(); + final List markers = new ArrayList<>(); + for (Node node = rowNode.getLastChild(); node != null; node = node.getPreviousSibling()) { + if (node.getNodeType() == Node.ELEMENT_NODE && "tc".equals(node.getLocalName())) { + break; + } + if (isBookmarkMarker(node)) { + markers.add(0, node); + } + } + for (Node marker : markers) { + copyNativeRowBookmarkMarker(sourceRow, marker, outputRow); + } + } + + /** + * Copies native bookmark markers located after the cell at the given + * logical index and before the following cell. Word can place a + * bookmarkEnd directly under w:tr between two w:tc elements. + * + * @param sourceRow + * the source row + * @param outputRow + * the generated row + * @param cellIndex + * zero-based index of the source cell just generated + */ + public void copyNativeBookmarksAfterCell(CTRow sourceRow, CTRow outputRow, int cellIndex) { + final Node rowNode = sourceRow.getDomNode(); + int currentCellIndex = -1; + Node node = rowNode.getFirstChild(); + + while (node != null) { + if (isWordElement(node, "tc")) { + currentCellIndex++; + if (currentCellIndex == cellIndex) { + node = node.getNextSibling(); + break; + } + } + node = node.getNextSibling(); + } + + while (node != null && !isWordElement(node, "tc")) { + if (isBookmarkMarker(node)) { + copyNativeRowBookmarkMarker(sourceRow, node, outputRow); + } + node = node.getNextSibling(); + } + } + + /** + * Copies bookmark starts located in a row cell when the matching bookmark + * end is a direct child of the table row. Word uses this structure for + * cross-references to complete rows in reference tables. + * + * @param sourceRow + * the source row + * @param outputRow + * the generated row + */ + public void copyNativeBookmarksSpanningRow(XWPFTableRow sourceRow, XWPFTableRow outputRow) { + final Set rowEndIDs = new LinkedHashSet<>(); + final Node sourceRowNode = sourceRow.getCtRow().getDomNode(); + for (Node node = sourceRowNode.getFirstChild(); node != null; node = node.getNextSibling()) { + if (isWordElement(node, "bookmarkEnd")) { + final BigInteger id = getWordBigIntegerAttribute(node, "id"); + if (id != null) { + rowEndIDs.add(id); + } + } + } + + final List sourceCells = sourceRow.getCtRow().getTcList(); + final List outputCells = outputRow.getCtRow().getTcList(); + final int cellCount = Math.min(sourceCells.size(), outputCells.size()); + for (int cellIndex = 0; cellIndex < cellCount; cellIndex++) { + final List sourceParagraphs = sourceCells.get(cellIndex).getPList(); + final List outputParagraphs = outputCells.get(cellIndex).getPList(); + final int paragraphCount = Math.min(sourceParagraphs.size(), outputParagraphs.size()); + for (int paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) { + final Node paragraphNode = sourceParagraphs.get(paragraphIndex).getDomNode(); + for (Node node = paragraphNode.getFirstChild(); node != null; node = node.getNextSibling()) { + if (isWordElement(node, "bookmarkStart")) { + final BigInteger id = getWordBigIntegerAttribute(node, "id"); + final String name = getWordAttribute(node, "name"); + if (id != null && name != null && rowEndIDs.contains(id) + && !containsBookmark(outputRow.getCtRow(), name)) { + copyNativeBookmarkStart(name, id, outputParagraphs.get(paragraphIndex)); + } + } + } + } + } + } + + private boolean containsBookmark(CTRow row, String name) { + for (CTTc cell : row.getTcList()) { + for (CTP paragraph : cell.getPList()) { + for (CTBookmark bookmark : paragraph.getBookmarkStartList()) { + if (name.equals(bookmark.getName())) { + return true; + } + } + } + } + return false; + } + + private void copyNativeRowBookmarkMarker(CTRow sourceRow, Node marker, CTRow outputRow) { + final BigInteger sourceID = getWordBigIntegerAttribute(marker, "id"); + if (sourceID == null) { + return; + } + if (isWordElement(marker, "bookmarkStart")) { + final String name = getWordAttribute(marker, "name"); + if (name != null) { + final CTBookmark target = outputRow.addNewBookmarkStart(); + final BigInteger newID = getRandomID(); + target.setName(name); + target.setId(newID); + nativeBookmarkIDs.put(sourceID, newID); + } + } else if (isWordElement(marker, "bookmarkEnd")) { + final CTMarkupRange target = outputRow.addNewBookmarkEnd(); + final BigInteger newID = nativeBookmarkIDs.remove(sourceID); + if (newID != null) { + target.setId(newID); + } else { + target.setId(sourceID); + } + } + } + + private void copyNativeBookmarkStart(String name, BigInteger sourceID, CTP outputParagraph) { + final CTBookmark target = outputParagraph.addNewBookmarkStart(); + final BigInteger newID = getRandomID(); + target.setName(name); + target.setId(newID); + nativeBookmarkIDs.put(sourceID, newID); + } + + private boolean isWordElement(Node node, String localName) { + return node != null && node.getNodeType() == Node.ELEMENT_NODE + && localName.equals(node.getLocalName()); + } + + private String getWordAttribute(Node node, String localName) { + final Node attribute = node.getAttributes() != null + ? node.getAttributes().getNamedItemNS(WORDPROCESSINGML_NAMESPACE, localName) + : null; + return attribute != null ? attribute.getNodeValue() : null; + } + + private BigInteger getWordBigIntegerAttribute(Node node, String localName) { + final String value = getWordAttribute(node, localName); + try { + return value != null ? new BigInteger(value) : null; + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Copies the native bookmark markers adjacent to a source run while + * preserving their order in the paragraph. + * + * @param sourceRun + * the source run + * @param outputParagraph + * the output paragraph + * @param after + * {@code true} to inspect following siblings, {@code false} to + * inspect preceding siblings + */ + private void copyNativeBookmarkMarkers(XWPFRun sourceRun, XWPFParagraph outputParagraph, boolean after) { + final XWPFParagraph sourceParagraph = (XWPFParagraph) sourceRun.getParent(); + final CTP sourceCTP = sourceParagraph.getCTP(); + final Node paragraphNode = sourceCTP.getDomNode(); + final Node anchor = getParagraphChild(sourceRun.getCTR().getDomNode(), paragraphNode); + + if (anchor == null) { + return; + } + + Node node = after ? anchor.getNextSibling() : anchor.getPreviousSibling(); + final List markers = new ArrayList<>(); + + while (node != null && !isContentBearingElement(node)) { + if (isBookmarkMarker(node)) { + if (after) { + markers.add(node); + } else { + markers.add(0, node); + } + } + node = after ? node.getNextSibling() : node.getPreviousSibling(); + } + + // Markers between two content elements are owned by the element on + // their left. This prevents copying them both after one run and before + // the following run, while still allowing the same template block to + // be generated more than once by a repetition. + if (!after && node != null) { + return; + } + + for (Node marker : markers) { + copyNativeBookmarkMarker(sourceCTP, marker, outputParagraph); + } + } + + /** + * Gets the direct child of the paragraph containing the given node. + */ + private Node getParagraphChild(Node node, Node paragraphNode) { + Node current = node; + while (current != null) { + final Node parent = current.getParentNode(); + if (parent != null && parent.isSameNode(paragraphNode)) { + return current; + } + current = parent; + } + return null; + } + + /** + * Tells whether a node is a native Word bookmark marker. + */ + private boolean isBookmarkMarker(Node node) { + return node.getNodeType() == Node.ELEMENT_NODE + && ("bookmarkStart".equals(node.getLocalName()) || "bookmarkEnd".equals(node.getLocalName())); + } + + /** + * Tells whether a paragraph child carries generated or copied content. + */ + private boolean isContentBearingElement(Node node) { + if (node.getNodeType() != Node.ELEMENT_NODE) { + return false; + } + final String localName = node.getLocalName(); + return "r".equals(localName) || "hyperlink".equals(localName) || "fldSimple".equals(localName) + || "sdt".equals(localName) || "smartTag".equals(localName) || "customXml".equals(localName); + } + + /** + * Copies one native bookmark marker. + */ + private void copyNativeBookmarkMarker(CTP sourceParagraph, Node marker, XWPFParagraph outputParagraph) { + if ("bookmarkStart".equals(marker.getLocalName())) { + final CTBookmark source = findBookmarkStart(sourceParagraph, marker); + if (source != null) { + copyNativeBookmarkStart(source, outputParagraph); + } + } else if ("bookmarkEnd".equals(marker.getLocalName())) { + final CTMarkupRange source = findBookmarkEnd(sourceParagraph, marker); + if (source != null) { + copyNativeBookmarkEnd(source, outputParagraph); + } + } + } + + private CTBookmark findBookmarkStart(CTP paragraph, Node marker) { + for (CTBookmark bookmark : paragraph.getBookmarkStartList()) { + if (bookmark.getDomNode().isSameNode(marker)) { + return bookmark; + } + } + return null; + } + + private CTMarkupRange findBookmarkEnd(CTP paragraph, Node marker) { + for (CTMarkupRange range : paragraph.getBookmarkEndList()) { + if (range.getDomNode().isSameNode(marker)) { + return range; + } + } + return null; + } + + private void copyNativeBookmarkStart(CTBookmark source, XWPFParagraph outputParagraph) { + copyNativeBookmarkStart(source, outputParagraph.getCTP()); + } + + private void copyNativeBookmarkStart(CTBookmark source, CTP outputParagraph) { + final CTBookmark target = outputParagraph.addNewBookmarkStart(); + target.set(source.copy()); + final BigInteger newID = getRandomID(); + target.setId(newID); + nativeBookmarkIDs.put(source.getId(), newID); + } + + private void copyNativeBookmarkEnd(CTMarkupRange source, XWPFParagraph outputParagraph) { + final CTMarkupRange target = outputParagraph.getCTP().addNewBookmarkEnd(); + target.set(source.copy()); + final BigInteger newID = nativeBookmarkIDs.remove(source.getId()); + if (newID != null) { + target.setId(newID); + } + } + /** * Starts a bookmark in the given {@link XWPFParagraph} with the given name. * @@ -122,7 +523,6 @@ public void startBookmark(GenerationResult result, XWPFParagraph paragraph, Stri } else { throw new IllegalStateException("this should not happend"); } - pendingRef.setStringValue(String.format(REF_TAG, bookmark.getName())); } } } @@ -175,6 +575,68 @@ private BigInteger getRandomID() { return new BigInteger(buffer.array()).abs(); } + /** + * Inserts a reference with the bookmark page to the given name in the given {@link XWPFParagraph}. + * + * @param paragraph + * the {@link XWPFParagraph} + * @param name + * the bookmark name + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error should be inserted + */ + public void insertPageReference(XWPFParagraph paragraph, String name, boolean optional) { + insertReference(paragraph, name, null, PAGE_REF_TAG, optional); + } + + /** + * Inserts a reference with the bookmark section to the given name in the given {@link XWPFParagraph}. + * + * @param paragraph + * the {@link XWPFParagraph} + * @param name + * the bookmark name + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error should be inserted + */ + public void insertSectionReference(XWPFParagraph paragraph, String name, boolean optional) { + insertReference(paragraph, name, null, SECTION_REF_TAG, optional); + } + + /** + * Inserts a reference with the bookmark text to the given name in the given {@link XWPFParagraph}. + * + * @param paragraph + * the {@link XWPFParagraph} + * @param name + * the bookmark name + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error should be inserted + */ + public void insertTextReference(XWPFParagraph paragraph, String name, boolean optional) { + insertReference(paragraph, name, null, REF_TAG, optional); + } + + /** + * Inserts a reference with a custom text to the given name in the given {@link XWPFParagraph}. + * + * @param paragraph + * the {@link XWPFParagraph} + * @param name + * the bookmark name + * @param text + * the text + * @param optional + * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an + * error should be inserted + */ + public void insertCustomTextReference(XWPFParagraph paragraph, String name, String text, boolean optional) { + insertReference(paragraph, name, text, REF_TAG, optional); + } + /** * Inserts a pending reference to the given name in the given {@link XWPFParagraph}. * @@ -184,21 +646,23 @@ private BigInteger getRandomID() { * the bookmark name * @param text * the text + * @param format + * the instruction text format * @param optional * true if this reference can be omitted when the bookmark declaration doesn't exists, false if an * error */ - public void insertReference(XWPFParagraph paragraph, String name, String text, boolean optional) { + public void insertReference(XWPFParagraph paragraph, String name, String text, String format, boolean optional) { final CTBookmark bookmark = bookmarks.get(name); if (bookmark != null) { - insertReference(paragraph, bookmark, text); + insertReference(paragraph, bookmark, text, format); } else { final XWPFRun messageRun = paragraph.createRun(); final CTText ref = insertPendingReference(paragraph, name, text); if (optional) { optionalReferences.put(ref, text); } - ref.setStringValue(String.format(REF_TAG, name)); + ref.setStringValue(String.format(format, name)); messagePositions.put(ref, messageRun); Set pendingRefs = pendingReferences.get(name); if (pendingRefs == null) { @@ -219,11 +683,13 @@ public void insertReference(XWPFParagraph paragraph, String name, String text, b * the {@link CTBookmark} * @param text * the text + * @param format + * the instruction text format */ - private void insertReference(XWPFParagraph paragraph, CTBookmark bookmark, String text) { + private void insertReference(XWPFParagraph paragraph, CTBookmark bookmark, String text, String format) { final String name = bookmark.getName(); final CTText pendingCTText = insertPendingReference(paragraph, name, text); - pendingCTText.setStringValue(String.format(REF_TAG, name)); + pendingCTText.setStringValue(String.format(format, name)); } /** @@ -320,10 +786,14 @@ public boolean markDanglingReferences(GenerationResult result) { if (res) { for (Entry> entry : pendingReferences.entrySet()) { for (CTText ref : entry.getValue()) { - final String text = optionalReferences.remove(ref); - if (text != null) { + if (optionalReferences.containsKey(ref)) { + final String text = optionalReferences.remove(ref); final XWPFRun refRun = messagePositions.remove(ref); - refRun.setText(text); + if (text != null) { + refRun.setText(text); + } else { + refRun.getCTR().setInstrTextArray(null); + } final XWPFParagraph paragraph = (XWPFParagraph) refRun.getParent(); final int refRunIndex = paragraph.getRuns().indexOf(refRun); paragraph.removeRun(refRunIndex - 1); @@ -398,6 +868,7 @@ public void reset() { xmlObjectToName.clear(); referenceIDs.clear(); messagePositions.clear(); + nativeBookmarkIDs.clear(); } } diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/M2DocEvaluator.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/M2DocEvaluator.java index d23cfa250..6510581c3 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/M2DocEvaluator.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/M2DocEvaluator.java @@ -27,6 +27,7 @@ import java.util.concurrent.CancellationException; import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart; +import org.apache.poi.ooxml.POIXMLProperties.CustomProperties; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.BreakType; @@ -65,6 +66,10 @@ import org.eclipse.emf.ecore.EObject; import org.obeonetwork.m2doc.POIServices; import org.obeonetwork.m2doc.element.MBookmark; +import org.obeonetwork.m2doc.element.MBookmarkCustomTextRef; +import org.obeonetwork.m2doc.element.MBookmarkPageRef; +import org.obeonetwork.m2doc.element.MBookmarkSectionRef; +import org.obeonetwork.m2doc.element.MBookmarkTextRef; import org.obeonetwork.m2doc.element.MBorder; import org.obeonetwork.m2doc.element.MElement; import org.obeonetwork.m2doc.element.MElementContainer.HAlignment; @@ -109,6 +114,8 @@ import org.obeonetwork.m2doc.util.FieldUtils; import org.obeonetwork.m2doc.util.M2DocUtils; import org.obeonetwork.m2doc.util.SequenceField; +import org.openxmlformats.schemas.officeDocument.x2006.customProperties.CTProperties; +import org.openxmlformats.schemas.officeDocument.x2006.customProperties.CTProperty; import org.openxmlformats.schemas.officeDocument.x2006.sharedTypes.STOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHMerge; @@ -365,9 +372,38 @@ public XWPFParagraph caseDocumentTemplate(DocumentTemplate documentTemplate) { worked(monitor, unitOfWork); } + removeM2DocCustomProperties(document); + return currentGeneratedParagraph; } + /** + * Removes M2Doc-internal custom document properties from the generated + * document while preserving all user-defined and third-party properties. + * + *

M2Doc stores template metadata such as {@code m:M2DocVersion} and + * {@code m:uri:...} in {@code docProps/custom.xml}. These properties are + * useful while processing the template, but they must not leak into the + * generated deliverable.

+ * + * @param document + * the generated document + */ + private void removeM2DocCustomProperties(XWPFDocument document) { + final CustomProperties customProperties = document.getProperties().getCustomProperties(); + final CTProperties properties = customProperties.getUnderlyingProperties(); + + // Iterate backwards because XMLBeans exposes the properties as an + // indexed live array. Removing an item shifts every following item. + for (int i = properties.sizeOfPropertyArray() - 1; i >= 0; i--) { + final CTProperty property = properties.getPropertyArray(i); + final String name = property.getName(); + if (name != null && name.startsWith("m:")) { + properties.removeProperty(i); + } + } + } + /** * Updates sequences (SEQ) numbers in the given {@link XWPFDocument}. * @@ -476,6 +512,8 @@ private XWPFRun insertRun(XWPFParagraph paragraph, XWPFRun srcRun) { newParagraph = paragraph; } + bookmarkManager.copyNativeBookmarksBefore(srcRun, newParagraph); + if (srcRun instanceof XWPFHyperlinkRun) { // Hyperlinks meta information is saved in the paragraph and not in the run. So we have to update the paragrapah with a copy of // the hyperlink to insert. @@ -493,6 +531,8 @@ private XWPFRun insertRun(XWPFParagraph paragraph, XWPFRun srcRun) { newRun.getCTR().set(srcRun.getCTR()); } + bookmarkManager.copyNativeBookmarksAfter(srcRun, newParagraph); + return newRun; } @@ -515,7 +555,11 @@ private XWPFRun insertFieldRunReplacement(XWPFParagraph paragraph, XWPFRun srcRu newParagraph = paragraph; } - return insertString(newParagraph, srcRun, replacement); + bookmarkManager.copyNativeBookmarksBefore(srcRun, newParagraph); + final XWPFRun res = insertString(newParagraph, srcRun, replacement); + bookmarkManager.copyNativeBookmarksAfter(srcRun, newParagraph); + + return res; } /** @@ -603,6 +647,11 @@ private XWPFParagraph createNewParagraph(IBody body, XWPFParagraph srcParagraph) ctp.getRList().clear(); ctp.getFldSimpleList().clear(); ctp.getHyperlinkList().clear(); + // Bookmark start/end elements are paragraph-level siblings of runs. + // Keeping them while rebuilding the runs places both boundaries before + // the generated text and turns the bookmark into an empty range. + ctp.getBookmarkStartList().clear(); + ctp.getBookmarkEndList().clear(); res.getCTP().set(ctp); int runNb = res.getRuns().size(); for (int i = 0; i < runNb; i++) { @@ -612,6 +661,10 @@ private XWPFParagraph createNewParagraph(IBody body, XWPFParagraph srcParagraph) currentGeneratedParagraph = res; forceNewParagraph = false; + // Empty paragraphs have no run on which the normal before/after + // bookmark reconstruction can be triggered. + bookmarkManager.copyNativeBookmarksFromEmptyParagraph(srcParagraph, res); + return res; } @@ -704,6 +757,14 @@ private XWPFParagraph insertObject(XWPFParagraph paragraph, Object object, XWPFR res = insertMHyperLink(paragraph, run, (MHyperLink) object); } else if (object instanceof MBookmark) { res = insertMBookmark(paragraph, run, (MBookmark) object); + } else if (object instanceof MBookmarkPageRef) { + res = insertMBookmarkPageRef(paragraph, run, (MBookmarkPageRef) object); + } else if (object instanceof MBookmarkSectionRef) { + res = insertMBookmarkSectionRef(paragraph, run, (MBookmarkSectionRef) object); + } else if (object instanceof MBookmarkTextRef) { + res = insertMBookmarkTextRef(paragraph, run, (MBookmarkTextRef) object); + } else if (object instanceof MBookmarkCustomTextRef) { + res = insertMBookmarkCustomTextRef(paragraph, run, (MBookmarkCustomTextRef) object); } else if (object instanceof MImage) { if (object != MImage.EMPTY) { final XWPFRun imageRun = insertFieldRunReplacement(paragraph, run, ""); @@ -860,13 +921,87 @@ public XWPFParagraph caseComment(Comment comment) { private XWPFParagraph insertMBookmark(XWPFParagraph paragraph, XWPFRun run, MBookmark bookmark) { final XWPFParagraph res = (XWPFParagraph) insertFieldRunReplacement(paragraph, run, "").getParent(); - if (bookmark.isReference()) { - bookmarkManager.insertReference(res, bookmark.getId(), bookmark.getText(), bookmark.isOptional()); - } else { - bookmarkManager.startBookmark(result, res, bookmark.getId()); - insertFieldRunReplacement(res, run, bookmark.getText()); - bookmarkManager.endBookmark(result, res, bookmark.getId()); - } + bookmarkManager.startBookmark(result, res, bookmark.getId()); + insertFieldRunReplacement(res, run, bookmark.getText()); + bookmarkManager.endBookmark(result, res, bookmark.getId()); + + return res; + } + + /** + * Inserts a {@link MBookmarkPageRef} in the given {@link XWPFRun}. + * + * @param paragraph + * the {@link XWPFParagraph} to modify + * @param run + * the {@link XWPFRun} + * @param reference + * the {@link MBookmarkPageRef} + * @return the {@link XWPFParagraph} where the bookmark page reference was inserted + */ + private XWPFParagraph insertMBookmarkPageRef(XWPFParagraph paragraph, XWPFRun run, MBookmarkPageRef reference) { + final XWPFParagraph res = (XWPFParagraph) insertFieldRunReplacement(paragraph, run, "").getParent(); + + bookmarkManager.insertPageReference(res, reference.getId(), reference.isOptional()); + + return res; + } + + /** + * Inserts a {@link MBookmarkSectionRef} in the given {@link XWPFRun}. + * + * @param paragraph + * the {@link XWPFParagraph} to modify + * @param run + * the {@link XWPFRun} + * @param reference + * the {@link MBookmarkSectionRef} + * @return the {@link XWPFParagraph} where the bookmark section reference was inserted + */ + private XWPFParagraph insertMBookmarkSectionRef(XWPFParagraph paragraph, XWPFRun run, + MBookmarkSectionRef reference) { + final XWPFParagraph res = (XWPFParagraph) insertFieldRunReplacement(paragraph, run, "").getParent(); + + bookmarkManager.insertSectionReference(res, reference.getId(), reference.isOptional()); + + return res; + } + + /** + * Inserts a {@link MBookmarkTextRef} in the given {@link XWPFRun}. + * + * @param paragraph + * the {@link XWPFParagraph} to modify + * @param run + * the {@link XWPFRun} + * @param reference + * the {@link MBookmarkTextRef} + * @return the {@link XWPFParagraph} where the bookmark section reference was inserted + */ + private XWPFParagraph insertMBookmarkTextRef(XWPFParagraph paragraph, XWPFRun run, MBookmarkTextRef reference) { + final XWPFParagraph res = (XWPFParagraph) insertFieldRunReplacement(paragraph, run, "").getParent(); + + bookmarkManager.insertTextReference(res, reference.getId(), reference.isOptional()); + + return res; + } + + /** + * Inserts a {@link MBookmarkCustomTextRef} in the given {@link XWPFRun}. + * + * @param paragraph + * the {@link XWPFParagraph} to modify + * @param run + * the {@link XWPFRun} + * @param reference + * the {@link MBookmarkCustomTextRef} + * @return the {@link XWPFParagraph} where the bookmark section reference was inserted + */ + private XWPFParagraph insertMBookmarkCustomTextRef(XWPFParagraph paragraph, XWPFRun run, + MBookmarkCustomTextRef reference) { + final XWPFParagraph res = (XWPFParagraph) insertFieldRunReplacement(paragraph, run, "").getParent(); + + bookmarkManager.insertCustomTextReference(res, reference.getId(), reference.getText(), reference.isOptional()); return res; } @@ -983,7 +1118,7 @@ private int getAbsoluteImageWidth(XWPFParagraph paragraph, MImage image) { final CTSectPr section = paragraph.getCTP().getPPr().getSectPr(); res = getAbsoluteWidthFromSection(image, section); } else { - // no section attached to the paragraph try the containing body + // no section attached to the paragraph try the containing boby final IBody body = paragraph.getBody(); if (body instanceof XWPFDocument) { final CTSectPr section = ((XWPFDocument) body).getDocument().getBody().getSectPr(); @@ -993,20 +1128,8 @@ private int getAbsoluteImageWidth(XWPFParagraph paragraph, MImage image) { .getSectPr(); res = getAbsoluteWidthFromSection(image, section); } else if (body instanceof XWPFTableCell) { - final XWPFTableCell cell = (XWPFTableCell) body; - final int cellWidth; - // avoid an NPE from POI - if (cell.getCTTc() != null && cell.getCTTc().getTcPr() != null - && cell.getCTTc().getTcPr().getTcW() != null && cell.getCTTc().getTcPr().getTcW().getW() != null) { - cellWidth = cell.getWidth(); - } else { - cellWidth = 0; - } - if (cellWidth > 0) { - res = (int) (cellWidth * image.getRelativeWidth() / 100.0d / UNIT_CONVERSION); - } else { - res = image.getWidth(); - } + final int cellWidth = ((XWPFTableCell) body).getWidth(); + res = (int) (cellWidth * image.getRelativeWidth() / 100.0d / UNIT_CONVERSION); } else { throw new UnsupportedOperationException("unknown type of IBody : " + body.getClass()); } @@ -1060,7 +1183,7 @@ private int getAbsoluteImageHeight(XWPFParagraph paragraph, MImage image) { final CTSectPr section = paragraph.getCTP().getPPr().getSectPr(); res = getAbsoluteHeightFromSection(image, section); } else { - // no section attached to the paragraph try the containing body + // no section attached to the paragraph try the containing boby final IBody body = paragraph.getBody(); if (body instanceof XWPFDocument) { final CTSectPr section = ((XWPFDocument) body).getDocument().getBody().getSectPr(); @@ -1070,12 +1193,8 @@ private int getAbsoluteImageHeight(XWPFParagraph paragraph, MImage image) { .getSectPr(); res = getAbsoluteHeightFromSection(image, section); } else if (body instanceof XWPFTableCell) { - final int rowHeight = ((XWPFTableCell) body).getTableRow().getHeight(); - if (rowHeight > 0) { - res = (int) (rowHeight * image.getRelativeHeight() / 100.0d / UNIT_CONVERSION); - } else { - res = image.getHeight(); - } + // TODO technically the cell height depend on its content + res = image.getHeight(); } else { throw new UnsupportedOperationException("unknown type of IBody : " + body.getClass()); } @@ -2361,14 +2480,24 @@ public XWPFParagraph caseRow(Row row) { currentGeneratedRow = new XWPFTableRow(currentGeneratedTable.getCTTbl().addNewTr(), currentGeneratedTable); final CTRow ctRow = (CTRow) row.getTableRow().getCtRow().copy(); ctRow.getTcList().clear(); + ctRow.getBookmarkStartList().clear(); + ctRow.getBookmarkEndList().clear(); while (!currentGeneratedRow.getTableCells().isEmpty()) { currentGeneratedRow.removeCell(0); } currentGeneratedRow.getCtRow().set(ctRow); - // iterate on cells. + bookmarkManager.copyNativeBookmarksBeforeRow(row.getTableRow().getCtRow(), + currentGeneratedRow.getCtRow()); + // Iterate on cells and preserve row-level markup at its exact + // position between the corresponding w:tc elements. + int cellIndex = 0; for (Cell cell : row.getCells()) { doSwitch(cell); + bookmarkManager.copyNativeBookmarksSpanningRow(row.getTableRow(), currentGeneratedRow); + bookmarkManager.copyNativeBookmarksAfterCell(row.getTableRow().getCtRow(), + currentGeneratedRow.getCtRow(), cellIndex); + cellIndex++; } } finally { currentGeneratedRow = savedRow; @@ -2544,7 +2673,7 @@ public XWPFParagraph caseLink(Link link) { currentParagraph = insertQueryEvaluationMessages(currentParagraph, link, textResult.getDiagnostic()); } else { - bookmarkManager.insertReference(currentParagraph, nameResult.getResult().toString(), + bookmarkManager.insertCustomTextReference(currentParagraph, nameResult.getResult().toString(), textResult.getResult().toString(), false); } } diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/RawCopier.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/RawCopier.java index 8b4d4da56..230bd3a3b 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/RawCopier.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/generator/RawCopier.java @@ -985,24 +985,30 @@ private void updateBookmarks(BookmarkManager bookmarkManager, final XWPFTable ou final XWPFTable inputTable) { final List inputRows = inputTable.getRows(); final List outputRows = outputTable.getRows(); - if (inputRows.size() == outputRows.size()) { - for (int rowIndex = 0; rowIndex < inputRows.size(); rowIndex++) { - final XWPFTableRow inputRow = inputRows.get(rowIndex); - final XWPFTableRow outputRow = outputRows.get(rowIndex); - final List inputCells = inputRow.getTableCells(); - final List outputCells = outputRow.getTableCells(); - for (int cellIndex = 0; cellIndex < inputCells.size(); cellIndex++) { - final XWPFTableCell inputCell = inputCells.get(cellIndex); - final XWPFTableCell outputCell = outputCells.get(cellIndex); - final List inputBodyElements = inputCell.getBodyElements(); - final List outputBodyElements = outputCell.getBodyElements(); - for (int bodyElementIndex = 0; bodyElementIndex < inputBodyElements.size(); bodyElementIndex++) { - final IBodyElement inputBodyElement = inputBodyElements.get(bodyElementIndex); - if (inputBodyElement instanceof XWPFParagraph) { - final IBodyElement outputBodyElement = outputBodyElements.get(bodyElementIndex); - updateBookmarks(bookmarkManager, ((XWPFParagraph) outputBodyElement).getCTP(), - ((XWPFParagraph) inputBodyElement).getCTP(), outputTable.getBody()); - } + final int rowCount = Math.min(inputRows.size(), outputRows.size()); + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { + final XWPFTableRow inputRow = inputRows.get(rowIndex); + final XWPFTableRow outputRow = outputRows.get(rowIndex); + final List inputCells = inputRow.getTableCells(); + final List outputCells = outputRow.getTableCells(); + final int cellCount = Math.min(inputCells.size(), outputCells.size()); + for (int cellIndex = 0; cellIndex < cellCount; cellIndex++) { + final XWPFTableCell inputCell = inputCells.get(cellIndex); + final XWPFTableCell outputCell = outputCells.get(cellIndex); + final List inputBodyElements = inputCell.getBodyElements(); + final List outputBodyElements = outputCell.getBodyElements(); + final int bodyElementCount = Math.min(inputBodyElements.size(), outputBodyElements.size()); + for (int bodyElementIndex = 0; bodyElementIndex < bodyElementCount; bodyElementIndex++) { + final IBodyElement inputBodyElement = inputBodyElements.get(bodyElementIndex); + final IBodyElement outputBodyElement = outputBodyElements.get(bodyElementIndex); + if (inputBodyElement instanceof XWPFParagraph + && outputBodyElement instanceof XWPFParagraph) { + updateBookmarks(bookmarkManager, ((XWPFParagraph) outputBodyElement).getCTP(), + ((XWPFParagraph) inputBodyElement).getCTP(), outputCell); + } else if (inputBodyElement instanceof XWPFTable + && outputBodyElement instanceof XWPFTable) { + updateBookmarks(bookmarkManager, (XWPFTable) outputBodyElement, + (XWPFTable) inputBodyElement); } } } @@ -1025,18 +1031,21 @@ private void updateBookmarks(BookmarkManager bookmarkManager, CTP outputParagrap IBody outputBody) { final List oldBookmarks = inputParagraph.getBookmarkStartList(); final List newBookmarks = outputParagraph.getBookmarkStartList(); - for (int bookmarkIndex = 0; bookmarkIndex < oldBookmarks.size(); bookmarkIndex++) { + final int bookmarkCount = Math.min(oldBookmarks.size(), newBookmarks.size()); + for (int bookmarkIndex = 0; bookmarkIndex < bookmarkCount; bookmarkIndex++) { bookmarkManager.updateXmlObject(newBookmarks.get(bookmarkIndex), oldBookmarks.get(bookmarkIndex), outputBody); } - List inputRuns = inputParagraph.getRList(); + final List inputRuns = inputParagraph.getRList(); final List outputRuns = outputParagraph.getRList(); - for (int runIndex = 0; runIndex < inputRuns.size(); runIndex++) { + final int runCount = Math.min(inputRuns.size(), outputRuns.size()); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final CTR inputRun = inputRuns.get(runIndex); final CTR outputRun = outputRuns.get(runIndex); final List inputTexts = inputRun.getInstrTextList(); final List outputTexts = outputRun.getInstrTextList(); - for (int textIndex = 0; textIndex < inputTexts.size(); textIndex++) { + final int textCount = Math.min(inputTexts.size(), outputTexts.size()); + for (int textIndex = 0; textIndex < textCount; textIndex++) { bookmarkManager.updateXmlObject(outputTexts.get(textIndex), inputTexts.get(textIndex), outputBody); } } diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/parser/AbstractBodyParser.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/parser/AbstractBodyParser.java index e1cf00b11..838c16797 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/parser/AbstractBodyParser.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/parser/AbstractBodyParser.java @@ -38,6 +38,7 @@ import org.obeonetwork.m2doc.template.TemplatePackage; import org.obeonetwork.m2doc.util.AQL56Compatibility; import org.obeonetwork.m2doc.util.M2DocUtils; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtBlock; import static org.obeonetwork.m2doc.util.M2DocUtils.message; @@ -270,7 +271,18 @@ protected Table parseTable(XWPFTable wtable) throws DocumentParserException { for (XWPFTableRow tablerow : wtable.getRows()) { Row row = (Row) EcoreUtil.create(TemplatePackage.Literals.ROW); table.getRows().add(row); - row.setTableRow(tablerow); + + /* + * Keep a detached snapshot of the complete source row before any + * cell is parsed. Parsing a cell uses a TokenIteratorFieldRewriter + * that can modify the underlying OOXML. If the live table row is + * stored in the AST, paragraph- and row-level bookmark markers can + * be lost before generation, in particular Word's _Ref bookmarks + * that start inside a cell and end directly under w:tr. + */ + final CTRow sourceRow = (CTRow) tablerow.getCtRow().copy(); + row.setTableRow(new XWPFTableRow(sourceRow, wtable)); + for (XWPFTableCell tableCell : tablerow.getTableCells()) { Cell cell = (Cell) EcoreUtil.create(TemplatePackage.Literals.CELL); row.getCells().add(cell); diff --git a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/services/LinkServices.java b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/services/LinkServices.java index a09616b54..7ec955d19 100644 --- a/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/services/LinkServices.java +++ b/plugins/org.obeonetwork.m2doc/src/org/obeonetwork/m2doc/services/LinkServices.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2024 Obeo. + * Copyright (c) 2016, 2025 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -16,8 +16,16 @@ import org.eclipse.acceleo.annotations.api.documentation.Param; import org.eclipse.acceleo.annotations.api.documentation.ServiceProvider; import org.obeonetwork.m2doc.element.MBookmark; +import org.obeonetwork.m2doc.element.MBookmarkCustomTextRef; +import org.obeonetwork.m2doc.element.MBookmarkPageRef; +import org.obeonetwork.m2doc.element.MBookmarkSectionRef; +import org.obeonetwork.m2doc.element.MBookmarkTextRef; import org.obeonetwork.m2doc.element.MHyperLink; +import org.obeonetwork.m2doc.element.impl.MBookmarkCustomTextRefImpl; import org.obeonetwork.m2doc.element.impl.MBookmarkImpl; +import org.obeonetwork.m2doc.element.impl.MBookmarkPageRefImpl; +import org.obeonetwork.m2doc.element.impl.MBookmarkSectionRefImpl; +import org.obeonetwork.m2doc.element.impl.MBookmarkTextRefImpl; import org.obeonetwork.m2doc.element.impl.MHyperLinkImpl; //@formatter:off @@ -90,7 +98,7 @@ public MHyperLink asLink(String text, String url, String toolTip) { ) // @formatter:on public MBookmark asBookmark(String text, String id) { - return new MBookmarkImpl(text, id, false); + return new MBookmarkImpl(text, id); } // @formatter:off @@ -106,7 +114,7 @@ public MBookmark asBookmark(String text, String id) { } ) // @formatter:on - public MBookmark asBookmarkRef(String text, String id) { + public MBookmarkCustomTextRef asBookmarkRef(String text, String id) { return asBookmarkRef(text, id, false); } @@ -124,12 +132,107 @@ public MBookmark asBookmarkRef(String text, String id) { } ) // @formatter:on - public MBookmark asBookmarkRef(String text, String id, boolean optional) { - final MBookmarkImpl res = new MBookmarkImpl(text, id, true); + public MBookmarkCustomTextRef asBookmarkRef(String text, String id, boolean optional) { + return new MBookmarkCustomTextRefImpl(text, id, optional); + } + + // @formatter:off + @Documentation( + value = "Inserts a page number bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + }, + result = "A bookmark reference with the page numder.", + examples = { + @Example(expression = "'Art1'.asBookmarkPageRef()", result = "a bookmark reference with the ID 'Art1' with the page number"), + } + ) + // @formatter:on + public MBookmarkPageRef asBookmarkPageRef(String id) { + return asBookmarkPageRef(id, false); + } + + // @formatter:off + @Documentation( + value = "Inserts a page number bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + @Param(name = "optional", value = "If true, the reference is omitted if the corresponding bookmark doesn't exists"), + }, + result = "A bookmark reference with the page numder.", + examples = { + @Example(expression = "'Art1'.asBookmarkPageRef(true)", result = "a bookmark reference with the ID 'Art1' with the page number only if a bookmark with ID 'Art1' exists"), + } + ) + // @formatter:on + public MBookmarkPageRef asBookmarkPageRef(String id, boolean optional) { + return new MBookmarkPageRefImpl(id, optional); + } + + // @formatter:off + @Documentation( + value = "Inserts a section bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + }, + result = "A bookmark reference with the section.", + examples = { + @Example(expression = "'Art1'.asBookmarkSectionRef(true)", result = "a bookmark reference with the ID 'Art1' with the section number"), + } + ) + // @formatter:on + public MBookmarkSectionRef asBookmarkSectionRef(String id) { + return asBookmarkSectionRef(id, false); + } + + // @formatter:off + @Documentation( + value = "Inserts a section bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + @Param(name = "optional", value = "If true, the reference is omitted if the corresponding bookmark doesn't exists"), + }, + result = "A bookmark reference with the section.", + examples = { + @Example(expression = "'Art1'.asBookmarkPageRef(true)", result = "a bookmark reference with the ID 'Art1' with the section number"), + } + ) + // @formatter:on + public MBookmarkSectionRef asBookmarkSectionRef(String id, boolean optional) { + return new MBookmarkSectionRefImpl(id, optional); + } - res.setOptional(optional); + // @formatter:off + @Documentation( + value = "Inserts a text bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + }, + result = "A bookmark reference with the bookmark text.", + examples = { + @Example(expression = "'Art1'.asBookmarkPageRef()", result = "a bookmark reference with the ID 'Art1' with the bookmark text"), + } + ) + // @formatter:on + public MBookmarkTextRef asBookmarkTextRef(String id) { + return asBookmarkTextRef(id, false); + } - return res; + // @formatter:off + @Documentation( + value = "Inserts a text bookmark reference", + params = { + @Param(name = "id", value = "The ID of the bookmark reference"), + @Param(name = "optional", value = "If true, the reference is omitted if the corresponding bookmark doesn't exists"), + }, + result = "A bookmark reference with the bookmark text.", + examples = { + @Example(expression = "'Art1'.asBookmarkPageRef(true)", result = "a bookmark reference with the ID 'Art1' with the bookmark text only if a bookmark with ID 'Art1' exists"), + } + ) + // @formatter:on + public MBookmarkTextRef asBookmarkTextRef(String id, boolean optional) { + return new MBookmarkTextRefImpl(id, optional); } }