From 8f242675c1b1620b00aec00f230df26280eb602a Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Sat, 3 Oct 2026 22:21:00 +0300 Subject: [PATCH] tdf#173712 oox: continue a PPTX numbered list that repeats startAt PowerPoint writes the same startAt on every paragraph of a list, and since tdf#168406 each startAt restarted it: 2. 2. 2. instead of 2. 3. 4. Restart only when a list begins at that level or its numbering type or startAt changes, a missing startAt being startAt="1". Paragraphs without a bullet of their own, also without a:pPr, take it from a:lstStyle. Don't set the restart flag in a:lstStyle itself: every paragraph inherits it. An unnumbered paragraph ends the lists at its level and deeper. Change-Id: I26720a6ba54a8050cfb6e0ba892096039c391c36 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211632 Reviewed-by: David Gilbert Tested-by: Jenkins --- oox/inc/drawingml/textbodycontext.hxx | 5 +- oox/inc/drawingml/textparagraphproperties.hxx | 1 + .../textparagraphpropertiescontext.hxx | 41 +++++++--- oox/source/drawingml/textbodycontext.cxx | 24 ++++-- .../textparagraphpropertiescontext.cxx | 79 +++++++------------ 5 files changed, 79 insertions(+), 71 deletions(-) diff --git a/oox/inc/drawingml/textbodycontext.hxx b/oox/inc/drawingml/textbodycontext.hxx index debed75bccb6..33f35978ed95 100644 --- a/oox/inc/drawingml/textbodycontext.hxx +++ b/oox/inc/drawingml/textbodycontext.hxx @@ -20,9 +20,8 @@ #ifndef INCLUDED_OOX_DRAWINGML_TEXTBODYCONTEXT_HXX #define INCLUDED_OOX_DRAWINGML_TEXTBODYCONTEXT_HXX -#include - #include +#include #include #include @@ -39,7 +38,7 @@ public: private: TextBody& mrTextBody; ShapePtr mpShapePtr; - uint16_t mListNumberingMask; + ListNumberingState maListNumberingState; }; // CT_RegularTextRun diff --git a/oox/inc/drawingml/textparagraphproperties.hxx b/oox/inc/drawingml/textparagraphproperties.hxx index c233fda17b47..744cb91a05f2 100644 --- a/oox/inc/drawingml/textparagraphproperties.hxx +++ b/oox/inc/drawingml/textparagraphproperties.hxx @@ -82,6 +82,7 @@ public: sal_Int16 getLevel( ) const { return mnLevel; } PropertyMap& getTextParagraphPropertyMap() { return maTextParagraphPropertyMap; } BulletList& getBulletList() { return maBulletList; } + const BulletList& getBulletList() const { return maBulletList; } TextCharacterProperties& getTextCharacterProperties() { return maTextCharacterProperties; } const TextCharacterProperties& getTextCharacterProperties() const { return maTextCharacterProperties; } diff --git a/oox/inc/drawingml/textparagraphpropertiescontext.hxx b/oox/inc/drawingml/textparagraphpropertiescontext.hxx index 6a002451e49b..e2184060d5fd 100644 --- a/oox/inc/drawingml/textparagraphpropertiescontext.hxx +++ b/oox/inc/drawingml/textparagraphpropertiescontext.hxx @@ -20,41 +20,60 @@ #ifndef INCLUDED_OOX_DRAWINGML_TEXTPARAGRAPHPROPERTIESCONTEXT_HXX #define INCLUDED_OOX_DRAWINGML_TEXTPARAGRAPHPROPERTIESCONTEXT_HXX +#include #include -#include #include +#include #include #include namespace oox::drawingml { +// Numbered list state of a text body, shared by its paragraphs +struct ListNumberingState +{ + explicit ListNumberingState(const TextListStyle& rListStyle) + : mrListStyle(rListStyle) + { + } + + // Numbering type, prefix, suffix and start value of a numbered paragraph + struct Num + { + css::uno::Any maType, maPrefix, maSuffix, maStartAt; + bool operator==(const Num&) const = default; + }; + + // Numbering of the last numbered paragraph at each level, empty if we're not in a list + // there (n=0 is what pptx calls level 1) + std::array maLastNum; + // a:lstStyle of the text body: a paragraph without a bullet of its own takes it from there + const TextListStyle& mrListStyle; + + // A paragraph at nLevel with rBullet as its own bullet; returns true if its numbering restarts + bool markParagraph(int nLevel, const BulletList& rBullet); +}; + class TextParagraphPropertiesContext final : public ::oox::core::ContextHandler2 { public: TextParagraphPropertiesContext( ::oox::core::ContextHandler2Helper const & rParent, const ::oox::AttributeList& rAttributes, TextParagraphProperties& rTextParagraphProperties, - uint16_t* pListNumberingMask = nullptr); + ListNumberingState* pListNumberingState = nullptr); virtual ~TextParagraphPropertiesContext() override; virtual ::oox::core::ContextHandlerRef onCreateContext( ::sal_Int32 Element, const ::oox::AttributeList& rAttribs ) override; private: - // Returns True iff we're tracking numbered lists, and we were not already in a numbered list at this level - bool markListNumbered(); - void markListUnnumbered(); - TextParagraphProperties& mrTextParagraphProperties; BulletList& mrBulletList; std::vector< css::style::TabStop > maTabList; std::shared_ptr< BlipFillProperties > mxBlipProps; - // A pointer to an integer mask where we track the list status - // If bit 'n' is 1 then we're in a numbered list at that level - // (n=0 is what pptx calls level 1) - uint16_t* mpListNumberingMask; - bool mbHaveNum; + // Where we track the list status, if we do + ListNumberingState* mpListNumberingState; }; } diff --git a/oox/source/drawingml/textbodycontext.cxx b/oox/source/drawingml/textbodycontext.cxx index 4566dfb7cbdb..0452d8ba2a04 100644 --- a/oox/source/drawingml/textbodycontext.cxx +++ b/oox/source/drawingml/textbodycontext.cxx @@ -49,25 +49,35 @@ namespace { class TextParagraphContext : public ContextHandler2 { public: - TextParagraphContext( ContextHandler2Helper const & rParent, TextParagraph& rPara, uint16_t& rListNumberingMask ); + TextParagraphContext( ContextHandler2Helper const & rParent, TextParagraph& rPara, ListNumberingState& rListNumberingState ); virtual ContextHandlerRef onCreateContext( sal_Int32 aElementToken, const AttributeList& rAttribs ) override; + virtual void onEndElement() override; protected: TextParagraph& mrParagraph; - uint16_t& mrListNumberingMask; + ListNumberingState& mrListNumberingState; }; } -TextParagraphContext::TextParagraphContext( ContextHandler2Helper const & rParent, TextParagraph& rPara, uint16_t& rListNumberingMask ) +TextParagraphContext::TextParagraphContext( ContextHandler2Helper const & rParent, TextParagraph& rPara, ListNumberingState& rListNumberingState ) : ContextHandler2( rParent ) , mrParagraph( rPara ) -, mrListNumberingMask( rListNumberingMask ) +, mrListNumberingState( rListNumberingState ) { mbEnableTrimSpace = false; } +void TextParagraphContext::onEndElement() +{ + // A paragraph without a:pPr is at level 0, with the bullet of the list style + TextParagraphProperties& rProps = mrParagraph.getProperties(); + if (isCurrentElement(A_TOKEN(p)) && !mrParagraph.hasProperties() + && mrListNumberingState.markParagraph(0, rProps.getBulletList())) + rProps.setRestartNumbering(true); +} + ContextHandlerRef TextParagraphContext::onCreateContext( sal_Int32 aElementToken, const AttributeList& rAttribs ) { // EG_TextRun @@ -96,7 +106,7 @@ ContextHandlerRef TextParagraphContext::onCreateContext( sal_Int32 aElementToken case A_TOKEN( pPr ): case W_TOKEN( pPr ): mrParagraph.setHasProperties(); - return new TextParagraphPropertiesContext( *this, rAttribs, mrParagraph.getProperties(), &mrListNumberingMask ); + return new TextParagraphPropertiesContext( *this, rAttribs, mrParagraph.getProperties(), &mrListNumberingState ); case A_TOKEN( endParaRPr ): return new TextCharacterPropertiesContext( *this, rAttribs, mrParagraph.getEndProperties() ); case W_TOKEN( sdt ): @@ -180,7 +190,7 @@ ContextHandlerRef RegularTextRunContext::onCreateContext( sal_Int32 aElementToke TextBodyContext::TextBodyContext( ContextHandler2Helper const & rParent, TextBody& rTextBody ) : ContextHandler2( rParent ) , mrTextBody( rTextBody ) -, mListNumberingMask(0) +, maListNumberingState( rTextBody.getTextListStyle() ) { } @@ -205,7 +215,7 @@ ContextHandlerRef TextBodyContext::onCreateContext( sal_Int32 aElementToken, con return new TextListStyleContext( *this, mrTextBody.getTextListStyle() ); case A_TOKEN( p ): // CT_TextParagraph case W_TOKEN( p ): - return new TextParagraphContext( *this, mrTextBody.addParagraph(), mListNumberingMask ); + return new TextParagraphContext( *this, mrTextBody.addParagraph(), maListNumberingState ); case W_TOKEN( sdt ): case W_TOKEN( sdtContent ): return this; diff --git a/oox/source/drawingml/textparagraphpropertiescontext.cxx b/oox/source/drawingml/textparagraphpropertiescontext.cxx index 3f0a47488784..5c2af554aa51 100644 --- a/oox/source/drawingml/textparagraphpropertiescontext.cxx +++ b/oox/source/drawingml/textparagraphpropertiescontext.cxx @@ -19,7 +19,10 @@ #include +#include + #include +#include #include #include #include @@ -77,12 +80,11 @@ double lclGetGraphicAspectRatio( const Reference< XGraphic >& rxGraphic ) TextParagraphPropertiesContext::TextParagraphPropertiesContext( ContextHandler2Helper const & rParent, const AttributeList& rAttribs, TextParagraphProperties& rTextParagraphProperties, - uint16_t* pListNumberingMask) + ListNumberingState* pListNumberingState) : ContextHandler2( rParent ) , mrTextParagraphProperties( rTextParagraphProperties ) , mrBulletList( rTextParagraphProperties.getBulletList() ) -, mpListNumberingMask( pListNumberingMask ) -, mbHaveNum( false ) +, mpListNumberingState( pListNumberingState ) { OUString sValue; @@ -192,8 +194,12 @@ TextParagraphPropertiesContext::~TextParagraphPropertiesContext() mrBulletList.setBulletAspectRatio( lclGetGraphicAspectRatio(mxBlipProps->mxFillGraphic) ); } - if( !mbHaveNum ) - markListUnnumbered(); + // The list is tracked only for a:pPr of a paragraph. Not for a:lvl1pPr etc. of a list + // style: a restart there would be inherited by every paragraph using the style + if (mpListNumberingState + && mpListNumberingState->markParagraph(mrTextParagraphProperties.getLevel(), mrBulletList)) + mrTextParagraphProperties.setRestartNumbering(true); + if( mrBulletList.is() ) rPropertyMap.setProperty( PROP_IsNumbering, true); sal_Int16 nLevel = mrTextParagraphProperties.getLevel(); @@ -204,50 +210,30 @@ TextParagraphPropertiesContext::~TextParagraphPropertiesContext() rPropertyMap.setProperty( PROP_ParaAdjust, *mrTextParagraphProperties.getParaAdjust()); } -bool TextParagraphPropertiesContext::markListNumbered() +bool ListNumberingState::markParagraph(int nLevel, const BulletList& rBullet) { - sal_Int16 nLevel = mrTextParagraphProperties.getLevel(); - - // We only track list state in some situations - if (mpListNumberingMask == nullptr) + const BulletList& rNumBullet + = rBullet.is() ? rBullet : mrListStyle.getListStyle()[nLevel].getBulletList(); + sal_Int16 nType = NumberingType::NUMBER_NONE; + rNumBullet.mnNumberingType >>= nType; + if (nType == NumberingType::NUMBER_NONE || nType == NumberingType::CHAR_SPECIAL + || nType == NumberingType::BITMAP) { + // Lists at our level and deeper end + std::fill(maLastNum.begin() + nLevel, maLastNum.end(), Num()); return false; } - uint16_t nOldMask = *mpListNumberingMask; - // The bit that represents this level (..0001000) - uint16_t nOurBit = static_cast(1) << nLevel; + // Lists at deeper levels end + std::fill(maLastNum.begin() + nLevel + 1, maLastNum.end(), Num()); - uint16_t nTmp = nOldMask; - // Clears all bits at our level and lower (..0000xxx) - nTmp &= nOurBit - 1; - // and put our bit in (..0001xxx) - nTmp |= nOurBit; - - *mpListNumberingMask = nTmp; - - return (nOldMask & nOurBit) == 0; -} - -void TextParagraphPropertiesContext::markListUnnumbered() -{ - sal_Int16 nLevel = mrTextParagraphProperties.getLevel(); - - // We only track list state in some situations - if (mpListNumberingMask == nullptr) - { - return; - } - - uint16_t nOldMask = *mpListNumberingMask; - // The bit that represents this level (..0001000) - uint16_t nOurBit = static_cast(1) << nLevel; - - uint16_t nTmp = nOldMask; - // Clears all bits at our level and lower (..0000xxx) - nTmp &= nOurBit - 1; - - *mpListNumberingMask = nTmp; + // A list continues while its numbering stays the same at this level, + // a missing startAt being the same as startAt="1" + Num aNum{ rNumBullet.mnNumberingType, rNumBullet.msNumberingPrefix, + rNumBullet.msNumberingSuffix, rNumBullet.mnStartAt }; + bool bRestart = maLastNum[nLevel] != aNum; + maLastNum[nLevel] = aNum; + return bRestart; } ContextHandlerRef TextParagraphPropertiesContext::onCreateContext( sal_Int32 aElementToken, const AttributeList& rAttribs ) @@ -292,16 +278,9 @@ ContextHandlerRef TextParagraphPropertiesContext::onCreateContext( sal_Int32 aEl break; case A_TOKEN( buAutoNum ): // CT_TextAutonumberBullet { - bool bStartingNumList = markListNumbered(); - mbHaveNum = true; - try { sal_Int32 nType = rAttribs.getToken( XML_type, 0 ); sal_Int32 nStartAt = rAttribs.getInteger( XML_startAt, -1 ); - if( nStartAt >= 0 || bStartingNumList ) - { - mrTextParagraphProperties.setRestartNumbering(true); - } if( nStartAt > 32767 ) {