Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  txtedt.cxx

  Sprache: C
 

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

#include <hintids.hxx>
#include <utility>
#include <vcl/svapp.hxx>
#include <svl/itemiter.hxx>
#include <svl/languageoptions.hxx>
#include <editeng/splwrap.hxx>
#include <editeng/langitem.hxx>
#include <editeng/fontitem.hxx>
#include <editeng/hangulhanja.hxx>
#include <i18nutil/transliteration.hxx>
#include <linguistic/misc.hxx>
#include <SwSmartTagMgr.hxx>
#include <o3tl/safeint.hxx>
#include <osl/diagnose.h>
#include <officecfg/Office/Writer.hxx>
#include <unotools/transliterationwrapper.hxx>
#include <sal/log.hxx>
#include <swmodule.hxx>
#include <splargs.hxx>
#include <viewopt.hxx>
#include <acmplwrd.hxx>
#include <doc.hxx>
#include <IDocumentRedlineAccess.hxx>
#include <IDocumentLayoutAccess.hxx>
#include <docsh.hxx>
#include <txtfld.hxx>
#include <txatbase.hxx>
#include <charatr.hxx>
#include <pam.hxx>
#include <hints.hxx>
#include <ndtxt.hxx>
#include <txtfrm.hxx>
#include <SwGrammarMarkUp.hxx>
#include <rootfrm.hxx>
#include <swscanner.hxx>

#include <breakit.hxx>
#include <UndoOverwrite.hxx>
#include <txatritr.hxx>
#include <redline.hxx>
#include <docary.hxx>
#include <scriptinfo.hxx>
#include <docstat.hxx>
#include <editsh.hxx>
#include <unotextmarkup.hxx>
#include <txtatr.hxx>
#include <fmtautofmt.hxx>
#include <istyleaccess.hxx>
#include <unicode/uchar.h>
#include <DocumentSettingManager.hxx>

#include <com/sun/star/i18n/WordType.hpp>
#include <com/sun/star/i18n/ScriptType.hpp>
#include <com/sun/star/i18n/XBreakIterator.hpp>

#include <vector>

#include <unotextrange.hxx>

using namespace ::com::sun::star;
using namespace ::com::sun::star::frame;
using namespace ::com::sun::star::i18n;
using namespace ::com::sun::star::beans;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::linguistic2;
using namespace ::com::sun::star::smarttags;

namespace
{
    void DetectAndMarkMissingDictionaries( SwDoc& rDoc,
                                           const uno::Reference< XSpellChecker1 >& xSpell,
                                           const LanguageType eActLang )
    {
        if( xSpell.is() && !xSpell->hasLanguage( eActLang.get() ) )
            rDoc.SetMissingDictionaries( true );
        else
            rDoc.SetMissingDictionaries( false );
    }
}

static bool lcl_HasComments(const SwTextNode& rNode)
{
    sal_Int32 nPosition = rNode.GetText().indexOf(CH_TXTATR_INWORD);
    while (nPosition != -1)
    {
        const SwTextAttr* pAttr = rNode.GetTextAttrForCharAt(nPosition);
        if (pAttr && pAttr->Which() == RES_TXTATR_ANNOTATION)
            return true;
        nPosition = rNode.GetText().indexOf(CH_TXTATR_INWORD, nPosition + 1);
    }
    return false;
}

// possible delimiter characters within URLs for word breaking
static bool lcl_IsDelim( const sal_Unicode c )
{
   return '#' == c || '$' == c || '%' == c || '&' == c || '+' == c ||
          ',' == c || '-' == c || '.' == c || '/' == c || ':' == c ||
          ';' == c || '=' == c || '?' == c || '@' == c || '_' == c;
}

// allow to check normal text with hyperlink by recognizing (parts of) URLs
static bool lcl_IsURL(std::u16string_view rWord,
    const SwTextNode &rNode, sal_Int32 nBegin, sal_Int32 nLen)
{
    // not a text with hyperlink
    if ( !rNode.GetTextAttrAt(nBegin, RES_TXTATR_INETFMT) )
        return false;

    // there is a dot in the word, which is not a period ("example.org")
    const size_t nPosAt = rWord.find('.');
    if (nPosAt != std::u16string_view::npos && nPosAt < rWord.length() - 1)
        return true;

    // an e-mail address ("user@example")
    if ( rWord.find('@') != std::u16string_view::npos )
        return true;

    const OUString& rText = rNode.GetText();

    // scheme (e.g. "http" in "http://" or "mailto" in "mailto:address"):
    // word is followed by 1) ':' + an alphanumeric character; 2) or ':' + a delimiter
    if ( nBegin + nLen + 2 <= rText.getLength() && ':' == rText[nBegin + nLen] )
    {
         sal_Unicode c = rText[nBegin + nLen + 1];
         if ( u_isalnum(c) || lcl_IsDelim(c) )
             return true;
    }

    // path, query, fragment (e.g. "path" in "example.org/path"):
    // word is preceded by 1) an alphanumeric character + a delimiter; 2) or two delimiters
    if ( 2 <= nBegin && lcl_IsDelim(rText[nBegin - 1]) )
    {
        sal_Unicode c = rText[nBegin - 2];
        if ( u_isalnum(c) || lcl_IsDelim(c) )
            return true;
    }

    return false;
}

/*
 * This has basically the same function as SwScriptInfo::MaskHiddenRanges,
 * only for deleted redlines
 */


static sal_Int32
lcl_MaskRedlines( const SwTextNode& rNode, OUStringBuffer& rText,
                         sal_Int32 nStt, sal_Int32 nEnd,
                         const sal_Unicode cChar )
{
    sal_Int32 nNumOfMaskedRedlines = 0;

    const SwDoc& rDoc = rNode.GetDoc();

    for ( SwRedlineTable::size_type nAct = rDoc.getIDocumentRedlineAccess().GetRedlinePos( rNode, RedlineType::Any ); nAct < rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++nAct )
    {
        const SwRangeRedline* pRed = rDoc.getIDocumentRedlineAccess().GetRedlineTable()[ nAct ];

        if ( pRed->Start()->GetNode() > rNode )
            break;

        if( RedlineType::Delete == pRed->GetType() )
        {
            sal_Int32 nRedlineEnd;
            sal_Int32 nRedlineStart;

            pRed->CalcStartEnd( rNode.GetIndex(), nRedlineStart, nRedlineEnd );

            if ( nRedlineEnd < nStt || nRedlineStart > nEnd )
                continue;

            while ( nRedlineStart < nRedlineEnd && nRedlineStart < nEnd )
            {
                if (nRedlineStart >= nStt)
                {
                    rText[nRedlineStart] = cChar;
                    ++nNumOfMaskedRedlines;
                }
                ++nRedlineStart;
            }
        }
    }

    return nNumOfMaskedRedlines;
}

/**
 * Used for spell checking. Deleted redlines and hidden characters are masked
 */

static bool
lcl_MaskRedlinesAndHiddenText( const SwTextNode& rNode, OUStringBuffer& rText,
                                      sal_Int32 nStt, sal_Int32 nEnd,
                                      const sal_Unicode cChar = CH_TXTATR_INWORD )
{
    sal_Int32 nRedlinesMasked = 0;
    sal_Int32 nHiddenCharsMasked = 0;

    const SwDoc& rDoc = rNode.GetDoc();
    const bool bShowChg = IDocumentRedlineAccess::IsShowChanges( rDoc.getIDocumentRedlineAccess().GetRedlineFlags() );

    // If called from word count or from spell checking, deleted redlines
    // should be masked:
    if ( bShowChg )
    {
        nRedlinesMasked = lcl_MaskRedlines( rNode, rText, nStt, nEnd, cChar );
    }

    const bool bHideHidden = !SwModule::get()->GetViewOption(rDoc.GetDocumentSettingManager().get(DocumentSettingId::HTML_MODE))->IsShowHiddenChar();

    // If called from word count, we want to mask the hidden ranges even
    // if they are visible:
    if ( bHideHidden )
    {
        nHiddenCharsMasked =
            SwScriptInfo::MaskHiddenRanges( rNode, rText, nStt, nEnd, cChar );
    }

    return (nRedlinesMasked > 0) || (nHiddenCharsMasked > 0);
}

/**
 * Used for spell checking. Calculates a rectangle for repaint.
 */

static SwRect lcl_CalculateRepaintRect(
        const SwTextFrame & rTextFrame, const SwTextNode & rNode,
        sal_Int32 const nChgStart, sal_Int32 const nChgEnd)
{
    TextFrameIndex const iChgStart(rTextFrame.MapModelToView(&rNode, nChgStart));
    TextFrameIndex const iChgEnd(rTextFrame.MapModelToView(&rNode, nChgEnd));

    SwRect aRect = rTextFrame.GetPaintArea();
    SwRect aTmp = rTextFrame.GetPaintArea();

    const SwTextFrame* pStartFrame = &rTextFrame;
    while( pStartFrame->HasFollow() &&
           iChgStart >= pStartFrame->GetFollow()->GetOffset())
        pStartFrame = pStartFrame->GetFollow();
    const SwTextFrame* pEndFrame = pStartFrame;
    while( pEndFrame->HasFollow() &&
           iChgEnd >= pEndFrame->GetFollow()->GetOffset())
        pEndFrame = pEndFrame->GetFollow();

    bool bSameFrame = true;

    if( rTextFrame.HasFollow() )
    {
        if( pEndFrame != pStartFrame )
        {
            bSameFrame = false;
            SwRect aStFrame( pStartFrame->GetPaintArea() );
            {
                SwRectFnSet aRectFnSet(pStartFrame);
                aRectFnSet.SetLeft( aTmp, aRectFnSet.GetLeft(aStFrame) );
                aRectFnSet.SetRight( aTmp, aRectFnSet.GetRight(aStFrame) );
                aRectFnSet.SetBottom( aTmp, aRectFnSet.GetBottom(aStFrame) );
            }
            aStFrame = pEndFrame->GetPaintArea();
            {
                SwRectFnSet aRectFnSet(pEndFrame);
                aRectFnSet.SetTop( aRect, aRectFnSet.GetTop(aStFrame) );
                aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aStFrame) );
                aRectFnSet.SetRight( aRect, aRectFnSet.GetRight(aStFrame) );
            }
            aRect.Union( aTmp );
            whiletrue )
            {
                pStartFrame = pStartFrame->GetFollow();
                if( pStartFrame == pEndFrame )
                    break;
                aRect.Union( pStartFrame->GetPaintArea() );
            }
        }
    }
    if( bSameFrame )
    {
        SwRectFnSet aRectFnSet(pStartFrame);
        if( aRectFnSet.GetTop(aTmp) == aRectFnSet.GetTop(aRect) )
            aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aTmp) );
        else
        {
            SwRect aStFrame( pStartFrame->GetPaintArea() );
            aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aStFrame) );
            aRectFnSet.SetRight( aRect, aRectFnSet.GetRight(aStFrame) );
            aRectFnSet.SetTop( aRect, aRectFnSet.GetTop(aTmp) );
        }

        if( aTmp.Height() > aRect.Height() )
            aRect.Height( aTmp.Height() );
    }

    return aRect;
}

/**
 * Used for automatic styles. Used during RstAttr.
 */

static bool lcl_HaveCommonAttributes( IStyleAccess& rStyleAccess,
                                      const SfxItemSet* pSet1,
                                      sal_uInt16 nWhichId,
                                      const SfxItemSet& rSet2,
                                      std::shared_ptr<SfxItemSet>& pStyleHandle )
{
    bool bRet = false;

    std::unique_ptr<SfxItemSet> pNewSet;

    if ( !pSet1 )
    {
        OSL_ENSURE( nWhichId, "lcl_HaveCommonAttributes not used correctly" );
        if ( SfxItemState::SET == rSet2.GetItemState( nWhichId, false ) )
        {
            pNewSet = rSet2.Clone();
            pNewSet->ClearItem( nWhichId );
        }
    }
    else if ( pSet1->Count() )
    {
        SfxItemIter aIter( *pSet1 );
        const SfxPoolItem* pItem = aIter.GetCurItem();
        do
        {
            if ( SfxItemState::SET == rSet2.GetItemState( pItem->Which(), false ) )
            {
                if ( !pNewSet )
                    pNewSet = rSet2.Clone();
                pNewSet->ClearItem( pItem->Which() );
            }

            pItem = aIter.NextItem();
        } while (pItem);
    }

    if ( pNewSet )
    {
        if ( pNewSet->Count() )
            pStyleHandle = rStyleAccess.getAutomaticStyle( *pNewSet, IStyleAccess::AUTO_STYLE_CHAR );
        bRet = true;
    }

    return bRet;
}

/** Delete all attributes
 *
 * 5 cases:
 * 1) The attribute is completely in the deletion range:
 *    -> delete it
 * 2) The end of the attribute is in the deletion range:
 *    -> delete it, then re-insert it with new end
 * 3) The start of the attribute is in the deletion range:
 *    -> delete it, then re-insert it with new start
 * 4) The attribute contains the deletion range:
 *       Split, i.e.,
 *    -> Delete, re-insert from old start to start of deletion range
 *    -> insert new attribute from end of deletion range to old end
 * 5) The attribute is outside the deletion range
 *    -> nothing to do
 *
 * @param nStt starting position
 * @param nLen length of the deletion
 * @param nWhich ???
 * @param pSet ???
 * @param bInclRefToxMark ???
 */


void SwTextNode::RstTextAttr(
    sal_Int32 nStt,
    const sal_Int32 nLen,
    const sal_uInt16 nWhich,
    const SfxItemSet* pSet,
    const bool bInclRefToxMark,
    const bool bExactRange )
{
    if ( !GetpSwpHints() )
        return;

    sal_Int32 nEnd = nStt + nLen;
    {
        // enlarge range for the reset of text attributes in case of an overlapping input field
        const SwTextInputField* pTextInputField = dynamic_cast<const SwTextInputField*>(GetTextAttrAt(nStt, RES_TXTATR_INPUTFIELD, ::sw::GetTextAttrMode::Parent));
        if ( pTextInputField == nullptr )
        {
            pTextInputField = dynamic_cast<const SwTextInputField*>(GetTextAttrAt(nEnd, RES_TXTATR_INPUTFIELD, ::sw::GetTextAttrMode::Parent));
        }
        if ( pTextInputField != nullptr )
        {
            if ( nStt > pTextInputField->GetStart() )
            {
                nStt = pTextInputField->GetStart();
            }
            if ( nEnd < *(pTextInputField->End()) )
            {
                nEnd = *(pTextInputField->End());
            }
        }
    }

    bool bChanged = false;

    // nMin and nMax initialized to maximum / minimum (inverse)
    sal_Int32 nMin = m_Text.getLength();
    sal_Int32 nMax = nStt;
    const bool bNoLen = nMin == 0;

    // We have to remember the "new" attributes that have
    // been introduced by splitting surrounding attributes (case 2,3,4).
    std::vector<SwTextAttr *> newAttributes;
    std::vector<SwTextAttr *> delAttributes;

    // iterate over attribute array until start of attribute is behind deletion range
    m_pSwpHints->SortIfNeedBe(); // trigger sorting now, we don't want it during iteration
    size_t i = 0;
    sal_Int32 nAttrStart = sal_Int32();
    SwTextAttr *pHt = nullptr;
    while ( (i < m_pSwpHints->Count())
            && ( ( ( nAttrStart = m_pSwpHints->GetWithoutResorting(i)->GetStart()) < nEnd )
                 || nLen==0 || (nEnd == nAttrStart && nAttrStart == m_Text.getLength()))
            && !bExactRange)
    {
        pHt = m_pSwpHints->GetWithoutResorting(i);

        // attributes without end stay in!
        // but consider <bInclRefToxMark> used by Undo
        const sal_Int32* const pAttrEnd = pHt->GetEnd();
        const bool bKeepAttrWithoutEnd =
            pAttrEnd == nullptr
            && ( !bInclRefToxMark
                 || ( RES_TXTATR_REFMARK != pHt->Which()
                      && RES_TXTATR_TOXMARK != pHt->Which()
                      && RES_TXTATR_META != pHt->Which()
                      && RES_TXTATR_METAFIELD != pHt->Which() ) );
        if ( bKeepAttrWithoutEnd )
        {

            i++;
            continue;
        }
        // attributes with content stay in
        if ( pHt->HasContent() )
        {
            ++i;
            continue;
        }

        // Default behavior is to process all attributes:
        bool bSkipAttr = false;
        std::shared_ptr<SfxItemSet> pStyleHandle;

        // 1. case: We want to reset only the attributes listed in pSet:
        if ( pSet )
        {
            bSkipAttr = SfxItemState::SET != pSet->GetItemState( pHt->Which(), false );
            if ( bSkipAttr && RES_TXTATR_AUTOFMT == pHt->Which() )
            {
                // if the current attribute is an autostyle, we have to check if the autostyle
                // and pSet have any attributes in common. If so, pStyleHandle will contain
                // a handle to AutoStyle / pSet:
                bSkipAttr = !lcl_HaveCommonAttributes( getIDocumentStyleAccess(), pSet, 0, *static_cast<const SwFormatAutoFormat&>(pHt->GetAttr()).GetStyleHandle(), pStyleHandle );
            }
        }
        else if ( nWhich )
        {
            // 2. case: We want to reset only the attributes with WhichId nWhich:
            bSkipAttr = nWhich != pHt->Which();
            if ( bSkipAttr && RES_TXTATR_AUTOFMT == pHt->Which() )
            {
                bSkipAttr = !lcl_HaveCommonAttributes( getIDocumentStyleAccess(), nullptr, nWhich, *static_cast<const SwFormatAutoFormat&>(pHt->GetAttr()).GetStyleHandle(), pStyleHandle );
            }
        }
        else if ( !bInclRefToxMark )
        {
            // 3. case: Reset all attributes except from ref/toxmarks:
            // skip hints with CH_TXTATR here
            // (deleting those is ONLY allowed for UNDO!)
            bSkipAttr = RES_TXTATR_REFMARK   == pHt->Which()
                     || RES_TXTATR_TOXMARK   == pHt->Which()
                     || RES_TXTATR_META      == pHt->Which()
                     || RES_TXTATR_METAFIELD == pHt->Which();
        }

        if ( bSkipAttr )
        {
            i++;
            continue;
        }

        if (nStt <= nAttrStart)     // Case: 1,3,5
        {
            const sal_Int32 nAttrEnd = pAttrEnd != nullptr
                                        ? *pAttrEnd
                                        : nAttrStart;
            if (nEnd > nAttrStart
                || (nEnd == nAttrEnd && nEnd == nAttrStart)) // Case: 1,3
            {
                if ( nMin > nAttrStart )
                    nMin = nAttrStart;
                if ( nMax < nAttrEnd )
                    nMax = nAttrEnd;
                // If only a no-extent hint is deleted, no resorting is needed
                bChanged = bChanged || nEnd > nAttrStart || bNoLen;
                if (nAttrEnd <= nEnd)   // Case: 1
                {
                    delAttributes.push_back(pHt);

                    if ( pStyleHandle )
                    {
                        SwTextAttr* pNew = MakeTextAttr( GetDoc(),
                                *pStyleHandle, nAttrStart, nAttrEnd );
                        newAttributes.push_back(pNew);
                    }
                }
                else    // Case: 3
                {
                    bChanged = true;
                    m_pSwpHints->NoteInHistory( pHt );
                    // UGLY: this may temporarily destroy the sorting!
                    pHt->SetStart(nEnd);
                    m_pSwpHints->NoteInHistory( pHt, true );

                    if ( pStyleHandle && nAttrStart < nEnd )
                    {
                        SwTextAttr* pNew = MakeTextAttr( GetDoc(),
                                *pStyleHandle, nAttrStart, nEnd );
                        newAttributes.push_back(pNew);
                    }
                }
            }
        }
        else if (pAttrEnd != nullptr)         // Case: 2,4,5
        {
            if (*pAttrEnd > nStt)       // Case: 2,4
            {
                if (*pAttrEnd < nEnd)   // Case: 2
                {
                    if ( nMin > nAttrStart )
                        nMin = nAttrStart;
                    if ( nMax < *pAttrEnd )
                        nMax = *pAttrEnd;
                    bChanged = true;

                    const sal_Int32 nAttrEnd = *pAttrEnd;

                    m_pSwpHints->NoteInHistory( pHt );
                    // UGLY: this may temporarily destroy the sorting!
                    pHt->SetEnd(nStt);
                    m_pSwpHints->NoteInHistory( pHt, true );

                    if ( pStyleHandle )
                    {
                        SwTextAttr* pNew = MakeTextAttr( GetDoc(),
                            *pStyleHandle, nStt, nAttrEnd );
                        newAttributes.push_back(pNew);
                    }
                }
                else if (nLen)  // Case: 4
                {
                    // for Length 0 both hints would be merged again by
                    // InsertHint, so leave them alone!
                    if ( nMin > nAttrStart )
                        nMin = nAttrStart;
                    if ( nMax < *pAttrEnd )
                        nMax = *pAttrEnd;
                    bChanged = true;
                    const sal_Int32 nTmpEnd = *pAttrEnd;
                    m_pSwpHints->NoteInHistory( pHt );
                    // UGLY: this may temporarily destroy the sorting!
                    pHt->SetEnd(nStt);
                    m_pSwpHints->NoteInHistory( pHt, true );

                    if ( pStyleHandle && nStt < nEnd )
                    {
                        SwTextAttr* pNew = MakeTextAttr( GetDoc(),
                            *pStyleHandle, nStt, nEnd );
                        newAttributes.push_back(pNew);
                    }

                    if( nEnd < nTmpEnd )
                    {
                        SwTextAttr* pNew = MakeTextAttr( GetDoc(),
                            pHt->GetAttr(), nEnd, nTmpEnd );
                        if ( pNew )
                        {
                            SwTextCharFormat* pCharFormat = dynamic_cast<SwTextCharFormat*>(pHt);
                            if ( pCharFormat )
                                static_txtattr_cast<SwTextCharFormat*>(pNew)->SetSortNumber(pCharFormat->GetSortNumber());

                            newAttributes.push_back(pNew);
                        }
                    }
                }
            }
        }
        ++i;
    }

    if (bExactRange)
    {
        // Only delete the hints which start at nStt and end at nEnd.
        for (i = 0; i < m_pSwpHints->Count(); ++i)
        {
            SwTextAttr* pHint = m_pSwpHints->Get(i);
            if ( (isTXTATR_WITHEND(pHint->Which()) && RES_TXTATR_AUTOFMT != pHint->Which())
                || pHint->GetStart() != nStt)
                continue;

            const sal_Int32* pHintEnd = pHint->GetEnd();
            if (!pHintEnd || *pHintEnd != nEnd)
                continue;

            delAttributes.push_back(pHint);
        }
    }

    if (bChanged && !delAttributes.empty())
    {   // Delete() calls GetStartOf() - requires sorted hints!
        m_pSwpHints->Resort();
    }

    // delay deleting the hints because it re-sorts the hints array
    for (SwTextAttr *const pDel : delAttributes)
    {
        m_pSwpHints->Delete(pDel);
        DestroyAttr(pDel);
    }

    // delay inserting the hints because it re-sorts the hints array
    for (SwTextAttr *const pNew : newAttributes)
    {
        InsertHint(pNew, SetAttrMode::NOHINTADJUST);
    }

    TryDeleteSwpHints();

    if (!bChanged)
        return;

    if ( HasHints() )
    {   // possibly sometimes Resort would be sufficient, but...
        m_pSwpHints->MergePortions(*this);
    }

    // TextFrame's respond to aHint, others to aNew
    SwUpdateAttr aHint(
        nMin,
        nMax,
        0);

    CallSwClientNotify(sw::UpdateAttrHint(nullptr, &aHint));
    CallSwClientNotify(SwFormatChangeHint(nullptr, GetFormatColl()));
}

static sal_Int32 clipIndexBounds(std::u16string_view aStr, sal_Int32 nPos)
{
    if (nPos < 0)
        return 0;
    if (nPos > sal_Int32(aStr.size()))
        return aStr.size();
    return nPos;
}

// Return current word:
// Search from left to right, so find the word before nPos.
// Except if at the start of the paragraph, then return the first word.
// If the first word consists only of whitespace, return an empty string.
OUString SwTextFrame::GetCurWord(SwPosition const& rPos) const
{
    TextFrameIndex const nPos(MapModelToViewPos(rPos));
    SwTextNode *const pTextNode(rPos.GetNode().GetTextNode());
    assert(pTextNode);
    OUString const& rText(GetText());
    assert(sal_Int32(nPos) <= rText.getLength()); // invalid index

    if (rText.isEmpty() || IsHiddenNow())
        return OUString();

    assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is());
    const uno::Reference< XBreakIterator > &rxBreak = g_pBreakIt->GetBreakIter();
    sal_Int16 nWordType = WordType::DICTIONARY_WORD;
    lang::Locale aLocale( g_pBreakIt->GetLocale(pTextNode->GetLang(rPos.GetContentIndex())) );
    Boundary aBndry =
        rxBreak->getWordBoundary(rText, sal_Int32(nPos), aLocale, nWordType, true);

    // if no word was found use previous word (if any)
    if (aBndry.startPos == aBndry.endPos)
    {
        aBndry = rxBreak->previousWord(rText, sal_Int32(nPos), aLocale, nWordType);
    }

    // check if word was found and if it uses a symbol font, if so
    // enforce returning an empty string
    if (aBndry.endPos != aBndry.startPos
        && IsSymbolAt(TextFrameIndex(aBndry.startPos)))
    {
        aBndry.endPos = aBndry.startPos;
    }

    // can have -1 as start/end of bounds not found
    aBndry.startPos = clipIndexBounds(rText, aBndry.startPos);
    aBndry.endPos = clipIndexBounds(rText, aBndry.endPos);

    return  rText.copy(aBndry.startPos,
                       aBndry.endPos - aBndry.startPos);
}

SwScanner::SwScanner( const SwTextNode& rNd, const OUString& rText,
    const LanguageType* pLang, const ModelToViewHelper& rConvMap,
    sal_uInt16 nType, sal_Int32 nStart, sal_Int32 nEnd, bool bClp )
    : SwScanner(
        [&rNd](sal_Int32 const nBegin, sal_uInt16 const nScript, bool const bNoChar)
            { return rNd.GetLang(nBegin, bNoChar ? 0 : 1, nScript); }
        , rText, pLang, rConvMap, nType, nStart, nEnd, bClp)
{
}

SwScanner::SwScanner(std::function<LanguageType(sal_Int32, sal_Int32, bool)> aGetLangOfChar,
                     OUString aText, const LanguageType* pLang,
                     ModelToViewHelper aConvMap, sal_uInt16 nType, sal_Int32 nStart,
                     sal_Int32 nEnd, bool bClp)
    : m_pGetLangOfChar(std::move(aGetLangOfChar))
    , m_aPreDashReplacementText(std::move(aText))
    , m_pLanguage(pLang)
    , m_ModelToView(std::move(aConvMap))
    , m_nLength(0)
    , m_nOverriddenDashCount(0)
    , m_nWordType(nType)
    , m_bClip(bClp)
{
    m_nStartPos = m_nBegin = nStart;
    m_nEndPos = nEnd;

    //MSWord f.e has special emdash and endash behaviour in that they break
    //words for the purposes of word counting, while a hyphen etc. doesn't.

    //The default configuration treats emdash/endash as a word break, but
    //additional ones can be added in under tools->options
    if (m_nWordType == i18n::WordType::WORD_COUNT)
    {
        OUString sDashes = officecfg::Office::Writer::WordCount::AdditionalSeparators::get();
        OUStringBuffer aBuf(m_aPreDashReplacementText);
        for (sal_Int32 i = m_nStartPos; i < m_nEndPos; ++i)
        {
            if (i < 0)
                continue;
            sal_Unicode cChar = aBuf[i];
            if (sDashes.indexOf(cChar) != -1)
            {
                aBuf[i] = ' ';
                ++m_nOverriddenDashCount;
            }
        }
        m_aText = aBuf.makeStringAndClear();
    }
    else
        m_aText = m_aPreDashReplacementText;

    assert(m_aPreDashReplacementText.getLength() == m_aText.getLength());

    LanguageType aNewLang;
    if ( m_pLanguage )
    {
        aNewLang = *m_pLanguage;
    }
    else
    {
        ModelToViewHelper::ModelPosition aModelBeginPos =
            m_ModelToView.ConvertToModelPosition( m_nBegin );
        aNewLang = m_pGetLangOfChar(aModelBeginPos.mnPos, 0, true);
    }
    if (m_aCurrentLang != aNewLang)
    {
        m_aCurrentLang = aNewLang;
        moCharClass.reset();
    }
}

namespace
{
// tdf#45271 For Chinese and Japanese, count characters instead of words
sal_Int32
forceEachCJCodePointToWord(const OUString& rText, sal_Int32 nBegin, sal_Int32 nLen,
                           const ModelToViewHelper* pModelToView,
                           const std::function<LanguageType(sal_Int32, sal_Int32, bool)>& fnGetLangOfChar)
{
    if (nLen > 1)
    {
        const uno::Reference<XBreakIterator>& rxBreak = g_pBreakIt->GetBreakIter();

        sal_uInt16 nCurrScript = rxBreak->getScriptType(rText, nBegin);

        sal_Int32 indexUtf16 = nBegin;
        rText.iterateCodePoints(&indexUtf16);

        // First character is Asian
        if (nCurrScript == i18n::ScriptType::ASIAN)
        {
            auto aModelBeginPos = pModelToView->ConvertToModelPosition(nBegin);
            auto aCurrentLang = fnGetLangOfChar(aModelBeginPos.mnPos, nCurrScript, false);

            // tdf#150621 Korean words must be counted as-is
            if (primary(aCurrentLang) == primary(LANGUAGE_KOREAN))
            {
                return nLen;
            }

            // Word is Chinese or Japanese, and must be truncated to a single character
            return indexUtf16 - nBegin;
        }

        // First character was not Asian, consider appearance of any Asian character
        // to be the end of the word
        while (indexUtf16 < nBegin + nLen)
        {
            nCurrScript = rxBreak->getScriptType(rText, indexUtf16);
            if (nCurrScript == i18n::ScriptType::ASIAN)
            {
                auto aModelBeginPos = pModelToView->ConvertToModelPosition(indexUtf16);
                auto aCurrentLang = fnGetLangOfChar(aModelBeginPos.mnPos, nCurrScript, false);

                // tdf#150621 Korean words must be counted as-is.
                // Note that script changes intentionally do not delimit words for counting.
                if (primary(aCurrentLang) == primary(LANGUAGE_KOREAN))
                {
                    return nLen;
                }

                // Word tail contains Chinese or Japanese, and must be truncated
                return indexUtf16 - nBegin;
            }
            rText.iterateCodePoints(&indexUtf16);
        }
    }
    return nLen;
}
}

bool SwScanner::NextWord()
{
    m_nBegin = m_nBegin + m_nLength;
    Boundary aBound;

    while ( true )
    {
        // skip non-letter characters:
        while (m_nBegin < m_aText.getLength())
        {
            if (m_nBegin >= 0 && !u_isspace(m_aText[m_nBegin]))
            {
                if ( !m_pLanguage )
                {
                    const sal_uInt16 nNextScriptType = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, m_nBegin );
                    ModelToViewHelper::ModelPosition aModelBeginPos =
                        m_ModelToView.ConvertToModelPosition( m_nBegin );
                    LanguageType aNewLang = m_pGetLangOfChar(aModelBeginPos.mnPos, nNextScriptType, false);
                    if (aNewLang != m_aCurrentLang)
                    {
                        m_aCurrentLang = aNewLang;
                        moCharClass.reset();
                    }
                }

                if ( m_nWordType != i18n::WordType::WORD_COUNT )
                {
                    if (!moCharClass)
                        moCharClass.emplace(LanguageTag( g_pBreakIt->GetLocale( m_aCurrentLang ) ));
                    if ( moCharClass->isLetterNumeric(OUString(m_aText[m_nBegin])) )
                        break;
                }
                else
                    break;
            }
            ++m_nBegin;
        }

        if ( m_nBegin >= m_aText.getLength() || m_nBegin >= m_nEndPos )
            return false;

        // get the word boundaries
        aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( m_aText, m_nBegin,
                g_pBreakIt->GetLocale( m_aCurrentLang ), m_nWordType, true );
        OSL_ENSURE( aBound.endPos >= aBound.startPos, "broken aBound result" );

        // we don't want to include preceding text
        // to count words in text with mixed script punctuation correctly,
        // but we want to include preceding symbols (eg. percent sign, section sign,
        // degree sign defined by dict_word_hu to spell check their affixed forms).
        if (m_nWordType == i18n::WordType::WORD_COUNT && aBound.startPos < m_nBegin)
            aBound.startPos = m_nBegin;

        //no word boundaries could be found
        if(aBound.endPos == aBound.startPos)
            return false;

        //if a word before is found it has to be searched for the next
        if(aBound.endPos == m_nBegin)
            ++m_nBegin;
        else
            break;
    } // end while( true )

    // #i89042, as discussed with HDU: don't evaluate script changes for word count. Use whole word.
    if ( m_nWordType == i18n::WordType::WORD_COUNT )
    {
        m_nBegin = std::max(aBound.startPos, m_nBegin);
        m_nLength   = 0;
        if (aBound.endPos > m_nBegin)
            m_nLength = aBound.endPos - m_nBegin;
    }
    else
    {
        // we have to differentiate between these cases:
        if ( aBound.startPos <= m_nBegin )
        {
            OSL_ENSURE( aBound.endPos >= m_nBegin, "Unexpected aBound result" );

            // restrict boundaries to script boundaries and nEndPos
            const sal_uInt16 nCurrScript = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, m_nBegin );
            OUString aTmpWord = m_aText.copy( m_nBegin, aBound.endPos - m_nBegin );
            const sal_Int32 nScriptEnd = m_nBegin +
                g_pBreakIt->GetBreakIter()->endOfScript( aTmpWord, 0, nCurrScript );
            const sal_Int32 nEnd = std::min( aBound.endPos, nScriptEnd );

            // restrict word start to last script change position
            sal_Int32 nScriptBegin = 0;
            if ( aBound.startPos < m_nBegin )
            {
                // search from nBegin backwards until the next script change
                aTmpWord = m_aText.copy( aBound.startPos,
                                       m_nBegin - aBound.startPos + 1 );
                nScriptBegin = aBound.startPos +
                    g_pBreakIt->GetBreakIter()->beginOfScript( aTmpWord, m_nBegin - aBound.startPos,
                                                    nCurrScript );
            }

            m_nBegin = std::max( aBound.startPos, nScriptBegin );
            m_nLength = nEnd - m_nBegin;
        }
        else
        {
            const sal_uInt16 nCurrScript = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, aBound.startPos );
            OUString aTmpWord = m_aText.copy( aBound.startPos,
                                             aBound.endPos - aBound.startPos );
            const sal_Int32 nScriptEnd = aBound.startPos +
                g_pBreakIt->GetBreakIter()->endOfScript( aTmpWord, 0, nCurrScript );
            const sal_Int32 nEnd = std::min( aBound.endPos, nScriptEnd );
            m_nBegin = aBound.startPos;
            m_nLength = nEnd - m_nBegin;
        }
    }

    // optionally clip the result of getWordBoundaries:
    if ( m_bClip )
    {
        aBound.startPos = std::max( aBound.startPos, m_nStartPos );
        aBound.endPos = std::min( aBound.endPos, m_nEndPos );
        if (aBound.endPos < aBound.startPos)
        {
            m_nBegin = m_nEndPos;
            m_nLength = 0; // found word is outside of search interval
        }
        else
        {
            m_nBegin = aBound.startPos;
            m_nLength = aBound.endPos - m_nBegin;
        }
    }

    if( ! m_nLength )
        return false;

    if (m_nWordType == i18n::WordType::WORD_COUNT)
    {
        m_nLength = forceEachCJCodePointToWord(m_aText, m_nBegin, m_nLength, &m_ModelToView,
                                               m_pGetLangOfChar);
    }

    m_aPrevWord = m_aWord;
    m_aWord = m_aPreDashReplacementText.copy( m_nBegin, m_nLength );

    return true;
}

// Note: this is a clone of SwTextFrame::AutoSpell_, so keep them in sync when fixing things!
bool SwTextNode::Spell(SwSpellArgs* pArgs, bool bIsReadOnly)
{
    // modify string according to redline information and hidden text
    const OUString aOldText( m_Text );
    OUStringBuffer buf(m_Text);
    const bool bContainsComments = lcl_HasComments(*this);
    const bool bRestoreString =
        lcl_MaskRedlinesAndHiddenText(*this, buf, 0, m_Text.getLength());
    if (bRestoreString)
    {   // ??? UGLY: is it really necessary to modify m_Text here?
        m_Text = buf.makeStringAndClear();
    }

    sal_Int32 nBegin = ( &pArgs->pStartPos->GetNode() != this )
        ? 0
        : pArgs->pStartPos->GetContentIndex();

    sal_Int32 nEnd = ( &pArgs->pEndPos->GetNode() != this )
            ? m_Text.getLength()
            : pArgs->pEndPos->GetContentIndex();

    pArgs->xSpellAlt = nullptr;

    bool bIsEditableSect = false;
    if (bIsReadOnly)
    {
        // Enable spell checking in editable sections in read-only mode.
        if (SwSectionNode* pSectNode = GetTextNode()->FindSectionNode())
        {
            bIsEditableSect = pSectNode->GetSection().IsEditInReadonly();
        }
    }

    // 4 cases:

    // 1. IsWrongDirty = 0 and GetWrong = 0
    //      Everything is checked and correct
    // 2. IsWrongDirty = 0 and GetWrong = 1
    //      Everything is checked and errors are identified in the wrong list
    // 3. IsWrongDirty = 1 and GetWrong = 0
    //      Nothing has been checked
    // 4. IsWrongDirty = 1 and GetWrong = 1
    //      Text has been checked but there is an invalid range in the wrong list

    // Nothing has to be done for case 1.
    if ((IsWrongDirty() || GetWrong()) && (!bIsReadOnly || bIsEditableSect) && m_Text.getLength())
    {
        if (nBegin > m_Text.getLength())
        {
            nBegin = m_Text.getLength();
        }
        if (nEnd > m_Text.getLength())
        {
            nEnd = m_Text.getLength();
        }

        if(!IsWrongDirty())
        {
            const sal_Int32 nTemp = GetWrong()->NextWrong( nBegin );
            if(nTemp > nEnd)
            {
                // reset original text
                if ( bRestoreString )
                {
                    m_Text = aOldText;
                }
                return false;
            }
            if(nTemp > nBegin)
                nBegin = nTemp;

        }

        // In case 2. we pass the wrong list to the scanned, because only
        // the words in the wrong list have to be checked
        SwScanner aScanner( *this, m_Text, nullptr, ModelToViewHelper(),
                            WordType::DICTIONARY_WORD,
                            nBegin, nEnd );
        bool bNextWord = aScanner.NextWord();
        while( !pArgs->xSpellAlt.is() && bNextWord )
        {
            bool bCalledNextWord = false;

            const OUString& rWord = aScanner.GetWord();

            // get next language for next word, consider language attributes
            // within the word
            LanguageType eActLang = aScanner.GetCurrentLanguage();
            DetectAndMarkMissingDictionaries( GetTextNode()->GetDoc(), pArgs->xSpeller, eActLang );

            if( rWord.getLength() > 0 && LANGUAGE_NONE != eActLang &&
                !lcl_IsURL(rWord, *this, aScanner.GetBegin(), aScanner.GetLen() ) )
            {
                if (pArgs->xSpeller.is())
                {
                    SvxSpellWrapper::CheckSpellLang( pArgs->xSpeller, eActLang );
                    pArgs->xSpellAlt = pArgs->xSpeller->spell( rWord, static_cast<sal_uInt16>(eActLang),
                                            Sequence< PropertyValue >() );
                }
                if( pArgs->xSpellAlt.is() )
                {
                    if ( IsSymbolAt(aScanner.GetBegin()) ||
                        // redlines can leave "in word" character within word,
                        // we must remove them before spell checking
                        // to avoid false alarm
                        ( (bRestoreString || bContainsComments) && pArgs->xSpeller->isValid( rWord.replaceAll(OUStringChar(CH_TXTATR_INWORD), ""),
                            static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ) ) )
                    {
                        pArgs->xSpellAlt = nullptr;
                    }
                    else
                    {
                        OUString sPrevWord = aScanner.GetPrevWord();
                        auto nWordBegin = aScanner.GetBegin();
                        auto nWordEnd = aScanner.GetEnd();
                        bNextWord = aScanner.NextWord();
                        const OUString& rActualWord = aScanner.GetPrevWord();
                        bCalledNextWord = true;
                        // check space separated word pairs in the dictionary, e.g. "vice versa"
                        if ( !((bNextWord && !linguistic::HasDigits(aScanner.GetWord()) &&
                            pArgs->xSpeller->isValid( rActualWord + " " + aScanner.GetWord(),
                                static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() )) ||
                           ( !sPrevWord.isEmpty() && !linguistic::HasDigits(sPrevWord) &&
                            pArgs->xSpeller->isValid( sPrevWord + " " + rActualWord,
                                static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ))) )
                        {
                            // make sure the selection build later from the data
                            // below does not include "in word" character to the
                            // left and right in order to preserve those. Therefore
                            // count those "in words" in order to modify the
                            // selection accordingly.
                            const sal_Unicode* pChar = aScanner.GetPrevWord().getStr();
                            sal_Int32 nLeft = 0;
                            while (*pChar++ == CH_TXTATR_INWORD)
                                ++nLeft;
                            pChar = rActualWord.getLength() ? rActualWord.getStr() + rActualWord.getLength() - 1 : nullptr;
                            sal_Int32 nRight = 0;
                            while (pChar && *pChar-- == CH_TXTATR_INWORD)
                                ++nRight;

                            pArgs->pStartPos->Assign(*this, nWordEnd - nRight );
                            pArgs->pEndPos->Assign(*this, nWordBegin + nLeft );
                        }
                        else
                        {
                            pArgs->xSpellAlt = nullptr;
                        }
                    }
                }
            }

            if ( !bCalledNextWord )
                bNextWord = aScanner.NextWord();
        }
    }

    // reset original text
    if ( bRestoreString )
    {
        m_Text = aOldText;
    }

    return pArgs->xSpellAlt.is();
}

void SwTextNode::SetLanguageAndFont( const SwPaM &rPaM,
    LanguageType nLang, sal_uInt16 nLangWhichId,
    const vcl::Font *pFont,  sal_uInt16 nFontWhichId )
{
    SwEditShell *pEditShell = GetDoc().GetEditShell();
    if (!pEditShell)
        return;
    SfxItemSet aSet(pEditShell->GetAttrPool(), nLangWhichId, nLangWhichId );
    if (pFont)
        aSet.MergeRange(nFontWhichId, nFontWhichId); // Keep it sorted
    aSet.Put( SvxLanguageItem( nLang, nLangWhichId ) );

    OSL_ENSURE( pFont, "target font missing?" );
    if (pFont)
    {
        SvxFontItem aFontItem = static_cast<const SvxFontItem&>( aSet.Get( nFontWhichId ) );
        aFontItem.SetFamilyName(   pFont->GetFamilyName());
        aFontItem.SetFamily(       pFont->GetFamilyType());
        aFontItem.SetStyleName(    pFont->GetStyleName());
        aFontItem.SetPitch(        pFont->GetPitch());
        aFontItem.SetCharSet( pFont->GetCharSet() );
        aSet.Put( aFontItem );
    }

    GetDoc().getIDocumentContentOperations().InsertItemSet( rPaM, aSet );
    // SetAttr( aSet );    <- Does not set language attribute of empty paragraphs correctly,
    //                     <- because since there is no selection the flag to garbage
    //                     <- collect all attributes is set, and therefore attributes spanned
    //                     <- over empty selection are removed.

}

bool SwTextNode::Convert( SwConversionArgs &rArgs )
{
    // get range of text within node to be converted
    // (either all the text or the text within the selection
    // when the conversion was started)
    const sal_Int32 nTextBegin = ( &rArgs.pStartPos->GetNode() == this )
        ? std::min(rArgs.pStartPos->GetContentIndex(), m_Text.getLength())
        : 0;

    const sal_Int32 nTextEnd = ( &rArgs.pEndPos->GetNode() == this )
        ?  std::min(rArgs.pEndPos->GetContentIndex(), m_Text.getLength())
        :  m_Text.getLength();

    rArgs.aConvText.clear();

    // modify string according to redline information and hidden text
    const OUString aOldText( m_Text );
    OUStringBuffer buf(m_Text);
    const bool bRestoreString =
        lcl_MaskRedlinesAndHiddenText(*this, buf, 0, m_Text.getLength());
    if (bRestoreString)
    {   // ??? UGLY: is it really necessary to modify m_Text here?
        m_Text = buf.makeStringAndClear();
    }

    bool    bFound  = false;
    sal_Int32  nBegin  = nTextBegin;
    sal_Int32  nLen = 0;
    LanguageType nLangFound = LANGUAGE_NONE;
    if (m_Text.isEmpty())
    {
        if (rArgs.bAllowImplicitChangesForNotConvertibleText)
        {
            // create SwPaM with mark & point spanning empty paragraph
            //SwPaM aCurPaM( *this, *this, nBegin, nBegin + nLen ); <-- wrong c-tor, does sth different
            SwPaM aCurPaM( *this, 0 );

            SetLanguageAndFont( aCurPaM,
                    rArgs.nConvTargetLang, RES_CHRATR_CJK_LANGUAGE,
                    rArgs.pTargetFont, RES_CHRATR_CJK_FONT );
        }
    }
    else
    {
        SwLanguageIterator aIter( *this, nBegin );

        // Implicit changes require setting new attributes, which in turn destroys
        // the attribute sequence on that aIter iterates. We store the necessary
        // coordinates and apply those changes after iterating through the text.
        typedef std::pair<sal_Int32, sal_Int32> ImplicitChangesRange;
        std::vector<ImplicitChangesRange> aImplicitChanges;

        // find non zero length text portion of appropriate language
        do {
            nLangFound = aIter.GetLanguage();
            bool bLangOk =  (nLangFound == rArgs.nConvSrcLang) ||
                                (editeng::HangulHanjaConversion::IsChinese( nLangFound ) &&
                                 editeng::HangulHanjaConversion::IsChinese( rArgs.nConvSrcLang ));

            sal_Int32 nChPos = aIter.GetChgPos();
            // the position at the end of the paragraph is COMPLETE_STRING and
            // thus must be cut to the end of the actual string.
            assert(nChPos != -1);
            if (nChPos == -1 || nChPos == COMPLETE_STRING)
            {
                nChPos = m_Text.getLength();
            }

            nLen = nChPos - nBegin;
            bFound = bLangOk && nLen > 0;
            if (!bFound)
            {
                // create SwPaM with mark & point spanning the attributed text
                //SwPaM aCurPaM( *this, *this, nBegin, nBegin + nLen ); <-- wrong c-tor, does sth different
                SwPaM aCurPaM( *this, nBegin );
                aCurPaM.SetMark();
                aCurPaM.GetPoint()->SetContent(nBegin + nLen);

                // check script type of selected text
                if (SwEditShell *pEditShell = GetDoc().GetEditShell())
                {
                    pEditShell->Push();             // save current cursor on stack
                    pEditShell->SetSelection( aCurPaM );
                    bool bIsAsianScript = (SvtScriptType::ASIAN == pEditShell->GetScriptType());
                    pEditShell->Pop(SwCursorShell::PopMode::DeleteCurrent); // restore cursor from stack

                    if (!bIsAsianScript && rArgs.bAllowImplicitChangesForNotConvertibleText)
                    {
                        // Store for later use
                        aImplicitChanges.emplace_back(nBegin, nBegin+nLen);
                    }
                }
                nBegin = nChPos;    // start of next language portion
            }
        } while (!bFound && aIter.Next());  /* loop while nothing was found and still sth is left to be searched */

        // Apply implicit changes, if any, now that aIter is no longer used
        for (const auto& rImplicitChange : aImplicitChanges)
        {
            SwPaM aPaM( *this, rImplicitChange.first );
            aPaM.SetMark();
            aPaM.GetPoint()->SetContent( rImplicitChange.second );
            SetLanguageAndFont( aPaM, rArgs.nConvTargetLang, RES_CHRATR_CJK_LANGUAGE, rArgs.pTargetFont, RES_CHRATR_CJK_FONT );
        }

    }

    // keep resulting text within selection / range of text to be converted
    if (nBegin < nTextBegin)
        nBegin = nTextBegin;
    if (nBegin + nLen > nTextEnd)
        nLen = nTextEnd - nBegin;
    bool bInSelection = nBegin < nTextEnd;

    if (bFound && bInSelection)     // convertible text found within selection/range?
    {
        OSL_ENSURE( !m_Text.isEmpty(), "convertible text portion missing!" );
        rArgs.aConvText     = m_Text.copy(nBegin, nLen);
        rArgs.nConvTextLang = nLangFound;

        // position where to start looking in next iteration (after current ends)
        rArgs.pStartPos->Assign(*this, nBegin + nLen );
        // end position (when we have travelled over the whole document)
        rArgs.pEndPos->Assign(*this, nBegin );
    }

    // restore original text
    if ( bRestoreString )
    {
        m_Text = aOldText;
    }

    return !rArgs.aConvText.isEmpty();
}

// Note: this is a clone of SwTextNode::Spell, so keep them in sync when fixing things!
SwRect SwTextFrame::AutoSpell_(SwTextNode & rNode, sal_Int32 nActPos)
{
    SwRect aRect;
    assert(sw::FrameContainsNode(*this, rNode.GetIndex()));
    SwTextNode *const pNode(&rNode);
    if (!nActPos)
        nActPos = COMPLETE_STRING;

    SwAutoCompleteWord& rACW = SwDoc::GetAutoCompleteWords();

    // modify string according to redline information and hidden text
    const OUString aOldText( pNode->GetText() );
    OUStringBuffer buf(pNode->m_Text);
    const bool bContainsComments = lcl_HasComments(rNode);
    const bool bRestoreString =
        lcl_MaskRedlinesAndHiddenText(*pNode, buf, 0, pNode->GetText().getLength());
    if (bRestoreString)
    {   // ??? UGLY: is it really necessary to modify m_Text here? just for GetLang()?
        pNode->m_Text = buf.makeStringAndClear();
    }

    // a change of data indicates that at least one word has been modified

    sal_Int32 nBegin = 0;
    sal_Int32 nEnd = pNode->GetText().getLength();
    sal_Int32 nInsertPos = 0;
    sal_Int32 nChgStart = COMPLETE_STRING;
    sal_Int32 nChgEnd = 0;
    sal_Int32 nInvStart = COMPLETE_STRING;
    sal_Int32 nInvEnd = 0;

    const bool bAddAutoCmpl = pNode->IsAutoCompleteWordDirty() &&
                                  SwViewOption::IsAutoCompleteWords();

    if( pNode->GetWrong() )
    {
        nBegin = pNode->GetWrong()->GetBeginInv();
        if( COMPLETE_STRING != nBegin )
        {
            nEnd = std::max(pNode->GetWrong()->GetEndInv(), pNode->GetText().getLength());
        }

        // get word around nBegin, we start at nBegin - 1
        if ( COMPLETE_STRING != nBegin )
        {
            if ( nBegin )
                --nBegin;

            LanguageType eActLang = pNode->GetLang( nBegin );
            Boundary aBound =
                g_pBreakIt->GetBreakIter()->getWordBoundary( pNode->GetText(), nBegin,
                    g_pBreakIt->GetLocale( eActLang ),
                    WordType::DICTIONARY_WORD, true );
            nBegin = aBound.startPos;
        }

        // get the position in the wrong list
        nInsertPos = pNode->GetWrong()->GetWrongPos( nBegin );

        // sometimes we have to skip one entry
        if( nInsertPos < pNode->GetWrong()->Count() &&
            nBegin == pNode->GetWrong()->Pos( nInsertPos ) +
                      pNode->GetWrong()->Len( nInsertPos ) )
                nInsertPos++;
    }

    bool bFresh = nBegin < nEnd;
    bool bPending(false);

    if( bFresh )
    {
        uno::Reference< XSpellChecker1 > xSpell( ::GetSpellChecker() );
        SwDoc& rDoc = pNode->GetDoc();

        SwScanner aScanner( *pNode, pNode->GetText(), nullptr, ModelToViewHelper(),
                            WordType::DICTIONARY_WORD, nBegin, nEnd);

        bool bNextWord = aScanner.NextWord();
        while( bNextWord )
        {
            const OUString& rWord = aScanner.GetWord();
            nBegin = aScanner.GetBegin();
            sal_Int32 nLen = aScanner.GetLen();
            bool bCalledNextWord = false;

            // get next language for next word, consider language attributes
            // within the word
            LanguageType eActLang = aScanner.GetCurrentLanguage();
            DetectAndMarkMissingDictionaries( rDoc, xSpell, eActLang );

            bool bSpell = xSpell.is() && xSpell->hasLanguage( static_cast<sal_uInt16>(eActLang) );
            if( bSpell && !rWord.isEmpty() && !lcl_IsURL(rWord, *pNode, nBegin, nLen) )
            {
                // check for: bAlter => xHyphWord.is()
                OSL_ENSURE(!bSpell || xSpell.is(), "NULL pointer");
                if( !xSpell->isValid( rWord, static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ) &&
                    // redlines can leave "in word" character within word,
                    // we must remove them before spell checking
                    // to avoid false alarm
                    ((!bRestoreString && !bContainsComments) || !xSpell->isValid( rWord.replaceAll(OUStringChar(CH_TXTATR_INWORD), ""),
                            static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ) ) )
                {
                    OUString sPrevWord = aScanner.GetPrevWord();
                    bNextWord = aScanner.NextWord();
                    bCalledNextWord = true;
                    // check space separated word pairs in the dictionary, e.g. "vice versa"
                    if ( !((bNextWord && !linguistic::HasDigits(aScanner.GetWord()) &&
                            xSpell->isValid( aScanner.GetPrevWord() + " " + aScanner.GetWord(),
                                static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() )) ||
                           (!sPrevWord.isEmpty() && !linguistic::HasDigits(sPrevWord) &&
                            xSpell->isValid( sPrevWord + " " + aScanner.GetPrevWord(),
                                static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ))) )
                    {
                        sal_Int32 nSmartTagStt = nBegin;
                        sal_Int32 nDummy = 1;
                        if ( !pNode->GetSmartTags() || !pNode->GetSmartTags()->InWrongWord( nSmartTagStt, nDummy ) )
                        {
                            if( !pNode->GetWrong() )
                            {
                                pNode->SetWrong( std::make_unique<SwWrongList>( WRONGLIST_SPELL ) );
                                pNode->GetWrong()->SetInvalid( 0, nEnd );
                            }
                            SwWrongList::FreshState const eState(pNode->GetWrong()->Fresh(
                                nChgStart, nChgEnd, nBegin, nLen, nInsertPos, nActPos));
                            switch (eState)
                            {
                                case SwWrongList::FreshState::FRESH:
                                    pNode->GetWrong()->Insert(OUString(), nullptr, nBegin, nLen, nInsertPos++);
                                    break;
                                case SwWrongList::FreshState::CURSOR:
                                    bPending = true;
                                    [[fallthrough]]; // to mark as invalid
                                case SwWrongList::FreshState::NOTHING:
                                    nInvStart = nBegin;
                                    nInvEnd = nBegin + nLen;
                                    break;
                            }
                        }
                    }
                    else if( bAddAutoCmpl && rACW.GetMinWordLen() <= aScanner.GetPrevWord().getLength() )
                    {
                        // tdf#119695 only add the word if the cursor position is outside the word
                        // so that the incomplete words are not added as autocomplete candidates
                        bool bCursorOutsideWord = nActPos > nBegin + nLen || nActPos < nBegin;
                        if (bCursorOutsideWord)
                            rACW.InsertWord(aScanner.GetPrevWord(), rDoc);
                    }
                }
                else if( bAddAutoCmpl && rACW.GetMinWordLen() <= rWord.getLength() )
                {
                    // tdf#119695 only add the word if the cursor position is outside the word
                    // so that the incomplete words are not added as autocomplete candidates
                    bool bCursorOutsideWord = nActPos > nBegin + nLen || nActPos < nBegin;
                    if (bCursorOutsideWord)
                        rACW.InsertWord(rWord, rDoc);
                }
            }

            if ( !bCalledNextWord )
                 bNextWord = aScanner.NextWord();
        }
    }

    // reset original text
    // i63141 before calling GetCharRect(..) with formatting!
    if ( bRestoreString )
    {
        pNode->m_Text = aOldText;
    }
    if( pNode->GetWrong() )
    {
        if( bFresh )
            pNode->GetWrong()->Fresh( nChgStart, nChgEnd,
                                      nEnd, 0, nInsertPos, nActPos );

        // Calculate repaint area:

        if( nChgStart < nChgEnd )
        {
            aRect = lcl_CalculateRepaintRect(*this, rNode, nChgStart, nChgEnd);

            // fdo#71558 notify misspelled word to accessibility
#if !ENABLE_WASM_STRIP_ACCESSIBILITY
            SwViewShell* pViewSh = getRootFrame() ? getRootFrame()->GetCurrShell() : nullptr;
            if( pViewSh )
                pViewSh->InvalidateAccessibleParaAttrs( *this );
#endif
        }

        pNode->GetWrong()->SetInvalid( nInvStart, nInvEnd );
        pNode->SetWrongDirty(
            (COMPLETE_STRING != pNode->GetWrong()->GetBeginInv())
                ? (bPending
                    ? sw::WrongState::PENDING
                    : sw::WrongState::TODO)
                : sw::WrongState::DONE);
        if( !pNode->GetWrong()->Count() && ! pNode->IsWrongDirty() )
            pNode->ClearWrong();

        if (bPending && getRootFrame())
        {
            if (SwViewShell* pViewSh = getRootFrame()->GetCurrShell())
            {
                pViewSh->OnSpellWrongStatePending();
            }
        }
    }
    else
        pNode->SetWrongDirty(sw::WrongState::DONE);

    if( bAddAutoCmpl )
        pNode->SetAutoCompleteWordDirty( false );

    return aRect;
}

/** Function: SmartTagScan

    Function scans words in current text and checks them in the
    smarttag libraries. If the check returns true to bounds of the
    recognized words are stored into a list that is used later for drawing
    the underline.

    @return SwRect Repaint area
*/

SwRect SwTextFrame::SmartTagScan(SwTextNode & rNode)
{
    SwRect aRet;

    assert(sw::FrameContainsNode(*this, rNode.GetIndex()));
    SwTextNode *const pNode = &rNode;
    const OUString& rText = pNode->GetText();

    // Iterate over language portions
    SmartTagMgr& rSmartTagMgr = SwSmartTagMgr::Get();

    SwWrongList* pSmartTagList = pNode->GetSmartTags();

    sal_Int32 nBegin = 0;
    sal_Int32 nEnd = rText.getLength();

    if ( pSmartTagList )
    {
        if ( pSmartTagList->GetBeginInv() != COMPLETE_STRING )
        {
            nBegin = pSmartTagList->GetBeginInv();
            nEnd = std::min( pSmartTagList->GetEndInv(), rText.getLength() );

            if ( nBegin < nEnd )
            {
                const LanguageType aCurrLang = pNode->GetLang( nBegin );
                const css::lang::Locale aCurrLocale = g_pBreakIt->GetLocale( aCurrLang );
                nBegin = g_pBreakIt->GetBreakIter()->beginOfSentence( rText, nBegin, aCurrLocale );
                nEnd = g_pBreakIt->GetBreakIter()->endOfSentence(rText, nEnd, aCurrLocale);
                if (nEnd > rText.getLength() || nEnd < 0)
                    nEnd = rText.getLength();
            }
        }
    }

    const sal_uInt16 nNumberOfEntries = pSmartTagList ? pSmartTagList->Count() : 0;
    sal_uInt16 nNumberOfRemovedEntries = 0;
    sal_uInt16 nNumberOfInsertedEntries = 0;

    // clear smart tag list between nBegin and nEnd:
    if ( 0 != nNumberOfEntries )
    {
        sal_Int32 nChgStart = COMPLETE_STRING;
        sal_Int32 nChgEnd = 0;
        const sal_uInt16 nCurrentIndex = pSmartTagList->GetWrongPos( nBegin );
        pSmartTagList->Fresh( nChgStart, nChgEnd, nBegin, nEnd - nBegin, nCurrentIndex, COMPLETE_STRING );
        nNumberOfRemovedEntries = nNumberOfEntries - pSmartTagList->Count();
    }

    if ( nBegin < nEnd )
    {
        // Expand the string:
        const ModelToViewHelper aConversionMap(*pNode, getRootFrame() /*TODO - replace or expand fields for smart tags?*/);
        const OUString& aExpandText = aConversionMap.getViewText();

        // Ownership ov ConversionMap is passed to SwXTextMarkup object!
        uno::Reference<text::XTextMarkup> const xTextMarkup =
             new SwXTextMarkup(pNode, aConversionMap);

        css::uno::Reference< css::frame::XController > xController = pNode->GetDoc().GetDocShell()->GetController();

        SwPosition start(*pNode, nBegin);
        SwPosition end  (*pNode, nEnd);
        rtl::Reference<SwXTextRange> xRange = SwXTextRange::CreateXTextRange(pNode->GetDoc(), start, &end);

        rSmartTagMgr.RecognizeTextRange(xRange, xTextMarkup, xController);

        sal_Int32 nLangBegin = nBegin;
        sal_Int32 nLangEnd;

        // smart tag recognition has to be done for each language portion:
        SwLanguageIterator aIter( *pNode, nLangBegin );

        do
        {
            const LanguageType nLang = aIter.GetLanguage();
            const css::lang::Locale aLocale = g_pBreakIt->GetLocale( nLang );
            nLangEnd = std::min<sal_Int32>( nEnd, aIter.GetChgPos() );

            const sal_Int32 nExpandBegin = aConversionMap.ConvertToViewPosition( nLangBegin );
            const sal_Int32 nExpandEnd   = aConversionMap.ConvertToViewPosition( nLangEnd );

            rSmartTagMgr.RecognizeString(aExpandText, xTextMarkup, xController, aLocale, nExpandBegin, nExpandEnd - nExpandBegin );

            nLangBegin = nLangEnd;
        }
        while ( aIter.Next() && nLangEnd < nEnd );

        pSmartTagList = pNode->GetSmartTags();

        const sal_uInt16 nNumberOfEntriesAfterRecognize = pSmartTagList ? pSmartTagList->Count() : 0;
        nNumberOfInsertedEntries = nNumberOfEntriesAfterRecognize - ( nNumberOfEntries - nNumberOfRemovedEntries );
    }

    if( pSmartTagList )
    {
        // Update WrongList stuff
        pSmartTagList->SetInvalid( COMPLETE_STRING, 0 );
        pNode->SetSmartTagDirty( COMPLETE_STRING != pSmartTagList->GetBeginInv() );

        if( !pSmartTagList->Count() && !pNode->IsSmartTagDirty() )
            pNode->ClearSmartTags();

        // Calculate repaint area:
        if ( nBegin < nEnd && ( 0 != nNumberOfRemovedEntries ||
                                0 != nNumberOfInsertedEntries ) )
        {
            aRet = lcl_CalculateRepaintRect(*this, rNode, nBegin, nEnd);
        }
    }
    else
        pNode->SetSmartTagDirty( false );

    return aRet;
}

void SwTextFrame::CollectAutoCmplWrds(SwTextNode & rNode, sal_Int32 nActPos)
{
    assert(sw::FrameContainsNode(*this, rNode.GetIndex())); (voidthis;
    SwTextNode *const pNode(&rNode);
    if (!nActPos)
        nActPos = COMPLETE_STRING;

    SwDoc& rDoc = pNode->GetDoc();
    SwAutoCompleteWord& rACW = SwDoc::GetAutoCompleteWords();

    sal_Int32  nBegin = 0;
    sal_Int32  nEnd = pNode->GetText().getLength();
    sal_Int32  nLen;
    bool bACWDirty = false;

    if( nBegin < nEnd )
    {
        int nCnt = 200;
        SwScanner aScanner( *pNode, pNode->GetText(), nullptr, ModelToViewHelper(),
                            WordType::DICTIONARY_WORD, nBegin, nEnd );
        while( aScanner.NextWord() )
        {
            nBegin = aScanner.GetBegin();
            nLen = aScanner.GetLen();
            if( rACW.GetMinWordLen() <= nLen )
            {
                const OUString& rWord = aScanner.GetWord();

                if( nActPos < nBegin || ( nBegin + nLen ) < nActPos )
                {
                    if( rACW.GetMinWordLen() <= rWord.getLength() )
                        rACW.InsertWord( rWord, rDoc );
                }
                else
                    bACWDirty = true;
            }
            if( !--nCnt )
            {
                // don't wait for TIMER here, so we can finish big paragraphs
                if (Application::AnyInput(VCL_INPUT_ANY & VclInputFlags(~VclInputFlags::TIMER)))
                    return;
                nCnt = 100;
            }
        }
    }

    if (!bACWDirty)
        pNode->SetAutoCompleteWordDirty( false );
}

SwInterHyphInfoTextFrame::SwInterHyphInfoTextFrame(
        SwTextFrame const& rFrame, SwTextNode const& rNode,
        SwInterHyphInfo const& rHyphInfo)
    : m_nStart(rFrame.MapModelToView(&rNode, rHyphInfo.m_nStart))
    , m_nEnd(rFrame.MapModelToView(&rNode, rHyphInfo.m_nEnd))
    , m_nWordStart(0)
    , m_nWordLen(0)
{
}

void SwInterHyphInfoTextFrame::UpdateTextNodeHyphInfo(SwTextFrame const& rFrame,
        SwTextNode const& rNode, SwInterHyphInfo & o_rHyphInfo)
{
    std::pair<SwTextNode const*, sal_Int32> const wordStart(rFrame.MapViewToModel(m_nWordStart));
    std::pair<SwTextNode const*, sal_Int32> const wordEnd(rFrame.MapViewToModel(m_nWordStart+m_nWordLen));
    if (wordStart.first != &rNode || wordEnd.first != &rNode)
    {   // not sure if this can happen since nStart/nEnd are in rNode
        SAL_WARN("sw.core""UpdateTextNodeHyphInfo: outside of node");
        return;
    }
    o_rHyphInfo.m_nWordStart = wordStart.second;
    o_rHyphInfo.m_nWordLen = wordEnd.second - wordStart.second;
    o_rHyphInfo.SetHyphWord(m_xHyphWord);
}

/// Find the SwTextFrame and call its Hyphenate
bool SwTextNode::Hyphenate( SwInterHyphInfo &rHyphInf )
{
    // shortcut: paragraph doesn't have a language set:
    if ( LANGUAGE_NONE == GetSwAttrSet().GetLanguage().GetLanguage()
         && LanguageType(USHRT_MAX) == GetLang(0, m_Text.getLength()))
    {
        return false;
    }

    SwTextFrame *pFrame = ::sw::SwHyphIterCacheLastTextFrame(this,
        [&rHyphInf, this]() {
            std::pair<Point, bool> tmp;
            Point const*const pPoint = rHyphInf.GetCursorPos();
            if (pPoint)
            {
                tmp.first = *pPoint;
                tmp.second = true;
            }
            return static_cast<SwTextFrame*>(this->getLayoutFrame(
                this->GetDoc().getIDocumentLayoutAccess().GetCurrentLayout(),
                nullptr, pPoint ? &tmp : nullptr));
        });
    if (!pFrame)
    {
        // There was a comment here that claimed that the following assertion
        // shouldn't exist as it's triggered by "Trennung ueber Sonderbereiche",
        // (hyphenation across special sections?), whatever that means.
        OSL_ENSURE( pFrame, "!SwTextNode::Hyphenate: can't find any frame" );
        return false;
    }
    SwInterHyphInfoTextFrame aHyphInfo(*pFrame, *this, rHyphInf);

    pFrame = &(pFrame->GetFrameAtOfst( aHyphInfo.m_nStart ));

    while( pFrame )
    {
        if (pFrame->Hyphenate(aHyphInfo))
        {
            // The layout is not robust wrt. "direct formatting"
            // cf. layact.cxx, SwLayAction::TurboAction_(), if( !pCnt->IsValid() ...
            pFrame->SetCompletePaint();
            aHyphInfo.UpdateTextNodeHyphInfo(*pFrame, *this, rHyphInf);
            return true;
        }
        pFrame = pFrame->GetFollow();
        if( pFrame )
        {
            aHyphInfo.m_nEnd = aHyphInfo.m_nEnd - (pFrame->GetOffset() - aHyphInfo.m_nStart);
            aHyphInfo.m_nStart = pFrame->GetOffset();
        }
    }
    return false;
}

namespace
{
    struct swTransliterationChgData
    {
        sal_Int32               nStart;
        sal_Int32               nLen;
        OUString                sChanged;
        Sequence< sal_Int32 >   aOffsets;
    };
}

// change text to Upper/Lower/Hiragana/Katakana/...
void SwTextNode::TransliterateText(
    utl::TransliterationWrapper& rTrans,
    sal_Int32 nStt, sal_Int32 nEnd,
    SwUndoTransliterate* pUndo, bool bUseRedlining )
{
    if (nStt >= nEnd)
        return;

    const sal_Int32 selStart = nStt;
    const sal_Int32 selEnd = nEnd;

    // since we don't use Hiragana/Katakana or half-width/full-width transliterations here
    // it is fine to use ANYWORD_IGNOREWHITESPACES. (ANY_WORD btw is broken and will
    // occasionally miss words in consecutive sentences). Also with ANYWORD_IGNOREWHITESPACES
    // text like 'just-in-time' will be converted to 'Just-In-Time' which seems to be the
    // proper thing to do.
    const sal_Int16 nWordType = WordType::ANYWORD_IGNOREWHITESPACES;

    // In order to have less trouble with changing text size, e.g. because
    // of ligatures or German small sz being resolved, we need to process
    // the text replacements from end to start.
    // This way the offsets for the yet to be changed words will be
    // left unchanged by the already replaced text.
    // For this we temporarily save the changes to be done in this vector
    std::vector< swTransliterationChgData >   aChanges;
    swTransliterationChgData                  aChgData;

    if (rTrans.getType() == TransliterationFlags::TITLE_CASE)
    {
        // for 'capitalize every word' we need to iterate over each word

        Boundary aSttBndry;
        Boundary aEndBndry;
        aSttBndry = g_pBreakIt->GetBreakIter()->getWordBoundary(
                    GetText(), nStt,
                    g_pBreakIt->GetLocale( GetLang( nStt ) ),
                    nWordType,
                    true /*prefer forward direction*/);
        aEndBndry = g_pBreakIt->GetBreakIter()->getWordBoundary(
                    GetText(), nEnd,
                    g_pBreakIt->GetLocale( GetLang( nEnd ) ),
                    nWordType,
                    false /*prefer backward direction*/);

        // prevent backtracking to the previous word if selection is at word boundary
        if (aSttBndry.endPos <= nStt)
        {
            aSttBndry = g_pBreakIt->GetBreakIter()->nextWord(
                    GetText(), aSttBndry.endPos,
                    g_pBreakIt->GetLocale( GetLang( aSttBndry.endPos ) ),
                    nWordType);
        }
        // prevent advancing to the next word if selection is at word boundary
        if (aEndBndry.startPos >= nEnd)
        {
            aEndBndry = g_pBreakIt->GetBreakIter()->previousWord(
                    GetText(), aEndBndry.startPos,
                    g_pBreakIt->GetLocale( GetLang( aEndBndry.startPos ) ),
                    nWordType);
        }

        /* Nothing to do if user selection lies entirely outside of word start and end boundary computed above.
         * Skip this node, because otherwise the below logic for constraining to the selection will fail */

        if (aSttBndry.startPos >= selEnd || aEndBndry.endPos <= selStart) {
            return;
        }

        // prevent going outside of the user's selection, which may
        // start in the middle of a word
        aSttBndry.startPos = std::max(aSttBndry.startPos, selStart);
        aEndBndry.startPos = std::max(aSttBndry.startPos, aEndBndry.startPos);

        Boundary aCurWordBndry( aSttBndry );
        while (aCurWordBndry.startPos <= aEndBndry.startPos)
        {
            nStt = aCurWordBndry.startPos;
            nEnd = aCurWordBndry.endPos;
            const sal_Int32 nLen = nEnd - nStt;
            OSL_ENSURE( nLen > 0, "invalid word length of 0" );

            Sequence <sal_Int32> aOffsets;
            OUString const sChgd( rTrans.transliterate(
                        GetText(), GetLang(nStt), nStt, nLen, &aOffsets) );

            assert(nStt < m_Text.getLength());
            if (0 != rtl_ustr_shortenedCompare_WithLength(
                        m_Text.getStr() + nStt, m_Text.getLength() - nStt,
                        sChgd.getStr(), sChgd.getLength(), nLen))
            {
                aChgData.nStart     = nStt;
                aChgData.nLen       = nLen;
                aChgData.sChanged   = sChgd;
                aChgData.aOffsets   = std::move(aOffsets);
                aChanges.push_back( aChgData );
            }

            aCurWordBndry = g_pBreakIt->GetBreakIter()->nextWord(
                    GetText(), nStt,
                    g_pBreakIt->GetLocale(GetLang(nStt, 1)),
                    nWordType);
        }
    }
    else if (rTrans.getType() == TransliterationFlags::SENTENCE_CASE)
    {
        // For 'sentence case' we need to iterate sentence by sentence.
        // nLastStart and nLastEnd are the boundaries of the last sentence in
        // the user's selection.
        sal_Int32 nLastStart = g_pBreakIt->GetBreakIter()->beginOfSentence(
                GetText(), nEnd,
                g_pBreakIt->GetLocale( GetLang( nEnd ) ) );
        sal_Int32 nLastEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
                GetText(), nLastStart,
                g_pBreakIt->GetLocale( GetLang( nLastStart ) ) );

        // Begin with the starting point of the user's selection (it may not be
        // the beginning of a sentence)...
        sal_Int32 nCurrentStart = nStt;
        // ...And extend to the end of the first sentence
        sal_Int32 nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
                GetText(), nCurrentStart,
                g_pBreakIt->GetLocale( GetLang( nCurrentStart ) ) );

        // prevent backtracking to the previous sentence if selection starts at end of a sentence
        if (nCurrentEnd <= nStt)
        {
            // now nCurrentStart is probably located on a non-letter word. (unless we
            // are in Asian text with no spaces...)
            // Thus to get the real sentence start we should locate the next real word,
            // that is one found by DICTIONARY_WORD
            i18n::Boundary aBndry = g_pBreakIt->GetBreakIter()->nextWord(
                    GetText(), nCurrentEnd,
                    g_pBreakIt->GetLocale( GetLang( nCurrentEnd ) ),
                    i18n::WordType::DICTIONARY_WORD);

            // now get new current sentence boundaries
            nCurrentStart = g_pBreakIt->GetBreakIter()->beginOfSentence(
                    GetText(), aBndry.startPos,
                    g_pBreakIt->GetLocale( GetLang( aBndry.startPos) ) );
            nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
                    GetText(), nCurrentStart,
                    g_pBreakIt->GetLocale( GetLang( nCurrentStart) ) );
        }
        // prevent advancing to the next sentence if selection ends at start of a sentence
        if (nLastStart >= nEnd)
        {
            // now nCurrentStart is probably located on a non-letter word. (unless we
            // are in Asian text with no spaces...)
            // Thus to get the real sentence start we should locate the previous real word,
            // that is one found by DICTIONARY_WORD
            i18n::Boundary aBndry = g_pBreakIt->GetBreakIter()->previousWord(
                    GetText(), nLastStart,
                    g_pBreakIt->GetLocale( GetLang( nLastStart) ),
                    i18n::WordType::DICTIONARY_WORD);
            nLastEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
                    GetText(), aBndry.startPos,
                    g_pBreakIt->GetLocale( GetLang( aBndry.startPos) ) );
            if (nCurrentEnd > nLastEnd)
                nCurrentEnd = nLastEnd;
        }

        // Prevent going outside of the user's selection
        nCurrentStart = std::max(selStart, nCurrentStart);
        nCurrentEnd = std::min(selEnd, nCurrentEnd);
        nLastEnd = std::min(selEnd, nLastEnd);

        while (nCurrentStart < nLastEnd)
        {
            sal_Int32 nLen = nCurrentEnd - nCurrentStart;
            OSL_ENSURE( nLen > 0, "invalid word length of 0" );

            Sequence <sal_Int32> aOffsets;
            OUString const sChgd( rTrans.transliterate(GetText(),
                GetLang(nCurrentStart), nCurrentStart, nLen, &aOffsets) );

            assert(nStt < m_Text.getLength());
            if (0 != rtl_ustr_shortenedCompare_WithLength(
                        m_Text.getStr() + nStt, m_Text.getLength() - nStt,
                        sChgd.getStr(), sChgd.getLength(), nLen))
            {
                aChgData.nStart     = nCurrentStart;
                aChgData.nLen       = nLen;
                aChgData.sChanged   = sChgd;
                aChgData.aOffsets   = std::move(aOffsets);
                aChanges.push_back( aChgData );
            }

            Boundary aFirstWordBndry = g_pBreakIt->GetBreakIter()->nextWord(
                    GetText(), nCurrentEnd,
                    g_pBreakIt->GetLocale( GetLang( nCurrentEnd ) ),
                    nWordType);
            nCurrentStart = aFirstWordBndry.startPos;
            nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
                    GetText(), nCurrentStart,
                    g_pBreakIt->GetLocale( GetLang( nCurrentStart ) ) );
        }
    }
    else
    {
        // here we may transliterate over complete language portions...

        std::unique_ptr<SwLanguageIterator> pIter;
        if( rTrans.needLanguageForTheMode() )
            pIter.reset(new SwLanguageIterator( *this, nStt ));

        sal_Int32 nEndPos = 0;
        LanguageType nLang = LANGUAGE_NONE;
        sal_Int32 nLoopControlRuns = 0;
        do {
            if( pIter )
            {
                nLang = pIter->GetLanguage();
                nEndPos = pIter->GetChgPos();
                if( nEndPos > nEnd )
                    nEndPos = nEnd;
            }
            else
            {
                nLang = LANGUAGE_SYSTEM;
                nEndPos = nEnd;
            }
            const sal_Int32 nLen = nEndPos - nStt;

            Sequence <sal_Int32> aOffsets;
            OUString const sChgd( rTrans.transliterate(
                        m_Text, nLang, nStt, nLen, &aOffsets) );

            assert(nStt < m_Text.getLength());
            if (0 != rtl_ustr_shortenedCompare_WithLength(
                        m_Text.getStr() + nStt, m_Text.getLength() - nStt,
                        sChgd.getStr(), sChgd.getLength(), nLen))
            {
                aChgData.nStart     = nStt;
                aChgData.nLen       = nLen;
                aChgData.sChanged   = sChgd;
                aChgData.aOffsets   = std::move(aOffsets);
                aChanges.push_back( aChgData );
            }

            nStt = nEndPos;

            // tdf#157937 selection containing tracked changes needs loop control:
            // stop looping, if there are too much empty transliterations
            if ( sChgd.isEmpty() )
                ++nLoopControlRuns;

        } while( nEndPos < nEnd && pIter && pIter->Next() && nLoopControlRuns < 100 );
    }

    if (aChanges.empty())
        return;

    // now apply the changes from end to start to leave the offsets of the
    // yet unchanged text parts remain the same.
    size_t nSum(0);

    for (size_t i = 0; i < aChanges.size(); ++i)
    {   // check this here since AddChanges cannot be moved below
        // call to ReplaceTextOnly
        swTransliterationChgData & rData =
            aChanges[ aChanges.size() - 1 - i ];

        nSum += rData.sChanged.getLength() - rData.nLen;
        if (nSum > o3tl::make_unsigned(GetSpaceLeft()))
        {
            SAL_WARN("sw.core""SwTextNode::ReplaceTextOnly: "
                    "node text with insertion > node capacity.");
            return;
        }

        if ( bUseRedlining )
        {
            // create SwPaM with mark & point spanning the attributed text
            //SwPaM aCurPaM( *this, *this, nBegin, nBegin + nLen ); <-- wrong c-tor, does sth different
            SwPaM aCurPaM( *this, rData.nStart );
            aCurPaM.SetMark();
            aCurPaM.GetPoint()->SetContent( rData.nStart + rData.nLen );
            // replace the changed words
            if ( aCurPaM.GetText() != rData.sChanged )
                GetDoc().getIDocumentContentOperations().ReplaceRange( aCurPaM, rData.sChanged, false );
        }
        else
        {
            if (pUndo)
                pUndo->AddChanges( *this, rData.nStart, rData.nLen, rData.aOffsets );
            ReplaceTextOnly( rData.nStart, rData.nLen, rData.sChanged, rData.aOffsets );
        }
    }
}

void SwTextNode::ReplaceTextOnly( sal_Int32 nPos, sal_Int32 nLen,
                                std::u16string_view aText,
                                const Sequence<sal_Int32>& rOffsets )
{
    assert(sal_Int32(aText.size()) - nLen <= GetSpaceLeft());

    m_Text = m_Text.replaceAt(nPos, nLen, aText);

    sal_Int32 nTLen = aText.size();
    const sal_Int32* pOffsets = rOffsets.getConstArray();
    // now look for no 1-1 mapping -> move the indices!
    sal_Int32 nMyOff = nPos;
    for( sal_Int32 nI = 0; nI < nTLen; ++nI )
    {
        const sal_Int32 nOff = pOffsets[ nI ];
        if( nOff < nMyOff )
        {
            // something is inserted
            sal_Int32 nCnt = 1;
            while( nI + nCnt < nTLen && nOff == pOffsets[ nI + nCnt ] )
                ++nCnt;

            Update(SwContentIndex(this, nMyOff), nCnt, UpdateMode::Default);
            nMyOff = nOff;
            //nMyOff -= nCnt;
            nI += nCnt - 1;
        }
        else if( nOff > nMyOff )
        {
            // something is deleted
            Update(SwContentIndex(this, nMyOff + 1), nOff - nMyOff, UpdateMode::Negative);
            nMyOff = nOff;
        }
        ++nMyOff;
    }
    if( nMyOff < nLen )
        // something is deleted at the end
        Update(SwContentIndex(this, nMyOff), nLen - nMyOff, UpdateMode::Negative);

    // notify the layout!
    const auto aDelHint = sw::DeleteText(nPos, nTLen);
    CallSwClientNotify(aDelHint);

    const auto aInsHint = sw::MakeInsertText(*this, nPos, nTLen);
    CallSwClientNotify(aInsHint);
}

// the return values allows us to see if we did the heavy-
// lifting required to actually break and count the words.
bool SwTextNode::CountWords( SwDocStat& rStat,
                            sal_Int32 nStt, sal_Int32 nEnd ) const
{
    if( nStt > nEnd )
    {   // bad call
        return false;
    }
    if (IsInRedlines())
    {   //not counting txtnodes used to hold deleted redline content
        return false;
    }
    bool bCountAll = ( (0 == nStt) && (GetText().getLength() == nEnd) );
    ++rStat.nAllPara; // #i93174#: count _all_ paragraphs
    if ( IsHidden() )
    {   // not counting hidden paras
        return false;
    }
    // count words in numbering string if started at beginning of para:
    bool bCountNumbering = nStt == 0;
    bool bHasBullet = false, bHasNumbering = false;
    OUString sNumString;
    if (bCountNumbering)
    {
        sNumString = GetNumString();
        bHasNumbering = !sNumString.isEmpty();
        if (!bHasNumbering)
            bHasBullet = HasBullet();
        bCountNumbering = bHasNumbering || bHasBullet;
    }

    if( nStt == nEnd && !bCountNumbering)
    {   // unnumbered empty node or empty selection
        if (bCountAll)
        {
            SetWordCountDirty( false ); // reset flag to speed up DoIdleJob
        }
        return false;
    }

    // count of non-empty paras
    ++rStat.nPara;

    // Shortcut when counting whole paragraph and current count is clean
    if ( bCountAll && !IsWordCountDirty() )
    {
        // accumulate into DocStat record to return the values

        rStat.nWord += m_aParagraphIdleData.nNumberOfWords;
        rStat.nAsianWord += m_aParagraphIdleData.nNumberOfAsianWords;
        rStat.nChar += m_aParagraphIdleData.nNumberOfChars;
        rStat.nCharExcludingSpaces += m_aParagraphIdleData.nNumberOfCharsExcludingSpaces;
        return false;
    }

    // ConversionMap to expand fields, remove invisible and redline deleted text for scanner
    const ModelToViewHelper aConversionMap(*this,
        getIDocumentLayoutAccess().GetCurrentLayout(),
        ExpandMode::ExpandFields | ExpandMode::ExpandFootnote | ExpandMode::HideInvisible | ExpandMode::HideDeletions | ExpandMode::HideFieldmarkCommands);
    const OUString& aExpandText = aConversionMap.getViewText();

    if (aExpandText.isEmpty() && !bCountNumbering)
    {
        if (bCountAll)
        {
            SetWordCountDirty( false ); // reset flag to speed up DoIdleJob
        }
        return false;
    }

    // map start and end points onto the ConversionMap
    const sal_Int32 nExpandBegin = aConversionMap.ConvertToViewPosition( nStt );
    const sal_Int32 nExpandEnd   = aConversionMap.ConvertToViewPosition( nEnd );

    //do the count
    // all counts exclude hidden paras and hidden+redlined within para
    // definition of space/white chars in SwScanner (and BreakIter!)
    // uses both u_isspace and BreakIter getWordBoundary in SwScanner
    sal_uInt32 nTmpWords = 0;        // count of all words
    sal_uInt32 nTmpAsianWords = 0;   //count of all Asian codepoints
    sal_uInt32 nTmpChars = 0;        // count of all chars
    sal_uInt32 nTmpCharsExcludingSpaces = 0;  // all non-white chars

    // count words in masked and expanded text:
    if (!aExpandText.isEmpty())
    {
        assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is());

        // zero is NULL for pLanguage -----------v               last param = true for clipping
        SwScanner aScanner( *this, aExpandText, nullptr, aConversionMap, i18n::WordType::WORD_COUNT,
                            nExpandBegin, nExpandEnd, true );

        // used to filter out scanner returning almost empty strings (len=1; unichar=0x0001)
        const OUString aBreakWord( CH_TXTATR_BREAKWORD );

        while ( aScanner.NextWord() )
        {
            if( !aExpandText.match(aBreakWord, aScanner.GetBegin() ))
            {
                ++nTmpWords;
                const OUString &rWord = aScanner.GetWord();
                if (g_pBreakIt->GetBreakIter()->getScriptType(rWord, 0) == i18n::ScriptType::ASIAN)
                    ++nTmpAsianWords;
                nTmpCharsExcludingSpaces += g_pBreakIt->getGraphemeCount(rWord);
            }
        }

        nTmpCharsExcludingSpaces += aScanner.getOverriddenDashCount();

        nTmpChars = g_pBreakIt->getGraphemeCount(aExpandText, nExpandBegin, nExpandEnd);
    }

    // no nTmpCharsExcludingSpaces adjust needed neither for blanked out MaskedChars
    // nor for mid-word selection - set scanner bClip = true at creation

    // count outline number label - ? no expansion into map
    // always counts all of number-ish label
    if (bHasNumbering) // count words in numbering string
    {
        LanguageType aLanguage = GetLang( 0 );

        SwScanner aScanner( *this, sNumString, &aLanguage, ModelToViewHelper(),
                            i18n::WordType::WORD_COUNT, 0, sNumString.getLength(), true );

        while ( aScanner.NextWord() )
        {
            ++nTmpWords;
            const OUString &rWord = aScanner.GetWord();
            if (g_pBreakIt->GetBreakIter()->getScriptType(rWord, 0) == i18n::ScriptType::ASIAN)
                ++nTmpAsianWords;
            nTmpCharsExcludingSpaces += g_pBreakIt->getGraphemeCount(rWord);
        }

        nTmpCharsExcludingSpaces += aScanner.getOverriddenDashCount();
        nTmpChars += g_pBreakIt->getGraphemeCount(sNumString);
    }
    else if ( bHasBullet )
    {
        ++nTmpWords;
        ++nTmpChars;
        ++nTmpCharsExcludingSpaces;
    }

    // If counting the whole para then update cached values and mark clean
    if ( bCountAll )
    {
        m_aParagraphIdleData.nNumberOfWords = nTmpWords;
        m_aParagraphIdleData.nNumberOfAsianWords = nTmpAsianWords;
        m_aParagraphIdleData.nNumberOfChars = nTmpChars;
        m_aParagraphIdleData.nNumberOfCharsExcludingSpaces = nTmpCharsExcludingSpaces;
        SetWordCountDirty( false );
    }
    // accumulate into DocStat record to return the values
    rStat.nWord += nTmpWords;
    rStat.nAsianWord += nTmpAsianWords;
    rStat.nChar += nTmpChars;
    rStat.nCharExcludingSpaces += nTmpCharsExcludingSpaces;

    return true;
}

void SwTextNode::SetWrong( std::unique_ptr<SwWrongList> pNew )
{
    m_aParagraphIdleData.pWrong = std::move(pNew);
}

void SwTextNode::ClearWrong()
{
    m_aParagraphIdleData.pWrong.reset();
}

std::unique_ptr<SwWrongList> SwTextNode::ReleaseWrong()
{
    return std::move(m_aParagraphIdleData.pWrong);
}

SwWrongList* SwTextNode::GetWrong()
{
    return m_aParagraphIdleData.pWrong.get();
}

// #i71360#
const SwWrongList* SwTextNode::GetWrong() const
{
    return m_aParagraphIdleData.pWrong.get();
}

void SwTextNode::SetGrammarCheck( std::unique_ptr<SwGrammarMarkUp> pNew )
{
    m_aParagraphIdleData.pGrammarCheck = std::move(pNew);
}

void SwTextNode::ClearGrammarCheck()
{
    m_aParagraphIdleData.pGrammarCheck.reset();
}

std::unique_ptr<SwGrammarMarkUp> SwTextNode::ReleaseGrammarCheck()
{
    return std::move(m_aParagraphIdleData.pGrammarCheck);
}

SwGrammarMarkUp* SwTextNode::GetGrammarCheck()
{
    return m_aParagraphIdleData.pGrammarCheck.get();
}

SwWrongList const* SwTextNode::GetGrammarCheck() const
{
    return static_cast<SwWrongList const*>(const_cast<SwTextNode*>(this)->GetGrammarCheck());
}

void SwTextNode::SetSmartTags( std::unique_ptr<SwWrongList> pNew )
{
    OSL_ENSURE( !pNew || SwSmartTagMgr::Get().IsSmartTagsEnabled(),
            "Weird - we have a smart tag list without any recognizers?" );

    m_aParagraphIdleData.pSmartTags = std::move(pNew);
}

void SwTextNode::ClearSmartTags()
{
    m_aParagraphIdleData.pSmartTags.reset();
}

std::unique_ptr<SwWrongList> SwTextNode::ReleaseSmartTags()
{
    return std::move(m_aParagraphIdleData.pSmartTags);
}

SwWrongList* SwTextNode::GetSmartTags()
{
    return m_aParagraphIdleData.pSmartTags.get();
}

SwWrongList const* SwTextNode::GetSmartTags() const
{
    return const_cast<SwWrongList const*>(const_cast<SwTextNode*>(this)->GetSmartTags());
}

void SwTextNode::SetWordCountDirty( bool bNew ) const
{
    m_aParagraphIdleData.bWordCountDirty = bNew;
}

bool SwTextNode::IsWordCountDirty() const
{
    return m_aParagraphIdleData.bWordCountDirty;
}

void SwTextNode::SetWrongDirty(sw::WrongState eNew) const
{
    m_aParagraphIdleData.eWrongDirty = eNew;
}

sw::WrongState SwTextNode::GetWrongDirty() const
{
    return m_aParagraphIdleData.eWrongDirty;
}

bool SwTextNode::IsWrongDirty() const
{
    return m_aParagraphIdleData.eWrongDirty != sw::WrongState::DONE;
}

void SwTextNode::SetGrammarCheckDirty( bool bNew ) const
{
    m_aParagraphIdleData.bGrammarCheckDirty = bNew;
}

bool SwTextNode::IsGrammarCheckDirty() const
{
    return m_aParagraphIdleData.bGrammarCheckDirty;
}

void SwTextNode::SetSmartTagDirty( bool bNew ) const
{
    m_aParagraphIdleData.bSmartTagDirty = bNew;
}

bool SwTextNode::IsSmartTagDirty() const
{
    return m_aParagraphIdleData.bSmartTagDirty;
}

void SwTextNode::SetAutoCompleteWordDirty( bool bNew ) const
{
    m_aParagraphIdleData.bAutoComplDirty = bNew;
}

bool SwTextNode::IsAutoCompleteWordDirty() const
{
    return m_aParagraphIdleData.bAutoComplDirty;
}

// <-- Paragraph statistics end

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

Messung V0.5 in Prozent
C=89 H=91 G=89

¤ Dauer der Verarbeitung: 0.40 Sekunden  (vorverarbeitet am  2026-05-06) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge