tdf160017: make sure to emit the closing tags in correct order

This reimplements how the starts and ends of attributes are stored in
HTMLEndPosLst. Instead of a plain list, now it is a sorted map, with
positions as keys, and a vector of HTMLStartEndPos* as values.

In commit b94b1fe936ddc4a9b86fbeb9c9c6ab0fca52f0bc (CharBrd 9.1: HTML
filters, 2013-09-08), the character borders attributes started to be
set in a special order, in front of the position's other attributes,
to allow merging them. But that created a problem of knowing in which
order to close respective tags.

The change here sorts the closing tags for the current node only when
writing them. At this point, it is possible to consider the opening
positions correctly.

Change-Id: I466ffa1c0eb28874ded003035e0cf772e31585b3
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/164325
Tested-by: Jenkins
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
diff --git a/sw/qa/extras/htmlexport/data/char_border_and_font_color.fodt b/sw/qa/extras/htmlexport/data/char_border_and_font_color.fodt
new file mode 100644
index 0000000..bda2ec6
--- /dev/null
+++ b/sw/qa/extras/htmlexport/data/char_border_and_font_color.fodt
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>

<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
 <office:automatic-styles>
  <style:style style:name="P1" style:family="paragraph">
   <style:text-properties fo:color="#000000" loext:border="none"/>
  </style:style>
 </office:automatic-styles>
 <office:body>
  <office:text>
   <text:p text:style-name="P1">foo</text:p>
  </office:text>
 </office:body>
</office:document>
\ No newline at end of file
diff --git a/sw/qa/extras/htmlexport/htmlexport.cxx b/sw/qa/extras/htmlexport/htmlexport.cxx
index fcb6092..45eb061 100644
--- a/sw/qa/extras/htmlexport/htmlexport.cxx
+++ b/sw/qa/extras/htmlexport/htmlexport.cxx
@@ -3017,6 +3017,27 @@ CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_NoBrClearForImageWrap)
        0);
}

CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testReqIF_Tdf160017_spanClosingOrder)
{
    // Given a document with a paragraph having explicit font color and character border properties:
    createSwDoc("char_border_and_font_color.fodt");
    // When exporting to reqif:
    ExportToReqif();
    // Without the fix, this would fail, because there was an extra closing </reqif-xhtml:span>
    WrapReqifFromTempFile();
}

CPPUNIT_TEST_FIXTURE(SwHtmlDomExportTest, testHTML_Tdf160017_spanClosingOrder)
{
    // Given a document with a paragraph having explicit font color and character border properties:
    createSwDoc("char_border_and_font_color.fodt");
    // When exporting to HTML:
    ExportToHTML();
    // Parse it as XML (strict!)
    // Without the fix, this would fail, because span and font elements closed in wrong order
    CPPUNIT_ASSERT(parseXml(maTempFile));
}

} // end of anonymous namespace
CPPUNIT_PLUGIN_IMPLEMENT();

diff --git a/sw/source/filter/html/css1atr.cxx b/sw/source/filter/html/css1atr.cxx
index a07b0c9..48badab 100644
--- a/sw/source/filter/html/css1atr.cxx
+++ b/sw/source/filter/html/css1atr.cxx
@@ -3301,14 +3301,16 @@ SwHTMLWriter& OutCSS1_SvxBox( SwHTMLWriter& rWrt, const SfxPoolItem& rHt )

    if( rHt.Which() == RES_CHRATR_BOX )
    {
        constexpr std::string_view inline_block("inline-block");
        if( rWrt.m_bTagOn )
        {
            // Inline-block to make the line height changing correspond to the character border
            rWrt.OutCSS1_PropertyAscii(sCSS1_P_display, "inline-block");
            rWrt.OutCSS1_PropertyAscii(sCSS1_P_display, inline_block);
        }
        else
        {
            HTMLOutFuncs::Out_AsciiTag( rWrt.Strm(), Concat2View(rWrt.GetNamespace() + OOO_STRING_SVTOOLS_HTML_span), false );
            if (!IgnorePropertyForReqIF(rWrt.mbReqIF, sCSS1_P_display, inline_block))
                HTMLOutFuncs::Out_AsciiTag( rWrt.Strm(), Concat2View(rWrt.GetNamespace() + OOO_STRING_SVTOOLS_HTML_span), false );
            return rWrt;
        }
    }
diff --git a/sw/source/filter/html/htmlatr.cxx b/sw/source/filter/html/htmlatr.cxx
index 9f67d1e..c880082 100644
--- a/sw/source/filter/html/htmlatr.cxx
+++ b/sw/source/filter/html/htmlatr.cxx
@@ -1058,7 +1058,7 @@ public:

    HTMLStartEndPos( const SfxPoolItem& rItem, sal_Int32 nStt, sal_Int32 nE );

    const SfxPoolItem* GetItem() const { return m_pItem.get(); }
    const SfxPoolItem& GetItem() const { return *m_pItem; }

    void SetStart(sal_Int32 nStt) { m_nStart = nStt; }
    sal_Int32 GetStart() const { return m_nStart; }
@@ -1075,7 +1075,7 @@ HTMLStartEndPos::HTMLStartEndPos(const SfxPoolItem& rItem, sal_Int32 nStt, sal_I
    , m_pItem(rItem.Clone())
{}

typedef std::vector<HTMLStartEndPos *> HTMLStartEndPositions;
typedef std::map<sal_Int32, std::vector<HTMLStartEndPos*>> HTMLStartEndPositions;

namespace {

@@ -1091,8 +1091,8 @@ enum HTMLOnOffState { HTML_NOT_SUPPORTED,   // unsupported Attribute

class HTMLEndPosLst
{
    HTMLStartEndPositions m_aStartLst; // list, sorted for start positions
    HTMLStartEndPositions m_aEndLst; // list, sorted for end positions
    HTMLStartEndPositions m_aStartLst; // list, each position's elements sorted by appearance order
    HTMLStartEndPositions m_aEndLst; // list, no sort of elements in position
    std::deque<sal_Int32> m_aScriptChgLst; // positions where script changes
        // 0 is not contained in this list,
        // but the text length
@@ -1110,8 +1110,7 @@ class HTMLEndPosLst

    // Insert/remove a SttEndPos in/from the Start and End lists.
    // The end position is known.
    void InsertItem_( HTMLStartEndPos *pPos, HTMLStartEndPositions::size_type nEndPos );
    void RemoveItem_( HTMLStartEndPositions::size_type nEndPos );
    void InsertItem_(HTMLStartEndPos* pPos);

    // determine the 'type' of the attribute
    HTMLOnOffState GetHTMLItemState( const SfxPoolItem& rItem );
@@ -1125,8 +1124,7 @@ class HTMLEndPosLst
                                          sal_Int32 nEndPos );

    // adapt the end of a split item
    void FixSplittedItem( HTMLStartEndPos *pPos, sal_Int32 nNewEnd,
                            HTMLStartEndPositions::size_type nStartPos );
    void FixSplittedItem(HTMLStartEndPos* pPos, sal_Int32 nNewEnd);

    // insert an attribute in the lists and, if necessary, split it
    void InsertItem( const SfxPoolItem& rItem, sal_Int32 nStart,
@@ -1144,6 +1142,8 @@ class HTMLEndPosLst
    const SwHTMLFormatInfo *GetFormatInfo( const SwFormat& rFormat,
                                     SwHTMLFormatInfos& rFormatInfos );

    void OutEndAttrs(SwHTMLWriter& rWrt, std::vector<HTMLStartEndPos*>& posItems);

public:

    HTMLEndPosLst( SwDoc *pDoc, SwDoc* pTemplate, std::optional<Color> xDfltColor,
@@ -1169,36 +1169,46 @@ public:
    bool IsHTMLMode(sal_uLong nMode) const { return (m_nHTMLMode & nMode) != 0; }
};

struct SortEnds
{
    HTMLStartEndPositions& m_startList;
    SortEnds(HTMLStartEndPositions& startList) : m_startList(startList) {}
    bool operator()(const HTMLStartEndPos* p1, const HTMLStartEndPos* p2)
    {
        // if p1 start after p2, then it ends before
        if (p1->GetStart() > p2->GetStart())
            return true;
        if (p1->GetStart() < p2->GetStart())
            return false;
        for (const auto p : m_startList[p1->GetStart()])
        {
            if (p == p1)
                return false;
            if (p == p2)
                return true;
        }
        assert(!"Neither p1 nor p2 found in their start list");
        return false;
    }
};

#ifndef NDEBUG
bool IsEmpty(const HTMLStartEndPositions& l)
{
    return std::find_if(l.begin(), l.end(), [](auto& i) { return !i.second.empty(); }) == l.end();
}
#endif

}

void HTMLEndPosLst::InsertItem_( HTMLStartEndPos *pPos, HTMLStartEndPositions::size_type nEndPos )
void HTMLEndPosLst::InsertItem_(HTMLStartEndPos* pPos)
{
    // Insert the attribute in the Start list behind all attributes that
    // were started before, or at the same position.
    sal_Int32 nStart = pPos->GetStart();
    HTMLStartEndPositions::size_type i {0};
    // Character border attribute must be the first which is written out because of border merge.
    auto& posItems1 = m_aStartLst[pPos->GetStart()];
    auto it = pPos->GetItem().Which() == RES_CHRATR_BOX ? posItems1.begin() : posItems1.end();
    posItems1.insert(it, pPos);

    while (i < m_aStartLst.size() && m_aStartLst[i]->GetStart() <= nStart)
        ++i;
    m_aStartLst.insert(m_aStartLst.begin() + i, pPos);

    // the position in the End list was supplied
    m_aEndLst.insert(m_aEndLst.begin() + nEndPos, pPos);
}

void HTMLEndPosLst::RemoveItem_( HTMLStartEndPositions::size_type nEndPos )
{
    HTMLStartEndPos* pPos = m_aEndLst[nEndPos];

    // now, we are looking for it in the Start list
    HTMLStartEndPositions::iterator it = std::find(m_aStartLst.begin(), m_aStartLst.end(), pPos);
    OSL_ENSURE(it != m_aStartLst.end(), "Item not found in Start List!");
    if (it != m_aStartLst.end())
        m_aStartLst.erase(it);

    m_aEndLst.erase(m_aEndLst.begin() + nEndPos);

    delete pPos;
    m_aEndLst[pPos->GetEnd()].push_back(pPos);
}

HTMLOnOffState HTMLEndPosLst::GetHTMLItemState( const SfxPoolItem& rItem )
@@ -1352,23 +1362,25 @@ HTMLOnOffState HTMLEndPosLst::GetHTMLItemState( const SfxPoolItem& rItem )

bool HTMLEndPosLst::ExistsOnTagItem( sal_uInt16 nWhich, sal_Int32 nPos )
{
    for (auto pTest : m_aStartLst)
    for (const auto& [startPos, items] : m_aStartLst)
    {
        if( pTest->GetStart() > nPos )
        if (startPos > nPos)
        {
            // this attribute, and all attributes that follow, start later
            break;
        }
        else if( pTest->GetEnd() > nPos )

        for (const auto* pTest : items)
        {
            // the attribute starts before, or at, the current position and
            // ends after it
            const SfxPoolItem *pItem = pTest->GetItem();
            if( pItem->Which() == nWhich &&
                HTML_ON_VALUE == GetHTMLItemState(*pItem) )
            if (pTest->GetEnd() > nPos)
            {
                // an OnTag attribute was found
                return true;
                // the attribute starts before, or at, the current position and ends after it
                const SfxPoolItem& rItem = pTest->GetItem();
                if (rItem.Which() == nWhich && HTML_ON_VALUE == GetHTMLItemState(rItem))
                {
                    // an OnTag attribute was found
                    return true;
                }
            }
        }
    }
@@ -1386,24 +1398,17 @@ bool HTMLEndPosLst::ExistsOffTagItem( sal_uInt16 nWhich, sal_Int32 nStartPos,
        return false;
    }

    for (auto pTest : m_aStartLst)
    for (const auto* pTest : m_aStartLst[nStartPos])
    {
        if( pTest->GetStart() > nStartPos )
        if (pTest->GetEnd() == nEndPos)
        {
            // this attribute, and all attributes that follow, start later
            break;
        }
        else if( pTest->GetStart()==nStartPos &&
                 pTest->GetEnd()==nEndPos )
        {
            // the attribute starts before or at the current position and
            // ends after it
            const SfxPoolItem *pItem = pTest->GetItem();
            sal_uInt16 nTstWhich = pItem->Which();
            // the attribute starts before or at the current position and ends after it
            const SfxPoolItem& rItem = pTest->GetItem();
            sal_uInt16 nTstWhich = rItem.Which();
            if( (nTstWhich == RES_CHRATR_CROSSEDOUT ||
                 nTstWhich == RES_CHRATR_UNDERLINE ||
                 nTstWhich == RES_CHRATR_BLINK) &&
                HTML_OFF_VALUE == GetHTMLItemState(*pItem) )
                HTML_OFF_VALUE == GetHTMLItemState(rItem) )
            {
                // an OffTag attribute was found that is exported the same
                // way as the current item
@@ -1415,55 +1420,51 @@ bool HTMLEndPosLst::ExistsOffTagItem( sal_uInt16 nWhich, sal_Int32 nStartPos,
    return false;
}

void HTMLEndPosLst::FixSplittedItem( HTMLStartEndPos *pPos, sal_Int32 nNewEnd,
                                        HTMLStartEndPositions::size_type nStartPos )
void HTMLEndPosLst::FixSplittedItem(HTMLStartEndPos* pPos, sal_Int32 nNewEnd)
{
    // remove the item from the End list
    std::erase(m_aEndLst[pPos->GetEnd()], pPos);
    // fix the end position accordingly
    pPos->SetEnd( nNewEnd );

    // remove the item from the End list
    HTMLStartEndPositions::iterator it = std::find(m_aEndLst.begin(), m_aEndLst.end(), pPos);
    OSL_ENSURE(it != m_aEndLst.end(), "Item not found in End List!");
    if (it != m_aEndLst.end())
        m_aEndLst.erase(it);

    // from now on, it is closed as the last one at the corresponding position
    HTMLStartEndPositions::size_type nEndPos {0};
    while (nEndPos < m_aEndLst.size() && m_aEndLst[nEndPos]->GetEnd() <= nNewEnd)
        ++nEndPos;
    m_aEndLst.insert(m_aEndLst.begin() + nEndPos, pPos);
    // from now on, it is closed at the corresponding position
    m_aEndLst[nNewEnd].push_back(pPos);

    // now, adjust the attributes that got started afterwards
    for (HTMLStartEndPositions::size_type i = nStartPos + 1; i < m_aStartLst.size(); ++i)
    const sal_Int32 nPos = pPos->GetStart();
    for (const auto& [startPos, items] : m_aStartLst)
    {
        HTMLStartEndPos* pTest = m_aStartLst[i];
        sal_Int32 nTestEnd = pTest->GetEnd();
        if( pTest->GetStart() >= nNewEnd )
        {
            // the Test attribute and all the following ones start, after the
            // split attribute ends
        if (startPos < nPos)
            continue;

        if (startPos >= nNewEnd)
            break;
        }
        else if( nTestEnd > nNewEnd )

        auto it = items.begin();
        if (startPos == nPos)
        {
            it = std::find(items.begin(), items.end(), pPos);
            if (it != items.end())
                ++it;
        }
        for (; it != items.end(); ++it)
        {
            HTMLStartEndPos* pTest = *it;
            const sal_Int32 nTestEnd = pTest->GetEnd();
            if (nTestEnd <= nNewEnd)
                continue;

            // the Test attribute starts before the split attribute
            // ends, and ends afterwards, i.e., it must be split, as well

            // remove the attribute from the End list
            std::erase(m_aEndLst[pTest->GetEnd()], pTest);
            // set the new end
            pTest->SetEnd( nNewEnd );

            // remove the attribute from the End list
            it = std::find(m_aEndLst.begin(), m_aEndLst.end(), pTest);
            OSL_ENSURE(it != m_aEndLst.end(), "Item not found in End List!");
            if (it != m_aEndLst.end())
                m_aEndLst.erase(it);

            // it now ends as the first attribute in the respective position.
            // We already know this position in the End list.
            m_aEndLst.insert(m_aEndLst.begin() + nEndPos, pTest);
            // it now ends in the respective position.
            m_aEndLst[nNewEnd].push_back(pTest);

            // insert the 'rest' of the attribute
            InsertItem( *pTest->GetItem(), nNewEnd, nTestEnd );
            InsertItem( pTest->GetItem(), nNewEnd, nTestEnd );
        }
    }
}
@@ -1471,36 +1472,38 @@ void HTMLEndPosLst::FixSplittedItem( HTMLStartEndPos *pPos, sal_Int32 nNewEnd,
void HTMLEndPosLst::InsertItem( const SfxPoolItem& rItem, sal_Int32 nStart,
                                                          sal_Int32 nEnd )
{
    HTMLStartEndPositions::size_type i;
    for (i = 0; i < m_aEndLst.size(); i++)
    assert(nStart < nEnd);

    for (auto& [endPos, items] : m_aEndLst)
    {
        HTMLStartEndPos* pTest = m_aEndLst[i];
        sal_Int32 nTestEnd = pTest->GetEnd();
        if( nTestEnd <= nStart )
        if (endPos <= nStart)
        {
            // the Test attribute ends, before the new one starts
            continue;
        }
        else if( nTestEnd < nEnd )
        {
            if( pTest->GetStart() < nStart )
            {
                // the Test attribute ends, before the new one ends. Thus, the
                // new attribute must be split.
                InsertItem_( new HTMLStartEndPos( rItem, nStart, nTestEnd ), i );
                nStart = nTestEnd;
            }
        }
        else
        if (endPos >= nEnd)
        {
            // the Test attribute (and all that follow) ends, before the new
            // one ends
            break;
        }

        std::sort(items.begin(), items.end(), SortEnds(m_aStartLst));

        for (HTMLStartEndPos* pTest : items)
        {
            if( pTest->GetStart() < nStart )
            {
                // the Test attribute ends, before the new one ends. Thus, the
                // new attribute must be split.
                InsertItem_(new HTMLStartEndPos(rItem, nStart, endPos));
                nStart = endPos;
            }
        }
    }

    // one attribute must still be inserted
    InsertItem_( new HTMLStartEndPos( rItem, nStart, nEnd ), i );
    InsertItem_(new HTMLStartEndPos(rItem, nStart, nEnd));
}

void HTMLEndPosLst::SplitItem( const SfxPoolItem& rItem, sal_Int32 nStart,
@@ -1511,59 +1514,47 @@ void HTMLEndPosLst::SplitItem( const SfxPoolItem& rItem, sal_Int32 nStart,
    // first, we must search for the old items by using the start list and
    // determine the new item range

    for (HTMLStartEndPositions::size_type i = 0; i < m_aStartLst.size(); ++i)
    for (auto& [nTestStart, items] : m_aStartLst)
    {
        HTMLStartEndPos* pTest = m_aStartLst[i];
        sal_Int32 nTestStart = pTest->GetStart();
        sal_Int32 nTestEnd = pTest->GetEnd();

        if( nTestStart >= nEnd )
        {
            // this attribute, and all that follow, start later
            break;
        }
        else if( nTestEnd > nStart )

        for (auto it = items.begin(); it != items.end();)
        {
            HTMLStartEndPos* pTest = *it;
            sal_Int32 nTestEnd = pTest->GetEnd();
            if (nTestEnd <= nStart)
                continue;

            // the Test attribute ends in the range that must be deleted
            const SfxPoolItem *pItem = pTest->GetItem();
            const SfxPoolItem& rTestItem = pTest->GetItem();

            // only the corresponding OnTag attributes have to be considered
            if( pItem->Which() == nWhich &&
                HTML_ON_VALUE == GetHTMLItemState( *pItem ) )
            if (rTestItem.Which() == nWhich && HTML_ON_VALUE == GetHTMLItemState(rTestItem))
            {
                bool bDelete = true;
                // if necessary, insert the second part of the split
                // attribute
                if (nTestEnd > nEnd)
                    InsertItem(pTest->GetItem(), nEnd, nTestEnd);

                if( nTestStart < nStart )
                {
                    // the start of the new attribute corresponds to the new
                    // end of the attribute
                    FixSplittedItem( pTest, nStart, i );
                    bDelete = false;
                }
                else
                if (nTestStart >= nStart)
                {
                    // the Test item only starts after the new end of the
                    // attribute. Therefore, it can be completely erased.
                    m_aStartLst.erase(m_aStartLst.begin() + i);
                    i--;

                    HTMLStartEndPositions::iterator it
                        = std::find(m_aEndLst.begin(), m_aEndLst.end(), pTest);
                    OSL_ENSURE(it != m_aEndLst.end(), "Item not found in End List!");
                    if (it != m_aEndLst.end())
                        m_aEndLst.erase(it);
                }

                // if necessary, insert the second part of the split
                // attribute
                if( nTestEnd > nEnd )
                {
                    InsertItem( *pTest->GetItem(), nEnd, nTestEnd );
                }

                if( bDelete )
                    it = items.erase(it);
                    std::erase(m_aEndLst[pTest->GetEnd()], pTest);
                    delete pTest;
                    continue;
                }

                // the start of the new attribute corresponds to the new
                // end of the attribute
                FixSplittedItem(pTest, nStart);
            }
            ++it;
        }
    }
}
@@ -1611,8 +1602,8 @@ HTMLEndPosLst::HTMLEndPosLst(SwDoc* pD, SwDoc* pTempl, std::optional<Color> xDfl

HTMLEndPosLst::~HTMLEndPosLst()
{
    OSL_ENSURE(m_aStartLst.empty(), "Start List not empty in destructor");
    OSL_ENSURE(m_aEndLst.empty(), "End List not empty in destructor");
    assert(IsEmpty(m_aStartLst) && "Start List not empty in destructor");
    assert(IsEmpty(m_aEndLst) && "End List not empty in destructor");
}

void HTMLEndPosLst::InsertNoScript( const SfxPoolItem& rItem,
@@ -1900,53 +1891,25 @@ void HTMLEndPosLst::OutStartAttrs( SwHTMLWriter& rWrt, sal_Int32 nPos )
{
    rWrt.m_bTagOn = true;

    // Character border attribute must be the first which is written out
    // because of border merge.
    HTMLStartEndPositions::size_type nCharBoxIndex = 0;
    while (nCharBoxIndex < m_aStartLst.size()
           && m_aStartLst[nCharBoxIndex]->GetItem()->Which() != RES_CHRATR_BOX)
    {
        ++nCharBoxIndex;
    }

    auto it = m_aStartLst.find(nPos);
    if (it == m_aStartLst.end())
        return;
    // the attributes of the start list are sorted in ascending order
    for (HTMLStartEndPositions::size_type i = 0; i < m_aStartLst.size(); ++i)
    for (HTMLStartEndPos* pPos : it->second)
    {
        HTMLStartEndPos *pPos = nullptr;
        if (nCharBoxIndex < m_aStartLst.size())
        // output the attribute
        sal_uInt16 nCSS1Script = rWrt.m_nCSS1Script;
        sal_uInt16 nWhich = pPos->GetItem().Which();
        if( RES_TXTATR_CHARFMT == nWhich ||
            RES_TXTATR_INETFMT == nWhich ||
             RES_PARATR_DROP == nWhich )
        {
            if( i == 0 )
                pPos = m_aStartLst[nCharBoxIndex];
            else if( i == nCharBoxIndex )
                pPos = m_aStartLst[0];
            else
                pPos = m_aStartLst[i];
            rWrt.m_nCSS1Script = GetScriptAtPos( nPos, nCSS1Script );
        }
        else
            pPos = m_aStartLst[i];

        sal_Int32 nStart = pPos->GetStart();
        if( nStart > nPos )
        {
            // this attribute, and all that follow, will be opened later on
            break;
        }
        else if( nStart == nPos )
        {
            // output the attribute
            sal_uInt16 nCSS1Script = rWrt.m_nCSS1Script;
            sal_uInt16 nWhich = pPos->GetItem()->Which();
            if( RES_TXTATR_CHARFMT == nWhich ||
                RES_TXTATR_INETFMT == nWhich ||
                 RES_PARATR_DROP == nWhich )
            {
                rWrt.m_nCSS1Script = GetScriptAtPos( nPos, nCSS1Script );
            }
            HTMLOutFuncs::FlushToAscii( rWrt.Strm() ); // was one time only - do we still need it?
            Out( aHTMLAttrFnTab, *pPos->GetItem(), rWrt );
            rWrt.maStartedAttributes[pPos->GetItem()->Which()]++;
            rWrt.m_nCSS1Script = nCSS1Script;
        }
        HTMLOutFuncs::FlushToAscii( rWrt.Strm() ); // was one time only - do we still need it?
        Out( aHTMLAttrFnTab, pPos->GetItem(), rWrt );
        rWrt.maStartedAttributes[pPos->GetItem().Which()]++;
        rWrt.m_nCSS1Script = nCSS1Script;
    }
}

@@ -1954,59 +1917,55 @@ void HTMLEndPosLst::OutEndAttrs( SwHTMLWriter& rWrt, sal_Int32 nPos )
{
    rWrt.m_bTagOn = false;

    // the attributes in the End list are sorted in ascending order
    HTMLStartEndPositions::size_type i {0};
    while (i < m_aEndLst.size())
    if (nPos == SAL_MAX_INT32)
    {
        HTMLStartEndPos* pPos = m_aEndLst[i];
        sal_Int32 nEnd = pPos->GetEnd();
        for (auto& element : m_aEndLst)
            OutEndAttrs(rWrt, element.second);
    }
    else
    {
        auto it = m_aEndLst.find(nPos);
        if (it != m_aEndLst.end())
            OutEndAttrs(rWrt, it->second);
    }
}

        if( SAL_MAX_INT32 == nPos || nEnd == nPos )
void HTMLEndPosLst::OutEndAttrs(SwHTMLWriter& rWrt, std::vector<HTMLStartEndPos*>& posItems)
{
    std::sort(posItems.begin(), posItems.end(), SortEnds(m_aStartLst));
    for (auto it = posItems.begin(); it != posItems.end(); it = posItems.erase(it))
    {
        HTMLStartEndPos* pPos = *it;
        HTMLOutFuncs::FlushToAscii( rWrt.Strm() ); // was one time only - do we still need it?
        // Skip closing span if next character span has the same border (border merge)
        bool bSkipOut = false;
        if( pPos->GetItem().Which() == RES_CHRATR_BOX )
        {
            HTMLOutFuncs::FlushToAscii( rWrt.Strm() ); // was one time only - do we still need it?
            // Skip closing span if next character span has the same border (border merge)
            bool bSkipOut = false;
            if( pPos->GetItem()->Which() == RES_CHRATR_BOX )
            auto& startPosItems = m_aStartLst[pPos->GetEnd()];
            for (auto it2 = startPosItems.begin(); it2 != startPosItems.end(); ++it2)
            {
                HTMLStartEndPositions::iterator it
                    = std::find(m_aStartLst.begin(), m_aStartLst.end(), pPos);
                OSL_ENSURE(it != m_aStartLst.end(), "Item not found in Start List!");
                if (it != m_aStartLst.end())
                    ++it;
                while (it != m_aStartLst.end())
                HTMLStartEndPos* pEndPos = *it2;
                if( pEndPos->GetItem().Which() == RES_CHRATR_BOX &&
                    static_cast<const SvxBoxItem&>(pEndPos->GetItem()) ==
                    static_cast<const SvxBoxItem&>(pPos->GetItem()) )
                {
                    HTMLStartEndPos *pEndPos = *it;
                    if( pEndPos->GetItem()->Which() == RES_CHRATR_BOX &&
                        *static_cast<const SvxBoxItem*>(pEndPos->GetItem()) ==
                        *static_cast<const SvxBoxItem*>(pPos->GetItem()) )
                    {
                        pEndPos->SetStart(pPos->GetStart());
                        bSkipOut = true;
                        break;
                    }
                    ++it;
                    startPosItems.erase(it2);
                    pEndPos->SetStart(pPos->GetStart());
                    auto& oldStartPosItems = m_aStartLst[pEndPos->GetStart()];
                    oldStartPosItems.insert(oldStartPosItems.begin(), pEndPos);
                    bSkipOut = true;
                    break;
                }
            }
            if( !bSkipOut )
            {
                Out( aHTMLAttrFnTab, *pPos->GetItem(), rWrt );
                rWrt.maStartedAttributes[pPos->GetItem()->Which()]--;
            }
            RemoveItem_( i );
        }
        else if( nEnd > nPos )
        if( !bSkipOut )
        {
            // this attribute, and all that follow, are closed later on
            break;
            Out( aHTMLAttrFnTab, pPos->GetItem(), rWrt );
            rWrt.maStartedAttributes[pPos->GetItem().Which()]--;
        }
        else
        {
            // The attribute is closed before the current position. This
            // is not allowed, but we can handle it anyway.
            OSL_ENSURE( nEnd >= nPos,
                    "The attribute should've been closed a long time ago" );
            i++;
        }

        std::erase(m_aStartLst[pPos->GetStart()], pPos);
        delete pPos;
    }
}