diff options
Diffstat (limited to 'sw/source/core/text/porlay.cxx')
-rw-r--r-- | sw/source/core/text/porlay.cxx | 2996 |
1 files changed, 2996 insertions, 0 deletions
diff --git a/sw/source/core/text/porlay.cxx b/sw/source/core/text/porlay.cxx new file mode 100644 index 0000000000..cae54845be --- /dev/null +++ b/sw/source/core/text/porlay.cxx @@ -0,0 +1,2996 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include "porlay.hxx" +#include "itrform2.hxx" +#include "porglue.hxx" +#include "redlnitr.hxx" +#include "porfly.hxx" +#include "porrst.hxx" +#include "pormulti.hxx" +#include "pordrop.hxx" +#include <breakit.hxx> +#include <unicode/uchar.h> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/i18n/CharacterIteratorMode.hpp> +#include <com/sun/star/i18n/UnicodeType.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <paratr.hxx> +#include <sal/log.hxx> +#include <optional> +#include <editeng/adjustitem.hxx> +#include <editeng/charhiddenitem.hxx> +#include <editeng/escapementitem.hxx> +#include <svl/asiancfg.hxx> +#include <svl/languageoptions.hxx> +#include <tools/multisel.hxx> +#include <unotools/charclass.hxx> +#include <charfmt.hxx> +#include <docary.hxx> +#include <fmtanchr.hxx> +#include <redline.hxx> +#include <calbck.hxx> +#include <doc.hxx> +#include <swscanner.hxx> +#include <txatbase.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentContentOperations.hxx> +#include <IMark.hxx> +#include <sortedobjs.hxx> +#include <com/sun/star/frame/XModel.hpp> +#include <com/sun/star/text/XBookmarksSupplier.hpp> +#include <officecfg/Office/Common.hxx> +#include <comphelper/processfactory.hxx> +#include <docsh.hxx> +#include <unobookmark.hxx> +#include <unocrsrhelper.hxx> +#include <frmatr.hxx> +#include <vcl/kernarray.hxx> +#include <editeng/ulspitem.hxx> +#include <com/sun/star/rdf/Statement.hpp> +#include <com/sun/star/rdf/URI.hpp> +#include <com/sun/star/rdf/URIs.hpp> +#include <com/sun/star/rdf/XDocumentMetadataAccess.hpp> +#include <com/sun/star/rdf/XLiteral.hpp> +#include <com/sun/star/text/XTextContent.hpp> + +#include <unicode/ubidi.h> +#include <i18nutil/scripttypedetector.hxx> +#include <i18nutil/unicode.hxx> + +using namespace ::com::sun::star; +using namespace i18n::ScriptType; + +/* + https://www.khtt.net/en/page/1821/the-big-kashida-secret + + the rules of priorities that govern the addition of kashidas in Arabic text + made ... for ... Explorer 5.5 browser. + + The kashida justification is based on a connection priority scheme that + decides where kashidas are put automatically. + + This is how the software decides on kashida-inserting priorities: + 1. First it looks for characters with the highest priority in each word, + which means kashida-extensions will only been used in one position in each + word. Not more. + 2. The kashida will be connected to the character with the highest priority. + 3. If kashida connection opportunities are found with an equal level of + priority in one word, the kashida will be placed towards the end of the + word. + + The priority list of characters and the positioning is as follows: + 1. after a kashida that is manually placed in the text by the user, + 2. after a Seen or Sad (initial and medial form), + 3. before the final form of Taa Marbutah, Haa, Dal, + 4. before the final form of Alef, Tah Lam, Kaf and Gaf, + 5. before the preceding medial Baa of Ra, Ya and Alef Maqsurah, + 6. before the final form of Waw, Ain, Qaf and Fa, + 7. before the final form of other characters that can be connected. +*/ + +#define IS_JOINING_GROUP(c, g) ( u_getIntPropertyValue( (c), UCHAR_JOINING_GROUP ) == U_JG_##g ) +#define isAinChar(c) IS_JOINING_GROUP((c), AIN) +#define isAlefChar(c) IS_JOINING_GROUP((c), ALEF) +#define isDalChar(c) IS_JOINING_GROUP((c), DAL) +#define isFehChar(c) (IS_JOINING_GROUP((c), FEH) || IS_JOINING_GROUP((c), AFRICAN_FEH)) +#define isGafChar(c) IS_JOINING_GROUP((c), GAF) +#define isHehChar(c) IS_JOINING_GROUP((c), HEH) +#define isKafChar(c) IS_JOINING_GROUP((c), KAF) +#define isLamChar(c) IS_JOINING_GROUP((c), LAM) +#define isQafChar(c) (IS_JOINING_GROUP((c), QAF) || IS_JOINING_GROUP((c), AFRICAN_QAF)) +#define isRehChar(c) IS_JOINING_GROUP((c), REH) +#define isTahChar(c) IS_JOINING_GROUP((c), TAH) +#define isTehMarbutaChar(c) IS_JOINING_GROUP((c), TEH_MARBUTA) +#define isWawChar(c) IS_JOINING_GROUP((c), WAW) +#define isSeenOrSadChar(c) (IS_JOINING_GROUP((c), SAD) || IS_JOINING_GROUP((c), SEEN)) + +// Beh and characters that behave like Beh in medial form. +static bool isBehChar(sal_Unicode cCh) +{ + bool bRet = false; + switch (u_getIntPropertyValue(cCh, UCHAR_JOINING_GROUP)) + { + case U_JG_BEH: + case U_JG_NOON: + case U_JG_AFRICAN_NOON: + case U_JG_NYA: + case U_JG_YEH: + case U_JG_FARSI_YEH: + case U_JG_BURUSHASKI_YEH_BARREE: + bRet = true; + break; + default: + bRet = false; + break; + } + + return bRet; +} + +// Yeh and characters that behave like Yeh in final form. +static bool isYehChar(sal_Unicode cCh) +{ + bool bRet = false; + switch (u_getIntPropertyValue(cCh, UCHAR_JOINING_GROUP)) + { + case U_JG_YEH: + case U_JG_FARSI_YEH: + case U_JG_YEH_BARREE: + case U_JG_BURUSHASKI_YEH_BARREE: + case U_JG_YEH_WITH_TAIL: + bRet = true; + break; + default: + bRet = false; + break; + } + + return bRet; +} + +static bool isTransparentChar ( sal_Unicode cCh ) +{ + return u_getIntPropertyValue( cCh, UCHAR_JOINING_TYPE ) == U_JT_TRANSPARENT; +} + +// Checks if cCh + cNectCh builds a ligature (used for Kashidas) +static bool lcl_IsLigature( sal_Unicode cCh, sal_Unicode cNextCh ) +{ + // Lam + Alef + return ( isLamChar ( cCh ) && isAlefChar ( cNextCh )); +} + +// Checks if cCh is connectable to cPrevCh (used for Kashidas) +static bool lcl_ConnectToPrev( sal_Unicode cCh, sal_Unicode cPrevCh ) +{ + const int32_t nJoiningType = u_getIntPropertyValue( cPrevCh, UCHAR_JOINING_TYPE ); + bool bRet = nJoiningType != U_JT_RIGHT_JOINING && nJoiningType != U_JT_NON_JOINING; + + // check for ligatures cPrevChar + cChar + if( bRet ) + bRet = !lcl_IsLigature( cPrevCh, cCh ); + + return bRet; +} + +static bool lcl_HasStrongLTR ( std::u16string_view rText, sal_Int32 nStart, sal_Int32 nEnd ) + { + for( sal_Int32 nCharIdx = nStart; nCharIdx < nEnd; ++nCharIdx ) + { + const UCharDirection nCharDir = u_charDirection ( rText[ nCharIdx ] ); + if ( nCharDir == U_LEFT_TO_RIGHT || + nCharDir == U_LEFT_TO_RIGHT_EMBEDDING || + nCharDir == U_LEFT_TO_RIGHT_OVERRIDE ) + return true; + } + return false; + } + +// This is (meant to be) functionally equivalent to 'delete m_pNext' where +// deleting a SwLineLayout recursively deletes the owned m_pNext SwLineLayout. +// +// Here, instead of using a potentially deep stack, iterate over all the +// SwLineLayouts that would be deleted recursively and delete them linearly +void SwLineLayout::DeleteNext() +{ + if (!m_pNext) + return; + SwLineLayout* pNext = m_pNext; + do + { + SwLineLayout* pLastNext = pNext; + pNext = pNext->GetNext(); + pLastNext->SetNext(nullptr); + delete pLastNext; + } + while (pNext); +} + +void SwLineLayout::Height(const SwTwips nNew, const bool bText) +{ + SwPosSize::Height(nNew); + if (bText) + m_nTextHeight = nNew; +} + +// class SwLineLayout: This is the layout of a single line, which is made +// up of its dimension, the character count and the word spacing in the line. +// Line objects are managed in an own pool, in order to store them continuously +// in memory so that they are paged out together and don't fragment memory. +SwLineLayout::~SwLineLayout() +{ + Truncate(); + DeleteNext(); + m_pLLSpaceAdd.reset(); + m_pKanaComp.reset(); +} + +SwLinePortion *SwLineLayout::Insert( SwLinePortion *pIns ) +{ + // First attribute change: copy mass and length from *pIns into the first + // text portion + if( !mpNextPortion ) + { + if( GetLen() ) + { + mpNextPortion = SwTextPortion::CopyLinePortion(*this); + if( IsBlinking() ) + { + SetBlinking( false ); + } + } + else + { + SetNextPortion( pIns ); + return pIns; + } + } + // Call with scope or we'll end up with recursion! + return mpNextPortion->SwLinePortion::Insert( pIns ); +} + +SwLinePortion *SwLineLayout::Append( SwLinePortion *pIns ) +{ + // First attribute change: copy mass and length from *pIns into the first + // text portion + if( !mpNextPortion ) + mpNextPortion = SwTextPortion::CopyLinePortion(*this); + // Call with scope or we'll end up with recursion! + return mpNextPortion->SwLinePortion::Append( pIns ); +} + +// For special treatment of empty lines + +bool SwLineLayout::Format( SwTextFormatInfo &rInf ) +{ + if( GetLen() ) + return SwTextPortion::Format( rInf ); + + Height( rInf.GetTextHeight() ); + return true; +} + +// We collect all FlyPortions at the beginning of the line and make that a +// MarginPortion. +SwMarginPortion *SwLineLayout::CalcLeftMargin() +{ + SwMarginPortion *pLeft = (GetNextPortion() && GetNextPortion()->IsMarginPortion()) ? + static_cast<SwMarginPortion *>(GetNextPortion()) : nullptr; + if( !GetNextPortion() ) + SetNextPortion(SwTextPortion::CopyLinePortion(*this)); + if( !pLeft ) + { + pLeft = new SwMarginPortion; + pLeft->SetNextPortion( GetNextPortion() ); + SetNextPortion( pLeft ); + } + else + { + pLeft->Height( 0 ); + pLeft->Width( 0 ); + pLeft->SetLen(TextFrameIndex(0)); + pLeft->SetAscent( 0 ); + pLeft->SetNextPortion( nullptr ); + pLeft->SetFixWidth(0); + } + + SwLinePortion *pPos = pLeft->GetNextPortion(); + while( pPos ) + { + if( pPos->IsFlyPortion() ) + { + // The FlyPortion gets sucked out... + pLeft->Join( static_cast<SwGluePortion*>(pPos) ); + pPos = pLeft->GetNextPortion(); + if( GetpKanaComp() && !GetKanaComp().empty() ) + GetKanaComp().pop_front(); + } + else + pPos = nullptr; + } + return pLeft; +} + +void SwLineLayout::InitSpaceAdd() +{ + if ( !m_pLLSpaceAdd ) + CreateSpaceAdd(); + else + SetLLSpaceAdd( 0, 0 ); +} + +void SwLineLayout::CreateSpaceAdd( const tools::Long nInit ) +{ + m_pLLSpaceAdd.reset( new std::vector<tools::Long> ); + SetLLSpaceAdd( nInit, 0 ); +} + +// #i3952# Returns true if there are only blanks in [nStt, nEnd[ +// Used to implement IgnoreTabsAndBlanksForLineCalculation compat flag +static bool lcl_HasOnlyBlanks(std::u16string_view rText, TextFrameIndex nStt, TextFrameIndex nEnd) +{ + while ( nStt < nEnd ) + { + switch (rText[sal_Int32(nStt++)]) + { + case 0x0020: // SPACE + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x2005: // FOUR-PER-EM SPACE + case 0x3000: // IDEOGRAPHIC SPACE + continue; + default: + return false; + } + } + return true; +} + +// Swapped out from FormatLine() +void SwLineLayout::CalcLine( SwTextFormatter &rLine, SwTextFormatInfo &rInf ) +{ + const sal_uInt16 nLineWidth = rInf.RealWidth(); + + sal_uInt16 nFlyAscent = 0; + sal_uInt16 nFlyHeight = 0; + sal_uInt16 nFlyDescent = 0; + + // If this line has a clearing break, then this is the portion's height. + sal_uInt16 nBreakHeight = 0; + + bool bOnlyPostIts = true; + SetHanging( false ); + + bool bTmpDummy = !GetLen(); + SwFlyCntPortion* pFlyCnt = nullptr; + if( bTmpDummy ) + { + nFlyAscent = 0; + nFlyHeight = 0; + nFlyDescent = 0; + } + + // #i3952# + const bool bIgnoreBlanksAndTabsForLineHeightCalculation = + rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::IGNORE_TABS_AND_BLANKS_FOR_LINE_CALCULATION); + + bool bHasBlankPortion = false; + bool bHasOnlyBlankPortions = true; + bool bHasFlyPortion = false; + + if( mpNextPortion ) + { + SetContent( false ); + if( mpNextPortion->IsBreakPortion() ) + { + SetLen( mpNextPortion->GetLen() ); + if( GetLen() ) + bTmpDummy = false; + } + else + { + const SwTwips nLineHeight = Height(); + Init( GetNextPortion() ); + SwLinePortion *pPos = mpNextPortion; + SwLinePortion *pLast = this; + sal_uInt16 nMaxDescent = 0; + + // A group is a segment in the portion chain of pCurr or a fixed + // portion spanning to the end or the next fixed portion + while( pPos ) + { + SAL_WARN_IF( PortionType::NONE == pPos->GetWhichPor(), + "sw.core", "SwLineLayout::CalcLine: don't use SwLinePortions !" ); + + // Null portions are eliminated. They can form if two FlyFrames + // overlap. + // coverity[deref_arg] - "Cut" means next "GetNextPortion" returns a different Portion + if( !pPos->Compress() ) + { + // Only take over Height and Ascent if the rest of the line + // is empty. + if( !pPos->GetNextPortion() ) + { + if( !Height() ) + Height( pPos->Height(), false ); + if( !GetAscent() ) + SetAscent( pPos->GetAscent() ); + } + SwLinePortion* pPortion = pLast->Cut( pPos ); + rLine.ClearIfIsFirstOfBorderMerge(pPortion); + delete pPortion; + pPos = pLast->GetNextPortion(); + continue; + } + + TextFrameIndex const nPorSttIdx = rInf.GetLineStart() + mnLineLength; + mnLineLength += pPos->GetLen(); + AddPrtWidth( pPos->Width() ); + + // #i3952# + if (bIgnoreBlanksAndTabsForLineHeightCalculation && !rInf.GetLineStart()) + { + if ( pPos->InTabGrp() || pPos->IsHolePortion() || + ( pPos->IsTextPortion() && + lcl_HasOnlyBlanks( rInf.GetText(), nPorSttIdx, nPorSttIdx + pPos->GetLen() ) ) ) + { + pLast = pPos; + pPos = pPos->GetNextPortion(); + bHasBlankPortion = true; + continue; + } + } + + // Ignore drop portion height + // tdf#130804 ... and bookmark portions + if ((pPos->IsDropPortion() && static_cast<SwDropPortion*>(pPos)->GetLines() > 1) + || pPos->GetWhichPor() == PortionType::Bookmark) + { + pLast = pPos; + pPos = pPos->GetNextPortion(); + continue; + } + + bHasOnlyBlankPortions = false; + + // We had an attribute change: Sum up/build maxima of length and mass + + SwTwips nPosHeight = pPos->Height(); + SwTwips nPosAscent = pPos->GetAscent(); + + SAL_WARN_IF( nPosHeight < nPosAscent, + "sw.core", "SwLineLayout::CalcLine: bad ascent or height" ); + + if( pPos->IsHangingPortion() ) + { + SetHanging(true); + rInf.GetParaPortion()->SetMargin(); + } + else if( !bHasFlyPortion && ( pPos->IsFlyCntPortion() || pPos->IsFlyPortion() ) ) + bHasFlyPortion = true; + + // A line break portion only influences the height of the line in case it's the only + // portion in the line, except when it's a clearing break. + bool bClearingBreak = false; + if (pPos->IsBreakPortion()) + { + auto pBreakPortion = static_cast<SwBreakPortion*>(pPos); + bClearingBreak = pBreakPortion->GetClear() != SwLineBreakClear::NONE; + nBreakHeight = nPosHeight; + } + if (!(pPos->IsBreakPortion() && !bClearingBreak) || !Height()) + { + if (!pPos->IsPostItsPortion()) bOnlyPostIts = false; + + if( bTmpDummy && !mnLineLength ) + { + if( pPos->IsFlyPortion() ) + { + if( nFlyHeight < nPosHeight ) + nFlyHeight = nPosHeight; + if( nFlyAscent < nPosAscent ) + nFlyAscent = nPosAscent; + if( nFlyDescent < nPosHeight - nPosAscent ) + nFlyDescent = nPosHeight - nPosAscent; + } + else + { + if( pPos->InNumberGrp() ) + { + sal_uInt16 nTmp = rInf.GetFont()->GetAscent( + rInf.GetVsh(), *rInf.GetOut() ); + if( nTmp > nPosAscent ) + { + nPosHeight += nTmp - nPosAscent; + nPosAscent = nTmp; + } + nTmp = rInf.GetFont()->GetHeight( rInf.GetVsh(), + *rInf.GetOut() ); + if( nTmp > nPosHeight ) + nPosHeight = nTmp; + } + Height( nPosHeight, false ); + mnAscent = nPosAscent; + nMaxDescent = nPosHeight - nPosAscent; + } + } + else if( !pPos->IsFlyPortion() ) + { + if( Height() < nPosHeight ) + { + // Height is set to 0 when Init() is called. + if (bIgnoreBlanksAndTabsForLineHeightCalculation && pPos->IsFlyCntPortion()) + // Compat flag set: take the line height, if it's larger. + Height(std::max(nPosHeight, nLineHeight), false); + else + // Just care about the portion height. + Height(nPosHeight, pPos->IsTextPortion()); + } + SwFlyCntPortion* pAsFly(nullptr); + if(pPos->IsFlyCntPortion()) + pAsFly = static_cast<SwFlyCntPortion*>(pPos); + if( pAsFly || ( pPos->IsMultiPortion() + && static_cast<SwMultiPortion*>(pPos)->HasFlyInContent() ) ) + rLine.SetFlyInCntBase(); + if(pAsFly && pAsFly->GetAlign() != sw::LineAlign::NONE) + { + pAsFly->SetMax(false); + if( !pFlyCnt || pPos->Height() > pFlyCnt->Height() ) + pFlyCnt = pAsFly; + } + else + { + if( mnAscent < nPosAscent ) + mnAscent = nPosAscent; + if( nMaxDescent < nPosHeight - nPosAscent ) + nMaxDescent = nPosHeight - nPosAscent; + } + } + } + else if( pPos->GetLen() ) + bTmpDummy = false; + + if( !HasContent() && !pPos->InNumberGrp() ) + { + if ( pPos->InExpGrp() ) + { + OUString aText; + if( pPos->GetExpText( rInf, aText ) && !aText.isEmpty() ) + SetContent(true); + } + else if( ( pPos->InTextGrp() || pPos->IsMultiPortion() ) && + pPos->GetLen() ) + SetContent(true); + } + + bTmpDummy &= !HasContent() && ( !pPos->Width() || pPos->IsFlyPortion() ); + + pLast = pPos; + pPos = pPos->GetNextPortion(); + } + + if( pFlyCnt ) + { + if( pFlyCnt->Height() == Height() ) + { + pFlyCnt->SetMax( true ); + if( Height() > nMaxDescent + mnAscent ) + { + if( sw::LineAlign::BOTTOM == pFlyCnt->GetAlign() ) + mnAscent = Height() - nMaxDescent; + else if( sw::LineAlign::CENTER == pFlyCnt->GetAlign() ) + mnAscent = ( Height() + mnAscent - nMaxDescent ) / 2; + } + pFlyCnt->SetAscent( mnAscent ); + } + } + + if( bTmpDummy && nFlyHeight ) + { + mnAscent = nFlyAscent; + if( nFlyDescent > nFlyHeight - nFlyAscent ) + Height( nFlyHeight + nFlyDescent, false ); + else + { + if (nBreakHeight > nFlyHeight) + { + // The line has no content, but it has a clearing break: then the line + // height is not only the intersection of the fly and line's rectangle, but + // also includes the clearing break's height. + Height(nBreakHeight, false); + } + else + { + Height(nFlyHeight, false); + } + } + } + else if( nMaxDescent > Height() - mnAscent ) + Height( nMaxDescent + mnAscent, false ); + + if( bOnlyPostIts && !( bHasBlankPortion && bHasOnlyBlankPortions ) ) + { + Height( rInf.GetFont()->GetHeight( rInf.GetVsh(), *rInf.GetOut() ) ); + mnAscent = rInf.GetFont()->GetAscent( rInf.GetVsh(), *rInf.GetOut() ); + } + } + } + else + { + SetContent( !bTmpDummy ); + + // #i3952# + if ( bIgnoreBlanksAndTabsForLineHeightCalculation && + lcl_HasOnlyBlanks( rInf.GetText(), rInf.GetLineStart(), rInf.GetLineStart() + GetLen() ) ) + { + bHasBlankPortion = true; + } + } + + // #i3952# Whitespace does not increase line height + if ( bHasBlankPortion && bHasOnlyBlankPortions ) + { + sal_uInt16 nTmpAscent = GetAscent(); + sal_uInt16 nTmpHeight = Height(); + rLine.GetAttrHandler().GetDefaultAscentAndHeight( rInf.GetVsh(), *rInf.GetOut(), nTmpAscent, nTmpHeight ); + + short nEscapement = rLine.GetAttrHandler().GetFont()->GetEscapement(); + if (GetAscent() && Height() && !nTmpAscent && !nTmpHeight + && (nEscapement == DFLT_ESC_AUTO_SUPER || nEscapement == DFLT_ESC_AUTO_SUB)) + { + // We already had a calculated ascent + height, it would be cleared, automatic + // sub/superscript is set and we have no content. In this case it makes no sense to + // clear the old, correct ascent/height. + nTmpAscent = GetAscent(); + nTmpHeight = Height(); + } + + if (nTmpAscent < GetAscent() || GetAscent() <= 0) + SetAscent(nTmpAscent); + if (nTmpHeight < Height() || Height() <= 0) + Height(nTmpHeight, false); + } + + // Robust: + if( nLineWidth < Width() ) + Width( nLineWidth ); + SAL_WARN_IF( nLineWidth < Width(), "sw.core", "SwLineLayout::CalcLine: line is bursting" ); + SetDummy( bTmpDummy ); + std::pair<SwTextNode const*, sal_Int32> const start( + rInf.GetTextFrame()->MapViewToModel(rLine.GetStart())); + std::pair<SwTextNode const*, sal_Int32> const end( + rInf.GetTextFrame()->MapViewToModel(rLine.GetEnd())); + bool bHasRedline = rLine.GetRedln(); + if( bHasRedline ) + { + OUString sRedlineText; + bool bHasRedlineEnd; + enum RedlineType eRedlineEnd; + bHasRedline = rLine.GetRedln()->CheckLine(start.first->GetIndex(), start.second, + end.first->GetIndex(), end.second, sRedlineText, bHasRedlineEnd, eRedlineEnd); + if( bHasRedline ) + { + SetRedlineText( sRedlineText ); + if( bHasRedlineEnd ) + SetRedlineEnd( bHasRedlineEnd ); + if( eRedlineEnd != RedlineType::None ) + SetRedlineEndType( eRedlineEnd ); + } + } + SetRedline( bHasRedline ); + + // redlining: set crossing out for deleted anchored objects + if ( !bHasFlyPortion ) + return; + + SwLinePortion *pPos = mpNextPortion; + TextFrameIndex nLineLength; + while ( pPos ) + { + TextFrameIndex const nPorSttIdx = rInf.GetLineStart() + nLineLength; + nLineLength += pPos->GetLen(); + // anchored as characters + if( pPos->IsFlyCntPortion() ) + { + bool bDeleted = false; + size_t nAuthor = std::string::npos; + if ( bHasRedline ) + { + OUString sRedlineText; + bool bHasRedlineEnd; + enum RedlineType eRedlineEnd; + std::pair<SwTextNode const*, sal_Int32> const flyStart( + rInf.GetTextFrame()->MapViewToModel(nPorSttIdx)); + bool bHasFlyRedline = rLine.GetRedln()->CheckLine(flyStart.first->GetIndex(), + flyStart.second, flyStart.first->GetIndex(), flyStart.second, sRedlineText, + bHasRedlineEnd, eRedlineEnd, /*pAuthorAtPos=*/&nAuthor); + bDeleted = bHasFlyRedline && eRedlineEnd == RedlineType::Delete; + } + static_cast<SwFlyCntPortion*>(pPos)->SetDeleted(bDeleted); + static_cast<SwFlyCntPortion*>(pPos)->SetAuthor(nAuthor); + } + // anchored to characters + else if ( pPos->IsFlyPortion() ) + { + const IDocumentRedlineAccess& rIDRA = + rInf.GetTextFrame()->GetDoc().getIDocumentRedlineAccess(); + SwSortedObjs *pObjs = rInf.GetTextFrame()->GetDrawObjs(); + if ( pObjs && IDocumentRedlineAccess::IsShowChanges( rIDRA.GetRedlineFlags() ) ) + { + for ( size_t i = 0; rInf.GetTextFrame()->GetDrawObjs() && i < pObjs->size(); ++i ) + { + SwAnchoredObject* pAnchoredObj = (*rInf.GetTextFrame()->GetDrawObjs())[i]; + if ( auto pFly = pAnchoredObj->DynCastFlyFrame() ) + { + bool bDeleted = false; + size_t nAuthor = std::string::npos; + const SwFormatAnchor& rAnchor = pAnchoredObj->GetFrameFormat().GetAnchor(); + if ( rAnchor.GetAnchorId() == RndStdIds::FLY_AT_CHAR ) + { + SwPosition aAnchor = *rAnchor.GetContentAnchor(); + SwRedlineTable::size_type n = 0; + const SwRangeRedline* pFnd = + rIDRA.GetRedlineTable().FindAtPosition( aAnchor, n ); + if ( pFnd && RedlineType::Delete == pFnd->GetType() ) + { + bDeleted = true; + nAuthor = pFnd->GetAuthor(); + } + } + pFly->SetDeleted(bDeleted); + pFly->SetAuthor(nAuthor); + } + } + } + } + pPos = pPos->GetNextPortion(); + } +} + +// #i47162# - add optional parameter <_bNoFlyCntPorAndLinePor> +// to control, if the fly content portions and line portion are considered. +void SwLineLayout::MaxAscentDescent( SwTwips& _orAscent, + SwTwips& _orDescent, + SwTwips& _orObjAscent, + SwTwips& _orObjDescent, + const SwLinePortion* _pDontConsiderPortion, + const bool _bNoFlyCntPorAndLinePor ) const +{ + _orAscent = 0; + _orDescent = 0; + _orObjAscent = 0; + _orObjDescent = 0; + + const SwLinePortion* pTmpPortion = this; + if ( !pTmpPortion->GetLen() && pTmpPortion->GetNextPortion() ) + { + pTmpPortion = pTmpPortion->GetNextPortion(); + } + + while ( pTmpPortion ) + { + if ( !pTmpPortion->IsBreakPortion() && !pTmpPortion->IsFlyPortion() && + // tdf#130804 ignore bookmark portions + pTmpPortion->GetWhichPor() != PortionType::Bookmark && + ( !_bNoFlyCntPorAndLinePor || + ( !pTmpPortion->IsFlyCntPortion() && + !(pTmpPortion == this && pTmpPortion->GetNextPortion() ) ) ) ) + { + SwTwips nPortionAsc = pTmpPortion->GetAscent(); + SwTwips nPortionDesc = pTmpPortion->Height() - nPortionAsc; + + const bool bFlyCmp = pTmpPortion->IsFlyCntPortion() ? + static_cast<const SwFlyCntPortion*>(pTmpPortion)->IsMax() : + ( pTmpPortion != _pDontConsiderPortion ); + + if ( bFlyCmp ) + { + _orObjAscent = std::max( _orObjAscent, nPortionAsc ); + _orObjDescent = std::max( _orObjDescent, nPortionDesc ); + } + + if ( !pTmpPortion->IsFlyCntPortion() && !pTmpPortion->IsGrfNumPortion() ) + { + _orAscent = std::max( _orAscent, nPortionAsc ); + _orDescent = std::max( _orDescent, nPortionDesc ); + } + } + pTmpPortion = pTmpPortion->GetNextPortion(); + } +} + +void SwLineLayout::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwLineLayout")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwLineLayout::ResetFlags() +{ + m_bFormatAdj = m_bDummy = m_bEndHyph = m_bMidHyph = m_bFly + = m_bRest = m_bBlinking = m_bClipping = m_bContent = m_bRedline + = m_bRedlineEnd = m_bForcedLeftMargin = m_bHanging = false; + m_eRedlineEnd = RedlineType::None; +} + +SwLineLayout::SwLineLayout() + : m_pNext( nullptr ), + m_nRealHeight( 0 ), + m_nTextHeight( 0 ), + m_bUnderscore( false ) +{ + ResetFlags(); + SetWhichPor( PortionType::Lay ); +} + +SwLinePortion *SwLineLayout::GetFirstPortion() const +{ + const SwLinePortion *pRet = mpNextPortion ? mpNextPortion : this; + return const_cast<SwLinePortion*>(pRet); +} + +SwCharRange &SwCharRange::operator+=(const SwCharRange &rRange) +{ + if (TextFrameIndex(0) != rRange.m_nLen) + { + if (TextFrameIndex(0) == m_nLen) { + m_nStart = rRange.m_nStart; + m_nLen = rRange.m_nLen ; + } + else { + if(rRange.m_nStart + rRange.m_nLen > m_nStart + m_nLen) { + m_nLen = rRange.m_nStart + rRange.m_nLen - m_nStart; + } + if(rRange.m_nStart < m_nStart) { + m_nLen += m_nStart - rRange.m_nStart; + m_nStart = rRange.m_nStart; + } + } + } + return *this; +} + +SwScriptInfo::SwScriptInfo() + : m_nInvalidityPos(0) + , m_nDefaultDir(0) +{ +}; + +SwScriptInfo::~SwScriptInfo() +{ +} + +// Converts i18n Script Type (LATIN, ASIAN, COMPLEX, WEAK) to +// Sw Script Types (SwFontScript::Latin, SwFontScript::CJK, SwFontScript::CTL), used to identify the font +static SwFontScript lcl_ScriptToFont(sal_uInt16 const nScript) +{ + switch ( nScript ) { + case i18n::ScriptType::LATIN : return SwFontScript::Latin; + case i18n::ScriptType::ASIAN : return SwFontScript::CJK; + case i18n::ScriptType::COMPLEX : return SwFontScript::CTL; + } + + OSL_FAIL( "Somebody tells lies about the script type!" ); + return SwFontScript::Latin; +} + +SwFontScript SwScriptInfo::WhichFont(TextFrameIndex const nIdx) const +{ + const sal_uInt16 nScript(ScriptType(nIdx)); + return lcl_ScriptToFont(nScript); +} + +SwFontScript SwScriptInfo::WhichFont(sal_Int32 nIdx, OUString const& rText) +{ + const sal_uInt16 nScript(g_pBreakIt->GetRealScriptOfText(rText, nIdx)); + return lcl_ScriptToFont(nScript); +} + +static Color getBookmarkColor(const SwTextNode& rNode, const sw::mark::IBookmark* pBookmark) +{ + // search custom color in metadata, otherwise use COL_TRANSPARENT; + Color c = COL_TRANSPARENT; + + try + { + SwDoc& rDoc = const_cast<SwDoc&>(rNode.GetDoc()); + const rtl::Reference< SwXBookmark > xRef = SwXBookmark::CreateXBookmark(rDoc, + const_cast<sw::mark::IMark*>(static_cast<const sw::mark::IMark*>(pBookmark))); + const css::uno::Reference<css::rdf::XResource> xSubject(xRef); + uno::Reference<frame::XModel> xModel = rDoc.GetDocShell()->GetBaseModel(); + + static uno::Reference< uno::XComponentContext > xContext( + ::comphelper::getProcessComponentContext()); + + static uno::Reference< rdf::XURI > xODF_SHADING( + rdf::URI::createKnown(xContext, rdf::URIs::LO_EXT_SHADING), uno::UNO_SET_THROW); + + uno::Reference<rdf::XDocumentMetadataAccess> xDocumentMetadataAccess( + rDoc.GetDocShell()->GetBaseModel(), uno::UNO_QUERY); + const uno::Reference<rdf::XRepository>& xRepository = + xDocumentMetadataAccess->getRDFRepository(); + const uno::Reference<container::XEnumeration> xEnum( + xRepository->getStatements(xSubject, xODF_SHADING, nullptr), uno::UNO_SET_THROW); + + rdf::Statement stmt; + if ( xEnum->hasMoreElements() && (xEnum->nextElement() >>= stmt) ) + { + const uno::Reference<rdf::XLiteral> xObject(stmt.Object, uno::UNO_QUERY); + if ( xObject.is() ) + c = Color::STRtoRGB(xObject->getValue()); + } + } + catch (const lang::IllegalArgumentException&) + { + } + + return c; +} + +static void InitBookmarks( + std::optional<std::vector<sw::Extent>::const_iterator> oPrevIter, + std::vector<sw::Extent>::const_iterator iter, + std::vector<sw::Extent>::const_iterator const end, + TextFrameIndex nOffset, + std::vector<std::pair<sw::mark::IBookmark const*, SwScriptInfo::MarkKind>> & rBookmarks, + std::vector<std::tuple<TextFrameIndex, SwScriptInfo::MarkKind, Color, OUString>> & o_rBookmarks) +{ + SwTextNode const*const pNode(iter->pNode); + for (auto const& it : rBookmarks) + { + assert(iter->pNode == pNode || pNode->GetIndex() < iter->pNode->GetIndex()); + assert(!oPrevIter || (*oPrevIter)->pNode->GetIndex() <= pNode->GetIndex()); + + // search for custom bookmark boundary mark color + Color c = getBookmarkColor(*pNode, it.first); + + switch (it.second) + { + case SwScriptInfo::MarkKind::Start: + { + // SwUndoSaveContent::DelContentIndex() is rather messy but + // apparently bookmarks "on the edge" are deleted if + // * point: equals start-of-selection (not end-of-selection) + // * expanded: one position equals edge of selection + // and other does not (is inside) + // interesting case: if end[/start] of the mark is on the + // start of first[/end of last] extent, and the other one + // is outside this merged paragraph, is it deleted or not? + // assume "no" because the line break it contains isn't deleted. + SwPosition const& rStart(it.first->GetMarkStart()); + SwPosition const& rEnd(it.first->GetMarkEnd()); + assert(&rStart.GetNode() == pNode); + while (iter != end) + { + if (&rStart.GetNode() != iter->pNode // iter moved to next node + || rStart.GetContentIndex() < iter->nStart) + { + if (rEnd.GetNodeIndex() < iter->pNode->GetIndex() + || (&rEnd.GetNode() == iter->pNode && rEnd.GetContentIndex() <= iter->nStart)) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + break; + } + } + else if (rStart.GetContentIndex() <= iter->nEnd) + { + auto const iterNext(iter + 1); + if (rStart.GetContentIndex() == iter->nEnd + && (iterNext == end + ? &rEnd.GetNode() == iter->pNode + : (rEnd.GetNodeIndex() < iterNext->pNode->GetIndex() + || (&rEnd.GetNode() == iterNext->pNode && rEnd.GetContentIndex() < iterNext->nStart)))) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rStart.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + break; + } + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; // bookmarks are sorted... + } + } + if (iter == end) + { + if (pNode->GetIndex() < rEnd.GetNodeIndex()) // pNode is last node of merged + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + } + } + break; + } + case SwScriptInfo::MarkKind::End: + { + SwPosition const& rEnd(it.first->GetMarkEnd()); + assert(&rEnd.GetNode() == pNode); + while (true) + { + if (iter == end + || &rEnd.GetNode() != iter->pNode // iter moved to next node + || rEnd.GetContentIndex() <= iter->nStart) + { + SwPosition const& rStart(it.first->GetMarkStart()); + // oPrevIter may point to pNode or a preceding node + if (oPrevIter + ? ((*oPrevIter)->pNode->GetIndex() < rStart.GetNodeIndex() + || ((*oPrevIter)->pNode == &rStart.GetNode() + && ((iter != end && &rEnd.GetNode() == iter->pNode && rEnd.GetContentIndex() == iter->nStart) + ? (*oPrevIter)->nEnd < rStart.GetContentIndex() + : (*oPrevIter)->nEnd <= rStart.GetContentIndex()))) + : rStart.GetNode() == rEnd.GetNode()) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + break; + } + } + else if (rEnd.GetContentIndex() <= iter->nEnd) + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rEnd.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + break; + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; + } + } + break; + } + case SwScriptInfo::MarkKind::Point: + { + SwPosition const& rPos(it.first->GetMarkPos()); + assert(&rPos.GetNode() == pNode); + while (iter != end) + { + if (&rPos.GetNode() != iter->pNode // iter moved to next node + || rPos.GetContentIndex() < iter->nStart) + { + break; // deleted - skip it + } + else if (rPos.GetContentIndex() <= iter->nEnd) + { + if (rPos.GetContentIndex() == iter->nEnd + && rPos.GetContentIndex() != iter->pNode->Len()) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rPos.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + } + break; + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; + } + } + break; + } + } + if (iter == end) + { + break; // remaining marks are hidden + } + } +} + +// searches for script changes in rText and stores them +void SwScriptInfo::InitScriptInfo(const SwTextNode& rNode, + sw::MergedPara const*const pMerged) +{ + InitScriptInfo( rNode, pMerged, m_nDefaultDir == UBIDI_RTL ); +} + +void SwScriptInfo::InitScriptInfo(const SwTextNode& rNode, + sw::MergedPara const*const pMerged, bool bRTL) +{ + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + const OUString& rText(pMerged ? pMerged->mergedText : rNode.GetText()); + + // HIDDEN TEXT INFORMATION + + m_Bookmarks.clear(); + m_HiddenChg.clear(); + if (pMerged) + { + SwTextNode const* pNode(nullptr); + TextFrameIndex nOffset(0); + std::optional<std::vector<sw::Extent>::const_iterator> oPrevIter; + for (auto iter = pMerged->extents.begin(); iter != pMerged->extents.end(); + oPrevIter = iter) + { + if (iter->pNode == pNode) + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + ++iter; + continue; // skip extents at end of previous node + } + pNode = iter->pNode; + Range aRange( 0, pNode->Len() > 0 ? pNode->Len() - 1 : 0 ); + MultiSelection aHiddenMulti( aRange ); + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> bookmarks; + CalcHiddenRanges(*pNode, aHiddenMulti, &bookmarks); + + InitBookmarks(oPrevIter, iter, pMerged->extents.end(), nOffset, bookmarks, m_Bookmarks); + + for (sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nStart = rRange.Min(); + const sal_Int32 nEnd = rRange.Max() + 1; + bool isStartHandled(false); + ::std::optional<sal_Int32> oExtend; + + if (nEnd <= iter->nStart) + { // entirely in gap, skip this hidden range + continue; + } + + do + { + if (!isStartHandled && nStart <= iter->nEnd) + { + isStartHandled = true; + if (nStart <= iter->nStart && !m_HiddenChg.empty() + && m_HiddenChg.back() == nOffset) + { + // previous one went until end of extent, extend it + oExtend.emplace(::std::min(iter->nEnd, nEnd) - ::std::max(iter->nStart, nStart)); + } + else + { + m_HiddenChg.push_back(nOffset + TextFrameIndex(::std::max(nStart - iter->nStart, sal_Int32(0)))); + } + } + else if (oExtend) + { + *oExtend += ::std::min(iter->nEnd, nEnd) - iter->nStart; + } + if (nEnd <= iter->nEnd) + { + if (oExtend) + { + m_HiddenChg.back() += TextFrameIndex(*oExtend); + } + else + { + m_HiddenChg.push_back(nOffset + TextFrameIndex(::std::max(nEnd - iter->nStart, sal_Int32(0)))); + } + break; // iterate to next hidden range + } + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + ++iter; + } + while (iter != pMerged->extents.end() && iter->pNode == pNode); + if (iter == pMerged->extents.end() || iter->pNode != pNode) + { + if (isStartHandled) + { // dangling end + if (oExtend) + { + m_HiddenChg.back() += TextFrameIndex(*oExtend); + } + else + { + m_HiddenChg.push_back(nOffset); + } + } // else: beyond last extent in node, ignore + break; // skip hidden ranges beyond last extent in node + } + } + } + } + else + { + Range aRange( 0, !rText.isEmpty() ? rText.getLength() - 1 : 0 ); + MultiSelection aHiddenMulti( aRange ); + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> bookmarks; + CalcHiddenRanges(rNode, aHiddenMulti, &bookmarks); + + for (auto const& it : bookmarks) + { + // don't show __RefHeading__ bookmarks, which are hidden in Navigator, too + // (They are inserted automatically e.g. with the ToC at the beginning of + // the headings) + if (it.first->GetName().startsWith( + IDocumentMarkAccess::GetCrossRefHeadingBookmarkNamePrefix())) + { + continue; + } + + // search for custom bookmark boundary mark color + Color c = getBookmarkColor(rNode, it.first); + + switch (it.second) + { + case MarkKind::Start: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkStart().GetContentIndex()), it.second, c, it.first->GetName()); + break; + case MarkKind::End: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkEnd().GetContentIndex()), it.second, c, it.first->GetName()); + break; + case MarkKind::Point: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkPos().GetContentIndex()), it.second, c, it.first->GetName()); + break; + } + } + + m_HiddenChg.reserve( aHiddenMulti.GetRangeCount() * 2 ); + for (sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nStart = rRange.Min(); + const sal_Int32 nEnd = rRange.Max() + (rText.isEmpty() ? 0 : 1); + + m_HiddenChg.push_back( TextFrameIndex(nStart) ); + m_HiddenChg.push_back( TextFrameIndex(nEnd) ); + } + } + + // SCRIPT AND SCRIPT RELATED INFORMATION + + TextFrameIndex nChg = m_nInvalidityPos; + + // COMPLETE_STRING means the data structure is up to date + m_nInvalidityPos = TextFrameIndex(COMPLETE_STRING); + + // this is the default direction + m_nDefaultDir = static_cast<sal_uInt8>(bRTL ? UBIDI_RTL : UBIDI_LTR); + + // counter for script info arrays + size_t nCnt = 0; + // counter for compression information arrays + size_t nCntComp = 0; + // counter for kashida array + size_t nCntKash = 0; + + sal_Int16 nScript = i18n::ScriptType::LATIN; + + // compression type + const CharCompressType aCompEnum = rNode.getIDocumentSettingAccess()->getCharacterCompressionType(); + + auto const& rParaItems((pMerged ? *pMerged->pParaPropsNode : rNode).GetSwAttrSet()); + // justification type + const bool bAdjustBlock = SvxAdjust::Block == rParaItems.GetAdjust().GetAdjust(); + + // FIND INVALID RANGES IN SCRIPT INFO ARRAYS: + + if( nChg ) + { + // if change position = 0 we do not use any data from the arrays + // because by deleting all characters of the first group at the beginning + // of a paragraph nScript is set to a wrong value + SAL_WARN_IF( !CountScriptChg(), "sw.core", "Where're my changes of script?" ); + while( nCnt < CountScriptChg() ) + { + if ( nChg > GetScriptChg( nCnt ) ) + nCnt++; + else + { + nScript = GetScriptType( nCnt ); + break; + } + } + if( CharCompressType::NONE != aCompEnum ) + { + while( nCntComp < CountCompChg() ) + { + if ( nChg <= GetCompStart( nCntComp ) ) + break; + nCntComp++; + } + } + if ( bAdjustBlock ) + { + while( nCntKash < CountKashida() ) + { + if ( nChg <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + } + } + + // ADJUST nChg VALUE: + + // by stepping back one position we know that we are inside a group + // declared as an nScript group + if ( nChg ) + --nChg; + + const TextFrameIndex nGrpStart = nCnt ? GetScriptChg(nCnt - 1) : TextFrameIndex(0); + + // we go back in our group until we reach the first character of + // type nScript + while ( nChg > nGrpStart && + nScript != g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))) + --nChg; + + // If we are at the start of a group, we do not trust nScript, + // we better get nScript from the breakiterator: + if ( nChg == nGrpStart ) + nScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))); + + // INVALID DATA FROM THE SCRIPT INFO ARRAYS HAS TO BE DELETED: + + // remove invalid entries from script information arrays + m_ScriptChanges.erase(m_ScriptChanges.begin() + nCnt, m_ScriptChanges.end()); + + // get the start of the last compression group + TextFrameIndex nLastCompression = nChg; + if( nCntComp ) + { + --nCntComp; + nLastCompression = GetCompStart( nCntComp ); + if( nChg >= nLastCompression + GetCompLen( nCntComp ) ) + { + nLastCompression = nChg; + ++nCntComp; + } + } + + // remove invalid entries from compression information arrays + m_CompressionChanges.erase(m_CompressionChanges.begin() + nCntComp, + m_CompressionChanges.end()); + + // get the start of the last kashida group + TextFrameIndex nLastKashida = nChg; + if( nCntKash && i18n::ScriptType::COMPLEX == nScript ) + { + --nCntKash; + nLastKashida = GetKashida( nCntKash ); + } + + // remove invalid entries from kashida array + m_Kashida.erase(m_Kashida.begin() + nCntKash, m_Kashida.end()); + + // TAKE CARE OF WEAK CHARACTERS: WE MUST FIND AN APPROPRIATE + // SCRIPT FOR WEAK CHARACTERS AT THE BEGINNING OF A PARAGRAPH + + if (WEAK == g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))) + { + // If the beginning of the current group is weak, this means that + // all of the characters in this group are weak. We have to assign + // the scripts to these characters depending on the fonts which are + // set for these characters to display them. + TextFrameIndex nEnd( + g_pBreakIt->GetBreakIter()->endOfScript(rText, sal_Int32(nChg), WEAK)); + + if (nEnd > TextFrameIndex(rText.getLength()) || nEnd < TextFrameIndex(0)) + nEnd = TextFrameIndex(rText.getLength()); + + nScript = SvtLanguageOptions::GetI18NScriptTypeOfLanguage( GetAppLanguage() ); + + SAL_WARN_IF( i18n::ScriptType::LATIN != nScript && + i18n::ScriptType::ASIAN != nScript && + i18n::ScriptType::COMPLEX != nScript, "sw.core", "Wrong default language" ); + + nChg = nEnd; + + // Get next script type or set to weak in order to exit + sal_uInt8 nNextScript = (nEnd < TextFrameIndex(rText.getLength())) + ? static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nEnd))) + : sal_uInt8(WEAK); + + if ( nScript != nNextScript ) + { + m_ScriptChanges.emplace_back(nEnd, nScript); + nCnt++; + nScript = nNextScript; + } + } + + // UPDATE THE SCRIPT INFO ARRAYS: + + while (nChg < TextFrameIndex(rText.getLength()) + || (m_ScriptChanges.empty() && rText.isEmpty())) + { + SAL_WARN_IF( i18n::ScriptType::WEAK == nScript, + "sw.core", "Inserting WEAK into SwScriptInfo structure" ); + + TextFrameIndex nSearchStt = nChg; + nChg = TextFrameIndex(g_pBreakIt->GetBreakIter()->endOfScript( + rText, sal_Int32(nSearchStt), nScript)); + + if (nChg > TextFrameIndex(rText.getLength()) || nChg < TextFrameIndex(0)) + nChg = TextFrameIndex(rText.getLength()); + + // special case for dotted circle since it can be used with complex + // before a mark, so we want it associated with the mark's script + // tdf#112594: another special case for NNBSP followed by a Mongolian + // character, since NNBSP has special uses in Mongolian (tdf#112594) + auto nPos = sal_Int32(nChg); + auto nPrevPos = nPos; + auto nPrevChar = rText.iterateCodePoints(&nPrevPos, -1); + if (nChg < TextFrameIndex(rText.getLength()) && nChg > TextFrameIndex(0) && + i18n::ScriptType::WEAK == g_pBreakIt->GetBreakIter()->getScriptType(rText, nPrevPos)) + { + auto nChar = rText.iterateCodePoints(&nPos, 0); + auto nType = unicode::getUnicodeType(nChar); + if (nType == css::i18n::UnicodeType::NON_SPACING_MARK || + nType == css::i18n::UnicodeType::ENCLOSING_MARK || + nType == css::i18n::UnicodeType::COMBINING_SPACING_MARK || + (nPrevChar == CHAR_NNBSP && + u_getIntPropertyValue(nChar, UCHAR_SCRIPT) == USCRIPT_MONGOLIAN)) + { + nPos = nPrevPos; + } + } + m_ScriptChanges.emplace_back(TextFrameIndex(nPos), nScript); + ++nCnt; + + // if current script is asian, we search for compressible characters + // in this range + if ( CharCompressType::NONE != aCompEnum && + i18n::ScriptType::ASIAN == nScript ) + { + CompType ePrevState = NONE; + CompType eState = NONE; + TextFrameIndex nPrevChg = nLastCompression; + + while ( nLastCompression < nChg ) + { + sal_Unicode cChar = rText[ sal_Int32(nLastCompression) ]; + + // examine current character + switch ( cChar ) + { + // Left punctuation found + case 0x3008: case 0x300A: case 0x300C: case 0x300E: + case 0x3010: case 0x3014: case 0x3016: case 0x3018: + case 0x301A: case 0x301D: + case 0xFF08: case 0xFF3B: case 0xFF5B: + eState = SPECIAL_LEFT; + break; + // Right punctuation found + case 0x3009: case 0x300B: + case 0x300D: case 0x300F: case 0x3011: case 0x3015: + case 0x3017: case 0x3019: case 0x301B: case 0x301E: + case 0x301F: + case 0xFF09: case 0xFF3D: case 0xFF5D: + eState = SPECIAL_RIGHT; + break; + case 0x3001: case 0x3002: // Fullstop or comma + case 0xFF0C: case 0xFF0E: case 0xFF1A: case 0xFF1B: + eState = SPECIAL_MIDDLE ; + break; + default: + eState = ( 0x3040 <= cChar && 0x3100 > cChar ) ? KANA : NONE; + } + + // insert range of compressible characters + if( ePrevState != eState ) + { + if ( ePrevState != NONE ) + { + // insert start and type + if ( CharCompressType::PunctuationAndKana == aCompEnum || + ePrevState != KANA ) + { + m_CompressionChanges.emplace_back(nPrevChg, + nLastCompression - nPrevChg, ePrevState); + } + } + + ePrevState = eState; + nPrevChg = nLastCompression; + } + + nLastCompression++; + } + + // we still have to examine last entry + if ( ePrevState != NONE ) + { + // insert start and type + if ( CharCompressType::PunctuationAndKana == aCompEnum || + ePrevState != KANA ) + { + m_CompressionChanges.emplace_back(nPrevChg, + nLastCompression - nPrevChg, ePrevState); + } + } + } + + // we search for connecting opportunities (kashida) + else if ( bAdjustBlock && i18n::ScriptType::COMPLEX == nScript ) + { + // sw_redlinehide: this is the only place that uses SwScanner with + // frame text, so we convert to sal_Int32 here + std::function<LanguageType (sal_Int32, sal_Int32, bool)> const pGetLangOfCharM( + [&pMerged](sal_Int32 const nBegin, sal_uInt16 const script, bool const bNoChar) + { + std::pair<SwTextNode const*, sal_Int32> const pos( + sw::MapViewToModel(*pMerged, TextFrameIndex(nBegin))); + return pos.first->GetLang(pos.second, bNoChar ? 0 : 1, script); + }); + std::function<LanguageType (sal_Int32, sal_Int32, bool)> const pGetLangOfChar1( + [&rNode](sal_Int32 const nBegin, sal_uInt16 const script, bool const bNoChar) + { return rNode.GetLang(nBegin, bNoChar ? 0 : 1, script); }); + auto pGetLangOfChar(pMerged ? pGetLangOfCharM : pGetLangOfChar1); + SwScanner aScanner( std::move(pGetLangOfChar), rText, nullptr, ModelToViewHelper(), + i18n::WordType::DICTIONARY_WORD, + sal_Int32(nLastKashida), sal_Int32(nChg)); + + // the search has to be performed on a per word base + while ( aScanner.NextWord() ) + { + const OUString& rWord = aScanner.GetWord(); + + sal_Int32 nIdx = 0, nPrevIdx = 0; + sal_Int32 nKashidaPos = -1; + sal_Unicode cCh, cPrevCh = 0; + + int nPriorityLevel = 7; // 0..6 = level found + // 7 not found + + sal_Int32 nWordLen = rWord.getLength(); + + // ignore trailing vowel chars + while( nWordLen && isTransparentChar( rWord[ nWordLen - 1 ] )) + --nWordLen; + + while (nIdx < nWordLen) + { + cCh = rWord[ nIdx ]; + + // 1. Priority: + // after user inserted kashida + if ( 0x640 == cCh ) + { + nKashidaPos = aScanner.GetBegin() + nIdx; + nPriorityLevel = 0; + } + + // 2. Priority: + // after a Seen or Sad + if (nPriorityLevel >= 1 && nIdx < nWordLen - 1) + { + if( isSeenOrSadChar( cCh ) + && (rWord[ nIdx+1 ] != 0x200C) ) // #i98410#: prevent ZWNJ expansion + { + nKashidaPos = aScanner.GetBegin() + nIdx; + nPriorityLevel = 1; + } + } + + // 3. Priority: + // before final form of Teh Marbuta, Heh, Dal + if ( nPriorityLevel >= 2 && nIdx > 0 ) + { + if ( isTehMarbutaChar ( cCh ) || // Teh Marbuta (right joining) + isDalChar ( cCh ) || // Dal (right joining) final form may appear in the middle of word + ( isHehChar ( cCh ) && nIdx == nWordLen - 1)) // Heh (dual joining) only at end of word + { + + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 2; + } + } + } + + // 4. Priority: + // before final form of Alef, Tah, Lam, Kaf or Gaf + if ( nPriorityLevel >= 3 && nIdx > 0 ) + { + if ( isAlefChar ( cCh ) || // Alef (right joining) final form may appear in the middle of word + (( isLamChar ( cCh ) || // Lam, + isTahChar ( cCh ) || // Tah, + isKafChar ( cCh ) || // Kaf (all dual joining) + isGafChar ( cCh ) ) + && nIdx == nWordLen - 1)) // only at end of word + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 3; + } + } + } + + // 5. Priority: + // before medial Beh-like + if ( nPriorityLevel >= 4 && nIdx > 0 && nIdx < nWordLen - 1 ) + { + if ( isBehChar ( cCh ) ) + { + // check if next character is Reh or Yeh-like + sal_Unicode cNextCh = rWord[ nIdx + 1 ]; + if ( isRehChar ( cNextCh ) || isYehChar ( cNextCh )) + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 4; + } + } + } + } + + // 6. Priority: + // before the final form of Waw, Ain, Qaf and Feh + if ( nPriorityLevel >= 5 && nIdx > 0 ) + { + if ( isWawChar ( cCh ) || // Wav (right joining) + // final form may appear in the middle of word + (( isAinChar ( cCh ) || // Ain (dual joining) + isQafChar ( cCh ) || // Qaf (dual joining) + isFehChar ( cCh ) ) // Feh (dual joining) + && nIdx == nWordLen - 1)) // only at end of word + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 5; + } + } + } + + // other connecting possibilities + if ( nPriorityLevel >= 6 && nIdx > 0 ) + { + // Reh, Zain + if ( isRehChar ( cCh ) ) + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 6; + } + } + } + + // Do not consider vowel marks when checking if a character + // can be connected to previous character. + if ( !isTransparentChar ( cCh) ) + { + cPrevCh = cCh; + nPrevIdx = nIdx; + } + + ++nIdx; + } // end of current word + + if ( -1 != nKashidaPos ) + { + m_Kashida.insert(m_Kashida.begin() + nCntKash, TextFrameIndex(nKashidaPos)); + nCntKash++; + } + } // end of kashida search + } + + if (nChg < TextFrameIndex(rText.getLength())) + nScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))); + + nLastCompression = nChg; + nLastKashida = nChg; + } + +#if OSL_DEBUG_LEVEL > 0 + // check kashida data + TextFrameIndex nTmpKashidaPos(-1); + bool bWrongKash = false; + for (size_t i = 0; i < m_Kashida.size(); ++i) + { + TextFrameIndex nCurrKashidaPos = GetKashida( i ); + if ( nCurrKashidaPos <= nTmpKashidaPos ) + { + bWrongKash = true; + break; + } + nTmpKashidaPos = nCurrKashidaPos; + } + SAL_WARN_IF( bWrongKash, "sw.core", "Kashida array contains wrong data" ); +#endif + + // remove invalid entries from direction information arrays + m_DirectionChanges.clear(); + + // Perform Unicode Bidi Algorithm for text direction information + { + UpdateBidiInfo( rText ); + + // #i16354# Change script type for RTL text to CTL: + // 1. All text in RTL runs will use the CTL font + // #i89825# change the script type also to CTL (hennerdrewes) + // 2. Text in embedded LTR runs that does not have any strong LTR characters (numbers!) + for (size_t nDirIdx = 0; nDirIdx < m_DirectionChanges.size(); ++nDirIdx) + { + const sal_uInt8 nCurrDirType = GetDirType( nDirIdx ); + // nStart is start of RTL run: + const TextFrameIndex nStart = nDirIdx > 0 ? GetDirChg(nDirIdx - 1) : TextFrameIndex(0); + // nEnd is end of RTL run: + const TextFrameIndex nEnd = GetDirChg( nDirIdx ); + + if ( nCurrDirType % 2 == UBIDI_RTL || // text in RTL run + (nCurrDirType > UBIDI_LTR && // non-strong text in embedded LTR run + !lcl_HasStrongLTR(rText, sal_Int32(nStart), sal_Int32(nEnd)))) + { + // nScriptIdx points into the ScriptArrays: + size_t nScriptIdx = 0; + + // Skip entries in ScriptArray which are not inside the RTL run: + // Make nScriptIdx become the index of the script group with + // 1. nStartPosOfGroup <= nStart and + // 2. nEndPosOfGroup > nStart + while ( GetScriptChg( nScriptIdx ) <= nStart ) + ++nScriptIdx; + + const TextFrameIndex nStartPosOfGroup = nScriptIdx + ? GetScriptChg(nScriptIdx - 1) + : TextFrameIndex(0); + const sal_uInt8 nScriptTypeOfGroup = GetScriptType( nScriptIdx ); + + SAL_WARN_IF( nStartPosOfGroup > nStart || GetScriptChg( nScriptIdx ) <= nStart, + "sw.core", "Script override with CTL font trouble" ); + + // Check if we have to insert a new script change at + // position nStart. If nStartPosOfGroup < nStart, + // we have to insert a new script change: + if (nStart > TextFrameIndex(0) && nStartPosOfGroup < nStart) + { + m_ScriptChanges.insert(m_ScriptChanges.begin() + nScriptIdx, + ScriptChangeInfo(nStart, nScriptTypeOfGroup) ); + ++nScriptIdx; + } + + // Remove entries in ScriptArray which end inside the RTL run: + while (nScriptIdx < m_ScriptChanges.size() + && GetScriptChg(nScriptIdx) <= nEnd) + { + m_ScriptChanges.erase(m_ScriptChanges.begin() + nScriptIdx); + } + + // Insert a new entry in ScriptArray for the end of the RTL run: + m_ScriptChanges.insert(m_ScriptChanges.begin() + nScriptIdx, + ScriptChangeInfo(nEnd, i18n::ScriptType::COMPLEX) ); + +#if OSL_DEBUG_LEVEL > 1 + // Check that ScriptChangeInfos are in increasing order of + // position and that we don't have "empty" changes. + sal_uInt8 nLastTyp = i18n::ScriptType::WEAK; + TextFrameIndex nLastPos = TextFrameIndex(0); + for (const auto& rScriptChange : m_ScriptChanges) + { + SAL_WARN_IF( nLastTyp == rScriptChange.type || + nLastPos >= rScriptChange.position, + "sw.core", "Heavy InitScriptType() confusion" ); + nLastPos = rScriptChange.position; + nLastTyp = rScriptChange.type; + } +#endif + } + } + } +} + +void SwScriptInfo::UpdateBidiInfo( const OUString& rText ) +{ + // remove invalid entries from direction information arrays + m_DirectionChanges.clear(); + + // Bidi functions from icu 2.0 + + UErrorCode nError = U_ZERO_ERROR; + UBiDi* pBidi = ubidi_openSized( rText.getLength(), 0, &nError ); + nError = U_ZERO_ERROR; + + ubidi_setPara( pBidi, reinterpret_cast<const UChar *>(rText.getStr()), rText.getLength(), + m_nDefaultDir, nullptr, &nError ); + nError = U_ZERO_ERROR; + int nCount = ubidi_countRuns( pBidi, &nError ); + int32_t nStart = 0; + int32_t nEnd; + UBiDiLevel nCurrDir; + for ( int nIdx = 0; nIdx < nCount; ++nIdx ) + { + ubidi_getLogicalRun( pBidi, nStart, &nEnd, &nCurrDir ); + m_DirectionChanges.emplace_back(TextFrameIndex(nEnd), nCurrDir); + nStart = nEnd; + } + + ubidi_close( pBidi ); +} + +// returns the position of the next character which belongs to another script +// than the character of the actual (input) position. +// If there's no script change until the end of the paragraph, it will return +// COMPLETE_STRING. +// Scripts are Asian (Chinese, Japanese, Korean), +// Latin ( English etc.) +// and Complex ( Hebrew, Arabian ) +TextFrameIndex SwScriptInfo::NextScriptChg(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountScriptChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetScriptChg( nX ) ) + return GetScriptChg( nX ); + } + + return TextFrameIndex(COMPLETE_STRING); +} + +// returns the script of the character at the input position +sal_Int16 SwScriptInfo::ScriptType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountScriptChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetScriptChg( nX ) ) + return GetScriptType( nX ); + } + + // the default is the application language script + return SvtLanguageOptions::GetI18NScriptTypeOfLanguage( GetAppLanguage() ); +} + +TextFrameIndex SwScriptInfo::NextDirChg(const TextFrameIndex nPos, + const sal_uInt8* pLevel ) const +{ + const sal_uInt8 nCurrDir = pLevel ? *pLevel : 62; + const size_t nEnd = CountDirChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetDirChg( nX ) && + ( nX + 1 == nEnd || GetDirType( nX + 1 ) <= nCurrDir ) ) + return GetDirChg( nX ); + } + + return TextFrameIndex(COMPLETE_STRING); +} + +sal_uInt8 SwScriptInfo::DirType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountDirChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetDirChg( nX ) ) + return GetDirType( nX ); + } + + return 0; +} + +TextFrameIndex SwScriptInfo::NextHiddenChg(TextFrameIndex const nPos) const +{ + for (auto const& it : m_HiddenChg) + { + if (nPos < it) + { + return it; + } + } + return TextFrameIndex(COMPLETE_STRING); +} + +TextFrameIndex SwScriptInfo::NextBookmark(TextFrameIndex const nPos) const +{ + for (auto const& it : m_Bookmarks) + { + if (nPos < std::get<0>(it)) + { + return std::get<0>(it); + } + } + return TextFrameIndex(COMPLETE_STRING); +} + +std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>> + SwScriptInfo::GetBookmarks(TextFrameIndex const nPos) +{ + std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>> aColors; + for (auto const& it : m_Bookmarks) + { + if (nPos == std::get<0>(it)) + { + const OUString& sName = std::get<3>(it); + // filter hidden bookmarks imported from OOXML + // TODO import them as hidden bookmarks + if ( !( sName.startsWith("_Toc") || sName.startsWith("_Ref") ) ) + aColors.push_back(std::tuple<MarkKind, Color, + OUString>(std::get<1>(it), std::get<2>(it), std::get<3>(it))); + } + else if (nPos < std::get<0>(it)) + { + break; + } + } + + // sort bookmark boundary marks at the same position + // mark order: ] | [ + // color order: [c1 [c2 [c3 ... c3] c2] c1] + sort(aColors.begin(), aColors.end(), + [](std::tuple<MarkKind, Color, OUString> const a, std::tuple<MarkKind, Color, OUString> const b) { + return (MarkKind::End == std::get<0>(a) && MarkKind::End != std::get<0>(b)) || + (MarkKind::Point == std::get<0>(a) && MarkKind::Start == std::get<0>(b)) || + // if both are end or start, order by color + (MarkKind::End == std::get<0>(a) && MarkKind::End == std::get<0>(b) && std::get<1>(a) < std::get<1>(b)) || + (MarkKind::Start == std::get<0>(a) && MarkKind::Start == std::get<0>(b) && std::get<1>(b) < std::get<1>(a));}); + + return aColors; +} + +// Takes a string and replaced the hidden ranges with cChar. +sal_Int32 SwScriptInfo::MaskHiddenRanges( const SwTextNode& rNode, OUStringBuffer & rText, + const sal_Int32 nStt, const sal_Int32 nEnd, + const sal_Unicode cChar ) +{ + assert(rNode.GetText().getLength() == rText.getLength()); + + std::vector<sal_Int32> aList; + sal_Int32 nHiddenStart; + sal_Int32 nHiddenEnd; + sal_Int32 nNumOfHiddenChars = 0; + GetBoundsOfHiddenRange( rNode, 0, nHiddenStart, nHiddenEnd, &aList ); + auto rFirst( aList.crbegin() ); + auto rLast( aList.crend() ); + while ( rFirst != rLast ) + { + nHiddenEnd = *(rFirst++); + nHiddenStart = *(rFirst++); + + if ( nHiddenEnd < nStt || nHiddenStart > nEnd ) + continue; + + while ( nHiddenStart < nHiddenEnd && nHiddenStart < nEnd ) + { + if (nHiddenStart >= nStt) + { + rText[nHiddenStart] = cChar; + ++nNumOfHiddenChars; + } + ++nHiddenStart; + } + } + + return nNumOfHiddenChars; +} + +// Takes a SwTextNode and deletes the hidden ranges from the node. +void SwScriptInfo::DeleteHiddenRanges( SwTextNode& rNode ) +{ + std::vector<sal_Int32> aList; + sal_Int32 nHiddenStart; + sal_Int32 nHiddenEnd; + GetBoundsOfHiddenRange( rNode, 0, nHiddenStart, nHiddenEnd, &aList ); + auto rFirst( aList.crbegin() ); + auto rLast( aList.crend() ); + while ( rFirst != rLast ) + { + nHiddenEnd = *(rFirst++); + nHiddenStart = *(rFirst++); + + SwPaM aPam( rNode, nHiddenStart, rNode, nHiddenEnd ); + rNode.getIDocumentContentOperations().DeleteRange( aPam ); + } +} + +bool SwScriptInfo::GetBoundsOfHiddenRange( const SwTextNode& rNode, sal_Int32 nPos, + sal_Int32& rnStartPos, sal_Int32& rnEndPos, + std::vector<sal_Int32>* pList ) +{ + rnStartPos = COMPLETE_STRING; + rnEndPos = 0; + + bool bNewContainsHiddenChars = false; + + // Optimization: First examine the flags at the text node: + + if ( !rNode.IsCalcHiddenCharFlags() ) + { + bool bWholePara = rNode.HasHiddenCharAttribute( true ); + bool bContainsHiddenChars = rNode.HasHiddenCharAttribute( false ); + if ( !bContainsHiddenChars ) + return false; + + if ( bWholePara ) + { + if ( pList ) + { + pList->push_back( 0 ); + pList->push_back(rNode.GetText().getLength()); + } + + rnStartPos = 0; + rnEndPos = rNode.GetText().getLength(); + return true; + } + } + + // sw_redlinehide: this won't work if it's merged +#if 0 + const SwScriptInfo* pSI = SwScriptInfo::GetScriptInfo( rNode ); + if ( pSI ) + { + + // Check first, if we have a valid SwScriptInfo object for this text node: + + bNewContainsHiddenChars = pSI->GetBoundsOfHiddenRange( nPos, rnStartPos, rnEndPos, pList ); + const bool bNewHiddenCharsHidePara = + rnStartPos == 0 && rnEndPos >= rNode.GetText().getLength(); + rNode.SetHiddenCharAttribute( bNewHiddenCharsHidePara, bNewContainsHiddenChars ); + } + else +#endif + { + + // No valid SwScriptInfo Object, we have to do it the hard way: + + Range aRange(0, (!rNode.GetText().isEmpty()) + ? rNode.GetText().getLength() - 1 + : 0); + MultiSelection aHiddenMulti( aRange ); + SwScriptInfo::CalcHiddenRanges(rNode, aHiddenMulti, nullptr); + for( sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i ) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nHiddenStart = rRange.Min(); + const sal_Int32 nHiddenEnd = rRange.Max() + 1; + + if ( nHiddenStart > nPos ) + break; + if (nPos < nHiddenEnd) + { + rnStartPos = nHiddenStart; + rnEndPos = std::min<sal_Int32>(nHiddenEnd, + rNode.GetText().getLength()); + break; + } + } + + if ( pList ) + { + for( sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i ) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + pList->push_back( rRange.Min() ); + pList->push_back( rRange.Max() + 1 ); + } + } + + bNewContainsHiddenChars = aHiddenMulti.GetRangeCount() > 0; + } + + return bNewContainsHiddenChars; +} + +bool SwScriptInfo::GetBoundsOfHiddenRange(TextFrameIndex nPos, + TextFrameIndex & rnStartPos, TextFrameIndex & rnEndPos) const +{ + rnStartPos = TextFrameIndex(COMPLETE_STRING); + rnEndPos = TextFrameIndex(0); + + const size_t nEnd = CountHiddenChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + const TextFrameIndex nHiddenStart = GetHiddenChg( nX++ ); + const TextFrameIndex nHiddenEnd = GetHiddenChg( nX ); + + if ( nHiddenStart > nPos ) + break; + if (nPos < nHiddenEnd) + { + rnStartPos = nHiddenStart; + rnEndPos = nHiddenEnd; + break; + } + } + + return CountHiddenChg() > 0; +} + +bool SwScriptInfo::IsInHiddenRange( const SwTextNode& rNode, sal_Int32 nPos ) +{ + sal_Int32 nStartPos; + sal_Int32 nEndPos; + SwScriptInfo::GetBoundsOfHiddenRange( rNode, nPos, nStartPos, nEndPos ); + return nStartPos != COMPLETE_STRING; +} + +#ifdef DBG_UTIL +// returns the type of the compressed character +SwScriptInfo::CompType SwScriptInfo::DbgCompType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountCompChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + const TextFrameIndex nChg = GetCompStart(nX); + + if ( nPos < nChg ) + return NONE; + + if( nPos < nChg + GetCompLen( nX ) ) + return GetCompType( nX ); + } + return NONE; +} +#endif + +// returns, if there are compressible kanas or specials +// between nStart and nEnd +size_t SwScriptInfo::HasKana(TextFrameIndex const nStart, TextFrameIndex const nLen) const +{ + const size_t nCnt = CountCompChg(); + TextFrameIndex nEnd = nStart + nLen; + + for( size_t nX = 0; nX < nCnt; ++nX ) + { + TextFrameIndex nKanaStart = GetCompStart(nX); + TextFrameIndex nKanaEnd = nKanaStart + GetCompLen(nX); + + if ( nKanaStart >= nEnd ) + return SAL_MAX_SIZE; + + if ( nStart < nKanaEnd ) + return nX; + } + + return SAL_MAX_SIZE; +} + +tools::Long SwScriptInfo::Compress(KernArray& rKernArray, TextFrameIndex nIdx, TextFrameIndex nLen, + const sal_uInt16 nCompress, const sal_uInt16 nFontHeight, + bool bCenter, + Point* pPoint ) const +{ + SAL_WARN_IF( !nCompress, "sw.core", "Compression without compression?!" ); + SAL_WARN_IF( !nLen, "sw.core", "Compression without text?!" ); + const size_t nCompCount = CountCompChg(); + + // In asian typography, there are full width and half width characters. + // Full width punctuation characters can be compressed by 50% + // to determine this, we compare the font width with 75% of its height + const tools::Long nMinWidth = ( 3 * nFontHeight ) / 4; + + size_t nCompIdx = HasKana( nIdx, nLen ); + + if ( SAL_MAX_SIZE == nCompIdx ) + return 0; + + TextFrameIndex nChg = GetCompStart( nCompIdx ); + TextFrameIndex nCompLen = GetCompLen( nCompIdx ); + sal_Int32 nI = 0; + nLen += nIdx; + + if( nChg > nIdx ) + { + nI = sal_Int32(nChg - nIdx); + nIdx = nChg; + } + else if( nIdx < nChg + nCompLen ) + nCompLen -= nIdx - nChg; + + if( nIdx > nLen || nCompIdx >= nCompCount ) + return 0; + + tools::Long nSub = 0; + tools::Long nLast = nI ? rKernArray[ nI - 1 ] : 0; + do + { + const CompType nType = GetCompType( nCompIdx ); +#ifdef DBG_UTIL + SAL_WARN_IF( nType != DbgCompType( nIdx ), "sw.core", "Gimme the right type!" ); +#endif + nCompLen += nIdx; + if( nCompLen > nLen ) + nCompLen = nLen; + + // are we allowed to compress the character? + if ( rKernArray[ nI ] - nLast < nMinWidth ) + { + nIdx++; nI++; + } + else + { + while( nIdx < nCompLen ) + { + SAL_WARN_IF( SwScriptInfo::NONE == nType, "sw.core", "None compression?!" ); + + // nLast is width of current character + nLast -= rKernArray[ nI ]; + + nLast *= nCompress; + tools::Long nMove = 0; + if( SwScriptInfo::KANA != nType ) + { + nLast /= 24000; + if( pPoint && SwScriptInfo::SPECIAL_LEFT == nType ) + { + if( nI ) + nMove = nLast; + else + { + pPoint->AdjustX(nLast ); + nLast = 0; + } + } + else if( bCenter && SwScriptInfo::SPECIAL_MIDDLE == nType ) + nMove = nLast / 2; + } + else + nLast /= 100000; + nSub -= nLast; + nLast = rKernArray[ nI ]; + if( nI && nMove ) + rKernArray.adjust(nI - 1, nMove); + rKernArray.adjust(nI, -nSub); + ++nI; + ++nIdx; + } + } + + if( nIdx >= nLen ) + break; + + TextFrameIndex nTmpChg = nLen; + if( ++nCompIdx < nCompCount ) + { + nTmpChg = GetCompStart( nCompIdx ); + if( nTmpChg > nLen ) + nTmpChg = nLen; + nCompLen = GetCompLen( nCompIdx ); + } + + while( nIdx < nTmpChg ) + { + nLast = rKernArray[ nI ]; + rKernArray.adjust(nI, -nSub); + ++nI; + ++nIdx; + } + } while( nIdx < nLen ); + return nSub; +} + +// Note on calling KashidaJustify(): +// Kashida positions may be marked as invalid. Therefore KashidaJustify may return the clean +// total number of kashida positions, or the number of kashida positions after some positions +// have been dropped, depending on the state of the m_KashidaInvalid set. + +sal_Int32 SwScriptInfo::KashidaJustify( KernArray* pKernArray, + sal_Bool* pKashidaArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, + tools::Long nSpaceAdd ) const +{ + SAL_WARN_IF( !nLen, "sw.core", "Kashida justification without text?!" ); + + if( !IsKashidaLine(nStt)) + return -1; + + // evaluate kashida information in collected in SwScriptInfo + + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + ++nCntKash; + } + + const TextFrameIndex nEnd = nStt + nLen; + + size_t nCntKashEnd = nCntKash; + while ( nCntKashEnd < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKashEnd ) ) + break; + ++nCntKashEnd; + } + + size_t nActualKashCount = nCntKashEnd - nCntKash; + for (size_t i = nCntKash; i < nCntKashEnd; ++i) + { + if ( nActualKashCount && !IsKashidaValid ( i ) ) + --nActualKashCount; + } + + if ( !pKernArray ) + return nActualKashCount; + + // do nothing if there is no more kashida + if ( nCntKash < CountKashida() ) + { + // skip any invalid kashidas + while (nCntKash < nCntKashEnd && !IsKashidaValid(nCntKash)) + ++nCntKash; + + TextFrameIndex nIdx = nCntKash < nCntKashEnd && IsKashidaValid(nCntKash) + ? GetKashida(nCntKash) + : nEnd; + tools::Long nKashAdd = nSpaceAdd; + + while ( nIdx < nEnd ) + { + TextFrameIndex nArrayPos = nIdx - nStt; + + // mark Kashida insertion positions, code in VCL will use this + // array to know where to insert Kashida. + if (pKashidaArray) + pKashidaArray[sal_Int32(nArrayPos)] = true; + + // next kashida position + ++nCntKash; + while (nCntKash < nCntKashEnd && !IsKashidaValid(nCntKash)) + ++nCntKash; + + nIdx = nCntKash < nCntKashEnd && IsKashidaValid(nCntKash) ? GetKashida(nCntKash) : nEnd; + if ( nIdx > nEnd ) + nIdx = nEnd; + + const TextFrameIndex nArrayEnd = nIdx - nStt; + + while ( nArrayPos < nArrayEnd ) + { + pKernArray->adjust(sal_Int32(nArrayPos), nKashAdd); + ++nArrayPos; + } + nKashAdd += nSpaceAdd; + } + } + + return 0; +} + +// Checks if the current text is 'Arabic' text. Note that only the first +// character has to be checked because a ctl portion only contains one +// script, see NewTextPortion +bool SwScriptInfo::IsArabicText(const OUString& rText, + TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + using namespace ::com::sun::star::i18n; + static const ScriptTypeList typeList[] = { + { UnicodeScript_kArabic, UnicodeScript_kArabic, sal_Int16(UnicodeScript_kArabic) }, // 11, + { UnicodeScript_kScriptCount, UnicodeScript_kScriptCount, sal_Int16(UnicodeScript_kScriptCount) } // 88 + }; + + // go forward if current position does not hold a regular character: + const CharClass& rCC = GetAppCharClass(); + sal_Int32 nIdx = sal_Int32(nStt); + const sal_Int32 nEnd = sal_Int32(nStt + nLen); + while ( nIdx < nEnd && !rCC.isLetterNumeric( rText, nIdx ) ) + { + ++nIdx; + } + + if( nIdx == nEnd ) + { + // no regular character found in this portion. Go backward: + --nIdx; + while ( nIdx >= 0 && !rCC.isLetterNumeric( rText, nIdx ) ) + { + --nIdx; + } + } + + if( nIdx >= 0 ) + { + const sal_Unicode cCh = rText[nIdx]; + const sal_Int16 type = unicode::getUnicodeScriptType( cCh, typeList, sal_Int16(UnicodeScript_kScriptCount) ); + return type == sal_Int16(UnicodeScript_kArabic); + } + return false; +} + +bool SwScriptInfo::IsKashidaValid(size_t const nKashPos) const +{ + return m_KashidaInvalid.find(nKashPos) == m_KashidaInvalid.end(); +} + +void SwScriptInfo::ClearKashidaInvalid(size_t const nKashPos) +{ + m_KashidaInvalid.erase(nKashPos); +} + +// bMark == true: +// marks the first valid kashida in the given text range as invalid +// bMark == false: +// clears all kashida invalid flags in the given text range +bool SwScriptInfo::MarkOrClearKashidaInvalid( + TextFrameIndex const nStt, TextFrameIndex const nLen, + bool bMark, sal_Int32 nMarkCount) +{ + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + + const TextFrameIndex nEnd = nStt + nLen; + + while ( nCntKash < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKash ) ) + break; + if(bMark) + { + if ( MarkKashidaInvalid ( nCntKash ) ) + { + --nMarkCount; + if (!nMarkCount) + return true; + } + } + else + { + ClearKashidaInvalid ( nCntKash ); + } + nCntKash++; + } + return false; +} + +bool SwScriptInfo::MarkKashidaInvalid(size_t const nKashPos) +{ + return m_KashidaInvalid.insert(nKashPos).second; +} + +// retrieve the kashida positions in the given text range +void SwScriptInfo::GetKashidaPositions( + TextFrameIndex const nStt, TextFrameIndex const nLen, + std::vector<TextFrameIndex>& rKashidaPosition) +{ + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + + const TextFrameIndex nEnd = nStt + nLen; + + size_t nCntKashEnd = nCntKash; + while ( nCntKashEnd < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKashEnd ) ) + break; + rKashidaPosition.push_back(GetKashida(nCntKashEnd)); + nCntKashEnd++; + } +} + +void SwScriptInfo::SetNoKashidaLine(TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + m_NoKashidaLine.push_back( nStt ); + m_NoKashidaLineEnd.push_back( nStt + nLen ); +} + +// determines if the line uses kashida justification +bool SwScriptInfo::IsKashidaLine(TextFrameIndex const nCharIdx) const +{ + for (size_t i = 0; i < m_NoKashidaLine.size(); ++i) + { + if (nCharIdx >= m_NoKashidaLine[i] && nCharIdx < m_NoKashidaLineEnd[i]) + return false; + } + return true; +} + +void SwScriptInfo::ClearNoKashidaLine(TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + size_t i = 0; + while (i < m_NoKashidaLine.size()) + { + if (nStt + nLen >= m_NoKashidaLine[i] && nStt < m_NoKashidaLineEnd[i]) + { + m_NoKashidaLine.erase(m_NoKashidaLine.begin() + i); + m_NoKashidaLineEnd.erase(m_NoKashidaLineEnd.begin() + i); + } + else + ++i; + } +} + +// mark the given character indices as invalid kashida positions +void SwScriptInfo::MarkKashidasInvalid(sal_Int32 const nCnt, + const TextFrameIndex* pKashidaPositions) +{ + SAL_WARN_IF( !pKashidaPositions || nCnt == 0, "sw.core", "Where are kashidas?" ); + + size_t nCntKash = 0; + sal_Int32 nKashidaPosIdx = 0; + + while (nCntKash < CountKashida() && nKashidaPosIdx < nCnt) + { + if ( pKashidaPositions [nKashidaPosIdx] > GetKashida( nCntKash ) ) + { + ++nCntKash; + continue; + } + + if ( pKashidaPositions [nKashidaPosIdx] != GetKashida( nCntKash ) || !IsKashidaValid ( nCntKash ) ) + return; // something is wrong + + MarkKashidaInvalid ( nCntKash ); + nKashidaPosIdx++; + } +} + +TextFrameIndex SwScriptInfo::ThaiJustify( std::u16string_view aText, KernArray* pKernArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, + TextFrameIndex nNumberOfBlanks, + tools::Long nSpaceAdd ) +{ + SAL_WARN_IF( nStt + nLen > TextFrameIndex(aText.size()), "sw.core", "String in ThaiJustify too small" ); + + SwTwips nNumOfTwipsToDistribute = nSpaceAdd * sal_Int32(nNumberOfBlanks) / + SPACING_PRECISION_FACTOR; + + tools::Long nSpaceSum = 0; + TextFrameIndex nCnt(0); + + for (sal_Int32 nI = 0; nI < sal_Int32(nLen); ++nI) + { + const sal_Unicode cCh = aText[sal_Int32(nStt) + nI]; + + // check if character is not above or below base + if ( ( 0xE34 > cCh || cCh > 0xE3A ) && + ( 0xE47 > cCh || cCh > 0xE4E ) && cCh != 0xE31 ) + { + if (nNumberOfBlanks > TextFrameIndex(0)) + { + nSpaceAdd = nNumOfTwipsToDistribute / sal_Int32(nNumberOfBlanks); + --nNumberOfBlanks; + nNumOfTwipsToDistribute -= nSpaceAdd; + } + nSpaceSum += nSpaceAdd; + ++nCnt; + } + + if (pKernArray) + pKernArray->adjust(nI, nSpaceSum); + } + + return nCnt; +} + +SwScriptInfo* SwScriptInfo::GetScriptInfo( const SwTextNode& rTNd, + SwTextFrame const**const o_ppFrame, + bool const bAllowInvalid) +{ + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(rTNd); + SwScriptInfo* pScriptInfo = nullptr; + + for( SwTextFrame* pLast = aIter.First(); pLast; pLast = aIter.Next() ) + { + pScriptInfo = const_cast<SwScriptInfo*>(pLast->GetScriptInfo()); + if ( pScriptInfo ) + { + if (bAllowInvalid || + TextFrameIndex(COMPLETE_STRING) == pScriptInfo->GetInvalidityA()) + { + if (o_ppFrame) + { + *o_ppFrame = pLast; + } + break; + } + pScriptInfo = nullptr; + } + } + + return pScriptInfo; +} + +SwParaPortion::SwParaPortion() +{ + FormatReset(); + m_bFlys = m_bFootnoteNum = m_bMargin = false; + SetWhichPor( PortionType::Para ); +} + +SwParaPortion::~SwParaPortion() +{ +} + +TextFrameIndex SwParaPortion::GetParLen() const +{ + TextFrameIndex nLen(0); + const SwLineLayout *pLay = this; + while( pLay ) + { + nLen += pLay->GetLen(); + pLay = pLay->GetNext(); + } + return nLen; +} + +bool SwParaPortion::HasNumberingPortion(FootnoteOrNot const eFootnote) const +{ + SwLinePortion const* pPortion(nullptr); + // the first line may contain only fly portion... + for (SwLineLayout const* pLine = this; pLine && !pPortion; pLine = pLine->GetNext()) + { + pPortion = pLine->GetFirstPortion(); + while (pPortion && (pPortion->InGlueGrp() || pPortion->IsKernPortion() || pPortion->IsFlyPortion())) + { // skip margins and fly spacers - numbering should be first then + pPortion = pPortion->GetNextPortion(); + } + } + if (pPortion && pPortion->InHyphGrp()) + { // weird special case, bullet with soft hyphen + pPortion = pPortion->GetNextPortion(); + } + return pPortion && pPortion->InNumberGrp() + && (eFootnote == SwParaPortion::FootnoteToo || !pPortion->IsFootnoteNumPortion()); +} + +bool SwParaPortion::HasContentPortions() const +{ + SwLinePortion const* pPortion(nullptr); + for (SwLineLayout const* pLine = this; pLine && !pPortion; pLine = pLine->GetNext()) + { + pPortion = pLine->GetFirstPortion(); + while (pPortion && (pPortion->InGlueGrp() || pPortion->IsKernPortion() || pPortion->IsFlyPortion())) + { // skip margins and fly spacers + pPortion = pPortion->GetNextPortion(); + } + } + return pPortion != nullptr; +} + +const SwDropPortion *SwParaPortion::FindDropPortion() const +{ + const SwLineLayout *pLay = this; + while( pLay && pLay->IsDummy() ) + pLay = pLay->GetNext(); + while( pLay ) + { + const SwLinePortion *pPos = pLay->GetNextPortion(); + while ( pPos && !pPos->GetLen() ) + pPos = pPos->GetNextPortion(); + if( pPos && pPos->IsDropPortion() ) + return static_cast<const SwDropPortion *>(pPos); + pLay = pLay->GetLen() ? nullptr : pLay->GetNext(); + } + return nullptr; +} + +void SwParaPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwParaPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwLineLayout::Init( SwLinePortion* pNextPortion ) +{ + Height( 0, false ); + Width( 0 ); + SetLen(TextFrameIndex(0)); + SetAscent( 0 ); + SetRealHeight( 0 ); + SetNextPortion( pNextPortion ); +} + +// looks for hanging punctuation portions in the paragraph +// and return the maximum right offset of them. +// If no such portion is found, the Margin/Hanging-flags will be updated. +SwTwips SwLineLayout::GetHangingMargin_() const +{ + SwLinePortion* pPor = GetNextPortion(); + bool bFound = false; + SwTwips nDiff = 0; + while( pPor) + { + if( pPor->IsHangingPortion() ) + { + nDiff = static_cast<SwHangingPortion*>(pPor)->GetInnerWidth() - pPor->Width(); + if( nDiff ) + bFound = true; + } + // the last post its portion + else if ( pPor->IsPostItsPortion() && ! pPor->GetNextPortion() ) + nDiff = mnAscent; + + pPor = pPor->GetNextPortion(); + } + if( !bFound ) // update the hanging-flag + const_cast<SwLineLayout*>(this)->SetHanging( false ); + return nDiff; +} + +SwTwips SwTextFrame::HangingMargin() const +{ + SAL_WARN_IF( !HasPara(), "sw.core", "Don't call me without a paraportion" ); + if( !GetPara()->IsMargin() ) + return 0; + const SwLineLayout* pLine = GetPara(); + SwTwips nRet = 0; + do + { + SwTwips nDiff = pLine->GetHangingMargin(); + if( nDiff > nRet ) + nRet = nDiff; + pLine = pLine->GetNext(); + } while ( pLine ); + if( !nRet ) // update the margin-flag + const_cast<SwParaPortion*>(GetPara())->SetMargin( false ); + return nRet; +} + +SwTwips SwTextFrame::GetLowerMarginForFlyIntersect() const +{ + const IDocumentSettingAccess& rIDSA = GetDoc().getIDocumentSettingAccess(); + if (!rIDSA.get(DocumentSettingId::TAB_OVER_MARGIN)) + { + // Word >= 2013 style or Writer style: lower margin is ignored when determining the text + // frame height. + return 0; + } + + const SwAttrSet* pAttrSet = GetTextNodeForParaProps()->GetpSwAttrSet(); + if (!pAttrSet) + { + return 0; + } + + // If it has multiple lines, then probably it already has the needed fly portion. + // Limit this to empty paragraphs for now. + if ((GetPara() && GetPara()->GetNext()) || !GetText().isEmpty()) + { + return 0; + } + + return pAttrSet->GetULSpace().GetLower(); +} + +void SwScriptInfo::selectHiddenTextProperty(const SwTextNode& rNode, + MultiSelection & rHiddenMulti, + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> *const pBookmarks) +{ + assert((rNode.GetText().isEmpty() && rHiddenMulti.GetTotalRange().Len() == 1) + || (rNode.GetText().getLength() == rHiddenMulti.GetTotalRange().Len())); + + const SvxCharHiddenItem* pItem = rNode.GetSwAttrSet().GetItemIfSet( RES_CHRATR_HIDDEN ); + if( pItem && pItem->GetValue() ) + { + rHiddenMulti.SelectAll(); + } + + const SwpHints* pHints = rNode.GetpSwpHints(); + + if( pHints ) + { + for( size_t nTmp = 0; nTmp < pHints->Count(); ++nTmp ) + { + const SwTextAttr* pTextAttr = pHints->Get( nTmp ); + const SvxCharHiddenItem* pHiddenItem = CharFormat::GetItem( *pTextAttr, RES_CHRATR_HIDDEN ); + if( pHiddenItem ) + { + const sal_Int32 nSt = pTextAttr->GetStart(); + const sal_Int32 nEnd = *pTextAttr->End(); + if( nEnd > nSt ) + { + Range aTmp( nSt, nEnd - 1 ); + rHiddenMulti.Select( aTmp, pHiddenItem->GetValue() ); + } + } + } + } + + for (const SwContentIndex* pIndex = rNode.GetFirstIndex(); pIndex; pIndex = pIndex->GetNext()) + { + const sw::mark::IMark* pMark = pIndex->GetMark(); + const sw::mark::IBookmark* pBookmark = dynamic_cast<const sw::mark::IBookmark*>(pMark); + if (pBookmarks && pBookmark) + { + if (!pBookmark->IsExpanded()) + { + pBookmarks->emplace_back(pBookmark, MarkKind::Point); + } + else if (pIndex == &pBookmark->GetMarkStart().nContent) + { + pBookmarks->emplace_back(pBookmark, MarkKind::Start); + } + else + { + assert(pIndex == &pBookmark->GetMarkEnd().nContent); + pBookmarks->emplace_back(pBookmark, MarkKind::End); + } + } + + // condition is evaluated in DocumentFieldsManager::UpdateExpFields() + if (pBookmark && pBookmark->IsHidden()) + { + // intersect bookmark range with textnode range and add the intersection to rHiddenMulti + + const sal_Int32 nSt = pBookmark->GetMarkStart().GetContentIndex(); + const sal_Int32 nEnd = pBookmark->GetMarkEnd().GetContentIndex(); + + if( nEnd > nSt ) + { + Range aTmp( nSt, nEnd - 1 ); + rHiddenMulti.Select(aTmp, true); + } + } + } +} + +void SwScriptInfo::selectRedLineDeleted(const SwTextNode& rNode, MultiSelection &rHiddenMulti, bool bSelect) +{ + assert((rNode.GetText().isEmpty() && rHiddenMulti.GetTotalRange().Len() == 1) + || (rNode.GetText().getLength() == rHiddenMulti.GetTotalRange().Len())); + + const IDocumentRedlineAccess& rIDRA = rNode.getIDocumentRedlineAccess(); + if ( !IDocumentRedlineAccess::IsShowChanges( rIDRA.GetRedlineFlags() ) ) + return; + + SwRedlineTable::size_type nAct = rIDRA.GetRedlinePos( rNode, RedlineType::Any ); + + for ( ; nAct < rIDRA.GetRedlineTable().size(); nAct++ ) + { + const SwRangeRedline* pRed = rIDRA.GetRedlineTable()[ nAct ]; + + if (pRed->Start()->GetNode() > rNode) + break; + + if (pRed->GetType() != RedlineType::Delete) + continue; + + sal_Int32 nRedlStart; + sal_Int32 nRedlnEnd; + pRed->CalcStartEnd( rNode.GetIndex(), nRedlStart, nRedlnEnd ); + //clip it if the redline extends past the end of the nodes text + nRedlnEnd = std::min<sal_Int32>(nRedlnEnd, rNode.GetText().getLength()); + if ( nRedlnEnd > nRedlStart ) + { + Range aTmp( nRedlStart, nRedlnEnd - 1 ); + rHiddenMulti.Select( aTmp, bSelect ); + } + } +} + +// Returns a MultiSection indicating the hidden ranges. +void SwScriptInfo::CalcHiddenRanges( const SwTextNode& rNode, + MultiSelection & rHiddenMulti, + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> *const pBookmarks) +{ + selectHiddenTextProperty(rNode, rHiddenMulti, pBookmarks); + + // If there are any hidden ranges in the current text node, we have + // to unhide the redlining ranges: + selectRedLineDeleted(rNode, rHiddenMulti, false); + + // We calculated a lot of stuff. Finally we can update the flags at the text node. + + const bool bNewContainsHiddenChars = rHiddenMulti.GetRangeCount() > 0; + bool bNewHiddenCharsHidePara = false; + if ( bNewContainsHiddenChars ) + { + const Range& rRange = rHiddenMulti.GetRange( 0 ); + const sal_Int32 nHiddenStart = rRange.Min(); + const sal_Int32 nHiddenEnd = rRange.Max() + 1; + bNewHiddenCharsHidePara = + (nHiddenStart == 0 && nHiddenEnd >= rNode.GetText().getLength()); + } + rNode.SetHiddenCharAttribute( bNewHiddenCharsHidePara, bNewContainsHiddenChars ); +} + +TextFrameIndex SwScriptInfo::CountCJKCharacters(const OUString &rText, + TextFrameIndex nPos, TextFrameIndex const nEnd, LanguageType aLang) +{ + TextFrameIndex nCount(0); + if (nEnd > nPos) + { + sal_Int32 nDone = 0; + const lang::Locale &rLocale = g_pBreakIt->GetLocale( aLang ); + while ( nPos < nEnd ) + { + nPos = TextFrameIndex(g_pBreakIt->GetBreakIter()->nextCharacters( + rText, sal_Int32(nPos), + rLocale, + i18n::CharacterIteratorMode::SKIPCELL, 1, nDone)); + nCount++; + } + } + else + nCount = nEnd - nPos ; + + return nCount; +} + +void SwScriptInfo::CJKJustify( const OUString& rText, KernArray& rKernArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, LanguageType aLang, + tools::Long nSpaceAdd, bool bIsSpaceStop ) +{ + assert( sal_Int32(nStt) >= 0 ); + if (sal_Int32(nLen) <= 0) + return; + + tools::Long nSpaceSum = 0; + const lang::Locale &rLocale = g_pBreakIt->GetLocale( aLang ); + sal_Int32 nDone = 0; + sal_Int32 nNext(nStt); + for ( sal_Int32 nI = 0; nI < sal_Int32(nLen); ++nI ) + { + if (nI + sal_Int32(nStt) == nNext) + { + nNext = g_pBreakIt->GetBreakIter()->nextCharacters( rText, nNext, + rLocale, + i18n::CharacterIteratorMode::SKIPCELL, 1, nDone ); + if (nNext < sal_Int32(nStt + nLen) || !bIsSpaceStop) + nSpaceSum += nSpaceAdd; + } + rKernArray.adjust(nI, nSpaceSum); + } +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |