From 82bbf63582bdf28e7918e58ebf6657a9144bc9f3 Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Tue, 13 Jun 2023 23:15:08 +0300 Subject: tdf#155823: Improve the check if the list id is not required The implementation introduced in commit 8f48f91009caa86d896f247059874242ed18bf39 (SwNumRule::HasContinueList) was a bit naive: it assumed that maTextNodeList is sorted (it is not, and so, valid cases to avoid the id were missed); it assumed that a given list can only consist of items of a single numbering style, and so only tested the list of nodes referenced from maTextNodeList of given SwNumRule. I.e., this implementation targeted a special case of a list style fully covering a single continuous list. This skipped ids for list items with list styles, in which maTextNodeList passed the check in HasContinueList, but which were followed by items with a different list style, continuing the same list. This constellation outputs continue-list attribute in the following items (see XMLTextParagraphExport::exportListChange), which references the skipped id. The resulting ODF is an invalid XML (an xml:id is missing that is referenced), and also does not allow to continue such a list. The change tries to fix this, using a list of nodes in XMLTextParagraphExport, and analyzing if the list of the current paragraph has a continuation that needs to reference this list id. Two new hidden properties introduced in SwXParagraph and SwXTextDocument: "ODFExport_NodeIndex" and "ODFExport_ListNodes", resp. They allow to pipe the data to the export. The previous special casing of property state for "ListId", used in SwNumRule::HasContinueList, is removed together with the mentioned function. The intention is to have a logic allowing to detect 100% cases where the list id is required, and where it's not required. A related unit test for tdf#149668 was fixed to not rely on the mentioned ListId property state workaround, and moved from sw/qa/core/unocore to xmloff/qa/unit. Change-Id: If6a6ac7a3dfe0b2ea143229678a603875153eedb Reviewed-on: https://gerrit.libreoffice.org/c/core/+/153044 Tested-by: Jenkins Reviewed-by: Mike Kaganski --- include/xmloff/txtparae.hxx | 6 + sw/inc/numrule.hxx | 3 - sw/qa/core/unocore/unocore.cxx | 41 ------ sw/source/core/doc/number.cxx | 23 ---- sw/source/core/unocore/unoparagraph.cxx | 18 ++- sw/source/uibase/uno/unotxdoc.cxx | 28 ++++ .../qa/unit/data/differentListStylesInOneList.fodt | 47 +++++++ xmloff/qa/unit/text.cxx | 104 +++++++++++++++ xmloff/source/text/XMLTextNumRuleInfo.cxx | 12 +- xmloff/source/text/XMLTextNumRuleInfo.hxx | 5 +- xmloff/source/text/txtparae.cxx | 146 ++++++++++++++++++--- 11 files changed, 328 insertions(+), 105 deletions(-) create mode 100644 xmloff/qa/unit/data/differentListStylesInOneList.fodt diff --git a/include/xmloff/txtparae.hxx b/include/xmloff/txtparae.hxx index 1ba8b0b1a0c8..af23376d1160 100644 --- a/include/xmloff/txtparae.hxx +++ b/include/xmloff/txtparae.hxx @@ -112,6 +112,9 @@ class XMLOFF_DLLPUBLIC XMLTextParagraphExport : public XMLStyleExport XMLTextListsHelper* mpTextListsHelper; ::std::vector< std::unique_ptr > maTextListsHelperStack; + struct DocumentListNodes; + std::unique_ptr mpDocumentListNodes; + o3tl::sorted_vector> maFrameRecurseGuard; o3tl::sorted_vector> maShapeRecurseGuard; @@ -537,6 +540,9 @@ public: void PopTextListsHelper(); private: + bool ShouldSkipListId(const css::uno::Reference& xTextContent); + bool ExportListId() const; + XMLTextParagraphExport(XMLTextParagraphExport const &) = delete; }; diff --git a/sw/inc/numrule.hxx b/sw/inc/numrule.hxx index fde2c8de0fb8..f642e21e746c 100644 --- a/sw/inc/numrule.hxx +++ b/sw/inc/numrule.hxx @@ -272,9 +272,6 @@ public: void dumpAsXml(xmlTextWriterPtr w) const; void GetGrabBagItem(css::uno::Any& rVal) const; void SetGrabBagItem(const css::uno::Any& rVal); - - /// Is it possible that this numbering has multiple lists? - bool HasContinueList() const; }; /// namespace for static functions and methods for numbering and bullets diff --git a/sw/qa/core/unocore/unocore.cxx b/sw/qa/core/unocore/unocore.cxx index 605813a719b3..a1e931e75fb4 100644 --- a/sw/qa/core/unocore/unocore.cxx +++ b/sw/qa/core/unocore/unocore.cxx @@ -668,47 +668,6 @@ CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testContentControlDate) CPPUNIT_ASSERT_EQUAL(OUString("sdtContentLocked"), pContentControl->GetLock()); } -CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testListIdState) -{ - // Given a document with 3 paragraphs: an outer numbering on para 1 & 3, an inner numbering on - // para 2: - createSwDoc(); - SwDoc* pDoc = getSwDoc(); - SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); - { - SfxItemSetFixed aSet(pWrtShell->GetAttrPool()); - SwNumRuleItem aItem("Numbering ABC"); - aSet.Put(aItem); - pWrtShell->SetAttrSet(aSet); - } - pWrtShell->SplitNode(); - { - SfxItemSetFixed aSet(pWrtShell->GetAttrPool()); - SwNumRuleItem aItem("Numbering 123"); - aSet.Put(aItem); - pWrtShell->SetAttrSet(aSet); - } - pWrtShell->SplitNode(); - { - SfxItemSetFixed aSet(pWrtShell->GetAttrPool()); - SwNumRuleItem aItem("Numbering ABC"); - aSet.Put(aItem); - pWrtShell->SetAttrSet(aSet); - } - - // When checking if xml:id="..." needs writing for the first paragraph during ODT export: - uno::Reference xPara(getParagraph(1), uno::UNO_QUERY); - beans::PropertyState eState = xPara->getPropertyState("ListId"); - - // Then make sure that xml:id="..." gets written for para 1, as it'll be continued in para 3. - // Without the accompanying fix in place, this test would have failed with: - // - Expected: 0 (DIRECT_VALUE) - // - Actual : 1 (DEFAULT_VALUE) - // i.e. para 1 didn't write an xml:id="..." but para 3 referred to it using continue-list="...", - // which is inconsistent. - CPPUNIT_ASSERT_EQUAL(beans::PropertyState_DIRECT_VALUE, eState); -} - CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testContentControlPlainText) { // Given an empty document: diff --git a/sw/source/core/doc/number.cxx b/sw/source/core/doc/number.cxx index 7dc2953608cf..f953a93dbde4 100644 --- a/sw/source/core/doc/number.cxx +++ b/sw/source/core/doc/number.cxx @@ -1153,29 +1153,6 @@ void SwNumRule::SetGrabBagItem(const uno::Any& rVal) mpGrabBagItem->PutValue(rVal, 0); } -bool SwNumRule::HasContinueList() const -{ - // In case all text nodes are after each other, then we won't have a later list that wants to - // continue us. - SwNodeOffset nIndex(0); - for (size_t i = 0; i < maTextNodeList.size(); ++i) - { - SwTextNode* pNode = maTextNodeList[i]; - if (i > 0) - { - if (pNode->GetIndex() != nIndex + 1) - { - // May have a continue list. - return true; - } - } - nIndex = pNode->GetIndex(); - } - - // Definitely won't have a continue list. - return false; -} - namespace numfunc { namespace { diff --git a/sw/source/core/unocore/unoparagraph.cxx b/sw/source/core/unocore/unoparagraph.cxx index 1d0fae809f41..15f9b56c490f 100644 --- a/sw/source/core/unocore/unoparagraph.cxx +++ b/sw/source/core/unocore/unoparagraph.cxx @@ -542,6 +542,14 @@ uno::Sequence< uno::Any > SwXParagraph::Impl::GetPropertyValues_Impl( continue; } + if (pPropertyNames[nProp] == "ODFExport_NodeIndex") + { + // A hack to avoid writing random list ids to ODF when they are not referred later + // see XMLTextParagraphExport::DocumentListNodes::ShouldSkipListId + pValues[nProp] <<= rTextNode.GetIndex().get(); + continue; + } + SfxItemPropertyMapEntry const*const pEntry = rMap.getByName( pPropertyNames[nProp] ); if (!pEntry) @@ -931,16 +939,6 @@ static beans::PropertyState lcl_SwXParagraph_getPropertyState( bDone = true; break; } - case FN_UNO_LIST_ID: - { - SwNumRule* pNumRule = rTextNode.GetNumRule(); - if (pNumRule && pNumRule->HasContinueList()) - { - eRet = beans::PropertyState_DIRECT_VALUE; - } - bDone = true; - break; - } case FN_UNO_ANCHOR_TYPES: { bDone = true; diff --git a/sw/source/uibase/uno/unotxdoc.cxx b/sw/source/uibase/uno/unotxdoc.cxx index fadfe90a82b6..51c777ee47da 100644 --- a/sw/source/uibase/uno/unotxdoc.cxx +++ b/sw/source/uibase/uno/unotxdoc.cxx @@ -1969,6 +1969,34 @@ Any SwXTextDocument::getPropertyValue(const OUString& rPropertyName) if(!IsValid()) throw DisposedException("", static_cast< XTextDocument* >(this)); + if (rPropertyName == "ODFExport_ListNodes") + { + // A hack to avoid writing random list ids to ODF when they are not referred later + // see XMLTextParagraphExport::DocumentListNodes ctor + + // Sequence of nodes, each of them represented by four-element sequence: + // [ index, styleIntPtr, list_id, isRestart ] + std::vector> nodes; + + const SwDoc& rDoc = *m_pDocShell->GetDoc(); + for (const SwNumRule* pNumRule : rDoc.GetNumRuleTable()) + { + SwNumRule::tTextNodeList textNodes; + pNumRule->GetTextNodeList(textNodes); + css::uno::Any styleIntPtr(reinterpret_cast(pNumRule)); + + for (const SwTextNode* pTextNode : textNodes) + { + css::uno::Any index(pTextNode->GetIndex().get()); + css::uno::Any list_id(pTextNode->GetListId()); + css::uno::Any isRestart(pTextNode->IsListRestart()); + + nodes.push_back({ index, styleIntPtr, list_id, isRestart }); + } + } + return css::uno::Any(comphelper::containerToSequence(nodes)); + } + const SfxItemPropertyMapEntry* pEntry = m_pPropSet->getPropertyMap().getByName( rPropertyName); if(!pEntry) diff --git a/xmloff/qa/unit/data/differentListStylesInOneList.fodt b/xmloff/qa/unit/data/differentListStylesInOneList.fodt new file mode 100644 index 000000000000..5f90135fbb23 --- /dev/null +++ b/xmloff/qa/unit/data/differentListStylesInOneList.fodt @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + Item1 (ListStyleOne) + + + + + + Item2 (ListStyleOne) + + + + + + Item3 (ListStyleAnother) + + + + + + Item4 (ListStyleOne) + + + + + \ No newline at end of file diff --git a/xmloff/qa/unit/text.cxx b/xmloff/qa/unit/text.cxx index 20147161d52f..bff3f8b14821 100644 --- a/xmloff/qa/unit/text.cxx +++ b/xmloff/qa/unit/text.cxx @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -243,6 +244,109 @@ CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testListId) assertXPathNoAttribute(pXmlDoc, "//text:list", "id"); } +CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testListId2) +{ + // tdf#155823 Given a document with a list consisting of items having different list styles: + loadFromURL(u"differentListStylesInOneList.fodt"); + + auto xTextDocument(mxComponent.queryThrow()); + auto xParaEnumAccess(xTextDocument->getText().queryThrow()); + auto xParaEnum(xParaEnumAccess->createEnumeration()); + + auto xPara(xParaEnum->nextElement().queryThrow()); + auto aActual(xPara->getPropertyValue("ListLabelString").get()); + CPPUNIT_ASSERT_EQUAL(OUString("1."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("2."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("3."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("4."), aActual); + + // When storing that document as ODF: + // Without the fix in place, automatic validation would fail with: + // Error: "list123456789012345" is referenced by an IDREF, but not defined. + saveAndReload("writer8"); + + xTextDocument.set(mxComponent.queryThrow()); + xParaEnumAccess.set(xTextDocument->getText().queryThrow()); + xParaEnum.set(xParaEnumAccess->createEnumeration()); + + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("1."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("2."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + CPPUNIT_ASSERT_EQUAL(OUString("3."), aActual); + xParaEnum->nextElement(); // Skip empty intermediate paragraph + + // Check that the last item number is correct + + xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY); + aActual = xPara->getPropertyValue("ListLabelString").get(); + // Without the fix in place, this would fail with: + // - Expected: 4. + // - Actual : 1. + // i.e. the numbering was not continued. + CPPUNIT_ASSERT_EQUAL(OUString("4."), aActual); + + // Then make sure that required xml:id="..." attributes is written when the style changes: + xmlDocUniquePtr pXmlDoc = parseExport("content.xml"); + CPPUNIT_ASSERT(pXmlDoc); + // Without the fix in place, this would fail, + // i.e. xml:id="..." was omitted, even though it was needed for the next item. + OUString id + = getXPath(pXmlDoc, "/office:document-content/office:body/office:text/text:list[3]", "id"); + CPPUNIT_ASSERT(!id.isEmpty()); + assertXPath(pXmlDoc, "/office:document-content/office:body/office:text/text:list[4]", + "continue-list", id); +} + +CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testListIdState) +{ + // tdf#149668: given a document with 3 paragraphs: an outer numbering on para 1 & 3, an inner + // numbering on para 2: + mxComponent = loadFromDesktop("private:factory/swriter"); + auto xTextDocument(mxComponent.queryThrow()); + auto xText(xTextDocument->getText()); + xText->insertControlCharacter(xText->getEnd(), css::text::ControlCharacter::PARAGRAPH_BREAK, + false); + xText->insertControlCharacter(xText->getEnd(), css::text::ControlCharacter::PARAGRAPH_BREAK, + false); + + auto paraEnumAccess(xText.queryThrow()); + auto paraEnum(paraEnumAccess->createEnumeration()); + auto xParaProps(paraEnum->nextElement().queryThrow()); + xParaProps->setPropertyValue("NumberingStyleName", css::uno::Any(OUString("Numbering ABC"))); + xParaProps.set(paraEnum->nextElement().queryThrow()); + xParaProps->setPropertyValue("NumberingStyleName", css::uno::Any(OUString("Numbering 123"))); + xParaProps.set(paraEnum->nextElement().queryThrow()); + xParaProps->setPropertyValue("NumberingStyleName", css::uno::Any(OUString("Numbering ABC"))); + + // When storing that document as ODF: + save("writer8"); + xmlDocUniquePtr pXmlDoc = parseExport("content.xml"); + + // Make sure that xml:id="..." gets written for para 1, as it'll be continued in para 3. + // Without the accompanying fix in place, this test would have failed, + // i.e. para 1 didn't write an xml:id="..." but para 3 referred to it using continue-list="...", + // which is inconsistent. + OUString id + = getXPath(pXmlDoc, "/office:document-content/office:body/office:text/text:list[1]", "id"); + CPPUNIT_ASSERT(!id.isEmpty()); +} + CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testClearingBreakExport) { // Given a document with a clearing break: diff --git a/xmloff/source/text/XMLTextNumRuleInfo.cxx b/xmloff/source/text/XMLTextNumRuleInfo.cxx index 062b92879ee2..5f9a5e2b7a54 100644 --- a/xmloff/source/text/XMLTextNumRuleInfo.cxx +++ b/xmloff/source/text/XMLTextNumRuleInfo.cxx @@ -54,14 +54,14 @@ void XMLTextNumRuleInfo::Set( const css::uno::Reference < css::text::XTextContent > & xTextContent, const bool bOutlineStyleAsNormalListStyle, const XMLTextListAutoStylePool& rListAutoPool, - const bool bExportTextNumberElement ) + const bool bExportTextNumberElement, + const bool bListIdIsDefault ) { Reset(); // Written OpenDocument file format doesn't fit to the created text document (#i69627#) mbOutlineStyleAsNormalListStyle = bOutlineStyleAsNormalListStyle; Reference< XPropertySet > xPropSet( xTextContent, UNO_QUERY ); - Reference xPropState(xTextContent, UNO_QUERY); Reference< XPropertySetInfo > xPropSetInfo = xPropSet->getPropertySetInfo(); // check if this paragraph supports a numbering @@ -138,14 +138,10 @@ void XMLTextNumRuleInfo::Set( if( xPropSetInfo->hasPropertyByName( "ListId" ) ) { xPropSet->getPropertyValue( "ListId" ) >>= msListId; - - if (xPropState.is()) - { - mbListIdIsDefault - = xPropState->getPropertyState("ListId") == PropertyState_DEFAULT_VALUE; - } } + mbListIdIsDefault = bListIdIsDefault; + mbContinueingPreviousSubTree = false; if( xPropSetInfo->hasPropertyByName( "ContinueingPreviousSubTree" ) ) { diff --git a/xmloff/source/text/XMLTextNumRuleInfo.hxx b/xmloff/source/text/XMLTextNumRuleInfo.hxx index adb405411164..7cbc3cf8d4fb 100644 --- a/xmloff/source/text/XMLTextNumRuleInfo.hxx +++ b/xmloff/source/text/XMLTextNumRuleInfo.hxx @@ -66,9 +66,10 @@ public: inline XMLTextNumRuleInfo& operator=( const XMLTextNumRuleInfo& rInfo ); void Set( const css::uno::Reference < css::text::XTextContent > & rTextContent, - const bool bOutlineStyleAsNormalListStyle, + bool bOutlineStyleAsNormalListStyle, const XMLTextListAutoStylePool& rListAutoPool, - const bool bExportTextNumberElement ); + bool bExportTextNumberElement, + bool bListIdIsDefault ); inline void Reset(); const OUString& GetNumRulesName() const diff --git a/xmloff/source/text/txtparae.cxx b/xmloff/source/text/txtparae.cxx index 2b3af03b93dd..c78c504bfa50 100644 --- a/xmloff/source/text/txtparae.cxx +++ b/xmloff/source/text/txtparae.cxx @@ -997,7 +997,7 @@ void XMLTextParagraphExport::exportListChange( // end a list if ( rPrevInfo.GetLevel() > 0 ) { - sal_Int16 nListLevelsToBeClosed = 0; + sal_uInt32 nListLevelsToBeClosed = 0; // unsigned larger type to safely multiply and compare if ( !rNextInfo.BelongsToSameList( rPrevInfo ) || rNextInfo.GetLevel() <= 0 ) { @@ -1007,13 +1007,11 @@ void XMLTextParagraphExport::exportListChange( else if ( rPrevInfo.GetLevel() > rNextInfo.GetLevel() ) { // close corresponding sub lists - SAL_WARN_IF( rNextInfo.GetLevel() <= 0, "xmloff", - " 0> not hold. Serious defect." ); nListLevelsToBeClosed = rPrevInfo.GetLevel() - rNextInfo.GetLevel(); } if ( nListLevelsToBeClosed > 0 && - maListElements.size() >= sal::static_int_cast< sal_uInt32 >( 2 * nListLevelsToBeClosed ) ) + maListElements.size() >= 2 * nListLevelsToBeClosed ) { do { for(size_t j = 0; j < 2; ++j) @@ -1031,11 +1029,6 @@ void XMLTextParagraphExport::exportListChange( } } - const bool bExportODF = - bool( GetExport().getExportFlags() & SvXMLExportFlags::OASIS ); - const SvtSaveOptions::ODFSaneDefaultVersion eODFDefaultVersion = - GetExport().getSaneDefaultVersion(); - // start a new list if ( rNextInfo.GetLevel() > 0 ) { @@ -1051,8 +1044,6 @@ void XMLTextParagraphExport::exportListChange( else if ( rNextInfo.GetLevel() > rPrevInfo.GetLevel() ) { // open corresponding sub lists - SAL_WARN_IF( rPrevInfo.GetLevel() <= 0, "xmloff", - " 0> not hold. Serious defect." ); nListLevelsToBeOpened = rNextInfo.GetLevel() - rPrevInfo.GetLevel(); } @@ -1074,8 +1065,7 @@ void XMLTextParagraphExport::exportListChange( { if ( !mpTextListsHelper->IsListProcessed( sListId ) ) { - if ( bExportODF && - eODFDefaultVersion >= SvtSaveOptions::ODFSVER_012 && + if ( ExportListId() && !sListId.isEmpty() && !rNextInfo.IsListIdDefault() ) { /* Property text:id at element has to be @@ -1093,8 +1083,7 @@ void XMLTextParagraphExport::exportListChange( { const OUString sNewListId( mpTextListsHelper->GenerateNewListId() ); - if ( bExportODF && - eODFDefaultVersion >= SvtSaveOptions::ODFSVER_012 && + if ( ExportListId() && !sListId.isEmpty() && !rNextInfo.IsListIdDefault() ) { /* Property text:id at element has to be @@ -1124,8 +1113,7 @@ void XMLTextParagraphExport::exportListChange( } else { - if ( bExportODF && - eODFDefaultVersion >= SvtSaveOptions::ODFSVER_012 && + if ( ExportListId() && !sListId.isEmpty() ) { GetExport().AddAttribute( XML_NAMESPACE_TEXT, @@ -1803,6 +1791,127 @@ void XMLTextParagraphExport::exportText( m_pRedlineExport->ExportStartOrEndRedline( xPropertySet, false ); } +bool XMLTextParagraphExport::ExportListId() const +{ + return (GetExport().getExportFlags() & SvXMLExportFlags::OASIS) + && GetExport().getSaneDefaultVersion() >= SvtSaveOptions::ODFSVER_012; +} + +struct XMLTextParagraphExport::DocumentListNodes +{ + struct NodeData + { + sal_Int32 index; // see SwNode::GetIndex and SwNodeOffset + sal_uInt64 style_id; // actually a pointer to NumRule + OUString list_id; + bool isRestart; + }; + std::vector docListNodes; + DocumentListNodes(const css::uno::Reference& xModel) + { + // Sequence of nodes, each of them represented by four-element sequence, + // corresponding to NodeData members + css::uno::Sequence> nodes; + if (auto xPropSet = xModel.query()) + { + try + { + // See SwXTextDocument::getPropertyValue + xPropSet->getPropertyValue("ODFExport_ListNodes") >>= nodes; + } + catch (css::beans::UnknownPropertyException&) + { + // That's absolutely fine! + } + } + + docListNodes.reserve(nodes.getLength()); + for (const auto& node : nodes) + { + assert(node.getLength() == 4); + docListNodes.push_back({ node[0].get(), node[1].get(), + node[2].get(), node[3].get() }); + } + + std::sort(docListNodes.begin(), docListNodes.end(), + [](const NodeData& lhs, const NodeData& rhs) { return lhs.index < rhs.index; }); + } + bool ShouldSkipListId(const Reference& xTextContent) const + { + if (docListNodes.empty()) + return false; + + if (auto xPropSet = xTextContent.query()) + { + sal_Int32 index = 0; + try + { + // See SwXParagraph::Impl::GetPropertyValues_Impl + xPropSet->getPropertyValue("ODFExport_NodeIndex") >>= index; + } + catch (css::beans::UnknownPropertyException&) + { + // That's absolutely fine! + return false; + } + + auto it = std::lower_bound(docListNodes.begin(), docListNodes.end(), index, + [](const NodeData& lhs, sal_Int32 rhs) + { return lhs.index < rhs; }); + if (it == docListNodes.end() || it->index != index) + return false; + + // We need to write the id, when there will be continuation of the list either with + // a different list style, or after another list. + + for (auto next = it + 1; next != docListNodes.end(); ++next) + { + if (it->list_id != next->list_id) + { + // List changed. We will have to refer to this id, only if there will + // appear a continuation of this list + return std::find_if(next + 1, docListNodes.end(), + [list_id = it->list_id](const NodeData& data) + { return data.list_id == list_id; }) + == docListNodes.end(); + } + + if (it->style_id != next->style_id) + { + // Same list, new style -> this "next" will refer to the id, no skipping + return false; + } + if (it->index + 1 != next->index) + { + // we have a gap before the next node with the same list and style, + // with no other lists in between. There will be a continuation; + // in case of restart, there will be a reference to the id; + // otherwise, there will be simple 'text:continue-numbering="true"'. + return !next->isRestart; + } + it = next; // walk through adjacent nodes of the same list + } + // all nodes were adjacent and of the same list and style -> no continuation, skip id + return true; + } + + return false; + } +}; + +bool XMLTextParagraphExport::ShouldSkipListId(const Reference& xTextContent) +{ + if (!mpDocumentListNodes) + { + if (ExportListId()) + mpDocumentListNodes.reset(new DocumentListNodes(GetExport().GetModel())); + else + mpDocumentListNodes.reset(new DocumentListNodes({})); + } + + return mpDocumentListNodes->ShouldSkipListId(xTextContent); +} + void XMLTextParagraphExport::exportTextContentEnumeration( const Reference < XEnumeration > & rContEnum, bool bAutoStyles, @@ -1861,7 +1970,8 @@ void XMLTextParagraphExport::exportTextContentEnumeration( aNextNumInfo.Set( xTxtCntnt, GetExport().writeOutlineStyleAsNormalListStyle(), GetListAutoStylePool(), - GetExport().exportTextNumberElement() ); + GetExport().exportTextNumberElement(), + ShouldSkipListId(xTxtCntnt) ); exportListAndSectionChange( xCurrentTextSection, aPropSetHelper, TEXT_SECTION, xTxtCntnt, -- cgit