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 <freedesktop@treblig.org>
Tested-by: Jenkins
This commit is contained in:
Dmytro Kulyk
2026-10-05 22:02:45 +02:00
committed by David Gilbert
parent 1440fb4cb8
commit 8f242675c1
5 changed files with 79 additions and 71 deletions
+2 -3
View File
@@ -20,9 +20,8 @@
#ifndef INCLUDED_OOX_DRAWINGML_TEXTBODYCONTEXT_HXX
#define INCLUDED_OOX_DRAWINGML_TEXTBODYCONTEXT_HXX
#include <stdint.h>
#include <drawingml/textbody.hxx>
#include <drawingml/textparagraphpropertiescontext.hxx>
#include <drawingml/textrun.hxx>
#include <oox/core/contexthandler2.hxx>
@@ -39,7 +38,7 @@ public:
private:
TextBody& mrTextBody;
ShapePtr mpShapePtr;
uint16_t mListNumberingMask;
ListNumberingState maListNumberingState;
};
// CT_RegularTextRun
@@ -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; }
@@ -20,41 +20,60 @@
#ifndef INCLUDED_OOX_DRAWINGML_TEXTPARAGRAPHPROPERTIESCONTEXT_HXX
#define INCLUDED_OOX_DRAWINGML_TEXTPARAGRAPHPROPERTIESCONTEXT_HXX
#include <array>
#include <vector>
#include <stdint.h>
#include <com/sun/star/style/TabStop.hpp>
#include <drawingml/textliststyle.hxx>
#include <drawingml/textparagraphproperties.hxx>
#include <oox/core/contexthandler2.hxx>
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<Num, NUM_TEXT_LIST_STYLE_ENTRIES> 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;
};
}
+17 -7
View File
@@ -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;
@@ -19,7 +19,10 @@
#include <drawingml/textparagraphpropertiescontext.hxx>
#include <algorithm>
#include <com/sun/star/text/WritingMode2.hpp>
#include <com/sun/star/style/NumberingType.hpp>
#include <com/sun/star/style/ParagraphAdjust.hpp>
#include <com/sun/star/xml/sax/SAXException.hpp>
#include <com/sun/star/graphic/XGraphic.hpp>
@@ -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<uint16_t>(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<uint16_t>(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 )
{