/* -*- 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/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* pDoc ) :
ScEEParser( pEditEngine ),
mpDoc( pDoc )
{
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* pDocP ) :
ScHTMLParser( pEditP, pDocP ),
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 = mpDoc->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.nStartPara = pE->aSel.nEndPara;
mxActEntry->aSel.nStartPos = pE->aSel.nEndPos;
}
}
mxActEntry->aSel.nEndPara = mxActEntry->aSel.nStartPara;
mxActEntry->aSel.nEndPos = mxActEntry->aSel.nStartPos;
}
void ScHTMLLayoutParser::EntryEnd( ScEEParseEntry* pE, const ESelection& rSel )
{
if ( rSel.nEndPara >= pE->aSel.nStartPara )
{
pE->aSel.nEndPara = rSel.nEndPara;
pE->aSel.nEndPos = rSel.nEndPos;
}
else if ( rSel.nStartPara == pE->aSel.nStartPara - 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 ( !mpDoc->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 > mpDoc->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 > mpDoc->MaxCol() || nTmp > mpDoc->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.nStartPara < rSel.nEndPara
&& pEdit->GetTextLen( rSel.nStartPara ) == 0 )
{ // Strip preceding empty paragraphs
rSel.nStartPara++;
}
while ( rSel.nEndPos == 0 && rSel.nEndPara > rSel.nStartPara )
{ // Strip successive empty paragraphs
rSel.nEndPara--;
rSel.nEndPos = pEdit->GetTextLen( rSel.nEndPara );
}
if ( rSel.nStartPara > rSel.nEndPara )
{ // Gives GPF in CreateTextObject
OSL_FAIL( "CloseEntry: EditEngine ESelection Start > End" );
rSel.nEndPara = rSel.nStartPara;
}
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.nEndPos )
{
// 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 <= mpDoc->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 <= mpDoc->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.nStartPara == rSel.nEndPara &&
rSel.nStartPara <= pInfo->aSelection.nEndPara &&
pEdit->GetTextLen( rSel.nStartPara ) == 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 = mpDoc->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 = mpDoc->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::UNDERLINE_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.nStartPara = rInfo.aSelection.nStartPara;
aSel.nStartPos = rInfo.aSelection.nStartPos;
// adjust end position
if( (aSel.nEndPara < aSel.nStartPara) || ((aSel.nEndPara == aSel.nStartPara) && (aSel.nEndPos < aSel.nStartPos)) )
{
aSel.nEndPara = aSel.nStartPara;
aSel.nEndPos = aSel.nStartPos;
}
}
void ScHTMLEntry::AdjustEnd( const HtmlImportInfo& rInfo )
{
OSL_ENSURE( (aSel.nEndPara < rInfo.aSelection.nEndPara) ||
((aSel.nEndPara == rInfo.aSelection.nEndPara) && (aSel.nEndPos <= rInfo.aSelection.nEndPos)),
"ScHTMLQueryParser::AdjustEntryEnd - invalid end position" );
// set end position
aSel.nEndPara = rInfo.aSelection.nEndPara;
aSel.nEndPos = rInfo.aSelection.nEndPos;
}
void ScHTMLEntry::Strip( const EditEngine& rEditEngine )
{
// strip leading empty paragraphs
while( (aSel.nStartPara < aSel.nEndPara) && (rEditEngine.GetTextLen( aSel.nStartPara ) <= aSel.nStartPos) )
{
++aSel.nStartPara;
aSel.nStartPos = 0;
}
// strip trailing empty paragraphs
while( (aSel.nStartPara < aSel.nEndPara) && (aSel.nEndPos == 0) )
{
--aSel.nEndPara;
aSel.nEndPos = rEditEngine.GetTextLen( aSel.nEndPara );
}
}
/** 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* pDoc, const ScAddress& rFirstPos ) const
{
OSL_ENSURE( pDoc, "ScHTMLTable::ApplyCellBorders - no document" );
if( pDoc && 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 );
pDoc->ApplyAttr( nCellCol, nCellRow, rFirstPos.Tab(), aBorderItem );
}
}
}
}
}
for( ScHTMLTableIterator aIter( mxNestedTables.get() ); aIter.is(); ++aIter )
aIter->ApplyCellBorders( pDoc, 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* pDoc ) :
ScHTMLParser( pEditEngine, pDoc ),
mnUnusedId( SC_HTML_GLOBAL_TABLE ),
mbTitleOn( false )
{
mxGlobTable.reset(
new ScHTMLGlobalTable(*pPool, *pEdit, maList, mnUnusedId, this, *pDoc));
mpCurrTable = mxGlobTable.get();
}
ScHTMLQueryParser::~ScHTMLQueryParser()
{
}
ErrCode ScHTMLQueryParser::Read( SvStream& rStrm, const OUString& rBaseURL )
{
SvKeyValueIteratorRef xValues;
SvKeyValueIterator* pAttributes = nullptr;
ScDocShell* pObjSh = mpDoc->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>
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( mpDoc->GetDocumentShell() )
{
HTMLParser* pParser = static_cast< HTMLParser* >( rInfo.pParser );
uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
static_cast<cppu::OWeakObject*>(mpDoc->GetDocumentShell()->GetModel()), uno::UNO_QUERY_THROW);
pParser->ParseMetaOptions(
xDPS->getDocumentProperties(),
mpDoc->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() && mpDoc->GetDocumentShell())
{
uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
static_cast<cppu::OWeakObject*>(mpDoc->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: */
↑ V530 The return value of function 'append' is required to be utilized.
↑ V530 The return value of function 'append' is required to be utilized.
↑ V530 The return value of function 'append' is required to be utilized.
↑ V547 Expression '!"can't move"' is always false.
↑ V547 Expression '!"can't move"' is always false.