/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <memory>
#include <sal/config.h>

#include <comphelper/lok.hxx>
#include <comphelper/string.hxx>

#include <scitems.hxx>

#include <editeng/colritem.hxx>
#include <editeng/crossedoutitem.hxx>
#include <editeng/brushitem.hxx>
#include <editeng/editeng.hxx>
#include <editeng/fhgtitem.hxx>
#include <editeng/fontitem.hxx>
#include <editeng/postitem.hxx>
#include <editeng/udlnitem.hxx>
#include <editeng/wghtitem.hxx>
#include <editeng/borderline.hxx>
#include <editeng/boxitem.hxx>
#include <editeng/justifyitem.hxx>
#include <sal/log.hxx>
#include <sfx2/objsh.hxx>
#include <sfx2/lokhelper.hxx>
#include <svl/numformat.hxx>
#include <svl/intitem.hxx>
#include <utility>
#include <vcl/graphicfilter.hxx>
#include <svtools/parhtml.hxx>
#include <svtools/htmlkywd.hxx>
#include <svtools/htmltokn.h>

#include <vcl/outdev.hxx>
#include <vcl/svapp.hxx>
#include <tools/hostfilter.hxx>
#include <tools/urlobj.hxx>
#include <osl/diagnose.h>
#include <o3tl/string_view.hxx>

#include <rtl/tencinfo.h>

#include <attrib.hxx>
#include <htmlpars.hxx>
#include <global.hxx>
#include <document.hxx>
#include <docsh.hxx>
#include <rangelst.hxx>

#include <orcus/css_parser.hpp>
#include <boost/property_tree/json_parser.hpp>

#include <com/sun/star/document/XDocumentProperties.hpp>
#include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
#include <com/sun/star/frame/XModel.hpp>
#include <numeric>
#include <officecfg/Office/Common.hxx>

using ::editeng::SvxBorderLine;
using namespace ::com::sun::star;

namespace
{
/// data-sheets-value from google sheets, value is a JSON.
void ParseDataSheetsValue(const OUString& rDataSheetsValue, std::optional<OUString>& rVal, std::optional<OUString>& rNum)
{
    OString aEncodedOption = rDataSheetsValue.toUtf8();
    const char* pEncodedOption = aEncodedOption.getStr();
    std::stringstream aStream(pEncodedOption);
    boost::property_tree::ptree aTree;
    try
    {
        boost::property_tree::read_json(aStream, aTree);
    }
    catch (const std::exception&)
    {
        SAL_WARN("sc", "ParseDataSheetsValue: not well-formed json");
        return;
    }
    // The "1" key describes the original data type.
    auto it = aTree.find("1");
    if (it != aTree.not_found())
    {
        int nValueType = std::stoi(it->second.get_value<std::string>());
        switch (nValueType)
        {
            case 2:
            {
                // 2 is text.
                // See SfxHTMLParser::GetTableDataOptionsValNum(), we leave the parse and a number
                // language unspecified.
                rNum = ";;@";
                break;
            }
            case 3:
            {
                // 3 is number.
                it = aTree.find("3");
                if (it != aTree.not_found())
                {
                    rVal = OUString::fromUtf8(it->second.get_value<std::string>());
                }
                break;
            }
            case 4:
            {
                // 4 is boolean.
                it = aTree.find("4");
                if (it != aTree.not_found())
                {
                    rVal = OUString::fromUtf8(it->second.get_value<std::string>());
                }
                rNum = ";;BOOLEAN";
                break;
            }
        }
    }
}

/// data-sheets-numberformat from google sheets, value is a JSON.
void ParseDataSheetsNumberformat(const OUString& rDataSheetsValue, std::optional<OUString>& rNum)
{
    OString aEncodedOption = rDataSheetsValue.toUtf8();
    const char* pEncodedOption = aEncodedOption.getStr();
    std::stringstream aStream(pEncodedOption);
    boost::property_tree::ptree aTree;
    boost::property_tree::read_json(aStream, aTree);
    // The "1" key describes the other keys.
    auto it = aTree.find("1");
    if (it != aTree.not_found())
    {
        int nType = std::stoi(it->second.get_value<std::string>());
        switch (nType)
        {
            case 2:
            {
                // 2 is number format.
                it = aTree.find("2");
                if (it != aTree.not_found())
                {
                    // Leave the parse and a number language unspecified.
                    OUString aNum = ";;" + OUString::fromUtf8(it->second.get_value<std::string>());
                    rNum = aNum;
                }
                break;
            }
        }
    }
}

/// data-sheets-formula from google sheets, grammar is R1C1 reference style.
void ParseDataSheetsFormula(const OUString& rDataSheetsFormula, std::optional<OUString>& rVal,
                            std::optional<formula::FormulaGrammar::Grammar>& rGrammar)
{
    rVal = rDataSheetsFormula;
    rGrammar = formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1;
}
}

ScHTMLStyles::ScHTMLStyles() : maEmpty() {}

void ScHTMLStyles::add(const char* pElemName, size_t nElemName, const char* pClassName, size_t nClassName,
                       const OUString& aProp, const OUString& aValue)
{
    if (nElemName)
    {
        OUString aElem(pElemName, nElemName, RTL_TEXTENCODING_UTF8);
        aElem = aElem.toAsciiLowerCase();
        if (nClassName)
        {
            // Both element and class names given.
            ElemsType::iterator itrElem = m_ElemProps.find(aElem);
            if (itrElem == m_ElemProps.end())
            {
                // new element
                std::pair<ElemsType::iterator, bool> r =
                    m_ElemProps.insert(std::make_pair(aElem, NamePropsType()));
                if (!r.second)
                    // insertion failed.
                    return;
                itrElem = r.first;
            }

            NamePropsType& rClsProps = itrElem->second;
            OUString aClass(pClassName, nClassName, RTL_TEXTENCODING_UTF8);
            aClass = aClass.toAsciiLowerCase();
            insertProp(rClsProps, aClass, aProp, aValue);
        }
        else
        {
            // Element name only. Add it to the element global.
            insertProp(m_ElemGlobalProps, aElem, aProp, aValue);
        }
    }
    else
    {
        if (nClassName)
        {
            // Class name only. Add it to the global.
            OUString aClass(pClassName, nClassName, RTL_TEXTENCODING_UTF8);
            aClass = aClass.toAsciiLowerCase();
            insertProp(m_GlobalProps, aClass, aProp, aValue);
        }
    }
}

const OUString& ScHTMLStyles::getPropertyValue(
    const OUString& rElem, const OUString& rClass, const OUString& rPropName) const
{
    // First, look into the element-class storage.
    {
        auto const itr = m_ElemProps.find(rElem);
        if (itr != m_ElemProps.end())
        {
            const NamePropsType& rClasses = itr->second;
            NamePropsType::const_iterator itr2 = rClasses.find(rClass);
            if (itr2 != rClasses.end())
            {
                const PropsType& rProps = itr2->second;
                PropsType::const_iterator itr3 = rProps.find(rPropName);
                if (itr3 != rProps.end())
                    return itr3->second;
            }
        }
    }
    // Next, look into the class global storage.
    {
        auto const itr = m_GlobalProps.find(rClass);
        if (itr != m_GlobalProps.end())
        {
            const PropsType& rProps = itr->second;
            PropsType::const_iterator itr2 = rProps.find(rPropName);
            if (itr2 != rProps.end())
                return itr2->second;
        }
    }
    // As the last resort, look into the element global storage.
    {
        auto const itr = m_ElemGlobalProps.find(rClass);
        if (itr != m_ElemGlobalProps.end())
        {
            const PropsType& rProps = itr->second;
            PropsType::const_iterator itr2 = rProps.find(rPropName);
            if (itr2 != rProps.end())
                return itr2->second;
        }
    }

    return maEmpty; // nothing found.
}

void ScHTMLStyles::insertProp(
    NamePropsType& rStore, const OUString& aName,
    const OUString& aProp, const OUString& aValue)
{
    NamePropsType::iterator itr = rStore.find(aName);
    if (itr == rStore.end())
    {
        // new element
        std::pair<NamePropsType::iterator, bool> r =
            rStore.insert(std::make_pair(aName, PropsType()));
        if (!r.second)
            // insertion failed.
            return;

        itr = r.first;
    }

    PropsType& rProps = itr->second;
    rProps.emplace(aProp, aValue);
}

// BASE class for HTML parser classes

ScHTMLParser::ScHTMLParser( EditEngine* pEditEngine, ScDocument& rDoc ) :
    ScEEParser( pEditEngine ),
    mrDoc( rDoc )
{
    maFontHeights[0] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_1::get() * 20;
    maFontHeights[1] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_2::get() * 20;
    maFontHeights[2] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_3::get() * 20;
    maFontHeights[3] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_4::get() * 20;
    maFontHeights[4] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_5::get() * 20;
    maFontHeights[5] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_6::get() * 20;
    maFontHeights[6] = officecfg::Office::Common::Filter::HTML::Import::FontSize::Size_7::get() * 20;
}

ScHTMLParser::~ScHTMLParser()
{
}

ScHTMLLayoutParser::ScHTMLLayoutParser(
    EditEngine* pEditP, OUString _aBaseURL, const Size& aPageSizeP,
    ScDocument& rDoc ) :
        ScHTMLParser( pEditP, rDoc ),
        aPageSize( aPageSizeP ),
        aBaseURL(std::move( _aBaseURL )),
        xLockedList( new ScRangeList ),
        xLocalColOffset( new ScHTMLColOffset ),
        nFirstTableCell(0),
        nTableLevel(0),
        nTable(0),
        nMaxTable(0),
        nColCntStart(0),
        nMaxCol(0),
        nTableWidth(0),
        nColOffset(0),
        nColOffsetStart(0),
        nOffsetTolerance( SC_HTML_OFFSET_TOLERANCE_SMALL ),
        bFirstRow( true ),
        bTabInTabCell( false ),
        bInCell( false ),
        bInTitle( false )
{
    MakeColNoRef( xLocalColOffset.get(), 0, 0, 0, 0 );
    MakeColNoRef( &maColOffset, 0, 0, 0, 0 );
}

ScHTMLLayoutParser::~ScHTMLLayoutParser()
{
    while (!aTableStack.empty())
        aTableStack.pop();
    xLocalColOffset.reset();
    if ( pTables )
    {
        for( auto& rEntry : *pTables)
            rEntry.second.reset();
        pTables.reset();
    }
}

ErrCode ScHTMLLayoutParser::Read( SvStream& rStream, const OUString& rBaseURL )
{
    Link<HtmlImportInfo&,void> aOldLink = pEdit->GetHtmlImportHdl();
    pEdit->SetHtmlImportHdl( LINK( this, ScHTMLLayoutParser, HTMLImportHdl ) );

    ScDocShell* pObjSh = mrDoc.GetDocumentShell();
    bool bLoading = pObjSh && pObjSh->IsLoading();

    SvKeyValueIteratorRef xValues;
    SvKeyValueIterator* pAttributes = nullptr;
    if ( bLoading )
        pAttributes = pObjSh->GetHeaderAttributes();
    else
    {
        // When not loading, set up fake http headers to force the SfxHTMLParser to use UTF8
        // (used when pasting from clipboard)
        const char* pCharSet = rtl_getBestMimeCharsetFromTextEncoding( RTL_TEXTENCODING_UTF8 );
        if( pCharSet )
        {
            OUString aContentType = "text/html; charset=" +
                OUString::createFromAscii( pCharSet );

            xValues = new SvKeyValueIterator;
            xValues->Append( SvKeyValue( u"" OOO_STRING_SVTOOLS_HTML_META_content_type ""_ustr, aContentType ) );
            pAttributes = xValues.get();
        }
    }

    ErrCode nErr = pEdit->Read( rStream, rBaseURL, EETextFormat::Html, pAttributes );

    pEdit->SetHtmlImportHdl( aOldLink );
    // Create column width
    Adjust();
    OutputDevice* pDefaultDev = Application::GetDefaultDevice();
    sal_uInt16 nCount = maColOffset.size();
    sal_uLong nOff = maColOffset[0];
    Size aSize;
    for ( sal_uInt16 j = 1; j < nCount; j++ )
    {
        aSize.setWidth( maColOffset[j] - nOff );
        aSize = pDefaultDev->PixelToLogic( aSize, MapMode( MapUnit::MapTwip ) );
        maColWidths[ j-1 ] = aSize.Width();
        nOff = maColOffset[j];
    }
    return nErr;
}

const ScHTMLTable* ScHTMLLayoutParser::GetGlobalTable() const
{
    return nullptr;
}

void ScHTMLLayoutParser::NewActEntry( const ScEEParseEntry* pE )
{
    ScEEParser::NewActEntry( pE );
    if ( pE )
    {
        if ( !pE->aSel.HasRange() )
        {   // Completely empty, following text ends up in the same paragraph!
            mxActEntry->aSel.start = pE->aSel.end;
        }
    }
    mxActEntry->aSel.CollapseToStart();
}

void ScHTMLLayoutParser::EntryEnd( ScEEParseEntry* pE, const ESelection& rSel )
{
    if (rSel.end.nPara >= pE->aSel.start.nPara)
    {
        pE->aSel.end = rSel.end;
    }
    else if (rSel.start.nPara == pE->aSel.start.nPara - 1 && !pE->aSel.HasRange())
    {   // Did not attach a paragraph, but empty, do nothing
    }
    else
    {
        OSL_FAIL( "EntryEnd: EditEngine ESelection End < Start" );
    }
}

void ScHTMLLayoutParser::NextRow( const HtmlImportInfo* pInfo )
{
    if ( bInCell )
        CloseEntry( pInfo );
    if ( nRowMax < ++nRowCnt )
        nRowMax = nRowCnt;
    nColCnt = nColCntStart;
    nColOffset = nColOffsetStart;
    bFirstRow = false;
}

bool ScHTMLLayoutParser::SeekOffset( const ScHTMLColOffset* pOffset, sal_uInt16 nOffset,
        SCCOL* pCol, sal_uInt16 nOffsetTol )
{
    assert(pOffset && "ScHTMLLayoutParser::SeekOffset - illegal call");
    ScHTMLColOffset::const_iterator it = pOffset->find( nOffset );
    bool bFound = it != pOffset->end();
    size_t nPos = it - pOffset->begin();
    if (nPos > o3tl::make_unsigned(std::numeric_limits<SCCOL>::max()))
        return false;
    *pCol = static_cast<SCCOL>(nPos);
    if ( bFound )
        return true;
    sal_uInt16 nCount = pOffset->size();
    if ( !nCount )
        return false;
    // nPos is the position of insertion, that's where the next higher one is (or isn't)
    if ( nPos < nCount && (((*pOffset)[nPos] - nOffsetTol) <= nOffset) )
        return true;
    // Not smaller than everything else? Then compare with the next lower one
    else if ( nPos && (((*pOffset)[nPos-1] + nOffsetTol) >= nOffset) )
    {
        (*pCol)--;
        return true;
    }
    return false;
}

void ScHTMLLayoutParser::MakeCol( ScHTMLColOffset* pOffset, sal_uInt16& nOffset,
        sal_uInt16& nWidth, sal_uInt16 nOffsetTol, sal_uInt16 nWidthTol )
{
    assert(pOffset && "ScHTMLLayoutParser::MakeCol - illegal call");
    SCCOL nPos;
    if ( SeekOffset( pOffset, nOffset, &nPos, nOffsetTol ) )
        nOffset = static_cast<sal_uInt16>((*pOffset)[nPos]);
    else
        pOffset->insert( nOffset );
    if ( nWidth )
    {
        if ( SeekOffset( pOffset, nOffset + nWidth, &nPos, nWidthTol ) )
            nWidth = static_cast<sal_uInt16>((*pOffset)[nPos]) - nOffset;
        else
            pOffset->insert( nOffset + nWidth );
    }
}

void ScHTMLLayoutParser::MakeColNoRef( ScHTMLColOffset* pOffset, sal_uInt16 nOffset,
        sal_uInt16 nWidth, sal_uInt16 nOffsetTol, sal_uInt16 nWidthTol )
{
    assert(pOffset && "ScHTMLLayoutParser::MakeColNoRef - illegal call");
    SCCOL nPos;
    if ( SeekOffset( pOffset, nOffset, &nPos, nOffsetTol ) )
        nOffset = static_cast<sal_uInt16>((*pOffset)[nPos]);
    else
        pOffset->insert( nOffset );
    if ( nWidth )
    {
        if ( !SeekOffset( pOffset, nOffset + nWidth, &nPos, nWidthTol ) )
            pOffset->insert( nOffset + nWidth );
    }
}

void ScHTMLLayoutParser::ModifyOffset( ScHTMLColOffset* pOffset, sal_uInt16& nOldOffset,
            sal_uInt16& nNewOffset, sal_uInt16 nOffsetTol )
{
    assert(pOffset && "ScHTMLLayoutParser::ModifyOffset - illegal call");
    SCCOL nPos;
    if ( !SeekOffset( pOffset, nOldOffset, &nPos, nOffsetTol ) )
    {
        if ( SeekOffset( pOffset, nNewOffset, &nPos, nOffsetTol ) )
            nNewOffset = static_cast<sal_uInt16>((*pOffset)[nPos]);
        else
            pOffset->insert( nNewOffset );
        return ;
    }
    nOldOffset = static_cast<sal_uInt16>((*pOffset)[nPos]);
    SCCOL nPos2;
    if ( SeekOffset( pOffset, nNewOffset, &nPos2, nOffsetTol ) )
    {
        nNewOffset = static_cast<sal_uInt16>((*pOffset)[nPos2]);
        return ;
    }
    tools::Long nDiff = nNewOffset - nOldOffset;
    if ( nDiff < 0 )
    {
        do
        {
            const_cast<sal_uLong&>((*pOffset)[nPos]) += nDiff;
        } while ( nPos-- );
    }
    else
    {
        do
        {
            const_cast<sal_uLong&>((*pOffset)[nPos]) += nDiff;
        } while ( ++nPos < static_cast<sal_uInt16>(pOffset->size()) );
    }
}

void ScHTMLLayoutParser::SkipLocked( ScEEParseEntry* pE, bool bJoin )
{
    if ( !mrDoc.ValidCol(pE->nCol) )
        return;

// Or else this would create a wrong value at ScAddress (chance for an infinite loop)!
    bool bBadCol = false;
    bool bAgain;

    SCCOL nEndCol(0);
    SCROW nEndRow(0);
    bool bFail = o3tl::checked_add<SCCOL>(pE->nCol, pE->nColOverlap - 1, nEndCol) ||
                 o3tl::checked_add<SCROW>(pE->nRow, pE->nRowOverlap - 1, nEndRow);

    if (bFail || nEndRow > mrDoc.MaxRow())
    {
        SAL_WARN("sc", "invalid range: " << pE->nCol << " " << pE->nColOverlap <<
                                     " " << pE->nRow << " " << pE->nRowOverlap);
        return;
    }

    ScRange aRange(pE->nCol, pE->nRow, 0, nEndCol, nEndRow, 0);
    do
    {
        bAgain = false;
        for ( size_t i =  0, nRanges = xLockedList->size(); i < nRanges; ++i )
        {
            ScRange & rR = (*xLockedList)[i];
            if ( rR.Intersects( aRange ) )
            {
                SCCOL nTmp(0);
                bFail = o3tl::checked_add<SCCOL>(rR.aEnd.Col(), 1, pE->nCol) ||
                        o3tl::checked_add<SCCOL>(pE->nCol, pE->nRowOverlap - 1, nTmp);
                if ( bFail || pE->nCol > mrDoc.MaxCol() || nTmp > mrDoc.MaxCol() )
                    bBadCol = true;
                else
                {
                    bAgain = true;
                    aRange.aStart.SetCol( pE->nCol );
                    aRange.aEnd.SetCol( nTmp );
                }
                break;
            }
        }
    } while ( bAgain );
    if ( bJoin && !bBadCol )
        xLockedList->Join( aRange );
}

void ScHTMLLayoutParser::Adjust()
{
    xLockedList->RemoveAll();

    std::stack< std::unique_ptr<ScHTMLAdjustStackEntry> > aStack;
    sal_uInt16 nTab = 0;
    SCCOL nLastCol = SCCOL_MAX;
    SCROW nNextRow = 0;
    SCROW nCurRow = 0;
    sal_uInt16 nPageWidth = static_cast<sal_uInt16>(aPageSize.Width());
    InnerMap* pTab = nullptr;
    for (auto& pE : maList)
    {
        if ( pE->nTab < nTab )
        {   // Table finished
            if ( !aStack.empty() )
            {
                std::unique_ptr<ScHTMLAdjustStackEntry> pS = std::move(aStack.top());
                aStack.pop();

                nLastCol = pS->nLastCol;
                nNextRow = pS->nNextRow;
                nCurRow = pS->nCurRow;
            }
            nTab = pE->nTab;
            if (pTables)
            {
                OuterMap::const_iterator it = pTables->find( nTab );
                if ( it != pTables->end() )
                    pTab = it->second.get();
            }

        }
        SCROW nRow = pE->nRow;
        if ( pE->nCol <= nLastCol )
        {   // Next row
            if ( pE->nRow < nNextRow )
                pE->nRow = nCurRow = nNextRow;
            else
                nCurRow = nNextRow = pE->nRow;
            SCROW nR = 0;
            if ( pTab )
            {
                InnerMap::const_iterator it = pTab->find( nCurRow );
                if ( it != pTab->end() )
                    nR = it->second;
            }
            if ( nR )
                nNextRow += nR;
            else
                nNextRow++;
        }
        else
            pE->nRow = nCurRow;
        nLastCol = pE->nCol; // Read column
        if ( pE->nTab > nTab )
        {   // New table
            aStack.push( std::make_unique<ScHTMLAdjustStackEntry>(
                nLastCol, nNextRow, nCurRow ) );
            nTab = pE->nTab;
            if ( pTables )
            {
                OuterMap::const_iterator it = pTables->find( nTab );
                if ( it != pTables->end() )
                    pTab = it->second.get();
            }
            // New line spacing
            SCROW nR = 0;
            if ( pTab )
            {
                InnerMap::const_iterator it = pTab->find( nCurRow );
                if ( it != pTab->end() )
                    nR = it->second;
            }
            if ( nR )
                nNextRow = nCurRow + nR;
            else
                nNextRow = nCurRow + 1;
        }
        if ( nTab == 0 )
            pE->nWidth = nPageWidth;
        else
        {   // Real table, no paragraphs on the field
            if ( pTab )
            {
                SCROW nRowSpan = pE->nRowOverlap;
                for ( SCROW j=0; j < nRowSpan; j++ )
                {   // RowSpan resulting from merged rows
                    SCROW nRows = 0;
                    InnerMap::const_iterator it = pTab->find( nRow+j );
                    if ( it != pTab->end() )
                        nRows = it->second;
                    if ( nRows > 1 )
                    {
                        pE->nRowOverlap += nRows - 1;
                        if ( j == 0 )
                        {   // Merged rows move the next row
                            SCROW nTmp = nCurRow + nRows;
                            if ( nNextRow < nTmp )
                                nNextRow = nTmp;
                        }
                    }
                }
            }
        }
        // Real column
        (void)SeekOffset( &maColOffset, pE->nOffset, &pE->nCol, nOffsetTolerance );
        SCCOL nColBeforeSkip = pE->nCol;
        SkipLocked(pE.get(), false);
        if ( pE->nCol != nColBeforeSkip )
        {
            size_t nCount = maColOffset.size();
            if (pE->nCol < 0 || nCount <= o3tl::make_unsigned(pE->nCol))
            {
                pE->nOffset = static_cast<sal_uInt16>(maColOffset[nCount-1]);
                MakeCol( &maColOffset, pE->nOffset, pE->nWidth, nOffsetTolerance, nOffsetTolerance );
            }
            else
            {
                pE->nOffset = static_cast<sal_uInt16>(maColOffset[pE->nCol]);
            }
        }
        SCCOL nPos;
        if ( pE->nWidth && SeekOffset( &maColOffset, pE->nOffset + pE->nWidth, &nPos, nOffsetTolerance ) )
            pE->nColOverlap = (nPos > pE->nCol ? nPos - pE->nCol : 1);
        else
        {
        //FIXME: This may not be correct, but works anyway ...
            pE->nColOverlap = 1;
        }
        SCCOL nColTmp = o3tl::saturating_add(pE->nCol, pE->nColOverlap);
        SCROW nRowTmp = o3tl::saturating_add(pE->nRow, pE->nRowOverlap);
        xLockedList->Join(ScRange(pE->nCol, pE->nRow, 0,
                                  o3tl::saturating_sub<SCCOL>(nColTmp, 1),
                                  o3tl::saturating_sub<SCROW>(nRowTmp, 1), 0));
        // Take over MaxDimensions
        if ( nColMax < nColTmp )
            nColMax = nColTmp;
        if ( nRowMax < nRowTmp )
            nRowMax = nRowTmp;
    }
}

sal_uInt16 ScHTMLLayoutParser::GetWidth( const ScEEParseEntry* pE )
{
    if ( pE->nWidth )
        return pE->nWidth;
    sal_Int32 nTmp = std::min( static_cast<sal_Int32>( pE->nCol -
                nColCntStart + pE->nColOverlap),
            static_cast<sal_Int32>( xLocalColOffset->size() - 1));
    SCCOL nPos = (nTmp < 0 ? 0 : static_cast<SCCOL>(nTmp));
    sal_uInt16 nOff2 = static_cast<sal_uInt16>((*xLocalColOffset)[nPos]);
    if ( pE->nOffset < nOff2 )
        return nOff2 - pE->nOffset;
    return 0;
}

void ScHTMLLayoutParser::SetWidths()
{
    if ( !nTableWidth )
        nTableWidth = static_cast<sal_uInt16>(aPageSize.Width());
    SCCOL nColsPerRow = nMaxCol - nColCntStart;
    if ( nColsPerRow <= 0 )
        nColsPerRow = 1;
    if ( xLocalColOffset->size() <= 2 )
    {   // Only PageSize, there was no width setting
        sal_uInt16 nWidth = nTableWidth / static_cast<sal_uInt16>(nColsPerRow);
        sal_uInt16 nOff = nColOffsetStart;
        xLocalColOffset->clear();
        for (int nCol = 0; nCol <= nColsPerRow; ++nCol, nOff = nOff + nWidth)
        {
            MakeColNoRef( xLocalColOffset.get(), nOff, 0, 0, 0 );
        }
        nTableWidth = static_cast<sal_uInt16>(xLocalColOffset->back() - xLocalColOffset->front());
        const auto nColsAvailable = xLocalColOffset->size();
        for ( size_t i = nFirstTableCell, nListSize = maList.size(); i < nListSize; ++i )
        {
            auto& pE = maList[ i ];
            if ( pE->nTab == nTable )
            {
                const size_t nColRequested = pE->nCol - nColCntStart;
                if (nColRequested < nColsAvailable)
                    pE->nOffset = static_cast<sal_uInt16>((*xLocalColOffset)[nColRequested]);
                else
                    SAL_WARN("sc", "missing information for column: " << nColRequested);
                pE->nWidth = 0; // to be recalculated later
            }
        }
    }
    else
    {   // Some without width
        // Why actually no pE?
        if ( nFirstTableCell < maList.size() )
        {
            std::unique_ptr<sal_uInt16[]> pOffsets(new sal_uInt16[ nColsPerRow+1 ]);
            memset( pOffsets.get(), 0, (nColsPerRow+1) * sizeof(sal_uInt16) );
            std::unique_ptr<sal_uInt16[]> pWidths(new sal_uInt16[ nColsPerRow ]);
            memset( pWidths.get(), 0, nColsPerRow * sizeof(sal_uInt16) );
            pOffsets[0] = nColOffsetStart;
            for ( size_t i = nFirstTableCell, nListSize = maList.size(); i < nListSize; ++i )
            {
                auto& pE = maList[ i ];
                if ( pE->nTab == nTable && pE->nWidth )
                {
                    SCCOL nCol = pE->nCol - nColCntStart;
                    if (nCol >= 0 && nCol < nColsPerRow)
                    {
                        if ( pE->nColOverlap == 1 )
                        {
                            if ( pWidths[nCol] < pE->nWidth )
                                pWidths[nCol] = pE->nWidth;
                        }
                        else
                        {   // try to find a single undefined width
                            sal_uInt16 nTotal = 0;
                            bool bFound = false;
                            SCCOL nHere = 0;
                            SCCOL nStop = std::min( static_cast<SCCOL>(nCol + pE->nColOverlap), nColsPerRow );
                            for ( ; nCol < nStop; nCol++ )
                            {
                                if ( pWidths[nCol] )
                                    nTotal = nTotal + pWidths[nCol];
                                else
                                {
                                    if ( bFound )
                                    {
                                        bFound = false;
                                        break;  // for
                                    }
                                    bFound = true;
                                    nHere = nCol;
                                }
                            }
                            if ( bFound && pE->nWidth > nTotal )
                                pWidths[nHere] = pE->nWidth - nTotal;
                        }
                    }
                }
            }
            sal_uInt16 nWidths = 0;
            sal_uInt16 nUnknown = 0;
            for (SCCOL nCol = 0; nCol < nColsPerRow; nCol++)
            {
                if ( pWidths[nCol] )
                    nWidths = nWidths + pWidths[nCol];
                else
                    nUnknown++;
            }
            if ( nUnknown )
            {
                sal_uInt16 nW = ((nWidths < nTableWidth) ?
                    ((nTableWidth - nWidths) / nUnknown) :
                    (nTableWidth / nUnknown));
                for (SCCOL nCol = 0; nCol < nColsPerRow; nCol++)
                {
                    if ( !pWidths[nCol] )
                        pWidths[nCol] = nW;
                }
            }
            for (int nCol = 1; nCol <= nColsPerRow; nCol++)
            {
                pOffsets[nCol] = pOffsets[nCol-1] + pWidths[nCol-1];
            }
            xLocalColOffset->clear();
            for (int nCol = 0; nCol <= nColsPerRow; nCol++)
            {
                MakeColNoRef( xLocalColOffset.get(), pOffsets[nCol], 0, 0, 0 );
            }
            nTableWidth = pOffsets[nColsPerRow] - pOffsets[0];

            for ( size_t i = nFirstTableCell, nListSize = maList.size(); i < nListSize; ++i )
            {
                auto& pE = maList[ i ];
                if (pE->nTab != nTable)
                    continue;
                SCCOL nCol = pE->nCol - nColCntStart;
                OSL_ENSURE( nCol < nColsPerRow, "ScHTMLLayoutParser::SetWidths: column overflow" );
                if (nCol >= nColsPerRow)
                    continue;
                if (nCol < 0)
                {
                    SAL_WARN("sc", "negative offset: " << nCol);
                    continue;
                }
                pE->nOffset = pOffsets[nCol];
                nCol = nCol + pE->nColOverlap;
                if ( nCol > nColsPerRow )
                    nCol = nColsPerRow;
                if (nCol < 0)
                {
                    SAL_WARN("sc", "negative offset: " << nCol);
                    continue;
                }
                pE->nWidth = pOffsets[nCol] - pE->nOffset;
            }
        }
    }
    if ( !xLocalColOffset->empty() )
    {
        sal_uInt16 nMax = static_cast<sal_uInt16>(xLocalColOffset->back());
        if ( aPageSize.Width() < nMax )
            aPageSize.setWidth( nMax );
        if (nTableLevel == 0)
        {
            // Local table is very outer table, create missing offsets.
            for (auto it = xLocalColOffset->begin(); it != xLocalColOffset->end(); ++it)
            {
                // Only exact offsets, do not use MakeColNoRef().
                maColOffset.insert(*it);
            }
        }
    }
    for ( size_t i = nFirstTableCell, nListSize = maList.size(); i < nListSize; ++i )
    {
        auto& pE = maList[ i ];
        if ( pE->nTab == nTable )
        {
            if ( !pE->nWidth )
            {
                pE->nWidth = GetWidth(pE.get());
                OSL_ENSURE( pE->nWidth, "SetWidths: pE->nWidth == 0" );
            }
            MakeCol( &maColOffset, pE->nOffset, pE->nWidth, nOffsetTolerance, nOffsetTolerance );
        }
    }
}

void ScHTMLLayoutParser::Colonize( ScEEParseEntry* pE )
{
    if ( pE->nCol == SCCOL_MAX )
        pE->nCol = nColCnt;
    if ( pE->nRow == SCROW_MAX )
        pE->nRow = nRowCnt;
    SCCOL nCol = pE->nCol;
    SkipLocked( pE ); // Change of columns to the right

    if ( nCol < pE->nCol )
    {   // Replaced
        nCol = pE->nCol - nColCntStart;
        SCCOL nCount = static_cast<SCCOL>(xLocalColOffset->size());
        if (nCol >= 0 && nCol < nCount)
            nColOffset = static_cast<sal_uInt16>((*xLocalColOffset)[nCol]);
        else
            nColOffset = static_cast<sal_uInt16>((*xLocalColOffset)[nCount - 1]);
    }
    pE->nOffset = nColOffset;
    sal_uInt16 nWidth = GetWidth( pE );
    MakeCol( xLocalColOffset.get(), pE->nOffset, nWidth, nOffsetTolerance, nOffsetTolerance );
    if ( pE->nWidth )
        pE->nWidth = nWidth;
    nColOffset = pE->nOffset + nWidth;
    if ( nTableWidth < nColOffset - nColOffsetStart )
        nTableWidth = nColOffset - nColOffsetStart;
}

void ScHTMLLayoutParser::CloseEntry( const HtmlImportInfo* pInfo )
{
    bInCell = false;
    if ( bTabInTabCell )
    {   // From the stack in TableOff
        bTabInTabCell = false;
        SAL_WARN_IF(maList.empty(), "sc", "unexpected close entry without open");
        NewActEntry(maList.empty() ? nullptr : maList.back().get()); // New free flying mxActEntry
        return ;
    }
    if (mxActEntry->nTab == 0)
        mxActEntry->nWidth = static_cast<sal_uInt16>(aPageSize.Width());
    Colonize(mxActEntry.get());
    nColCnt = mxActEntry->nCol + mxActEntry->nColOverlap;
    if ( nMaxCol < nColCnt )
        nMaxCol = nColCnt;      // TableStack MaxCol
    if ( nColMax < nColCnt )
        nColMax = nColCnt;      // Global MaxCol for ScEEParser GetDimensions!
    EntryEnd(mxActEntry.get(), pInfo->aSelection);
    ESelection& rSel = mxActEntry->aSel;
    while ( rSel.start.nPara < rSel.end.nPara
            && pEdit->GetTextLen( rSel.start.nPara ) == 0 )
    {   // Strip preceding empty paragraphs
        rSel.start.nPara++;
    }
    while ( rSel.end.nIndex == 0 && rSel.end.nPara > rSel.start.nPara )
    {   // Strip successive empty paragraphs
        rSel.end.nPara--;
        rSel.end.nIndex = pEdit->GetTextLen( rSel.end.nPara );
    }
    if ( rSel.start.nPara > rSel.end.nPara )
    {   // Gives GPF in CreateTextObject
        OSL_FAIL( "CloseEntry: EditEngine ESelection Start > End" );
        rSel.end.nPara = rSel.start.nPara;
    }
    if ( rSel.HasRange() )
        mxActEntry->aItemSet.Put( ScLineBreakCell(true) );
    maList.push_back(mxActEntry);
    NewActEntry(mxActEntry.get()); // New free flying mxActEntry
}

IMPL_LINK( ScHTMLLayoutParser, HTMLImportHdl, HtmlImportInfo&, rInfo, void )
{
    switch ( rInfo.eState )
    {
        case HtmlImportState::NextToken:
            ProcToken( &rInfo );
            break;
        case HtmlImportState::Start:
            break;
        case HtmlImportState::End:
            if (rInfo.aSelection.end.nIndex)
            {
                // If text remains: create paragraph, without calling CloseEntry().
                if( bInCell )   // ...but only in opened table cells.
                {
                    bInCell = false;
                    NextRow( &rInfo );
                    bInCell = true;
                }
                CloseEntry( &rInfo );
            }
            while ( nTableLevel > 0 )
                TableOff( &rInfo );      // close tables, if </TABLE> missing
            break;
        case HtmlImportState::SetAttr:
            break;
        case HtmlImportState::InsertText:
            break;
        case HtmlImportState::InsertPara:
            if ( nTableLevel < 1 )
            {
                CloseEntry( &rInfo );
                NextRow( &rInfo );
            }
            break;
        case HtmlImportState::InsertField:
            break;
        default:
            OSL_FAIL("HTMLImportHdl: unknown ImportInfo.eState");
    }
}

void ScHTMLLayoutParser::HandleDataSheetsAttributes(const HTMLOptions& rOptions)
{
    for (const auto& rOption : rOptions)
    {
        switch (rOption.GetToken())
        {
            case HtmlOptionId::DSVAL:
            {
                ParseDataSheetsValue(rOption.GetString(), mxActEntry->pValStr, mxActEntry->pNumStr);
                break;
            }
            case HtmlOptionId::DSNUM:
            {
                ParseDataSheetsNumberformat(rOption.GetString(), mxActEntry->pNumStr);
                break;
            }
            case HtmlOptionId::DSFORMULA:
            {
                ParseDataSheetsFormula(rOption.GetString(), mxActEntry->moFormulaStr,
                                       mxActEntry->moFormulaGrammar);
                break;
            }
            default:
                break;
        }
    }
}

void ScHTMLLayoutParser::TableDataOn( HtmlImportInfo* pInfo )
{
    if ( bInCell )
        CloseEntry( pInfo );
    if ( !nTableLevel )
    {
        OSL_FAIL( "dumbo doc! <TH> or <TD> without previous <TABLE>" );
        TableOn( pInfo );
    }
    bInCell = true;
    bool bHorJustifyCenterTH = (pInfo->nToken == HtmlTokenId::TABLEHEADER_ON);
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    for (const auto & rOption : rOptions)
    {
        switch( rOption.GetToken() )
        {
            case HtmlOptionId::COLSPAN:
            {
                sal_Int32 nColOverlap = rOption.GetString().toInt32();
                if (nColOverlap >= 0 && nColOverlap <= mrDoc.MaxCol())
                    mxActEntry->nColOverlap = static_cast<SCCOL>(nColOverlap);
                else
                    SAL_WARN("sc", "ScHTMLLayoutParser::TableDataOn ignoring colspan: " << nColOverlap);
            }
            break;
            case HtmlOptionId::ROWSPAN:
            {
                sal_Int32 nRowOverlap = rOption.GetString().toInt32();
                if (nRowOverlap >= 0 && nRowOverlap <= mrDoc.MaxRow())
                    mxActEntry->nRowOverlap = static_cast<SCROW>(nRowOverlap);
                else
                    SAL_WARN("sc", "ScHTMLLayoutParser::TableDataOn ignoring rowspan: " << nRowOverlap);
                if (comphelper::IsFuzzing())
                    mxActEntry->nRowOverlap = std::min(mxActEntry->nRowOverlap, sal_Int32(1024));
            }
            break;
            case HtmlOptionId::ALIGN:
            {
                bHorJustifyCenterTH = false;
                SvxCellHorJustify eVal;
                const OUString& rOptVal = rOption.GetString();
                if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_right )  )
                    eVal = SvxCellHorJustify::Right;
                else if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_center ) )
                    eVal = SvxCellHorJustify::Center;
                else if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_left ) )
                    eVal = SvxCellHorJustify::Left;
                else
                    eVal = SvxCellHorJustify::Standard;
                if ( eVal != SvxCellHorJustify::Standard )
                    mxActEntry->aItemSet.Put(SvxHorJustifyItem(eVal, ATTR_HOR_JUSTIFY));
            }
            break;
            case HtmlOptionId::VALIGN:
            {
                SvxCellVerJustify eVal;
                const OUString& rOptVal = rOption.GetString();
                if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_top ) )
                    eVal = SvxCellVerJustify::Top;
                else if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_middle ) )
                    eVal = SvxCellVerJustify::Center;
                else if ( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_bottom ) )
                    eVal = SvxCellVerJustify::Bottom;
                else
                    eVal = SvxCellVerJustify::Standard;
                mxActEntry->aItemSet.Put(SvxVerJustifyItem(eVal, ATTR_VER_JUSTIFY));
            }
            break;
            case HtmlOptionId::WIDTH:
            {
                mxActEntry->nWidth = GetWidthPixel(rOption);
            }
            break;
            case HtmlOptionId::BGCOLOR:
            {
                Color aColor;
                rOption.GetColor( aColor );
                mxActEntry->aItemSet.Put(SvxBrushItem(aColor, ATTR_BACKGROUND));
            }
            break;
            case HtmlOptionId::SDVAL:
            {
                mxActEntry->pValStr = rOption.GetString();
            }
            break;
            case HtmlOptionId::SDNUM:
            {
                mxActEntry->pNumStr = rOption.GetString();
            }
            break;
            default: break;
        }
    }

    HandleDataSheetsAttributes(rOptions);

    mxActEntry->nCol = nColCnt;
    mxActEntry->nRow = nRowCnt;
    mxActEntry->nTab = nTable;

    if ( bHorJustifyCenterTH )
        mxActEntry->aItemSet.Put(
            SvxHorJustifyItem( SvxCellHorJustify::Center, ATTR_HOR_JUSTIFY) );
}

void ScHTMLLayoutParser::SpanOn(HtmlImportInfo* pInfo)
{
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    HandleDataSheetsAttributes(rOptions);
}

void ScHTMLLayoutParser::TableRowOn( const HtmlImportInfo* pInfo )
{
    if ( nColCnt > nColCntStart )
        NextRow( pInfo ); // The optional TableRowOff wasn't there
    nColOffset = nColOffsetStart;
}

void ScHTMLLayoutParser::TableRowOff( const HtmlImportInfo* pInfo )
{
    NextRow( pInfo );
}

void ScHTMLLayoutParser::TableDataOff( const HtmlImportInfo* pInfo )
{
    if ( bInCell )
        CloseEntry( pInfo ); // Only if it really was one
}

void ScHTMLLayoutParser::TableOn( HtmlImportInfo* pInfo )
{
    if ( ++nTableLevel > 1 )
    {   // Table in Table
        sal_uInt16 nTmpColOffset = nColOffset; // Will be changed in Colonize()
        Colonize(mxActEntry.get());
        aTableStack.push( std::make_unique<ScHTMLTableStackEntry>(
            mxActEntry, xLockedList, xLocalColOffset, nFirstTableCell,
            nRowCnt, nColCntStart, nMaxCol, nTable,
            nTableWidth, nColOffset, nColOffsetStart,
            bFirstRow ) );
        sal_uInt16 nLastWidth = nTableWidth;
        nTableWidth = GetWidth(mxActEntry.get());
        if ( nTableWidth == nLastWidth && nMaxCol - nColCntStart > 1 )
        {   // There must be more than one, so this one cannot be enough
            nTableWidth = nLastWidth / static_cast<sal_uInt16>((nMaxCol - nColCntStart));
        }
        nLastWidth = nTableWidth;
        if ( pInfo->nToken == HtmlTokenId::TABLE_ON )
        {   // It can still be TD or TH, if we didn't have a TABLE earlier
            const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
            for (const auto & rOption : rOptions)
            {
                switch( rOption.GetToken() )
                {
                    case HtmlOptionId::WIDTH:
                    {   // Percent: of document width or outer cell
                        nTableWidth = GetWidthPixel( rOption );
                    }
                    break;
                    case HtmlOptionId::BORDER:
                        // Border is: ((pOption->GetString().Len() == 0) || (pOption->GetNumber() != 0));
                    break;
                    default: break;
                }
            }
        }
        bInCell = false;
        if ( bTabInTabCell && (nTableWidth >= nLastWidth) )
        {   // Multiple tables in one cell, underneath each other
            bTabInTabCell = false;
            NextRow( pInfo );
        }
        else
        {   // It start's in this cell or next to each other
            bTabInTabCell = false;
            nColCntStart = nColCnt;
            nColOffset = nTmpColOffset;
            nColOffsetStart = nColOffset;
        }

        NewActEntry(!maList.empty() ? maList.back().get() : nullptr); // New free flying mxActEntry
        xLockedList = new ScRangeList;
    }
    else
    {   // Simple table at the document level
        EntryEnd(mxActEntry.get(), pInfo->aSelection);
        if (mxActEntry->aSel.HasRange())
        {   // Flying text left
            CloseEntry( pInfo );
            NextRow( pInfo );
        }
        aTableStack.push( std::make_unique<ScHTMLTableStackEntry>(
            mxActEntry, xLockedList, xLocalColOffset, nFirstTableCell,
            nRowCnt, nColCntStart, nMaxCol, nTable,
            nTableWidth, nColOffset, nColOffsetStart,
            bFirstRow ) );
        // As soon as we have multiple tables we need to be tolerant with the offsets.
        if (nMaxTable > 0)
            nOffsetTolerance = SC_HTML_OFFSET_TOLERANCE_LARGE;
        nTableWidth = 0;
        if ( pInfo->nToken == HtmlTokenId::TABLE_ON )
        {
            // It can still be TD or TH, if we didn't have a TABLE earlier
            const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
            for (const auto & rOption : rOptions)
            {
                switch( rOption.GetToken() )
                {
                    case HtmlOptionId::WIDTH:
                    {   // Percent: of document width or outer cell
                        nTableWidth = GetWidthPixel( rOption );
                    }
                    break;
                    case HtmlOptionId::BORDER:
                        //BorderOn is: ((pOption->GetString().Len() == 0) || (pOption->GetNumber() != 0));
                    break;
                    default: break;
                }
            }
        }
    }
    nTable = ++nMaxTable;
    bFirstRow = true;
    nFirstTableCell = maList.size();

    xLocalColOffset.reset(new ScHTMLColOffset);
    MakeColNoRef( xLocalColOffset.get(), nColOffsetStart, 0, 0, 0 );
}

void ScHTMLLayoutParser::TableOff( const HtmlImportInfo* pInfo )
{
    if ( bInCell )
        CloseEntry( pInfo );
    if ( nColCnt > nColCntStart )
        TableRowOff( pInfo ); // The optional TableRowOff wasn't
    if ( !nTableLevel )
    {
        OSL_FAIL( "dumbo doc! </TABLE> without opening <TABLE>" );
        return ;
    }
    if ( --nTableLevel > 0 )
    {   // Table in Table done
        if ( !aTableStack.empty() )
        {
            std::unique_ptr<ScHTMLTableStackEntry> pS = std::move(aTableStack.top());
            aTableStack.pop();

            auto& pE = pS->xCellEntry;
            SCROW nRows = nRowCnt - pS->nRowCnt;
            if ( nRows > 1 )
            {   // Insert size of table at this position
                SCROW nRow = pS->nRowCnt;
                sal_uInt16 nTab = pS->nTable;
                if ( !pTables )
                    pTables.reset( new OuterMap );
                // Height of outer table
                OuterMap::const_iterator it = pTables->find( nTab );
                InnerMap* pTab1;
                if ( it == pTables->end() )
                {
                    pTab1 = new InnerMap;
                    (*pTables)[ nTab ].reset(pTab1);
                }
                else
                    pTab1 = it->second.get();
                SCROW nRowSpan = pE->nRowOverlap;
                using SCUROW = std::make_unsigned_t<SCROW>;
                SCUROW nRowKGV;
                SCROW nRowsPerRow1; // Outer table
                SCROW nRowsPerRow2; // Inner table
                if ( nRowSpan > 1 )
                {   // LCM to which we can map the inner and outer rows
                    nRowKGV = std::lcm<SCUROW>(nRowSpan, nRows);
                    nRowsPerRow1 = nRowKGV / nRowSpan;
                    nRowsPerRow2 = nRowKGV / nRows;
                }
                else
                {
                    nRowKGV = nRowsPerRow1 = nRows;
                    nRowsPerRow2 = 1;
                }
                InnerMap* pTab2 = nullptr;
                if ( nRowsPerRow2 > 1 )
                {   // Height of the inner table
                    pTab2 = new InnerMap;
                    (*pTables)[ nTable ].reset(pTab2);
                }
                // Abuse void* Data entry of the Table class for height mapping
                if ( nRowKGV > 1 )
                {
                    if ( nRowsPerRow1 > 1 )
                    {   // Outer
                        for ( SCROW j=0; j < nRowSpan; j++ )
                        {
                            sal_uLong nRowKey = nRow + j;
                            SCROW nR = (*pTab1)[ nRowKey ];
                            if ( !nR )
                                (*pTab1)[ nRowKey ] = nRowsPerRow1;
                            else if ( nRowsPerRow1 > nR )
                                (*pTab1)[ nRowKey ] = nRowsPerRow1;
                            //TODO: How can we improve on this?
                            else if ( nRowsPerRow1 < nR && nRowSpan == 1
                              && nTable == nMaxTable )
                            {   // Still some space left, merge in a better way (if possible)
                                SCROW nAdd = nRowsPerRow1 - (nR % nRowsPerRow1);
                                nR += nAdd;
                                if ( (nR % nRows) == 0 )
                                {   // Only if representable
                                    SCROW nR2 = (*pTab1)[ nRowKey+1 ];
                                    if ( nR2 > nAdd )
                                    {   // Only if we really have enough space
                                        (*pTab1)[ nRowKey ] = nR;
                                        (*pTab1)[ nRowKey+1 ] = nR2 - nAdd;
                                        nRowsPerRow2 = nR / nRows;
                                    }
                                }
                            }
                        }
                    }
                    if ( nRowsPerRow2 > 1 )
                    {   // Inner
                        if ( !pTab2 )
                        {   // nRowsPerRow2 could be've been incremented
                            pTab2 = new InnerMap;
                            (*pTables)[ nTable ].reset(pTab2);
                        }
                        for ( SCROW j=0; j < nRows; j++ )
                        {
                            sal_uLong nRowKey = nRow + j;
                            (*pTab2)[ nRowKey ] = nRowsPerRow2;
                        }
                    }
                }
            }

            SetWidths();

            if ( !pE->nWidth )
                pE->nWidth = nTableWidth;
            else if ( pE->nWidth < nTableWidth )
            {
                sal_uInt16 nOldOffset = pE->nOffset + pE->nWidth;
                sal_uInt16 nNewOffset = pE->nOffset + nTableWidth;
                ModifyOffset( pS->xLocalColOffset.get(), nOldOffset, nNewOffset, nOffsetTolerance );
                sal_uInt16 nTmp = nNewOffset - pE->nOffset - pE->nWidth;
                pE->nWidth = nNewOffset - pE->nOffset;
                pS->nTableWidth = pS->nTableWidth + nTmp;
                if ( pS->nColOffset >= nOldOffset )
                    pS->nColOffset = pS->nColOffset + nTmp;
            }

            nColCnt = pE->nCol + pE->nColOverlap;
            nRowCnt = pS->nRowCnt;
            nColCntStart = pS->nColCntStart;
            nMaxCol = pS->nMaxCol;
            nTable = pS->nTable;
            nTableWidth = pS->nTableWidth;
            nFirstTableCell = pS->nFirstTableCell;
            nColOffset = pS->nColOffset;
            nColOffsetStart = pS->nColOffsetStart;
            bFirstRow = pS->bFirstRow;
            xLockedList = pS->xLockedList;
            xLocalColOffset = pS->xLocalColOffset;
            // mxActEntry is kept around if a table is started in the same row
            // (anything's possible in HTML); will be deleted by CloseEntry
            mxActEntry = pE;
        }
        bTabInTabCell = true;
        bInCell = true;
    }
    else
    {   // Simple table finished
        SetWidths();
        nMaxCol = 0;
        nTable = 0;
        if ( !aTableStack.empty() )
        {
            ScHTMLTableStackEntry* pS = aTableStack.top().get();
            xLocalColOffset = std::move(pS->xLocalColOffset);
            aTableStack.pop();
        }
    }
}

void ScHTMLLayoutParser::Image( HtmlImportInfo* pInfo )
{
    mxActEntry->maImageList.push_back(std::make_unique<ScHTMLImage>());
    ScHTMLImage* pImage = mxActEntry->maImageList.back().get();
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    for (const auto & rOption : rOptions)
    {
        switch( rOption.GetToken() )
        {
            case HtmlOptionId::SRC:
            {
                pImage->aURL = INetURLObject::GetAbsURL( aBaseURL, rOption.GetString() );
            }
            break;
            case HtmlOptionId::ALT:
            {
                if (!mxActEntry->bHasGraphic)
                {   // ALT text only if not any image loaded
                    if (!mxActEntry->aAltText.isEmpty())
                        mxActEntry->aAltText += "; ";

                    mxActEntry->aAltText += rOption.GetString();
                }
            }
            break;
            case HtmlOptionId::WIDTH:
            {
                pImage->aSize.setWidth( static_cast<tools::Long>(rOption.GetNumber()) );
            }
            break;
            case HtmlOptionId::HEIGHT:
            {
                pImage->aSize.setHeight( static_cast<tools::Long>(rOption.GetNumber()) );
            }
            break;
            case HtmlOptionId::HSPACE:
            {
                pImage->aSpace.setX( static_cast<tools::Long>(rOption.GetNumber()) );
            }
            break;
            case HtmlOptionId::VSPACE:
            {
                pImage->aSpace.setY( static_cast<tools::Long>(rOption.GetNumber()) );
            }
            break;
            default: break;
        }
    }
    if (pImage->aURL.isEmpty())
    {
        OSL_FAIL( "Image: graphic without URL ?!?" );
        return ;
    }

    if (comphelper::LibreOfficeKit::isActive())
    {
        INetURLObject aURL(pImage->aURL);
        if (HostFilter::isForbidden(aURL.GetHost()))
            SfxLokHelper::sendNetworkAccessError("paste");
    }

    sal_uInt16 nFormat;
    std::optional<Graphic> oGraphic(std::in_place);
    GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter();
    if ( ERRCODE_NONE != GraphicFilter::LoadGraphic( pImage->aURL, pImage->aFilterName,
            *oGraphic, &rFilter, &nFormat ) )
    {
        return ; // Bad luck
    }
    if (!mxActEntry->bHasGraphic)
    {   // discard any ALT text in this cell if we have any image
        mxActEntry->bHasGraphic = true;
        mxActEntry->aAltText.clear();
    }
    pImage->aFilterName = rFilter.GetImportFormatName( nFormat );
    pImage->oGraphic = std::move( oGraphic );
    if ( !(pImage->aSize.Width() && pImage->aSize.Height()) )
    {
        OutputDevice* pDefaultDev = Application::GetDefaultDevice();
        pImage->aSize = pDefaultDev->LogicToPixel( pImage->oGraphic->GetPrefSize(),
            pImage->oGraphic->GetPrefMapMode() );
    }
    if (mxActEntry->maImageList.empty())
        return;

    tools::Long nWidth = 0;
    for (const std::unique_ptr<ScHTMLImage> & pI : mxActEntry->maImageList)
    {
        if ( pI->nDir & nHorizontal )
            nWidth += pI->aSize.Width() + 2 * pI->aSpace.X();
        else
            nWidth = 0;
    }
    if ( mxActEntry->nWidth
      && (nWidth + pImage->aSize.Width() + 2 * pImage->aSpace.X()
            >= mxActEntry->nWidth) )
        mxActEntry->maImageList.back()->nDir = nVertical;
}

void ScHTMLLayoutParser::ColOn( HtmlImportInfo* pInfo )
{
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    for (const auto & rOption : rOptions)
    {
        if( rOption.GetToken() == HtmlOptionId::WIDTH )
        {
            sal_uInt16 nVal = GetWidthPixel( rOption );
            MakeCol( xLocalColOffset.get(), nColOffset, nVal, 0, 0 );
            nColOffset = nColOffset + nVal;
        }
    }
}

sal_uInt16 ScHTMLLayoutParser::GetWidthPixel( const HTMLOption& rOption )
{
    const OUString& rOptVal = rOption.GetString();
    if ( rOptVal.indexOf('%') != -1 )
    {   // Percent
        sal_uInt16 nW = (nTableWidth ? nTableWidth : static_cast<sal_uInt16>(aPageSize.Width()));
        return static_cast<sal_uInt16>((rOption.GetNumber() * nW) / 100);
    }
    else
    {
        if ( rOptVal.indexOf('*') != -1 )
        {   // Relative to what?
            // TODO: Collect all relative values in ColArray and then MakeCol
            return 0;
        }
        else
            return static_cast<sal_uInt16>(rOption.GetNumber()); // Pixel
    }
}

void ScHTMLLayoutParser::AnchorOn( HtmlImportInfo* pInfo )
{
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    for (const auto & rOption : rOptions)
    {
        if( rOption.GetToken() == HtmlOptionId::NAME )
            mxActEntry->pName = rOption.GetString();
    }
}

bool ScHTMLLayoutParser::IsAtBeginningOfText( const HtmlImportInfo* pInfo )
{
    ESelection& rSel = mxActEntry->aSel;
    return rSel.start.nPara == rSel.end.nPara &&
        rSel.start.nPara <= pInfo->aSelection.end.nPara &&
        pEdit->GetTextLen( rSel.start.nPara ) == 0;
}

void ScHTMLLayoutParser::FontOn( HtmlImportInfo* pInfo )
{
    if ( !IsAtBeginningOfText( pInfo ) )
        return;

// Only at the start of the text; applies to whole line
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(pInfo->pParser)->GetOptions();
    for (const auto & rOption : rOptions)
    {
        switch( rOption.GetToken() )
        {
            case HtmlOptionId::FACE :
            {
                const OUString& rFace = rOption.GetString();
                OUStringBuffer aFontName;
                sal_Int32 nPos = 0;
                while( nPos != -1 )
                {
                    // Font list, VCL uses the semicolon as separator
                    // HTML uses the comma
                    std::u16string_view aFName = o3tl::getToken(rFace, 0, ',', nPos );
                    aFName = comphelper::string::strip(aFName, ' ');
                    if( !aFontName.isEmpty() )
                        aFontName.append(";");
                    aFontName.append(aFName);
                }
                if ( !aFontName.isEmpty() )
                    mxActEntry->aItemSet.Put( SvxFontItem( FAMILY_DONTKNOW,
                        aFontName.makeStringAndClear(), OUString(), PITCH_DONTKNOW,
                        RTL_TEXTENCODING_DONTKNOW, ATTR_FONT ) );
            }
            break;
            case HtmlOptionId::SIZE :
            {
                sal_uInt16 nSize = static_cast<sal_uInt16>(rOption.GetNumber());
                if ( nSize == 0 )
                    nSize = 1;
                else if ( nSize > SC_HTML_FONTSIZES )
                    nSize = SC_HTML_FONTSIZES;
                mxActEntry->aItemSet.Put( SvxFontHeightItem(
                    maFontHeights[nSize-1], 100, ATTR_FONT_HEIGHT ) );
            }
            break;
            case HtmlOptionId::COLOR :
            {
                Color aColor;
                rOption.GetColor( aColor );
                mxActEntry->aItemSet.Put( SvxColorItem( aColor, ATTR_FONT_COLOR ) );
            }
            break;
            default: break;
        }
    }
}

void ScHTMLLayoutParser::ProcToken( HtmlImportInfo* pInfo )
{
    switch ( pInfo->nToken )
    {
        case HtmlTokenId::META:
        if (ScDocShell* pDocSh = mrDoc.GetDocumentShell())
        {
            HTMLParser* pParser = static_cast<HTMLParser*>(pInfo->pParser);
            uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
                static_cast<cppu::OWeakObject*>(pDocSh->GetModel()), uno::UNO_QUERY_THROW);
            pParser->ParseMetaOptions(
                xDPS->getDocumentProperties(),
                pDocSh->GetHeaderAttributes() );
        }
        break;
        case HtmlTokenId::TITLE_ON:
        {
            bInTitle = true;
            aString.clear();
        }
        break;
        case HtmlTokenId::TITLE_OFF:
        {
            ScDocShell* pDocSh = mrDoc.GetDocumentShell();
            if ( bInTitle && !aString.isEmpty() && pDocSh )
            {
                // Remove blanks from line breaks
                aString = aString.trim();
                uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
                    static_cast<cppu::OWeakObject*>(pDocSh->GetModel()),
                    uno::UNO_QUERY_THROW);
                xDPS->getDocumentProperties()->setTitle(aString);
            }
            bInTitle = false;
        }
        break;
        case HtmlTokenId::TABLE_ON:
        {
            TableOn( pInfo );
        }
        break;
        case HtmlTokenId::COL_ON:
        {
            ColOn( pInfo );
        }
        break;
        case HtmlTokenId::TABLEHEADER_ON:       // Opens row
        {
            if ( bInCell )
                CloseEntry( pInfo );
            // Do not set bInCell to true, TableDataOn does that
            mxActEntry->aItemSet.Put(
                SvxWeightItem( WEIGHT_BOLD, ATTR_FONT_WEIGHT) );
            [[fallthrough]];
        }
        case HtmlTokenId::TABLEDATA_ON:         // Opens cell
        {
            TableDataOn( pInfo );
        }
        break;
        case HtmlTokenId::SPAN_ON:
        {
            SpanOn(pInfo);
        }
        break;
        case HtmlTokenId::TABLEHEADER_OFF:
        case HtmlTokenId::TABLEDATA_OFF:        // Closes cell
        {
            TableDataOff( pInfo );
        }
        break;
        case HtmlTokenId::TABLEROW_ON:          // Before first cell in row
        {
            TableRowOn( pInfo );
        }
        break;
        case HtmlTokenId::TABLEROW_OFF:         // After last cell in row
        {
            TableRowOff( pInfo );
        }
        break;
        case HtmlTokenId::TABLE_OFF:
        {
            TableOff( pInfo );
        }
        break;
        case HtmlTokenId::IMAGE:
        {
            Image( pInfo );
        }
        break;
        case HtmlTokenId::PARABREAK_OFF:
        {   // We continue vertically after an image
            if (!mxActEntry->maImageList.empty())
                mxActEntry->maImageList.back()->nDir = nVertical;
        }
        break;
        case HtmlTokenId::ANCHOR_ON:
        {
            AnchorOn( pInfo );
        }
        break;
        case HtmlTokenId::FONT_ON :
        {
            FontOn( pInfo );
        }
        break;
        case HtmlTokenId::BIGPRINT_ON :
        {
            // TODO: Remember current font size and increase by 1
            if ( IsAtBeginningOfText( pInfo ) )
                mxActEntry->aItemSet.Put( SvxFontHeightItem(
                    maFontHeights[3], 100, ATTR_FONT_HEIGHT ) );
        }
        break;
        case HtmlTokenId::SMALLPRINT_ON :
        {
            // TODO: Remember current font size and decrease by 1
            if ( IsAtBeginningOfText( pInfo ) )
                mxActEntry->aItemSet.Put( SvxFontHeightItem(
                    maFontHeights[0], 100, ATTR_FONT_HEIGHT ) );
        }
        break;
        case HtmlTokenId::BOLD_ON :
        case HtmlTokenId::STRONG_ON :
        {
            if ( IsAtBeginningOfText( pInfo ) )
                mxActEntry->aItemSet.Put( SvxWeightItem( WEIGHT_BOLD,
                    ATTR_FONT_WEIGHT ) );
        }
        break;
        case HtmlTokenId::ITALIC_ON :
        case HtmlTokenId::EMPHASIS_ON :
        case HtmlTokenId::ADDRESS_ON :
        case HtmlTokenId::BLOCKQUOTE_ON :
        case HtmlTokenId::BLOCKQUOTE30_ON :
        case HtmlTokenId::CITATION_ON :
        case HtmlTokenId::VARIABLE_ON :
        {
            if ( IsAtBeginningOfText( pInfo ) )
                mxActEntry->aItemSet.Put( SvxPostureItem( ITALIC_NORMAL,
                    ATTR_FONT_POSTURE ) );
        }
        break;
        case HtmlTokenId::DEFINSTANCE_ON :
        {
            if ( IsAtBeginningOfText( pInfo ) )
            {
                mxActEntry->aItemSet.Put( SvxWeightItem( WEIGHT_BOLD,
                    ATTR_FONT_WEIGHT ) );
                mxActEntry->aItemSet.Put( SvxPostureItem( ITALIC_NORMAL,
                    ATTR_FONT_POSTURE ) );
            }
        }
        break;
        case HtmlTokenId::STRIKE_ON:
        case HtmlTokenId::STRIKETHROUGH_ON:
        case HtmlTokenId::DELETEDTEXT_ON:
        {
            if (IsAtBeginningOfText(pInfo))
                mxActEntry->aItemSet.Put(SvxCrossedOutItem(STRIKEOUT_SINGLE, ATTR_FONT_CROSSEDOUT));
        }
        break;
        case HtmlTokenId::UNDERLINE_ON :
        case HtmlTokenId::INSERTEDTEXT_ON:
        {
            if ( IsAtBeginningOfText( pInfo ) )
                mxActEntry->aItemSet.Put( SvxUnderlineItem( LINESTYLE_SINGLE,
                    ATTR_FONT_UNDERLINE ) );
        }
        break;
        case HtmlTokenId::TEXTTOKEN:
        {
            if ( bInTitle )
                aString += pInfo->aText;
        }
        break;
        default: ;
    }
}

// HTML DATA QUERY PARSER

template< typename Type >
static Type getLimitedValue( const Type& rValue, const Type& rMin, const Type& rMax )
{ return std::clamp( rValue, rMin, rMax ); }

ScHTMLEntry::ScHTMLEntry( const SfxItemSet& rItemSet, ScHTMLTableId nTableId ) :
    ScEEParseEntry( rItemSet ),
    mbImportAlways( false )
{
    nTab = nTableId;
    bEntirePara = false;
}

bool ScHTMLEntry::HasContents() const
{
     return mbImportAlways || aSel.HasRange() || !aAltText.isEmpty() || IsTable();
}

void ScHTMLEntry::AdjustStart( const HtmlImportInfo& rInfo )
{
    // set start position
    aSel.start = rInfo.aSelection.start;
    // adjust end position
    if (!aSel.IsAdjusted())
    {
        aSel.CollapseToStart();
    }
}

void ScHTMLEntry::AdjustEnd( const HtmlImportInfo& rInfo )
{
    OSL_ENSURE( !(rInfo.aSelection.end < aSel.end),
                "ScHTMLQueryParser::AdjustEntryEnd - invalid end position" );
    // set end position
    aSel.end = rInfo.aSelection.end;
}

void ScHTMLEntry::Strip( const EditEngine& rEditEngine )
{
    // strip leading empty paragraphs
    while( (aSel.start.nPara < aSel.end.nPara) && (rEditEngine.GetTextLen( aSel.start.nPara ) <= aSel.start.nIndex) )
    {
        ++aSel.start.nPara;
        aSel.start.nIndex = 0;
    }
    // strip trailing empty paragraphs
    while( (aSel.start.nPara < aSel.end.nPara) && (aSel.end.nIndex == 0) )
    {
        --aSel.end.nPara;
        aSel.end.nIndex = rEditEngine.GetTextLen( aSel.end.nPara );
    }
}

/** A map of ScHTMLTable objects.

    Organizes the tables with a unique table key. Stores nested tables inside
    the parent table and forms in this way a tree structure of tables. An
    instance of this class owns the contained table objects and deletes them
    on destruction.
 */
class ScHTMLTableMap final
{
private:
    typedef std::shared_ptr< ScHTMLTable >          ScHTMLTablePtr;
    typedef std::map< ScHTMLTableId, ScHTMLTablePtr > ScHTMLTableStdMap;

public:
    typedef ScHTMLTableStdMap::iterator             iterator;
    typedef ScHTMLTableStdMap::const_iterator       const_iterator;

private:
    ScHTMLTable&        mrParentTable;      /// Reference to parent table.
    ScHTMLTableStdMap   maTables;           /// Container for all table objects.
    mutable ScHTMLTable* mpCurrTable;       /// Current table, used for fast search.

public:
    explicit            ScHTMLTableMap( ScHTMLTable& rParentTable );

    const_iterator begin() const { return maTables.begin(); }
    const_iterator end() const { return maTables.end(); }

    /** Returns the specified table.
        @param nTableId  Unique identifier of the table.
        @param bDeep  true = searches deep in all nested table; false = only in this container. */
    ScHTMLTable*        FindTable( ScHTMLTableId nTableId, bool bDeep = true ) const;

    /** Inserts a new table into the container. This container owns the created table.
        @param bPreFormText  true = New table is based on preformatted text (<pre> tag). */
    ScHTMLTable*        CreateTable( const HtmlImportInfo& rInfo, bool bPreFormText, const ScDocument& rDoc );

private:
    /** Sets a working table with its index for search optimization. */
    void         SetCurrTable( ScHTMLTable* pTable ) const
                            { if( pTable ) mpCurrTable = pTable; }
};

ScHTMLTableMap::ScHTMLTableMap( ScHTMLTable& rParentTable ) :
    mrParentTable(rParentTable),
    mpCurrTable(nullptr)
{
}

ScHTMLTable* ScHTMLTableMap::FindTable( ScHTMLTableId nTableId, bool bDeep ) const
{
    ScHTMLTable* pResult = nullptr;
    if( mpCurrTable && (nTableId == mpCurrTable->GetTableId()) )
        pResult = mpCurrTable;              // cached table
    else
    {
        const_iterator aFind = maTables.find( nTableId );
        if( aFind != maTables.end() )
            pResult = aFind->second.get();  // table from this container
    }

    // not found -> search deep in nested tables
    if( !pResult && bDeep )
        for( const_iterator aIter = begin(), aEnd = end(); !pResult && (aIter != aEnd); ++aIter )
            pResult = aIter->second->FindNestedTable( nTableId );

    SetCurrTable( pResult );
    return pResult;
}

ScHTMLTable* ScHTMLTableMap::CreateTable( const HtmlImportInfo& rInfo, bool bPreFormText, const ScDocument& rDoc )
{
    ScHTMLTable* pTable = new ScHTMLTable( mrParentTable, rInfo, bPreFormText, rDoc );
    maTables[ pTable->GetTableId() ].reset( pTable );
    SetCurrTable( pTable );
    return pTable;
}

namespace {

/** Simplified forward iterator for convenience.

    Before the iterator can be dereferenced, it must be tested with the is()
    method. The iterator may be invalid directly after construction (e.g. empty
    container).
 */
class ScHTMLTableIterator
{
public:
    /** Constructs the iterator for the passed table map.
        @param pTableMap  Pointer to the table map (is allowed to be NULL). */
    explicit            ScHTMLTableIterator( const ScHTMLTableMap* pTableMap );

    bool         is() const { return mpTableMap && maIter != maEnd; }
    ScHTMLTable* operator->() { return maIter->second.get(); }
    ScHTMLTableIterator& operator++() { ++maIter; return *this; }

private:
    ScHTMLTableMap::const_iterator maIter;
    ScHTMLTableMap::const_iterator maEnd;
    const ScHTMLTableMap* mpTableMap;
};

}

ScHTMLTableIterator::ScHTMLTableIterator( const ScHTMLTableMap* pTableMap ) :
    mpTableMap(pTableMap)
{
    if( pTableMap )
    {
        maIter = pTableMap->begin();
        maEnd = pTableMap->end();
    }
}

ScHTMLTableAutoId::ScHTMLTableAutoId( ScHTMLTableId& rnUnusedId ) :
    mnTableId( rnUnusedId ),
    mrnUnusedId( rnUnusedId )
{
    ++mrnUnusedId;
}

ScHTMLTable::ScHTMLTable( ScHTMLTable& rParentTable, const HtmlImportInfo& rInfo, bool bPreFormText, const ScDocument& rDoc ) :
    mpParentTable( &rParentTable ),
    maTableId( rParentTable.maTableId.mrnUnusedId ),
    maTableItemSet( rParentTable.GetCurrItemSet() ),
    mrEditEngine( rParentTable.mrEditEngine ),
    mrEEParseList( rParentTable.mrEEParseList ),
    mpCurrEntryVector( nullptr ),
    maSize( 1, 1 ),
    mpParser(rParentTable.mpParser),
    mrDoc(rDoc),
    mbBorderOn( false ),
    mbPreFormText( bPreFormText ),
    mbRowOn( false ),
    mbDataOn( false ),
    mbPushEmptyLine( false ),
    mbCaptionOn ( false )
{
    if( mbPreFormText )
    {
        ImplRowOn();
        ImplDataOn( ScHTMLSize( 1, 1 ) );
    }
    else
    {
        ProcessFormatOptions( maTableItemSet, rInfo );
        const HTMLOptions& rOptions = static_cast<HTMLParser*>(rInfo.pParser)->GetOptions();
        for (const auto& rOption : rOptions)
        {
            switch( rOption.GetToken() )
            {
                case HtmlOptionId::BORDER:
                    mbBorderOn = rOption.GetString().isEmpty() || (rOption.GetNumber() != 0);
                break;
                case HtmlOptionId::ID:
                    maTableName = rOption.GetString();
                break;
                default: break;
            }
        }
    }

    CreateNewEntry( rInfo );
}

ScHTMLTable::ScHTMLTable(
    SfxItemPool& rPool,
    EditEngine& rEditEngine,
    std::vector<std::shared_ptr<ScEEParseEntry>>& rEEParseList,
    ScHTMLTableId& rnUnusedId, ScHTMLParser* pParser, const ScDocument& rDoc
) :
    mpParentTable( nullptr ),
    maTableId( rnUnusedId ),
    maTableItemSet( rPool ),
    mrEditEngine( rEditEngine ),
    mrEEParseList( rEEParseList ),
    mpCurrEntryVector( nullptr ),
    maSize( 1, 1 ),
    mpParser(pParser),
    mrDoc(rDoc),
    mbBorderOn( false ),
    mbPreFormText( false ),
    mbRowOn( false ),
    mbDataOn( false ),
    mbPushEmptyLine( false ),
    mbCaptionOn ( false )
{
    // open the first "cell" of the document
    ImplRowOn();
    ImplDataOn( ScHTMLSize( 1, 1 ) );
    mxCurrEntry = CreateEntry();
}

ScHTMLTable::~ScHTMLTable()
{
}

const SfxItemSet& ScHTMLTable::GetCurrItemSet() const
{
    // first try cell item set, then row item set, then table item set
    return moDataItemSet ? *moDataItemSet : (moRowItemSet ? *moRowItemSet : maTableItemSet);
}

ScHTMLSize ScHTMLTable::GetSpan( const ScHTMLPos& rCellPos ) const
{
    ScHTMLSize aSpan( 1, 1 );
    const ScRange* pRange = maVMergedCells.Find( rCellPos.MakeAddr() );
    if (!pRange)
        pRange = maHMergedCells.Find( rCellPos.MakeAddr() );
    if (pRange)
        aSpan.Set( pRange->aEnd.Col() - pRange->aStart.Col() + 1, pRange->aEnd.Row() - pRange->aStart.Row() + 1 );
    return aSpan;
}

ScHTMLTable* ScHTMLTable::FindNestedTable( ScHTMLTableId nTableId ) const
{
    return mxNestedTables ? mxNestedTables->FindTable( nTableId ) : nullptr;
}

void ScHTMLTable::PutItem( const SfxPoolItem& rItem )
{
    OSL_ENSURE( mxCurrEntry, "ScHTMLTable::PutItem - no current entry" );
    if( mxCurrEntry && mxCurrEntry->IsEmpty() )
        mxCurrEntry->GetItemSet().Put( rItem );
}

void ScHTMLTable::PutText( const HtmlImportInfo& rInfo )
{
    OSL_ENSURE( mxCurrEntry, "ScHTMLTable::PutText - no current entry" );
    if( mxCurrEntry )
    {
        if( !mxCurrEntry->HasContents() && IsSpaceCharInfo( rInfo ) )
            mxCurrEntry->AdjustStart( rInfo );
        else
            mxCurrEntry->AdjustEnd( rInfo );
        if (mbCaptionOn)
            maCaptionBuffer.append(rInfo.aText);

    }
}

void ScHTMLTable::InsertPara( const HtmlImportInfo& rInfo )
{
    if( mxCurrEntry && mbDataOn && !IsEmptyCell() )
        mxCurrEntry->SetImportAlways();
    PushEntry( rInfo );
    CreateNewEntry( rInfo );
    InsertLeadingEmptyLine();
}

void ScHTMLTable::BreakOn()
{
    // empty line, if <br> is at start of cell
    mbPushEmptyLine = !mbPreFormText && mbDataOn && IsEmptyCell();
}

void ScHTMLTable::HeadingOn()
{
    // call directly, InsertPara() has not been called before
    InsertLeadingEmptyLine();
}

void ScHTMLTable::InsertLeadingEmptyLine()
{
    // empty line, if <p>, </p>, <h?>, or </h*> are not at start of cell
    mbPushEmptyLine = !mbPreFormText && mbDataOn && !IsEmptyCell();
}

void ScHTMLTable::AnchorOn()
{
    OSL_ENSURE( mxCurrEntry, "ScHTMLTable::AnchorOn - no current entry" );
    // don't skip entries with single hyperlinks
    if( mxCurrEntry )
        mxCurrEntry->SetImportAlways();
}

ScHTMLTable* ScHTMLTable::TableOn( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo );
    return InsertNestedTable( rInfo, false );
}

ScHTMLTable* ScHTMLTable::TableOff( const HtmlImportInfo& rInfo )
{
    return mbPreFormText ? this : CloseTable( rInfo );
}

void ScHTMLTable::CaptionOn()
{
    mbCaptionOn = true;
    maCaptionBuffer.setLength(0);
}

void ScHTMLTable::CaptionOff()
{
    if (!mbCaptionOn)
        return;
    maCaption = maCaptionBuffer.makeStringAndClear().trim();
    mbCaptionOn = false;
}

ScHTMLTable* ScHTMLTable::PreOn( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo );
    return InsertNestedTable( rInfo, true );
}

ScHTMLTable* ScHTMLTable::PreOff( const HtmlImportInfo& rInfo )
{
    return mbPreFormText ? CloseTable( rInfo ) : this;
}

void ScHTMLTable::RowOn( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo, true );
    if( mpParentTable && !mbPreFormText )   // no rows allowed in global and preformatted tables
    {
        ImplRowOn();
        ProcessFormatOptions( *moRowItemSet, rInfo );
    }
    CreateNewEntry( rInfo );
}

void ScHTMLTable::RowOff( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo, true );
    if( mpParentTable && !mbPreFormText )   // no rows allowed in global and preformatted tables
        ImplRowOff();
    CreateNewEntry( rInfo );
}

namespace {

/**
 * Decode a number format string stored in Excel-generated HTML's CSS
 * region.
 */
OUString decodeNumberFormat(const OUString& rFmt)
{
    OUStringBuffer aBuf;
    const sal_Unicode* p = rFmt.getStr();
    sal_Int32 n = rFmt.getLength();
    for (sal_Int32 i = 0; i < n; ++i, ++p)
    {
        if (*p == '\\')
        {
            // Skip '\'.
            ++i;
            ++p;

            // Parse all subsequent digits until first non-digit is found.
            sal_Int32 nDigitCount = 0;
            const sal_Unicode* p1 = p;
            for (; i < n; ++i, ++p, ++nDigitCount)
            {
                if (*p < '0' || '9' < *p)
                {
                    --i;
                    --p;
                    break;
                }

            }
            if (nDigitCount)
            {
                // Hex-encoded character found. Decode it back into its
                // original character. An example of number format with
                // hex-encoded chars: "\0022$\0022\#\,\#\#0\.00"
                sal_uInt32 nVal = OUString(p1, nDigitCount).toUInt32(16);
                aBuf.append(static_cast<sal_Unicode>(nVal));
            }
        }
        else
            aBuf.append(*p);
    }
    return aBuf.makeStringAndClear();
}

}

void ScHTMLTable::DataOn( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo, true );
    if( mpParentTable && !mbPreFormText )   // no cells allowed in global and preformatted tables
    {
        // read needed options from the <td> tag
        ScHTMLSize aSpanSize( 1, 1 );
        std::optional<OUString> pValStr, pNumStr;
        const HTMLOptions& rOptions = static_cast<HTMLParser*>(rInfo.pParser)->GetOptions();
        sal_uInt32 nNumberFormat = NUMBERFORMAT_ENTRY_NOT_FOUND;
        for (const auto& rOption : rOptions)
        {
            switch (rOption.GetToken())
            {
                case HtmlOptionId::COLSPAN:
                    aSpanSize.mnCols = static_cast<SCCOL>( getLimitedValue<sal_Int32>( rOption.GetString().toInt32(), 1, 256 ) );
                break;
                case HtmlOptionId::ROWSPAN:
                    aSpanSize.mnRows = static_cast<SCROW>( getLimitedValue<sal_Int32>( rOption.GetString().toInt32(), 1, 256 ) );
                break;
                case HtmlOptionId::SDVAL:
                    pValStr = rOption.GetString();
                break;
                case HtmlOptionId::SDNUM:
                    pNumStr = rOption.GetString();
                break;
                case HtmlOptionId::CLASS:
                {
                    // Pick up the number format associated with this class (if
                    // any).
                    const OUString& aClass = rOption.GetString();
                    const ScHTMLStyles& rStyles = mpParser->GetStyles();
                    const OUString& rVal = rStyles.getPropertyValue(u"td"_ustr, aClass, u"mso-number-format"_ustr);
                    if (!rVal.isEmpty())
                    {
                        OUString aNumFmt = decodeNumberFormat(rVal);

                        nNumberFormat = GetFormatTable()->GetEntryKey(aNumFmt);
                        if (nNumberFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
                        {
                            sal_Int32 nErrPos  = 0;
                            SvNumFormatType nDummy;
                            bool bValidFmt = GetFormatTable()->PutEntry(aNumFmt, nErrPos, nDummy, nNumberFormat);
                            if (!bValidFmt)
                                nNumberFormat = NUMBERFORMAT_ENTRY_NOT_FOUND;
                        }
                    }
                }
                break;
                case HtmlOptionId::DSVAL:
                {
                    ParseDataSheetsValue(rOption.GetString(), pValStr, pNumStr);
                }
                break;
                default: break;
            }
        }

        ImplDataOn( aSpanSize );

        if (nNumberFormat != NUMBERFORMAT_ENTRY_NOT_FOUND)
            moDataItemSet->Put( SfxUInt32Item(ATTR_VALUE_FORMAT, nNumberFormat) );

        ProcessFormatOptions( *moDataItemSet, rInfo );
        CreateNewEntry( rInfo );
        mxCurrEntry->pValStr = std::move(pValStr);
        mxCurrEntry->pNumStr = std::move(pNumStr);
    }
    else
        CreateNewEntry( rInfo );
}

void ScHTMLTable::DataOff( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo, true );
    if( mpParentTable && !mbPreFormText )   // no cells allowed in global and preformatted tables
        ImplDataOff();
    CreateNewEntry( rInfo );
}

void ScHTMLTable::BodyOn( const HtmlImportInfo& rInfo )
{
    bool bPushed = PushEntry( rInfo );
    if( !mpParentTable )
    {
        // do not start new row, if nothing (no title) precedes the body.
        if( bPushed || !mbRowOn )
            ImplRowOn();
        if( bPushed || !mbDataOn )
            ImplDataOn( ScHTMLSize( 1, 1 ) );
        ProcessFormatOptions( *moDataItemSet, rInfo );
    }
    CreateNewEntry( rInfo );
}

void ScHTMLTable::BodyOff( const HtmlImportInfo& rInfo )
{
    PushEntry( rInfo );
    if( !mpParentTable )
    {
        ImplDataOff();
        ImplRowOff();
    }
    CreateNewEntry( rInfo );
}

ScHTMLTable* ScHTMLTable::CloseTable( const HtmlImportInfo& rInfo )
{
    if( mpParentTable )     // not allowed to close global table
    {
        PushEntry( rInfo, mbDataOn );
        ImplDataOff();
        ImplRowOff();
        mpParentTable->PushTableEntry( GetTableId() );
        mpParentTable->CreateNewEntry( rInfo );
        if( mbPreFormText ) // enclose preformatted table with empty lines in parent table
            mpParentTable->InsertLeadingEmptyLine();
        return mpParentTable;
    }
    return this;
}

SCCOLROW ScHTMLTable::GetDocSize( ScHTMLOrient eOrient, SCCOLROW nCellPos ) const
{
    const ScSizeVec& rSizes = maCumSizes[ eOrient ];
    size_t nIndex = static_cast< size_t >( nCellPos );
    if( nIndex >= rSizes.size() ) return 0;
    return (nIndex == 0) ? rSizes.front() : (rSizes[ nIndex ] - rSizes[ nIndex - 1 ]);
}

SCCOLROW ScHTMLTable::GetDocSize( ScHTMLOrient eOrient, SCCOLROW nCellBegin, SCCOLROW nCellEnd ) const
{
    const ScSizeVec& rSizes = maCumSizes[ eOrient ];
    size_t nBeginIdx = static_cast< size_t >( std::max< SCCOLROW >( nCellBegin, 0 ) );
    size_t nEndIdx = static_cast< size_t >( std::min< SCCOLROW >( nCellEnd, static_cast< SCCOLROW >( rSizes.size() ) ) );
    if (nBeginIdx >= nEndIdx ) return 0;
    return rSizes[ nEndIdx - 1 ] - ((nBeginIdx == 0) ? 0 : rSizes[ nBeginIdx - 1 ]);
}

SCCOLROW ScHTMLTable::GetDocSize( ScHTMLOrient eOrient ) const
{
    const ScSizeVec& rSizes = maCumSizes[ eOrient ];
    return rSizes.empty() ? 0 : rSizes.back();
}

ScHTMLSize ScHTMLTable::GetDocSize( const ScHTMLPos& rCellPos ) const
{
    ScHTMLSize aCellSpan = GetSpan( rCellPos );
    return ScHTMLSize(
        static_cast< SCCOL >( GetDocSize( tdCol, rCellPos.mnCol, rCellPos.mnCol + aCellSpan.mnCols ) ),
        static_cast< SCROW >( GetDocSize( tdRow, rCellPos.mnRow, rCellPos.mnRow + aCellSpan.mnRows ) ) );
}

SCCOLROW ScHTMLTable::GetDocPos( ScHTMLOrient eOrient, SCCOLROW nCellPos ) const
{
    return maDocBasePos.Get( eOrient ) + GetDocSize( eOrient, 0, nCellPos );
}

ScHTMLPos ScHTMLTable::GetDocPos( const ScHTMLPos& rCellPos ) const
{
    return ScHTMLPos(
        static_cast< SCCOL >( GetDocPos( tdCol, rCellPos.mnCol ) ),
        static_cast< SCROW >( GetDocPos( tdRow, rCellPos.mnRow ) ) );
}

void ScHTMLTable::GetDocRange( ScRange& rRange ) const
{
    rRange.aStart = rRange.aEnd = maDocBasePos.MakeAddr();
    ScAddress aErrorPos( ScAddress::UNINITIALIZED );
    if (!rRange.aEnd.Move( static_cast< SCCOL >( GetDocSize( tdCol ) ) - 1,
                static_cast< SCROW >( GetDocSize( tdRow ) ) - 1, 0, aErrorPos, mrDoc ))
    {
        assert(!"can't move");
    }
}

void ScHTMLTable::ApplyCellBorders( ScDocument& rDoc, const ScAddress& rFirstPos ) const
{
    if( mbBorderOn )
    {
        const SCCOL nLastCol = maSize.mnCols - 1;
        const SCROW nLastRow = maSize.mnRows - 1;
        const tools::Long nOuterLine = SvxBorderLineWidth::Medium;
        const tools::Long nInnerLine = SvxBorderLineWidth::Hairline;
        SvxBorderLine aOuterLine(nullptr, nOuterLine, SvxBorderLineStyle::SOLID);
        SvxBorderLine aInnerLine(nullptr, nInnerLine, SvxBorderLineStyle::SOLID);
        SvxBoxItem aBorderItem( ATTR_BORDER );

        for( SCCOL nCol = 0; nCol <= nLastCol; ++nCol )
        {
            SvxBorderLine* pLeftLine = (nCol == 0) ? &aOuterLine : &aInnerLine;
            SvxBorderLine* pRightLine = (nCol == nLastCol) ? &aOuterLine : &aInnerLine;
            SCCOL nCellCol1 = static_cast< SCCOL >( GetDocPos( tdCol, nCol ) ) + rFirstPos.Col();
            SCCOL nCellCol2 = nCellCol1 + static_cast< SCCOL >( GetDocSize( tdCol, nCol ) ) - 1;
            for( SCROW nRow = 0; nRow <= nLastRow; ++nRow )
            {
                SvxBorderLine* pTopLine = (nRow == 0) ? &aOuterLine : &aInnerLine;
                SvxBorderLine* pBottomLine = (nRow == nLastRow) ? &aOuterLine : &aInnerLine;
                SCROW nCellRow1 = GetDocPos( tdRow, nRow ) + rFirstPos.Row();
                SCROW nCellRow2 = nCellRow1 + GetDocSize( tdRow, nRow ) - 1;
                for( SCCOL nCellCol = nCellCol1; nCellCol <= nCellCol2; ++nCellCol )
                {
                    aBorderItem.SetLine( (nCellCol == nCellCol1) ? pLeftLine : nullptr, SvxBoxItemLine::LEFT );
                    aBorderItem.SetLine( (nCellCol == nCellCol2) ? pRightLine : nullptr, SvxBoxItemLine::RIGHT );
                    for( SCROW nCellRow = nCellRow1; nCellRow <= nCellRow2; ++nCellRow )
                    {
                        aBorderItem.SetLine( (nCellRow == nCellRow1) ? pTopLine : nullptr, SvxBoxItemLine::TOP );
                        aBorderItem.SetLine( (nCellRow == nCellRow2) ? pBottomLine : nullptr, SvxBoxItemLine::BOTTOM );
                        rDoc.ApplyAttr( nCellCol, nCellRow, rFirstPos.Tab(), aBorderItem );
                    }
                }
            }
        }
    }

    for( ScHTMLTableIterator aIter( mxNestedTables.get() ); aIter.is(); ++aIter )
        aIter->ApplyCellBorders( rDoc, rFirstPos );
}

SvNumberFormatter* ScHTMLTable::GetFormatTable()
{
    return mpParser->GetDoc().GetFormatTable();
}

bool ScHTMLTable::IsEmptyCell() const
{
    return mpCurrEntryVector && mpCurrEntryVector->empty();
}

bool ScHTMLTable::IsSpaceCharInfo( const HtmlImportInfo& rInfo )
{
    return (rInfo.nToken == HtmlTokenId::TEXTTOKEN) && (rInfo.aText.getLength() == 1) && (rInfo.aText[ 0 ] == ' ');
}

ScHTMLTable::ScHTMLEntryPtr ScHTMLTable::CreateEntry() const
{
    return std::make_unique<ScHTMLEntry>( GetCurrItemSet() );
}

void ScHTMLTable::CreateNewEntry( const HtmlImportInfo& rInfo )
{
    OSL_ENSURE( !mxCurrEntry, "ScHTMLTable::CreateNewEntry - old entry still present" );
    mxCurrEntry = CreateEntry();
    mxCurrEntry->aSel = rInfo.aSelection;
}

void ScHTMLTable::ImplPushEntryToVector( ScHTMLEntryVector& rEntryVector, ScHTMLEntryPtr& rxEntry )
{
    // HTML entry list does not own the entries
    rEntryVector.push_back( rxEntry.get() );
    // mrEEParseList (reference to member of ScEEParser) owns the entries
    mrEEParseList.push_back(std::shared_ptr<ScEEParseEntry>(rxEntry.release()));
}

bool ScHTMLTable::PushEntry( ScHTMLEntryPtr& rxEntry )
{
    bool bPushed = false;
    if( rxEntry && rxEntry->HasContents() )
    {
        if( mpCurrEntryVector )
        {
            if( mbPushEmptyLine )
            {
                ScHTMLEntryPtr xEmptyEntry = CreateEntry();
                ImplPushEntryToVector( *mpCurrEntryVector, xEmptyEntry );
                mbPushEmptyLine = false;
            }
            ImplPushEntryToVector( *mpCurrEntryVector, rxEntry );
            bPushed = true;
        }
        else if( mpParentTable )
        {
            bPushed = mpParentTable->PushEntry( rxEntry );
        }
        else
        {
            OSL_FAIL( "ScHTMLTable::PushEntry - cannot push entry, no parent found" );
        }
    }
    return bPushed;
}

bool ScHTMLTable::PushEntry( const HtmlImportInfo& rInfo, bool bLastInCell )
{
    OSL_ENSURE( mxCurrEntry, "ScHTMLTable::PushEntry - no current entry" );
    bool bPushed = false;
    if( mxCurrEntry )
    {
        mxCurrEntry->AdjustEnd( rInfo );
        mxCurrEntry->Strip( mrEditEngine );

        // import entry always, if it is the last in cell, and cell is still empty
        if( bLastInCell && IsEmptyCell() )
        {
            mxCurrEntry->SetImportAlways();
            // don't insert empty lines before single empty entries
            if( mxCurrEntry->IsEmpty() )
                mbPushEmptyLine = false;
        }

        bPushed = PushEntry( mxCurrEntry );
        mxCurrEntry.reset();
    }
    return bPushed;
}

void ScHTMLTable::PushTableEntry( ScHTMLTableId nTableId )
{
    OSL_ENSURE( nTableId != SC_HTML_GLOBAL_TABLE, "ScHTMLTable::PushTableEntry - cannot push global table" );
    if( nTableId != SC_HTML_GLOBAL_TABLE )
    {
        ScHTMLEntryPtr xEntry( new ScHTMLEntry( maTableItemSet, nTableId ) );
        PushEntry( xEntry );
    }
}

ScHTMLTable* ScHTMLTable::GetExistingTable( ScHTMLTableId nTableId ) const
{
    ScHTMLTable* pTable = ((nTableId != SC_HTML_GLOBAL_TABLE) && mxNestedTables) ?
        mxNestedTables->FindTable( nTableId, false ) : nullptr;
    OSL_ENSURE( pTable || (nTableId == SC_HTML_GLOBAL_TABLE), "ScHTMLTable::GetExistingTable - table not found" );
    return pTable;
}

ScHTMLTable* ScHTMLTable::InsertNestedTable( const HtmlImportInfo& rInfo, bool bPreFormText )
{
    if( !mxNestedTables )
        mxNestedTables.reset( new ScHTMLTableMap( *this ) );
    if( bPreFormText )      // enclose new preformatted table with empty lines
        InsertLeadingEmptyLine();
    return mxNestedTables->CreateTable( rInfo, bPreFormText, mrDoc );
}

void ScHTMLTable::InsertNewCell( const ScHTMLSize& rSpanSize )
{
    ScRange* pRange;

    /*  Find an unused cell by skipping all merged ranges that cover the
        current cell position stored in maCurrCell. */
    for (;;)
    {
        pRange = maVMergedCells.Find( maCurrCell.MakeAddr() );
        if (!pRange)
            pRange = maHMergedCells.Find( maCurrCell.MakeAddr() );
        if (!pRange)
            break;
        maCurrCell.mnCol = pRange->aEnd.Col() + 1;
    }
    mpCurrEntryVector = &maEntryMap[ maCurrCell ];

    /*  If the new cell is merged horizontally, try to find collisions with
        other vertically merged ranges. In this case, shrink existing
        vertically merged ranges (do not shrink the new cell). */
    SCCOL nColEnd = maCurrCell.mnCol + rSpanSize.mnCols;
    for( ScAddress aAddr( maCurrCell.MakeAddr() ); aAddr.Col() < nColEnd; aAddr.IncCol() )
        if( (pRange = maVMergedCells.Find( aAddr )) != nullptr )
            pRange->aEnd.SetRow( maCurrCell.mnRow - 1 );

    // insert the new range into the cell lists
    ScRange aNewRange( maCurrCell.MakeAddr() );
    ScAddress aErrorPos( ScAddress::UNINITIALIZED );
    if (!aNewRange.aEnd.Move( rSpanSize.mnCols - 1, rSpanSize.mnRows - 1, 0, aErrorPos, mrDoc ))
    {
        assert(!"can't move");
    }
    if( rSpanSize.mnRows > 1 )
    {
        maVMergedCells.push_back( aNewRange );
        /*  Do not insert vertically merged ranges into maUsedCells yet,
            because they may be shrunken (see above). The final vertically
            merged ranges are inserted in FillEmptyCells(). */
    }
    else
    {
        if( rSpanSize.mnCols > 1 )
            maHMergedCells.push_back( aNewRange );
        /*  Insert horizontally merged ranges and single cells into
            maUsedCells, they will not be changed anymore. */
        maUsedCells.Join( aNewRange );
    }

    // adjust table size
    maSize.mnCols = std::max< SCCOL >( maSize.mnCols, aNewRange.aEnd.Col() + 1 );
    maSize.mnRows = std::max< SCROW >( maSize.mnRows, aNewRange.aEnd.Row() + 1 );
}

void ScHTMLTable::ImplRowOn()
{
    if( mbRowOn )
        ImplRowOff();
    moRowItemSet.emplace( maTableItemSet );
    maCurrCell.mnCol = 0;
    mbRowOn = true;
    mbDataOn = false;
}

void ScHTMLTable::ImplRowOff()
{
    if( mbDataOn )
        ImplDataOff();
    if( mbRowOn )
    {
        moRowItemSet.reset();
        ++maCurrCell.mnRow;
        mbRowOn = mbDataOn = false;
    }
}

void ScHTMLTable::ImplDataOn( const ScHTMLSize& rSpanSize )
{
    if( mbDataOn )
        ImplDataOff();
    if( !mbRowOn )
        ImplRowOn();
    moDataItemSet.emplace( *moRowItemSet );
    InsertNewCell( rSpanSize );
    mbDataOn = true;
    mbPushEmptyLine = false;
}

void ScHTMLTable::ImplDataOff()
{
    if( mbDataOn )
    {
        moDataItemSet.reset();
        ++maCurrCell.mnCol;
        mpCurrEntryVector = nullptr;
        mbDataOn = false;
    }
}

void ScHTMLTable::ProcessFormatOptions( SfxItemSet& rItemSet, const HtmlImportInfo& rInfo )
{
    // special handling for table header cells
    if( rInfo.nToken == HtmlTokenId::TABLEHEADER_ON )
    {
        rItemSet.Put( SvxWeightItem( WEIGHT_BOLD, ATTR_FONT_WEIGHT ) );
        rItemSet.Put( SvxHorJustifyItem( SvxCellHorJustify::Center, ATTR_HOR_JUSTIFY ) );
    }

    const HTMLOptions& rOptions = static_cast<HTMLParser*>(rInfo.pParser)->GetOptions();
    for (const auto& rOption : rOptions)
    {
        switch( rOption.GetToken() )
        {
            case HtmlOptionId::ALIGN:
            {
                SvxCellHorJustify eVal = SvxCellHorJustify::Standard;
                const OUString& rOptVal = rOption.GetString();
                if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_right ) )
                    eVal = SvxCellHorJustify::Right;
                else if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_center ) )
                    eVal = SvxCellHorJustify::Center;
                else if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_AL_left ) )
                    eVal = SvxCellHorJustify::Left;
                if( eVal != SvxCellHorJustify::Standard )
                    rItemSet.Put( SvxHorJustifyItem( eVal, ATTR_HOR_JUSTIFY ) );
            }
            break;

            case HtmlOptionId::VALIGN:
            {
                SvxCellVerJustify eVal = SvxCellVerJustify::Standard;
                const OUString& rOptVal = rOption.GetString();
                if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_top ) )
                    eVal = SvxCellVerJustify::Top;
                else if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_middle ) )
                    eVal = SvxCellVerJustify::Center;
                else if( rOptVal.equalsIgnoreAsciiCase( OOO_STRING_SVTOOLS_HTML_VA_bottom ) )
                    eVal = SvxCellVerJustify::Bottom;
                if( eVal != SvxCellVerJustify::Standard )
                    rItemSet.Put( SvxVerJustifyItem( eVal, ATTR_VER_JUSTIFY ) );
            }
            break;

            case HtmlOptionId::BGCOLOR:
            {
                Color aColor;
                rOption.GetColor( aColor );
                rItemSet.Put( SvxBrushItem( aColor, ATTR_BACKGROUND ) );
            }
            break;
            default: break;
        }
    }
}

void ScHTMLTable::SetDocSize( ScHTMLOrient eOrient, SCCOLROW nCellPos, SCCOLROW nSize )
{
    OSL_ENSURE( nCellPos >= 0, "ScHTMLTable::SetDocSize - unexpected negative position" );
    ScSizeVec& rSizes = maCumSizes[ eOrient ];
    size_t nIndex = static_cast< size_t >( nCellPos );
    // expand with height/width == 1
    while( nIndex >= rSizes.size() )
        rSizes.push_back( rSizes.empty() ? 1 : (rSizes.back() + 1) );
    // update size of passed position and all following
    // #i109987# only grow, don't shrink - use the largest needed size
    SCCOLROW nDiff = nSize - ((nIndex == 0) ? rSizes.front() : (rSizes[ nIndex ] - rSizes[ nIndex - 1 ]));
    if( nDiff > 0 )
        std::for_each(rSizes.begin() + nIndex, rSizes.end(), [&nDiff](SCCOLROW& rSize) { rSize += nDiff; });
}

void ScHTMLTable::CalcNeededDocSize(
        ScHTMLOrient eOrient, SCCOLROW nCellPos, SCCOLROW nCellSpan, SCCOLROW nRealDocSize )
{
    SCCOLROW nDiffSize = 0;
    // in merged columns/rows: reduce needed size by size of leading columns
    while( nCellSpan > 1 )
    {
        nDiffSize += GetDocSize( eOrient, nCellPos );
        --nCellSpan;
        ++nCellPos;
    }
    // set remaining needed size to last column/row
    nRealDocSize -= std::min< SCCOLROW >( nRealDocSize - 1, nDiffSize );
    SetDocSize( eOrient, nCellPos, nRealDocSize );
}

void ScHTMLTable::FillEmptyCells()
{
    for( ScHTMLTableIterator aIter( mxNestedTables.get() ); aIter.is(); ++aIter )
        aIter->FillEmptyCells();

    // insert the final vertically merged ranges into maUsedCells
    for ( size_t i = 0, nRanges = maVMergedCells.size(); i < nRanges; ++i )
    {
        ScRange & rRange = maVMergedCells[ i ];
        maUsedCells.Join( rRange );
    }

    for( ScAddress aAddr; aAddr.Row() < maSize.mnRows; aAddr.IncRow() )
    {
        for( aAddr.SetCol( 0 ); aAddr.Col() < maSize.mnCols; aAddr.IncCol() )
        {
            if( !maUsedCells.Find( aAddr ) )
            {
                // create a range for the lock list (used to calc. cell span)
                ScRange aRange( aAddr );
                do
                {
                    aRange.aEnd.IncCol();
                }
                while( (aRange.aEnd.Col() < maSize.mnCols) && !maUsedCells.Find( aRange.aEnd ) );
                aRange.aEnd.IncCol( -1 );
                maUsedCells.Join( aRange );

                // insert a dummy entry
                ScHTMLEntryPtr xEntry = CreateEntry();
                ImplPushEntryToVector( maEntryMap[ ScHTMLPos( aAddr ) ], xEntry );
            }
        }
    }
}

void ScHTMLTable::RecalcDocSize()
{
    // recalc table sizes recursively from inner to outer
    for( ScHTMLTableIterator aIter( mxNestedTables.get() ); aIter.is(); ++aIter )
        aIter->RecalcDocSize();

    /*  Two passes: first calculates the sizes of single columns/rows, then
        the sizes of spanned columns/rows. This allows to fill nested tables
        into merged cells optimally. */
    static const sal_uInt16 PASS_SINGLE = 0;
    static const sal_uInt16 PASS_SPANNED = 1;
    for( sal_uInt16 nPass = PASS_SINGLE; nPass <= PASS_SPANNED; ++nPass )
    {
        // iterate through every table cell
        for( const auto& [rCellPos, rEntryVector] : maEntryMap )
        {
            ScHTMLSize aCellSpan = GetSpan( rCellPos );

            // process the dimension of the current cell in this pass?
            // (pass is single and span is 1) or (pass is not single and span is not 1)
            bool bProcessColWidth = ((nPass == PASS_SINGLE) == (aCellSpan.mnCols == 1));
            bool bProcessRowHeight = ((nPass == PASS_SINGLE) == (aCellSpan.mnRows == 1));
            if( bProcessColWidth || bProcessRowHeight )
            {
                ScHTMLSize aDocSize( 1, 0 );    // resulting size of the cell in document

                // expand the cell size for each cell parse entry
                for( const auto& rpEntry : rEntryVector )
                {
                    ScHTMLTable* pTable = GetExistingTable( rpEntry->GetTableId() );
                    // find entry with maximum width
                    if( bProcessColWidth && pTable )
                        aDocSize.mnCols = std::max( aDocSize.mnCols, static_cast< SCCOL >( pTable->GetDocSize( tdCol ) ) );
                    // add up height of each entry
                    if( bProcessRowHeight )
                        aDocSize.mnRows += pTable ? pTable->GetDocSize( tdRow ) : 1;
                }
                if( !aDocSize.mnRows )
                    aDocSize.mnRows = 1;

                if( bProcessColWidth )
                    CalcNeededDocSize( tdCol, rCellPos.mnCol, aCellSpan.mnCols, aDocSize.mnCols );
                if( bProcessRowHeight )
                    CalcNeededDocSize( tdRow, rCellPos.mnRow, aCellSpan.mnRows, aDocSize.mnRows );
            }
        }
    }
}

void ScHTMLTable::RecalcDocPos( const ScHTMLPos& rBasePos )
{
    maDocBasePos = rBasePos;
    // after the previous assignment it is allowed to call GetDocPos() methods

    // iterate through every table cell
    for( auto& [rCellPos, rEntryVector] : maEntryMap )
    {
        // fixed doc position of the entire cell (first entry)
        const ScHTMLPos aCellDocPos( GetDocPos( rCellPos ) );
        // fixed doc size of the entire cell
        const ScHTMLSize aCellDocSize( GetDocSize( rCellPos ) );

        // running doc position for single entries
        ScHTMLPos aEntryDocPos( aCellDocPos );

        ScHTMLEntry* pEntry = nullptr;
        for( const auto& rpEntry : rEntryVector )
        {
            pEntry = rpEntry;
            if( ScHTMLTable* pTable = GetExistingTable( pEntry->GetTableId() ) )
            {
                pTable->RecalcDocPos( aEntryDocPos );   // recalc nested table
                pEntry->nCol = SCCOL_MAX;
                pEntry->nRow = SCROW_MAX;
                SCROW nTableRows = static_cast< SCROW >( pTable->GetDocSize( tdRow ) );

                // use this entry to pad empty space right of table
                if( mpParentTable )     // ... but not in global table
                {
                    SCCOL nStartCol = aEntryDocPos.mnCol + static_cast< SCCOL >( pTable->GetDocSize( tdCol ) );
                    SCCOL nNextCol = aEntryDocPos.mnCol + aCellDocSize.mnCols;
                    if( nStartCol < nNextCol )
                    {
                        pEntry->nCol = nStartCol;
                        pEntry->nRow = aEntryDocPos.mnRow;
                        pEntry->nColOverlap = nNextCol - nStartCol;
                        pEntry->nRowOverlap = nTableRows;
                    }
                }
                aEntryDocPos.mnRow += nTableRows;
            }
            else
            {
                pEntry->nCol = aEntryDocPos.mnCol;
                pEntry->nRow = aEntryDocPos.mnRow;
                if( mpParentTable )    // do not merge in global table
                    pEntry->nColOverlap = aCellDocSize.mnCols;
                ++aEntryDocPos.mnRow;
            }
        }

        // pEntry points now to last entry.
        if( pEntry )
        {
            if( (pEntry == rEntryVector.front()) && (pEntry->GetTableId() == SC_HTML_NO_TABLE) )
            {
                // pEntry is the only entry in this cell - merge rows of cell with single non-table entry.
                pEntry->nRowOverlap = aCellDocSize.mnRows;
            }
            else
            {
                // fill up incomplete entry lists
                SCROW nFirstUnusedRow = aCellDocPos.mnRow + aCellDocSize.mnRows;
                while( aEntryDocPos.mnRow < nFirstUnusedRow )
                {
                    ScHTMLEntryPtr xDummyEntry( new ScHTMLEntry( pEntry->GetItemSet() ) );
                    xDummyEntry->nCol = aEntryDocPos.mnCol;
                    xDummyEntry->nRow = aEntryDocPos.mnRow;
                    xDummyEntry->nColOverlap = aCellDocSize.mnCols;
                    ImplPushEntryToVector( rEntryVector, xDummyEntry );
                    ++aEntryDocPos.mnRow;
                }
            }
        }
    }
}

ScHTMLGlobalTable::ScHTMLGlobalTable(
    SfxItemPool& rPool,
    EditEngine& rEditEngine,
    std::vector<std::shared_ptr<ScEEParseEntry>>& rEEParseVector,
    ScHTMLTableId& rnUnusedId,
    ScHTMLParser* pParser,
    const ScDocument& rDoc
) :
    ScHTMLTable( rPool, rEditEngine, rEEParseVector, rnUnusedId, pParser, rDoc )
{
}

ScHTMLGlobalTable::~ScHTMLGlobalTable()
{
}

void ScHTMLGlobalTable::Recalc()
{
    // Fills up empty cells with a dummy entry. */
    FillEmptyCells();
    // recalc table sizes of all nested tables and this table
    RecalcDocSize();
    // recalc document positions of all entries in this table and in nested tables
    RecalcDocPos( GetDocPos() );
}

ScHTMLQueryParser::ScHTMLQueryParser( EditEngine* pEditEngine, ScDocument& rDoc ) :
    ScHTMLParser( pEditEngine, rDoc ),
    mnUnusedId( SC_HTML_GLOBAL_TABLE ),
    mbTitleOn( false )
{
    mxGlobTable.reset(
        new ScHTMLGlobalTable(*pPool, *pEdit, maList, mnUnusedId, this, rDoc));
    mpCurrTable = mxGlobTable.get();
}

ScHTMLQueryParser::~ScHTMLQueryParser()
{
}

ErrCode ScHTMLQueryParser::Read( SvStream& rStrm, const OUString& rBaseURL  )
{
    SvKeyValueIteratorRef xValues;
    SvKeyValueIterator* pAttributes = nullptr;

    ScDocShell* pObjSh = mrDoc.GetDocumentShell();
    if( pObjSh && pObjSh->IsLoading() )
    {
        pAttributes = pObjSh->GetHeaderAttributes();
    }
    else
    {
        /*  When not loading, set up fake HTTP headers to force the SfxHTMLParser
            to use UTF8 (used when pasting from clipboard) */
        const char* pCharSet = rtl_getBestMimeCharsetFromTextEncoding( RTL_TEXTENCODING_UTF8 );
        if( pCharSet )
        {
            OUString aContentType = "text/html; charset=" +
                OUString::createFromAscii( pCharSet );

            xValues = new SvKeyValueIterator;
            xValues->Append( SvKeyValue( u"" OOO_STRING_SVTOOLS_HTML_META_content_type ""_ustr, aContentType ) );
            pAttributes = xValues.get();
        }
    }

    Link<HtmlImportInfo&,void> aOldLink = pEdit->GetHtmlImportHdl();
    pEdit->SetHtmlImportHdl( LINK( this, ScHTMLQueryParser, HTMLImportHdl ) );
    ErrCode nErr = pEdit->Read( rStrm, rBaseURL, EETextFormat::Html, pAttributes );
    pEdit->SetHtmlImportHdl( aOldLink );

    mxGlobTable->Recalc();
    nColMax = static_cast< SCCOL >( mxGlobTable->GetDocSize( tdCol ) - 1 );
    nRowMax = static_cast< SCROW >( mxGlobTable->GetDocSize( tdRow ) - 1 );

    return nErr;
}

const ScHTMLTable* ScHTMLQueryParser::GetGlobalTable() const
{
    return mxGlobTable.get();
}

void ScHTMLQueryParser::ProcessToken( const HtmlImportInfo& rInfo )
{
    switch( rInfo.nToken )
    {
// --- meta data ---
        case HtmlTokenId::META:             MetaOn( rInfo );                break;  // <meta>

// --- title handling ---
        case HtmlTokenId::TITLE_ON:         TitleOn();                      break;  // <title>
        case HtmlTokenId::TITLE_OFF:        TitleOff( rInfo );              break;  // </title>

        case HtmlTokenId::STYLE_ON:                                         break;
        case HtmlTokenId::STYLE_OFF:        ParseStyle(rInfo.aText);        break;

// --- body handling ---
        case HtmlTokenId::BODY_ON:          mpCurrTable->BodyOn( rInfo );   break;  // <body>
        case HtmlTokenId::BODY_OFF:         mpCurrTable->BodyOff( rInfo );  break;  // </body>

// --- insert text ---
        case HtmlTokenId::TEXTTOKEN:        InsertText( rInfo );            break;  // any text
        case HtmlTokenId::LINEBREAK:        mpCurrTable->BreakOn();         break;  // <br>
        case HtmlTokenId::HEAD1_ON:                                                 // <h1>
        case HtmlTokenId::HEAD2_ON:                                                 // <h2>
        case HtmlTokenId::HEAD3_ON:                                                 // <h3>
        case HtmlTokenId::HEAD4_ON:                                                 // <h4>
        case HtmlTokenId::HEAD5_ON:                                                 // <h5>
        case HtmlTokenId::HEAD6_ON:                                                 // <h6>
        case HtmlTokenId::PARABREAK_ON:     mpCurrTable->HeadingOn();       break;  // <p>

// --- misc. contents ---
        case HtmlTokenId::ANCHOR_ON:        mpCurrTable->AnchorOn();        break;  // <a>

// --- table handling ---
        case HtmlTokenId::TABLE_ON:         TableOn( rInfo );               break;  // <table>
        case HtmlTokenId::TABLE_OFF:        TableOff( rInfo );              break;  // </table>
        case HtmlTokenId::CAPTION_ON:       mpCurrTable->CaptionOn();       break;  // <caption>
        case HtmlTokenId::CAPTION_OFF:      mpCurrTable->CaptionOff();      break;  // </caption>
        case HtmlTokenId::TABLEROW_ON:      mpCurrTable->RowOn( rInfo );    break;  // <tr>
        case HtmlTokenId::TABLEROW_OFF:     mpCurrTable->RowOff( rInfo );   break;  // </tr>
        case HtmlTokenId::TABLEHEADER_ON:                                           // <th>
        case HtmlTokenId::TABLEDATA_ON:     mpCurrTable->DataOn( rInfo );   break;  // <td>
        case HtmlTokenId::TABLEHEADER_OFF:                                          // </th>
        case HtmlTokenId::TABLEDATA_OFF:    mpCurrTable->DataOff( rInfo );  break;  // </td>
        case HtmlTokenId::PREFORMTXT_ON:    PreOn( rInfo );                 break;  // <pre>
        case HtmlTokenId::PREFORMTXT_OFF:   PreOff( rInfo );                break;  // </pre>

// --- formatting ---
        case HtmlTokenId::FONT_ON:          FontOn( rInfo );                break;  // <font>

        case HtmlTokenId::BIGPRINT_ON:      // <big>
            //! TODO: store current font size, use following size
            mpCurrTable->PutItem( SvxFontHeightItem( maFontHeights[ 3 ], 100, ATTR_FONT_HEIGHT ) );
        break;
        case HtmlTokenId::SMALLPRINT_ON:    // <small>
            //! TODO: store current font size, use preceding size
            mpCurrTable->PutItem( SvxFontHeightItem( maFontHeights[ 0 ], 100, ATTR_FONT_HEIGHT ) );
        break;

        case HtmlTokenId::BOLD_ON:          // <b>
        case HtmlTokenId::STRONG_ON:        // <strong>
            mpCurrTable->PutItem( SvxWeightItem( WEIGHT_BOLD, ATTR_FONT_WEIGHT ) );
        break;

        case HtmlTokenId::ITALIC_ON:        // <i>
        case HtmlTokenId::EMPHASIS_ON:      // <em>
        case HtmlTokenId::ADDRESS_ON:       // <address>
        case HtmlTokenId::BLOCKQUOTE_ON:    // <blockquote>
        case HtmlTokenId::BLOCKQUOTE30_ON:  // <bq>
        case HtmlTokenId::CITATION_ON:      // <cite>
        case HtmlTokenId::VARIABLE_ON:      // <var>
            mpCurrTable->PutItem( SvxPostureItem( ITALIC_NORMAL, ATTR_FONT_POSTURE ) );
        break;

        case HtmlTokenId::DEFINSTANCE_ON:   // <dfn>
            mpCurrTable->PutItem( SvxWeightItem( WEIGHT_BOLD, ATTR_FONT_WEIGHT ) );
            mpCurrTable->PutItem( SvxPostureItem( ITALIC_NORMAL, ATTR_FONT_POSTURE ) );
        break;

        case HtmlTokenId::UNDERLINE_ON:     // <u>
        case HtmlTokenId::INSERTEDTEXT_ON:  // <ins>
            mpCurrTable->PutItem( SvxUnderlineItem( LINESTYLE_SINGLE, ATTR_FONT_UNDERLINE ) );
        break;
        default: break;
    }
}

void ScHTMLQueryParser::InsertText( const HtmlImportInfo& rInfo )
{
    mpCurrTable->PutText( rInfo );
    if( mbTitleOn )
        maTitle.append(rInfo.aText);
}

void ScHTMLQueryParser::FontOn( const HtmlImportInfo& rInfo )
{
    const HTMLOptions& rOptions = static_cast<HTMLParser*>(rInfo.pParser)->GetOptions();
    for (const auto& rOption : rOptions)
    {
        switch( rOption.GetToken() )
        {
            case HtmlOptionId::FACE :
            {
                const OUString& rFace = rOption.GetString();
                OUString aFontName;
                sal_Int32 nPos = 0;
                while( nPos != -1 )
                {
                    // font list separator: VCL = ';' HTML = ','
                    std::u16string_view aFName = comphelper::string::strip(o3tl::getToken(rFace, 0, ',', nPos), ' ');
                    aFontName = ScGlobal::addToken(aFontName, aFName, ';');
                }
                if ( !aFontName.isEmpty() )
                    mpCurrTable->PutItem( SvxFontItem( FAMILY_DONTKNOW,
                        aFontName, OUString(), PITCH_DONTKNOW,
                        RTL_TEXTENCODING_DONTKNOW, ATTR_FONT ) );
            }
            break;
            case HtmlOptionId::SIZE :
            {
                sal_uInt32 nSize = getLimitedValue< sal_uInt32 >( rOption.GetNumber(), 1, SC_HTML_FONTSIZES );
                mpCurrTable->PutItem( SvxFontHeightItem( maFontHeights[ nSize - 1 ], 100, ATTR_FONT_HEIGHT ) );
            }
            break;
            case HtmlOptionId::COLOR :
            {
                Color aColor;
                rOption.GetColor( aColor );
                mpCurrTable->PutItem( SvxColorItem( aColor, ATTR_FONT_COLOR ) );
            }
            break;
            default: break;
        }
    }
}

void ScHTMLQueryParser::MetaOn( const HtmlImportInfo& rInfo )
{
    if( mrDoc.GetDocumentShell() )
    {
        HTMLParser* pParser = static_cast< HTMLParser* >( rInfo.pParser );

        uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
            static_cast<cppu::OWeakObject*>(mrDoc.GetDocumentShell()->GetModel()), uno::UNO_QUERY_THROW);
        pParser->ParseMetaOptions(
            xDPS->getDocumentProperties(),
            mrDoc.GetDocumentShell()->GetHeaderAttributes() );
    }
}

void ScHTMLQueryParser::TitleOn()
{
    mbTitleOn = true;
    maTitle.setLength(0);
}

void ScHTMLQueryParser::TitleOff( const HtmlImportInfo& rInfo )
{
    if( !mbTitleOn )
        return;

    OUString aTitle = maTitle.makeStringAndClear().trim();
    if (!aTitle.isEmpty() && mrDoc.GetDocumentShell())
    {
        uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
            static_cast<cppu::OWeakObject*>(mrDoc.GetDocumentShell()->GetModel()), uno::UNO_QUERY_THROW);

        xDPS->getDocumentProperties()->setTitle(aTitle);
    }
    InsertText( rInfo );
    mbTitleOn = false;
}

void ScHTMLQueryParser::TableOn( const HtmlImportInfo& rInfo )
{
    mpCurrTable = mpCurrTable->TableOn( rInfo );
}

void ScHTMLQueryParser::TableOff( const HtmlImportInfo& rInfo )
{
    mpCurrTable = mpCurrTable->TableOff( rInfo );
}

void ScHTMLQueryParser::PreOn( const HtmlImportInfo& rInfo )
{
    mpCurrTable = mpCurrTable->PreOn( rInfo );
}

void ScHTMLQueryParser::PreOff( const HtmlImportInfo& rInfo )
{
    mpCurrTable = mpCurrTable->PreOff( rInfo );
}

void ScHTMLQueryParser::CloseTable( const HtmlImportInfo& rInfo )
{
    mpCurrTable = mpCurrTable->CloseTable( rInfo );
}

namespace {

/**
 * Handler class for the CSS parser.
 */
class CSSHandler: public orcus::css_handler
{
    typedef std::pair<std::string_view, std::string_view> SelectorName; // element : class
    typedef std::vector<SelectorName> SelectorNames;

    SelectorNames maSelectorNames;      // current selector names
    std::string_view maPropName;        // current property name.
    std::string_view maPropValue;                 // current property value.
    ScHTMLStyles& mrStyles;

public:
    explicit CSSHandler(ScHTMLStyles& rStyles):
        mrStyles(rStyles)
     {}

    // selector name not starting with "." or "#" (i.e. element selectors)
    void simple_selector_type(std::string_view aElem)
    {
        std::string_view aClass{};  // class name not given - to be added in the "element global" storage
        SelectorName aName(aElem, aClass);

        maSelectorNames.push_back(aName);
    }

    // selector names starting with a "." (i.e. class selector)
    void simple_selector_class(std::string_view aClass)
    {
        std::string_view aElem{};   // no element given - should be added in the "global" storage
        SelectorName aName(aElem, aClass);

        maSelectorNames.push_back(aName);
    }

    // TODO: Add other selectors

    void property_name(std::string_view aPropName)
    {
        maPropName = aPropName;
    }

    void value(std::string_view aValue)
    {
        maPropValue = aValue;
    }

    void end_block()
    {
        maSelectorNames.clear();
    }

    void end_property()
    {
        for (const auto& rSelName : maSelectorNames)
        {
            // Add this property to the collection for each selector.
            std::string_view aElem = rSelName.first;
            std::string_view aClass = rSelName.second;
            OUString aName(maPropName.data(), maPropName.size(), RTL_TEXTENCODING_UTF8);
            OUString aValue(maPropValue.data(), maPropValue.size(), RTL_TEXTENCODING_UTF8);
            mrStyles.add(aElem.data(), aElem.size(), aClass.data(), aClass.size(), aName, aValue);
        }
        maPropName = std::string_view{};
        maPropValue = std::string_view{};
    }

};

}

void ScHTMLQueryParser::ParseStyle(std::u16string_view rStrm)
{
    OString aStr = OUStringToOString(rStrm, RTL_TEXTENCODING_UTF8);
    CSSHandler aHdl(GetStyles());
    orcus::css_parser<CSSHandler> aParser(aStr, aHdl);
    try
    {
        aParser.parse();
    }
    catch (const orcus::parse_error& rOrcusParseError)
    {
        SAL_WARN("sc", "ScHTMLQueryParser::ParseStyle: " << rOrcusParseError.what());
        // TODO: Parsing of CSS failed.  Do nothing for now.
    }
}

IMPL_LINK( ScHTMLQueryParser, HTMLImportHdl, HtmlImportInfo&, rInfo, void )
{
    switch( rInfo.eState )
    {
        case HtmlImportState::Start:
        break;

        case HtmlImportState::NextToken:
            ProcessToken( rInfo );
        break;

        case HtmlImportState::InsertPara:
            mpCurrTable->InsertPara( rInfo );
        break;

        case HtmlImportState::SetAttr:
        case HtmlImportState::InsertText:
        case HtmlImportState::InsertField:
        break;

        case HtmlImportState::End:
            while( mpCurrTable->GetTableId() != SC_HTML_GLOBAL_TABLE )
                CloseTable( rInfo );
        break;

        default:
            OSL_FAIL( "ScHTMLQueryParser::HTMLImportHdl - unknown ImportInfo::eState" );
    }
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
