22#include <QRegularExpression>
25#include <QTextBoundaryFinder>
29using namespace Qt::StringLiterals;
39 const QString in = input.normalized( QString::NormalizationForm_C );
41 out.reserve( in.size() );
44 const qsizetype n = in.size();
48 const QChar
c = in.at( i );
52 if (
c.isHighSurrogate() && i + 1 < n )
54 const QChar c2 = in.at( i + 1 );
55 if ( c2.isLowSurrogate() )
59 const QString key = in.mid( i, len ).normalized( QString::NormalizationForm_C );
63 out.append( it.value() );
76 const QString uuid = QUuid::createUuid().toString( QUuid::StringFormat::WithoutBraces );
77 QString
id = base.isEmpty() ? uuid : base +
'_' + uuid;
81 const thread_local QRegularExpression idRx( uR
"([\W])"_s );
82 id.replace( idRx, u
"_"_s );
88 if (
string.isEmpty() )
91 switch ( capitalization )
98 return string.toUpper();
102 return string.toLower();
106 QString temp = string;
108 QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word,
string.constData(),
string.length(),
nullptr, 0 );
109 QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme,
string.constData(),
string.length(),
nullptr, 0 );
111 wordSplitter.setPosition( 0 );
113 while ( ( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem ) || wordSplitter.toNextBoundary() >= 0 )
116 letterSplitter.setPosition( wordSplitter.position() );
117 ( void ) letterSplitter.toNextBoundary();
118 QString substr =
string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
119 temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
128 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(
'|' );
129 const thread_local QStringList newPhraseSeparators = QObject::tr(
".|:" ).split(
'|' );
130 const thread_local QRegularExpression splitWords = QRegularExpression( u
"\\b"_s, QRegularExpression::UseUnicodePropertiesOption );
132 const bool allSameCase =
string.toLower() ==
string ||
string.toUpper() == string;
133 const QStringList parts = ( allSameCase ?
string.toLower() :
string ).split( splitWords, Qt::SkipEmptyParts );
135 bool firstWord =
true;
137 int lastWord = parts.count() - 1;
138 for (
const QString &word : std::as_const( parts ) )
140 if ( newPhraseSeparators.contains( word.trimmed() ) )
145 else if ( firstWord || ( i == lastWord ) || !smallWords.contains( word ) )
147 result += word.at( 0 ).toUpper() + word.mid( 1 );
161 result.remove(
' ' );
172 for (
int i = 0; i <
string.size(); ++i )
174 QChar ch =
string.at( i );
175 if ( ch.unicode() > 160 )
176 encoded += u
"&#%1;"_s.arg(
static_cast< int >( ch.unicode() ) );
177 else if ( ch.unicode() == 38 )
178 encoded +=
"&"_L1;
179 else if ( ch.unicode() == 60 )
180 encoded +=
"<"_L1;
181 else if ( ch.unicode() == 62 )
182 encoded +=
">"_L1;
191 int length1 = string1.length();
192 int length2 = string2.length();
195 if ( string1.isEmpty() )
199 else if ( string2.isEmpty() )
205 QString s1( caseSensitive ? string1 : string1.toLower() );
206 QString s2( caseSensitive ? string2 : string2.toLower() );
208 const QChar *s1Char = s1.constData();
209 const QChar *s2Char = s2.constData();
212 int commonPrefixLen = 0;
213 while ( length1 > 0 && length2 > 0 && *s1Char == *s2Char )
223 while ( length1 > 0 && length2 > 0 && s1.at( commonPrefixLen + length1 - 1 ) == s2.at( commonPrefixLen + length2 - 1 ) )
234 else if ( length2 == 0 )
240 if ( length1 > length2 )
243 std::swap( length1, length2 );
247 std::vector< int > col( length2 + 1, 0 );
248 std::vector< int > prevCol;
249 prevCol.reserve( length2 + 1 );
250 for (
int i = 0; i < length2 + 1; ++i )
252 prevCol.emplace_back( i );
254 const QChar *s2start = s2Char;
255 for (
int i = 0; i < length1; ++i )
259 for (
int j = 0; j < length2; ++j )
261 col[j + 1] = std::min( std::min( 1 + col[j], 1 + prevCol[1 + j] ), prevCol[j] + ( ( *s1Char == *s2Char ) ? 0 : 1 ) );
267 return prevCol[length2];
272 if ( string1.isEmpty() || string2.isEmpty() )
279 QString s1( caseSensitive ? string1 : string1.toLower() );
280 QString s2( caseSensitive ? string2 : string2.toLower() );
288 int *currentScores =
new int[s2.length()];
289 int *previousScores =
new int[s2.length()];
290 int maxCommonLength = 0;
291 int lastMaxBeginIndex = 0;
293 const QChar *s1Char = s1.constData();
294 const QChar *s2Char = s2.constData();
295 const QChar *s2Start = s2Char;
297 for (
int i = 0; i < s1.length(); ++i )
299 for (
int j = 0; j < s2.length(); ++j )
301 if ( *s1Char != *s2Char )
303 currentScores[j] = 0;
307 if ( i == 0 || j == 0 )
309 currentScores[j] = 1;
313 currentScores[j] = 1 + previousScores[j - 1];
316 if ( maxCommonLength < currentScores[j] )
318 maxCommonLength = currentScores[j];
319 lastMaxBeginIndex = i;
324 std::swap( currentScores, previousScores );
328 delete[] currentScores;
329 delete[] previousScores;
330 return string1.mid( lastMaxBeginIndex - maxCommonLength + 1, maxCommonLength );
335 if ( string1.isEmpty() && string2.isEmpty() )
341 if ( string1.length() != string2.length() )
348 QString s1( caseSensitive ? string1 : string1.toLower() );
349 QString s2( caseSensitive ? string2 : string2.toLower() );
358 const QChar *s1Char = s1.constData();
359 const QChar *s2Char = s2.constData();
361 for (
int i = 0; i < string1.length(); ++i )
363 if ( *s1Char != *s2Char )
374 if (
string.isEmpty() )
377 QString tmp =
string.toUpper();
380 QChar *char1 = tmp.data();
381 QChar *char2 = tmp.data();
383 for (
int i = 0; i < tmp.length(); ++i, ++char2 )
385 if ( ( *char2 ).unicode() >= 0x41 && ( *char2 ).unicode() <= 0x5A && ( i == 0 || ( ( *char2 ).unicode() != 0x41 && ( *char2 ).unicode() != 0x45
386 && ( *char2 ).unicode() != 0x48 && ( *char2 ).unicode() != 0x49
387 && ( *char2 ).unicode() != 0x4F && ( *char2 ).unicode() != 0x55
388 && ( *char2 ).unicode() != 0x57 && ( *char2 ).unicode() != 0x59 ) ) )
395 tmp.truncate( outLen );
397 QChar *tmpChar = tmp.data();
399 for (
int i = 1; i < tmp.length(); ++i, ++tmpChar )
401 switch ( ( *tmpChar ).unicode() )
407 tmp.replace( i, 1, QChar( 0x31 ) );
418 tmp.replace( i, 1, QChar( 0x32 ) );
423 tmp.replace( i, 1, QChar( 0x33 ) );
427 tmp.replace( i, 1, QChar( 0x34 ) );
432 tmp.replace( i, 1, QChar( 0x35 ) );
436 tmp.replace( i, 1, QChar( 0x36 ) );
446 for (
int i = 1; i < tmp.length(); ++i, ++char2 )
448 if ( *char2 != *char1 )
457 tmp.truncate( outLen );
458 if ( tmp.length() < 4 )
470 QString candidateNormalized = candidate.simplified().normalized( QString::NormalizationForm_C ).toLower();
471 QString searchNormalized = search.simplified().normalized( QString::NormalizationForm_C ).toLower();
473 int candidateLength = candidateNormalized.length();
474 int searchLength = searchNormalized.length();
478 if ( candidateLength == 0 || searchLength == 0 )
481 int candidateIdx = 0;
486 bool isPreviousIndexMatching =
false;
487 bool isWordOpen =
true;
490 while ( candidateIdx < candidateLength )
492 QChar candidateChar = candidateNormalized[candidateIdx++];
493 bool isCandidateCharWordEnd = candidateChar ==
' ' || candidateChar.isPunct();
496 if ( candidateIdx == 1 )
499 else if ( isCandidateCharWordEnd )
506 if ( searchIdx >= searchLength )
509 QChar searchChar = searchNormalized[searchIdx];
510 bool isSearchCharWordEnd = searchChar ==
' ' || searchChar.isPunct();
513 if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) )
518 if ( isSearchCharWordEnd )
522 else if ( isPreviousIndexMatching )
530 else if ( isPreviousIndexMatching )
540 isPreviousIndexMatching =
true;
545 isPreviousIndexMatching =
false;
550 if ( searchIdx >= searchLength )
552 bool isEndOfWord = ( candidateIdx >= candidateLength ) ?
true : candidateNormalized[candidateIdx] ==
' ' || candidateNormalized[candidateIdx].isPunct();
563 if ( searchIdx < searchLength )
566 return static_cast<float>( std::max( score, 0 ) ) / std::max( maxScore, 1 );
572 QString converted = string;
576 const thread_local QRegularExpression urlRegEx(
577 u
"((?:(?:['\"\\(]?http|https|ftp|file)://[^\\s]+[^\\s,.]+)|(?:\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~\\s]|/)))))"_s
579 const thread_local QRegularExpression groupedStringRegEx( u
"^(['\"\\(]+)(.*?)(?:['\")]+)"_s );
580 const thread_local QRegularExpression protoRegEx( u
"^(?:f|ht)tps?://|file://"_s );
581 const thread_local QRegularExpression emailRegEx( u
"([\\w._%+-]+@[\\w.-]+\\.[A-Za-z]+)"_s );
583 std::size_t offset = 0;
585 QRegularExpressionMatch match = urlRegEx.match( converted );
586 while ( match.hasMatch() )
589 QString url = match.captured( 1 );
590 std::size_t urlStart = match.capturedStart( 1 );
592 QString protoUrl = url;
593 const QRegularExpressionMatch groupedStringMatch = groupedStringRegEx.match( protoUrl );
594 if ( groupedStringMatch.hasMatch() )
596 url = groupedStringMatch.captured( 2 );
598 urlStart += groupedStringMatch.capturedLength( 1 );
600 if ( !protoRegEx.match( protoUrl ).hasMatch() )
602 protoUrl.prepend(
"http://" );
604 QString anchor = u
"<a href=\"%1\">%2</a>"_s.arg( protoUrl.toHtmlEscaped(), url.toHtmlEscaped() );
605 converted.replace( urlStart, url.length(), anchor );
606 offset = urlStart + anchor.length();
607 match = urlRegEx.match( converted, offset );
610 match = emailRegEx.match( converted );
611 while ( match.hasMatch() )
614 QString email = match.captured( 1 );
615 QString anchor = u
"<a href=\"mailto:%1\">%1</a>"_s.arg( email.toHtmlEscaped() );
616 converted.replace( match.capturedStart( 1 ), email.length(), anchor );
617 offset = match.capturedStart( 1 ) + anchor.length();
618 match = emailRegEx.match( converted, offset );
629 const thread_local QRegularExpression rxUrl( u
"^(http|https|ftp|file)://\\S+$"_s );
630 return rxUrl.match(
string ).hasMatch();
636 QString converted = html;
637 converted.replace(
"<br>"_L1,
"\n"_L1 );
638 converted.replace(
"<b>"_L1,
"**"_L1 );
639 converted.replace(
"</b>"_L1,
"**"_L1 );
640 converted.replace(
"<pre>"_L1,
"\n```\n"_L1 );
641 converted.replace(
"</pre>"_L1,
"```\n"_L1 );
643 const thread_local QRegularExpression hrefRegEx( u
"<a\\s+href\\s*=\\s*([^<>]*)\\s*>([^<>]*)</a>"_s );
646 QRegularExpressionMatch match = hrefRegEx.match( converted );
647 while ( match.hasMatch() )
649 QString url = match.captured( 1 ).replace(
"\""_L1, QString() );
650 url.replace(
'\'', QString() );
651 QString name = match.captured( 2 );
652 QString anchor = u
"[%1](%2)"_s.arg( name, url );
653 converted.replace( match.capturedStart(), match.capturedLength(), anchor );
654 offset = match.capturedStart() + anchor.length();
655 match = hrefRegEx.match( converted, offset );
661QString
QgsStringUtils::wordWrap(
const QString &
string,
const int length,
const bool useMaxLineLength,
const QString &customDelimiter )
663 if (
string.isEmpty() || length == 0 )
667 QRegularExpression rx;
668 int delimiterLength = 0;
670 if ( !customDelimiter.isEmpty() )
672 rx.setPattern( QRegularExpression::escape( customDelimiter ) );
673 delimiterLength = customDelimiter.length();
678 rx.setPattern( u
"[\\x{200B}\\s]"_s );
682 const QStringList lines =
string.split(
'\n' );
683 int strLength, strCurrent, strHit, lastHit;
685 for (
int i = 0; i < lines.size(); i++ )
687 const QString line = lines.at( i );
688 strLength = line.length();
689 if ( strLength <= length )
692 newstr.append( line );
693 if ( i < lines.size() - 1 )
694 newstr.append(
'\n' );
701 while ( strCurrent < strLength )
705 if ( useMaxLineLength )
708 strHit = ( strCurrent + length >= strLength ) ? -1 : line.lastIndexOf( rx, strCurrent + length );
709 if ( strHit == lastHit || strHit == -1 )
712 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
718 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
722 newstr.append( QStringView { line }.mid( strCurrent, strHit - strCurrent ) );
723 newstr.append(
'\n' );
724 strCurrent = strHit + delimiterLength;
728 newstr.append( QStringView { line }.mid( strCurrent ) );
729 strCurrent = strLength;
732 if ( i < lines.size() - 1 )
733 newstr.append(
'\n' );
741 string =
string.replace(
',', QChar( 65040 ) ).replace( QChar( 8229 ), QChar( 65072 ) );
742 string =
string.replace( QChar( 12289 ), QChar( 65041 ) ).replace( QChar( 12290 ), QChar( 65042 ) );
743 string =
string.replace(
':', QChar( 65043 ) ).replace(
';', QChar( 65044 ) );
744 string =
string.replace(
'!', QChar( 65045 ) ).replace(
'?', QChar( 65046 ) );
745 string =
string.replace( QChar( 12310 ), QChar( 65047 ) ).replace( QChar( 12311 ), QChar( 65048 ) );
746 string =
string.replace( QChar( 8230 ), QChar( 65049 ) );
747 string =
string.replace( QChar( 8212 ), QChar( 65073 ) ).replace( QChar( 8211 ), QChar( 65074 ) );
748 string =
string.replace(
'_', QChar( 65075 ) ).replace( QChar( 65103 ), QChar( 65076 ) );
749 string =
string.replace(
'(', QChar( 65077 ) ).replace(
')', QChar( 65078 ) );
750 string =
string.replace(
'{', QChar( 65079 ) ).replace(
'}', QChar( 65080 ) );
751 string =
string.replace(
'<', QChar( 65087 ) ).replace(
'>', QChar( 65088 ) );
752 string =
string.replace(
'[', QChar( 65095 ) ).replace(
']', QChar( 65096 ) );
753 string =
string.replace( QChar( 12308 ), QChar( 65081 ) ).replace( QChar( 12309 ), QChar( 65082 ) );
754 string =
string.replace( QChar( 12304 ), QChar( 65083 ) ).replace( QChar( 12305 ), QChar( 65084 ) );
755 string =
string.replace( QChar( 12298 ), QChar( 65085 ) ).replace( QChar( 12299 ), QChar( 65086 ) );
756 string =
string.replace( QChar( 12300 ), QChar( 65089 ) ).replace( QChar( 12301 ), QChar( 65090 ) );
757 string =
string.replace( QChar( 12302 ), QChar( 65091 ) ).replace( QChar( 12303 ), QChar( 65092 ) );
764 const QLatin1Char backslash(
'\\' );
765 const int count =
string.count();
768 escaped.reserve( count * 2 );
769 for (
int i = 0; i < count; i++ )
771 switch (
string.at( i ).toLatin1() )
787 escaped.append( backslash );
789 escaped.append(
string.at( i ) );
796 const int charactersToTruncate =
string.length() - maxLength;
797 if ( charactersToTruncate <= 0 )
801 const int truncateFrom =
string.length() / 2 - ( charactersToTruncate + 1 ) / 2;
802 if ( truncateFrom <= 0 )
803 return QChar( 0x2026 );
805 return QStringView(
string ).first( truncateFrom ) + QString( QChar( 0x2026 ) ) + QStringView(
string ).sliced( truncateFrom + charactersToTruncate + 1 );
810 if ( candidate.trimmed().isEmpty() )
813 const thread_local QRegularExpression rxWhitespace( u
"\\s+"_s );
814 const QStringList parts = words.split( rxWhitespace, Qt::SkipEmptyParts );
817 for (
const QString &word : parts )
819 if ( !candidate.contains( word, sensitivity ) )
831 if ( mWholeWordOnly )
833 mRx.setPattern( u
"\\b%1\\b"_s.arg( mMatch ) );
834 mRx.setPatternOptions( mCaseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption );
840 QString result = input;
841 if ( !mWholeWordOnly )
843 return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
847 return result.replace( mRx, mReplacement );
854 map.insert( u
"match"_s, mMatch );
855 map.insert( u
"replace"_s, mReplacement );
856 map.insert( u
"caseSensitive"_s, mCaseSensitive ? u
"1"_s : u
"0"_s );
857 map.insert( u
"wholeWord"_s, mWholeWordOnly ? u
"1"_s : u
"0"_s );
868 QString result = input;
871 result = r.process( result );
881 QDomElement propEl = doc.createElement( u
"replacement"_s );
882 QgsStringMap::const_iterator it = props.constBegin();
883 for ( ; it != props.constEnd(); ++it )
885 propEl.setAttribute( it.key(), it.value() );
887 elem.appendChild( propEl );
893 mReplacements.clear();
894 QDomNodeList nodelist = elem.elementsByTagName( u
"replacement"_s );
895 for (
int i = 0; i < nodelist.count(); i++ )
897 QDomElement replacementElem = nodelist.at( i ).toElement();
898 QDomNamedNodeMap nodeMap = replacementElem.attributes();
901 for (
int j = 0; j < nodeMap.count(); ++j )
903 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 createUniqueId(const QString &base=QString())
Generates a unique identifier by appending a random UUID to base.
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