Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsstringutils.cpp
3 : : ------------------
4 : : begin : June 2015
5 : : copyright : (C) 2015 by Nyall Dawson
6 : : email : nyall dot dawson at gmail dot com
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include "qgsstringutils.h"
17 : : #include "qgslogger.h"
18 : : #include <QVector>
19 : : #include <QRegExp>
20 : : #include <QStringList>
21 : : #include <QTextBoundaryFinder>
22 : : #include <QRegularExpression>
23 : : #include <cstdlib> // for std::abs
24 : :
25 : 0 : QString QgsStringUtils::capitalize( const QString &string, QgsStringUtils::Capitalization capitalization )
26 : : {
27 : 0 : if ( string.isEmpty() )
28 : 0 : return QString();
29 : :
30 : 0 : switch ( capitalization )
31 : : {
32 : : case MixedCase:
33 : 0 : return string;
34 : :
35 : : case AllUppercase:
36 : 0 : return string.toUpper();
37 : :
38 : : case AllLowercase:
39 : 0 : return string.toLower();
40 : :
41 : : case ForceFirstLetterToCapital:
42 : : {
43 : 0 : QString temp = string;
44 : :
45 : 0 : QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), nullptr, 0 );
46 : 0 : QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), nullptr, 0 );
47 : :
48 : 0 : wordSplitter.setPosition( 0 );
49 : 0 : bool first = true;
50 : 0 : while ( ( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
51 : 0 : || wordSplitter.toNextBoundary() >= 0 )
52 : : {
53 : 0 : first = false;
54 : 0 : letterSplitter.setPosition( wordSplitter.position() );
55 : 0 : letterSplitter.toNextBoundary();
56 : 0 : QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
57 : 0 : temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
58 : 0 : }
59 : 0 : return temp;
60 : 0 : }
61 : :
62 : : case TitleCase:
63 : : {
64 : : // yes, this is MASSIVELY simplifying the problem!!
65 : :
66 : 0 : static QStringList smallWords;
67 : 0 : static QStringList newPhraseSeparators;
68 : 0 : static QRegularExpression splitWords;
69 : 0 : if ( smallWords.empty() )
70 : : {
71 : 0 : 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( '|' );
72 : 0 : newPhraseSeparators = QObject::tr( ".|:" ).split( '|' );
73 : 0 : splitWords = QRegularExpression( QStringLiteral( "\\b" ), QRegularExpression::UseUnicodePropertiesOption );
74 : 0 : }
75 : :
76 : 0 : const bool allSameCase = string.toLower() == string || string.toUpper() == string;
77 : : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
78 : : const QStringList parts = ( allSameCase ? string.toLower() : string ).split( splitWords, QString::SkipEmptyParts );
79 : : #else
80 : 0 : const QStringList parts = ( allSameCase ? string.toLower() : string ).split( splitWords, Qt::SkipEmptyParts );
81 : : #endif
82 : 0 : QString result;
83 : 0 : bool firstWord = true;
84 : 0 : int i = 0;
85 : 0 : int lastWord = parts.count() - 1;
86 : 0 : for ( const QString &word : std::as_const( parts ) )
87 : : {
88 : 0 : if ( newPhraseSeparators.contains( word.trimmed() ) )
89 : : {
90 : 0 : firstWord = true;
91 : 0 : result += word;
92 : 0 : }
93 : 0 : else if ( firstWord || ( i == lastWord ) || !smallWords.contains( word ) )
94 : : {
95 : 0 : result += word.at( 0 ).toUpper() + word.mid( 1 );
96 : 0 : firstWord = false;
97 : 0 : }
98 : : else
99 : : {
100 : 0 : result += word;
101 : : }
102 : 0 : i++;
103 : : }
104 : 0 : return result;
105 : 0 : }
106 : :
107 : : case UpperCamelCase:
108 : 0 : QString result = QgsStringUtils::capitalize( string.toLower(), QgsStringUtils::ForceFirstLetterToCapital ).simplified();
109 : 0 : result.remove( ' ' );
110 : 0 : return result;
111 : 0 : }
112 : : // no warnings
113 : 0 : return string;
114 : 0 : }
115 : :
116 : : // original code from http://www.qtcentre.org/threads/52456-HTML-Unicode-ampersand-encoding
117 : 0 : QString QgsStringUtils::ampersandEncode( const QString &string )
118 : : {
119 : 0 : QString encoded;
120 : 0 : for ( int i = 0; i < string.size(); ++i )
121 : : {
122 : 0 : QChar ch = string.at( i );
123 : 0 : if ( ch.unicode() > 160 )
124 : 0 : encoded += QStringLiteral( "&#%1;" ).arg( static_cast< int >( ch.unicode() ) );
125 : 0 : else if ( ch.unicode() == 38 )
126 : 0 : encoded += QLatin1String( "&" );
127 : 0 : else if ( ch.unicode() == 60 )
128 : 0 : encoded += QLatin1String( "<" );
129 : 0 : else if ( ch.unicode() == 62 )
130 : 0 : encoded += QLatin1String( ">" );
131 : : else
132 : 0 : encoded += ch;
133 : 0 : }
134 : 0 : return encoded;
135 : 0 : }
136 : :
137 : 0 : int QgsStringUtils::levenshteinDistance( const QString &string1, const QString &string2, bool caseSensitive )
138 : : {
139 : 0 : int length1 = string1.length();
140 : 0 : int length2 = string2.length();
141 : :
142 : : //empty strings? solution is trivial...
143 : 0 : if ( string1.isEmpty() )
144 : : {
145 : 0 : return length2;
146 : : }
147 : 0 : else if ( string2.isEmpty() )
148 : : {
149 : 0 : return length1;
150 : : }
151 : :
152 : : //handle case sensitive flag (or not)
153 : 0 : QString s1( caseSensitive ? string1 : string1.toLower() );
154 : 0 : QString s2( caseSensitive ? string2 : string2.toLower() );
155 : :
156 : 0 : const QChar *s1Char = s1.constData();
157 : 0 : const QChar *s2Char = s2.constData();
158 : :
159 : : //strip out any common prefix
160 : 0 : int commonPrefixLen = 0;
161 : 0 : while ( length1 > 0 && length2 > 0 && *s1Char == *s2Char )
162 : : {
163 : 0 : commonPrefixLen++;
164 : 0 : length1--;
165 : 0 : length2--;
166 : 0 : s1Char++;
167 : 0 : s2Char++;
168 : : }
169 : :
170 : : //strip out any common suffix
171 : 0 : while ( length1 > 0 && length2 > 0 && s1.at( commonPrefixLen + length1 - 1 ) == s2.at( commonPrefixLen + length2 - 1 ) )
172 : : {
173 : 0 : length1--;
174 : 0 : length2--;
175 : : }
176 : :
177 : : //fully checked either string? if so, the answer is easy...
178 : 0 : if ( length1 == 0 )
179 : : {
180 : 0 : return length2;
181 : : }
182 : 0 : else if ( length2 == 0 )
183 : : {
184 : 0 : return length1;
185 : : }
186 : :
187 : : //ensure the inner loop is longer
188 : 0 : if ( length1 > length2 )
189 : : {
190 : 0 : std::swap( s1, s2 );
191 : 0 : std::swap( length1, length2 );
192 : 0 : }
193 : :
194 : : //levenshtein algorithm begins here
195 : 0 : QVector< int > col;
196 : 0 : col.fill( 0, length2 + 1 );
197 : 0 : QVector< int > prevCol;
198 : 0 : prevCol.reserve( length2 + 1 );
199 : 0 : for ( int i = 0; i < length2 + 1; ++i )
200 : : {
201 : 0 : prevCol << i;
202 : 0 : }
203 : 0 : const QChar *s2start = s2Char;
204 : 0 : for ( int i = 0; i < length1; ++i )
205 : : {
206 : 0 : col[0] = i + 1;
207 : 0 : s2Char = s2start;
208 : 0 : for ( int j = 0; j < length2; ++j )
209 : : {
210 : 0 : col[j + 1] = std::min( std::min( 1 + col[j], 1 + prevCol[1 + j] ), prevCol[j] + ( ( *s1Char == *s2Char ) ? 0 : 1 ) );
211 : 0 : s2Char++;
212 : 0 : }
213 : 0 : col.swap( prevCol );
214 : 0 : s1Char++;
215 : 0 : }
216 : 0 : return prevCol[length2];
217 : 0 : }
218 : :
219 : 0 : QString QgsStringUtils::longestCommonSubstring( const QString &string1, const QString &string2, bool caseSensitive )
220 : : {
221 : 0 : if ( string1.isEmpty() || string2.isEmpty() )
222 : : {
223 : : //empty strings, solution is trivial...
224 : 0 : return QString();
225 : : }
226 : :
227 : : //handle case sensitive flag (or not)
228 : 0 : QString s1( caseSensitive ? string1 : string1.toLower() );
229 : 0 : QString s2( caseSensitive ? string2 : string2.toLower() );
230 : :
231 : 0 : if ( s1 == s2 )
232 : : {
233 : : //another trivial case, identical strings
234 : 0 : return s1;
235 : : }
236 : :
237 : 0 : int *currentScores = new int [ s2.length()];
238 : 0 : int *previousScores = new int [ s2.length()];
239 : 0 : int maxCommonLength = 0;
240 : 0 : int lastMaxBeginIndex = 0;
241 : :
242 : 0 : const QChar *s1Char = s1.constData();
243 : 0 : const QChar *s2Char = s2.constData();
244 : 0 : const QChar *s2Start = s2Char;
245 : :
246 : 0 : for ( int i = 0; i < s1.length(); ++i )
247 : : {
248 : 0 : for ( int j = 0; j < s2.length(); ++j )
249 : : {
250 : 0 : if ( *s1Char != *s2Char )
251 : : {
252 : 0 : currentScores[j] = 0;
253 : 0 : }
254 : : else
255 : : {
256 : 0 : if ( i == 0 || j == 0 )
257 : : {
258 : 0 : currentScores[j] = 1;
259 : 0 : }
260 : : else
261 : : {
262 : 0 : currentScores[j] = 1 + previousScores[j - 1];
263 : : }
264 : :
265 : 0 : if ( maxCommonLength < currentScores[j] )
266 : : {
267 : 0 : maxCommonLength = currentScores[j];
268 : 0 : lastMaxBeginIndex = i;
269 : 0 : }
270 : : }
271 : 0 : s2Char++;
272 : 0 : }
273 : 0 : std::swap( currentScores, previousScores );
274 : 0 : s1Char++;
275 : 0 : s2Char = s2Start;
276 : 0 : }
277 : 0 : delete [] currentScores;
278 : 0 : delete [] previousScores;
279 : 0 : return string1.mid( lastMaxBeginIndex - maxCommonLength + 1, maxCommonLength );
280 : 0 : }
281 : :
282 : 0 : int QgsStringUtils::hammingDistance( const QString &string1, const QString &string2, bool caseSensitive )
283 : : {
284 : 0 : if ( string1.isEmpty() && string2.isEmpty() )
285 : : {
286 : : //empty strings, solution is trivial...
287 : 0 : return 0;
288 : : }
289 : :
290 : 0 : if ( string1.length() != string2.length() )
291 : : {
292 : : //invalid inputs
293 : 0 : return -1;
294 : : }
295 : :
296 : : //handle case sensitive flag (or not)
297 : 0 : QString s1( caseSensitive ? string1 : string1.toLower() );
298 : 0 : QString s2( caseSensitive ? string2 : string2.toLower() );
299 : :
300 : 0 : if ( s1 == s2 )
301 : : {
302 : : //another trivial case, identical strings
303 : 0 : return 0;
304 : : }
305 : :
306 : 0 : int distance = 0;
307 : 0 : const QChar *s1Char = s1.constData();
308 : 0 : const QChar *s2Char = s2.constData();
309 : :
310 : 0 : for ( int i = 0; i < string1.length(); ++i )
311 : : {
312 : 0 : if ( *s1Char != *s2Char )
313 : 0 : distance++;
314 : 0 : s1Char++;
315 : 0 : s2Char++;
316 : 0 : }
317 : :
318 : 0 : return distance;
319 : 0 : }
320 : :
321 : 0 : QString QgsStringUtils::soundex( const QString &string )
322 : : {
323 : 0 : if ( string.isEmpty() )
324 : 0 : return QString();
325 : :
326 : 0 : QString tmp = string.toUpper();
327 : :
328 : : //strip non character codes, and vowel like characters after the first character
329 : 0 : QChar *char1 = tmp.data();
330 : 0 : QChar *char2 = tmp.data();
331 : 0 : int outLen = 0;
332 : 0 : for ( int i = 0; i < tmp.length(); ++i, ++char2 )
333 : : {
334 : 0 : if ( ( *char2 ).unicode() >= 0x41 && ( *char2 ).unicode() <= 0x5A && ( i == 0 || ( ( *char2 ).unicode() != 0x41 && ( *char2 ).unicode() != 0x45
335 : 0 : && ( *char2 ).unicode() != 0x48 && ( *char2 ).unicode() != 0x49
336 : 0 : && ( *char2 ).unicode() != 0x4F && ( *char2 ).unicode() != 0x55
337 : 0 : && ( *char2 ).unicode() != 0x57 && ( *char2 ).unicode() != 0x59 ) ) )
338 : : {
339 : 0 : *char1 = *char2;
340 : 0 : char1++;
341 : 0 : outLen++;
342 : 0 : }
343 : 0 : }
344 : 0 : tmp.truncate( outLen );
345 : :
346 : 0 : QChar *tmpChar = tmp.data();
347 : 0 : tmpChar++;
348 : 0 : for ( int i = 1; i < tmp.length(); ++i, ++tmpChar )
349 : : {
350 : 0 : switch ( ( *tmpChar ).unicode() )
351 : : {
352 : : case 0x42:
353 : : case 0x46:
354 : : case 0x50:
355 : : case 0x56:
356 : 0 : tmp.replace( i, 1, QChar( 0x31 ) );
357 : 0 : break;
358 : :
359 : : case 0x43:
360 : : case 0x47:
361 : : case 0x4A:
362 : : case 0x4B:
363 : : case 0x51:
364 : : case 0x53:
365 : : case 0x58:
366 : : case 0x5A:
367 : 0 : tmp.replace( i, 1, QChar( 0x32 ) );
368 : 0 : break;
369 : :
370 : : case 0x44:
371 : : case 0x54:
372 : 0 : tmp.replace( i, 1, QChar( 0x33 ) );
373 : 0 : break;
374 : :
375 : : case 0x4C:
376 : 0 : tmp.replace( i, 1, QChar( 0x34 ) );
377 : 0 : break;
378 : :
379 : : case 0x4D:
380 : : case 0x4E:
381 : 0 : tmp.replace( i, 1, QChar( 0x35 ) );
382 : 0 : break;
383 : :
384 : : case 0x52:
385 : 0 : tmp.replace( i, 1, QChar( 0x36 ) );
386 : 0 : break;
387 : : }
388 : 0 : }
389 : :
390 : : //remove adjacent duplicates
391 : 0 : char1 = tmp.data();
392 : 0 : char2 = tmp.data();
393 : 0 : char2++;
394 : 0 : outLen = 1;
395 : 0 : for ( int i = 1; i < tmp.length(); ++i, ++char2 )
396 : : {
397 : 0 : if ( *char2 != *char1 )
398 : : {
399 : 0 : char1++;
400 : 0 : *char1 = *char2;
401 : 0 : outLen++;
402 : 0 : if ( outLen == 4 )
403 : 0 : break;
404 : 0 : }
405 : 0 : }
406 : 0 : tmp.truncate( outLen );
407 : 0 : if ( tmp.length() < 4 )
408 : : {
409 : 0 : tmp.append( "000" );
410 : 0 : tmp.truncate( 4 );
411 : 0 : }
412 : :
413 : 0 : return tmp;
414 : 0 : }
415 : :
416 : :
417 : 0 : double QgsStringUtils::fuzzyScore( const QString &candidate, const QString &search )
418 : : {
419 : 0 : QString candidateNormalized = candidate.simplified().normalized( QString:: NormalizationForm_C ).toLower();
420 : 0 : QString searchNormalized = search.simplified().normalized( QString:: NormalizationForm_C ).toLower();
421 : :
422 : 0 : int candidateLength = candidateNormalized.length();
423 : 0 : int searchLength = searchNormalized.length();
424 : 0 : int score = 0;
425 : :
426 : : // if the candidate and the search term are empty, no other option than 0 score
427 : 0 : if ( candidateLength == 0 || searchLength == 0 )
428 : 0 : return score;
429 : :
430 : 0 : int candidateIdx = 0;
431 : 0 : int searchIdx = 0;
432 : : // there is always at least one word
433 : 0 : int maxScore = FUZZY_SCORE_WORD_MATCH;
434 : :
435 : 0 : bool isPreviousIndexMatching = false;
436 : 0 : bool isWordOpen = true;
437 : :
438 : : // loop trough each candidate char and calculate the potential max score
439 : 0 : while ( candidateIdx < candidateLength )
440 : : {
441 : 0 : QChar candidateChar = candidateNormalized[ candidateIdx++ ];
442 : 0 : bool isCandidateCharWordEnd = candidateChar == ' ' || candidateChar.isPunct();
443 : :
444 : : // the first char is always the default score
445 : 0 : if ( candidateIdx == 1 )
446 : 0 : maxScore += FUZZY_SCORE_NEW_MATCH;
447 : : // every space character or underscore is a opportunity for a new word
448 : 0 : else if ( isCandidateCharWordEnd )
449 : 0 : maxScore += FUZZY_SCORE_WORD_MATCH;
450 : : // potentially we can match every other character
451 : : else
452 : 0 : maxScore += FUZZY_SCORE_CONSECUTIVE_MATCH;
453 : :
454 : : // we looped through all the characters
455 : 0 : if ( searchIdx >= searchLength )
456 : 0 : continue;
457 : :
458 : 0 : QChar searchChar = searchNormalized[ searchIdx ];
459 : 0 : bool isSearchCharWordEnd = searchChar == ' ' || searchChar.isPunct();
460 : :
461 : : // match!
462 : 0 : if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) )
463 : : {
464 : 0 : searchIdx++;
465 : :
466 : : // if we have just successfully finished a word, give higher score
467 : 0 : if ( isSearchCharWordEnd )
468 : : {
469 : 0 : if ( isWordOpen )
470 : 0 : score += FUZZY_SCORE_WORD_MATCH;
471 : 0 : else if ( isPreviousIndexMatching )
472 : 0 : score += FUZZY_SCORE_CONSECUTIVE_MATCH;
473 : : else
474 : 0 : score += FUZZY_SCORE_NEW_MATCH;
475 : :
476 : 0 : isWordOpen = true;
477 : 0 : }
478 : : // if we have consecutive characters matching, give higher score
479 : 0 : else if ( isPreviousIndexMatching )
480 : : {
481 : 0 : score += FUZZY_SCORE_CONSECUTIVE_MATCH;
482 : 0 : }
483 : : // normal score for new independent character that matches
484 : : else
485 : : {
486 : 0 : score += FUZZY_SCORE_NEW_MATCH;
487 : : }
488 : :
489 : 0 : isPreviousIndexMatching = true;
490 : 0 : }
491 : : // if the current character does NOT match, we are sure we cannot build a word for now
492 : : else
493 : : {
494 : 0 : isPreviousIndexMatching = false;
495 : 0 : isWordOpen = false;
496 : : }
497 : :
498 : : // if the search string is covered, check if the last match is end of word
499 : 0 : if ( searchIdx >= searchLength )
500 : : {
501 : 0 : bool isEndOfWord = ( candidateIdx >= candidateLength )
502 : : ? true
503 : 0 : : candidateNormalized[candidateIdx] == ' ' || candidateNormalized[candidateIdx].isPunct();
504 : :
505 : 0 : if ( isEndOfWord )
506 : 0 : score += FUZZY_SCORE_WORD_MATCH;
507 : 0 : }
508 : :
509 : : // QgsLogger::debug( QStringLiteral( "TMP: %1 | %2 | %3 | %4 | %5" ).arg( candidateChar, searchChar, QString::number(score), QString::number(isCandidateCharWordEnd), QString::number(isSearchCharWordEnd) ) + QStringLiteral( __FILE__ ) );
510 : : }
511 : :
512 : : // QgsLogger::debug( QStringLiteral( "RES: %1 | %2" ).arg( QString::number(maxScore), QString::number(score) ) + QStringLiteral( __FILE__ ) );
513 : : // we didn't loop through all the search chars, it means, that they are not present in the current candidate
514 : 0 : if ( searchIdx < searchLength )
515 : 0 : score = 0;
516 : :
517 : 0 : return static_cast<float>( std::max( score, 0 ) ) / std::max( maxScore, 1 );
518 : 0 : }
519 : :
520 : :
521 : 0 : QString QgsStringUtils::insertLinks( const QString &string, bool *foundLinks )
522 : : {
523 : 0 : QString converted = string;
524 : :
525 : : // http://alanstorm.com/url_regex_explained
526 : : // note - there's more robust implementations available, but we need one which works within the limitation of QRegExp
527 : 0 : static QRegExp urlRegEx( "(\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~\\s]|/))))" );
528 : 0 : static QRegExp protoRegEx( "^(?:f|ht)tps?://|file://" );
529 : 0 : static QRegExp emailRegEx( "([\\w._%+-]+@[\\w.-]+\\.[A-Za-z]+)" );
530 : :
531 : 0 : int offset = 0;
532 : 0 : bool found = false;
533 : 0 : while ( urlRegEx.indexIn( converted, offset ) != -1 )
534 : : {
535 : 0 : found = true;
536 : 0 : QString url = urlRegEx.cap( 1 );
537 : 0 : QString protoUrl = url;
538 : 0 : if ( protoRegEx.indexIn( protoUrl ) == -1 )
539 : : {
540 : 0 : protoUrl.prepend( "http://" );
541 : 0 : }
542 : 0 : QString anchor = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( protoUrl.toHtmlEscaped(), url.toHtmlEscaped() );
543 : 0 : converted.replace( urlRegEx.pos( 1 ), url.length(), anchor );
544 : 0 : offset = urlRegEx.pos( 1 ) + anchor.length();
545 : 0 : }
546 : 0 : offset = 0;
547 : 0 : while ( emailRegEx.indexIn( converted, offset ) != -1 )
548 : : {
549 : 0 : found = true;
550 : 0 : QString email = emailRegEx.cap( 1 );
551 : 0 : QString anchor = QStringLiteral( "<a href=\"mailto:%1\">%1</a>" ).arg( email.toHtmlEscaped() );
552 : 0 : converted.replace( emailRegEx.pos( 1 ), email.length(), anchor );
553 : 0 : offset = emailRegEx.pos( 1 ) + anchor.length();
554 : 0 : }
555 : :
556 : 0 : if ( foundLinks )
557 : 0 : *foundLinks = found;
558 : :
559 : 0 : return converted;
560 : 0 : }
561 : :
562 : 0 : QString QgsStringUtils::htmlToMarkdown( const QString &html )
563 : : {
564 : : // Any changes in this function must be copied to qgscrashreport.cpp too
565 : 0 : QString converted = html;
566 : 0 : converted.replace( QLatin1String( "<br>" ), QLatin1String( "\n" ) );
567 : 0 : converted.replace( QLatin1String( "<b>" ), QLatin1String( "**" ) );
568 : 0 : converted.replace( QLatin1String( "</b>" ), QLatin1String( "**" ) );
569 : :
570 : 0 : static QRegExp hrefRegEx( "<a\\s+href\\s*=\\s*([^<>]*)\\s*>([^<>]*)</a>" );
571 : 0 : int offset = 0;
572 : 0 : while ( hrefRegEx.indexIn( converted, offset ) != -1 )
573 : : {
574 : 0 : QString url = hrefRegEx.cap( 1 ).replace( QLatin1String( "\"" ), QString() );
575 : 0 : url.replace( '\'', QString() );
576 : 0 : QString name = hrefRegEx.cap( 2 );
577 : 0 : QString anchor = QStringLiteral( "[%1](%2)" ).arg( name, url );
578 : 0 : converted.replace( hrefRegEx, anchor );
579 : 0 : offset = hrefRegEx.pos( 1 ) + anchor.length();
580 : 0 : }
581 : :
582 : 0 : return converted;
583 : 0 : }
584 : :
585 : 0 : QString QgsStringUtils::wordWrap( const QString &string, const int length, const bool useMaxLineLength, const QString &customDelimiter )
586 : : {
587 : 0 : if ( string.isEmpty() || length == 0 )
588 : 0 : return string;
589 : :
590 : 0 : QString newstr;
591 : 0 : QRegExp rx;
592 : 0 : int delimiterLength = 0;
593 : :
594 : 0 : if ( !customDelimiter.isEmpty() )
595 : : {
596 : 0 : rx.setPatternSyntax( QRegExp::FixedString );
597 : 0 : rx.setPattern( customDelimiter );
598 : 0 : delimiterLength = customDelimiter.length();
599 : 0 : }
600 : : else
601 : : {
602 : : // \x200B is a ZERO-WIDTH SPACE, needed for worwrap to support a number of complex scripts (Indic, Arabic, etc.)
603 : 0 : rx.setPattern( QStringLiteral( "[\\s\\x200B]" ) );
604 : 0 : delimiterLength = 1;
605 : : }
606 : :
607 : 0 : const QStringList lines = string.split( '\n' );
608 : : int strLength, strCurrent, strHit, lastHit;
609 : :
610 : 0 : for ( int i = 0; i < lines.size(); i++ )
611 : : {
612 : 0 : strLength = lines.at( i ).length();
613 : 0 : strCurrent = 0;
614 : 0 : strHit = 0;
615 : 0 : lastHit = 0;
616 : :
617 : 0 : while ( strCurrent < strLength )
618 : : {
619 : : // positive wrap value = desired maximum line width to wrap
620 : : // negative wrap value = desired minimum line width before wrap
621 : 0 : if ( useMaxLineLength )
622 : : {
623 : : //first try to locate delimiter backwards
624 : 0 : strHit = lines.at( i ).lastIndexOf( rx, strCurrent + length );
625 : 0 : if ( strHit == lastHit || strHit == -1 )
626 : : {
627 : : //if no new backward delimiter found, try to locate forward
628 : 0 : strHit = lines.at( i ).indexOf( rx, strCurrent + std::abs( length ) );
629 : 0 : }
630 : 0 : lastHit = strHit;
631 : 0 : }
632 : : else
633 : : {
634 : 0 : strHit = lines.at( i ).indexOf( rx, strCurrent + std::abs( length ) );
635 : : }
636 : 0 : if ( strHit > -1 )
637 : : {
638 : 0 : newstr.append( lines.at( i ).midRef( strCurrent, strHit - strCurrent ) );
639 : 0 : newstr.append( '\n' );
640 : 0 : strCurrent = strHit + delimiterLength;
641 : 0 : }
642 : : else
643 : : {
644 : 0 : newstr.append( lines.at( i ).midRef( strCurrent ) );
645 : 0 : strCurrent = strLength;
646 : : }
647 : : }
648 : 0 : if ( i < lines.size() - 1 )
649 : 0 : newstr.append( '\n' );
650 : 0 : }
651 : :
652 : 0 : return newstr;
653 : 0 : }
654 : :
655 : 0 : QString QgsStringUtils::substituteVerticalCharacters( QString string )
656 : : {
657 : 0 : string = string.replace( ',', QChar( 65040 ) ).replace( QChar( 8229 ), QChar( 65072 ) ); // comma & two-dot leader
658 : 0 : string = string.replace( QChar( 12289 ), QChar( 65041 ) ).replace( QChar( 12290 ), QChar( 65042 ) ); // ideographic comma & full stop
659 : 0 : string = string.replace( ':', QChar( 65043 ) ).replace( ';', QChar( 65044 ) );
660 : 0 : string = string.replace( '!', QChar( 65045 ) ).replace( '?', QChar( 65046 ) );
661 : 0 : string = string.replace( QChar( 12310 ), QChar( 65047 ) ).replace( QChar( 12311 ), QChar( 65048 ) ); // white lenticular brackets
662 : 0 : string = string.replace( QChar( 8230 ), QChar( 65049 ) ); // three-dot ellipse
663 : 0 : string = string.replace( QChar( 8212 ), QChar( 65073 ) ).replace( QChar( 8211 ), QChar( 65074 ) ); // em & en dash
664 : 0 : string = string.replace( '_', QChar( 65075 ) ).replace( QChar( 65103 ), QChar( 65076 ) ); // low line & wavy low line
665 : 0 : string = string.replace( '(', QChar( 65077 ) ).replace( ')', QChar( 65078 ) );
666 : 0 : string = string.replace( '{', QChar( 65079 ) ).replace( '}', QChar( 65080 ) );
667 : 0 : string = string.replace( '<', QChar( 65087 ) ).replace( '>', QChar( 65088 ) );
668 : 0 : string = string.replace( '[', QChar( 65095 ) ).replace( ']', QChar( 65096 ) );
669 : 0 : string = string.replace( QChar( 12308 ), QChar( 65081 ) ).replace( QChar( 12309 ), QChar( 65082 ) ); // tortoise shell brackets
670 : 0 : string = string.replace( QChar( 12304 ), QChar( 65083 ) ).replace( QChar( 12305 ), QChar( 65084 ) ); // black lenticular brackets
671 : 0 : string = string.replace( QChar( 12298 ), QChar( 65085 ) ).replace( QChar( 12299 ), QChar( 65086 ) ); // double angle brackets
672 : 0 : string = string.replace( QChar( 12300 ), QChar( 65089 ) ).replace( QChar( 12301 ), QChar( 65090 ) ); // corner brackets
673 : 0 : string = string.replace( QChar( 12302 ), QChar( 65091 ) ).replace( QChar( 12303 ), QChar( 65092 ) ); // white corner brackets
674 : 0 : return string;
675 : : }
676 : :
677 : 0 : QgsStringReplacement::QgsStringReplacement( const QString &match, const QString &replacement, bool caseSensitive, bool wholeWordOnly )
678 : 0 : : mMatch( match )
679 : 0 : , mReplacement( replacement )
680 : 0 : , mCaseSensitive( caseSensitive )
681 : 0 : , mWholeWordOnly( wholeWordOnly )
682 : : {
683 : 0 : if ( mWholeWordOnly )
684 : 0 : mRx = QRegExp( QString( "\\b%1\\b" ).arg( mMatch ),
685 : 0 : mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
686 : 0 : }
687 : :
688 : 0 : QString QgsStringReplacement::process( const QString &input ) const
689 : : {
690 : 0 : QString result = input;
691 : 0 : if ( !mWholeWordOnly )
692 : : {
693 : 0 : return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
694 : : }
695 : : else
696 : : {
697 : 0 : return result.replace( mRx, mReplacement );
698 : : }
699 : 0 : }
700 : :
701 : 0 : QgsStringMap QgsStringReplacement::properties() const
702 : : {
703 : 0 : QgsStringMap map;
704 : 0 : map.insert( QStringLiteral( "match" ), mMatch );
705 : 0 : map.insert( QStringLiteral( "replace" ), mReplacement );
706 : 0 : map.insert( QStringLiteral( "caseSensitive" ), mCaseSensitive ? "1" : "0" );
707 : 0 : map.insert( QStringLiteral( "wholeWord" ), mWholeWordOnly ? "1" : "0" );
708 : 0 : return map;
709 : 0 : }
710 : :
711 : 0 : QgsStringReplacement QgsStringReplacement::fromProperties( const QgsStringMap &properties )
712 : : {
713 : 0 : return QgsStringReplacement( properties.value( QStringLiteral( "match" ) ),
714 : 0 : properties.value( QStringLiteral( "replace" ) ),
715 : 0 : properties.value( QStringLiteral( "caseSensitive" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ),
716 : 0 : properties.value( QStringLiteral( "wholeWord" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) );
717 : 0 : }
718 : :
719 : 0 : QString QgsStringReplacementCollection::process( const QString &input ) const
720 : : {
721 : 0 : QString result = input;
722 : 0 : const auto constMReplacements = mReplacements;
723 : 0 : for ( const QgsStringReplacement &r : constMReplacements )
724 : : {
725 : 0 : result = r.process( result );
726 : : }
727 : 0 : return result;
728 : 0 : }
729 : :
730 : 0 : void QgsStringReplacementCollection::writeXml( QDomElement &elem, QDomDocument &doc ) const
731 : : {
732 : 0 : const auto constMReplacements = mReplacements;
733 : 0 : for ( const QgsStringReplacement &r : constMReplacements )
734 : : {
735 : 0 : QgsStringMap props = r.properties();
736 : 0 : QDomElement propEl = doc.createElement( QStringLiteral( "replacement" ) );
737 : 0 : QgsStringMap::const_iterator it = props.constBegin();
738 : 0 : for ( ; it != props.constEnd(); ++it )
739 : : {
740 : 0 : propEl.setAttribute( it.key(), it.value() );
741 : 0 : }
742 : 0 : elem.appendChild( propEl );
743 : 0 : }
744 : 0 : }
745 : :
746 : 0 : void QgsStringReplacementCollection::readXml( const QDomElement &elem )
747 : : {
748 : 0 : mReplacements.clear();
749 : 0 : QDomNodeList nodelist = elem.elementsByTagName( QStringLiteral( "replacement" ) );
750 : 0 : for ( int i = 0; i < nodelist.count(); i++ )
751 : : {
752 : 0 : QDomElement replacementElem = nodelist.at( i ).toElement();
753 : 0 : QDomNamedNodeMap nodeMap = replacementElem.attributes();
754 : :
755 : 0 : QgsStringMap props;
756 : 0 : for ( int j = 0; j < nodeMap.count(); ++j )
757 : : {
758 : 0 : props.insert( nodeMap.item( j ).nodeName(), nodeMap.item( j ).nodeValue() );
759 : 0 : }
760 : 0 : mReplacements << QgsStringReplacement::fromProperties( props );
761 : 0 : }
762 : :
763 : 0 : }
|