22#include <QRegularExpression>
25#include <QTextBoundaryFinder>
29using namespace Qt::StringLiterals;
35 constexpr char16_t MAX_SIMPLE_CHAR = 127;
36 if ( std::none_of( input.cbegin(), input.cend(), []( QChar
c ) { return c.unicode() > MAX_SIMPLE_CHAR; } ) )
47 const QString in = input.normalized( QString::NormalizationForm_C );
49 out.reserve( in.size() );
52 const qsizetype n = in.size();
56 const QChar
c = in.at( i );
58 if (
c.unicode() <= MAX_SIMPLE_CHAR )
68 char32_t ucs4 =
c.unicode();
71 if (
c.isHighSurrogate() && i + 1 < n )
73 const QChar c2 = in.at( i + 1 );
74 if ( c2.isLowSurrogate() )
76 ucs4 = QChar::surrogateToUcs4(
c, c2 );
81 auto it = UNACCENT_MAP.constFind( ucs4 );
82 if ( it != UNACCENT_MAP.constEnd() )
84 out.append( it.value() );
91 out.append( in.at( i + 1 ) );
104 const QString uuid = QUuid::createUuid().toString( QUuid::StringFormat::WithoutBraces );
105 QString
id = base.isEmpty() ? uuid : base +
'_' + uuid;
109 const thread_local QRegularExpression idRx( uR
"([\W])"_s );
110 id.replace( idRx, u
"_"_s );
116 if (
string.isEmpty() )
119 switch ( capitalization )
126 return string.toUpper();
130 return string.toLower();
134 QString temp = string;
136 QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word,
string.constData(),
string.length(),
nullptr, 0 );
137 QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme,
string.constData(),
string.length(),
nullptr, 0 );
139 wordSplitter.setPosition( 0 );
141 while ( ( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem ) || wordSplitter.toNextBoundary() >= 0 )
144 letterSplitter.setPosition( wordSplitter.position() );
145 ( void ) letterSplitter.toNextBoundary();
146 QString substr =
string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
147 temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
156 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(
'|' );
157 const thread_local QStringList newPhraseSeparators = QObject::tr(
".|:" ).split(
'|' );
158 const thread_local QRegularExpression splitWords = QRegularExpression( u
"\\b"_s, QRegularExpression::UseUnicodePropertiesOption );
160 const bool allSameCase =
string.toLower() ==
string ||
string.toUpper() == string;
161 const QStringList parts = ( allSameCase ?
string.toLower() :
string ).split( splitWords, Qt::SkipEmptyParts );
163 bool firstWord =
true;
165 int lastWord = parts.count() - 1;
166 for (
const QString &word : std::as_const( parts ) )
168 if ( newPhraseSeparators.contains( word.trimmed() ) )
173 else if ( firstWord || ( i == lastWord ) || !smallWords.contains( word ) )
175 result += word.at( 0 ).toUpper() + word.mid( 1 );
189 result.remove(
' ' );
200 for (
int i = 0; i <
string.size(); ++i )
202 QChar ch =
string.at( i );
203 if ( ch.unicode() > 160 )
204 encoded += u
"&#%1;"_s.arg(
static_cast< int >( ch.unicode() ) );
205 else if ( ch.unicode() == 38 )
206 encoded +=
"&"_L1;
207 else if ( ch.unicode() == 60 )
208 encoded +=
"<"_L1;
209 else if ( ch.unicode() == 62 )
210 encoded +=
">"_L1;
219 int length1 = string1.length();
220 int length2 = string2.length();
223 if ( string1.isEmpty() )
227 else if ( string2.isEmpty() )
233 QString s1( caseSensitive ? string1 : string1.toLower() );
234 QString s2( caseSensitive ? string2 : string2.toLower() );
236 const QChar *s1Char = s1.constData();
237 const QChar *s2Char = s2.constData();
240 int commonPrefixLen = 0;
241 while ( length1 > 0 && length2 > 0 && *s1Char == *s2Char )
251 while ( length1 > 0 && length2 > 0 && s1.at( commonPrefixLen + length1 - 1 ) == s2.at( commonPrefixLen + length2 - 1 ) )
262 else if ( length2 == 0 )
268 if ( length1 > length2 )
271 std::swap( length1, length2 );
275 std::vector< int > col( length2 + 1, 0 );
276 std::vector< int > prevCol;
277 prevCol.reserve( length2 + 1 );
278 for (
int i = 0; i < length2 + 1; ++i )
280 prevCol.emplace_back( i );
282 const QChar *s2start = s2Char;
283 for (
int i = 0; i < length1; ++i )
287 for (
int j = 0; j < length2; ++j )
289 col[j + 1] = std::min( std::min( 1 + col[j], 1 + prevCol[1 + j] ), prevCol[j] + ( ( *s1Char == *s2Char ) ? 0 : 1 ) );
295 return prevCol[length2];
300 if ( string1.isEmpty() || string2.isEmpty() )
307 QString s1( caseSensitive ? string1 : string1.toLower() );
308 QString s2( caseSensitive ? string2 : string2.toLower() );
316 int *currentScores =
new int[s2.length()];
317 int *previousScores =
new int[s2.length()];
318 int maxCommonLength = 0;
319 int lastMaxBeginIndex = 0;
321 const QChar *s1Char = s1.constData();
322 const QChar *s2Char = s2.constData();
323 const QChar *s2Start = s2Char;
325 for (
int i = 0; i < s1.length(); ++i )
327 for (
int j = 0; j < s2.length(); ++j )
329 if ( *s1Char != *s2Char )
331 currentScores[j] = 0;
335 if ( i == 0 || j == 0 )
337 currentScores[j] = 1;
341 currentScores[j] = 1 + previousScores[j - 1];
344 if ( maxCommonLength < currentScores[j] )
346 maxCommonLength = currentScores[j];
347 lastMaxBeginIndex = i;
352 std::swap( currentScores, previousScores );
356 delete[] currentScores;
357 delete[] previousScores;
358 return string1.mid( lastMaxBeginIndex - maxCommonLength + 1, maxCommonLength );
363 if ( string1.isEmpty() && string2.isEmpty() )
369 if ( string1.length() != string2.length() )
376 QString s1( caseSensitive ? string1 : string1.toLower() );
377 QString s2( caseSensitive ? string2 : string2.toLower() );
386 const QChar *s1Char = s1.constData();
387 const QChar *s2Char = s2.constData();
389 for (
int i = 0; i < string1.length(); ++i )
391 if ( *s1Char != *s2Char )
402 if (
string.isEmpty() )
405 QString tmp =
string.toUpper();
408 QChar *char1 = tmp.data();
409 QChar *char2 = tmp.data();
411 for (
int i = 0; i < tmp.length(); ++i, ++char2 )
413 if ( ( *char2 ).unicode() >= 0x41 && ( *char2 ).unicode() <= 0x5A && ( i == 0 || ( ( *char2 ).unicode() != 0x41 && ( *char2 ).unicode() != 0x45
414 && ( *char2 ).unicode() != 0x48 && ( *char2 ).unicode() != 0x49
415 && ( *char2 ).unicode() != 0x4F && ( *char2 ).unicode() != 0x55
416 && ( *char2 ).unicode() != 0x57 && ( *char2 ).unicode() != 0x59 ) ) )
423 tmp.truncate( outLen );
425 QChar *tmpChar = tmp.data();
427 for (
int i = 1; i < tmp.length(); ++i, ++tmpChar )
429 switch ( ( *tmpChar ).unicode() )
435 tmp.replace( i, 1, QChar( 0x31 ) );
446 tmp.replace( i, 1, QChar( 0x32 ) );
451 tmp.replace( i, 1, QChar( 0x33 ) );
455 tmp.replace( i, 1, QChar( 0x34 ) );
460 tmp.replace( i, 1, QChar( 0x35 ) );
464 tmp.replace( i, 1, QChar( 0x36 ) );
474 for (
int i = 1; i < tmp.length(); ++i, ++char2 )
476 if ( *char2 != *char1 )
485 tmp.truncate( outLen );
486 if ( tmp.length() < 4 )
498 QString candidateNormalized = candidate.simplified().normalized( QString::NormalizationForm_C ).toLower();
499 QString searchNormalized = search.simplified().normalized( QString::NormalizationForm_C ).toLower();
501 int candidateLength = candidateNormalized.length();
502 int searchLength = searchNormalized.length();
506 if ( candidateLength == 0 || searchLength == 0 )
509 int candidateIdx = 0;
514 bool isPreviousIndexMatching =
false;
515 bool isWordOpen =
true;
518 while ( candidateIdx < candidateLength )
520 QChar candidateChar = candidateNormalized[candidateIdx++];
521 bool isCandidateCharWordEnd = candidateChar ==
' ' || candidateChar.isPunct();
524 if ( candidateIdx == 1 )
527 else if ( isCandidateCharWordEnd )
534 if ( searchIdx >= searchLength )
537 QChar searchChar = searchNormalized[searchIdx];
538 bool isSearchCharWordEnd = searchChar ==
' ' || searchChar.isPunct();
541 if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) )
546 if ( isSearchCharWordEnd )
550 else if ( isPreviousIndexMatching )
558 else if ( isPreviousIndexMatching )
568 isPreviousIndexMatching =
true;
573 isPreviousIndexMatching =
false;
578 if ( searchIdx >= searchLength )
580 bool isEndOfWord = ( candidateIdx >= candidateLength ) ?
true : candidateNormalized[candidateIdx] ==
' ' || candidateNormalized[candidateIdx].isPunct();
591 if ( searchIdx < searchLength )
594 return static_cast<float>( std::max( score, 0 ) ) / std::max( maxScore, 1 );
600 QString converted = string;
604 const thread_local QRegularExpression urlRegEx(
605 u
"((?:(?:['\"\\(]?http|https|ftp|file)://[^\\s]+[^\\s,.]+)|(?:\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~\\s]|/)))))"_s
607 const thread_local QRegularExpression groupedStringRegEx( u
"^(['\"\\(]+)(.*?)(?:['\")]+)"_s );
608 const thread_local QRegularExpression protoRegEx( u
"^(?:f|ht)tps?://|file://"_s );
609 const thread_local QRegularExpression emailRegEx( u
"([\\w._%+-]+@[\\w.-]+\\.[A-Za-z]+)"_s );
611 std::size_t offset = 0;
613 QRegularExpressionMatch match = urlRegEx.match( converted );
614 while ( match.hasMatch() )
617 QString url = match.captured( 1 );
618 std::size_t urlStart = match.capturedStart( 1 );
620 QString protoUrl = url;
621 const QRegularExpressionMatch groupedStringMatch = groupedStringRegEx.match( protoUrl );
622 if ( groupedStringMatch.hasMatch() )
624 url = groupedStringMatch.captured( 2 );
626 urlStart += groupedStringMatch.capturedLength( 1 );
628 if ( !protoRegEx.match( protoUrl ).hasMatch() )
630 protoUrl.prepend(
"http://" );
632 QString anchor = u
"<a href=\"%1\">%2</a>"_s.arg( protoUrl.toHtmlEscaped(), url.toHtmlEscaped() );
633 converted.replace( urlStart, url.length(), anchor );
634 offset = urlStart + anchor.length();
635 match = urlRegEx.match( converted, offset );
638 match = emailRegEx.match( converted );
639 while ( match.hasMatch() )
642 QString email = match.captured( 1 );
643 QString anchor = u
"<a href=\"mailto:%1\">%1</a>"_s.arg( email.toHtmlEscaped() );
644 converted.replace( match.capturedStart( 1 ), email.length(), anchor );
645 offset = match.capturedStart( 1 ) + anchor.length();
646 match = emailRegEx.match( converted, offset );
657 const thread_local QRegularExpression rxUrl( u
"^(http|https|ftp|file)://\\S+$"_s );
658 return rxUrl.match(
string ).hasMatch();
664 QString converted = html;
665 converted.replace(
"<br>"_L1,
"\n"_L1 );
666 converted.replace(
"<b>"_L1,
"**"_L1 );
667 converted.replace(
"</b>"_L1,
"**"_L1 );
668 converted.replace(
"<pre>"_L1,
"\n```\n"_L1 );
669 converted.replace(
"</pre>"_L1,
"```\n"_L1 );
671 const thread_local QRegularExpression hrefRegEx( u
"<a\\s+href\\s*=\\s*([^<>]*)\\s*>([^<>]*)</a>"_s );
674 QRegularExpressionMatch match = hrefRegEx.match( converted );
675 while ( match.hasMatch() )
677 QString url = match.captured( 1 ).replace(
"\""_L1, QString() );
678 url.replace(
'\'', QString() );
679 QString name = match.captured( 2 );
680 QString anchor = u
"[%1](%2)"_s.arg( name, url );
681 converted.replace( match.capturedStart(), match.capturedLength(), anchor );
682 offset = match.capturedStart() + anchor.length();
683 match = hrefRegEx.match( converted, offset );
689QString
QgsStringUtils::wordWrap(
const QString &
string,
const int length,
const bool useMaxLineLength,
const QString &customDelimiter )
691 if (
string.isEmpty() || length == 0 )
695 QRegularExpression rx;
696 int delimiterLength = 0;
698 if ( !customDelimiter.isEmpty() )
700 rx.setPattern( QRegularExpression::escape( customDelimiter ) );
701 delimiterLength = customDelimiter.length();
706 rx.setPattern( u
"[\\x{200B}\\s]"_s );
710 const QStringList lines =
string.split(
'\n' );
711 int strLength, strCurrent, strHit, lastHit;
713 for (
int i = 0; i < lines.size(); i++ )
715 const QString line = lines.at( i );
716 strLength = line.length();
717 if ( strLength <= length )
720 newstr.append( line );
721 if ( i < lines.size() - 1 )
722 newstr.append(
'\n' );
729 while ( strCurrent < strLength )
733 if ( useMaxLineLength )
736 strHit = ( strCurrent + length >= strLength ) ? -1 : line.lastIndexOf( rx, strCurrent + length );
737 if ( strHit == lastHit || strHit == -1 )
740 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
746 strHit = ( strCurrent + std::abs( length ) >= strLength ) ? -1 : line.indexOf( rx, strCurrent + std::abs( length ) );
750 newstr.append( QStringView { line }.mid( strCurrent, strHit - strCurrent ) );
751 newstr.append(
'\n' );
752 strCurrent = strHit + delimiterLength;
756 newstr.append( QStringView { line }.mid( strCurrent ) );
757 strCurrent = strLength;
760 if ( i < lines.size() - 1 )
761 newstr.append(
'\n' );
769 string =
string.replace(
',', QChar( 65040 ) ).replace( QChar( 8229 ), QChar( 65072 ) );
770 string =
string.replace( QChar( 12289 ), QChar( 65041 ) ).replace( QChar( 12290 ), QChar( 65042 ) );
771 string =
string.replace(
':', QChar( 65043 ) ).replace(
';', QChar( 65044 ) );
772 string =
string.replace(
'!', QChar( 65045 ) ).replace(
'?', QChar( 65046 ) );
773 string =
string.replace( QChar( 12310 ), QChar( 65047 ) ).replace( QChar( 12311 ), QChar( 65048 ) );
774 string =
string.replace( QChar( 8230 ), QChar( 65049 ) );
775 string =
string.replace( QChar( 8212 ), QChar( 65073 ) ).replace( QChar( 8211 ), QChar( 65074 ) );
776 string =
string.replace(
'_', QChar( 65075 ) ).replace( QChar( 65103 ), QChar( 65076 ) );
777 string =
string.replace(
'(', QChar( 65077 ) ).replace(
')', QChar( 65078 ) );
778 string =
string.replace(
'{', QChar( 65079 ) ).replace(
'}', QChar( 65080 ) );
779 string =
string.replace(
'<', QChar( 65087 ) ).replace(
'>', QChar( 65088 ) );
780 string =
string.replace(
'[', QChar( 65095 ) ).replace(
']', QChar( 65096 ) );
781 string =
string.replace( QChar( 12308 ), QChar( 65081 ) ).replace( QChar( 12309 ), QChar( 65082 ) );
782 string =
string.replace( QChar( 12304 ), QChar( 65083 ) ).replace( QChar( 12305 ), QChar( 65084 ) );
783 string =
string.replace( QChar( 12298 ), QChar( 65085 ) ).replace( QChar( 12299 ), QChar( 65086 ) );
784 string =
string.replace( QChar( 12300 ), QChar( 65089 ) ).replace( QChar( 12301 ), QChar( 65090 ) );
785 string =
string.replace( QChar( 12302 ), QChar( 65091 ) ).replace( QChar( 12303 ), QChar( 65092 ) );
792 const QLatin1Char backslash(
'\\' );
793 const int count =
string.count();
796 escaped.reserve( count * 2 );
797 for (
int i = 0; i < count; i++ )
799 switch (
string.at( i ).toLatin1() )
815 escaped.append( backslash );
817 escaped.append(
string.at( i ) );
824 const int charactersToTruncate =
string.length() - maxLength;
825 if ( charactersToTruncate <= 0 )
829 const int truncateFrom =
string.length() / 2 - ( charactersToTruncate + 1 ) / 2;
830 if ( truncateFrom <= 0 )
831 return QChar( 0x2026 );
833 return QStringView(
string ).first( truncateFrom ) + QString( QChar( 0x2026 ) ) + QStringView(
string ).sliced( truncateFrom + charactersToTruncate + 1 );
838 if ( candidate.trimmed().isEmpty() )
841 const thread_local QRegularExpression rxWhitespace( u
"\\s+"_s );
842 const QStringList parts = words.split( rxWhitespace, Qt::SkipEmptyParts );
845 for (
const QString &word : parts )
847 if ( !candidate.contains( word, sensitivity ) )
859 if ( mWholeWordOnly )
861 mRx.setPattern( u
"\\b%1\\b"_s.arg( mMatch ) );
862 mRx.setPatternOptions( mCaseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption );
868 QString result = input;
869 if ( !mWholeWordOnly )
871 return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
875 return result.replace( mRx, mReplacement );
882 map.insert( u
"match"_s, mMatch );
883 map.insert( u
"replace"_s, mReplacement );
884 map.insert( u
"caseSensitive"_s, mCaseSensitive ? u
"1"_s : u
"0"_s );
885 map.insert( u
"wholeWord"_s, mWholeWordOnly ? u
"1"_s : u
"0"_s );
896 QString result = input;
899 result = r.process( result );
909 QDomElement propEl = doc.createElement( u
"replacement"_s );
910 QgsStringMap::const_iterator it = props.constBegin();
911 for ( ; it != props.constEnd(); ++it )
913 propEl.setAttribute( it.key(), it.value() );
915 elem.appendChild( propEl );
921 mReplacements.clear();
922 QDomNodeList nodelist = elem.elementsByTagName( u
"replacement"_s );
923 for (
int i = 0; i < nodelist.count(); i++ )
925 QDomElement replacementElem = nodelist.at( i ).toElement();
926 QDomNamedNodeMap nodeMap = replacementElem.attributes();
929 for (
int j = 0; j < nodeMap.count(); ++j )
931 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 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 QHash< char32_t, QString > createUnaccentMap()
Generates the unaccent mapping table (auto-generated by script at build time).
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.
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