/* -*- 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 <editdoc.hxx>

#include <editeng/autodiritem.hxx>
#include <editeng/tstpitem.hxx>
#include <editeng/colritem.hxx>
#include <editeng/fontitem.hxx>
#include <editeng/crossedoutitem.hxx>
#include <editeng/fhgtitem.hxx>
#include <editeng/flditem.hxx>
#include <editeng/postitem.hxx>
#include <editeng/kernitem.hxx>
#include <editeng/wrlmitem.hxx>
#include <editeng/wghtitem.hxx>
#include <editeng/udlnitem.hxx>
#include <editeng/cmapitem.hxx>
#include <editeng/contouritem.hxx>
#include <editeng/escapementitem.hxx>
#include <editeng/shdditem.hxx>
#include <editeng/autokernitem.hxx>
#include <editeng/langitem.hxx>
#include <editeng/emphasismarkitem.hxx>
#include <editeng/charscaleitem.hxx>
#include <editeng/charreliefitem.hxx>
#include <editeng/editids.hrc>
#include <editeng/editdata.hxx>
#include <editeng/lrspitem.hxx>
#include <editeng/ulspitem.hxx>
#include <editeng/lspcitem.hxx>
#if ENABLE_YRS
#include <editeng/frmdiritem.hxx>
#include <editeng/hngpnctitem.hxx>
#include <editeng/forbiddenruleitem.hxx>
#include <editeng/scriptspaceitem.hxx>
#include <editeng/adjustitem.hxx>
#include <editeng/justifyitem.hxx>
#include <editeng/numdef.hxx>
#include <editeng/yrs.hxx>
#include <editeng/yrstransactionsupplier.hxx>
#include <svl/itemiter.hxx>
#endif

#include <editeng/eerdll.hxx>
#include <eerdll2.hxx>
#include "impedit.hxx"

#include <rtl/ustrbuf.hxx>
#include <sal/log.hxx>
#include <o3tl/safeint.hxx>
#include <osl/diagnose.h>

#include <svl/grabbagitem.hxx>
#include <svl/voiditem.hxx>
#include <tools/debug.hxx>
#include <com/sun/star/i18n/ScriptType.hpp>
#include <libxml/xmlwriter.h>

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <memory>
#include <set>
#include <string_view>
#include <utility>

using namespace ::com::sun::star;


sal_uInt16 GetScriptItemId( sal_uInt16 nItemId, SvtScriptType nScriptType )
{
    sal_uInt16 nId = nItemId;

    if ( ( nScriptType == SvtScriptType::ASIAN ) ||
         ( nScriptType == SvtScriptType::COMPLEX ) )
    {
        switch ( nItemId )
        {
            case EE_CHAR_LANGUAGE:
                nId = ( nScriptType == SvtScriptType::ASIAN ) ? EE_CHAR_LANGUAGE_CJK : EE_CHAR_LANGUAGE_CTL;
            break;
            case EE_CHAR_FONTINFO:
                nId = ( nScriptType == SvtScriptType::ASIAN ) ? EE_CHAR_FONTINFO_CJK : EE_CHAR_FONTINFO_CTL;
            break;
            case EE_CHAR_FONTHEIGHT:
                nId = ( nScriptType == SvtScriptType::ASIAN ) ? EE_CHAR_FONTHEIGHT_CJK : EE_CHAR_FONTHEIGHT_CTL;
            break;
            case EE_CHAR_WEIGHT:
                nId = ( nScriptType == SvtScriptType::ASIAN ) ? EE_CHAR_WEIGHT_CJK : EE_CHAR_WEIGHT_CTL;
            break;
            case EE_CHAR_ITALIC:
                nId = ( nScriptType == SvtScriptType::ASIAN ) ? EE_CHAR_ITALIC_CJK : EE_CHAR_ITALIC_CTL;
            break;
        }
    }

    return nId;
}

bool IsScriptItemValid( sal_uInt16 nItemId, short nScriptType )
{
    bool bValid = true;

    switch ( nItemId )
    {
        case EE_CHAR_LANGUAGE:
            bValid = nScriptType == i18n::ScriptType::LATIN;
        break;
        case EE_CHAR_LANGUAGE_CJK:
            bValid = nScriptType == i18n::ScriptType::ASIAN;
        break;
        case EE_CHAR_LANGUAGE_CTL:
            bValid = nScriptType == i18n::ScriptType::COMPLEX;
        break;
        case EE_CHAR_FONTINFO:
            bValid = nScriptType == i18n::ScriptType::LATIN;
        break;
        case EE_CHAR_FONTINFO_CJK:
            bValid = nScriptType == i18n::ScriptType::ASIAN;
        break;
        case EE_CHAR_FONTINFO_CTL:
            bValid = nScriptType == i18n::ScriptType::COMPLEX;
        break;
        case EE_CHAR_FONTHEIGHT:
            bValid = nScriptType == i18n::ScriptType::LATIN;
        break;
        case EE_CHAR_FONTHEIGHT_CJK:
            bValid = nScriptType == i18n::ScriptType::ASIAN;
        break;
        case EE_CHAR_FONTHEIGHT_CTL:
            bValid = nScriptType == i18n::ScriptType::COMPLEX;
        break;
        case EE_CHAR_WEIGHT:
            bValid = nScriptType == i18n::ScriptType::LATIN;
        break;
        case EE_CHAR_WEIGHT_CJK:
            bValid = nScriptType == i18n::ScriptType::ASIAN;
        break;
        case EE_CHAR_WEIGHT_CTL:
            bValid = nScriptType == i18n::ScriptType::COMPLEX;
        break;
        case EE_CHAR_ITALIC:
            bValid = nScriptType == i18n::ScriptType::LATIN;
        break;
        case EE_CHAR_ITALIC_CJK:
            bValid = nScriptType == i18n::ScriptType::ASIAN;
        break;
        case EE_CHAR_ITALIC_CTL:
            bValid = nScriptType == i18n::ScriptType::COMPLEX;
        break;
    }

    return bValid;
}

EditCharAttrib* MakeCharAttrib( SfxItemPool& rPool, const SfxPoolItem& rAttr, sal_Int32 nS, sal_Int32 nE )
{
    // Create a new attribute in the pool
    switch( rAttr.Which() )
    {
        case EE_CHAR_LANGUAGE:
        case EE_CHAR_LANGUAGE_CJK:
        case EE_CHAR_LANGUAGE_CTL:
        {
            return new EditCharAttribLanguage(rPool, rAttr, nS, nE);
        }
        break;
        case EE_CHAR_COLOR:
        {
            return new EditCharAttribColor(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_FONTINFO:
        case EE_CHAR_FONTINFO_CJK:
        case EE_CHAR_FONTINFO_CTL:
        {
            return new EditCharAttribFont(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_FONTHEIGHT:
        case EE_CHAR_FONTHEIGHT_CJK:
        case EE_CHAR_FONTHEIGHT_CTL:
        {
            return new EditCharAttribFontHeight(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_FONTWIDTH:
        {
            return new EditCharAttribFontWidth(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_WEIGHT:
        case EE_CHAR_WEIGHT_CJK:
        case EE_CHAR_WEIGHT_CTL:
        {
            return new EditCharAttribWeight(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_UNDERLINE:
        {
            return new EditCharAttribUnderline(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_OVERLINE:
        {
            return new EditCharAttribOverline(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_EMPHASISMARK:
        {
            return new EditCharAttribEmphasisMark(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_RELIEF:
        {
            return new EditCharAttribRelief(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_STRIKEOUT:
        {
            return new EditCharAttribStrikeout(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_ITALIC:
        case EE_CHAR_ITALIC_CJK:
        case EE_CHAR_ITALIC_CTL:
        {
            return new EditCharAttribItalic(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_OUTLINE:
        {
            return new EditCharAttribOutline(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_SHADOW:
        {
            return new EditCharAttribShadow(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_ESCAPEMENT:
        {
            return new EditCharAttribEscapement(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_PAIRKERNING:
        {
            return new EditCharAttribPairKerning(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_KERNING:
        {
            return new EditCharAttribKerning(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_WLM:
        {
            return new EditCharAttribWordLineMode(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_XMLATTRIBS:
        {
            return new EditCharAttrib(rPool, rAttr, nS, nE);  // Attribute is only for holding XML information...
        }
        break;
        case EE_CHAR_CASEMAP:
        {
            return new EditCharAttribCaseMap(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_GRABBAG:
        {
            return new EditCharAttribGrabBag(rPool, rAttr, nS, nE );
        }
        break;
        case EE_FEATURE_TAB:
        {
            return new EditCharAttribTab(rPool, rAttr, nS );
        }
        break;
        case EE_FEATURE_LINEBR:
        {
            return new EditCharAttribLineBreak(rPool, rAttr, nS );
        }
        break;
        case EE_FEATURE_FIELD:
        {
            return new EditCharAttribField(rPool, rAttr, nS );
        }
        break;
        case EE_CHAR_BKGCOLOR:
        {
            return new EditCharAttribBackgroundColor(rPool, rAttr, nS, nE );
        }
        break;
        case EE_CHAR_RUBY:
        {
            return new EditCharAttribRuby(rPool, rAttr, nS, nE);
        }
        break;
        case EE_CHAR_SCRIPT_HINT:
        {
            return new EditCharAttribScriptHint(rPool, rAttr, nS, nE);
        }
        break;
        default:
        break;
    }

    OSL_FAIL( "Invalid Attribute!" );
    return nullptr;
}

void ParaPortion::MarkInvalid(sal_Int32 nStart, sal_Int32 nDiff)
{
    if (!mbInvalid)
    {
//      mnInvalidPosEnd = nStart;    // ??? => CreateLines
        mnInvalidPosStart = nDiff >= 0 ? nStart : nStart + nDiff;
        mnInvalidDiff = nDiff;
    }
    else
    {
        // Simple tap in succession
        if (nDiff > 0 && mnInvalidDiff > 0 && (mnInvalidPosStart + mnInvalidDiff) == nStart)
        {
            mnInvalidDiff = mnInvalidDiff + nDiff;
        }
        // Simple delete in succession
        else if (nDiff < 0 && mnInvalidDiff < 0 && mnInvalidPosStart == nStart)
        {
            mnInvalidPosStart = mnInvalidPosStart + nDiff;
            mnInvalidDiff = mnInvalidDiff + nDiff;
        }
        else
        {
//          mnInvalidPosEnd = pNode->Len();
            DBG_ASSERT(nDiff >= 0 || (nStart + nDiff) >= 0, "MarkInvalid: Diff out of Range");
            mnInvalidPosStart = std::min(mnInvalidPosStart, nDiff < 0 ? nStart + nDiff : nDiff);
            mnInvalidDiff = 0;
            mbSimple = false;
        }
    }
    mbInvalid = true;
    maScriptInfos.clear();
    maWritingDirectionInfos.clear();
}

void ParaPortion::MarkSelectionInvalid(sal_Int32 nStart)
{
    if ( !mbInvalid )
    {
        mnInvalidPosStart = nStart;
    }
    else
    {
        mnInvalidPosStart = std::min(mnInvalidPosStart, nStart);
    }
    mnInvalidDiff = 0;
    mbInvalid = true;
    mbSimple = false;
    maScriptInfos.clear();
    maWritingDirectionInfos.clear();
}

sal_Int32 ParaPortion::GetLineNumber( sal_Int32 nIndex ) const
{
    const sal_Int32 nCount = maLineList.Count();
    assert(nCount > 0 && "Empty ParaPortion in GetLine!");
    if (nCount == 0)
        return 0;
    DBG_ASSERT(mbVisible, "Why GetLine() on an invisible paragraph?");

    for ( sal_Int32 nLine = 0; nLine < nCount; nLine++ )
    {
        if (maLineList[nLine].IsIn(nIndex))
            return nLine;
    }

    // Then it should be at the end of the last line!
    DBG_ASSERT(nIndex == maLineList[nCount - 1].GetEnd(), "Index dead wrong!");
    return nCount - 1;
}

void ParaPortion::CorrectValuesBehindLastFormattedLine( sal_Int32 nLastFormattedLine )
{
    sal_Int32 nLines = maLineList.Count();
    DBG_ASSERT( nLines, "CorrectPortionNumbersFromLine: Empty Portion?" );
    if ( nLastFormattedLine < ( nLines - 1 ) )
    {
        const EditLine& rLastFormatted = maLineList[ nLastFormattedLine ];
        const EditLine& rUnformatted = maLineList[ nLastFormattedLine+1 ];
        sal_Int32 nPortionDiff = rUnformatted.GetStartPortion() - rLastFormatted.GetEndPortion();
        sal_Int32 nTextDiff = rUnformatted.GetStart() - rLastFormatted.GetEnd();
        nTextDiff++;    // LastFormatted->GetEnd() was included => 1 deducted too much!

        // The first unformatted must begin exactly one Portion behind the last
        // of the formatted:
        // If the modified line was split into one portion, can
        // nLastEnd > nNextStart!
        int nPDiff = -( nPortionDiff-1 );
        int nTDiff = -( nTextDiff-1 );
        if ( nPDiff || nTDiff )
        {
            for ( sal_Int32 nL = nLastFormattedLine+1; nL < nLines; nL++ )
            {
                EditLine& rLine = maLineList[ nL ];

                rLine.GetStartPortion() = rLine.GetStartPortion() + nPDiff;
                rLine.GetEndPortion() = rLine.GetEndPortion() + nPDiff;

                rLine.GetStart() = rLine.GetStart() + nTDiff;
                rLine.GetEnd() = rLine.GetEnd() + nTDiff;

                rLine.SetValid();
            }
        }
    }
    DBG_ASSERT(maLineList[maLineList.Count() - 1].GetEnd() == mpNode->Len(), "CorrectLines: The end is not right!");
}

// Shared reverse lookup acceleration pieces ...

namespace {

template<typename Array, typename Val>
sal_Int32 FastGetPos(const Array& rArray, const Val* p, sal_Int32& rLastPos)
{
    sal_Int32 nArrayLen = rArray.size();

    // Through certain filter code-paths we do a lot of appends, which in
    // turn call GetPos - creating some N^2 nightmares. If we have a
    // non-trivially large list, do a few checks from the end first.
    if (rLastPos > 16 && nArrayLen > 16)
    {
        sal_Int32 nEnd;
        if (rLastPos > nArrayLen - 2)
            nEnd = nArrayLen;
        else
            nEnd = rLastPos + 2;

        for (sal_Int32 nIdx = rLastPos - 2; nIdx < nEnd; ++nIdx)
        {
            if (rArray.at(nIdx).get() == p)
            {
                rLastPos = nIdx;
                return nIdx;
            }
        }
    }
    // The world's lamest linear search from svarray...
    for (sal_Int32 nIdx = 0; nIdx < nArrayLen; ++nIdx)
        if (rArray.at(nIdx).get() == p)
        {
            rLastPos = nIdx;
            return rLastPos;
        }

    // XXX "not found" condition for sal_Int32 indexes
    return EE_PARA_MAX;
}

}

sal_Int32 ParaPortionList::GetPos(const ParaPortion* p) const
{
    return FastGetPos(maPortions, p, nLastCache);
}

std::unique_ptr<ParaPortion> ParaPortionList::Release(sal_Int32 nPos)
{
    if (nPos < 0 || maPortions.size() <= o3tl::make_unsigned(nPos))
    {
        SAL_WARN( "editeng", "ParaPortionList::Release - out of bounds pos " << nPos);
        return nullptr;
    }
    std::unique_ptr<ParaPortion> p = std::move(maPortions[nPos]);
    maPortions.erase(maPortions.begin()+nPos);
    return p;
}

void ParaPortionList::Remove(sal_Int32 nPos)
{
    if (nPos < 0 || maPortions.size() <= o3tl::make_unsigned(nPos))
    {
        SAL_WARN( "editeng", "ParaPortionList::Remove - out of bounds pos " << nPos);
        return;
    }
    maPortions.erase(maPortions.begin()+nPos);
}

void ParaPortionList::Insert(sal_Int32 nPos, std::unique_ptr<ParaPortion> p)
{
    if (nPos < 0 || maPortions.size() < o3tl::make_unsigned(nPos))
    {
        SAL_WARN( "editeng", "ParaPortionList::Insert - out of bounds pos " << nPos);
        return;
    }
    maPortions.insert(maPortions.begin()+nPos, std::move(p));
}

void ParaPortionList::Append(std::unique_ptr<ParaPortion> p)
{
    maPortions.push_back(std::move(p));
}

sal_Int32 ParaPortionList::Count() const
{
    size_t nSize = maPortions.size();
    if (nSize > SAL_MAX_INT32)
    {
        SAL_WARN( "editeng", "ParaPortionList::Count - overflow " << nSize);
        return SAL_MAX_INT32;
    }
    return nSize;
}

void ParaPortionList::Reset()
{
    maPortions.clear();
}

tools::Long ParaPortionList::GetYOffset(const ParaPortion* pPPortion) const
{
    tools::Long nHeight = 0;
    for (const auto & rPortion : maPortions)
    {
        const ParaPortion* pTmpPortion = rPortion.get();
        if ( pTmpPortion == pPPortion )
            return nHeight;
        nHeight += pTmpPortion->GetHeight();
    }
    OSL_FAIL( "GetYOffset: Portion not found" );
    return nHeight;
}

sal_Int32 ParaPortionList::FindParagraph(tools::Long nYOffset) const
{
    tools::Long nY = 0;
    for (size_t i = 0, n = maPortions.size(); i < n; ++i)
    {
        nY += maPortions[i]->GetHeight(); // should also be correct even in bVisible!
        if ( nY > nYOffset )
            return i <= SAL_MAX_INT32 ? static_cast<sal_Int32>(i) : SAL_MAX_INT32;
    }
    return EE_PARA_MAX;
}

#if OSL_DEBUG_LEVEL > 0 && !defined NDEBUG
void
ParaPortionList::DbgCheck(ParaPortionList const& rParas, EditDoc const& rDoc)
{
    assert(rParas.Count() == rDoc.Count());
    for (sal_Int32 i = 0; i < rParas.Count(); ++i)
    {
        assert(rParas.SafeGetObject(i) != nullptr);
        assert(rParas.SafeGetObject(i)->GetNode() != nullptr);
        assert(rParas.SafeGetObject(i)->GetNode() == rDoc.GetObject(i));
    }
}
#endif

ContentAttribsInfo::ContentAttribsInfo( SfxItemSet aParaAttribs ) :
        aPrevParaAttribs(std::move( aParaAttribs))
{
}

void ContentAttribsInfo::AppendCharAttrib(EditCharAttrib* pNew)
{
    aPrevCharAttribs.push_back(std::unique_ptr<EditCharAttrib>(pNew));
}

void ConvertItem( std::unique_ptr<SfxPoolItem>& rPoolItem, MapUnit eSourceUnit, MapUnit eDestUnit )
{
    DBG_ASSERT( eSourceUnit != eDestUnit, "ConvertItem - Why?!" );

    switch ( rPoolItem->Which() )
    {
        case EE_PARA_LRSPACE:
        {
            assert(dynamic_cast<const SvxLRSpaceItem *>(rPoolItem.get()) != nullptr);
            SvxLRSpaceItem& rItem = static_cast<SvxLRSpaceItem&>(*rPoolItem);
            if (rItem.GetTextFirstLineOffset().m_nUnit == css::util::MeasureUnit::TWIP)
            {
                rItem.SetTextFirstLineOffset(
                    SvxIndentValue::twips(sal::static_int_cast<short>(OutputDevice::LogicToLogic(
                        rItem.ResolveTextFirstLineOffset({}), eSourceUnit, eDestUnit))));
            }
            rItem.SetTextLeft(SvxIndentValue::twips(
                OutputDevice::LogicToLogic(rItem.ResolveTextLeft({}), eSourceUnit, eDestUnit)));
            if (rItem.GetRight().m_nUnit == css::util::MeasureUnit::TWIP)
            {
                rItem.SetRight(SvxIndentValue::twips(
                    OutputDevice::LogicToLogic(rItem.ResolveRight({}), eSourceUnit, eDestUnit)));
            }
        }
        break;
        case EE_PARA_ULSPACE:
        {
            assert(dynamic_cast<const SvxULSpaceItem *>(rPoolItem.get()) != nullptr);
            SvxULSpaceItem& rItem = static_cast<SvxULSpaceItem&>(*rPoolItem);
            rItem.SetUpper( sal::static_int_cast< sal_uInt16 >( OutputDevice::LogicToLogic( rItem.GetUpper(), eSourceUnit, eDestUnit ) ) );
            rItem.SetLower( sal::static_int_cast< sal_uInt16 >( OutputDevice::LogicToLogic( rItem.GetLower(), eSourceUnit, eDestUnit ) ) );
        }
        break;
        case EE_PARA_SBL:
        {
            assert(dynamic_cast<const SvxLineSpacingItem *>(rPoolItem.get()) != nullptr);
            SvxLineSpacingItem& rItem = static_cast<SvxLineSpacingItem&>(*rPoolItem);
            // SetLineHeight changes also eLineSpace!
            if ( rItem.GetLineSpaceRule() == SvxLineSpaceRule::Min )
                rItem.SetLineHeight( sal::static_int_cast< sal_uInt16 >( OutputDevice::LogicToLogic( rItem.GetLineHeight(), eSourceUnit, eDestUnit ) ) );
        }
        break;
        case EE_PARA_TABS:
        {
            assert(dynamic_cast<const SvxTabStopItem *>(rPoolItem.get()) != nullptr);
            SvxTabStopItem& rItem = static_cast<SvxTabStopItem&>(*rPoolItem);
            SvxTabStopItem* pNewItem(new SvxTabStopItem(EE_PARA_TABS));

            if (sal_Int32 nDefTabDistance = rItem.GetDefaultDistance())
            {
                pNewItem->SetDefaultDistance(
                    OutputDevice::LogicToLogic(nDefTabDistance, eSourceUnit, eDestUnit));
            }

            for ( sal_uInt16 i = 0; i < rItem.Count(); i++ )
            {
                const SvxTabStop& rTab = rItem[i];
                SvxTabStop aNewStop( OutputDevice::LogicToLogic( rTab.GetTabPos(), eSourceUnit, eDestUnit ), rTab.GetAdjustment(), rTab.GetDecimal(), rTab.GetFill() );
                pNewItem->Insert( aNewStop );
            }
            rPoolItem.reset(pNewItem);
        }
        break;
        case EE_CHAR_FONTHEIGHT:
        case EE_CHAR_FONTHEIGHT_CJK:
        case EE_CHAR_FONTHEIGHT_CTL:
        {
            assert(dynamic_cast<const SvxFontHeightItem *>(rPoolItem.get()) != nullptr);
            SvxFontHeightItem& rItem = static_cast<SvxFontHeightItem&>(*rPoolItem);
            rItem.SetHeight( OutputDevice::LogicToLogic( rItem.GetHeight(), eSourceUnit, eDestUnit ) );
        }
        break;
    }
}

void ConvertAndPutItems( SfxItemSet& rDest, const SfxItemSet& rSource, const MapUnit* pSourceUnit, const MapUnit* pDestUnit )
{
    const SfxItemPool* pSourcePool = rSource.GetPool();
    const SfxItemPool* pDestPool = rDest.GetPool();

    for ( sal_uInt16 nWhich = EE_PARA_START; nWhich <= EE_CHAR_END; nWhich++ )
    {
        // If possible go through SlotID ...

        sal_uInt16 nSourceWhich = nWhich;
        sal_uInt16 nSlot = pDestPool->GetTrueSlotId( nWhich );
        if ( nSlot )
        {
            sal_uInt16 nW = pSourcePool->GetTrueWhichIDFromSlotID( nSlot );
            if ( nW )
                nSourceWhich = nW;
        }

        if ( rSource.GetItemState( nSourceWhich, false ) == SfxItemState::SET )
        {
            MapUnit eSourceUnit = pSourceUnit ? *pSourceUnit : pSourcePool->GetMetric( nSourceWhich );
            MapUnit eDestUnit = pDestUnit ? *pDestUnit : pDestPool->GetMetric( nWhich );
            if ( eSourceUnit != eDestUnit )
            {
                std::unique_ptr<SfxPoolItem> pItem(rSource.Get( nSourceWhich ).Clone());
                ConvertItem( pItem, eSourceUnit, eDestUnit );
                pItem->SetWhich(nWhich);
                rDest.Put( std::move(pItem) );
            }
            else
            {
                rDest.Put( rSource.Get( nSourceWhich ).CloneSetWhich(nWhich) );
            }
        }
    }
}

bool EditPaM::DbgIsBuggy( EditDoc const & rDoc ) const
{
    return !pNode ||
           rDoc.GetPos( pNode ) >= rDoc.Count() ||
           nIndex > pNode->Len();
}

bool EditSelection::DbgIsBuggy( EditDoc const & rDoc ) const
{
    return aStartPaM.DbgIsBuggy( rDoc ) || aEndPaM.DbgIsBuggy( rDoc );
}

void EditSelection::Adjust( const EditDoc& rNodes )
{
    DBG_ASSERT( aStartPaM.GetIndex() <= aStartPaM.GetNode()->Len(), "Index out of range in Adjust(1)" );
    DBG_ASSERT( aEndPaM.GetIndex() <= aEndPaM.GetNode()->Len(), "Index out of range in Adjust(2)" );

    const ContentNode* pStartNode = aStartPaM.GetNode();
    const ContentNode* pEndNode = aEndPaM.GetNode();

    sal_Int32 nStartNode = rNodes.GetPos( pStartNode );
    sal_Int32 nEndNode = rNodes.GetPos( pEndNode );

    DBG_ASSERT( nStartNode != SAL_MAX_INT32, "Node out of range in Adjust(1)" );
    DBG_ASSERT( nEndNode != SAL_MAX_INT32, "Node out of range in Adjust(2)" );

    const bool bSwap = ( nStartNode > nEndNode ) ||
                       ( ( nStartNode == nEndNode ) &&
                         ( aStartPaM.GetIndex() > aEndPaM.GetIndex() ) );

    if ( bSwap )
    {
        EditPaM aTmpPaM( aStartPaM );
        aStartPaM = aEndPaM;
        aEndPaM = aTmpPaM;
    }
}

#if ENABLE_YRS
namespace {

struct YrsReplayGuard
{
    IYrsTransactionSupplier *const m_pYrsSupplier;
    IYrsTransactionSupplier::Mode m_Mode;

    explicit YrsReplayGuard(IYrsTransactionSupplier *const pYrsSupplier)
        : m_pYrsSupplier(pYrsSupplier)
    {
        if (m_pYrsSupplier)
        {
            m_Mode = m_pYrsSupplier->SetMode(IYrsTransactionSupplier::Mode::Replay);
        }
    }
    ~YrsReplayGuard()
    {
        if (m_pYrsSupplier)
        {
            m_pYrsSupplier->SetMode(m_Mode);
        }
    }
};

struct YrsWrite
{
    YTransaction *const pTxn;
    Branch *const pProps;
    Branch *const pText;
};

constexpr char CH_PARA = 0x0d;

YrsWrite GetYrsWrite(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rId, YTransaction *const pInTxn = nullptr)
{
    if (!pYrsSupplier)
    {
        return { nullptr, nullptr, nullptr };
    }
    YDoc *const pDoc{pYrsSupplier->GetYDoc()};
    YTransaction *const pTxn{pInTxn ? pInTxn : pYrsSupplier->GetWriteTransaction()};
    // write is disabled when receiving edits from peers
    if (!pTxn)
    {
        return { nullptr, nullptr, nullptr };
    }
    assert(pDoc);
    Branch *const pComments{pYrsSupplier->GetCommentMap()};
    ::std::unique_ptr<YOutput, YOutputDeleter> const pComment{ymap_get(pComments, pTxn, rId.getStr())};
    yvalidate(pComment->tag == Y_ARRAY);
    yvalidate(pComment->len == 1);
    Branch *const pCommentArray{pComment->value.y_type};
    ::std::unique_ptr<YOutput, YOutputDeleter> const pProps{yarray_get(pCommentArray, pTxn, 1)};
    yvalidate(pProps->tag == Y_MAP);
    yvalidate(pProps->len == 1);
    ::std::unique_ptr<YOutput, YOutputDeleter> const pText{yarray_get(pCommentArray, pTxn, 2)};
    yvalidate(pText->tag == Y_TEXT);
    yvalidate(pText->len == 1);
    return { pTxn, pProps->value.y_type, pText->value.y_type };
}

void YrsSetVertical(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rCommentId, bool const isVertical)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    YInput const input{yinput_bool(isVertical ? Y_TRUE : Y_FALSE)};
    ymap_insert(yw.pProps, yw.pTxn, "is-vertical", &input);
}

void YrsSetRotation(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rCommentId, TextRotation const nRotation)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    YInput const input{yinput_long(static_cast<int64_t>(nRotation))};
    ymap_insert(yw.pProps, yw.pTxn, "rotation", &input);
}

void YrsSetDefTab(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rCommentId, sal_uInt16 const nDefTab)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    YInput const input{yinput_long(static_cast<int64_t>(nDefTab))};
    ymap_insert(yw.pProps, yw.pTxn, "def-tab", &input);
}

void YrsInsertAttribImplImpl(YrsWrite const& yw, SfxPoolItem const& rItm,
    uint32_t const nStart, uint32_t const nLen)
{
    ::std::vector<YInput> tabStops;
    ::std::vector<::std::vector<YInput>> tabStopValues;
    ::std::vector<OString> tempStrings;
    ::std::vector<YInput> itemArray;
    ::std::vector<char const*> itemNames;
    YInput attr;
    char const* attrName;
    switch (rItm.Which())
    {
        case EE_CHAR_COLOR:
        case EE_CHAR_BKGCOLOR:
        {
            sal_uInt32 const nColor{static_cast<SvxColorItem const&>(rItm).getColor()};
            attr = yinput_long(nColor);
            attrName = rItm.Which() == EE_CHAR_COLOR ? "EE_CHAR_COLOR" : "EE_CHAR_BKGCOLOR";
            break;
        }
        case EE_CHAR_FONTINFO:
        case EE_CHAR_FONTINFO_CJK:
        case EE_CHAR_FONTINFO_CTL:
        {
            SvxFontItem const& rItem{static_cast<SvxFontItem const&>(rItm)};
            tempStrings.reserve(2); // prevent realloc
            tempStrings.emplace_back(OUStringToOString(rItem.GetFamilyName(), RTL_TEXTENCODING_UTF8));
            itemArray.emplace_back(yinput_string(tempStrings.back().getStr()));
            itemNames.emplace_back("familyname");
            tempStrings.emplace_back(OUStringToOString(rItem.GetStyleName(), RTL_TEXTENCODING_UTF8));
            itemArray.emplace_back(yinput_string(tempStrings.back().getStr()));
            itemNames.emplace_back("style");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetFamily())));
            itemNames.emplace_back("family");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPitch())));
            itemNames.emplace_back("pitch");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetCharSet())));
            itemNames.emplace_back("charset");
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = rItm.Which() == EE_CHAR_FONTINFO
                ? "EE_CHAR_FONTINFO"
                : rItm.Which() == EE_CHAR_FONTINFO_CJK ? "EE_CHAR_FONTINFO_CJK" : "EE_CHAR_FONTINFO_CTL";
            break;
        }
        case EE_CHAR_FONTHEIGHT:
        case EE_CHAR_FONTHEIGHT_CJK:
        case EE_CHAR_FONTHEIGHT_CTL:
        {
            SvxFontHeightItem const& rItem{static_cast<SvxFontHeightItem const&>(rItm)};
            itemNames.emplace_back("height");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetHeight())));
            itemNames.emplace_back("prop");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetProp())));
            itemNames.emplace_back("propunit");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPropUnit())));
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = rItm.Which() == EE_CHAR_FONTHEIGHT
                ? "EE_CHAR_FONTHEIGHT"
                : rItm.Which() == EE_CHAR_FONTHEIGHT_CJK ? "EE_CHAR_FONTHEIGHT_CJK" : "EE_CHAR_FONTHEIGHT_CTL";
            break;
        }
        case EE_CHAR_FONTWIDTH:
        {
            SvxCharScaleWidthItem const& rItem{static_cast<SvxCharScaleWidthItem const&>(rItm)};
            attr = yinput_long(rItem.GetValue());
            attrName = "EE_CHAR_FONTWIDTH";
            break;
        }
        case EE_CHAR_WEIGHT:
        case EE_CHAR_WEIGHT_CJK:
        case EE_CHAR_WEIGHT_CTL:
        {
            SvxWeightItem const& rItem{static_cast<SvxWeightItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetWeight()));
            attrName = rItm.Which() == EE_CHAR_WEIGHT
                ? "EE_CHAR_WEIGHT"
                : rItm.Which() == EE_CHAR_WEIGHT_CJK ? "EE_CHAR_WEIGHT_CJK" : "EE_CHAR_WEIGHT_CTL";
            break;
        }
        case EE_CHAR_UNDERLINE:
        case EE_CHAR_OVERLINE:
        {
            SvxTextLineItem const& rItem{static_cast<SvxTextLineItem const&>(rItm)};
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineStyle())));
            itemNames.emplace_back("style");
            itemArray.emplace_back(yinput_long(uint64_t(sal_uInt32(rItem.GetColor()))));
            itemNames.emplace_back("color");
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = rItm.Which() == EE_CHAR_UNDERLINE ? "EE_CHAR_UNDERLINE" : "EE_CHAR_OVERLINE";
            break;
        }
        case EE_CHAR_STRIKEOUT:
        {
            SvxCrossedOutItem const& rItem{static_cast<SvxCrossedOutItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetStrikeout()));
            attrName = "EE_CHAR_STRIKEOUT";
            break;
        }
        case EE_CHAR_ITALIC:
        case EE_CHAR_ITALIC_CJK:
        case EE_CHAR_ITALIC_CTL:
        {
            SvxPostureItem const& rItem{static_cast<SvxPostureItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetPosture()));
            attrName = rItm.Which() == EE_CHAR_ITALIC
                ? "EE_CHAR_ITALIC"
                : rItm.Which() == EE_CHAR_ITALIC_CJK ? "EE_CHAR_ITALIC_CJK" : "EE_CHAR_ITALIC_CTL";
            break;
        }
        case EE_CHAR_OUTLINE:
        {
            SvxContourItem const& rItem{static_cast<SvxContourItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_CHAR_OUTLINE";
            break;
        }
        case EE_CHAR_SHADOW:
        {
            SvxShadowedItem const& rItem{static_cast<SvxShadowedItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_CHAR_SHADOW";
            break;
        }
        case EE_CHAR_ESCAPEMENT:
        {
            SvxEscapementItem const& rItem{static_cast<SvxEscapementItem const&>(rItm)};
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetEsc())));
            itemNames.emplace_back("esc");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetProportionalHeight())));
            itemNames.emplace_back("prop");
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = "EE_CHAR_ESCAPEMENT";
            break;
        }
        case EE_CHAR_PAIRKERNING:
        {
            SvxAutoKernItem const& rItem{static_cast<SvxAutoKernItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_CHAR_PAIRKERNING";
            break;
        }
        case EE_CHAR_KERNING:
        {
            SvxKerningItem const& rItem{static_cast<SvxKerningItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_CHAR_KERNING";
            break;
        }
        case EE_CHAR_WLM:
        {
            SvxWordLineModeItem const& rItem{static_cast<SvxWordLineModeItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_CHAR_WLM";
            break;
        }
        case EE_CHAR_LANGUAGE:
        case EE_CHAR_LANGUAGE_CJK:
        case EE_CHAR_LANGUAGE_CTL:
        {
            SvxLanguageItem const& rItem{static_cast<SvxLanguageItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue().get()));
            attrName = rItm.Which() == EE_CHAR_LANGUAGE
                ? "EE_CHAR_LANGUAGE"
                : rItm.Which() == EE_CHAR_LANGUAGE_CJK ? "EE_CHAR_LANGUAGE_CJK" : "EE_CHAR_LANGUAGE_CTL";
            break;
        }
        case EE_CHAR_EMPHASISMARK:
        {
            SvxEmphasisMarkItem const& rItem{static_cast<SvxEmphasisMarkItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_CHAR_EMPHASISMARK";
            break;
        }
        case EE_CHAR_RELIEF:
        {
            SvxCharReliefItem const& rItem{static_cast<SvxCharReliefItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_CHAR_RELIEF";
            break;
        }
        case EE_CHAR_CASEMAP:
        {
            SvxCaseMapItem const& rItem{static_cast<SvxCaseMapItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_CHAR_CASEMAP";
            break;
        }
        case EE_PARA_WRITINGDIR:
        {
            SvxFrameDirectionItem const& rItem{static_cast<SvxFrameDirectionItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_PARA_WRITINGDIR";
            break;
        }
        case EE_PARA_AUTOWRITINGDIR:
        {
            SvxAutoFrameDirectionItem const& rItem{ static_cast<SvxAutoFrameDirectionItem const&>(
                rItm) };
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_PARA_AUTOWRITINGDIR";
            break;
        }
        case EE_PARA_HANGINGPUNCTUATION:
        {
            SvxHangingPunctuationItem const& rItem{static_cast<SvxHangingPunctuationItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_PARA_HANGINGPUNCTUATION";
            break;
        }
        case EE_PARA_FORBIDDENRULES:
        {
            SvxForbiddenRuleItem const& rItem{static_cast<SvxForbiddenRuleItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_PARA_FORBIDDENRULES";
            break;
        }
        case EE_PARA_ASIANCJKSPACING:
        {
            SvxScriptSpaceItem const& rItem{static_cast<SvxScriptSpaceItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = "EE_PARA_ASIANCJKSPACING";
            break;
        }
//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId<SvxNumBulletItem>          EE_PARA_NUMBULLET          (EE_PARA_START+5);
        case EE_PARA_HYPHENATE:
        case EE_PARA_HYPHENATE_NO_CAPS:
        case EE_PARA_HYPHENATE_NO_LAST_WORD:
        case EE_PARA_BULLETSTATE:
        {
            SfxBoolItem const& rItem{static_cast<SfxBoolItem const&>(rItm)};
            attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE);
            attrName = rItm.Which() == EE_PARA_HYPHENATE
                ? "EE_PARA_HYPHENATE"
                : rItm.Which() == EE_PARA_HYPHENATE_NO_CAPS
                    ? "EE_PARA_HYPHENATE_NO_CAPS"
                    : rItm.Which() == EE_PARA_HYPHENATE_NO_LAST_WORD
                        ? "EE_PARA_HYPHENATE_NO_LAST_WORD"
                        : "EE_PARA_BULLETSTATE";
            break;
        }
//TODO no way to set this in comment? inline constexpr TypedWhichId<SvxLRSpaceItem>            EE_PARA_OUTLLRSPACE        (EE_PARA_START+10);
        case EE_PARA_OUTLLEVEL:
        {
            SfxInt16Item const& rItem{static_cast<SfxInt16Item const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_PARA_OUTLLEVEL";
            break;
        }
//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId<SvxBulletItem>             EE_PARA_BULLET             (EE_PARA_START+12);
        case EE_PARA_LRSPACE:
        {
            SvxLRSpaceItem const& rItem{static_cast<SvxLRSpaceItem const&>(rItm)};
            itemArray.emplace_back(yinput_float(rItem.GetTextFirstLineOffset().m_dValue));
            itemNames.emplace_back("first-line-offset");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetTextFirstLineOffset().m_nUnit)));
            itemNames.emplace_back("first-line-offset-unit");
            itemArray.emplace_back(yinput_float(rItem.GetLeft().m_dValue));
            itemNames.emplace_back("left-margin");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLeft().m_nUnit)));
            itemNames.emplace_back("left-margin-unit");
            itemArray.emplace_back(yinput_float(rItem.GetRight().m_dValue));
            itemNames.emplace_back("right-margin");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetRight().m_nUnit)));
            itemNames.emplace_back("right-margin-unit");
            itemArray.emplace_back(yinput_bool(rItem.IsAutoFirst() ? Y_TRUE : Y_FALSE));
            itemNames.emplace_back("auto-first");
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = "EE_PARA_LRSPACE";
            break;
        }
        case EE_PARA_ULSPACE:
        {
            SvxULSpaceItem const& rItem{static_cast<SvxULSpaceItem const&>(rItm)};
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetUpper())));
            itemNames.emplace_back("upper-margin");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLower())));
            itemNames.emplace_back("lower-margin");
            // TODO what does EE support here?
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = "EE_PARA_ULSPACE";
            break;
        }
        case EE_PARA_SBL:
        {
            SvxLineSpacingItem const& rItem{static_cast<SvxLineSpacingItem const&>(rItm)};
            switch (rItem.GetLineSpaceRule())
            {
                case SvxLineSpaceRule::Auto:
                    break;
                case SvxLineSpaceRule::Fix:
                    itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineHeight())));
                    itemNames.emplace_back("line-space-fix");
                    break;
                case SvxLineSpaceRule::Min:
                    itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineHeight())));
                    itemNames.emplace_back("line-space-min");
                    break;
            }
            switch (rItem.GetInterLineSpaceRule())
            {
                case SvxInterLineSpaceRule::Off:
                    break;
                case SvxInterLineSpaceRule::Prop:
                    itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPropLineSpace())));
                    itemNames.emplace_back("inter-line-space-prop");
                    break;
                case SvxInterLineSpaceRule::Fix:
                    itemArray.emplace_back(yinput_long(uint64_t(rItem.GetInterLineSpace())));
                    itemNames.emplace_back("inter-line-space-fix");
                    break;
            }
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = "EE_PARA_SBL";
            break;
        }
        case EE_PARA_JUST:
        {
            SvxAdjustItem const& rItem{static_cast<SvxAdjustItem const&>(rItm)};
            switch (rItem.GetAdjust())
            {
                case SvxAdjust::Left:
                case SvxAdjust::Right:
                case SvxAdjust::Center:
                    attr = yinput_long(uint64_t(rItem.GetAdjust()));
                    break;
                case SvxAdjust::Block:
                    switch (rItem.GetLastBlock())
                    {
                        case SvxAdjust::Left:
                            attr = yinput_long(uint64_t(SvxAdjust::Block));
                            break;
                        case SvxAdjust::Center:
                            attr = yinput_long(uint64_t(5));
                            break;
                        case SvxAdjust::Block:
                            attr = yinput_long(uint64_t(rItem.GetOneWord() == SvxAdjust::Block ? 7 : 6));
                            break;
                        default:
                            assert(false);
                    }
                    break;
                default:
                    assert(false);
            }
            attrName = "EE_PARA_JUST";
            break;
        }
        case EE_PARA_TABS:
        {
            SvxTabStopItem const& rItem{static_cast<SvxTabStopItem const&>(rItm)};
            itemNames.emplace_back("default-distance");
            itemArray.emplace_back(yinput_long(uint64_t(rItem.GetDefaultDistance())));
            tabStopValues.reserve(rItem.Count()); // prevent realloc
            for (decltype(rItem.Count()) i{0}; i < rItem.Count(); ++i)
            {
                SvxTabStop const& rTab{rItem.At(i)};
                char const*const names[]{"pos", "adjustment", "decimal", "fill"};
                tabStopValues.emplace_back();
                tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetTabPos())));
                tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetAdjustment())));
                tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetDecimal())));
                tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetFill())));
                tabStops.emplace_back(yinput_json_map(const_cast<char**>(names), tabStopValues.back().data(), 4));
            }
            itemNames.emplace_back("tab-stops");
            itemArray.emplace_back(yinput_json_array(tabStops.data(), tabStops.size()));
            attr = yinput_json_map(const_cast<char**>(itemNames.data()), itemArray.data(), itemArray.size());
            attrName = "EE_PARA_TABS";
            break;
        }
        case EE_PARA_JUST_METHOD:
        {
            SvxJustifyMethodItem const& rItem{static_cast<SvxJustifyMethodItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_PARA_JUST_METHOD";
            break;
        }
        case EE_PARA_VER_JUST:
        {
            SvxVerJustifyItem const& rItem{static_cast<SvxVerJustifyItem const&>(rItm)};
            attr = yinput_long(uint64_t(rItem.GetValue()));
            attrName = "EE_PARA_VER_JUST";
            break;
        }
        // these aren't editable?
//constexpr TypedWhichId<SvXMLAttrContainerItem> EE_CHAR_XMLATTRIBS     (EE_CHAR_START+27);
//constexpr TypedWhichId<SfxGrabBagItem>         EE_CHAR_GRABBAG        (EE_CHAR_START+30);

        default:
            assert(false);
    }
    assert(itemNames.size() == itemArray.size());
    YInput const attrs{yinput_json_map(const_cast<char**>(&attrName), &attr, 1)};
    ytext_format(yw.pText, yw.pTxn, nStart, nLen, &attrs);
}

void YrsInsertAttribImpl(YrsWrite const& yw, uint32_t const offset, EditCharAttrib const*const pAttr)
{
    auto const start{offset + pAttr->GetStart()};
    auto const len{pAttr->GetEnd() - pAttr->GetStart()};
    YrsInsertAttribImplImpl(yw, *pAttr->GetItem(), start, len);
}

void YrsInsertFeature(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, EditCharAttrib const*const pAttr)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += pAttr->GetStart();
    char const feature[]{ CH_FEATURE, '\0' };
    switch (pAttr->Which())
    {
        case EE_FEATURE_TAB:
        case EE_FEATURE_LINEBR:
        {
            YInput const type{yinput_string(pAttr->Which() == EE_FEATURE_TAB ? "tab" : "line")};
            YInput attrArray[]{ type };
            char const*const attrNames[]{ "feature" };
            YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 1)};
            ytext_insert(yw.pText, yw.pTxn, i, feature, &attrs);
            break;
        }
        case EE_FEATURE_FIELD:
        {
            SvxURLField const*const pURLField{dynamic_cast<SvxURLField const*>(dynamic_cast<SvxFieldItem const*>(pAttr->GetItem())->GetField())};
            assert(pURLField);
            YInput const type{yinput_string("url")};
            // ??? somehow this comes out as Y_JSON_NUM at the other end?
            YInput const format{yinput_long(static_cast<int64_t>(pURLField->GetFormat()))};
            OString const urlStr{OUStringToOString(pURLField->GetURL(), RTL_TEXTENCODING_UTF8)};
            YInput const url{yinput_string(urlStr.getStr())};
            OString const reprStr{OUStringToOString(pURLField->GetRepresentation(), RTL_TEXTENCODING_UTF8)};
            YInput const representation{yinput_string(reprStr.getStr())};
            OString const targetStr{OUStringToOString(pURLField->GetTargetFrame(), RTL_TEXTENCODING_UTF8)};
            YInput const targetframe{yinput_string(targetStr.getStr())};
            YInput attrArray[]{ type, format, url, representation, targetframe };
            char const*const attrNames[]{ "feature", "url-format", "url-url", "url-representation", "url-targetframe" };
            // don't use yinput_ymap for this!
            YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 5)};
            ytext_insert(yw.pText, yw.pTxn, i, feature, &attrs);

            break;
        }
        default: // EE_FEATURE_NOTCONV appears unused?
            assert(false);
    }
}

void YrsAddPara(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rCommentId, EditDoc const& rDoc, uint32_t const index)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    SAL_INFO("editeng.yrs", "YRS YrsAddPara");
    // need to encode into 1 YText
    char const para[]{ CH_PARA, '\0' };
    uint32_t i{0};
    // UTF-16 index should be equal to EditDoc one
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    ContentAttribs const& rParaAttribs{rDoc.GetObject(index)->GetContentAttribs()};
    auto const pStyle{rParaAttribs.GetStyleSheet()};
    if (pStyle)
    {
        OString const styleName{OUStringToOString(pStyle->GetName(), RTL_TEXTENCODING_UTF8)};
        YInput const style{yinput_string(styleName.getStr())};
        YInput attrArray[]{ style };
        char const*const attrNames[]{ "para-style" };
        YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 1)};
        ytext_insert(yw.pText, yw.pTxn, i, para, &attrs);
    }
    else
    {
        ytext_insert(yw.pText, yw.pTxn, i, para, nullptr);
    }
    for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem())
    {
        YrsInsertAttribImplImpl(yw, *it.GetCurItem(), i, 1);
    }
}

void YrsRemovePara(IYrsTransactionSupplier *const pYrsSupplier,
    OString const& rCommentId, EditDoc const& rDoc, uint32_t const index)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    if (index != 0)
    {
        for (auto paras{index}; paras != 0; --paras)
        {
            i += rDoc.GetObject(paras-1)->Len() + 1;
        }
    }
    uint32_t const len(rDoc.GetObject(index)->Len() + 1);
    ytext_remove_range(yw.pText, yw.pTxn, i, len);
}

void YrsClear(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    auto const len{ytext_len(yw.pText, yw.pTxn)};
    ytext_remove_range(yw.pText, yw.pTxn, 0, len);
}

void YrsInsertParaBreak(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, uint32_t const content)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    // need to encode into 1 YText
    char const para[]{ CH_PARA, '\0' };
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += content;
    ContentAttribs const& rParaAttribs{rDoc.GetObject(index)->GetContentAttribs()};
    OString const styleName{OUStringToOString(rParaAttribs.GetStyleSheet()->GetName(), RTL_TEXTENCODING_UTF8)};
    YInput const style{yinput_string(styleName.getStr())};
    YInput attrArray[]{ style };
    char const*const attrNames[]{ "para-style" };
    YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 1)};
    ytext_insert(yw.pText, yw.pTxn, i, para, &attrs);
    for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem())
    {
        YrsInsertAttribImplImpl(yw, *it.GetCurItem(), i, 1);
    }
}

void YrsInsertText(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, uint32_t const content, ::std::u16string_view const rText)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += content;
    OString const text{::rtl::OUStringToOString(rText, RTL_TEXTENCODING_UTF8)};
    ytext_insert(yw.pText, yw.pTxn, i, text.getStr(), nullptr);
}

void YrsConnectPara(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, uint32_t const pos)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += pos;
    ytext_remove_range(yw.pText, yw.pTxn, i, 1);
}

void YrsRemoveChars(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, uint32_t const content, uint32_t const length)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += content;
    ytext_remove_range(yw.pText, yw.pTxn, i, length);
}

void YrsSetStyle(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, ::std::u16string_view const rStyle)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += rDoc.GetObject(index)->Len();
    OString const styleName{OUStringToOString(rStyle, RTL_TEXTENCODING_UTF8)};
    YInput const style{yinput_string(styleName.getStr())};
    YInput attrArray[]{ style };
    char const*const attrNames[]{ "para-style" };
    YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 1)};
    ytext_format(yw.pText, yw.pTxn, i, 1, &attrs);
}

void YrsSetParaAttr(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, SfxPoolItem const& rItem)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    i += rDoc.GetObject(index)->Len();
    YrsInsertAttribImplImpl(yw, rItem, i, 1);
}

char const* YrsWhichToAttrName(sal_Int16 const nWhich)
{
    switch (nWhich)
    {
        case EE_CHAR_COLOR:
            return "EE_CHAR_COLOR";
        case EE_CHAR_BKGCOLOR:
            return "EE_CHAR_BKGCOLOR";
        case EE_CHAR_FONTINFO:
            return "EE_CHAR_FONTINFO";
        case EE_CHAR_FONTINFO_CJK:
            return "EE_CHAR_FONTINFO_CJK";
        case EE_CHAR_FONTINFO_CTL:
            return "EE_CHAR_FONTINFO_CTL";
        case EE_CHAR_FONTHEIGHT:
            return "EE_CHAR_FONTHEIGHT";
        case EE_CHAR_FONTHEIGHT_CJK:
            return "EE_CHAR_FONTHEIGHT_CJK";
        case EE_CHAR_FONTHEIGHT_CTL:
            return "EE_CHAR_FONTHEIGHT_CTL";
        case EE_CHAR_FONTWIDTH:
            return "EE_CHAR_FONTWIDTH";
        case EE_CHAR_WEIGHT:
            return "EE_CHAR_WEIGHT";
        case EE_CHAR_WEIGHT_CJK:
            return "EE_CHAR_WEIGHT_CJK";
        case EE_CHAR_WEIGHT_CTL:
            return "EE_CHAR_WEIGHT_CTL";
        case EE_CHAR_UNDERLINE:
            return "EE_CHAR_UNDERLINE";
        case EE_CHAR_OVERLINE:
            return "EE_CHAR_OVERLINE";
        case EE_CHAR_STRIKEOUT:
            return "EE_CHAR_STRIKEOUT";
        case EE_CHAR_ITALIC:
            return "EE_CHAR_ITALIC";
        case EE_CHAR_ITALIC_CJK:
            return "EE_CHAR_ITALIC_CJK";
        case EE_CHAR_ITALIC_CTL:
            return "EE_CHAR_ITALIC_CTL";
        case EE_CHAR_OUTLINE:
            return "EE_CHAR_OUTLINE";
        case EE_CHAR_SHADOW:
            return "EE_CHAR_SHADOW";
        case EE_CHAR_ESCAPEMENT:
            return "EE_CHAR_ESCAPEMENT";
        case EE_CHAR_PAIRKERNING:
            return "EE_CHAR_PAIRKERNING";
        case EE_CHAR_KERNING:
            return "EE_CHAR_KERNING";
        case EE_CHAR_WLM:
            return "EE_CHAR_WLM";
        case EE_CHAR_LANGUAGE:
            return "EE_CHAR_LANGUAGE";
        case EE_CHAR_LANGUAGE_CJK:
            return "EE_CHAR_LANGUAGE_CJK";
        case EE_CHAR_LANGUAGE_CTL:
            return "EE_CHAR_LANGUAGE_CTL";
        case EE_CHAR_EMPHASISMARK:
            return "EE_CHAR_EMPHASISMARK";
        case EE_CHAR_RELIEF:
            return "EE_CHAR_RELIEF";
        case EE_CHAR_CASEMAP:
            return "EE_CHAR_CASEMAP";
        case EE_PARA_WRITINGDIR:
            return "EE_PARA_WRITINGDIR";
        case EE_PARA_HANGINGPUNCTUATION:
            return "EE_PARA_HANGINGPUNCTUATION";
        case EE_PARA_FORBIDDENRULES:
            return "EE_PARA_FORBIDDENRULES";
        case EE_PARA_ASIANCJKSPACING:
            return "EE_PARA_ASIANCJKSPACING";
//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId<SvxNumBulletItem>          EE_PARA_NUMBULLET          (EE_PARA_START+5);
        case EE_PARA_HYPHENATE:
            return "EE_PARA_HYPHENATE";
        case EE_PARA_HYPHENATE_NO_CAPS:
            return "EE_PARA_HYPHENATE_NO_CAPS";
        case EE_PARA_HYPHENATE_NO_LAST_WORD:
            return "EE_PARA_HYPHENATE_NO_LAST_WORD";
        case EE_PARA_BULLETSTATE:
            return "EE_PARA_BULLETSTATE";
//TODO no way to set this in comment? inline constexpr TypedWhichId<SvxLRSpaceItem>            EE_PARA_OUTLLRSPACE        (EE_PARA_START+10);
        case EE_PARA_OUTLLEVEL:
            return "EE_PARA_OUTLLEVEL";
//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId<SvxBulletItem>             EE_PARA_BULLET             (EE_PARA_START+12);
        case EE_PARA_LRSPACE:
            return "EE_PARA_LRSPACE";
        case EE_PARA_ULSPACE:
            return "EE_PARA_ULSPACE";
        case EE_PARA_SBL:
            return "EE_PARA_SBL";
        case EE_PARA_JUST:
            return "EE_PARA_JUST";
        case EE_PARA_TABS:
            return "EE_PARA_TABS";
        case EE_PARA_JUST_METHOD:
            return "EE_PARA_JUST_METHOD";
        case EE_PARA_VER_JUST:
            return "EE_PARA_VER_JUST";
        default:
            assert(false);
            abort();
    }
}

void YrsRemoveAttrib(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId,
    EditDoc const& rDoc, uint32_t const index, sal_uInt16 const nWhich, sal_Int32 const nStart, sal_Int32 const nEnd)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    YInput const attr{yinput_null()};
    char const*const attrName{YrsWhichToAttrName(nWhich)};
    YInput const attrs{yinput_json_map(const_cast<char**>(&attrName), const_cast<YInput*>(&attr), 1)};
    ytext_format(yw.pText, yw.pTxn, i + nStart, nEnd - nStart, &attrs);
}

void YrsInsertAttrib(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, EditDoc const& rDoc, uint32_t const index, EditCharAttrib const*const pAttr)
{
    YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)};
    if (yw.pTxn == nullptr)
    {
        return;
    }
    uint32_t i{0};
    for (auto paras{index}; paras != 0; --paras)
    {
        i += rDoc.GetObject(paras-1)->Len() + 1;
    }
    YrsInsertAttribImpl(yw, i, pAttr);
}

uint64_t YrsReadInt(YOutput const& rValue)
{
    // with the v1 encoding, JSON is being sent apparently (like "family":2) , which has issues with integers being sometimes read as floats so workaround here
    if (rValue.tag == Y_JSON_INT)
    {
        return rValue.value.integer;
    }
    else
    {
        yvalidate(rValue.tag == Y_JSON_NUM);
        return ::std::lround(rValue.value.num);
    }
}

void YrsImplInsertAttr(SfxItemSet & rSet, ::std::vector<sal_uInt16> *const pRemoved,
    char const*const pKey, YOutput const& rValue)
{
    sal_uInt16 nWhich{0};
    if (strcmp(pKey, "EE_CHAR_COLOR") == 0)
    {
        nWhich = EE_CHAR_COLOR;
    }
    else if (strcmp(pKey, "EE_CHAR_BKGCOLOR") == 0)
    {
        nWhich = EE_CHAR_BKGCOLOR;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTINFO") == 0)
    {
        nWhich = EE_CHAR_FONTINFO;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTINFO_CJK") == 0)
    {
        nWhich = EE_CHAR_FONTINFO_CJK;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTINFO_CTL") == 0)
    {
        nWhich = EE_CHAR_FONTINFO_CTL;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT") == 0)
    {
        nWhich = EE_CHAR_FONTHEIGHT;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT_CJK") == 0)
    {
        nWhich = EE_CHAR_FONTHEIGHT_CJK;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT_CTL") == 0)
    {
        nWhich = EE_CHAR_FONTHEIGHT_CTL;
    }
    else if (strcmp(pKey, "EE_CHAR_FONTWIDTH") == 0)
    {
        nWhich = EE_CHAR_FONTWIDTH;
    }
    else if (strcmp(pKey, "EE_CHAR_WEIGHT") == 0)
    {
        nWhich = EE_CHAR_WEIGHT;
    }
    else if (strcmp(pKey, "EE_CHAR_WEIGHT_CJK") == 0)
    {
        nWhich = EE_CHAR_WEIGHT_CJK;
    }
    else if (strcmp(pKey, "EE_CHAR_WEIGHT_CTL") == 0)
    {
        nWhich = EE_CHAR_WEIGHT_CTL;
    }
    else if (strcmp(pKey, "EE_CHAR_UNDERLINE") == 0)
    {
        nWhich = EE_CHAR_UNDERLINE;
    }
    else if (strcmp(pKey, "EE_CHAR_OVERLINE") == 0)
    {
        nWhich = EE_CHAR_OVERLINE;
    }
    else if (strcmp(pKey, "EE_CHAR_STRIKEOUT") == 0)
    {
        nWhich = EE_CHAR_STRIKEOUT;
    }
    else if (strcmp(pKey, "EE_CHAR_ITALIC") == 0)
    {
        nWhich = EE_CHAR_ITALIC;
    }
    else if (strcmp(pKey, "EE_CHAR_ITALIC_CJK") == 0)
    {
        nWhich = EE_CHAR_ITALIC_CJK;
    }
    else if (strcmp(pKey, "EE_CHAR_ITALIC_CTL") == 0)
    {
        nWhich = EE_CHAR_ITALIC_CTL;
    }
    else if (strcmp(pKey, "EE_CHAR_OUTLINE") == 0)
    {
        nWhich = EE_CHAR_OUTLINE;
    }
    else if (strcmp(pKey, "EE_CHAR_SHADOW") == 0)
    {
        nWhich = EE_CHAR_SHADOW;
    }
    else if (strcmp(pKey, "EE_CHAR_ESCAPEMENT") == 0)
    {
        nWhich = EE_CHAR_ESCAPEMENT;
    }
    else if (strcmp(pKey, "EE_CHAR_PAIRKERNING") == 0)
    {
        nWhich = EE_CHAR_PAIRKERNING;
    }
    else if (strcmp(pKey, "EE_CHAR_KERNING") == 0)
    {
        nWhich = EE_CHAR_KERNING;
    }
    else if (strcmp(pKey, "EE_CHAR_WLM") == 0)
    {
        nWhich = EE_CHAR_WLM;
    }
    else if (strcmp(pKey, "EE_CHAR_LANGUAGE") == 0)
    {
        nWhich = EE_CHAR_LANGUAGE;
    }
    else if (strcmp(pKey, "EE_CHAR_LANGUAGE_CJK") == 0)
    {
        nWhich = EE_CHAR_LANGUAGE_CJK;
    }
    else if (strcmp(pKey, "EE_CHAR_LANGUAGE_CTL") == 0)
    {
        nWhich = EE_CHAR_LANGUAGE_CTL;
    }
    else if (strcmp(pKey, "EE_CHAR_EMPHASISMARK") == 0)
    {
        nWhich = EE_CHAR_EMPHASISMARK;
    }
    else if (strcmp(pKey, "EE_CHAR_RELIEF") == 0)
    {
        nWhich = EE_CHAR_RELIEF;
    }
    else if (strcmp(pKey, "EE_CHAR_CASEMAP") == 0)
    {
        nWhich = EE_CHAR_CASEMAP;
    }
    else if (strcmp(pKey, "EE_PARA_WRITINGDIR") == 0)
    {
        nWhich = EE_PARA_WRITINGDIR;
    }
    else if (strcmp(pKey, "EE_PARA_HANGINGPUNCTUATION") == 0)
    {
        nWhich = EE_PARA_HANGINGPUNCTUATION;
    }
    else if (strcmp(pKey, "EE_PARA_FORBIDDENRULES") == 0)
    {
        nWhich = EE_PARA_FORBIDDENRULES;
    }
    else if (strcmp(pKey, "EE_PARA_ASIANCJKSPACING") == 0)
    {
        nWhich = EE_PARA_ASIANCJKSPACING;
    }
    else if (strcmp(pKey, "EE_PARA_HYPHENATE") == 0)
    {
        nWhich = EE_PARA_HYPHENATE;
    }
    else if (strcmp(pKey, "EE_PARA_HYPHENATE_NO_CAPS") == 0)
    {
        nWhich = EE_PARA_HYPHENATE_NO_CAPS;
    }
    else if (strcmp(pKey, "EE_PARA_HYPHENATE_NO_LAST_WORD") == 0)
    {
        nWhich = EE_PARA_HYPHENATE_NO_LAST_WORD;
    }
    else if (strcmp(pKey, "EE_PARA_BULLETSTATE") == 0)
    {
        nWhich = EE_PARA_BULLETSTATE;
    }
    else if (strcmp(pKey, "EE_PARA_OUTLLEVEL") == 0)
    {
        nWhich = EE_PARA_OUTLLEVEL;
    }
    else if (strcmp(pKey, "EE_PARA_LRSPACE") == 0)
    {
        nWhich = EE_PARA_LRSPACE;
    }
    else if (strcmp(pKey, "EE_PARA_ULSPACE") == 0)
    {
        nWhich = EE_PARA_ULSPACE;
    }
    else if (strcmp(pKey, "EE_PARA_SBL") == 0)
    {
        nWhich = EE_PARA_SBL;
    }
    else if (strcmp(pKey, "EE_PARA_JUST") == 0)
    {
        nWhich = EE_PARA_JUST;
    }
    else if (strcmp(pKey, "EE_PARA_TABS") == 0)
    {
        nWhich = EE_PARA_TABS;
    }
    else if (strcmp(pKey, "EE_PARA_JUST_METHOD") == 0)
    {
        nWhich = EE_PARA_JUST_METHOD;
    }
    else if (strcmp(pKey, "EE_PARA_VER_JUST") == 0)
    {
        nWhich = EE_PARA_VER_JUST;
    }
    else if (pKey[0] == 'E' && pKey[1] == 'E' && pKey[2] == '_')
    {
        abort();
    }
    else
    {
        return;
    }

    if (rValue.tag == Y_JSON_NULL)
    {
        assert(pRemoved);
        if (pRemoved)
        {
            pRemoved->emplace_back(nWhich);
        }
        return;
    }
    else switch (nWhich)
    {
        case EE_CHAR_COLOR:
        case EE_CHAR_BKGCOLOR:
        {
            Color const c(ColorTransparency, YrsReadInt(rValue));
            SvxColorItem const item(c, nWhich);
            rSet.Put(item);
            break;
        }
        case EE_CHAR_FONTINFO:
        case EE_CHAR_FONTINFO_CJK:
        case EE_CHAR_FONTINFO_CTL:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<OUString> oFamilyName;
            ::std::optional<OUString> oStyle;
            ::std::optional<FontFamily> oFamily;
            ::std::optional<FontPitch> oPitch;
            ::std::optional<rtl_TextEncoding> oCharset;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "familyname") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_STR);
                    oFamilyName.emplace(OStringToOUString(rValue.value.map[i].value->value.str, RTL_TEXTENCODING_UTF8));
                }
                else if (strcmp(pEntry, "style") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_STR);
                    oStyle.emplace(OStringToOUString(rValue.value.map[i].value->value.str, RTL_TEXTENCODING_UTF8));
                }
                else if (strcmp(pEntry, "family") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(FAMILY_DONTKNOW <= value && value <= FAMILY_SYSTEM);
                    oFamily.emplace(FontFamily(value));
                }
                else if (strcmp(pEntry, "pitch") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(PITCH_DONTKNOW <= value && value <= PITCH_VARIABLE);
                    oPitch.emplace(FontPitch(value));
                }
                else if (strcmp(pEntry, "charset") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*RTL_TEXTENCODING_DONTKNOW <= value &&*/ value <= RTL_TEXTENCODING_UNICODE);
                    oCharset.emplace(rtl_TextEncoding(value));
                }
                else yvalidate(false);
            }
            if (oFamilyName && oStyle && oFamily && oPitch && oCharset)
            {
                SvxFontItem const item{
                    *oFamily, *oFamilyName, *oStyle, *oPitch, *oCharset, nWhich};
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_CHAR_FONTHEIGHT:
        case EE_CHAR_FONTHEIGHT_CJK:
        case EE_CHAR_FONTHEIGHT_CTL:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<sal_uInt32> oHeight;
            ::std::optional<sal_uInt16> oProp;
            ::std::optional<MapUnit> oMapUnit;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "height") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_INT32);
                    oHeight.emplace(value);
                }
                else if (strcmp(pEntry, "prop") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_INT16);
                    oProp.emplace(value);
                }
                else if (strcmp(pEntry, "propunit") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= ::std::underlying_type_t<MapUnit>(MapUnit::LAST));
                    oMapUnit.emplace(MapUnit(value));
                }
                else yvalidate(false);
            }
            if (oHeight && oProp && oMapUnit)
            {
                SvxFontHeightItem item{*oHeight, 100, nWhich};
                item.SetProp(*oProp, *oMapUnit);
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_CHAR_FONTWIDTH:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(/*0 <= value && */value <= SAL_MAX_INT16);
            SvxCharScaleWidthItem const item{sal_uInt16(value), TypedWhichId<SvxCharScaleWidthItem>(nWhich)};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_WEIGHT:
        case EE_CHAR_WEIGHT_CJK:
        case EE_CHAR_WEIGHT_CTL:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(WEIGHT_DONTKNOW <= value && value <= WEIGHT_BLACK);
            SvxWeightItem const item{FontWeight(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_UNDERLINE:
        case EE_CHAR_OVERLINE:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<FontLineStyle> oStyle;
            ::std::optional<Color> oColor;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "style") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(LINESTYLE_NONE <= value && value <= LINESTYLE_BOLDWAVE);
                    oStyle.emplace(FontLineStyle(value));
                }
                else if (strcmp(pEntry, "color") == 0)
                {
                    oColor.emplace(ColorTransparency, YrsReadInt(*rValue.value.map[i].value));
                }
                else yvalidate(false);
            }
            if (oStyle && oColor)
            {
                if (nWhich == EE_CHAR_UNDERLINE)
                {
                    SvxUnderlineItem item{*oStyle, EE_CHAR_UNDERLINE};
                    item.SetColor(*oColor);
                    rSet.Put(item);
                }
                else
                {
                    SvxOverlineItem item{*oStyle, EE_CHAR_OVERLINE};
                    item.SetColor(*oColor);
                    rSet.Put(item);
                }
            }
            else yvalidate(false);
            break;
        }
        case EE_CHAR_STRIKEOUT:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(STRIKEOUT_NONE <= value && value <= STRIKEOUT_X);
            SvxCrossedOutItem const item{FontStrikeout(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_ITALIC:
        case EE_CHAR_ITALIC_CJK:
        case EE_CHAR_ITALIC_CTL:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(ITALIC_NONE <= value && value <= ITALIC_DONTKNOW);
            SvxPostureItem const item{FontItalic(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_OUTLINE:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxContourItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_SHADOW:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxShadowedItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_ESCAPEMENT:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<short> oEsc;
            ::std::optional<sal_uInt8> oProp;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "esc") == 0)
                {
                    int64_t const value{static_cast<int64_t>(YrsReadInt(*rValue.value.map[i].value))};
                    yvalidate(SAL_MIN_INT16 <= value && value <= SAL_MAX_INT16);
                    oEsc.emplace(short(value));
                }
                else if (strcmp(pEntry, "prop") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT8);
                    oProp.emplace(sal_uInt8(value));
                }
                else yvalidate(false);
            }
            if (oEsc && oProp)
            {
                SvxEscapementItem const item{*oEsc, *oProp, EE_CHAR_ESCAPEMENT};
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_CHAR_PAIRKERNING:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxAutoKernItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_KERNING:
        {
            int64_t const value{static_cast<int64_t>(YrsReadInt(rValue))};
            yvalidate(SAL_MIN_INT16 <= value && value <= SAL_MAX_INT16);
            SvxKerningItem const item{short(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_WLM:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxWordLineModeItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_LANGUAGE:
        case EE_CHAR_LANGUAGE_CJK:
        case EE_CHAR_LANGUAGE_CTL:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
            SvxLanguageItem const item{LanguageType(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_EMPHASISMARK:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate((value & 0x300f) == value);
            SvxEmphasisMarkItem const item{FontEmphasisMark(value), TypedWhichId<SvxEmphasisMarkItem>(nWhich)};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_RELIEF:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(::std::underlying_type_t<FontRelief>(FontRelief::NONE) <= value && value <= ::std::underlying_type_t<FontRelief>(FontRelief::Engraved));
            SvxCharReliefItem const item{FontRelief(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_CHAR_CASEMAP:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(::std::underlying_type_t<SvxCaseMap>(SvxCaseMap::NotMapped) <= value && value < ::std::underlying_type_t<SvxCaseMap>(SvxCaseMap::End));
            SvxCaseMapItem const item{SvxCaseMap(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_WRITINGDIR:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(::std::underlying_type_t<SvxFrameDirection>(SvxFrameDirection::Horizontal_LR_TB) <= value && value < ::std::underlying_type_t<SvxFrameDirection>(SvxFrameDirection::Stacked));
            SvxFrameDirectionItem const item{SvxFrameDirection(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_AUTOWRITINGDIR:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxAutoFrameDirectionItem const item{ rValue.value.flag == Y_TRUE, nWhich };
            rSet.Put(item);
            break;
        }
        case EE_PARA_HANGINGPUNCTUATION:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxHangingPunctuationItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_FORBIDDENRULES:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxForbiddenRuleItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_ASIANCJKSPACING:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SvxScriptSpaceItem const item{rValue.value.flag == Y_TRUE, nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_HYPHENATE:
        case EE_PARA_HYPHENATE_NO_CAPS:
        case EE_PARA_HYPHENATE_NO_LAST_WORD:
        case EE_PARA_BULLETSTATE:
        {
            yvalidate(rValue.tag == Y_JSON_BOOL);
            SfxBoolItem const item{nWhich, rValue.value.flag == Y_TRUE};
            rSet.Put(item);
            break;
        }
        case EE_PARA_OUTLLEVEL:
        {
            int64_t const value{static_cast<int64_t>(YrsReadInt(rValue))};
            yvalidate(-1 <= value && value < SVX_MAX_NUM);
            SfxInt16Item const item{nWhich, sal_Int16(value)};
            rSet.Put(item);
            break;
        }
        case EE_PARA_LRSPACE:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<double> oFirstLineIndent;
            ::std::optional<sal_Int16> oFirstLineIndentUnit;
            ::std::optional<double> oLeft;
            ::std::optional<sal_Int16> oLeftUnit;
            ::std::optional<double> oRight;
            ::std::optional<sal_Int16> oRightUnit;
            ::std::optional<bool> oAutoFirst;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "first-line-offset") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM);
                    oFirstLineIndent.emplace(rValue.value.map[i].value->value.num);
                }
                else if (strcmp(pEntry, "first-line-offset-unit") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE);
                    oFirstLineIndentUnit.emplace(sal_Int16(value));
                }
                else if (strcmp(pEntry, "left-margin") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM);
                    oLeft.emplace(rValue.value.map[i].value->value.num);
                }
                else if (strcmp(pEntry, "left-margin-unit") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE);
                    oLeftUnit.emplace(sal_Int16(value));
                }
                else if (strcmp(pEntry, "right-margin") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM);
                    oRight.emplace(rValue.value.map[i].value->value.num);
                }
                else if (strcmp(pEntry, "right-margin-unit") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE);
                    oRightUnit.emplace(sal_Int16(value));
                }
                else if (strcmp(pEntry, "auto-first") == 0)
                {
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_BOOL);
                    oAutoFirst.emplace(rValue.value.map[i].value->value.flag == Y_TRUE);
                }
                else yvalidate(false);
            }
            if (oFirstLineIndent && oFirstLineIndentUnit && oAutoFirst.has_value()
                && oLeft && oLeftUnit && oRight && oRightUnit)
            {
                SvxLRSpaceItem item{{*oLeft, *oLeftUnit}, {*oRight, *oRightUnit}, {*oFirstLineIndent, *oFirstLineIndentUnit}, EE_PARA_LRSPACE};
                item.SetAutoFirst(*oAutoFirst);
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_PARA_ULSPACE:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<sal_uInt16> oUpper;
            ::std::optional<sal_uInt16> oLower;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "upper-margin") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oUpper.emplace(sal_uInt16(value));
                }
                else if (strcmp(pEntry, "lower-margin") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oLower.emplace(sal_uInt16(value));
                }
                else yvalidate(false);
            }
            if (oUpper && oLower)
            {
                SvxULSpaceItem const item{*oUpper, *oLower, EE_PARA_ULSPACE};
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_PARA_SBL:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<sal_uInt16> oLineSpaceFix;
            ::std::optional<sal_uInt16> oLineSpaceMin;
            ::std::optional<sal_uInt16> oInterLineSpaceProp;
            ::std::optional<sal_Int16> oInterLineSpaceFix;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "line-space-fix") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oLineSpaceFix.emplace(sal_uInt16(value));
                }
                else if (strcmp(pEntry, "line-space-min") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oLineSpaceMin.emplace(sal_uInt16(value));
                }
                else if (strcmp(pEntry, "inter-line-space-prop") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oInterLineSpaceProp.emplace(sal_uInt16(value));
                }
                else if (strcmp(pEntry, "inter-line-space-fix") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16);
                    oInterLineSpaceFix.emplace(sal_Int16(value));
                }
                else yvalidate(false);
            }
            SvxLineSpacingItem item{0, EE_PARA_SBL};
            if (oLineSpaceFix)
            {
                item.SetLineHeight(*oLineSpaceFix);
                item.SetLineSpaceRule(SvxLineSpaceRule::Fix);
                rSet.Put(item);
            }
            else if (oLineSpaceMin)
            {
                item.SetLineHeight(*oLineSpaceMin);
                rSet.Put(item);
            }
            else if (oInterLineSpaceProp)
            {
                item.SetPropLineSpace(*oInterLineSpaceProp);
                rSet.Put(item);
            }
            else if (oInterLineSpaceFix)
            {
                item.SetInterLineSpace(*oInterLineSpaceFix);
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_PARA_JUST:
        {
            auto const value{YrsReadInt(rValue)};
            SvxAdjustItem item{SvxAdjust(), nWhich};
            switch (value)
            {
                case uint64_t(SvxAdjust::Left):
                case uint64_t(SvxAdjust::Right):
                case uint64_t(SvxAdjust::Center):
                    item.SetAdjust(SvxAdjust(value));
                    break;
                case uint64_t(SvxAdjust::Block):
                    item.SetAdjust(SvxAdjust(value));
                    item.SetLastBlock(SvxAdjust::Left);
                    break;
                case 5:
                    item.SetAdjust(SvxAdjust::Block);
                    item.SetLastBlock(SvxAdjust::Center);
                    break;
                case 6:
                    item.SetAdjust(SvxAdjust::Block);
                    item.SetLastBlock(SvxAdjust::Block);
                    item.SetOneWord(SvxAdjust::Left);
                    break;
                case 7:
                    item.SetAdjust(SvxAdjust::Block);
                    item.SetLastBlock(SvxAdjust::Block);
                    item.SetOneWord(SvxAdjust::Block);
                    break;
                default:
                    abort();
            }
            rSet.Put(item);
            break;
        }
        case EE_PARA_TABS:
        {
            yvalidate(rValue.tag == Y_JSON_MAP);
            ::std::optional<sal_Int32> oDefault;
            ::std::optional<::std::vector<SvxTabStop>> oTabs;
            for (decltype(rValue.len) i = 0; i < rValue.len; ++i)
            {
                const char*const pEntry{rValue.value.map[i].key};
                if (strcmp(pEntry, "default-distance") == 0)
                {
                    auto const value{YrsReadInt(*rValue.value.map[i].value)};
                    yvalidate(/*0 <= value && */value <= SAL_MAX_INT32);
                    oDefault.emplace(sal_Int32(value));
                }
                else if (strcmp(pEntry, "tab-stops") == 0)
                {
                    oTabs.emplace();
                    yvalidate(rValue.value.map[i].value->tag == Y_JSON_ARR);
                    YOutput const& rArray{*rValue.value.map[i].value};
                    for (decltype(rArray.len) j = 0; j < rArray.len; ++j)
                    {
                        YOutput const& rMap{rArray.value.array[j]};
                        yvalidate(rMap.tag == Y_JSON_MAP);
                        ::std::optional<sal_Int32> oPos;
                        ::std::optional<SvxTabAdjust> oAdjust;
                        ::std::optional<sal_Unicode> oDecimal;
                        ::std::optional<sal_Unicode> oFill;
                        for (decltype(rMap.len) k = 0; k < rMap.len; ++k)
                        {
                            const char*const pE{rMap.value.map[k].key};
                            if (strcmp(pE, "pos") == 0)
                            {
                                auto const value{YrsReadInt(*rMap.value.map[k].value)};
                                yvalidate(/*0 <= value && */value <= SAL_MAX_INT32);
                                oPos.emplace(sal_Int32(value));
                            }
                            else if (strcmp(pE, "adjustment") == 0)
                            {
                                auto const value{YrsReadInt(*rMap.value.map[k].value)};
                                yvalidate(::std::underlying_type_t<SvxTabAdjust>(SvxTabAdjust::Left) <= value && value < ::std::underlying_type_t<SvxTabAdjust>(SvxTabAdjust::End));
                                oAdjust.emplace(SvxTabAdjust(value));
                            }
                            else if (strcmp(pE, "decimal") == 0)
                            {
                                auto const value{YrsReadInt(*rMap.value.map[k].value)};
                                yvalidate(/*0 <= value && */value < SAL_MAX_UINT16);
                                oDecimal.emplace(sal_Unicode(value));
                            }
                            else if (strcmp(pE, "fill") == 0)
                            {
                                auto const value{YrsReadInt(*rMap.value.map[k].value)};
                                yvalidate(/*0 <= value && */value < SAL_MAX_UINT16);
                                oFill.emplace(sal_Unicode(value));
                            }
                            else yvalidate(false);
                        }
                        if (oPos && oAdjust && oDecimal && oFill)
                        {
                            oTabs->emplace_back(*oPos, *oAdjust, *oDecimal, *oFill);
                        }
                        else yvalidate(false);
                    }
                }
            }
            if (oDefault && oTabs)
            {
                SvxTabStopItem item{nWhich};
                item.SetDefaultDistance(*oDefault);
                for (SvxTabStop const& it : *oTabs)
                {
                    item.Insert(it);
                }
                rSet.Put(item);
            }
            else yvalidate(false);
            break;
        }
        case EE_PARA_JUST_METHOD:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(::std::underlying_type_t<SvxCellJustifyMethod>(SvxCellJustifyMethod::Auto) <= value && value <= ::std::underlying_type_t<SvxCellJustifyMethod>(SvxCellJustifyMethod::Distribute));
            SvxJustifyMethodItem const item{SvxCellJustifyMethod(value), nWhich};
            rSet.Put(item);
            break;
        }
        case EE_PARA_VER_JUST:
        {
            auto const value{YrsReadInt(rValue)};
            yvalidate(::std::underlying_type_t<SvxCellVerJustify>(SvxCellVerJustify::Standard) <= value && value <= ::std::underlying_type_t<SvxCellVerJustify>(SvxCellVerJustify::Block));
            SvxVerJustifyItem const item{SvxCellVerJustify(value), nWhich};
            rSet.Put(item);
            break;
        }

        default:
            assert(false);
    }
}

// TODO: this could be a lot simpler if feature were a nested json-map like attr
template<typename T> void
YrsImplInsertFeature(ImpEditEngine & rIEE,
    EditPaM const& rPam,
    T const*const elements, uint32_t const len,
    ::std::function<::std::pair<char const*, YOutput const&>(T const&)> pGetAttr)
{
    ::std::optional<OUString> oFeature;
    ::std::optional<SvxURLFormat> oFormat;
    ::std::optional<OUString> oURL;
    ::std::optional<OUString> oRepresentation;
    ::std::optional<OUString> oTargetFrame;
    for (::std::remove_const_t<decltype(len)> k = 0; k < len; ++k)
    {
        T const& rAttr{elements[k]};
        auto [pKey, rValue]{pGetAttr(rAttr)};
        if (strcmp(pKey, "feature") == 0)
        {
            yvalidate(rValue.tag == Y_JSON_STR);
            oFeature.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8));
        }
        else if (strcmp(pKey, "url-format") == 0)
        {
            uint64_t const value{YrsReadInt(rValue)};
            switch (value)
            {
                case ::std::underlying_type_t<SvxURLFormat>(SvxURLFormat::AppDefault):
                case ::std::underlying_type_t<SvxURLFormat>(SvxURLFormat::Url):
                case ::std::underlying_type_t<SvxURLFormat>(SvxURLFormat::Repr):
                    oFormat.emplace(SvxURLFormat(value));
                    break;
                default:
                    yvalidate(false);
            }
        }
        else if (strcmp(pKey, "url-url") == 0)
        {
            yvalidate(rValue.tag == Y_JSON_STR);
            oURL.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8));
        }
        else if (strcmp(pKey, "url-representation") == 0)
        {
            yvalidate(rValue.tag == Y_JSON_STR);
            oRepresentation.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8));
        }
        else if (strcmp(pKey, "url-targetframe") == 0)
        {
            yvalidate(rValue.tag == Y_JSON_STR);
            oTargetFrame.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8));
        }
    }
    if (oFeature && *oFeature == "tab")
    {
        EditSelection const sel{rPam};
        rIEE.InsertTab(sel);
    }
    else if (oFeature && *oFeature == "line")
    {
        EditSelection const sel{rPam};
        rIEE.InsertLineBreak(sel);
    }
    else if (oFeature && *oFeature == "url")
    {
        if (oFormat && oURL && oRepresentation)
        {
            SvxURLField field{*oURL, *oRepresentation, *oFormat};
            if (oTargetFrame)
            {
                field.SetTargetFrame(*oTargetFrame);
            }
            SvxFieldItem const item{field, EE_FEATURE_FIELD};
            EditSelection const sel{rPam};
            rIEE.InsertField(sel, item);
        }
        else yvalidate(false);
    }
    else
    {
        yvalidate(false);
    }
}

} // namespace

void EditDoc::YrsSetStyle(sal_Int32 const index, ::std::u16string_view const rStyle)
{
    ::YrsSetStyle(m_pYrsSupplier, m_CommentId, *this, index, rStyle);
}

void EditDoc::YrsSetParaAttr(sal_Int32 const index, SfxPoolItem const& rItem)
{
    ::YrsSetParaAttr(m_pYrsSupplier, m_CommentId, *this, index, rItem);
}

void EditDoc::YrsWriteEEState()
{
    assert(m_pYrsSupplier);

    YrsWrite const yw{GetYrsWrite(m_pYrsSupplier, m_CommentId)};
    assert(yw.pTxn && yw.pProps && yw.pText);

    YInput const vertical{yinput_bool(mbIsVertical ? Y_TRUE : Y_FALSE)};
    ymap_insert(yw.pProps, yw.pTxn, "is-vertical", &vertical);
    YInput const rotation{yinput_long(static_cast<int64_t>(mnRotation))};
    ymap_insert(yw.pProps, yw.pTxn, "rotation", &rotation);
    YInput const deftab{yinput_long(static_cast<int64_t>(mnDefTab))};
    ymap_insert(yw.pProps, yw.pTxn, "def-tab", &deftab);

    for (::std::unique_ptr<ContentNode> const& rpNode : maContents)
    {
        auto const start{ytext_len(yw.pText, yw.pTxn)};
        sal_Int32 pos{0};
        for (sal_Int32 i = rpNode->GetString().indexOf(CH_FEATURE, pos);
              i != -1; i = rpNode->GetString().indexOf(CH_FEATURE, pos))
        {
            if (i != pos)
            {
                OString const content{OUStringToOString(rpNode->GetString().subView(pos, i - pos), RTL_TEXTENCODING_UTF8)};
                ytext_insert(yw.pText, yw.pTxn, start + pos, content.getStr(), nullptr);
            }
            EditCharAttrib const*const pAttrib{rpNode->GetCharAttribs().FindFeature(i)};
            assert(pAttrib);
            // this will insert the CH_FEATURE too
            YrsInsertFeature(m_pYrsSupplier, m_CommentId, *this, GetPos(rpNode.get()), pAttrib);
            pos = i+1;
        }
        if (pos != rpNode->GetString().getLength())
        {
            OString const content{OUStringToOString(rpNode->GetString().subView(pos, rpNode->GetString().getLength() - pos), RTL_TEXTENCODING_UTF8)};
            ytext_insert(yw.pText, yw.pTxn, start + pos, content.getStr(), nullptr);
        }
        for (::std::unique_ptr<EditCharAttrib> const& rpAttr : rpNode->GetCharAttribs().GetAttribs())
        {
            if (!rpAttr->IsFeature())
            {
                YrsInsertAttribImpl(yw, start, rpAttr.get());
            }
        }
        char const para[]{ CH_PARA, '\0' };
        ContentAttribs const& rParaAttribs{rpNode->GetContentAttribs()};
        OString const styleName{OUStringToOString(rpNode->GetStyleSheet()->GetName(), RTL_TEXTENCODING_UTF8)};
        YInput const style{yinput_string(styleName.getStr())};
        YInput attrArray[]{ style };
        char const*const attrNames[]{ "para-style" };
        YInput const attrs{yinput_json_map(const_cast<char**>(attrNames), attrArray, 1)};
        auto const end{ytext_len(yw.pText, yw.pTxn)};
        ytext_insert(yw.pText, yw.pTxn, end, para, &attrs);
        for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem())
        {
            YrsInsertAttribImplImpl(yw, *it.GetCurItem(), end, 1);
        }
    }
}

void EditDoc::YrsReadEEState(YTransaction *const pTxn, ImpEditEngine & rIEE)
{
    assert(m_pYrsSupplier);
    YrsWrite const yw{GetYrsWrite(m_pYrsSupplier, m_CommentId, pTxn)};
    assert(yw.pTxn && yw.pProps && yw.pText);

    ::std::unique_ptr<YOutput, YOutputDeleter> const pVertical{ymap_get(yw.pProps, yw.pTxn, "is-vertical")};
    yvalidate(!pVertical || pVertical->tag == Y_JSON_BOOL);
    if (pVertical && pVertical->tag == Y_JSON_BOOL)
    {
        SetVertical(pVertical->value.flag == Y_TRUE);
    }
    ::std::unique_ptr<YOutput, YOutputDeleter> const pRotation{ymap_get(yw.pProps, yw.pTxn, "rotation")};
    yvalidate(pRotation);
    {
        auto const value{YrsReadInt(*pRotation)};
        switch (value)
        {
            case int64_t(TextRotation::NONE):
            case int64_t(TextRotation::TOPTOBOTTOM):
            case int64_t(TextRotation::BOTTOMTOTOP):
                SetRotation(TextRotation(value));
                break;
            default:
                yvalidate(false);
        }
    }
    ::std::unique_ptr<YOutput, YOutputDeleter> const pDefTab{ymap_get(yw.pProps, yw.pTxn, "def-tab")};
    yvalidate(pDefTab);
    {
        auto const value{YrsReadInt(*pDefTab)};
        yvalidate(/*0 <= value && */value <= SAL_MAX_INT16);
        SetDefTab(value);
    }

    assert(maContents.size() == 1);
#if 0
    // there is a getImpl().GetEditSelection() too, who knows what else... try to reuse existing node?
    rIEE.GetParaPortions().Reset();
    maContents.clear();
    // the last paragraph cannot be removed, as cursors and a11y need it
    rIEE.RemoveParagraph(0); // remove pre-existing one from InitDoc()
#endif
    uint32_t chunks{0};
    YChunk *const pChunks{ytext_chunks(yw.pText, yw.pTxn, &chunks)};
    sal_Int32 nodes{0};
    ContentNode * pNode{nullptr};
    for (decltype(chunks) i = 0; i < chunks; ++i)
    {
        decltype(nodes) const nodeStart{nodes};
        sal_Int32 const posStart{pNode ? pNode->Len() : 0};

        yvalidate(pChunks[i].data.tag == Y_JSON_STR);
        OString const str(pChunks[i].data.value.str);
        sal_Int32 strStart{0};

        for (sal_Int32 j = 0; j < str.getLength(); ++j)
        {
            if (!pNode)
            {
                //pNode = new ContentNode(GetItemPool());
                //rIEE.InsertContent(pNode, nodes); // does not set DefFont?
                pNode = rIEE.ImpFastInsertParagraph(nodes).GetNode();
//                rIEE.GetParaPortions().Insert(nodes, ::std::make_unique<ParaPortion>(pNode));
//does not call ParagraphInserted so no a11y event                Insert(nodes, ::std::unique_ptr<ContentNode>(pNode));
                // calling UpdateSelections() is pointless, it only handles deletes
            }
            if (str[j] == CH_PARA)
            {
                if (j != 0)
                {
                    OString const portion{str.copy(strStart, j - strStart)};
                    pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8));
                }
                for (decltype(pChunks[i].fmt_len) k = 0; k < pChunks[i].fmt_len; ++k)
                {
                    if (strcmp(pChunks[i].fmt[k].key, "para-style") == 0)
                    {
                        yvalidate(pChunks[i].fmt[k].value->tag == Y_JSON_STR);
                        OUString const style{OStringToOUString(pChunks[i].fmt[k].value->value.str, RTL_TEXTENCODING_UTF8)};
                        SfxStyleSheet *const pStyle{dynamic_cast<SfxStyleSheet *>(
                            rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para))};
                        if (!pStyle) { abort(); }
                        pNode->SetStyleSheet(pStyle);
                    }
                }
                pNode = nullptr;
                ++nodes;
                strStart = j+1;
            }
            else if (str[j] == CH_FEATURE)
            {
                if (j != strStart)
                {
                    OString const portion{str.copy(strStart, j - strStart)};
                    pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8));
                }
                auto GetAttr = [](YMapEntry const& rEntry) -> ::std::pair<char const*, YOutput const&>
                    { return {rEntry.key, *rEntry.value}; };
                EditPaM const pam{pNode, pNode->Len()};
                YrsImplInsertFeature<YMapEntry>(rIEE, pam, pChunks[i].fmt, pChunks[i].fmt_len, GetAttr);
                strStart = j+1;
            }
        }
        if (strStart != str.getLength())
        {
            OString const portion{str.copy(strStart, str.getLength() - strStart)};
            pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8));
        }
        assert((pNode == nullptr) == (str[str.getLength()-1] == CH_PARA));
        ContentNode *const pEndNode{pNode ? pNode : maContents[nodes-1].get()};
        EditSelection const sel{EditPaM(maContents[nodeStart].get(), posStart), EditPaM(pEndNode, pEndNode->Len())};
        SfxItemSet set{rIEE.GetEmptyItemSet()};
        for (decltype(pChunks[i].fmt_len) j = 0; j < pChunks[i].fmt_len; ++j)
        {
            YrsImplInsertAttr(set, nullptr, pChunks[i].fmt[j].key, *pChunks[i].fmt[j].value);
        }
        if (set.Count())
        {
            rIEE.SetAttribs(sel, set);
        }
    }
    ychunks_destroy(pChunks, chunks);
    rIEE.RemoveParagraph(nodes); // remove pre-existing one from InitDoc()
}

bool EditDoc::YrsWriteEECursor(YTransaction *const pTxn, Branch const& rArray,
        YOutput const*const pCurrent, EditSelection const& rSelection)
{
    EditSelection sel{rSelection};
    sel.Adjust(*this); // unfortunately min/max naming may be misleading...
    YrsWrite const yw{GetYrsWrite(m_pYrsSupplier, m_CommentId)};
    uint32_t start{0};
    for (auto paras{GetPos(sel.Min().GetNode())}; paras != 0; --paras)
    {
        start += GetObject(paras-1)->Len() + 1;
    }
    uint32_t end{start};
    start += sel.Min().GetIndex();
    if (sel.HasRange())
    {
        auto const nStartNode{GetPos(sel.Min().GetNode())};
        for (auto paras{GetPos(sel.Max().GetNode()) - nStartNode};
            paras != 0; --paras)
        {
            end += GetObject(nStartNode + paras-1)->Len() + 1;
        }
        end += sel.Max().GetIndex();
    }
    else
    {
        end = start;
    }
    if (pCurrent != nullptr && pCurrent->tag == Y_ARRAY
        && yarray_len(pCurrent->value.y_type) == 2
        // could do without the comment id? no, empty-text leaves pBranch null!
        && yarray_get(pCurrent->value.y_type, pTxn, 0)->tag == Y_JSON_STR
        && strcmp(yarray_get(pCurrent->value.y_type, pTxn, 0)->value.str, m_CommentId.getStr()) == 0)
    {
        ::std::unique_ptr<YOutput, YOutputDeleter> const pWeak{yarray_get(pCurrent->value.y_type, pTxn, 1)};
        yvalidate(pWeak->tag == Y_WEAK_LINK);
        Branch * pBranch{nullptr};
        uint32_t oldStart{SAL_MAX_UINT32};
        uint32_t oldEnd{SAL_MAX_UINT32};
        yweak_read(pWeak->value.y_type, pTxn, &pBranch, &oldStart, &oldEnd);
        assert(oldStart != SAL_MAX_UINT32 && oldEnd != SAL_MAX_UINT32);
        if (oldStart == start && oldEnd == end)
        {
            return false;
        }
    }

    Weak const*const pWeak{ytext_quote(yw.pText, pTxn, &start, &end, Y_FALSE, Y_TRUE)};
    ::std::vector<YInput> positions;
    positions.push_back(yinput_string(m_CommentId.getStr()));
    positions.push_back(yinput_weak(pWeak));
    YInput const input{yinput_yarray(positions.data(), 2)};
    yarray_remove_range(&rArray, pTxn, 1, 1);
    yarray_insert_range(&rArray, pTxn, 1, &input, 1);
//    yweak_destroy(pWeak); // NO! UAF
    return true;
}

::std::optional<EditSelection> EditDoc::YrsReadEECursor(
    ::std::pair<int64_t, int64_t> const i_point,
    ::std::optional<::std::pair<int64_t, int64_t>> const i_oMark)
{
    ::std::optional<EditPaM> oMark;
#if ENABLE_YRS_WEAK
    auto start{i_point.first}; // returned from yweak_read(), need to convert
    ContentNode * pNode{nullptr};
    for (decltype(Count()) i = 0; i < Count(); ++i)
    {
        pNode = GetObject(i);
        if (start <= o3tl::make_unsigned(pNode->Len()))
        {
            break;
        }
        start -= pNode->Len() + 1;
    }
    yvalidate(start <= o3tl::make_unsigned(pNode->Len()));
    EditPaM point{pNode, static_cast<sal_Int32>(start)};
    if (i_oMark)
    {
        auto end{i_oMark->first};
        for (decltype(Count()) i = 0; i < Count(); ++i)
        {
            pNode = GetObject(i);
            if (end <= o3tl::make_unsigned(pNode->Len()))
            {
                break;
            }
            end -= pNode->Len() + 1;
        }
        yvalidate(end <= o3tl::make_unsigned(pNode->Len()));
        oMark.emplace(pNode, static_cast<sal_Int32>(end));
    }
#else
    if (i_oMark)
    {
//        yvalidate(i_oMark->first < Count());
        if (Count() <= i_oMark->first)
        {
            return {};
        }
        ContentNode & rNode{*GetObject(i_oMark->first)};
//        yvalidate(i_oMark->second <= o3tl::make_unsigned(rNode.Len()));
        if (o3tl::make_unsigned(rNode.Len()) < i_oMark->second)
        {
            return {};
        }
        oMark.emplace(&rNode, static_cast<sal_Int32>(i_oMark->second));
    }

    // the problem with using ints here is that multiple local edits may
    // occur before the peer sends the updated cursor for the first edit,
    // and then its position may be invalid; work around this here by
    // ignoring obviously invalid cursors.
//    yvalidate(i_point.first < Count());
    if (Count() <= i_point.first)
    {
        return {};
    }
    ContentNode & rNode{*GetObject(i_point.first)};
//    yvalidate(i_point.second <= o3tl::make_unsigned(rNode.Len()));
    if (o3tl::make_unsigned(rNode.Len()) < i_point.second)
    {
        return {};
    }
    EditPaM point{&rNode, static_cast<sal_Int32>(i_point.second)};
#endif

    return EditSelection{point, oMark ? *oMark : point};
}

static bool UpdatePosDelete(EditDoc & rDoc, EditPaM & rPos, ESelection const& rDeleted)
{
    auto const nPosNode{rDoc.GetPos(rPos.GetNode())};
    if (rDeleted.start.nPara == nPosNode)
    {
        if (rDeleted.start.nIndex < rPos.GetIndex())
        {
            if (rDeleted.start.nPara == rDeleted.end.nPara && rDeleted.end.nIndex < rPos.GetIndex())
            {
                rPos.SetIndex(rPos.GetIndex() - (rDeleted.end.nIndex - rDeleted.start.nIndex));
            }
            else
            {
                rPos.SetIndex(rDeleted.start.nIndex);
            }
            return true;
        }
    }
    else if (rDeleted.start.nPara < nPosNode && nPosNode <= rDeleted.end.nPara)
    {
        if (nPosNode == rDeleted.end.nPara && rDeleted.end.nIndex < rPos.GetIndex())
        {
            rPos.SetIndex(rPos.GetIndex() - (rDeleted.end.nIndex - rDeleted.start.nIndex));
        }
        else
        {
            rPos.SetIndex(rDeleted.start.nIndex);
        }
        rPos.SetNode(rDoc.GetObject(rDeleted.start.nPara));
        return true;
    }
#if 0
    else if (rDeleted.end.nPara < nPosNode)
    {
        rPos.SetNode(rDoc.GetObject(nPosNode - (rDeleted.end.nPara - rDeleted.start.nPara)));
        return true;
    }
#endif
    return false;

}

// TODO this is now sort of duplicated as UpdateSelectionsDelete, but
// consolidating that would probably require replacing EditSelection with
// ESelection in EditView
static void YrsAdjustCursorsDel(ImpEditEngine & rIEE, EditDoc & rDoc,
    sal_Int32 const startNode, sal_Int32 const startPos,
    sal_Int32 const endNode, sal_Int32 const endPos)
{
    ESelection const deleted{startNode, startPos, endNode, endPos};
    for (EditView *const pView : rIEE.GetEditViews())
    {
        EditSelection sel{pView->getImpl().GetEditSelection()};
        bool isChanged{false};
        isChanged |= UpdatePosDelete(rDoc, sel.Min(), deleted);
        isChanged |= UpdatePosDelete(rDoc, sel.Max(), deleted);
        if (isChanged)
        {
            pView->getImpl().SetEditSelection(sel);
        }
    }
}

void EditDoc::YrsApplyEEDelta(YTransaction *const /*pTxn*/, YTextEvent const*const pEvent, ImpEditEngine & rIEE)
{
    uint32_t lenC{0};
    YDeltaOut *const pChange{ytext_event_delta(pEvent, &lenC)};

    sal_Int32 node{0};
    sal_Int32 pos{0};

    for (decltype(lenC) i = 0; i < lenC; ++i)
    {
        switch (pChange[i].tag)
        {
            case Y_EVENT_CHANGE_ADD:
            {
                decltype(node) const nodeStart{node};
                decltype(pos) const posStart{pos};

                SfxStyleSheet * pStyle{nullptr};
                SfxItemSet set{rIEE.GetEmptyItemSet()};
                for (decltype(pChange[i].attributes_len) j = 0; j < pChange[i].attributes_len; ++j)
                {
                    YrsImplInsertAttr(set, nullptr, pChange[i].attributes[j].key, pChange[i].attributes[j].value);
                    if (strcmp(pChange[i].attributes[j].key, "para-style") == 0)
                    {
                        yvalidate(pChange[i].attributes[j].value.tag == Y_JSON_STR);
                        OUString const style{OStringToOUString(pChange[i].attributes[j].value.value.str, RTL_TEXTENCODING_UTF8)};
                        pStyle = dynamic_cast<SfxStyleSheet *>(
                            rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para));
                        if (!pStyle) { abort(); }
                    }
                }

                for (decltype(pChange[i].len) j = 0; j < pChange[i].len; ++j)
                {
                    switch (pChange[i].insert[j].tag)
                    {
                        case Y_JSON_STR:
                        {
                            OUString const str{OStringToOUString(::std::string_view(pChange[i].insert[j].value.str), RTL_TEXTENCODING_UTF8)};
                            if (str.getLength() == 1 && str[0] == CH_FEATURE)
                            {
                                auto GetAttr = [](YDeltaAttr const& rEntry) -> ::std::pair<char const*, YOutput const&>
                                    { return {rEntry.key, rEntry.value}; };
                                EditPaM const pam{maContents[node].get(), pos};
                                YrsImplInsertFeature<YDeltaAttr>(rIEE, pam, pChange[i].attributes, pChange[i].attributes_len, GetAttr);
                                ++pos;
                                break;
                            }
                            sal_Int32 index{0};
                            sal_Int32 iPara{str.indexOf(CH_PARA)};
                            if (iPara != -1)
                            {
                                if (index != iPara)
                                {
                                    EditSelection const sel{EditPaM{maContents[node].get(), pos}};
                                    rIEE.InsertText(sel, str.copy(index, iPara));
                                }
                                rIEE.SplitContent(node, pos + iPara);
                                rIEE.SetStyleSheet(node, pStyle);
                                index = iPara + 1;
                                pos = 0;
                                ++node;
                                iPara = str.indexOf(CH_PARA, index);
                            }
                            while (iPara != -1)
                            {
//                                EditSelection const sel{EditPaM{maContents[node].get(), pos}};
//                                rIEE.InsertText(sel, str);
                                rIEE.InsertParagraph(node, str.copy(index, iPara - index));
                                rIEE.SetStyleSheet(node, pStyle);
                                index = iPara + 1;
                                ++node;
                                iPara = str.indexOf(CH_PARA, index);
                            }
                            assert(iPara == -1);
                            if (index != str.getLength())
                            {
                                EditSelection const sel{EditPaM{maContents[node].get(), pos}};
                                rIEE.InsertText(sel, str.copy(index, str.getLength() - index));
                                pos += str.getLength() - index;
                            }
                            break;
                        }
                        default:
                            assert(false);
                    }
                }

                EditPaM const start{maContents[nodeStart].get(), posStart};
                EditPaM const end{maContents[node].get(), pos};
                EditSelection const sel{start, end};
                if (set.Count())
                {
                    rIEE.SetAttribs(sel, set);
                }
            }
            break;
            case Y_EVENT_CHANGE_DELETE:
            case Y_EVENT_CHANGE_RETAIN:
            {
                decltype(node) const nodeStart{node};
                decltype(pos) const posStart{pos};

                // len should be UTF16 via Y_OFFSET_UTF16
                sal_Int32 len{static_cast<sal_Int32>(pChange[i].len)};
                while (0 < len)
                {
                    yvalidate(o3tl::make_unsigned(node) < maContents.size());
                    ContentNode & rNode{*maContents[node]};
                    if (pos + len <= rNode.Len())
                    {
                        pos += len;
                        len = 0;
                    }
                    else
                    {
                        len -= rNode.Len() - pos + 1;
                        pos = 0;
                        ++node;
                    }
                }
                ::std::optional<EditSelection> oSel;
                // setting attribute on node special case - there may be
                // EE_PARA_* obviously and possibly also EE_CHAR_* but
                // unclear how those would be inserted - and a "para-style"
                if (pos == 0 && pChange[i].len == 1 && pChange[i].tag == Y_EVENT_CHANGE_RETAIN)
                {
                    assert(o3tl::make_unsigned(node) <= maContents.size());
                    EditPaM const start{maContents[node - 1].get(), 0};
                    oSel.emplace(start);
                }
                else
                {
                    if (pos == 0 && static_cast<size_t>(node) == maContents.size())
                    {   // adjust past-the-end position (formatting change)
                        --node;
                        pos = maContents[node]->Len();
                    }
                    assert(o3tl::make_unsigned(node) < maContents.size() && pos <= maContents[node]->Len());
                    EditPaM const start{maContents[nodeStart].get(), posStart};
                    EditPaM const end{maContents[node].get(), pos};
                    oSel.emplace(start, end);
                }
                if (pChange[i].tag == Y_EVENT_CHANGE_DELETE)
                {
                    YrsAdjustCursorsDel(rIEE, *this, nodeStart, posStart, node, pos);
                    ESelection const deleted{rIEE.CreateESel(*oSel)};
                    rIEE.UpdateSelectionsDelete(deleted);
                    rIEE.DeleteSelected(*oSel);
                    node = nodeStart;
                    pos = posStart;
                }
                else if (0 < pChange[i].attributes_len)
                {
                    assert(pChange[i].tag == Y_EVENT_CHANGE_RETAIN);
                    SfxItemSet set{rIEE.GetEmptyItemSet()};
                    ::std::vector<sal_uInt16> removed;
                    for (decltype(pChange[i].attributes_len) j = 0; j < pChange[i].attributes_len; ++j)
                    {
                        if (pos == 0 && pChange[i].len == 1
                            && strcmp(pChange[i].attributes[j].key, "para-style") == 0)
                        {
                            yvalidate(pChange[i].attributes[j].value.tag == Y_JSON_STR);
                            OUString const style{OStringToOUString(pChange[i].attributes[j].value.value.str, RTL_TEXTENCODING_UTF8)};
                            SfxStyleSheet *const pStyle{dynamic_cast<SfxStyleSheet *>(
                                rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para))};
                            if (!pStyle) { abort(); }
                            rIEE.SetStyleSheet(node - 1, pStyle);
                        }
                        else
                        {
                            YrsImplInsertAttr(set, &removed, pChange[i].attributes[j].key, pChange[i].attributes[j].value);
                        }
                    }
                    if (set.Count())
                    {
                        rIEE.SetAttribs(*oSel, set);
                    }
                    for (auto const nWhich : removed)
                    {
                        rIEE.RemoveCharAttribs(*oSel, EERemoveParaAttribsMode::RemoveAll, nWhich);
                    }
                }
            }
            break;
            default:
                assert(false);
        }
    }

    ytext_delta_destroy(pChange, lenC);
}

void EditDoc::SetYrsCommentId(IYrsTransactionSupplier *const pYrsSupplier, OString const& rId)
{
    assert(!m_pYrsSupplier);
    m_pYrsSupplier = pYrsSupplier;
    m_CommentId = rId;
}

OString EditDoc::GetYrsCommentId() const
{
    return m_CommentId;
}

void EditView::SetYrsCommentId(IYrsTransactionSupplier *const pYrsSupplier, OString const& rId)
{
    getEditEngine().GetEditDoc().SetYrsCommentId(pYrsSupplier, rId);
}

OString EditView::GetYrsCommentId() const
{
    return getEditEngine().GetEditDoc().GetYrsCommentId();
}

void EditView::YrsWriteEEState()
{
    return getEditEngine().GetEditDoc().YrsWriteEEState();
}

void EditView::YrsReadEEState(YTransaction *const pTxn)
{
    return getEditEngine().GetEditDoc().YrsReadEEState(pTxn, getImpEditEngine());
}

void EditView::YrsApplyEEDelta(YTransaction *const pTxn, YTextEvent const*const pEvent)
{
    return getEditEngine().GetEditDoc().YrsApplyEEDelta(pTxn, pEvent, getImpEditEngine());
}
#endif

EditDoc::EditDoc( SfxItemPool* pPool ) :
    mnLastCache(0),
    mpItemPool(pPool ? pPool : new EditEngineItemPool()),
    mnDefTab(DEFTAB),
    mbIsVertical(false),
    mnRotation(TextRotation::NONE),
    mbIsFixedCellHeight(false),
    mbModified(false),
    mbDisableAttributeExpanding(false)
{
#if ENABLE_YRS
    SAL_INFO("editeng.yrs", "YRS +EditDoc");
#endif
    // Don't create an empty node, Clear() will be called in EditEngine-CTOR
};

EditDoc::~EditDoc()
{
#if ENABLE_YRS
    SAL_INFO("editeng.yrs", "YRS -EditDoc");
#endif
    maContents.clear();
}

// not sure which of the members make sense to sync - if it's only a cache for
// some value elsewhere in the model then probably not?
void EditDoc::SetVertical(bool const bVertical)
{
    mbIsVertical = bVertical;
#if ENABLE_YRS
    YrsSetVertical(m_pYrsSupplier, m_CommentId, mbIsVertical);
#endif
}

void EditDoc::SetRotation(TextRotation const nRotation)
{
    mnRotation = nRotation;
#if ENABLE_YRS
    YrsSetRotation(m_pYrsSupplier, m_CommentId, mnRotation);
#endif
}

void EditDoc::SetDefTab(sal_uInt16 const nTab)
{
    mnDefTab = nTab ? nTab : DEFTAB;
#if ENABLE_YRS
    YrsSetDefTab(m_pYrsSupplier, m_CommentId, mnDefTab);
#endif
}

void CreateFont( SvxFont& rFont, const SfxItemSet& rSet, bool bSearchInParent, SvtScriptType nScriptType )
{
    vcl::Font aPrevFont( rFont );
    rFont.SetAlignment( ALIGN_BASELINE );

    sal_uInt16 nWhich_FontInfo = GetScriptItemId( EE_CHAR_FONTINFO, nScriptType );
    sal_uInt16 nWhich_Language = GetScriptItemId( EE_CHAR_LANGUAGE, nScriptType );
    sal_uInt16 nWhich_FontHeight = GetScriptItemId( EE_CHAR_FONTHEIGHT, nScriptType );
    sal_uInt16 nWhich_Weight = GetScriptItemId( EE_CHAR_WEIGHT, nScriptType );
    sal_uInt16 nWhich_Italic = GetScriptItemId( EE_CHAR_ITALIC, nScriptType );

    if ( bSearchInParent || ( rSet.GetItemState( nWhich_FontInfo ) == SfxItemState::SET ) )
    {
        const SvxFontItem& rFontItem = static_cast<const SvxFontItem&>(rSet.Get( nWhich_FontInfo ));
        rFont.SetFamilyName( rFontItem.GetFamilyName() );
        rFont.SetFamily( rFontItem.GetFamily() );
        rFont.SetPitch( rFontItem.GetPitch() );
        rFont.SetCharSet( rFontItem.GetCharSet() );
    }
    if ( bSearchInParent || ( rSet.GetItemState( nWhich_Language ) == SfxItemState::SET ) )
        rFont.SetLanguage( static_cast<const SvxLanguageItem&>(rSet.Get( nWhich_Language )).GetLanguage() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_COLOR ) == SfxItemState::SET ) )
        rFont.SetColor( rSet.Get( EE_CHAR_COLOR ).GetValue() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_BKGCOLOR ) == SfxItemState::SET ) )
    {
        auto& aColor = rSet.Get( EE_CHAR_BKGCOLOR ).GetValue();
        rFont.SetTransparent(aColor.IsTransparent());
        rFont.SetFillColor(aColor);
    }
    if ( bSearchInParent || ( rSet.GetItemState( nWhich_FontHeight ) == SfxItemState::SET ) )
        rFont.SetFontSize( Size( rFont.GetFontSize().Width(), static_cast<const SvxFontHeightItem&>(rSet.Get( nWhich_FontHeight ) ).GetHeight() ) );
    if ( bSearchInParent || ( rSet.GetItemState( nWhich_Weight ) == SfxItemState::SET ) )
        rFont.SetWeight( static_cast<const SvxWeightItem&>(rSet.Get( nWhich_Weight )).GetWeight() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_UNDERLINE ) == SfxItemState::SET ) )
        rFont.SetUnderline( rSet.Get( EE_CHAR_UNDERLINE ).GetLineStyle() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_OVERLINE ) == SfxItemState::SET ) )
        rFont.SetOverline( rSet.Get( EE_CHAR_OVERLINE ).GetLineStyle() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_STRIKEOUT ) == SfxItemState::SET ) )
        rFont.SetStrikeout( rSet.Get( EE_CHAR_STRIKEOUT ).GetStrikeout() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_CASEMAP ) == SfxItemState::SET ) )
        rFont.SetCaseMap( rSet.Get( EE_CHAR_CASEMAP ).GetCaseMap() );
    if ( bSearchInParent || ( rSet.GetItemState( nWhich_Italic ) == SfxItemState::SET ) )
        rFont.SetItalic( static_cast<const SvxPostureItem&>(rSet.Get( nWhich_Italic )).GetPosture() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_OUTLINE ) == SfxItemState::SET ) )
        rFont.SetOutline( rSet.Get( EE_CHAR_OUTLINE ).GetValue() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_SHADOW ) == SfxItemState::SET ) )
        rFont.SetShadow( rSet.Get( EE_CHAR_SHADOW ).GetValue() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_ESCAPEMENT ) == SfxItemState::SET ) )
    {
        const SvxEscapementItem& rEsc = rSet.Get( EE_CHAR_ESCAPEMENT );

        sal_uInt16 const nProp = rEsc.GetProportionalHeight();
        rFont.SetPropr( static_cast<sal_uInt8>(nProp) );

        short nEsc = rEsc.GetEsc();
        rFont.SetNonAutoEscapement( nEsc );
    }
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_PAIRKERNING ) == SfxItemState::SET ) )
        rFont.SetKerning( rSet.Get( EE_CHAR_PAIRKERNING ).GetValue() ? FontKerning::FontSpecific : FontKerning::NONE );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_KERNING ) == SfxItemState::SET ) )
        rFont.SetFixKerning( rSet.Get( EE_CHAR_KERNING ).GetValue() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_WLM ) == SfxItemState::SET ) )
        rFont.SetWordLineMode( rSet.Get( EE_CHAR_WLM ).GetValue() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_EMPHASISMARK ) == SfxItemState::SET ) )
        rFont.SetEmphasisMark( rSet.Get( EE_CHAR_EMPHASISMARK ).GetEmphasisMark() );
    if ( bSearchInParent || ( rSet.GetItemState( EE_CHAR_RELIEF ) == SfxItemState::SET ) )
        rFont.SetRelief( rSet.Get( EE_CHAR_RELIEF ).GetValue() );

    // Operator == compares the individual members of the font if the impl pointer is
    // not equal. If all members are the same, this assignment makes
    // sure that both also point to the same internal instance of the font.
    // To avoid this assignment, you would need to check in
    // every if statement above whether or not the new value differs from the
    // old value before making an assignment.
    if ( rFont == aPrevFont  )
        rFont = aPrevFont;  // => The same ImpPointer for IsSameInstance
}

void EditDoc::CreateDefFont( bool bUseStyles )
{
    SfxItemSet aTmpSet(SfxItemSet::makeFixedSfxItemSet<EE_PARA_START, EE_CHAR_END>(GetItemPool()));
    // maDefFont depends only on items and flags, no need to sync
    CreateFont(maDefFont, aTmpSet);
    maDefFont.SetVertical( IsEffectivelyVertical() );
    maDefFont.SetOrientation( Degree10(IsEffectivelyVertical() ? (IsTopToBottom() ? 2700 : 900) : 0) );

    for (std::unique_ptr<ContentNode>& pNode : maContents)
    {
        pNode->GetCharAttribs().GetDefFont() = maDefFont;
        if (bUseStyles)
            pNode->CreateDefFont();
    }
}

bool EditDoc::IsEffectivelyVertical() const
{
    return (mbIsVertical && mnRotation == TextRotation::NONE) ||
        (!mbIsVertical && mnRotation != TextRotation::NONE);
}

bool EditDoc::IsTopToBottom() const
{
    return (mbIsVertical && mnRotation == TextRotation::NONE) ||
        (!mbIsVertical && mnRotation == TextRotation::TOPTOBOTTOM);
}

bool EditDoc::GetVertical() const
{
    return mbIsVertical;
}

sal_Int32 EditDoc::GetPos(const ContentNode* pContentNode) const
{
    return FastGetPos(maContents, pContentNode, mnLastCache);
}

const ContentNode* EditDoc::GetObject(sal_Int32 nPos) const
{
    return 0 <= nPos && o3tl::make_unsigned(nPos) < maContents.size() ? maContents[nPos].get() : nullptr;
}

ContentNode* EditDoc::GetObject(sal_Int32 nPos)
{
    return 0 <= nPos && o3tl::make_unsigned(nPos) < maContents.size() ? maContents[nPos].get() : nullptr;
}

void EditDoc::Insert(sal_Int32 nPos, std::unique_ptr<ContentNode> pNode)
{
    if (nPos < 0 || nPos == SAL_MAX_INT32)
    {
        SAL_WARN( "editeng", "EditDoc::Insert - overflow pos " << nPos);
        return;
    }
    maContents.insert(maContents.begin()+nPos, std::move(pNode));
#if ENABLE_YRS
    YrsAddPara(m_pYrsSupplier, m_CommentId, *this, nPos);
#endif
}

void EditDoc::Remove(sal_Int32 nPos)
{
    if (nPos < 0 || o3tl::make_unsigned(nPos) >= maContents.size())
    {
        SAL_WARN( "editeng", "EditDoc::Remove - out of bounds pos " << nPos);
        return;
    }
#if ENABLE_YRS
    YrsRemovePara(m_pYrsSupplier, m_CommentId, *this, nPos);
#endif
    maContents.erase(maContents.begin() + nPos);
}

std::unique_ptr<ContentNode> EditDoc::Release(sal_Int32 nPos)
{
    if (nPos < 0 || o3tl::make_unsigned(nPos) >= maContents.size())
    {
        SAL_WARN( "editeng", "EditDoc::Release - out of bounds pos " << nPos);
        return nullptr;
    }

#if ENABLE_YRS
    YrsRemovePara(m_pYrsSupplier, m_CommentId, *this, nPos);
#endif
    std::unique_ptr<ContentNode> pNode = std::move(maContents[nPos]);
    maContents.erase(maContents.begin() + nPos);
    return pNode;
}

sal_Int32 EditDoc::Count() const
{
    size_t nSize = maContents.size();
    if (nSize > SAL_MAX_INT32)
    {
        SAL_WARN( "editeng", "EditDoc::Count - overflow " << nSize);
        return SAL_MAX_INT32;
    }
    return nSize;
}

OUString EditDoc::GetSepStr( LineEnd eEnd )
{
    if ( eEnd == LINEEND_CR )
        return u"\015"_ustr; // 0x0d
    if ( eEnd == LINEEND_LF )
        return u"\012"_ustr; // 0x0a
    return u"\015\012"_ustr; // 0x0d, 0x0a
}

OUString EditDoc::GetText( LineEnd eEnd ) const
{
    const sal_Int32 nNodes = Count();
    if (nNodes == 0)
        return OUString();

    const OUString aSep = EditDoc::GetSepStr( eEnd );
    const sal_Int32 nSepSize = aSep.getLength();
    const sal_Int32 nLen = GetTextLen() + (nNodes - 1)*nSepSize;

    OUStringBuffer aBuffer(nLen + 16); // leave some slack

    for ( sal_Int32 nNode = 0; nNode < nNodes; nNode++ )
    {
        if ( nSepSize && nNode>0 )
        {
            aBuffer.append(aSep);
        }
        aBuffer.append(GetParaAsString( GetObject(nNode) ));
    }

    return aBuffer.makeStringAndClear();
}

OUString EditDoc::GetParaAsString( sal_Int32 nNode ) const
{
    return GetParaAsString( GetObject( nNode ) );
}

OUString EditDoc::GetParaAsString(
    const ContentNode* pNode, sal_Int32 nStartPos, sal_Int32 nEndPos)
{
    return pNode->GetExpandedText(nStartPos, nEndPos);
}

EditPaM EditDoc::GetStartPaM() const
{
    ContentNode* p = const_cast<ContentNode*>(GetObject(0));
    return EditPaM(p, 0);
}

EditPaM EditDoc::GetEndPaM() const
{
    ContentNode* pLastNode = const_cast<ContentNode*>(GetObject(Count()-1));
    return EditPaM( pLastNode, pLastNode->Len() );
}

sal_Int32 EditDoc::GetTextLen() const
{
    sal_Int32 nLength = 0;
    for (auto const& pContent : maContents)
    {
        nLength += pContent->GetExpandedLen();
    }
    return nLength;
}

EditPaM EditDoc::Clear()
{
    maContents.clear();

#if ENABLE_YRS
    // Insert will call YrsAddPara()
    YrsClear(m_pYrsSupplier, m_CommentId);
#endif

    ContentNode* pNode = new ContentNode(GetItemPool());
    Insert(0, std::unique_ptr<ContentNode>(pNode));

    CreateDefFont(false);

    SetModified(false);

    return EditPaM( pNode, 0 );
}

namespace
{
struct ClearSpellErrorsHandler
{
    void operator() (std::unique_ptr<ContentNode> const & rNode)
    {
        rNode->DestroyWrongList();
    }
};
}

void EditDoc::ClearSpellErrors()
{
    std::for_each(maContents.begin(), maContents.end(), ClearSpellErrorsHandler());
}

void EditDoc::SetModified( bool b )
{
    mbModified = b;
    if (mbModified)
        maModifyHdl.Call(nullptr);
}

EditPaM EditDoc::RemoveText()
{
    // Keep the old ItemSet, to keep the chart Font.
    ContentNode* pPrevFirstNode = GetObject(0);
    SfxStyleSheet* pPrevStyle = pPrevFirstNode->GetStyleSheet();
    SfxItemSet aPrevSet( pPrevFirstNode->GetContentAttribs().GetItems() );
    vcl::Font aPrevFont( pPrevFirstNode->GetCharAttribs().GetDefFont() );

    maContents.clear();

#if ENABLE_YRS
    // Insert will call YrsAddPara()
    YrsClear(m_pYrsSupplier, m_CommentId);
#endif

    ContentNode* pNode = new ContentNode(GetItemPool());
    Insert(0, std::unique_ptr<ContentNode>(pNode));

    pNode->SetStyleSheet(pPrevStyle, false);
    pNode->GetContentAttribs().GetItems().Set( aPrevSet );
    pNode->GetCharAttribs().GetDefFont() = aPrevFont;

    SetModified(true);

    return EditPaM(pNode, 0);
}

EditPaM EditDoc::InsertText( EditPaM aPaM, const OUString& rStr )
{
    DBG_ASSERT( rStr.indexOf( 0x0A ) == -1, "EditDoc::InsertText: Newlines prohibited in paragraph!" );
    DBG_ASSERT( rStr.indexOf( 0x0D ) == -1, "EditDoc::InsertText: Newlines prohibited in paragraph!" );
    DBG_ASSERT( rStr.indexOf( '\t' ) == -1, "EditDoc::InsertText: Newlines prohibited in paragraph!" );

    ContentNode* pNode = aPaM.GetNode();
    assert(pNode);
    pNode->Insert( rStr, aPaM.GetIndex() );
    pNode->ExpandAttribs( aPaM.GetIndex(), rStr.getLength() );
    aPaM.SetIndex( aPaM.GetIndex() + rStr.getLength() );

    SetModified( true );

#if ENABLE_YRS
    YrsInsertText(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), aPaM.GetIndex() - rStr.getLength(), rStr);
#endif

    return aPaM;
}

EditPaM EditDoc::InsertParaBreak( EditPaM aPaM, bool bKeepEndingAttribs )
{
    assert(aPaM.GetNode());
    ContentNode* pCurNode = aPaM.GetNode();
    sal_Int32 nPos = GetPos( pCurNode );
    assert(nPos != EE_PARA_MAX);
    OUString aStr = aPaM.GetNode()->Copy( aPaM.GetIndex() );
    aPaM.GetNode()->Erase( aPaM.GetIndex() );

    // the paragraph attributes...
    ContentAttribs aContentAttribs( aPaM.GetNode()->GetContentAttribs() );

    // for a new paragraph we like to have the bullet/numbering visible by default
    aContentAttribs.GetItems().Put( SfxBoolItem( EE_PARA_BULLETSTATE, true) );

    // ContentNode constructor copies also the paragraph attributes
    ContentNode* pNode = new ContentNode(aStr, std::move(aContentAttribs));

    // Copy the Default Font
    pNode->GetCharAttribs().GetDefFont() = aPaM.GetNode()->GetCharAttribs().GetDefFont();
    SfxStyleSheet* pStyle = aPaM.GetNode()->GetStyleSheet();
    if ( pStyle )
    {
        OUString aFollow( pStyle->GetFollow() );
        if ( !aFollow.isEmpty() && ( aFollow != pStyle->GetName() ) )
        {
            SfxStyleSheetBase* pNext = pStyle->GetPool()->Find( aFollow, pStyle->GetFamily() );
            pNode->SetStyleSheet( static_cast<SfxStyleSheet*>(pNext) );
        }
    }

    // Character attributes may need to be copied or trimmed:
    pNode->CopyAndCutAttribs( aPaM.GetNode(), GetItemPool(), bKeepEndingAttribs );

#if ENABLE_YRS
    {
        // skip the YrsAddPara in Insert
        YrsReplayGuard const g{m_pYrsSupplier};
#endif
    Insert(nPos+1, std::unique_ptr<ContentNode>(pNode));
#if ENABLE_YRS
    }
#endif

    SetModified(true);

#if ENABLE_YRS
    YrsInsertParaBreak(m_pYrsSupplier, m_CommentId, *this, nPos, aPaM.GetIndex());
#endif

    aPaM.SetNode( pNode );
    aPaM.SetIndex( 0 );
    return aPaM;
}

EditPaM EditDoc::InsertFeature( EditPaM aPaM, const SfxPoolItem& rItem  )
{
    assert(aPaM.GetNode());

    aPaM.GetNode()->Insert( rtl::OUStringChar(CH_FEATURE), aPaM.GetIndex() );
    aPaM.GetNode()->ExpandAttribs( aPaM.GetIndex(), 1 );

    // Create a feature-attribute for the feature...
    EditCharAttrib* pAttrib = MakeCharAttrib( GetItemPool(), rItem, aPaM.GetIndex(), aPaM.GetIndex()+1 );
    assert(pAttrib);
    aPaM.GetNode()->GetCharAttribs().InsertAttrib( pAttrib );

    SetModified( true );

#if ENABLE_YRS
    YrsInsertFeature(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), pAttrib);
#endif

    aPaM.SetIndex( aPaM.GetIndex() + 1 );
    return aPaM;
}

EditPaM EditDoc::ConnectParagraphs( ContentNode* pLeft, ContentNode* pRight )
{
    const EditPaM aPaM( pLeft, pLeft->Len() );

    // First the attributes, otherwise nLen will not be correct!
    pLeft->AppendAttribs( pRight );
    // then the Text...
    pLeft->Append(pRight->GetString());

    // the one to the right disappears.
    sal_Int32 nRight = GetPos( pRight );
#if ENABLE_YRS
    {
    // skip the YrsRemovePara in Remove, the node even still has the text...
        YrsReplayGuard const g{m_pYrsSupplier};
#endif
    Remove( nRight );
#if ENABLE_YRS
    }
#endif

    SetModified(true);

#if ENABLE_YRS
    assert(nRight != 0);
    YrsConnectPara(m_pYrsSupplier, m_CommentId, *this, nRight - 1, aPaM.GetIndex());
#endif

    return aPaM;
}

void EditDoc::RemoveChars( EditPaM aPaM, sal_Int32 nChars )
{
    // Maybe remove Features!
    aPaM.GetNode()->Erase( aPaM.GetIndex(), nChars );
    aPaM.GetNode()->CollapseAttribs( aPaM.GetIndex(), nChars );

    SetModified( true );

#if ENABLE_YRS
    YrsRemoveChars(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), aPaM.GetIndex(), nChars);
#endif
}

void EditDoc::InsertAttribInSelection( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, const SfxPoolItem& rPoolItem )
{
    assert(pNode);
    DBG_ASSERT( nEnd <= pNode->Len(), "InsertAttrib: Attribute too large!" );

    // for Optimization:
    // This ends at the beginning of the selection => can be expanded
    EditCharAttrib* pEndingAttrib = nullptr;
    // This starts at the end of the selection => can be expanded
    EditCharAttrib* pStartingAttrib = nullptr;

    DBG_ASSERT( nStart <= nEnd, "Small miscalculations in InsertAttribInSelection" );

    RemoveAttribs( pNode, nStart, nEnd, pStartingAttrib, pEndingAttrib, rPoolItem.Which() );

    // tdf#132288  By default inserting an attribute beside another that is of
    // the same type expands the original instead of inserting another. But the
    // spell check dialog doesn't want that behaviour
    if (mbDisableAttributeExpanding)
    {
        pStartingAttrib = nullptr;
        pEndingAttrib = nullptr;
    }

    if ( pStartingAttrib && pEndingAttrib &&
         ( *(pStartingAttrib->GetItem()) == rPoolItem ) &&
         ( *(pEndingAttrib->GetItem()) == rPoolItem ) )
    {
        // Will become a large Attribute.
        pEndingAttrib->GetEnd() = pStartingAttrib->GetEnd();
        pNode->GetCharAttribs().Remove(pStartingAttrib);
#if ENABLE_YRS
        YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pEndingAttrib);
#endif
    }
    else if ( pStartingAttrib && ( *(pStartingAttrib->GetItem()) == rPoolItem ) )
    {
        pStartingAttrib->GetStart() = nStart;
#if ENABLE_YRS
        YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pStartingAttrib);
#endif
    }
    else if ( pEndingAttrib && ( *(pEndingAttrib->GetItem()) == rPoolItem ) )
    {
        pEndingAttrib->GetEnd() = nEnd;
#if ENABLE_YRS
        YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pEndingAttrib);
#endif
    }
    else
        InsertAttrib( rPoolItem, pNode, nStart, nEnd );

    if ( pStartingAttrib )
        pNode->GetCharAttribs().ResortAttribs();

    SetModified(true);
}

bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, sal_uInt16 nWhich )
{
    EditCharAttrib* pStarting;
    EditCharAttrib* pEnding;
    return RemoveAttribs( pNode, nStart, nEnd, pStarting, pEnding, nWhich );
}

bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, EditCharAttrib*& rpStarting, EditCharAttrib*& rpEnding, sal_uInt16 nWhich )
{

    assert(pNode);
    DBG_ASSERT( nEnd <= pNode->Len(), "InsertAttrib: Attribute too large!" );

    // This ends at the beginning of the selection => can be expanded
    rpEnding = nullptr;
    // This starts at the end of the selection => can be expanded
    rpStarting = nullptr;

    bool bChanged = false;
    bool bNeedsSorting = false;

    DBG_ASSERT( nStart <= nEnd, "Small miscalculations in InsertAttribInSelection" );

#if OSL_DEBUG_LEVEL > 0 && !defined NDEBUG
    CharAttribList::DbgCheckAttribs(pNode->GetCharAttribs());
#endif

    // iterate over the attributes ...
    std::size_t nAttr = 0;
    CharAttribList::AttribsType& rAttribs = pNode->GetCharAttribs().GetAttribs();
    EditCharAttrib* pAttr = GetAttrib(rAttribs, nAttr);
    while ( pAttr )
    {
        bool bRemoveAttrib = false;
        sal_uInt16 nAttrWhich = pAttr->Which();
        if ( ( nAttrWhich < EE_FEATURE_START ) && ( !nWhich || ( nAttrWhich == nWhich ) ) )
        {
            // Attribute starts in Selection
            if ( ( pAttr->GetStart() >= nStart ) && ( pAttr->GetStart() <= nEnd ) )
            {
                bChanged = true;
                if ( pAttr->GetEnd() > nEnd )
                {
                    bNeedsSorting = true;
#if ENABLE_YRS
                    YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), pAttr->GetStart(), nEnd);
#endif
                    pAttr->GetStart() = nEnd;   // then it starts after this
                    rpStarting = pAttr;
                    if ( nWhich )
                        break;  // There can be no further attributes here
                }
                else if ( !pAttr->IsFeature() || ( pAttr->GetStart() == nStart ) )
                {
                    // Delete feature only if on the exact spot
                    bRemoveAttrib = true;
                }
            }

            // Attribute ends in Selection
            else if ( ( pAttr->GetEnd() >= nStart ) && ( pAttr->GetEnd() <= nEnd ) )
            {
                bChanged = true;
                if ( ( pAttr->GetStart() < nStart ) && !pAttr->IsFeature() )
                {
#if ENABLE_YRS
                    YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, pAttr->GetEnd());
#endif
                    pAttr->GetEnd() = nStart;   // then it ends here
                    rpEnding = pAttr;
                }
                else if ( !pAttr->IsFeature() || ( pAttr->GetStart() == nStart ) )
                {
                    // Delete feature only if on the exact spot
                    bRemoveAttrib = true;
                }
            }
            // Attribute overlaps the selection
            else if ( ( pAttr->GetStart() <= nStart ) && ( pAttr->GetEnd() >= nEnd ) )
            {
                bChanged = true;
                if ( pAttr->GetStart() == nStart )
                {
                    bNeedsSorting = true;
                    pAttr->GetStart() = nEnd;
#if ENABLE_YRS
                    YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd);
#endif
                    rpStarting = pAttr;
                    if ( nWhich )
                        break;  // There can be further attributes!
                }
                else if ( pAttr->GetEnd() == nEnd )
                {
                    pAttr->GetEnd() = nStart;
#if ENABLE_YRS
                    YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd);
#endif
                    rpEnding = pAttr;
                    if ( nWhich )
                        break;  // There can be further attributes!
                }
                else // Attribute must be split ...
                {
                    bNeedsSorting = true;
                    sal_Int32 nOldEnd = pAttr->GetEnd();
                    pAttr->GetEnd() = nStart;
                    rpEnding = pAttr;
#if ENABLE_YRS
                    YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd);
                    {
                    YrsReplayGuard const g{m_pYrsSupplier};
#endif
                    InsertAttrib( *pAttr->GetItem(), pNode, nEnd, nOldEnd );
#if ENABLE_YRS
                    }
#endif
                    if ( nWhich )
                        break;  // There can be further attributes!
                }
            }
        }
        if ( bRemoveAttrib )
        {
#if ENABLE_YRS
            YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), pAttr->GetStart(), pAttr->GetEnd());
#endif
            DBG_ASSERT( ( pAttr != rpStarting ) && ( pAttr != rpEnding ), "Delete and retain the same attribute?" );
            DBG_ASSERT( !pAttr->IsFeature(), "RemoveAttribs: Remove a feature?!" );
            rAttribs.erase(rAttribs.begin()+nAttr);
        }
        else
        {
            nAttr++;
        }
        pAttr = GetAttrib(rAttribs, nAttr);
    }

    if ( bChanged )
    {
        // char attributes need to be sorted by start again
        if (bNeedsSorting)
            pNode->GetCharAttribs().ResortAttribs();
        SetModified(true);
    }

#if OSL_DEBUG_LEVEL > 0 && !defined NDEBUG
    CharAttribList::DbgCheckAttribs(pNode->GetCharAttribs());
#endif

    return bChanged;
}

void EditDoc::InsertAttrib( const SfxPoolItem& rPoolItem, ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd )
{
    // This method no longer checks whether a corresponding attribute already
    // exists at this place!
    EditCharAttrib* pAttrib = MakeCharAttrib( GetItemPool(), rPoolItem, nStart, nEnd );
    assert(pAttrib);
    pNode->GetCharAttribs().InsertAttrib( pAttrib );

    SetModified( true );

#if ENABLE_YRS
    YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttrib);
#endif
}

void EditDoc::InsertAttrib( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, const SfxPoolItem& rPoolItem )
{
    if ( nStart != nEnd )
    {
        InsertAttribInSelection( pNode, nStart, nEnd, rPoolItem );
    }
    else
    {
        // Check whether already a new attribute with WhichId exists at this place:
        CharAttribList& rAttrList = pNode->GetCharAttribs();
        EditCharAttrib* pAttr = rAttrList.FindEmptyAttrib( rPoolItem.Which(), nStart );
        if ( pAttr )
        {
            // Remove attribute...
            rAttrList.Remove(pAttr);
        }

        // check whether 'the same' attribute exist at this place.
        pAttr = rAttrList.FindAttrib( rPoolItem.Which(), nStart );
        if ( pAttr )
        {
            if ( pAttr->IsInside( nStart ) )    // split
            {
                // check again if really splitting, or return !
                sal_Int32 nOldEnd = pAttr->GetEnd();
                pAttr->GetEnd() = nStart;
                EditCharAttrib* pNew = MakeCharAttrib( GetItemPool(), *(pAttr->GetItem()), nStart, nOldEnd );
                rAttrList.InsertAttrib(pNew);
            }
            else if ( pAttr->GetEnd() == nStart )
            {
                DBG_ASSERT( !pAttr->IsEmpty(), "Still an empty attribute?" );
                // Check if exactly the same attribute
                if ( *(pAttr->GetItem()) == rPoolItem )
                    return;
            }
        }
        InsertAttrib( rPoolItem, pNode, nStart, nStart );
    }

    SetModified( true );
}

void EditDoc::FindAttribs( ContentNode* pNode, sal_Int32 nStartPos, sal_Int32 nEndPos, SfxItemSet& rCurSet )
{
    assert(pNode);
    DBG_ASSERT( nStartPos <= nEndPos, "Invalid region!" );

    std::size_t nAttr = 0;
    EditCharAttrib* pAttr = GetAttrib( pNode->GetCharAttribs().GetAttribs(), nAttr );
    // No Selection...
    if ( nStartPos == nEndPos )
    {
        while ( pAttr && ( pAttr->GetStart() <= nEndPos) )
        {
            const SfxPoolItem* pItem = nullptr;
            // Attribute is about...
            if ( ( pAttr->GetStart() < nStartPos ) && ( pAttr->GetEnd() > nStartPos ) )
                pItem = pAttr->GetItem();
            // Attribute ending here is not empty
            else if ( ( pAttr->GetStart() < nStartPos ) && ( pAttr->GetEnd() == nStartPos ) )
            {
                if ( !pNode->GetCharAttribs().FindEmptyAttrib( pAttr->GetItem()->Which(), nStartPos ) )
                    pItem = pAttr->GetItem();
            }
            // Attribute ending here is empty
            else if ( ( pAttr->GetStart() == nStartPos ) && ( pAttr->GetEnd() == nStartPos ) )
            {
                pItem = pAttr->GetItem();
            }
            // Attribute starts here
            else if ( ( pAttr->GetStart() == nStartPos ) && ( pAttr->GetEnd() > nStartPos ) )
            {
                if ( nStartPos == 0 )   // special case
                    pItem = pAttr->GetItem();
            }

            if ( pItem )
            {
                sal_uInt16 nWhich = pItem->Which();
                if ( rCurSet.GetItemState( nWhich ) == SfxItemState::DEFAULT )
                {
                    rCurSet.Put( *pItem );
                }
                else if ( rCurSet.GetItemState( nWhich ) == SfxItemState::SET )
                {
                    const SfxPoolItem& rItem = rCurSet.Get( nWhich );
                    if ( rItem != *pItem )
                    {
                        rCurSet.InvalidateItem( nWhich );
                    }
                }
            }
            nAttr++;
            pAttr = GetAttrib( pNode->GetCharAttribs().GetAttribs(), nAttr );
        }
    }
    else    // Selection
    {
        while ( pAttr && ( pAttr->GetStart() < nEndPos) )
        {
            const SfxPoolItem* pItem = nullptr;
            // Attribute is about...
            if ( ( pAttr->GetStart() <= nStartPos ) && ( pAttr->GetEnd() >= nEndPos ) )
                pItem = pAttr->GetItem();
            // Attribute starts right in the middle ...
            else if ( pAttr->GetStart() >= nStartPos )
            {
                // !!! pItem = pAttr->GetItem();
                // PItem is simply not enough, since one for example in case
                // of Shadow, would never find an unequal item, since such a
                // item represents its presence by absence!
                // If (...)
                // It needs to be examined on exactly the same attribute at the
                // break point, which is quite expensive.
                // Since optimization is done when inserting the attributes
                // this case does not appear so fast...
                // So based on the need for speed:
                rCurSet.InvalidateItem( pAttr->GetItem()->Which() );

            }
            // Attribute ends in the middle of it ...
            else if ( pAttr->GetEnd() > nStartPos )
            {
                rCurSet.InvalidateItem( pAttr->GetItem()->Which() );
            }

            if ( pItem )
            {
                sal_uInt16 nWhich = pItem->Which();
                if ( rCurSet.GetItemState( nWhich ) == SfxItemState::DEFAULT )
                {
                    rCurSet.Put( *pItem );
                }
                else if ( rCurSet.GetItemState( nWhich ) == SfxItemState::SET )
                {
                    const SfxPoolItem& rItem = rCurSet.Get( nWhich );
                    if ( rItem != *pItem )
                    {
                        rCurSet.InvalidateItem( nWhich );
                    }
                }
            }
            nAttr++;
            pAttr = GetAttrib( pNode->GetCharAttribs().GetAttribs(), nAttr );
        }
    }
}

void EditDoc::dumpAsXml(xmlTextWriterPtr pWriter) const
{
    bool bOwns = false;
    if (!pWriter)
    {
        pWriter = xmlNewTextWriterFilename("editdoc.xml", 0);
        xmlTextWriterSetIndent(pWriter,1);
        (void)xmlTextWriterSetIndentString(pWriter, BAD_CAST("  "));
        (void)xmlTextWriterStartDocument(pWriter, nullptr, nullptr, nullptr);
        bOwns = true;
    }

    (void)xmlTextWriterStartElement(pWriter, BAD_CAST("EditDoc"));
    for (auto const & i : maContents)
    {
        i->dumpAsXml(pWriter);
    }
    (void)xmlTextWriterEndElement(pWriter);

    if (bOwns)
    {
       (void)xmlTextWriterEndDocument(pWriter);
       xmlFreeTextWriter(pWriter);
    }
}

EditEngineItemPool::EditEngineItemPool()
: SfxItemPool(u"EditEngineItemPool"_ustr)
{
    registerItemInfoPackage(getItemInfoPackageEditEngine());
}

EditEngineItemPool::~EditEngineItemPool()
{
    SetSecondaryPool(nullptr);
}

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