/* -*- 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 <sal/config.h>

#include <string_view>

#include <scitems.hxx>
#include <editeng/eeitem.hxx>

#include <utility>
#include <vcl/svapp.hxx>
#include <editeng/boxitem.hxx>
#include <editeng/brushitem.hxx>
#include <editeng/colritem.hxx>
#include <editeng/crossedoutitem.hxx>
#include <editeng/fhgtitem.hxx>
#include <editeng/fontitem.hxx>
#include <editeng/postitem.hxx>
#include <editeng/udlnitem.hxx>
#include <editeng/wghtitem.hxx>
#include <editeng/justifyitem.hxx>
#include <svx/xoutbmp.hxx>
#include <editeng/editeng.hxx>
#include <svtools/htmlcfg.hxx>
#include <sfx2/docfile.hxx>
#include <sfx2/frmhtmlw.hxx>
#include <sfx2/objsh.hxx>
#include <svl/urihelper.hxx>
#include <svtools/htmlkywd.hxx>
#include <svtools/htmlout.hxx>
#include <svtools/parhtml.hxx>
#include <vcl/outdev.hxx>
#include <stdio.h>
#include <osl/diagnose.h>
#include <o3tl/unit_conversion.hxx>
#include <o3tl/string_view.hxx>

#include <htmlexp.hxx>
#include <global.hxx>
#include <postit.hxx>
#include <document.hxx>
#include <docsh.hxx>
#include <attrib.hxx>
#include <patattr.hxx>
#include <stlpool.hxx>
#include <scresid.hxx>
#include <formulacell.hxx>
#include <cellform.hxx>
#include <docoptio.hxx>
#include <editutil.hxx>
#include <ftools.hxx>
#include <cellvalue.hxx>
#include <conditio.hxx>
#include <colorscale.hxx>
#include <mtvelements.hxx>

#include <editeng/flditem.hxx>
#include <editeng/borderline.hxx>

// Without strings.hrc: error C2679: binary '=' : no operator defined which takes a
// right-hand operand of type 'const class String (__stdcall *)(class ScResId)'
// at
// const String aStrTable( ScResId( SCSTR_TABLE ) ); aStrOut = aStrTable;
// ?!???
#include <strings.hrc>
#include <globstr.hrc>

#include <com/sun/star/frame/XModel.hpp>
#include <com/sun/star/uno/Reference.h>
#include <com/sun/star/document/XDocumentProperties.hpp>
#include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
#include <rtl/strbuf.hxx>
#include <officecfg/Office/Common.hxx>
#include <tools/json_writer.hxx>
#include <svl/numformat.hxx>
#include <svl/zformat.hxx>

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

const char sMyBegComment[]   = "<!-- ";
const char sMyEndComment[]   = " -->";
const char sDisplay[]        = "display:";
const char sBorder[]         = "border:";
const char sBackground[]     = "background:";

const sal_uInt16 ScHTMLExport::nDefaultFontSize[SC_HTML_FONTSIZES] =
{
    HTMLFONTSZ1_DFLT, HTMLFONTSZ2_DFLT, HTMLFONTSZ3_DFLT, HTMLFONTSZ4_DFLT,
    HTMLFONTSZ5_DFLT, HTMLFONTSZ6_DFLT, HTMLFONTSZ7_DFLT
};

sal_uInt16 ScHTMLExport::nFontSize[SC_HTML_FONTSIZES] = { 0 };

const char* const ScHTMLExport::pFontSizeCss[SC_HTML_FONTSIZES] =
{
    "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"
};

const sal_uInt16 ScHTMLExport::nCellSpacing = 0;
const char ScHTMLExport::sIndentSource[nIndentMax+1] =
    "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

// Macros for HTML export

#define TAG_ON( tag )       HTMLOutFuncs::Out_AsciiTag( rStrm, tag )
#define TAG_OFF( tag )      HTMLOutFuncs::Out_AsciiTag( rStrm, tag, false )
#define OUT_STR( str )      HTMLOutFuncs::Out_String( rStrm, str, &aNonConvertibleChars )
#define OUT_LF()            rStrm.WriteOString( SAL_NEWLINE_STRING ).WriteOString( GetIndentStr() )
#define TAG_ON_LF( tag )    (TAG_ON( tag ).WriteOString( SAL_NEWLINE_STRING ).WriteOString( GetIndentStr() ))
#define TAG_OFF_LF( tag )   (TAG_OFF( tag ).WriteOString( SAL_NEWLINE_STRING ).WriteOString( GetIndentStr() ))
#define OUT_HR()            TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_horzrule )
#define OUT_COMMENT( comment )  (rStrm.WriteOString( sMyBegComment ), OUT_STR( comment ) \
                                .WriteOString( sMyEndComment ).WriteOString( SAL_NEWLINE_STRING ) \
                                .WriteOString( GetIndentStr() ))

#define OUT_SP_CSTR_ASS( s )    rStrm.WriteChar(' ').WriteOString( s ).WriteChar( '=' )

#define GLOBSTR(id) ScResId( id )

void ScFormatFilterPluginImpl::ScExportHTML( SvStream& rStrm, const OUString& rBaseURL, ScDocument* pDoc,
        const ScRange& rRange, const rtl_TextEncoding /*eNach*/, bool bAll,
        const OUString& rStreamPath, OUString& rNonConvertibleChars, const OUString& rFilterOptions )
{
    ScHTMLExport aEx( rStrm, rBaseURL, pDoc, rRange, bAll, rStreamPath, rFilterOptions );
    aEx.Write();
    rNonConvertibleChars = aEx.GetNonConvertibleChars();
}

static OString lcl_getColGroupString(sal_Int32 nSpan, sal_Int32 nWidth)
{
    OStringBuffer aByteStr(OString::Concat(OOO_STRING_SVTOOLS_HTML_colgroup)
        + " ");
    if( nSpan > 1 )
    {
        aByteStr.append(OString::Concat(OOO_STRING_SVTOOLS_HTML_O_span)
            + "=\""
            + OString::number(nSpan)
            + "\" ");
    }
    aByteStr.append(OString::Concat(OOO_STRING_SVTOOLS_HTML_O_width)
        + "=\""
        + OString::number(nWidth)
        + "\"");
    return aByteStr.makeStringAndClear();
}

static void lcl_AddStamp( OUString& rStr, std::u16string_view rName,
    const css::util::DateTime& rDateTime,
    const LocaleDataWrapper& rLoc )
{
    Date aD(rDateTime.Day, rDateTime.Month, rDateTime.Year);
    tools::Time aT(rDateTime.Hours, rDateTime.Minutes, rDateTime.Seconds,
            rDateTime.NanoSeconds);
    DateTime aDateTime(aD,aT);

    OUString        aStrDate    = rLoc.getDate( aDateTime );
    OUString        aStrTime    = rLoc.getTime( aDateTime );

    rStr += GLOBSTR( STR_BY ) + " ";
    if (!rName.empty())
        rStr += rName;
    else
        rStr += "???";
    rStr += " " + GLOBSTR( STR_ON ) + " ";
    if (!aStrDate.isEmpty())
        rStr += aStrDate;
    else
        rStr += "???";
    rStr += ", ";
    if (!aStrTime.isEmpty())
        rStr += aStrTime;
    else
        rStr += "???";
}

static OString lcl_makeHTMLColorTriplet(const Color& rColor)
{
    char    buf[24];

    // <font COLOR="#00FF40">hello</font>
    snprintf( buf, 24, "\"#%02X%02X%02X\"", rColor.GetRed(), rColor.GetGreen(), rColor.GetBlue() );

    return buf;
}

ScHTMLExport::ScHTMLExport( SvStream& rStrmP, OUString _aBaseURL, ScDocument* pDocP,
                            const ScRange& rRangeP, bool bAllP,
                            OUString aStreamPathP, std::u16string_view rFilterOptions ) :
    ScExportBase( rStrmP, pDocP, rRangeP ),
    aBaseURL(std::move( _aBaseURL )),
    aStreamPath(std::move( aStreamPathP )),
    pAppWin( Application::GetDefaultDevice() ),
    nUsedTables( 0 ),
    nIndent( 0 ),
    bAll( bAllP ),
    bTabHasGraphics( false ),
    bTabAlignedLeft( false ),
    bCalcAsShown( pDocP->GetDocOptions().IsCalcAsShown() ),
    bTableDataHeight( true ),
    mbSkipImages ( false ),
    mbSkipHeaderFooter( false )
{
    strcpy( sIndent, sIndentSource );
    sIndent[0] = 0;

    // set HTML configuration
    bCopyLocalFileToINet = officecfg::Office::Common::Filter::HTML::Export::LocalGraphic::get();

    if (rFilterOptions == u"SkipImages")
    {
        mbSkipImages = true;
    }
    else if (rFilterOptions == u"SkipHeaderFooter")
    {
        mbSkipHeaderFooter = true;
    }

    for ( sal_uInt16 j=0; j < SC_HTML_FONTSIZES; j++ )
    {
        sal_uInt16 nSize = SvxHtmlOptions::GetFontSize( j );
        // remember in Twips, like our SvxFontHeightItem
        if ( nSize )
            nFontSize[j] = nSize * 20;
        else
            nFontSize[j] = nDefaultFontSize[j] * 20;
    }

    const SCTAB nCount = pDoc->GetTableCount();
    for ( SCTAB nTab = 0; nTab < nCount; nTab++ )
    {
        if ( !IsEmptyTable( nTab ) )
            nUsedTables++;
    }
}

ScHTMLExport::~ScHTMLExport()
{
    aGraphList.clear();
}

sal_uInt16 ScHTMLExport::GetFontSizeNumber( sal_uInt16 nHeight )
{
    sal_uInt16 nSize = 1;
    for ( sal_uInt16 j=SC_HTML_FONTSIZES-1; j>0; j-- )
    {
        if( nHeight > (nFontSize[j] + nFontSize[j-1]) / 2 )
        {   // The one next to it
            nSize = j+1;
            break;
        }
    }
    return nSize;
}

const char* ScHTMLExport::GetFontSizeCss( sal_uInt16 nHeight )
{
    sal_uInt16 nSize = GetFontSizeNumber( nHeight );
    return pFontSizeCss[ nSize-1 ];
}

sal_uInt16 ScHTMLExport::ToPixel( sal_uInt16 nVal )
{
    if( nVal )
    {
        nVal = static_cast<sal_uInt16>(pAppWin->LogicToPixel(
                    Size( nVal, nVal ), MapMode( MapUnit::MapTwip ) ).Width());
        if( !nVal ) // If there's a Twip there should also be a Pixel
            nVal = 1;
    }
    return nVal;
}

Size ScHTMLExport::MMToPixel( const Size& rSize )
{
    Size aSize = pAppWin->LogicToPixel( rSize, MapMode( MapUnit::Map100thMM ) );
    // If there's something there should also be a Pixel
    if ( !aSize.Width() && rSize.Width() )
        aSize.setWidth( 1 );
    if ( !aSize.Height() && rSize.Height() )
        aSize.setHeight( 1 );
    return aSize;
}

void ScHTMLExport::Write()
{
    if (!mbSkipHeaderFooter)
    {
        rStrm.WriteChar( '<' ).WriteOString( OOO_STRING_SVTOOLS_HTML_doctype ).WriteChar( ' ' ).WriteOString( OOO_STRING_SVTOOLS_HTML_doctype5 ).WriteChar( '>' )
           .WriteOString( SAL_NEWLINE_STRING ).WriteOString( SAL_NEWLINE_STRING );
        TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_html );
        WriteHeader();
        OUT_LF();
    }
    WriteBody();
    OUT_LF();
    if (!mbSkipHeaderFooter)
        TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_html );
}

void ScHTMLExport::WriteHeader()
{
    IncIndent(1); TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_head );

    if ( pDoc->IsClipOrUndo() )
    {   // no real DocInfo available, but some META information like charset needed
        SfxFrameHTMLWriter::Out_DocInfo( rStrm, aBaseURL, nullptr, sIndent, &aNonConvertibleChars );
    }
    else
    {
        uno::Reference<document::XDocumentPropertiesSupplier> xDPS(
            static_cast<cppu::OWeakObject*>(pDoc->GetDocumentShell()->GetModel()), uno::UNO_QUERY_THROW);
        uno::Reference<document::XDocumentProperties> xDocProps
            = xDPS->getDocumentProperties();
        SfxFrameHTMLWriter::Out_DocInfo( rStrm, aBaseURL, xDocProps,
            sIndent, &aNonConvertibleChars );
        OUT_LF();

        if (!xDocProps->getPrintedBy().isEmpty())
        {
            OUT_COMMENT( GLOBSTR( STR_DOC_INFO ) );
            OUString aStrOut = GLOBSTR( STR_DOC_PRINTED ) + ": ";
            lcl_AddStamp( aStrOut, xDocProps->getPrintedBy(),
                xDocProps->getPrintDate(), ScGlobal::getLocaleData() );
            OUT_COMMENT( aStrOut );
        }

    }
    OUT_LF();

    // CSS1 StyleSheet
    PageDefaults( bAll ? 0 : aRange.aStart.Tab() );
    IncIndent(1);
    rStrm.WriteOString( "<" ).WriteOString( OOO_STRING_SVTOOLS_HTML_style ).WriteOString( " " ).WriteOString( OOO_STRING_SVTOOLS_HTML_O_type ).WriteOString( "=\"text/css\">" );

    OUT_LF();
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_body);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_division);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_table);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_thead);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_tbody);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_tfoot);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_tablerow);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_tableheader);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_tabledata);
    rStrm.WriteOString(",");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_parabreak);
    rStrm.WriteOString(" { ");
    rStrm.WriteOString("font-family:");

    if (!aHTMLStyle.aFontFamilyName.isEmpty())
    {
        const OUString& rList = aHTMLStyle.aFontFamilyName;
        for(sal_Int32 nPos {0};;)
        {
            rStrm.WriteChar( '\"' );
            OUT_STR( o3tl::getToken( rList, 0, ';', nPos ) );
            rStrm.WriteChar( '\"' );
            if (nPos<0)
                break;
            rStrm.WriteOString( ", " );
        }
    }
    rStrm.WriteOString("; ");
    rStrm.WriteOString("font-size:");
    rStrm.WriteOString(GetFontSizeCss(static_cast<sal_uInt16>(aHTMLStyle.nFontHeight)));
    rStrm.WriteOString(" }");

    OUT_LF();

    // write the style for the comments to make them stand out from normal cell content
    // this is done through only showing the cell contents when the custom indicator is hovered
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_anchor);
    rStrm.WriteOString(".comment-indicator:hover");
    rStrm.WriteOString(" + ");
    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_comment2);
    rStrm.WriteOString(" { ");
    rStrm.WriteOString(sBackground);
    rStrm.WriteOString("#ffd");
    rStrm.WriteOString("; ");
    rStrm.WriteOString("position:");
    rStrm.WriteOString("absolute");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(sDisplay);
    rStrm.WriteOString("block");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(sBorder);
    rStrm.WriteOString("1px solid black");
    rStrm.WriteOString("; ");
    rStrm.WriteOString("padding:");
    rStrm.WriteOString("0.5em");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(" } ");

    OUT_LF();

    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_anchor);
    rStrm.WriteOString(".comment-indicator");
    rStrm.WriteOString(" { ");
    rStrm.WriteOString(sBackground);
    rStrm.WriteOString("red");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(sDisplay);
    rStrm.WriteOString("inline-block");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(sBorder);
    rStrm.WriteOString("1px solid black");
    rStrm.WriteOString("; ");
    rStrm.WriteOString("width:");
    rStrm.WriteOString("0.5em");
    rStrm.WriteOString("; ");
    rStrm.WriteOString("height:");
    rStrm.WriteOString("0.5em");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(" } ");

    OUT_LF();

    rStrm.WriteOString(OOO_STRING_SVTOOLS_HTML_comment2);
    rStrm.WriteOString(" { ");
    rStrm.WriteOString(sDisplay);
    rStrm.WriteOString("none");
    rStrm.WriteOString("; ");
    rStrm.WriteOString(" } ");


    IncIndent(-1);
    OUT_LF();
    TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_style );

    IncIndent(-1);
    OUT_LF();
    TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_head );
}

void ScHTMLExport::WriteOverview()
{
    if ( nUsedTables <= 1 )
        return;

    IncIndent(1);
    OUT_HR();
    IncIndent(1); TAG_ON( OOO_STRING_SVTOOLS_HTML_parabreak ); TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_center );
    TAG_ON( OOO_STRING_SVTOOLS_HTML_head1 );
    OUT_STR( ScResId( STR_OVERVIEW ) );
    TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_head1 );

    OUString aStr;

    const SCTAB nCount = pDoc->GetTableCount();
    for ( SCTAB nTab = 0; nTab < nCount; nTab++ )
    {
        if ( !IsEmptyTable( nTab ) )
        {
            pDoc->GetName( nTab, aStr );
            rStrm.WriteOString( "<A HREF=\"#table" )
               .WriteOString( OString::number(nTab) )
               .WriteOString( "\">" );
            OUT_STR( aStr );
            rStrm.WriteOString( "</A>" );
            TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_linebreak );
        }
    }

    IncIndent(-1); OUT_LF();
    IncIndent(-1); TAG_OFF( OOO_STRING_SVTOOLS_HTML_center ); TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_parabreak );
}

const SfxItemSet& ScHTMLExport::PageDefaults( SCTAB nTab )
{
    SfxStyleSheetBasePool*  pStylePool  = pDoc->GetStyleSheetPool();
    SfxStyleSheetBase*      pStyleSheet = nullptr;
    OSL_ENSURE( pStylePool, "StylePool not found! :-(" );

    // remember defaults for compare in WriteCell
    if ( !aHTMLStyle.bInitialized )
    {
        pStyleSheet = pStylePool->Find(
                ScResId(STR_STYLENAME_STANDARD),
                SfxStyleFamily::Para );
        OSL_ENSURE( pStyleSheet, "ParaStyle not found! :-(" );
        if (!pStyleSheet)
            pStyleSheet = pStylePool->First(SfxStyleFamily::Para);
        const SfxItemSet& rSetPara = pStyleSheet->GetItemSet();

        aHTMLStyle.nDefaultScriptType = ScGlobal::GetDefaultScriptType();
        aHTMLStyle.aFontFamilyName = static_cast<const SvxFontItem&>((rSetPara.Get(
                        ScGlobal::GetScriptedWhichID(
                            aHTMLStyle.nDefaultScriptType, ATTR_FONT
                            )))).GetFamilyName();
        aHTMLStyle.nFontHeight = static_cast<const SvxFontHeightItem&>((rSetPara.Get(
                        ScGlobal::GetScriptedWhichID(
                            aHTMLStyle.nDefaultScriptType, ATTR_FONT_HEIGHT
                            )))).GetHeight();
        aHTMLStyle.nFontSizeNumber = GetFontSizeNumber( static_cast< sal_uInt16 >( aHTMLStyle.nFontHeight ) );
    }

    // Page style sheet printer settings, e.g. for background graphics.
    // There's only one background graphic in HTML!
    pStyleSheet = pStylePool->Find( pDoc->GetPageStyle( nTab ), SfxStyleFamily::Page );
    OSL_ENSURE( pStyleSheet, "PageStyle not found! :-(" );
    if (!pStyleSheet)
        pStyleSheet = pStylePool->First(SfxStyleFamily::Page);
    const SfxItemSet& rSet = pStyleSheet->GetItemSet();
    if ( !aHTMLStyle.bInitialized )
    {
        const SvxBrushItem* pBrushItem = &rSet.Get( ATTR_BACKGROUND );
        aHTMLStyle.aBackgroundColor = pBrushItem->GetColor();
        aHTMLStyle.bInitialized = true;
    }
    return rSet;
}

OString ScHTMLExport::BorderToStyle(const char* pBorderName,
        const SvxBorderLine* pLine, bool& bInsertSemicolon)
{
    OStringBuffer aOut;

    if ( pLine )
    {
        if ( bInsertSemicolon )
            aOut.append("; ");

        // which border
        aOut.append(OString::Concat("border-") + pBorderName + ": ");

        // thickness
        int nWidth = pLine->GetWidth();
        int nPxWidth = (nWidth > 0) ?
            std::max(o3tl::convert(nWidth, o3tl::Length::twip, o3tl::Length::px), sal_Int64(1)) : 0;
        aOut.append(OString::number(nPxWidth) + "px ");
        switch (pLine->GetBorderLineStyle())
        {
            case SvxBorderLineStyle::SOLID:
                aOut.append("solid");
                break;
            case SvxBorderLineStyle::DOTTED:
                aOut.append("dotted");
                break;
            case SvxBorderLineStyle::DASHED:
            case SvxBorderLineStyle::DASH_DOT:
            case SvxBorderLineStyle::DASH_DOT_DOT:
                aOut.append("dashed");
                break;
            case SvxBorderLineStyle::DOUBLE:
            case SvxBorderLineStyle::DOUBLE_THIN:
            case SvxBorderLineStyle::THINTHICK_SMALLGAP:
            case SvxBorderLineStyle::THINTHICK_MEDIUMGAP:
            case SvxBorderLineStyle::THINTHICK_LARGEGAP:
            case SvxBorderLineStyle::THICKTHIN_SMALLGAP:
            case SvxBorderLineStyle::THICKTHIN_MEDIUMGAP:
            case SvxBorderLineStyle::THICKTHIN_LARGEGAP:
                aOut.append("double");
                break;
            case SvxBorderLineStyle::EMBOSSED:
                aOut.append("ridge");
                break;
            case SvxBorderLineStyle::ENGRAVED:
                aOut.append("groove");
                break;
            case SvxBorderLineStyle::OUTSET:
                aOut.append("outset");
                break;
            case SvxBorderLineStyle::INSET:
                aOut.append("inset");
                break;
            default:
                aOut.append("hidden");
        }
        aOut.append(" #");

        // color
        char hex[7];
        snprintf( hex, 7, "%06" SAL_PRIxUINT32, static_cast<sal_uInt32>( pLine->GetColor().GetRGBColor() ) );
        hex[6] = 0;

        aOut.append(hex);

        bInsertSemicolon = true;
    }

    return aOut.makeStringAndClear();
}

void ScHTMLExport::WriteBody()
{
    const SfxItemSet& rSet = PageDefaults( bAll ? 0 : aRange.aStart.Tab() );
    const SvxBrushItem* pBrushItem = &rSet.Get( ATTR_BACKGROUND );

    // default text color black
    if (!mbSkipHeaderFooter)
    {
        rStrm.WriteChar( '<' ).WriteOString( OOO_STRING_SVTOOLS_HTML_body );

        if (!mbSkipImages)
        {
            if ( bAll && GPOS_NONE != pBrushItem->GetGraphicPos() )
            {
                OUString aLink = pBrushItem->GetGraphicLink();
                OUString aGrfNm;

                // Embedded graphic -> write using WriteGraphic
                if( aLink.isEmpty() )
                {
                    const Graphic* pGrf = pBrushItem->GetGraphic();
                    if( pGrf )
                    {
                        // Save graphic as (JPG) file
                        aGrfNm = aStreamPath;
                        ErrCode nErr = XOutBitmap::WriteGraphic( *pGrf, aGrfNm,
                            u"JPG"_ustr, XOutFlags::UseNativeIfPossible );
                        if( !nErr ) // Contains errors, as we have nothing to output
                        {
                            aGrfNm = URIHelper::SmartRel2Abs(
                                    INetURLObject(aBaseURL),
                                    aGrfNm, URIHelper::GetMaybeFileHdl());
                            aLink = aGrfNm;
                        }
                    }
                }
                else
                {
                    aGrfNm = aLink;
                    if( bCopyLocalFileToINet )
                    {
                        CopyLocalFileToINet( aGrfNm, aStreamPath );
                    }
                    else
                        aGrfNm = URIHelper::SmartRel2Abs(
                                INetURLObject(aBaseURL),
                                aGrfNm, URIHelper::GetMaybeFileHdl());
                    aLink = aGrfNm;
                }
                if( !aLink.isEmpty() )
                {
                    rStrm.WriteChar( ' ' ).WriteOString( OOO_STRING_SVTOOLS_HTML_O_background ).WriteOString( "=\"" );
                    OUT_STR( URIHelper::simpleNormalizedMakeRelative(
                                aBaseURL,
                                aLink ) ).WriteChar( '\"' );
                }
            }
        }
        if ( !aHTMLStyle.aBackgroundColor.IsTransparent() )
        {   // A transparent background color should always result in default
            // background of the browser. Also, HTMLOutFuncs::Out_Color() writes
            // black #000000 for COL_AUTO which is the same as white #ffffff with
            // transparency set to 0xff, our default background.
            OUT_SP_CSTR_ASS( OOO_STRING_SVTOOLS_HTML_O_bgcolor );
            HTMLOutFuncs::Out_Color( rStrm, aHTMLStyle.aBackgroundColor );
        }

        rStrm.WriteChar( '>' ); OUT_LF();

        // A marker right after <body> can be used, so that data-sheets-* attributes are considered
        // at all. This is disabled by default.
        OString aMarker;
        char* pEnv = getenv("SC_DEBUG_HTML_MARKER");
        if (pEnv)
        {
            aMarker = pEnv;
        }
        else if (comphelper::LibreOfficeKit::isActive())
        {
            aMarker = "<google-sheets-html-origin/>"_ostr;
        }
        rStrm.WriteOString(aMarker);
    }

    if ( bAll )
        WriteOverview();

    WriteTables();

    if (!mbSkipHeaderFooter)
        TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_body );
}

void ScHTMLExport::WriteTables()
{
    const SCTAB nTabCount = pDoc->GetTableCount();
    const OUString  aStrTable( ScResId( SCSTR_TABLE ) );
    OUString   aStr;
    OUString        aStrOut;
    SCCOL           nStartCol;
    SCROW           nStartRow;
    SCTAB           nStartTab;
    SCCOL           nEndCol;
    SCROW           nEndRow;
    SCTAB           nEndTab;
    SCCOL           nStartColFix = 0;
    SCROW           nStartRowFix = 0;
    SCCOL           nEndColFix = 0;
    SCROW           nEndRowFix = 0;
    ScDrawLayer*    pDrawLayer = pDoc->GetDrawLayer();
    if ( bAll )
    {
        nStartTab = 0;
        nEndTab = nTabCount - 1;
    }
    else
    {
        nStartCol = nStartColFix = aRange.aStart.Col();
        nStartRow = nStartRowFix = aRange.aStart.Row();
        nStartTab = aRange.aStart.Tab();
        nEndCol = nEndColFix = aRange.aEnd.Col();
        nEndRow = nEndRowFix = aRange.aEnd.Row();
        nEndTab = aRange.aEnd.Tab();
    }
    SCTAB nTableStrNum = 1;
    for ( SCTAB nTab=nStartTab; nTab<=nEndTab; nTab++ )
    {
        if ( !pDoc->IsVisible( nTab ) )
            continue;   // for

        if ( bAll )
        {
            if ( !GetDataArea( nTab, nStartCol, nStartRow, nEndCol, nEndRow ) )
                continue;   // for

            if ( nUsedTables > 1 )
            {
                aStrOut = aStrTable + " "  + OUString::number( nTableStrNum++ ) + ": ";

                OUT_HR();

                // Write anchor
                rStrm.WriteOString( "<A NAME=\"table" )
                   .WriteOString( OString::number(nTab) )
                   .WriteOString( "\">" );
                TAG_ON( OOO_STRING_SVTOOLS_HTML_head1 );
                OUT_STR( aStrOut );
                TAG_ON( OOO_STRING_SVTOOLS_HTML_emphasis );

                pDoc->GetName( nTab, aStr );
                OUT_STR( aStr );

                TAG_OFF( OOO_STRING_SVTOOLS_HTML_emphasis );
                TAG_OFF( OOO_STRING_SVTOOLS_HTML_head1 );
                rStrm.WriteOString( "</A>" ); OUT_LF();
            }
        }
        else
        {
            nStartCol = nStartColFix;
            nStartRow = nStartRowFix;
            nEndCol = nEndColFix;
            nEndRow = nEndRowFix;
            if ( !TrimDataArea( nTab, nStartCol, nStartRow, nEndCol, nEndRow ) )
                continue;   // for
        }

        // <TABLE ...>
        OStringBuffer aByteStrOut(OOO_STRING_SVTOOLS_HTML_table);

        bTabHasGraphics = bTabAlignedLeft = false;
        if ( bAll && pDrawLayer )
            PrepareGraphics( pDrawLayer, nTab, nStartCol, nStartRow,
                nEndCol, nEndRow );

        // more <TABLE ...>
        if ( bTabAlignedLeft )
        {
            aByteStrOut.append(" " OOO_STRING_SVTOOLS_HTML_O_align
                    "=\""
                    OOO_STRING_SVTOOLS_HTML_AL_left "\"");
        }
            // ALIGN=LEFT allow text and graphics to flow around
        // CELLSPACING
        aByteStrOut.append(" " OOO_STRING_SVTOOLS_HTML_O_cellspacing
                "=\"" +
                OString::number(nCellSpacing) + "\"");

        // BORDER=0, we do the styling of the cells in <TD>
        aByteStrOut.append(" " OOO_STRING_SVTOOLS_HTML_O_border "=\"0\"");
        IncIndent(1); TAG_ON_LF( aByteStrOut.makeStringAndClear() );

        // --- <COLGROUP> ----
        {
            SCCOL nCol = nStartCol;
            sal_Int32 nWidth = 0;
            sal_Int32 nSpan = 0;
            while( nCol <= nEndCol )
            {
                if( pDoc->ColHidden(nCol, nTab) )
                {
                    ++nCol;
                    continue;
                }

                if( nWidth != ToPixel( pDoc->GetColWidth( nCol, nTab ) ) )
                {
                    if( nSpan != 0 )
                    {
                        TAG_ON(lcl_getColGroupString(nSpan, nWidth));
                        TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_colgroup );
                    }
                    nWidth = ToPixel( pDoc->GetColWidth( nCol, nTab ) );
                    nSpan = 1;
                }
                else
                    nSpan++;
                nCol++;
            }
            if( nSpan )
            {
                TAG_ON(lcl_getColGroupString(nSpan, nWidth));
                TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_colgroup );
            }
        }

        // <TBODY> // Re-enable only when THEAD and TFOOT are exported
        // IncIndent(1); TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_tbody );
        // At least old (3.x, 4.x?) Netscape doesn't follow <TABLE COLS=n> and
        // <COL WIDTH=x> specified, but needs a width at every column.
        bool bHasHiddenRows = pDoc->HasHiddenRows(nStartRow, nEndRow, nTab);
        // We need to cache sc::ColumnBlockPosition per each column.
        std::vector< sc::ColumnBlockPosition > blockPos( nEndCol - nStartCol + 1 );
        for( SCCOL i = nStartCol; i <= nEndCol; ++i )
            pDoc->InitColumnBlockPosition( blockPos[ i - nStartCol ], nTab, i );
        for ( SCROW nRow=nStartRow; nRow<=nEndRow; nRow++ )
        {
            if ( bHasHiddenRows && pDoc->RowHidden(nRow, nTab) )
            {
                nRow = pDoc->FirstVisibleRow(nRow+1, nEndRow, nTab);
                --nRow;
                continue;   // for
            }

            IncIndent(1); TAG_ON_LF( OOO_STRING_SVTOOLS_HTML_tablerow );
            bTableDataHeight = true;  // height at every first cell of each row
            for ( SCCOL nCol2=nStartCol; nCol2<=nEndCol; nCol2++ )
            {
                if ( pDoc->ColHidden(nCol2, nTab) )
                    continue;   // for

                if ( nCol2 == nEndCol )
                    IncIndent(-1);
                WriteCell( blockPos[ nCol2 - nStartCol ], nCol2, nRow, nTab );
                bTableDataHeight = false;
            }

            if ( nRow == nEndRow )
                IncIndent(-1);
            TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_tablerow );
        }
        // TODO: Uncomment later
        // IncIndent(-1); TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_tbody );

        IncIndent(-1); TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_table );

        if ( bTabHasGraphics && !mbSkipImages )
        {
            // the rest that is not in a cell
            size_t ListSize = aGraphList.size();
            for ( size_t i = 0; i < ListSize; ++i )
            {
                ScHTMLGraphEntry* pE = &aGraphList[ i ];
                if ( !pE->bWritten )
                    WriteGraphEntry( pE );
            }
            aGraphList.clear();
            if ( bTabAlignedLeft )
            {
                // clear <TABLE ALIGN=LEFT> with <BR CLEAR=LEFT>
                aByteStrOut.append(
                        OOO_STRING_SVTOOLS_HTML_linebreak
                        " "
                        OOO_STRING_SVTOOLS_HTML_O_clear "="
                        OOO_STRING_SVTOOLS_HTML_AL_left);
                TAG_ON_LF( aByteStrOut.makeStringAndClear() );
            }
        }

        if ( bAll )
            OUT_COMMENT( u"**************************************************************************" );
    }
}

void ScHTMLExport::WriteCell( sc::ColumnBlockPosition& rBlockPos, SCCOL nCol, SCROW nRow, SCTAB nTab )
{
    std::optional<Color> aColorScale;
    ScAddress aPos( nCol, nRow, nTab );
    ScRefCellValue aCell(*pDoc, aPos, rBlockPos);
    const ScPatternAttr* pAttr = pDoc->GetPattern( nCol, nRow, nTab );
    const SfxItemSet* pCondItemSet = pDoc->GetCondResult( nCol, nRow, nTab, &aCell );
    if (!pCondItemSet)
    {
        ScConditionalFormatList* pCondList = pDoc->GetCondFormList(nTab);
        const ScCondFormatItem& rCondItem = pAttr->GetItem(ATTR_CONDITIONAL);
        const ScCondFormatIndexes& rCondIndex = rCondItem.GetCondFormatData();
        if (rCondIndex.size() > 0)
        {
            ScConditionalFormat* pCondFmt = pCondList->GetFormat(rCondIndex[0]);
            if (pCondFmt)
            {
                const ScColorScaleFormat* pEntry = dynamic_cast<const ScColorScaleFormat*>(pCondFmt->GetEntry(0));
                if (pEntry)
                    aColorScale = pEntry->GetColor(aPos);
            }
        }
    }

    const ScMergeFlagAttr& rMergeFlagAttr = pAttr->GetItem( ATTR_MERGE_FLAG, pCondItemSet );
    if ( rMergeFlagAttr.IsOverlapped() )
        return ;

    ScHTMLGraphEntry* pGraphEntry = nullptr;
    if ( bTabHasGraphics && !mbSkipImages )
    {
        size_t ListSize = aGraphList.size();
        for ( size_t i = 0; i < ListSize; ++i )
        {
            ScHTMLGraphEntry* pE = &aGraphList[ i ];
            if ( pE->bInCell && pE->aRange.Contains( aPos ) )
            {
                if ( pE->aRange.aStart == aPos )
                {
                    pGraphEntry = pE;
                    break;  // for
                }
                else
                    return ; // Is a Col/RowSpan, Overlapped
            }
        }
    }

    sal_uInt32 nFormat = pAttr->GetNumberFormat( pFormatter );
    bool bValueData = aCell.hasNumeric();
    SvtScriptType nScriptType = SvtScriptType::NONE;
    if (!aCell.isEmpty())
        nScriptType = pDoc->GetScriptType(nCol, nRow, nTab, &aCell);

    if ( nScriptType == SvtScriptType::NONE )
        nScriptType = aHTMLStyle.nDefaultScriptType;

    OStringBuffer aStrTD(OOO_STRING_SVTOOLS_HTML_tabledata);

    // border of the cells
    const SvxBoxItem& rBorder = pDoc->GetAttr( nCol, nRow, nTab, ATTR_BORDER );
    if (rBorder.GetTop() || rBorder.GetBottom() || rBorder.GetLeft() || rBorder.GetRight())
    {
        aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_style "=\"");

        bool bInsertSemicolon = false;
        aStrTD.append(BorderToStyle("top", rBorder.GetTop(), bInsertSemicolon));
        aStrTD.append(BorderToStyle("bottom", rBorder.GetBottom(), bInsertSemicolon));
        aStrTD.append(BorderToStyle("left", rBorder.GetLeft(), bInsertSemicolon));
        aStrTD.append(BorderToStyle("right", rBorder.GetRight(), bInsertSemicolon));

        aStrTD.append('"');
    }

    const char* pChar;
    sal_uInt16 nHeightPixel;

    const ScMergeAttr& rMergeAttr = pAttr->GetItem( ATTR_MERGE, pCondItemSet );
    if ( pGraphEntry || rMergeAttr.IsMerged() )
    {
        SCCOL nC, jC;
        SCROW nR;
        tools::Long v;
        if ( pGraphEntry )
            nC = std::max( SCCOL(pGraphEntry->aRange.aEnd.Col() - nCol + 1),
                           rMergeAttr.GetColMerge() );
        else
            nC = rMergeAttr.GetColMerge();
        if ( nC > 1 )
        {
            aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_colspan
                    "=" + OString::number(static_cast<sal_Int32>(nC)));
            nC = nC + nCol;
            for ( jC=nCol, v=0; jC<nC; jC++ )
                v += pDoc->GetColWidth( jC, nTab );
        }

        if ( pGraphEntry )
            nR = std::max( SCROW(pGraphEntry->aRange.aEnd.Row() - nRow + 1),
                           rMergeAttr.GetRowMerge() );
        else
            nR = rMergeAttr.GetRowMerge();
        if ( nR > 1 )
        {
            aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_rowspan
                "=" + OString::number(static_cast<sal_Int32>(nR)));
            nR += nRow;
            v = pDoc->GetRowHeight( nRow, nR-1, nTab );
            nHeightPixel = ToPixel( static_cast< sal_uInt16 >( v ) );
        }
        else
            nHeightPixel = ToPixel( pDoc->GetRowHeight( nRow, nTab ) );
    }
    else
        nHeightPixel = ToPixel( pDoc->GetRowHeight( nRow, nTab ) );

    if ( bTableDataHeight )
    {
        aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_height "=\"" +
                OString::number(nHeightPixel) + "\"");
    }

    const SvxFontItem& rFontItem = static_cast<const SvxFontItem&>( pAttr->GetItem(
            ScGlobal::GetScriptedWhichID( nScriptType, ATTR_FONT),
            pCondItemSet) );

    const SvxFontHeightItem& rFontHeightItem = static_cast<const SvxFontHeightItem&>(
        pAttr->GetItem( ScGlobal::GetScriptedWhichID( nScriptType,
                    ATTR_FONT_HEIGHT), pCondItemSet) );

    const SvxWeightItem& rWeightItem = static_cast<const SvxWeightItem&>( pAttr->GetItem(
            ScGlobal::GetScriptedWhichID( nScriptType, ATTR_FONT_WEIGHT),
            pCondItemSet) );

    const SvxPostureItem& rPostureItem = static_cast<const SvxPostureItem&>(
        pAttr->GetItem( ScGlobal::GetScriptedWhichID( nScriptType,
                    ATTR_FONT_POSTURE), pCondItemSet) );

    const SvxUnderlineItem& rUnderlineItem =
        pAttr->GetItem( ATTR_FONT_UNDERLINE, pCondItemSet );

    const SvxCrossedOutItem& rCrossedOutItem =
        pAttr->GetItem( ATTR_FONT_CROSSEDOUT, pCondItemSet );

    const SvxColorItem& rColorItem = pAttr->GetItem(
            ATTR_FONT_COLOR, pCondItemSet );

    const SvxHorJustifyItem& rHorJustifyItem =
        pAttr->GetItem( ATTR_HOR_JUSTIFY, pCondItemSet );

    const SvxVerJustifyItem& rVerJustifyItem =
        pAttr->GetItem( ATTR_VER_JUSTIFY, pCondItemSet );

    const SvxBrushItem& rBrushItem = pAttr->GetItem(
            ATTR_BACKGROUND, pCondItemSet );

    Color aBgColor;
    if ( aColorScale )
        aBgColor = *aColorScale;
    else if ( rBrushItem.GetColor().GetAlpha() == 0 )
        aBgColor = aHTMLStyle.aBackgroundColor; // No unwanted background color
    else
        aBgColor = rBrushItem.GetColor();

    bool bBold          = ( WEIGHT_BOLD      <= rWeightItem.GetWeight() );
    bool bItalic        = ( ITALIC_NONE      != rPostureItem.GetPosture() );
    bool bUnderline     = ( LINESTYLE_NONE   != rUnderlineItem.GetLineStyle() );
    bool bCrossedOut    = ( STRIKEOUT_SINGLE <= rCrossedOutItem.GetStrikeout() );
    bool bSetFontColor  = ( COL_AUTO         != rColorItem.GetValue() );  // default is AUTO now
    bool bSetFontName   = ( aHTMLStyle.aFontFamilyName  != rFontItem.GetFamilyName() );
    sal_uInt16 nSetFontSizeNumber = 0;
    sal_uInt32 nFontHeight = rFontHeightItem.GetHeight();
    if ( nFontHeight != aHTMLStyle.nFontHeight )
    {
        nSetFontSizeNumber = GetFontSizeNumber( static_cast<sal_uInt16>(nFontHeight) );
        if ( nSetFontSizeNumber == aHTMLStyle.nFontSizeNumber )
            nSetFontSizeNumber = 0;   // no difference, don't set
    }

    bool bSetFont = (bSetFontColor || bSetFontName || nSetFontSizeNumber);

    //! TODO: we could entirely use CSS1 here instead, but that would exclude
    //! Netscape 3.0 and Netscape 4.x without JavaScript enabled.
    //! Do we want that?

    switch( rHorJustifyItem.GetValue() )
    {
        case SvxCellHorJustify::Standard:
            pChar = (bValueData ? OOO_STRING_SVTOOLS_HTML_AL_right : OOO_STRING_SVTOOLS_HTML_AL_left);
            break;
        case SvxCellHorJustify::Center:    pChar = OOO_STRING_SVTOOLS_HTML_AL_center;  break;
        case SvxCellHorJustify::Block:     pChar = OOO_STRING_SVTOOLS_HTML_AL_justify; break;
        case SvxCellHorJustify::Right:     pChar = OOO_STRING_SVTOOLS_HTML_AL_right;   break;
        case SvxCellHorJustify::Left:
        case SvxCellHorJustify::Repeat:
        default:                        pChar = OOO_STRING_SVTOOLS_HTML_AL_left;    break;
    }

    aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_align "=\"" +
        OString::Concat(pChar) + "\"");

    switch( rVerJustifyItem.GetValue() )
    {
        case SvxCellVerJustify::Top:       pChar = OOO_STRING_SVTOOLS_HTML_VA_top;     break;
        case SvxCellVerJustify::Center:    pChar = OOO_STRING_SVTOOLS_HTML_VA_middle;  break;
        case SvxCellVerJustify::Bottom:    pChar = OOO_STRING_SVTOOLS_HTML_VA_bottom;  break;
        case SvxCellVerJustify::Standard:
        default:                        pChar = nullptr;
    }
    if ( pChar )
    {
        aStrTD.append(OString::Concat(" " OOO_STRING_SVTOOLS_HTML_O_valign "=") + pChar);
    }

    if ( aHTMLStyle.aBackgroundColor != aBgColor )
    {
        aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_bgcolor "="
            + lcl_makeHTMLColorTriplet(aBgColor));
    }

    double fVal = 0.0;
    if ( bValueData )
    {
        switch (aCell.getType())
        {
            case CELLTYPE_VALUE:
                fVal = aCell.getDouble();
                if ( bCalcAsShown && fVal != 0.0 )
                    fVal = pDoc->RoundValueAsShown( fVal, nFormat );
                break;
            case CELLTYPE_FORMULA:
                fVal = aCell.getFormula()->GetValue();
                break;
            default:
                OSL_FAIL( "value data with unsupported cell type" );
        }
    }

    aStrTD.append(HTMLOutFuncs::CreateTableDataOptionsValNum(bValueData, fVal,
        nFormat, *pFormatter, &aNonConvertibleChars));

    std::optional<tools::JsonWriter> oJson;
    const SvNumberformat* pNumberFormat = nullptr;
    if (bValueData)
    {
        if (nFormat)
        {
            const SvNumberformat* pFormatEntry = pFormatter->GetEntry(nFormat);
            if (pFormatEntry)
            {
                OUString aNumStr = pFormatEntry->GetFormatstring();
                if (aNumStr == "BOOLEAN")
                {
                    // 4 is boolean.
                    oJson.emplace();
                    oJson->put("1", static_cast<sal_Int32>(4));
                    oJson->put("4", static_cast<sal_Int32>(fVal));
                }
                else
                {
                    // 3 is number.
                    oJson.emplace();
                    oJson->put("1", static_cast<sal_Int32>(3));
                    oJson->put("3", static_cast<sal_Int32>(fVal));
                    pNumberFormat = pFormatEntry;
                }
            }
        }

        if (aCell.getType() == CELLTYPE_FORMULA)
        {
            // If it's a formula, then also emit that, grammar is R1C1 reference style.
            OUString aFormula = aCell.getFormula()->GetFormula(
                    formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1);
            aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_DSformula "=\""
                    + HTMLOutFuncs::ConvertStringToHTML(aFormula) + "\"");
        }
    }
    else
    {
        // 2 is text.
        oJson.emplace();
        oJson->put("1", static_cast<sal_Int32>(2));
        oJson->put("2", pDoc->GetString(aPos));
    }

    if (oJson)
    {
        OUString aJsonString = OUString::fromUtf8(oJson->finishAndGetAsOString());
        aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_DSval "=\""
                      + HTMLOutFuncs::ConvertStringToHTML(aJsonString) + "\"");
    }

    if (pNumberFormat)
    {
        // 2 is a number format.
        oJson.emplace();
        oJson->put("1", static_cast<sal_Int32>(2));
        oJson->put("2", pNumberFormat->GetFormatstring());
        // The number format is for a number.
        oJson->put("3", static_cast<sal_Int32>(1));
        OUString aJsonString = OUString::fromUtf8(oJson->finishAndGetAsOString());
        aStrTD.append(" " OOO_STRING_SVTOOLS_HTML_O_DSnum "=\""
                      + HTMLOutFuncs::ConvertStringToHTML(aJsonString) + "\"");
    }

    TAG_ON(aStrTD.makeStringAndClear());

    //write the note for this as the first thing in the tag
    ScPostIt* pNote = pDoc->HasNote(aPos) ? pDoc->GetNote(aPos) : nullptr;
    if (pNote)
    {
        //create the comment indicator
        OString aStr = OOO_STRING_SVTOOLS_HTML_anchor " "
            OOO_STRING_SVTOOLS_HTML_O_class "=\"comment-indicator\""_ostr;
        TAG_ON(aStr);
        TAG_OFF(OOO_STRING_SVTOOLS_HTML_anchor);
        OUT_LF();

        //create the element holding the contents
        //this is a bit naive, since it doesn't separate
        //lines into html breaklines yet
        TAG_ON(OOO_STRING_SVTOOLS_HTML_comment2);
        OUT_STR( pNote->GetText() );
        TAG_OFF(OOO_STRING_SVTOOLS_HTML_comment2);
        OUT_LF();
    }

    if ( bBold )        TAG_ON( OOO_STRING_SVTOOLS_HTML_bold );
    if ( bItalic )      TAG_ON( OOO_STRING_SVTOOLS_HTML_italic );
    if ( bUnderline )   TAG_ON( OOO_STRING_SVTOOLS_HTML_underline );
    if ( bCrossedOut )  TAG_ON( OOO_STRING_SVTOOLS_HTML_strikethrough );

    if ( bSetFont )
    {
        OStringBuffer aStr(OOO_STRING_SVTOOLS_HTML_font);
        if ( bSetFontName )
        {
            aStr.append(" " OOO_STRING_SVTOOLS_HTML_O_face "=\"");

            if (!rFontItem.GetFamilyName().isEmpty())
            {
                const OUString& rList = rFontItem.GetFamilyName();
                for (sal_Int32 nPos {0};;)
                {
                    OString aTmpStr = HTMLOutFuncs::ConvertStringToHTML(
                        o3tl::getToken( rList, 0, ';', nPos ),
                        &aNonConvertibleChars);
                    aStr.append(aTmpStr);
                    if (nPos<0)
                        break;
                    aStr.append(',');
                }
            }

            aStr.append('\"');
        }
        if ( nSetFontSizeNumber )
        {
            aStr.append(" " OOO_STRING_SVTOOLS_HTML_O_size "="
                + OString::number(static_cast<sal_Int32>(nSetFontSizeNumber)));
        }
        if ( bSetFontColor )
        {
            Color   aColor = rColorItem.GetValue();

            //  always export automatic text color as black
            if ( aColor == COL_AUTO )
                aColor = COL_BLACK;

            aStr.append(" " OOO_STRING_SVTOOLS_HTML_O_color "="
                + lcl_makeHTMLColorTriplet(aColor));
        }
        TAG_ON(aStr.makeStringAndClear());
    }

    OUString aURL;
    bool bWriteHyperLink(false);
    if (aCell.getType() == CELLTYPE_FORMULA)
    {
        ScFormulaCell* pFCell = aCell.getFormula();
        if (pFCell->IsHyperLinkCell())
        {
            OUString aCellText;
            pFCell->GetURLResult(aURL, aCellText);
            bWriteHyperLink = true;
        }
    }

    if (bWriteHyperLink)
    {
        OString aURLStr = HTMLOutFuncs::ConvertStringToHTML(aURL, &aNonConvertibleChars);
        OString aStr = OOO_STRING_SVTOOLS_HTML_anchor " " OOO_STRING_SVTOOLS_HTML_O_href "=\"" + aURLStr + "\"";
        TAG_ON(aStr);
    }

    OUString aStrOut;
    bool bFieldText = false;

    const Color* pColor;
    switch (aCell.getType())
    {
        case CELLTYPE_EDIT :
            bFieldText = WriteFieldText(aCell.getEditText());
            if ( bFieldText )
                break;
            [[fallthrough]];
        default:
            aStrOut = ScCellFormat::GetString(aCell, nFormat, &pColor, nullptr, *pDoc);
    }

    if ( !bFieldText )
    {
        if ( aStrOut.isEmpty() )
        {
            TAG_ON( OOO_STRING_SVTOOLS_HTML_linebreak ); // No completely empty line
        }
        else
        {
            sal_Int32 nPos = aStrOut.indexOf( '\n' );
            if ( nPos == -1 )
            {
                OUT_STR( aStrOut );
            }
            else
            {
                sal_Int32 nStartPos = 0;
                do
                {
                    OUString aSingleLine = aStrOut.copy( nStartPos, nPos - nStartPos );
                    OUT_STR( aSingleLine );
                    TAG_ON( OOO_STRING_SVTOOLS_HTML_linebreak );
                    nStartPos = nPos + 1;
                }
                while( ( nPos = aStrOut.indexOf( '\n', nStartPos ) ) != -1 );
                OUString aSingleLine = aStrOut.copy( nStartPos );
                OUT_STR( aSingleLine );
            }
        }
    }
    if ( pGraphEntry )
        WriteGraphEntry( pGraphEntry );

    if (bWriteHyperLink) { TAG_OFF(OOO_STRING_SVTOOLS_HTML_anchor); }

    if ( bSetFont )     TAG_OFF( OOO_STRING_SVTOOLS_HTML_font );
    if ( bCrossedOut )  TAG_OFF( OOO_STRING_SVTOOLS_HTML_strikethrough );
    if ( bUnderline )   TAG_OFF( OOO_STRING_SVTOOLS_HTML_underline );
    if ( bItalic )      TAG_OFF( OOO_STRING_SVTOOLS_HTML_italic );
    if ( bBold )        TAG_OFF( OOO_STRING_SVTOOLS_HTML_bold );

    TAG_OFF_LF( OOO_STRING_SVTOOLS_HTML_tabledata );
}

bool ScHTMLExport::WriteFieldText( const EditTextObject* pData )
{
    bool bFields = false;
    // text and anchor of URL fields, Doc-Engine is a ScFieldEditEngine
    EditEngine& rEngine = pDoc->GetEditEngine();
    rEngine.SetText( *pData );
    sal_Int32 nParas = rEngine.GetParagraphCount();
    if ( nParas )
    {
        ESelection aSel( 0, 0, nParas-1, rEngine.GetTextLen( nParas-1 ) );
        SfxItemSet aSet( rEngine.GetAttribs( aSel ) );
        SfxItemState eFieldState = aSet.GetItemState( EE_FEATURE_FIELD, false );
        if ( eFieldState == SfxItemState::INVALID || eFieldState == SfxItemState::SET )
            bFields = true;
    }
    if ( bFields )
    {
        bool bOldUpdateMode = rEngine.SetUpdateLayout( true );      // no portions if not formatted
        for ( sal_Int32 nPar=0; nPar < nParas; nPar++ )
        {
            if ( nPar > 0 )
                TAG_ON( OOO_STRING_SVTOOLS_HTML_linebreak );
            std::vector<sal_Int32> aPortions;
            rEngine.GetPortions( nPar, aPortions );
            sal_Int32 nStart = 0;
            for ( const sal_Int32 nEnd : aPortions )
            {
                ESelection aSel( nPar, nStart, nPar, nEnd );
                bool bUrl = false;
                // fields are single characters
                if ( nEnd == nStart+1 )
                {
                    SfxItemSet aSet = rEngine.GetAttribs( aSel );
                    if ( const SvxFieldItem* pFieldItem = aSet.GetItemIfSet( EE_FEATURE_FIELD, false ) )
                    {
                        const SvxFieldData* pField = pFieldItem->GetField();
                        if (const SvxURLField* pURLField = dynamic_cast<const SvxURLField*>(pField))
                        {
                            bUrl = true;
                            rStrm.WriteChar( '<' ).WriteOString( OOO_STRING_SVTOOLS_HTML_anchor ).WriteChar( ' ' ).WriteOString( OOO_STRING_SVTOOLS_HTML_O_href ).WriteOString( "=\"" );
                            OUT_STR( pURLField->GetURL() );
                            rStrm.WriteOString( "\">" );
                            OUT_STR( pURLField->GetRepresentation() );
                            rStrm.WriteOString( "</" ).WriteOString( OOO_STRING_SVTOOLS_HTML_anchor ).WriteChar( '>' );
                        }
                    }
                }
                if ( !bUrl )
                    OUT_STR( rEngine.GetText( aSel ) );
                nStart = nEnd;
            }
        }
        rEngine.SetUpdateLayout( bOldUpdateMode );
    }
    return bFields;
}

void ScHTMLExport::CopyLocalFileToINet( OUString& rFileNm,
        std::u16string_view rTargetNm )
{
    INetURLObject aFileUrl, aTargetUrl;
    aFileUrl.SetSmartURL( rFileNm );
    aTargetUrl.SetSmartURL( rTargetNm );
    if (!(INetProtocol::File == aFileUrl.GetProtocol()
            && (INetProtocol::Http == aTargetUrl.GetProtocol()
                || INetProtocol::Https == aTargetUrl.GetProtocol()
                || INetProtocol::VndSunStarWebdav == aTargetUrl.GetProtocol()
                || INetProtocol::Smb == aTargetUrl.GetProtocol()
                || INetProtocol::Sftp == aTargetUrl.GetProtocol()
                || INetProtocol::Cmis == aTargetUrl.GetProtocol())))
    {
        return;
    }

    if( pFileNameMap )
    {
        // Did we already move the file?
        std::map<OUString, OUString>::iterator it = pFileNameMap->find( rFileNm );
        if( it != pFileNameMap->end() )
        {
            rFileNm = it->second;
            return;
        }
    }
    else
    {
        pFileNameMap.reset( new std::map<OUString, OUString> );
    }

    bool bRet = false;
    SvFileStream aTmp( aFileUrl.PathToFileName(), StreamMode::READ );

    OUString aSrc = rFileNm;
    OUString aDest = aTargetUrl.GetPartBeforeLastName() + aFileUrl.GetLastName();

    SfxMedium aMedium( aDest, StreamMode::WRITE | StreamMode::SHARE_DENYNONE );

    {
        SvFileStream aCpy( aMedium.GetPhysicalName(), StreamMode::WRITE );
        aCpy.WriteStream( aTmp );
    }

    // Take over
    aMedium.Close();
    aMedium.Commit();

    bRet = ERRCODE_NONE == aMedium.GetErrorIgnoreWarning();

    if( bRet )
    {
        pFileNameMap->insert( std::make_pair( aSrc, aDest ) );
        rFileNm = aDest;
    }
}

void ScHTMLExport::IncIndent( short nVal )
{
    sIndent[nIndent] = '\t';
    nIndent = nIndent + nVal;
    if ( nIndent < 0 )
        nIndent = 0;
    else if ( nIndent > nIndentMax )
        nIndent = nIndentMax;
    sIndent[nIndent] = 0;
}

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