22#include <QRegularExpression>
25#include <QTextBoundaryFinder>
28using namespace Qt::StringLiterals;
38 const QString in = input.normalized( QString::NormalizationForm_C );
40 out.reserve( in.size() );
43 const qsizetype n = in.size();
47 const QChar
c = in.at( i );
51 if (
c.isHighSurrogate() && i + 1 < n )
53 const QChar c2 = in.at( i + 1 );
54 if ( c2.isLowSurrogate() )
58 const QString key = in.mid( i, len ).normalized( QString::NormalizationForm_C );
62 out.append( it.value() );
74 if (
string.isEmpty() )
77 switch ( capitalization )
84 return string.toUpper();
88 return string.toLower();
92 QString temp = string;
94 QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word,
string.constData(),
string.length(),
nullptr, 0 );
95 QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme,
string.constData(),
string.length(),
nullptr, 0 );
97 wordSplitter.setPosition( 0 );
99 while ( ( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem ) || wordSplitter.toNextBoundary() >= 0 )
102 letterSplitter.setPosition( wordSplitter.position() );
103 ( void ) letterSplitter.toNextBoundary();
104 QString substr =
string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
105 temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
114 const thread_local QStringList smallWords = QObject::tr(
"a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|s|the|to|vs.|vs|via" ).split(
'|' );
115 const thread_local QStringList newPhraseSeparators = QObject::tr(
".|:" ).split(
'|' );
116 const thread_local QRegularExpression splitWords = QRegularExpression( u
"\\b"_s, QRegularExpression::UseUnicodePropertiesOption );
118 const bool allSameCase =
string.toLower() ==
string ||
string.toUpper() == string;
119 const QStringList parts = ( allSameCase ?
string.toLower() :
string ).split( splitWords, Qt::SkipEmptyParts );
121 bool firstWord =
true;
123 int lastWord = parts.count() - 1;
124 for (
const QString &word : std::as_const( parts ) )
126 if ( newPhraseSeparators.contains( word.trimmed() ) )
131 else if ( firstWord || ( i == lastWord ) || !smallWords.contains( word ) )
133 result += word.at( 0 ).toUpper() + word.mid( 1 );
147 result.remove(
' ' );
158 for (
int i = 0; i <
string.size(); ++i )
160 QChar ch =
string.at( i );
161 if ( ch.unicode() > 160 )
162 encoded += u
"&#%1;"_s.arg(
static_cast< int >( ch.unicode() ) );
163 else if ( ch.unicode() == 38 )
164 encoded +=
"&"_L1;
165 else if ( ch.unicode() == 60 )
166 encoded +=
"<"_L1;
167 else if ( ch.unicode() == 62 )
168 encoded +=
">"_L1;
177 int length1 = string1.length();
178 int length2 = string2.length();
181 if ( string1.isEmpty() )
185 else if ( string2.isEmpty() )
191 QString s1( caseSensitive ? string1 : string1.toLower() );
192 QString s2( caseSensitive ? string2 : string2.toLower() );
194 const QChar *s1Char = s1.constData();
195 const QChar *s2Char = s2.constData();
198 int commonPrefixLen = 0;
199 while ( length1 > 0 && length2 > 0 && *s1Char == *s2Char )
209 while ( length1 > 0 && length2 > 0 && s1.at( commonPrefixLen + length1 - 1 ) == s2.at( commonPrefixLen + length2 - 1 ) )
220 else if ( length2 == 0 )
226 if ( length1 > length2 )
229 std::swap( length1, length2 );
233 std::vector< int > col( length2 + 1, 0 );
234 std::vector< int > prevCol;
235 prevCol.reserve( length2 + 1 );
236 for (
int i = 0; i < length2 + 1; ++i )
238 prevCol.emplace_back( i );
240 const QChar *s2start = s2Char;
241 for (
int i = 0; i < length1; ++i )
245 for (
int j = 0; j < length2; ++j )
247 col[j + 1] = std::min( std::min( 1 + col[j], 1 + prevCol[1 + j] ), prevCol[j] + ( ( *s1Char == *s2Char ) ? 0 : 1 ) );
253 return prevCol[length2];
258 if ( string1.isEmpty() || string2.isEmpty() )
265 QString s1( caseSensitive ? string1 : string1.toLower() );
266 QString s2( caseSensitive ? string2 : string2.toLower() );
274 int *currentScores =
new int[s2.length()];
275 int *previousScores =
new int[s2.length()];
276 int maxCommonLength = 0;
277 int lastMaxBeginIndex = 0;
279 const QChar *s1Char = s1.constData();
280 const QChar *s2Char = s2.constData();
281 const QChar *s2Start = s2Char;
283 for (
int i = 0; i < s1.length(); ++i )
285 for (
int j = 0; j < s2.length(); ++j )
287 if ( *s1Char != *s2Char )
289 currentScores[j] = 0;
293 if ( i == 0 || j == 0 )
295 currentScores[j] = 1;
299 currentScores[j] = 1 + previousScores[j - 1];
302 if ( maxCommonLength < currentScores[j] )
304 maxCommonLength = currentScores[j];
305 lastMaxBeginIndex = i;
310 std::swap( currentScores, previousScores );
314 delete[] currentScores;
315 delete[] previousScores;
316 return string1.mid( lastMaxBeginIndex - maxCommonLength + 1, maxCommonLength );
321 if ( string1.isEmpty() && string2.isEmpty() )
327 if ( string1.length() != string2.length() )
334 QString s1( caseSensitive ? string1 : string1.toLower() );
335 QString s2( caseSensitive ? string2 : string2.toLower() );
344 const QChar *s1Char = s1.constData();
345 const QChar *s2Char = s2.constData();
347 for (
int i = 0; i < string1.length(); ++i )
349 if ( *s1Char != *s2Char )
360 if (
string.isEmpty() )
363 QString tmp =
string.toUpper();
366 QChar *char1 = tmp.data();
367 QChar *char2 = tmp.data();
369 for (
int i = 0; i < tmp.length(); ++i, ++char2 )
371 if ( ( *char2 ).unicode() >= 0x41 && ( *char2 ).unicode() <= 0x5A && ( i == 0 || ( ( *char2 ).unicode() != 0x41 && ( *char2 ).unicode() != 0x45
372 && ( *char2 ).unicode() != 0x48 && ( *char2 ).unicode() != 0x49
373 && ( *char2 ).unicode() != 0x4F && ( *char2 ).unicode() != 0x55
374 && ( *char2 ).unicode() != 0x57 && ( *char2 ).unicode() != 0x59 ) ) )
381 tmp.truncate( outLen );
383 QChar *tmpChar = tmp.data();
385 for (
int i = 1; i < tmp.length(); ++i, ++tmpChar )
387 switch ( ( *tmpChar ).unicode() )
393 tmp.replace( i, 1, QChar( 0x31 ) );
404 tmp.replace( i, 1, QChar( 0x32 ) );
409 tmp.replace( i, 1, QChar( 0x33 ) );
413 tmp.replace( i, 1, QChar( 0x34 ) );
418 tmp.replace( i, 1, QChar( 0x35 ) );
422 tmp.replace( i, 1, QChar( 0x36 ) );
432 for (
int i = 1; i < tmp.length(); ++i, ++char2 )
434 if ( *char2 != *char1 )
443 tmp.truncate( outLen );
444 if ( tmp.length() < 4 )
456 QString candidateNormalized = candidate.simplified().normalized( QString::NormalizationForm_C ).toLower();
457 QString searchNormalized = search.simplified().normalized( QString::NormalizationForm_C ).toLower();
459 int candidateLength = candidateNormalized.length();
460 int searchLength = searchNormalized.length();
464 if ( candidateLength == 0 || searchLength == 0 )
467 int candidateIdx = 0;
472 bool isPreviousIndexMatching =
false;
473 bool isWordOpen =
true;
476 while ( candidateIdx < candidateLength )
478 QChar candidateChar = candidateNormalized[candidateIdx++];
479 bool isCandidateCharWordEnd = candidateChar ==
' ' || candidateChar.isPunct();
482 if ( candidateIdx == 1 )
485 else if ( isCandidateCharWordEnd )
492 if ( searchIdx >= searchLength )
495 QChar searchChar = searchNormalized[searchIdx];
496 bool isSearchCharWordEnd = searchChar ==
' ' || searchChar.isPunct();
499 if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) )
504 if ( isSearchCharWordEnd )
508 else if ( isPreviousIndexMatching )
516 else if ( isPreviousIndexMatching )
526 isPreviousIndexMatching =
true;
531 isPreviousIndexMatching =
false;
536 if ( searchIdx >= searchLength )
538 bool isEndOfWord = ( candidateIdx >= candidateLength ) ?
true : candidateNormalized[candidateIdx] ==
' ' || candidateNormalized[candidateIdx].isPunct();
549 if ( searchIdx < searchLength )
552 return static_cast<float>( std::max( score, 0 ) ) / std::max( maxScore, 1 );
558 QString converted = string;
562 const thread_local QRegularExpression urlRegEx(
563 u
"((?:(?:['\"\\(]?http|https|ftp|file)://[^\\s]+[^\\s,.]+)|(?:\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~\\s]|/)))))"_s
565 const thread_local QRegularExpression groupedStringRegEx( u
"^(['\"\\(]+)(.*?)(?:['\")]+)"_s );
566 const thread_local QRegularExpression protoRegEx( u
"^(?:f|ht)tps?://|file://"_s );
567 const thread_local QRegularExpression emailRegEx( u
"([\\w._%+-]+@[\\w.-]+\\.[A-Za-z]+)"_s );
569 std::size_t offset = 0;
571 QRegularExpressionMatch match = urlRegEx.match( converted );
572 while ( match.hasMatch() )
575 QString url = match.captured( 1 );
576 std::size_t urlStart = match.capturedStart( 1 );
578 QString protoUrl = url;
579 const QRegularExpressionMatch groupedStringMatch = groupedStringRegEx.match( protoUrl );
580 if ( groupedStringMatch.hasMatch() )
582 url = groupedStringMatch.captured( 2 );
584 urlStart += groupedStringMatch.capturedLength( 1 );
586 if ( !protoRegEx.match( protoUrl ).hasMatch() )
588 protoUrl.prepend(
"http://" );
590 QString anchor = u
"<a href=\"%1\">%2</a>"_s.arg( protoUrl.toHtmlEscaped(), url.toHtmlEscaped() );
591 converted.replace( urlStart, url.length(), anchor );
592 offset = urlStart + anchor.length();
593 match = urlRegEx.match( converted, offset );
596 match = emailRegEx.match( converted );
597 while ( match.hasMatch() )
600 QString email = match.captured( 1 );
601 QString anchor = u
"<a href=\"mailto:%1\">%1</a>"_s.arg( email.toHtmlEscaped() );
602 converted.replace( match.capturedStart( 1 ), email.length(), anchor );
603 offset = match.capturedStart( 1 ) + anchor.length();
604 match = emailRegEx.match( converted, offset );
615 const thread_local QRegularExpression rxUrl( u
"^(http|https|ftp|file)://\\S+$"_s );
616 return rxUrl.match(
string ).hasMatch();
622 QString converted = html;
623 converted.replace(
"<br>"_L1,
"\n"_L1 );
624 converted.replace(
"<b>"_L1,
"**"_L1 );
625 converted.replace(
"</b>"_L1,
"**"_L1 );
626 converted.replace(
"<pre>"_L1,
"\n```\n"_L1 );
627 converted.replace(
"</pre>"_L1,
"```\n"_L1 );
629 const thread_local QRegularExpression hrefRegEx( u
"<a\\s+href\\s*=\\s*([^<>]*)\\s*>([^<>]*)</a>"_s );
632 QRegularExpressionMatch match = hrefRegEx.match( converted );
633 while ( match.hasMatch() )
635 QString url = match.captured( 1 ).replace(
"\""_L1, QString() );
636 url.replace(
'\'', QString() );
637 QString name = match.captured( 2 );
638 QString anchor = u
"[%1](%2)"_s.arg( name, url );
639 converted.replace( match.capturedStart(), match.capturedLength(), anchor );
640 offset = match.capturedStart() + anchor.length();
641 match = hrefRegEx.match( converted, offset );
647QString
QgsStringUtils::wordWrap(
const QString &
string,
const int length,
const bool useMaxLineLength,
const QString &customDelimiter )
649 if (
string.isEmpty() || length == 0 )
653 QRegularExpression rx;
654 int delimiterLength = 0;
656 if ( !customDelimiter.isEmpty() )
658 rx.setPattern( QRegularExpression::escape( customDelimiter ) );
659 delimiterLength = customDelimiter.length();
664 rx.setPattern( u
"[\\x{200B}\\s]"_s );
668 const QStringList lines =
string.split(
'\n' );
669 int strLength, strCurrent, strHit, lastHit;
671 for (
int i = 0; i < lines.size(); i++ )
673 const QString line = lines.at( i );
674 strLength = line.length();
675 if ( strLength <= length )
678 newstr.append( line );
679 if ( i < lines.size() - 1 )
680 newstr.append(
'\n' );
687 while ( strCurrent < strLength )
691 if ( useMaxLineLength )
694 strHit = ( strCurrent + length >= strLength ) ? -1 : line.lastIndexOf( rx, strCurrent + length );
695 if ( strHit == lastHit || strHit == -1 )
698 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
704 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
708 newstr.append( QStringView { line }.mid( strCurrent, strHit - strCurrent ) );
709 newstr.append(
'\n' );
710 strCurrent = strHit + delimiterLength;
714 newstr.append( QStringView { line }.mid( strCurrent ) );
715 strCurrent = strLength;
718 if ( i < lines.size() - 1 )
719 newstr.append(
'\n' );
727 string =
string.replace(
',', QChar( 65040 ) ).replace( QChar( 8229 ), QChar( 65072 ) );
728 string =
string.replace( QChar( 12289 ), QChar( 65041 ) ).replace( QChar( 12290 ), QChar( 65042 ) );
729 string =
string.replace(
':', QChar( 65043 ) ).replace(
';', QChar( 65044 ) );
730 string =
string.replace(
'!', QChar( 65045 ) ).replace(
'?', QChar( 65046 ) );
731 string =
string.replace( QChar( 12310 ), QChar( 65047 ) ).replace( QChar( 12311 ), QChar( 65048 ) );
732 string =
string.replace( QChar( 8230 ), QChar( 65049 ) );
733 string =
string.replace( QChar( 8212 ), QChar( 65073 ) ).replace( QChar( 8211 ), QChar( 65074 ) );
734 string =
string.replace(
'_', QChar( 65075 ) ).replace( QChar( 65103 ), QChar( 65076 ) );
735 string =
string.replace(
'(', QChar( 65077 ) ).replace(
')', QChar( 65078 ) );
736 string =
string.replace(
'{', QChar( 65079 ) ).replace(
'}', QChar( 65080 ) );
737 string =
string.replace(
'<', QChar( 65087 ) ).replace(
'>', QChar( 65088 ) );
738 string =
string.replace(
'[', QChar( 65095 ) ).replace(
']', QChar( 65096 ) );
739 string =
string.replace( QChar( 12308 ), QChar( 65081 ) ).replace( QChar( 12309 ), QChar( 65082 ) );
740 string =
string.replace( QChar( 12304 ), QChar( 65083 ) ).replace( QChar( 12305 ), QChar( 65084 ) );
741 string =
string.replace( QChar( 12298 ), QChar( 65085 ) ).replace( QChar( 12299 ), QChar( 65086 ) );
742 string =
string.replace( QChar( 12300 ), QChar( 65089 ) ).replace( QChar( 12301 ), QChar( 65090 ) );
743 string =
string.replace( QChar( 12302 ), QChar( 65091 ) ).replace( QChar( 12303 ), QChar( 65092 ) );
750 const QLatin1Char backslash(
'\\' );
751 const int count =
string.count();
754 escaped.reserve( count * 2 );
755 for (
int i = 0; i < count; i++ )
757 switch (
string.at( i ).toLatin1() )
773 escaped.append( backslash );
775 escaped.append(
string.at( i ) );
782 const int charactersToTruncate =
string.length() - maxLength;
783 if ( charactersToTruncate <= 0 )
787 const int truncateFrom =
string.length() / 2 - ( charactersToTruncate + 1 ) / 2;
788 if ( truncateFrom <= 0 )
789 return QChar( 0x2026 );
791 return QStringView(
string ).first( truncateFrom ) + QString( QChar( 0x2026 ) ) + QStringView(
string ).sliced( truncateFrom + charactersToTruncate + 1 );
796 if ( candidate.trimmed().isEmpty() )
799 const thread_local QRegularExpression rxWhitespace( u
"\\s+"_s );
800 const QStringList parts = words.split( rxWhitespace, Qt::SkipEmptyParts );
803 for (
const QString &word : parts )
805 if ( !candidate.contains( word, sensitivity ) )
817 if ( mWholeWordOnly )
819 mRx.setPattern( u
"\\b%1\\b"_s.arg( mMatch ) );
820 mRx.setPatternOptions( mCaseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption );
826 QString result = input;
827 if ( !mWholeWordOnly )
829 return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
833 return result.replace( mRx, mReplacement );
840 map.insert( u
"match"_s, mMatch );
841 map.insert( u
"replace"_s, mReplacement );
842 map.insert( u
"caseSensitive"_s, mCaseSensitive ? u
"1"_s : u
"0"_s );
843 map.insert( u
"wholeWord"_s, mWholeWordOnly ? u
"1"_s : u
"0"_s );
854 QString result = input;
857 result = r.process( result );
867 QDomElement propEl = doc.createElement( u
"replacement"_s );
868 QgsStringMap::const_iterator it = props.constBegin();
869 for ( ; it != props.constEnd(); ++it )
871 propEl.setAttribute( it.key(), it.value() );
873 elem.appendChild( propEl );
879 mReplacements.clear();
880 QDomNodeList nodelist = elem.elementsByTagName( u
"replacement"_s );
881 for (
int i = 0; i < nodelist.count(); i++ )
883 QDomElement replacementElem = nodelist.at( i ).toElement();
884 QDomNamedNodeMap nodeMap = replacementElem.attributes();
887 for (
int j = 0; j < nodeMap.count(); ++j )
889 props.insert( nodeMap.item( j ).nodeName(), nodeMap.item( j ).nodeValue() );
Capitalization
String capitalization options.
@ AllSmallCaps
Force all characters to small caps.
@ MixedCase
Mixed case, ie no change.
@ UpperCamelCase
Convert the string to upper camel case. Note that this method does not unaccent characters.
@ AllLowercase
Convert all characters to lowercase.
@ TitleCase
Simple title case conversion - does not fully grammatically parse the text and uses simple rules only...
@ SmallCaps
Mixed case small caps.
@ ForceFirstLetterToCapital
Convert just the first letter of each word to uppercase, leave the rest untouched.
@ AllUppercase
Convert all characters to uppercase.
void readXml(const QDomElement &elem)
Reads the collection state from an XML element.
QString process(const QString &input) const
Processes a given input string, applying any valid replacements which should be made using QgsStringR...
void writeXml(QDomElement &elem, QDomDocument &doc) const
Writes the collection state to an XML element.
A representation of a single string replacement.
static QgsStringReplacement fromProperties(const QgsStringMap &properties)
Creates a new QgsStringReplacement from an encoded properties map.
QString process(const QString &input) const
Processes a given input string, applying any valid replacements which should be made.
bool wholeWordOnly() const
Returns true if match only applies to whole words, or false if partial word matches are permitted.
QString replacement() const
Returns the string to replace matches with.
bool caseSensitive() const
Returns true if match is case sensitive.
QgsStringReplacement(const QString &match, const QString &replacement, bool caseSensitive=false, bool wholeWordOnly=false)
Constructor for QgsStringReplacement.
QString match() const
Returns the string matched by this object.
QgsStringMap properties() const
Returns a map of the replacement properties.
static int hammingDistance(const QString &string1, const QString &string2, bool caseSensitive=false)
Returns the Hamming distance between two strings.
static QString soundex(const QString &string)
Returns the Soundex representation of a string.
static QHash< QString, QString > UNACCENT_MAP
Lookup table used by unaccent().
static int levenshteinDistance(const QString &string1, const QString &string2, bool caseSensitive=false)
Returns the Levenshtein edit distance between two strings.
static QString htmlToMarkdown(const QString &html)
Convert simple HTML to markdown.
static QString longestCommonSubstring(const QString &string1, const QString &string2, bool caseSensitive=false)
Returns the longest common substring between two strings.
static QString capitalize(const QString &string, Qgis::Capitalization capitalization)
Converts a string by applying capitalization rules to the string.
static QString substituteVerticalCharacters(QString string)
Returns a string with characters having vertical representation form substituted.
static QString unaccent(const QString &input)
Removes accents and other diacritical marks from a string, replacing accented characters with their u...
static bool containsByWord(const QString &candidate, const QString &words, Qt::CaseSensitivity sensitivity=Qt::CaseInsensitive)
Given a candidate string, returns true if the candidate contains all the individual words from anothe...
static QString insertLinks(const QString &string, bool *foundLinks=nullptr)
Returns a string with any URL (e.g., http(s)/ftp) and mailto: text converted to valid HTML <a ....
static double fuzzyScore(const QString &candidate, const QString &search)
Tests a candidate string to see how likely it is a match for a specified search string.
static QString qRegExpEscape(const QString &string)
Returns an escaped string matching the behavior of QRegExp::escape.
static QString ampersandEncode(const QString &string)
Makes a raw string safe for inclusion as a HTML/XML string literal.
static QString wordWrap(const QString &string, int length, bool useMaxLineLength=true, const QString &customDelimiter=QString())
Automatically wraps a string by inserting new line characters at appropriate locations in the string.
static bool isUrl(const QString &string)
Returns whether the string is a URL (http,https,ftp,file).
static QString truncateMiddleOfString(const QString &string, int maxLength)
Truncates a string to the specified maximum character length.
static QHash< QString, QString > createUnaccentMap()
Generates the unaccent mapping table (auto-generated by script at build time).
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QMap< QString, QString > QgsStringMap
#define FUZZY_SCORE_CONSECUTIVE_MATCH
#define FUZZY_SCORE_WORD_MATCH
#define FUZZY_SCORE_NEW_MATCH