/* -*- 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 <unotools/charclass.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,
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 = !SW_MOD()->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 );
while( true )
{
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::LegacyModifyHint(nullptr, &aHint));
SwFormatChg aNew( GetFormatColl() );
CallSwClientNotify(sw::LegacyModifyHint(nullptr, &aNew));
}
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());
if ( m_pLanguage )
{
m_aCurrentLang = *m_pLanguage;
}
else
{
ModelToViewHelper::ModelPosition aModelBeginPos =
m_ModelToView.ConvertToModelPosition( m_nBegin );
m_aCurrentLang = m_pGetLangOfChar(aModelBeginPos.mnPos, 0, true);
}
}
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,
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;
std::optional<CharClass> xLocalCharClass;
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 );
m_aCurrentLang = m_pGetLangOfChar(aModelBeginPos.mnPos, nNextScriptType, false);
}
if ( m_nWordType != i18n::WordType::WORD_COUNT )
{
xLocalCharClass.emplace(LanguageTag( g_pBreakIt->GetLocale( m_aCurrentLang ) ));
if ( xLocalCharClass->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)
{
// 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;
// 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() ) && 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())); (void) this;
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: */
↑ V530 The return value of function 'Union' is required to be utilized.
↑ V530 The return value of function 'Union' is required to be utilized.
↑ V560 A part of conditional expression is always false: !bSpell.
↑ V581 The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 1357, 1363.