added StringUtil.parseLong
[idea/community.git] / platform / util / src / com / intellij / openapi / util / text / StringUtil.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.util.text;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.progress.ProcessCanceledException;
20 import com.intellij.openapi.util.Pair;
21 import com.intellij.openapi.util.TextRange;
22 import com.intellij.util.*;
23 import com.intellij.util.containers.ContainerUtil;
24 import com.intellij.util.text.CharArrayUtil;
25 import com.intellij.util.text.CharSequenceSubSequence;
26 import com.intellij.util.text.StringFactory;
27 import org.jetbrains.annotations.Contract;
28 import org.jetbrains.annotations.NonNls;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import javax.swing.text.MutableAttributeSet;
33 import javax.swing.text.html.HTML;
34 import javax.swing.text.html.HTMLEditorKit;
35 import javax.swing.text.html.parser.ParserDelegator;
36 import java.beans.Introspector;
37 import java.io.IOException;
38 import java.io.Reader;
39 import java.io.StringReader;
40 import java.util.*;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43
44 import static java.lang.Math.max;
45 import static java.lang.Math.min;
46
47 //TeamCity inherits StringUtil: do not add private constructors!!!
48 @SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "MethodOverridesStaticMethodOfSuperclass"})
49 public class StringUtil extends StringUtilRt {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.text.StringUtil");
51
52   @NonNls private static final String VOWELS = "aeiouy";
53   @NonNls private static final Pattern EOL_SPLIT_KEEP_SEPARATORS = Pattern.compile("(?<=(\r\n|\n))|(?<=\r)(?=[^\n])");
54   @NonNls private static final Pattern EOL_SPLIT_PATTERN = Pattern.compile(" *(\r|\n|\r\n)+ *");
55   @NonNls private static final Pattern EOL_SPLIT_PATTERN_WITH_EMPTY = Pattern.compile(" *(\r|\n|\r\n) *");
56   @NonNls private static final Pattern EOL_SPLIT_DONT_TRIM_PATTERN = Pattern.compile("(\r|\n|\r\n)+");
57
58   private static class MyHtml2Text extends HTMLEditorKit.ParserCallback {
59     @NotNull private final StringBuilder myBuffer = new StringBuilder();
60
61     public void parse(Reader in) throws IOException {
62       myBuffer.setLength(0);
63       new ParserDelegator().parse(in, this, Boolean.TRUE);
64     }
65
66     @Override
67     public void handleText(char[] text, int pos) {
68       myBuffer.append(text);
69     }
70
71     @Override
72     public void handleStartTag(HTML.Tag tag, MutableAttributeSet set, int i) {
73       handleTag(tag);
74     }
75
76     @Override
77     public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet set, int i) {
78       handleTag(tag);
79     }
80
81     private void handleTag(HTML.Tag tag) {
82       if (tag.breaksFlow() && myBuffer.length() > 0) {
83         myBuffer.append(SystemProperties.getLineSeparator());
84       }
85     }
86
87     public String getText() {
88       return myBuffer.toString();
89     }
90   }
91
92   private static final MyHtml2Text html2TextParser = new MyHtml2Text();
93
94   public static final NotNullFunction<String, String> QUOTER = new NotNullFunction<String, String>() {
95     @Override
96     @NotNull
97     public String fun(String s) {
98       return "\"" + s + "\"";
99     }
100   };
101
102   public static final NotNullFunction<String, String> SINGLE_QUOTER = new NotNullFunction<String, String>() {
103     @Override
104     @NotNull
105     public String fun(String s) {
106       return "'" + s + "'";
107     }
108   };
109
110   @NotNull
111   @Contract(pure = true)
112   public static List<String> getWordsInStringLongestFirst(@NotNull String find) {
113     List<String> words = getWordsIn(find);
114     // hope long words are rare
115     Collections.sort(words, new Comparator<String>() {
116       @Override
117       public int compare(@NotNull final String o1, @NotNull final String o2) {
118         return o2.length() - o1.length();
119       }
120     });
121     return words;
122   }
123
124   @NotNull
125   @Contract(pure = true)
126   public static String escapePattern(@NotNull final String text) {
127     return replace(replace(text, "'", "''"), "{", "'{'");
128   }
129
130   @NotNull
131   @Contract(pure = true)
132   public static <T> Function<T, String> createToStringFunction(@NotNull Class<T> cls) {
133     return new Function<T, String>() {
134       @Override
135       public String fun(@NotNull T o) {
136         return o.toString();
137       }
138     };
139   }
140
141   @NotNull
142   public static Function<String, String> TRIMMER = new Function<String, String>() {
143     @Nullable
144     @Override
145     public String fun(@Nullable String s) {
146       return trim(s);
147     }
148   };
149
150   @NotNull
151   @Contract(pure = true)
152   public static String replace(@NonNls @NotNull String text, @NonNls @NotNull String oldS, @NonNls @NotNull String newS) {
153     return replace(text, oldS, newS, false);
154   }
155
156   @NotNull
157   @Contract(pure = true)
158   public static String replaceIgnoreCase(@NonNls @NotNull String text, @NonNls @NotNull String oldS, @NonNls @NotNull String newS) {
159     return replace(text, oldS, newS, true);
160   }
161
162   public static void replaceChar(@NotNull char[] buffer, char oldChar, char newChar, int start, int end) {
163     for (int i = start; i < end; i++) {
164       char c = buffer[i];
165       if (c == oldChar) {
166         buffer[i] = newChar;
167       }
168     }
169   }
170
171   @NotNull
172   @Contract(pure = true)
173   public static String replaceChar(@NotNull String buffer, char oldChar, char newChar) {
174     StringBuilder newBuffer = null;
175     for (int i = 0; i < buffer.length(); i++) {
176       char c = buffer.charAt(i);
177       if (c == oldChar) {
178         if (newBuffer == null) {
179           newBuffer = new StringBuilder(buffer.length());
180           newBuffer.append(buffer, 0, i);
181         }
182
183         newBuffer.append(newChar);
184       }
185       else if (newBuffer != null) {
186         newBuffer.append(c);
187       }
188     }
189     return newBuffer == null ? buffer : newBuffer.toString();
190   }
191
192   @Contract(pure = true)
193   public static String replace(@NonNls @NotNull final String text, @NonNls @NotNull final String oldS, @NonNls @NotNull final String newS, final boolean ignoreCase) {
194     if (text.length() < oldS.length()) return text;
195
196     StringBuilder newText = null;
197     int i = 0;
198
199     while (i < text.length()) {
200       final int index = ignoreCase? indexOfIgnoreCase(text, oldS, i) : text.indexOf(oldS, i);
201       if (index < 0) {
202         if (i == 0) {
203           return text;
204         }
205
206         newText.append(text, i, text.length());
207         break;
208       }
209       else {
210         if (newText == null) {
211           if (text.length() == oldS.length()) {
212             return newS;
213           }
214           newText = new StringBuilder(text.length() - i);
215         }
216
217         newText.append(text, i, index);
218         newText.append(newS);
219         i = index + oldS.length();
220       }
221     }
222     return newText != null ? newText.toString() : "";
223   }
224
225   /**
226    * Implementation copied from {@link String#indexOf(String, int)} except character comparisons made case insensitive
227    */
228   @Contract(pure = true)
229   public static int indexOfIgnoreCase(@NotNull String where, @NotNull String what, int fromIndex) {
230     int targetCount = what.length();
231     int sourceCount = where.length();
232
233     if (fromIndex >= sourceCount) {
234       return targetCount == 0 ? sourceCount : -1;
235     }
236
237     if (fromIndex < 0) {
238       fromIndex = 0;
239     }
240
241     if (targetCount == 0) {
242       return fromIndex;
243     }
244
245     char first = what.charAt(0);
246     int max = sourceCount - targetCount;
247
248     for (int i = fromIndex; i <= max; i++) {
249       /* Look for first character. */
250       if (!charsEqualIgnoreCase(where.charAt(i), first)) {
251         while (++i <= max && !charsEqualIgnoreCase(where.charAt(i), first)) ;
252       }
253
254       /* Found first character, now look at the rest of v2 */
255       if (i <= max) {
256         int j = i + 1;
257         int end = j + targetCount - 1;
258         for (int k = 1; j < end && charsEqualIgnoreCase(where.charAt(j), what.charAt(k)); j++, k++) ;
259
260         if (j == end) {
261           /* Found whole string. */
262           return i;
263         }
264       }
265     }
266
267     return -1;
268   }
269
270   @Contract(pure = true)
271   public static int indexOfIgnoreCase(@NotNull String where, char what, int fromIndex) {
272     int sourceCount = where.length();
273     for (int i = max(fromIndex, 0); i < sourceCount; i++) {
274       if (charsEqualIgnoreCase(where.charAt(i), what)) {
275         return i;
276       }
277     }
278
279     return -1;
280   }
281
282   @Contract(pure = true)
283   public static boolean containsIgnoreCase(@NotNull String where, @NotNull String what) {
284     return indexOfIgnoreCase(where, what, 0) >= 0;
285   }
286
287   @Contract(pure = true)
288   public static boolean endsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String suffix) {
289     return StringUtilRt.endsWithIgnoreCase(str, suffix);
290   }
291
292   @Contract(pure = true)
293   public static boolean startsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String prefix) {
294     return StringUtilRt.startsWithIgnoreCase(str, prefix);
295   }
296
297   @Contract(pure = true)
298   @NotNull
299   public static String stripHtml(@NotNull String html, boolean convertBreaks) {
300     if (convertBreaks) {
301       html = html.replaceAll("<br/?>", "\n\n");
302     }
303
304     return html.replaceAll("<(.|\n)*?>", "");
305   }
306
307   @Contract(value = "null -> null; !null -> !null", pure = true)
308   public static String toLowerCase(@Nullable final String str) {
309     //noinspection ConstantConditions
310     return str == null ? null : str.toLowerCase();
311   }
312
313   @NotNull
314   @Contract(pure = true)
315   public static String getPackageName(@NotNull String fqName) {
316     return getPackageName(fqName, '.');
317   }
318
319   /**
320    * Given a fqName returns the package name for the type or the containing type.
321    * <p/>
322    * <ul>
323    * <li><code>java.lang.String</code> -> <code>java.lang</code></li>
324    * <li><code>java.util.Map.Entry</code> -> <code>java.util.Map</code></li>
325    * </ul>
326    *
327    * @param fqName    a fully qualified type name. Not supposed to contain any type arguments
328    * @param separator the separator to use. Typically '.'
329    * @return the package name of the type or the declarator of the type. The empty string if the given fqName is unqualified
330    */
331   @NotNull
332   @Contract(pure = true)
333   public static String getPackageName(@NotNull String fqName, char separator) {
334     int lastPointIdx = fqName.lastIndexOf(separator);
335     if (lastPointIdx >= 0) {
336       return fqName.substring(0, lastPointIdx);
337     }
338     return "";
339   }
340
341   @Contract(pure = true)
342   public static int getLineBreakCount(@NotNull CharSequence text) {
343     int count = 0;
344     for (int i = 0; i < text.length(); i++) {
345       char c = text.charAt(i);
346       if (c == '\n') {
347         count++;
348       }
349       else if (c == '\r') {
350         if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
351           //noinspection AssignmentToForLoopParameter
352           i++;
353           count++;
354         }
355         else {
356           count++;
357         }
358       }
359     }
360     return count;
361   }
362
363   @Contract(pure = true)
364   public static boolean containsLineBreak(@NotNull CharSequence text) {
365     for (int i = 0; i < text.length(); i++) {
366       char c = text.charAt(i);
367       if (isLineBreak(c)) return true;
368     }
369     return false;
370   }
371
372   @Contract(pure = true)
373   public static boolean isLineBreak(char c) {
374     return c == '\n' || c == '\r';
375   }
376
377   @NotNull
378   @Contract(pure = true)
379   public static String escapeLineBreak(@NotNull String text) {
380     StringBuilder buffer = new StringBuilder(text.length());
381     for (int i = 0; i < text.length(); i++) {
382       char c = text.charAt(i);
383       switch (c) {
384         case '\n':
385           buffer.append("\\n");
386           break;
387         case '\r':
388           buffer.append("\\r");
389           break;
390         default:
391           buffer.append(c);
392       }
393     }
394     return buffer.toString();
395   }
396
397   @Contract(pure = true)
398   public static boolean endsWithLineBreak(@NotNull CharSequence text) {
399     int len = text.length();
400     return len > 0 && isLineBreak(text.charAt(len - 1));
401   }
402
403   @Contract(pure = true)
404   public static int lineColToOffset(@NotNull CharSequence text, int line, int col) {
405     int curLine = 0;
406     int offset = 0;
407     while (line != curLine) {
408       if (offset == text.length()) return -1;
409       char c = text.charAt(offset);
410       if (c == '\n') {
411         curLine++;
412       }
413       else if (c == '\r') {
414         curLine++;
415         if (offset < text.length() - 1 && text.charAt(offset + 1) == '\n') {
416           offset++;
417         }
418       }
419       offset++;
420     }
421     return offset + col;
422   }
423
424   @Contract(pure = true)
425   public static int offsetToLineNumber(@NotNull CharSequence text, int offset) {
426     int curLine = 0;
427     int curOffset = 0;
428     while (curOffset < offset) {
429       if (curOffset == text.length()) return -1;
430       char c = text.charAt(curOffset);
431       if (c == '\n') {
432         curLine++;
433       }
434       else if (c == '\r') {
435         curLine++;
436         if (curOffset < text.length() - 1 && text.charAt(curOffset + 1) == '\n') {
437           curOffset++;
438         }
439       }
440       curOffset++;
441     }
442     return curLine;
443   }
444
445   /**
446    * Classic dynamic programming algorithm for string differences.
447    */
448   @Contract(pure = true)
449   public static int difference(@NotNull String s1, @NotNull String s2) {
450     int[][] a = new int[s1.length()][s2.length()];
451
452     for (int i = 0; i < s1.length(); i++) {
453       a[i][0] = i;
454     }
455
456     for (int j = 0; j < s2.length(); j++) {
457       a[0][j] = j;
458     }
459
460     for (int i = 1; i < s1.length(); i++) {
461       for (int j = 1; j < s2.length(); j++) {
462
463         a[i][j] = min(min(a[i - 1][j - 1] + (s1.charAt(i) == s2.charAt(j) ? 0 : 1), a[i - 1][j] + 1), a[i][j - 1] + 1);
464       }
465     }
466
467     return a[s1.length() - 1][s2.length() - 1];
468   }
469
470   @NotNull
471   @Contract(pure = true)
472   public static String wordsToBeginFromUpperCase(@NotNull String s) {
473     return fixCapitalization(s, ourPrepositions, true);
474   }
475
476   @NotNull
477   @Contract(pure = true)
478   public static String wordsToBeginFromLowerCase(@NotNull String s) {
479     return fixCapitalization(s, ourPrepositions, false);
480   }
481
482   @NotNull
483   @Contract(pure = true)
484   public static String toTitleCase(@NotNull String s) {
485     return fixCapitalization(s, ArrayUtil.EMPTY_STRING_ARRAY, true);
486   }
487
488   @NotNull
489   private static String fixCapitalization(@NotNull String s, @NotNull String[] prepositions, boolean title) {
490     StringBuilder buffer = null;
491     for (int i = 0; i < s.length(); i++) {
492       char prevChar = i == 0 ? ' ' : s.charAt(i - 1);
493       char currChar = s.charAt(i);
494       if (!Character.isLetterOrDigit(prevChar) && prevChar != '\'') {
495         if (Character.isLetterOrDigit(currChar)) {
496           if (title || Character.isUpperCase(currChar)) {
497             int j = i;
498             for (; j < s.length(); j++) {
499               if (!Character.isLetterOrDigit(s.charAt(j))) {
500                 break;
501               }
502             }
503             if (!isPreposition(s, i, j - 1, prepositions)) {
504               if (buffer == null) {
505                 buffer = new StringBuilder(s);
506               }
507               buffer.setCharAt(i, title ? toUpperCase(currChar) : toLowerCase(currChar));
508             }
509           }
510         }
511       }
512     }
513     return buffer == null ? s : buffer.toString();
514   }
515
516   @NonNls private static final String[] ourPrepositions = {
517     "a", "an", "and", "as", "at", "but", "by", "down", "for", "from", "if", "in", "into", "not", "of", "on", "onto", "or", "out", "over",
518     "per", "nor", "the", "to", "up", "upon", "via", "with"
519   };
520
521   @Contract(pure = true)
522   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar) {
523     return isPreposition(s, firstChar, lastChar, ourPrepositions);
524   }
525
526   @Contract(pure = true)
527   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar, @NotNull String[] prepositions) {
528     for (String preposition : prepositions) {
529       boolean found = false;
530       if (lastChar - firstChar + 1 == preposition.length()) {
531         found = true;
532         for (int j = 0; j < preposition.length(); j++) {
533           if (toLowerCase(s.charAt(firstChar + j)) != preposition.charAt(j)) {
534             found = false;
535           }
536         }
537       }
538       if (found) {
539         return true;
540       }
541     }
542     return false;
543   }
544
545   @NotNull
546   @Contract(pure = true)
547   public static NotNullFunction<String, String> escaper(final boolean escapeSlash, @Nullable final String additionalChars) {
548     return new NotNullFunction<String, String>() {
549       @NotNull
550       @Override
551       public String fun(@NotNull String dom) {
552         final StringBuilder builder = new StringBuilder(dom.length());
553         escapeStringCharacters(dom.length(), dom, additionalChars, escapeSlash, builder);
554         return builder.toString();
555       }
556     };
557   }
558
559
560   public static void escapeStringCharacters(int length, @NotNull String str, @NotNull @NonNls StringBuilder buffer) {
561     escapeStringCharacters(length, str, "\"", buffer);
562   }
563
564   @NotNull
565   public static StringBuilder escapeStringCharacters(int length,
566                                                      @NotNull String str,
567                                                      @Nullable String additionalChars,
568                                                      @NotNull @NonNls StringBuilder buffer) {
569     return escapeStringCharacters(length, str, additionalChars, true, buffer);
570   }
571
572   @NotNull
573   public static StringBuilder escapeStringCharacters(int length,
574                                                      @NotNull String str,
575                                                      @Nullable String additionalChars,
576                                                      boolean escapeSlash,
577                                                      @NotNull @NonNls StringBuilder buffer) {
578     char prev = 0;
579     for (int idx = 0; idx < length; idx++) {
580       char ch = str.charAt(idx);
581       switch (ch) {
582         case '\b':
583           buffer.append("\\b");
584           break;
585
586         case '\t':
587           buffer.append("\\t");
588           break;
589
590         case '\n':
591           buffer.append("\\n");
592           break;
593
594         case '\f':
595           buffer.append("\\f");
596           break;
597
598         case '\r':
599           buffer.append("\\r");
600           break;
601
602         default:
603           if (escapeSlash && ch == '\\') {
604             buffer.append("\\\\");
605           }
606           else if (additionalChars != null && additionalChars.indexOf(ch) > -1 && (escapeSlash || prev != '\\')) {
607             buffer.append("\\").append(ch);
608           }
609           else if (!isPrintableUnicode(ch)) {
610             CharSequence hexCode = StringUtilRt.toUpperCase(Integer.toHexString(ch));
611             buffer.append("\\u");
612             int paddingCount = 4 - hexCode.length();
613             while (paddingCount-- > 0) {
614               buffer.append(0);
615             }
616             buffer.append(hexCode);
617           }
618           else {
619             buffer.append(ch);
620           }
621       }
622       prev = ch;
623     }
624     return buffer;
625   }
626
627   @Contract(pure = true)
628   private static boolean isPrintableUnicode(char c) {
629     int t = Character.getType(c);
630     return t != Character.UNASSIGNED && t != Character.LINE_SEPARATOR && t != Character.PARAGRAPH_SEPARATOR &&
631            t != Character.CONTROL && t != Character.FORMAT && t != Character.PRIVATE_USE && t != Character.SURROGATE;
632   }
633
634   @NotNull
635   @Contract(pure = true)
636   public static String escapeStringCharacters(@NotNull String s) {
637     StringBuilder buffer = new StringBuilder(s.length());
638     escapeStringCharacters(s.length(), s, "\"", buffer);
639     return buffer.toString();
640   }
641
642   @NotNull
643   @Contract(pure = true)
644   public static String escapeCharCharacters(@NotNull String s) {
645     StringBuilder buffer = new StringBuilder(s.length());
646     escapeStringCharacters(s.length(), s, "\'", buffer);
647     return buffer.toString();
648   }
649
650   @NotNull
651   @Contract(pure = true)
652   public static String unescapeStringCharacters(@NotNull String s) {
653     StringBuilder buffer = new StringBuilder(s.length());
654     unescapeStringCharacters(s.length(), s, buffer);
655     return buffer.toString();
656   }
657
658   private static boolean isQuoteAt(@NotNull String s, int ind) {
659     char ch = s.charAt(ind);
660     return ch == '\'' || ch == '\"';
661   }
662
663   @Contract(pure = true)
664   public static boolean isQuotedString(@NotNull String s) {
665     return s.length() > 1 && isQuoteAt(s, 0) && s.charAt(0) == s.charAt(s.length() - 1);
666   }
667
668   @NotNull
669   @Contract(pure = true)
670   public static String unquoteString(@NotNull String s) {
671     if (isQuotedString(s)) {
672       return s.substring(1, s.length() - 1);
673     }
674     return s;
675   }
676
677   @NotNull
678   @Contract(pure = true)
679   public static String unquoteString(@NotNull String s, char quotationChar) {
680     if (s.length() > 1 && quotationChar == s.charAt(0) && quotationChar == s.charAt(s.length() - 1)) {
681       return s.substring(1, s.length() - 1);
682     }
683     return s;
684   }
685
686   /**
687    * This is just an optimized version of Matcher.quoteReplacement
688    */
689   @NotNull
690   @Contract(pure = true)
691   public static String quoteReplacement(@NotNull String s) {
692     boolean needReplacements = false;
693
694     for (int i = 0; i < s.length(); i++) {
695       char c = s.charAt(i);
696       if (c == '\\' || c == '$') {
697         needReplacements = true;
698         break;
699       }
700     }
701
702     if (!needReplacements) return s;
703
704     StringBuilder sb = new StringBuilder(s.length() * 6 / 5);
705     for (int i = 0; i < s.length(); i++) {
706       char c = s.charAt(i);
707       if (c == '\\') {
708         sb.append('\\');
709         sb.append('\\');
710       }
711       else if (c == '$') {
712         sb.append('\\');
713         sb.append('$');
714       }
715       else {
716         sb.append(c);
717       }
718     }
719     return sb.toString();
720   }
721
722   private static void unescapeStringCharacters(int length, @NotNull String s, @NotNull StringBuilder buffer) {
723     boolean escaped = false;
724     for (int idx = 0; idx < length; idx++) {
725       char ch = s.charAt(idx);
726       if (!escaped) {
727         if (ch == '\\') {
728           escaped = true;
729         }
730         else {
731           buffer.append(ch);
732         }
733       }
734       else {
735         switch (ch) {
736           case 'n':
737             buffer.append('\n');
738             break;
739
740           case 'r':
741             buffer.append('\r');
742             break;
743
744           case 'b':
745             buffer.append('\b');
746             break;
747
748           case 't':
749             buffer.append('\t');
750             break;
751
752           case 'f':
753             buffer.append('\f');
754             break;
755
756           case '\'':
757             buffer.append('\'');
758             break;
759
760           case '\"':
761             buffer.append('\"');
762             break;
763
764           case '\\':
765             buffer.append('\\');
766             break;
767
768           case 'u':
769             if (idx + 4 < length) {
770               try {
771                 int code = Integer.parseInt(s.substring(idx + 1, idx + 5), 16);
772                 idx += 4;
773                 buffer.append((char)code);
774               }
775               catch (NumberFormatException e) {
776                 buffer.append("\\u");
777               }
778             }
779             else {
780               buffer.append("\\u");
781             }
782             break;
783
784           default:
785             buffer.append(ch);
786             break;
787         }
788         escaped = false;
789       }
790     }
791
792     if (escaped) buffer.append('\\');
793   }
794
795   @SuppressWarnings("HardCodedStringLiteral")
796   @NotNull
797   @Contract(pure = true)
798   public static String pluralize(@NotNull String suggestion) {
799     if (suggestion.endsWith("Child") || suggestion.endsWith("child")) {
800       return suggestion + "ren";
801     }
802
803     if (suggestion.equals("this")) {
804       return "these";
805     }
806     if (suggestion.equals("This")) {
807       return "These";
808     }
809     if (suggestion.equals("fix") || suggestion.equals("Fix")) {
810       return suggestion + "es";
811     }
812
813     if (endsWithIgnoreCase(suggestion, "es")) {
814       return suggestion;
815     }
816
817     int len = suggestion.length();
818     if (endsWithIgnoreCase(suggestion, "ex") || endsWithIgnoreCase(suggestion, "ix")) {
819       return suggestion.substring(0, len - 2) + "ices";
820     }
821     if (endsWithIgnoreCase(suggestion, "um")) {
822       return suggestion.substring(0, len - 2) + "a";
823     }
824     if (endsWithIgnoreCase(suggestion, "an")) {
825       return suggestion.substring(0, len - 2) + "en";
826     }
827
828     if (endsWithIgnoreCase(suggestion, "s") || endsWithIgnoreCase(suggestion, "x") || endsWithIgnoreCase(suggestion, "ch")) {
829       return suggestion + "es";
830     }
831
832     if (endsWithIgnoreCase(suggestion, "y") && len > 1 && !isVowel(toLowerCase(suggestion.charAt(len - 2)))) {
833       return suggestion.substring(0, len - 1) + "ies";
834     }
835
836     return suggestion + "s";
837   }
838
839   @NotNull
840   @Contract(pure = true)
841   public static String capitalizeWords(@NotNull String text,
842                                        boolean allWords) {
843     return capitalizeWords(text, " \t\n\r\f", allWords, false);
844   }
845
846   @NotNull
847   @Contract(pure = true)
848   public static String capitalizeWords(@NotNull String text,
849                                        @NotNull String tokenizerDelim,
850                                        boolean allWords,
851                                        boolean leaveOriginalDelims) {
852     final StringTokenizer tokenizer = new StringTokenizer(text, tokenizerDelim, leaveOriginalDelims);
853     final StringBuilder out = new StringBuilder(text.length());
854     boolean toCapitalize = true;
855     while (tokenizer.hasMoreTokens()) {
856       final String word = tokenizer.nextToken();
857       if (!leaveOriginalDelims && out.length() > 0) {
858         out.append(' ');
859       }
860       out.append(toCapitalize ? capitalize(word) : word);
861       if (!allWords) {
862         toCapitalize = false;
863       }
864     }
865     return out.toString();
866   }
867
868   @Contract(pure = true)
869   public static String decapitalize(String s) {
870     return Introspector.decapitalize(s);
871   }
872
873   @Contract(pure = true)
874   public static boolean isVowel(char c) {
875     return VOWELS.indexOf(c) >= 0;
876   }
877
878   @NotNull
879   @Contract(pure = true)
880   public static String capitalize(@NotNull String s) {
881     if (s.isEmpty()) return s;
882     if (s.length() == 1) return StringUtilRt.toUpperCase(s).toString();
883
884     // Optimization
885     if (Character.isUpperCase(s.charAt(0))) return s;
886     return toUpperCase(s.charAt(0)) + s.substring(1);
887   }
888
889   @Contract(value = "null -> false", pure = true)
890   public static boolean isCapitalized(@Nullable String s) {
891     return s != null && !s.isEmpty() && Character.isUpperCase(s.charAt(0));
892   }
893
894   @NotNull
895   @Contract(pure = true)
896   public static String capitalizeWithJavaBeanConvention(@NotNull String s) {
897     if (s.length() > 1 && Character.isUpperCase(s.charAt(1))) {
898       return s;
899     }
900     return capitalize(s);
901   }
902
903   @Contract(pure = true)
904   public static int stringHashCode(@NotNull CharSequence chars) {
905     if (chars instanceof String || chars instanceof CharSequenceWithStringHash) {
906       // we know for sure these classes have conformant (and maybe faster) hashCode()
907       return chars.hashCode();
908     }
909
910     return stringHashCode(chars, 0, chars.length());
911   }
912
913   @Contract(pure = true)
914   public static int stringHashCode(@NotNull CharSequence chars, int from, int to) {
915     int h = 0;
916     for (int off = from; off < to; off++) {
917       h = 31 * h + chars.charAt(off);
918     }
919     return h;
920   }
921
922   @Contract(pure = true)
923   public static int stringHashCode(char[] chars, int from, int to) {
924     int h = 0;
925     for (int off = from; off < to; off++) {
926       h = 31 * h + chars[off];
927     }
928     return h;
929   }
930
931   @Contract(pure = true)
932   public static int stringHashCodeInsensitive(@NotNull char[] chars, int from, int to) {
933     int h = 0;
934     for (int off = from; off < to; off++) {
935       h = 31 * h + toLowerCase(chars[off]);
936     }
937     return h;
938   }
939
940   @Contract(pure = true)
941   public static int stringHashCodeInsensitive(@NotNull CharSequence chars, int from, int to) {
942     int h = 0;
943     for (int off = from; off < to; off++) {
944       h = 31 * h + toLowerCase(chars.charAt(off));
945     }
946     return h;
947   }
948
949   @Contract(pure = true)
950   public static int stringHashCodeInsensitive(@NotNull CharSequence chars) {
951     return stringHashCodeInsensitive(chars, 0, chars.length());
952   }
953
954   @Contract(pure = true)
955   public static int stringHashCodeIgnoreWhitespaces(char[] chars, int from, int to) {
956     int h = 0;
957     for (int off = from; off < to; off++) {
958       char c = chars[off];
959       if (!isWhiteSpace(c)) {
960         h = 31 * h + c;
961       }
962     }
963     return h;
964   }
965
966   @Contract(pure = true)
967   public static int stringHashCodeIgnoreWhitespaces(@NotNull CharSequence chars, int from, int to) {
968     int h = 0;
969     for (int off = from; off < to; off++) {
970       char c = chars.charAt(off);
971       if (!isWhiteSpace(c)) {
972         h = 31 * h + c;
973       }
974     }
975     return h;
976   }
977
978   @Contract(pure = true)
979   public static int stringHashCodeIgnoreWhitespaces(@NotNull CharSequence chars) {
980     return stringHashCodeIgnoreWhitespaces(chars, 0, chars.length());
981   }
982
983   /**
984    * Equivalent to string.startsWith(prefixes[0] + prefixes[1] + ...) but avoids creating an object for concatenation.
985    */
986   @Contract(pure = true)
987   public static boolean startsWithConcatenation(@NotNull String string, @NotNull String... prefixes) {
988     int offset = 0;
989     for (String prefix : prefixes) {
990       int prefixLen = prefix.length();
991       if (!string.regionMatches(offset, prefix, 0, prefixLen)) {
992         return false;
993       }
994       offset += prefixLen;
995     }
996     return true;
997   }
998
999   @Contract(value = "null -> null; !null -> !null", pure = true)
1000   public static String trim(@Nullable String s) {
1001     return s == null ? null : s.trim();
1002   }
1003
1004   @NotNull
1005   @Contract(pure = true)
1006   public static String trimEnd(@NotNull String s, @NonNls @NotNull String suffix) {
1007     return trimEnd(s, suffix, false);
1008   }
1009
1010   @NotNull
1011   @Contract(pure = true)
1012   public static String trimEnd(@NotNull String s, @NonNls @NotNull String suffix, boolean ignoreCase) {
1013     boolean endsWith = ignoreCase ? endsWithIgnoreCase(s, suffix) : s.endsWith(suffix);
1014     if (endsWith) {
1015       return s.substring(0, s.length() - suffix.length());
1016     }
1017     return s;
1018   }
1019
1020   @NotNull
1021   @Contract(pure = true)
1022   public static String trimEnd(@NotNull String s, char suffix) {
1023     if (endsWithChar(s, suffix)) {
1024       return s.substring(0, s.length() - 1);
1025     }
1026     return s;
1027   }
1028
1029   @NotNull
1030   @Contract(pure = true)
1031   public static String trimLog(@NotNull final String text, final int limit) {
1032     if (limit > 5 && text.length() > limit) {
1033       return text.substring(0, limit - 5) + " ...\n";
1034     }
1035     return text;
1036   }
1037
1038   @NotNull
1039   @Contract(pure = true)
1040   public static String trimLeading(@NotNull String string) {
1041     return trimLeading((CharSequence)string).toString();
1042   }
1043   @NotNull
1044   @Contract(pure = true)
1045   public static CharSequence trimLeading(@NotNull CharSequence string) {
1046     int index = 0;
1047     while (index < string.length() && Character.isWhitespace(string.charAt(index))) index++;
1048     return string.subSequence(index, string.length());
1049   }
1050
1051   @NotNull
1052   @Contract(pure = true)
1053   public static String trimLeading(@NotNull String string, char symbol) {
1054     int index = 0;
1055     while (index < string.length() && string.charAt(index) == symbol) index++;
1056     return string.substring(index);
1057   }
1058
1059   @NotNull
1060   @Contract(pure = true)
1061   public static String trimTrailing(@NotNull String string) {
1062     return trimTrailing((CharSequence)string).toString();
1063   }
1064
1065   @NotNull
1066   @Contract(pure = true)
1067   public static CharSequence trimTrailing(@NotNull CharSequence string) {
1068     int index = string.length() - 1;
1069     while (index >= 0 && Character.isWhitespace(string.charAt(index))) index--;
1070     return string.subSequence(0, index + 1);
1071   }
1072
1073   @NotNull
1074   @Contract(pure = true)
1075   public static String trimTrailing(@NotNull String string, char symbol) {
1076     int index = string.length() - 1;
1077     while (index >= 0 && string.charAt(index) == symbol) index--;
1078     return string.substring(0, index + 1);
1079   }
1080
1081   @Contract(pure = true)
1082   public static boolean startsWithChar(@Nullable CharSequence s, char prefix) {
1083     return s != null && s.length() != 0 && s.charAt(0) == prefix;
1084   }
1085
1086   @Contract(pure = true)
1087   public static boolean endsWithChar(@Nullable CharSequence s, char suffix) {
1088     return StringUtilRt.endsWithChar(s, suffix);
1089   }
1090
1091   @NotNull
1092   @Contract(pure = true)
1093   public static String trimStart(@NotNull String s, @NonNls @NotNull String prefix) {
1094     if (s.startsWith(prefix)) {
1095       return s.substring(prefix.length());
1096     }
1097     return s;
1098   }
1099
1100   @NotNull
1101   @Contract(pure = true)
1102   public static String pluralize(@NotNull String base, int n) {
1103     if (n == 1) return base;
1104     return pluralize(base);
1105   }
1106
1107   public static void repeatSymbol(@NotNull Appendable buffer, char symbol, int times) {
1108     assert times >= 0 : times;
1109     try {
1110       for (int i = 0; i < times; i++) {
1111         buffer.append(symbol);
1112       }
1113     }
1114     catch (IOException e) {
1115       LOG.error(e);
1116     }
1117   }
1118
1119   @Contract(pure = true)
1120   public static String defaultIfEmpty(@Nullable String value, String defaultValue) {
1121     return isEmpty(value) ? defaultValue : value;
1122   }
1123
1124   @Contract(value = "null -> false", pure = true)
1125   public static boolean isNotEmpty(@Nullable String s) {
1126     return s != null && !s.isEmpty();
1127   }
1128
1129   @Contract(value = "null -> true", pure=true)
1130   public static boolean isEmpty(@Nullable String s) {
1131     return s == null || s.isEmpty();
1132   }
1133
1134   @Contract(value = "null -> true",pure = true)
1135   public static boolean isEmpty(@Nullable CharSequence cs) {
1136     return cs == null || cs.length() == 0;
1137   }
1138
1139   @Contract(pure = true)
1140   public static int length(@Nullable CharSequence cs) {
1141     return cs == null ? 0 : cs.length();
1142   }
1143
1144   @NotNull
1145   @Contract(pure = true)
1146   public static String notNullize(@Nullable final String s) {
1147     return notNullize(s, "");
1148   }
1149
1150   @NotNull
1151   @Contract(pure = true)
1152   public static String notNullize(@Nullable final String s, @NotNull String defaultValue) {
1153     return s == null ? defaultValue : s;
1154   }
1155
1156   @Nullable
1157   @Contract(pure = true)
1158   public static String nullize(@Nullable final String s) {
1159     return nullize(s, false);
1160   }
1161
1162   @Nullable
1163   @Contract(pure = true)
1164   public static String nullize(@Nullable final String s, boolean nullizeSpaces) {
1165     if (nullizeSpaces) {
1166       if (isEmptyOrSpaces(s)) return null;
1167     }
1168     else {
1169       if (isEmpty(s)) return null;
1170     }
1171     return s;
1172   }
1173
1174   @Contract(value = "null -> true",pure = true)
1175   // we need to keep this method to preserve backward compatibility
1176   public static boolean isEmptyOrSpaces(@Nullable String s) {
1177     return isEmptyOrSpaces((CharSequence)s);
1178   }
1179
1180   @Contract(value = "null -> true", pure = true)
1181   public static boolean isEmptyOrSpaces(@Nullable CharSequence s) {
1182     if (isEmpty(s)) {
1183       return true;
1184     }
1185     for (int i = 0; i < s.length(); i++) {
1186       if (s.charAt(i) > ' ') {
1187         return false;
1188       }
1189     }
1190     return true;
1191   }
1192
1193   /**
1194    * Allows to answer if given symbol is white space, tabulation or line feed.
1195    *
1196    * @param c symbol to check
1197    * @return <code>true</code> if given symbol is white space, tabulation or line feed; <code>false</code> otherwise
1198    */
1199   @Contract(pure = true)
1200   public static boolean isWhiteSpace(char c) {
1201     return c == '\n' || c == '\t' || c == ' ';
1202   }
1203
1204   @NotNull
1205   @Contract(pure = true)
1206   public static String getThrowableText(@NotNull Throwable aThrowable) {
1207     return ExceptionUtil.getThrowableText(aThrowable);
1208   }
1209
1210   @NotNull
1211   @Contract(pure = true)
1212   public static String getThrowableText(@NotNull Throwable aThrowable, @NonNls @NotNull final String stackFrameSkipPattern) {
1213     return ExceptionUtil.getThrowableText(aThrowable, stackFrameSkipPattern);
1214   }
1215
1216   @Nullable
1217   @Contract(pure = true)
1218   public static String getMessage(@NotNull Throwable e) {
1219     return ExceptionUtil.getMessage(e);
1220   }
1221
1222   @NotNull
1223   @Contract(pure = true)
1224   public static String repeatSymbol(final char aChar, final int count) {
1225     char[] buffer = new char[count];
1226     Arrays.fill(buffer, aChar);
1227     return StringFactory.createShared(buffer);
1228   }
1229
1230   @NotNull
1231   @Contract(pure = true)
1232   public static String repeat(@NotNull String s, int count) {
1233     assert count >= 0 : count;
1234     StringBuilder sb = new StringBuilder(s.length() * count);
1235     for (int i = 0; i < count; i++) {
1236       sb.append(s);
1237     }
1238     return sb.toString();
1239   }
1240
1241   @NotNull
1242   @Contract(pure = true)
1243   public static List<String> splitHonorQuotes(@NotNull String s, char separator) {
1244     final List<String> result = new ArrayList<String>();
1245     final StringBuilder builder = new StringBuilder(s.length());
1246     boolean inQuotes = false;
1247     for (int i = 0; i < s.length(); i++) {
1248       final char c = s.charAt(i);
1249       if (c == separator && !inQuotes) {
1250         if (builder.length() > 0) {
1251           result.add(builder.toString());
1252           builder.setLength(0);
1253         }
1254         continue;
1255       }
1256
1257       if ((c == '"' || c == '\'') && !(i > 0 && s.charAt(i - 1) == '\\')) {
1258         inQuotes = !inQuotes;
1259       }
1260       builder.append(c);
1261     }
1262
1263     if (builder.length() > 0) {
1264       result.add(builder.toString());
1265     }
1266     return result;
1267   }
1268
1269
1270   @NotNull
1271   @Contract(pure = true)
1272   public static List<String> split(@NotNull String s, @NotNull String separator) {
1273     return split(s, separator, true);
1274   }
1275   @NotNull
1276   @Contract(pure = true)
1277   public static List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator) {
1278     return split(s, separator, true, true);
1279   }
1280
1281   @NotNull
1282   @Contract(pure = true)
1283   public static List<String> split(@NotNull String s, @NotNull String separator,
1284                                    boolean excludeSeparator) {
1285     return split(s, separator, excludeSeparator, true);
1286   }
1287
1288   @NotNull
1289   @Contract(pure = true)
1290   public static List<String> split(@NotNull String s, @NotNull String separator,
1291                                    boolean excludeSeparator, boolean excludeEmptyStrings) {
1292     return (List)split((CharSequence)s,separator,excludeSeparator,excludeEmptyStrings);
1293   }
1294   @NotNull
1295   @Contract(pure = true)
1296   public static List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator,
1297                                    boolean excludeSeparator, boolean excludeEmptyStrings) {
1298     if (separator.length() == 0) {
1299       return Collections.singletonList(s);
1300     }
1301     List<CharSequence> result = new ArrayList<CharSequence>();
1302     int pos = 0;
1303     while (true) {
1304       int index = indexOf(s,separator, pos);
1305       if (index == -1) break;
1306       final int nextPos = index + separator.length();
1307       CharSequence token = s.subSequence(pos, excludeSeparator ? index : nextPos);
1308       if (token.length() != 0 || !excludeEmptyStrings) {
1309         result.add(token);
1310       }
1311       pos = nextPos;
1312     }
1313     if (pos < s.length() || !excludeEmptyStrings && pos == s.length()) {
1314       result.add(s.subSequence(pos, s.length()));
1315     }
1316     return result;
1317   }
1318
1319   @NotNull
1320   @Contract(pure = true)
1321   public static Iterable<String> tokenize(@NotNull String s, @NotNull String separators) {
1322     final com.intellij.util.text.StringTokenizer tokenizer = new com.intellij.util.text.StringTokenizer(s, separators);
1323     return new Iterable<String>() {
1324       @NotNull
1325       @Override
1326       public Iterator<String> iterator() {
1327         return new Iterator<String>() {
1328           @Override
1329           public boolean hasNext() {
1330             return tokenizer.hasMoreTokens();
1331           }
1332
1333           @Override
1334           public String next() {
1335             return tokenizer.nextToken();
1336           }
1337
1338           @Override
1339           public void remove() {
1340             throw new UnsupportedOperationException();
1341           }
1342         };
1343       }
1344     };
1345   }
1346
1347   @NotNull
1348   @Contract(pure = true)
1349   public static Iterable<String> tokenize(@NotNull final StringTokenizer tokenizer) {
1350     return new Iterable<String>() {
1351       @NotNull
1352       @Override
1353       public Iterator<String> iterator() {
1354         return new Iterator<String>() {
1355           @Override
1356           public boolean hasNext() {
1357             return tokenizer.hasMoreTokens();
1358           }
1359
1360           @Override
1361           public String next() {
1362             return tokenizer.nextToken();
1363           }
1364
1365           @Override
1366           public void remove() {
1367             throw new UnsupportedOperationException();
1368           }
1369         };
1370       }
1371     };
1372   }
1373
1374   /**
1375    * @return list containing all words in {@code text}, or {@link ContainerUtil#emptyList()} if there are none.
1376    * The <b>word</b> here means the maximum sub-string consisting entirely of characters which are <code>Character.isJavaIdentifierPart(c)</code>.
1377    */
1378   @NotNull
1379   @Contract(pure = true)
1380   public static List<String> getWordsIn(@NotNull String text) {
1381     List<String> result = null;
1382     int start = -1;
1383     for (int i = 0; i < text.length(); i++) {
1384       char c = text.charAt(i);
1385       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1386       if (isIdentifierPart && start == -1) {
1387         start = i;
1388       }
1389       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1390         if (result == null) {
1391           result = new SmartList<String>();
1392         }
1393         result.add(text.substring(start, i + 1));
1394       }
1395       else if (!isIdentifierPart && start != -1) {
1396         if (result == null) {
1397           result = new SmartList<String>();
1398         }
1399         result.add(text.substring(start, i));
1400         start = -1;
1401       }
1402     }
1403     if (result == null) {
1404       return ContainerUtil.emptyList();
1405     }
1406     return result;
1407   }
1408
1409   @NotNull
1410   @Contract(pure = true)
1411   public static List<TextRange> getWordIndicesIn(@NotNull String text) {
1412     List<TextRange> result = new SmartList<TextRange>();
1413     int start = -1;
1414     for (int i = 0; i < text.length(); i++) {
1415       char c = text.charAt(i);
1416       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1417       if (isIdentifierPart && start == -1) {
1418         start = i;
1419       }
1420       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1421         result.add(new TextRange(start, i + 1));
1422       }
1423       else if (!isIdentifierPart && start != -1) {
1424         result.add(new TextRange(start, i));
1425         start = -1;
1426       }
1427     }
1428     return result;
1429   }
1430
1431   @NotNull
1432   @Contract(pure = true)
1433   public static String join(@NotNull final String[] strings, @NotNull final String separator) {
1434     return join(strings, 0, strings.length, separator);
1435   }
1436
1437   @NotNull
1438   @Contract(pure = true)
1439   public static String join(@NotNull final String[] strings, int startIndex, int endIndex, @NotNull final String separator) {
1440     final StringBuilder result = new StringBuilder();
1441     for (int i = startIndex; i < endIndex; i++) {
1442       if (i > startIndex) result.append(separator);
1443       result.append(strings[i]);
1444     }
1445     return result.toString();
1446   }
1447
1448   @NotNull
1449   @Contract(pure = true)
1450   public static String[] zip(@NotNull String[] strings1, @NotNull String[] strings2, String separator) {
1451     if (strings1.length != strings2.length) throw new IllegalArgumentException();
1452
1453     String[] result = ArrayUtil.newStringArray(strings1.length);
1454     for (int i = 0; i < result.length; i++) {
1455       result[i] = strings1[i] + separator + strings2[i];
1456     }
1457
1458     return result;
1459   }
1460
1461   @NotNull
1462   @Contract(pure = true)
1463   public static String[] surround(@NotNull String[] strings1, String prefix, String suffix) {
1464     String[] result = ArrayUtil.newStringArray(strings1.length);
1465     for (int i = 0; i < result.length; i++) {
1466       result[i] = prefix + strings1[i] + suffix;
1467     }
1468
1469     return result;
1470   }
1471
1472   @NotNull
1473   @Contract(pure = true)
1474   public static <T> String join(@NotNull T[] items, @NotNull Function<T, String> f, @NotNull @NonNls String separator) {
1475     return join(Arrays.asList(items), f, separator);
1476   }
1477
1478   @NotNull
1479   @Contract(pure = true)
1480   public static <T> String join(@NotNull Collection<? extends T> items,
1481                                 @NotNull Function<? super T, String> f,
1482                                 @NotNull @NonNls String separator) {
1483     if (items.isEmpty()) return "";
1484     return join((Iterable<? extends T>)items, f, separator);
1485   }
1486
1487   @Contract(pure = true)
1488   public static String join(@NotNull Iterable<?> items, @NotNull @NonNls String separator) {
1489     StringBuilder result = new StringBuilder();
1490     for (Object item : items) {
1491       result.append(item).append(separator);
1492     }
1493     if (result.length() > 0) {
1494       result.setLength(result.length() - separator.length());
1495     }
1496     return result.toString();
1497   }
1498
1499   @NotNull
1500   @Contract(pure = true)
1501   public static <T> String join(@NotNull Iterable<? extends T> items,
1502                                 @NotNull Function<? super T, String> f,
1503                                 @NotNull @NonNls String separator) {
1504     final StringBuilder result = new StringBuilder();
1505     for (T item : items) {
1506       String string = f.fun(item);
1507       if (string != null && !string.isEmpty()) {
1508         if (result.length() != 0) result.append(separator);
1509         result.append(string);
1510       }
1511     }
1512     return result.toString();
1513   }
1514
1515   @NotNull
1516   @Contract(pure = true)
1517   public static String join(@NotNull Collection<String> strings, @NotNull String separator) {
1518     if (strings.size() <= 1) {
1519       return notNullize(ContainerUtil.getFirstItem(strings));
1520     }
1521     StringBuilder result = new StringBuilder();
1522     join(strings, separator, result);
1523     return result.toString();
1524   }
1525
1526   public static void join(@NotNull Collection<String> strings, @NotNull String separator, @NotNull StringBuilder result) {
1527     boolean isFirst = true;
1528     for (String string : strings) {
1529       if (string != null) {
1530         if (isFirst) {
1531           isFirst = false;
1532         }
1533         else {
1534           result.append(separator);
1535         }
1536         result.append(string);
1537       }
1538     }
1539   }
1540
1541   @NotNull
1542   @Contract(pure = true)
1543   public static String join(@NotNull final int[] strings, @NotNull final String separator) {
1544     final StringBuilder result = new StringBuilder();
1545     for (int i = 0; i < strings.length; i++) {
1546       if (i > 0) result.append(separator);
1547       result.append(strings[i]);
1548     }
1549     return result.toString();
1550   }
1551
1552   @NotNull
1553   @Contract(pure = true)
1554   public static String join(@Nullable final String... strings) {
1555     if (strings == null || strings.length == 0) return "";
1556
1557     final StringBuilder builder = new StringBuilder();
1558     for (final String string : strings) {
1559       builder.append(string);
1560     }
1561     return builder.toString();
1562   }
1563
1564   /**
1565    * Consider using {@link StringUtil#unquoteString(String)} instead.
1566    * Note: this method has an odd behavior:
1567    *   Quotes are removed even if leading and trailing quotes are different or
1568    *                           if there is only one quote (leading or trailing).
1569    */
1570   @NotNull
1571   @Contract(pure = true)
1572   public static String stripQuotesAroundValue(@NotNull String text) {
1573     final int len = text.length();
1574     if (len > 0) {
1575       final int from = isQuoteAt(text, 0) ? 1 : 0;
1576       final int to = len > 1 && isQuoteAt(text, len - 1) ? len - 1 : len;
1577       if (from > 0 || to < len) {
1578         return text.substring(from, to);
1579       }
1580     }
1581     return text;
1582   }
1583
1584   /**
1585    * Formats the specified file size as a string.
1586    *
1587    * @param fileSize the size to format.
1588    * @return the size formatted as a string.
1589    * @since 5.0.1
1590    */
1591   @NotNull
1592   @Contract(pure = true)
1593   public static String formatFileSize(long fileSize) {
1594     return formatValue(fileSize, null,
1595                        new String[]{"B", "K", "M", "G", "T", "P", "E"},
1596                        new long[]{1000, 1000, 1000, 1000, 1000, 1000});
1597   }
1598
1599   @NotNull
1600   @Contract(pure = true)
1601   public static String formatDuration(long duration) {
1602     return formatValue(duration, " ",
1603                        new String[]{"ms", "s", "m", "h", "d", "w", "mo", "yr", "c", "ml", "ep"},
1604                        new long[]{1000, 60, 60, 24, 7, 4, 12, 100, 10, 10000});
1605   }
1606
1607   @NotNull
1608   private static String formatValue(long value, String partSeparator, String[] units, long[] multipliers) {
1609     StringBuilder sb = new StringBuilder();
1610     long count = value;
1611     long remainder = 0;
1612     int i = 0;
1613     for (; i < units.length; i++) {
1614       long multiplier = i < multipliers.length ? multipliers[i] : -1;
1615       if (multiplier == -1 || count < multiplier) break;
1616       remainder = count % multiplier;
1617       count /= multiplier;
1618       if (partSeparator != null && (remainder != 0 || sb.length() > 0)) {
1619         sb.insert(0, units[i]).insert(0, remainder).insert(0, partSeparator);
1620       }
1621     }
1622     if (partSeparator != null || remainder == 0) {
1623       sb.insert(0, units[i]).insert(0, count);
1624     }
1625     else if (remainder > 0) {
1626       sb.append(String.format(Locale.US, "%.2f", count + (double)remainder / multipliers[i - 1])).append(units[i]);
1627     }
1628     return sb.toString();
1629   }
1630
1631   /**
1632    * Returns unpluralized variant using English based heuristics like properties -> property, names -> name, children -> child.
1633    * Returns <code>null</code> if failed to match appropriate heuristic.
1634    *
1635    * @param name english word in plural form
1636    * @return name in singular form or <code>null</code> if failed to find one.
1637    */
1638   @SuppressWarnings("HardCodedStringLiteral")
1639   @Nullable
1640   @Contract(pure = true)
1641   public static String unpluralize(@NotNull final String name) {
1642     if (name.endsWith("sses") || name.endsWith("shes") || name.endsWith("ches") || name.endsWith("xes")) { //?
1643       return name.substring(0, name.length() - 2);
1644     }
1645
1646     if (name.endsWith("ses")) {
1647       return name.substring(0, name.length() - 1);
1648     }
1649
1650     if (name.endsWith("ies")) {
1651       if (name.endsWith("cookies") || name.endsWith("Cookies")) {
1652         return name.substring(0, name.length() - "ookies".length()) + "ookie";
1653       }
1654
1655       return name.substring(0, name.length() - 3) + "y";
1656     }
1657
1658     if (name.endsWith("leaves") || name.endsWith("Leaves")) {
1659       return name.substring(0, name.length() - "eaves".length()) + "eaf";
1660     }
1661
1662     String result = stripEnding(name, "s");
1663     if (result != null) {
1664       return result;
1665     }
1666
1667     if (name.endsWith("children")) {
1668       return name.substring(0, name.length() - "children".length()) + "child";
1669     }
1670
1671     if (name.endsWith("Children") && name.length() > "Children".length()) {
1672       return name.substring(0, name.length() - "Children".length()) + "Child";
1673     }
1674
1675
1676     return null;
1677   }
1678
1679   @Nullable
1680   @Contract(pure = true)
1681   private static String stripEnding(@NotNull String name, @NotNull String ending) {
1682     if (name.endsWith(ending)) {
1683       if (name.equals(ending)) return name; // do not return empty string
1684       return name.substring(0, name.length() - 1);
1685     }
1686     return null;
1687   }
1688
1689   @Contract(pure = true)
1690   public static boolean containsAlphaCharacters(@NotNull String value) {
1691     for (int i = 0; i < value.length(); i++) {
1692       if (Character.isLetter(value.charAt(i))) return true;
1693     }
1694     return false;
1695   }
1696
1697   @Contract(pure = true)
1698   public static boolean containsAnyChar(@NotNull final String value, @NotNull final String chars) {
1699     if (chars.length() > value.length()) {
1700       return containsAnyChar(value, chars, 0, value.length());
1701     }
1702     else {
1703       return containsAnyChar(chars, value, 0, chars.length());
1704     }
1705   }
1706
1707   @Contract(pure = true)
1708   public static boolean containsAnyChar(@NotNull final String value,
1709                                         @NotNull final String chars,
1710                                         final int start, final int end) {
1711     for (int i = start; i < end; i++) {
1712       if (chars.indexOf(value.charAt(i)) >= 0) {
1713         return true;
1714       }
1715     }
1716
1717     return false;
1718   }
1719
1720   @Contract(pure = true)
1721   public static boolean containsChar(@NotNull final String value, final char ch) {
1722     return value.indexOf(ch) >= 0;
1723   }
1724
1725   /**
1726    * @deprecated use #capitalize(String)
1727    */
1728   @Contract(value = "null -> null; !null -> !null", pure = true)
1729   public static String firstLetterToUpperCase(@Nullable final String displayString) {
1730     if (displayString == null || displayString.isEmpty()) return displayString;
1731     char firstChar = displayString.charAt(0);
1732     char uppedFirstChar = toUpperCase(firstChar);
1733
1734     if (uppedFirstChar == firstChar) return displayString;
1735
1736     char[] buffer = displayString.toCharArray();
1737     buffer[0] = uppedFirstChar;
1738     return StringFactory.createShared(buffer);
1739   }
1740
1741   /**
1742    * Strip out all characters not accepted by given filter
1743    *
1744    * @param s      e.g. "/n    my string "
1745    * @param filter e.g. {@link CharFilter#NOT_WHITESPACE_FILTER}
1746    * @return stripped string e.g. "mystring"
1747    */
1748   @NotNull
1749   @Contract(pure = true)
1750   public static String strip(@NotNull final String s, @NotNull final CharFilter filter) {
1751     final StringBuilder result = new StringBuilder(s.length());
1752     for (int i = 0; i < s.length(); i++) {
1753       char ch = s.charAt(i);
1754       if (filter.accept(ch)) {
1755         result.append(ch);
1756       }
1757     }
1758     return result.toString();
1759   }
1760
1761   @NotNull
1762   @Contract(pure = true)
1763   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern) {
1764     return findMatches(s, pattern, 1);
1765   }
1766
1767   @NotNull
1768   @Contract(pure = true)
1769   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern, int groupIndex) {
1770     List<String> result = new SmartList<String>();
1771     Matcher m = pattern.matcher(s);
1772     while (m.find()) {
1773       String group = m.group(groupIndex);
1774       if (group != null) {
1775         result.add(group);
1776       }
1777     }
1778     return result;
1779   }
1780
1781   /**
1782    * Find position of the first character accepted by given filter.
1783    *
1784    * @param s      the string to search
1785    * @param filter search filter
1786    * @return position of the first character accepted or -1 if not found
1787    */
1788   @Contract(pure = true)
1789   public static int findFirst(@NotNull final CharSequence s, @NotNull CharFilter filter) {
1790     for (int i = 0; i < s.length(); i++) {
1791       char ch = s.charAt(i);
1792       if (filter.accept(ch)) {
1793         return i;
1794       }
1795     }
1796     return -1;
1797   }
1798
1799   @NotNull
1800   @Contract(pure = true)
1801   public static String replaceSubstring(@NotNull String string, @NotNull TextRange range, @NotNull String replacement) {
1802     return range.replace(string, replacement);
1803   }
1804
1805   @Contract(pure = true)
1806   public static boolean startsWithWhitespace(@NotNull String text) {
1807     return !text.isEmpty() && Character.isWhitespace(text.charAt(0));
1808   }
1809
1810   @Contract(pure = true)
1811   public static boolean isChar(CharSequence seq, int index, char c) {
1812     return index >= 0 && index < seq.length() && seq.charAt(index) == c;
1813   }
1814
1815   @Contract(pure = true)
1816   public static boolean startsWith(@NotNull CharSequence text, @NotNull CharSequence prefix) {
1817     int l1 = text.length();
1818     int l2 = prefix.length();
1819     if (l1 < l2) return false;
1820
1821     for (int i = 0; i < l2; i++) {
1822       if (text.charAt(i) != prefix.charAt(i)) return false;
1823     }
1824
1825     return true;
1826   }
1827
1828   @Contract(pure = true)
1829   public static boolean startsWith(@NotNull CharSequence text, int startIndex, @NotNull CharSequence prefix) {
1830     int l1 = text.length() - startIndex;
1831     int l2 = prefix.length();
1832     if (l1 < l2) return false;
1833
1834     for (int i = 0; i < l2; i++) {
1835       if (text.charAt(i + startIndex) != prefix.charAt(i)) return false;
1836     }
1837
1838     return true;
1839   }
1840
1841   @Contract(pure = true)
1842   public static boolean endsWith(@NotNull CharSequence text, @NotNull CharSequence suffix) {
1843     int l1 = text.length();
1844     int l2 = suffix.length();
1845     if (l1 < l2) return false;
1846
1847     for (int i = l1 - 1; i >= l1 - l2; i--) {
1848       if (text.charAt(i) != suffix.charAt(i + l2 - l1)) return false;
1849     }
1850
1851     return true;
1852   }
1853
1854   @NotNull
1855   @Contract(pure = true)
1856   public static String commonPrefix(@NotNull String s1, @NotNull String s2) {
1857     return s1.substring(0, commonPrefixLength(s1, s2));
1858   }
1859
1860   @Contract(pure = true)
1861   public static int commonPrefixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1862     int i;
1863     int minLength = min(s1.length(), s2.length());
1864     for (i = 0; i < minLength; i++) {
1865       if (s1.charAt(i) != s2.charAt(i)) {
1866         break;
1867       }
1868     }
1869     return i;
1870   }
1871
1872   @NotNull
1873   @Contract(pure = true)
1874   public static String commonSuffix(@NotNull String s1, @NotNull String s2) {
1875     return s1.substring(s1.length() - commonSuffixLength(s1, s2));
1876   }
1877
1878   @Contract(pure = true)
1879   public static int commonSuffixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1880     int s1Length = s1.length();
1881     int s2Length = s2.length();
1882     if (s1Length == 0 || s2Length == 0) return 0;
1883     int i;
1884     for (i = 0; i < s1Length && i < s2Length; i++) {
1885       if (s1.charAt(s1Length - i - 1) != s2.charAt(s2Length - i - 1)) {
1886         break;
1887       }
1888     }
1889     return i;
1890   }
1891
1892   /**
1893    * Allows to answer if target symbol is contained at given char sequence at <code>[start; end)</code> interval.
1894    *
1895    * @param s     target char sequence to check
1896    * @param start start offset to use within the given char sequence (inclusive)
1897    * @param end   end offset to use within the given char sequence (exclusive)
1898    * @param c     target symbol to check
1899    * @return <code>true</code> if given symbol is contained at the target range of the given char sequence;
1900    * <code>false</code> otherwise
1901    */
1902   @Contract(pure = true)
1903   public static boolean contains(@NotNull CharSequence s, int start, int end, char c) {
1904     return indexOf(s, c, start, end) >= 0;
1905   }
1906
1907   @Contract(pure = true)
1908   public static boolean containsWhitespaces(@Nullable CharSequence s) {
1909     if (s == null) return false;
1910
1911     for (int i = 0; i < s.length(); i++) {
1912       if (Character.isWhitespace(s.charAt(i))) return true;
1913     }
1914     return false;
1915   }
1916
1917   @Contract(pure = true)
1918   public static int indexOf(@NotNull CharSequence s, char c) {
1919     return indexOf(s, c, 0, s.length());
1920   }
1921
1922   @Contract(pure = true)
1923   public static int indexOf(@NotNull CharSequence s, char c, int start) {
1924     return indexOf(s, c, start, s.length());
1925   }
1926
1927   @Contract(pure = true)
1928   public static int indexOf(@NotNull CharSequence s, char c, int start, int end) {
1929     end = min(end, s.length());
1930     for (int i = max(start, 0); i < end; i++) {
1931       if (s.charAt(i) == c) return i;
1932     }
1933     return -1;
1934   }
1935
1936   @Contract(pure = true)
1937   public static boolean contains(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1938     return indexOf(sequence, infix) >= 0;
1939   }
1940
1941   @Contract(pure = true)
1942   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1943     return indexOf(sequence, infix, 0);
1944   }
1945
1946   @Contract(pure = true)
1947   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix, int start) {
1948     for (int i = start; i <= sequence.length() - infix.length(); i++) {
1949       if (startsWith(sequence, i, infix)) {
1950         return i;
1951       }
1952     }
1953     return -1;
1954   }
1955
1956   @Contract(pure = true)
1957   public static int indexOf(@NotNull CharSequence s, char c, int start, int end, boolean caseSensitive) {
1958     end = min(end, s.length());
1959     for (int i = max(start, 0); i < end; i++) {
1960       if (charsMatch(s.charAt(i), c, !caseSensitive)) return i;
1961     }
1962     return -1;
1963   }
1964
1965   @Contract(pure = true)
1966   public static int indexOf(@NotNull char[] s, char c, int start, int end, boolean caseSensitive) {
1967     end = min(end, s.length);
1968     for (int i = max(start, 0); i < end; i++) {
1969       if (charsMatch(s[i], c, !caseSensitive)) return i;
1970     }
1971     return -1;
1972   }
1973
1974   @Contract(pure = true)
1975   public static int indexOfSubstringEnd(@NotNull String text, @NotNull String subString) {
1976     int i = text.indexOf(subString);
1977     if (i == -1) return -1;
1978     return i + subString.length();
1979   }
1980
1981   @Contract(pure = true)
1982   public static int indexOfAny(@NotNull final String s, @NotNull final String chars) {
1983     return indexOfAny(s, chars, 0, s.length());
1984   }
1985
1986   @Contract(pure = true)
1987   public static int indexOfAny(@NotNull final CharSequence s, @NotNull final String chars) {
1988     return indexOfAny(s, chars, 0, s.length());
1989   }
1990
1991   @Contract(pure = true)
1992   public static int indexOfAny(@NotNull final String s, @NotNull final String chars, final int start, final int end) {
1993     return indexOfAny((CharSequence)s, chars, start, end);
1994   }
1995
1996   @Contract(pure = true)
1997   public static int indexOfAny(@NotNull final CharSequence s, @NotNull final String chars, final int start, int end) {
1998     end = min(end, s.length());
1999     for (int i = max(start, 0); i < end; i++) {
2000       if (containsChar(chars, s.charAt(i))) return i;
2001     }
2002     return -1;
2003   }
2004
2005   @Contract(pure = true)
2006   public static int lastIndexOfAny(@NotNull CharSequence s, @NotNull final String chars) {
2007     for (int i = s.length() - 1; i >= 0; i--) {
2008       if (containsChar(chars, s.charAt(i))) return i;
2009     }
2010     return -1;
2011   }
2012
2013   @Nullable
2014   @Contract(pure = true)
2015   public static String substringBefore(@NotNull String text, @NotNull String subString) {
2016     int i = text.indexOf(subString);
2017     if (i == -1) return null;
2018     return text.substring(0, i);
2019   }
2020
2021   @Nullable
2022   @Contract(pure = true)
2023   public static String substringAfter(@NotNull String text, @NotNull String subString) {
2024     int i = text.indexOf(subString);
2025     if (i == -1) return null;
2026     return text.substring(i + subString.length());
2027   }
2028
2029   /**
2030    * Allows to retrieve index of last occurrence of the given symbols at <code>[start; end)</code> sub-sequence of the given text.
2031    *
2032    * @param s     target text
2033    * @param c     target symbol which last occurrence we want to check
2034    * @param start start offset of the target text (inclusive)
2035    * @param end   end offset of the target text (exclusive)
2036    * @return index of the last occurrence of the given symbol at the target sub-sequence of the given text if any;
2037    * <code>-1</code> otherwise
2038    */
2039   @Contract(pure = true)
2040   public static int lastIndexOf(@NotNull CharSequence s, char c, int start, int end) {
2041     return StringUtilRt.lastIndexOf(s, c, start, end);
2042   }
2043
2044   @NotNull
2045   @Contract(pure = true)
2046   public static String first(@NotNull String text, final int maxLength, final boolean appendEllipsis) {
2047     return text.length() > maxLength ? text.substring(0, maxLength) + (appendEllipsis ? "..." : "") : text;
2048   }
2049
2050   @NotNull
2051   @Contract(pure = true)
2052   public static CharSequence first(@NotNull CharSequence text, final int length, final boolean appendEllipsis) {
2053     return text.length() > length ? text.subSequence(0, length) + (appendEllipsis ? "..." : "") : text;
2054   }
2055
2056   @NotNull
2057   @Contract(pure = true)
2058   public static CharSequence last(@NotNull CharSequence text, final int length, boolean prependEllipsis) {
2059     return text.length() > length ? (prependEllipsis ? "..." : "") + text.subSequence(text.length() - length, text.length()) : text;
2060   }
2061
2062   @NotNull
2063   @Contract(pure = true)
2064   public static String firstLast(@NotNull String text, int length) {
2065     return text.length() > length
2066            ? text.subSequence(0, length / 2) + "..." + text.subSequence(text.length() - length / 2, text.length())
2067            : text;
2068   }
2069
2070   @NotNull
2071   @Contract(pure = true)
2072   public static String escapeChar(@NotNull final String str, final char character) {
2073     return escapeChars(str, character);
2074   }
2075
2076   @NotNull
2077   @Contract(pure = true)
2078   public static String escapeChars(@NotNull final String str, final char... character) {
2079     final StringBuilder buf = new StringBuilder(str);
2080     for (char c : character) {
2081       escapeChar(buf, c);
2082     }
2083     return buf.toString();
2084   }
2085
2086   private static void escapeChar(@NotNull final StringBuilder buf, final char character) {
2087     int idx = 0;
2088     while ((idx = indexOf(buf, character, idx)) >= 0) {
2089       buf.insert(idx, "\\");
2090       idx += 2;
2091     }
2092   }
2093
2094   @NotNull
2095   @Contract(pure = true)
2096   public static String escapeQuotes(@NotNull final String str) {
2097     return escapeChar(str, '"');
2098   }
2099
2100   public static void escapeQuotes(@NotNull final StringBuilder buf) {
2101     escapeChar(buf, '"');
2102   }
2103
2104   @NotNull
2105   @Contract(pure = true)
2106   public static String escapeSlashes(@NotNull final String str) {
2107     return escapeChar(str, '/');
2108   }
2109
2110   @NotNull
2111   @Contract(pure = true)
2112   public static String escapeBackSlashes(@NotNull final String str) {
2113     return escapeChar(str, '\\');
2114   }
2115
2116   public static void escapeSlashes(@NotNull final StringBuilder buf) {
2117     escapeChar(buf, '/');
2118   }
2119
2120   @NotNull
2121   @Contract(pure = true)
2122   public static String unescapeSlashes(@NotNull final String str) {
2123     final StringBuilder buf = new StringBuilder(str.length());
2124     unescapeChar(buf, str, '/');
2125     return buf.toString();
2126   }
2127
2128   @NotNull
2129   @Contract(pure = true)
2130   public static String unescapeBackSlashes(@NotNull final String str) {
2131     final StringBuilder buf = new StringBuilder(str.length());
2132     unescapeChar(buf, str, '\\');
2133     return buf.toString();
2134   }
2135
2136   @NotNull
2137   @Contract(pure = true)
2138   public static String unescapeChar(@NotNull final String str, char unescapeChar) {
2139     final StringBuilder buf = new StringBuilder(str.length());
2140     unescapeChar(buf, str, unescapeChar);
2141     return buf.toString();
2142   }
2143
2144   private static void unescapeChar(@NotNull StringBuilder buf, @NotNull String str, char unescapeChar) {
2145     final int length = str.length();
2146     final int last = length - 1;
2147     for (int i = 0; i < length; i++) {
2148       char ch = str.charAt(i);
2149       if (ch == '\\' && i != last) {
2150         i++;
2151         ch = str.charAt(i);
2152         if (ch != unescapeChar) buf.append('\\');
2153       }
2154
2155       buf.append(ch);
2156     }
2157   }
2158
2159   public static void quote(@NotNull final StringBuilder builder) {
2160     quote(builder, '\"');
2161   }
2162
2163   public static void quote(@NotNull final StringBuilder builder, final char quotingChar) {
2164     builder.insert(0, quotingChar);
2165     builder.append(quotingChar);
2166   }
2167
2168   @NotNull
2169   @Contract(pure = true)
2170   public static String wrapWithDoubleQuote(@NotNull String str) {
2171     return '\"' + str + "\"";
2172   }
2173
2174   @NonNls private static final String[] REPLACES_REFS = {"&lt;", "&gt;", "&amp;", "&#39;", "&quot;"};
2175   @NonNls private static final String[] REPLACES_DISP = {"<", ">", "&", "'", "\""};
2176
2177   @Contract(value = "null -> null; !null -> !null",pure = true)
2178   public static String unescapeXml(@Nullable final String text) {
2179     if (text == null) return null;
2180     return replace(text, REPLACES_REFS, REPLACES_DISP);
2181   }
2182
2183   @Contract(value = "null -> null; !null -> !null",pure = true)
2184   public static String escapeXml(@Nullable final String text) {
2185     if (text == null) return null;
2186     return replace(text, REPLACES_DISP, REPLACES_REFS);
2187   }
2188
2189   public static String removeHtmlTags (@Nullable String htmlString) {
2190     if (isEmpty(htmlString)) return htmlString;
2191     try {
2192       html2TextParser.parse(new StringReader(htmlString));
2193     }
2194     catch (IOException e) {
2195         LOG.error(e);
2196     }
2197     return html2TextParser.getText();
2198   }
2199
2200   @NonNls private static final String[] MN_QUOTED = {"&&", "__"};
2201   @NonNls private static final String[] MN_CHARS = {"&", "_"};
2202
2203   @Contract(value = "null -> null; !null -> !null", pure = true)
2204   public static String escapeMnemonics(@Nullable String text) {
2205     if (text == null) return null;
2206     return replace(text, MN_CHARS, MN_QUOTED);
2207   }
2208
2209   @NotNull
2210   @Contract(pure = true)
2211   public static String htmlEmphasize(@NotNull String text) {
2212     return "<b><code>" + escapeXml(text) + "</code></b>";
2213   }
2214
2215
2216   @NotNull
2217   @Contract(pure = true)
2218   public static String escapeToRegexp(@NotNull String text) {
2219     final StringBuilder result = new StringBuilder(text.length());
2220     return escapeToRegexp(text, result).toString();
2221   }
2222
2223   @NotNull
2224   public static StringBuilder escapeToRegexp(@NotNull CharSequence text, @NotNull StringBuilder builder) {
2225     for (int i = 0; i < text.length(); i++) {
2226       final char c = text.charAt(i);
2227       if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') {
2228         builder.append(c);
2229       }
2230       else if (c == '\n') {
2231         builder.append("\\n");
2232       }
2233       else {
2234         builder.append('\\').append(c);
2235       }
2236     }
2237
2238     return builder;
2239   }
2240
2241   @Contract(pure = true)
2242   public static boolean isEscapedBackslash(@NotNull char[] chars, int startOffset, int backslashOffset) {
2243     if (chars[backslashOffset] != '\\') {
2244       return true;
2245     }
2246     boolean escaped = false;
2247     for (int i = startOffset; i < backslashOffset; i++) {
2248       if (chars[i] == '\\') {
2249         escaped = !escaped;
2250       }
2251       else {
2252         escaped = false;
2253       }
2254     }
2255     return escaped;
2256   }
2257
2258   @Contract(pure = true)
2259   public static boolean isEscapedBackslash(@NotNull CharSequence text, int startOffset, int backslashOffset) {
2260     if (text.charAt(backslashOffset) != '\\') {
2261       return true;
2262     }
2263     boolean escaped = false;
2264     for (int i = startOffset; i < backslashOffset; i++) {
2265       if (text.charAt(i) == '\\') {
2266         escaped = !escaped;
2267       }
2268       else {
2269         escaped = false;
2270       }
2271     }
2272     return escaped;
2273   }
2274
2275   @NotNull
2276   @Contract(pure = true)
2277   public static String replace(@NotNull String text, @NotNull String[] from, @NotNull String[] to) {
2278     final StringBuilder result = new StringBuilder(text.length());
2279     replace:
2280     for (int i = 0; i < text.length(); i++) {
2281       for (int j = 0; j < from.length; j += 1) {
2282         String toReplace = from[j];
2283         String replaceWith = to[j];
2284
2285         final int len = toReplace.length();
2286         if (text.regionMatches(i, toReplace, 0, len)) {
2287           result.append(replaceWith);
2288           i += len - 1;
2289           continue replace;
2290         }
2291       }
2292       result.append(text.charAt(i));
2293     }
2294     return result.toString();
2295   }
2296
2297   @NotNull
2298   @Contract(pure = true)
2299   public static String[] filterEmptyStrings(@NotNull String[] strings) {
2300     int emptyCount = 0;
2301     for (String string : strings) {
2302       if (string == null || string.isEmpty()) emptyCount++;
2303     }
2304     if (emptyCount == 0) return strings;
2305
2306     String[] result = ArrayUtil.newStringArray(strings.length - emptyCount);
2307     int count = 0;
2308     for (String string : strings) {
2309       if (string == null || string.isEmpty()) continue;
2310       result[count++] = string;
2311     }
2312
2313     return result;
2314   }
2315
2316   @Contract(pure = true)
2317   public static int countNewLines(@NotNull CharSequence text) {
2318     return countChars(text, '\n');
2319   }
2320
2321   @Contract(pure = true)
2322   public static int countChars(@NotNull CharSequence text, char c) {
2323     return countChars(text, c, 0, false);
2324   }
2325
2326   @Contract(pure = true)
2327   public static int countChars(@NotNull CharSequence text, char c, int offset, boolean continuous) {
2328     int count = 0;
2329     for (int i = offset; i < text.length(); ++i) {
2330       if (text.charAt(i) == c) {
2331         count++;
2332       }
2333       else if (continuous) {
2334         break;
2335       }
2336     }
2337     return count;
2338   }
2339
2340   @NotNull
2341   @Contract(pure = true)
2342   public static String capitalsOnly(@NotNull String s) {
2343     StringBuilder b = new StringBuilder();
2344     for (int i = 0; i < s.length(); i++) {
2345       if (Character.isUpperCase(s.charAt(i))) {
2346         b.append(s.charAt(i));
2347       }
2348     }
2349
2350     return b.toString();
2351   }
2352
2353   /**
2354    * @param args Strings to join.
2355    * @return {@code null} if any of given Strings is {@code null}.
2356    */
2357   @Nullable
2358   @Contract(pure = true)
2359   public static String joinOrNull(@NotNull String... args) {
2360     StringBuilder r = new StringBuilder();
2361     for (String arg : args) {
2362       if (arg == null) return null;
2363       r.append(arg);
2364     }
2365     return r.toString();
2366   }
2367
2368   @Nullable
2369   @Contract(pure = true)
2370   public static String getPropertyName(@NonNls @NotNull String methodName) {
2371     if (methodName.startsWith("get")) {
2372       return Introspector.decapitalize(methodName.substring(3));
2373     }
2374     if (methodName.startsWith("is")) {
2375       return Introspector.decapitalize(methodName.substring(2));
2376     }
2377     if (methodName.startsWith("set")) {
2378       return Introspector.decapitalize(methodName.substring(3));
2379     }
2380     return null;
2381   }
2382
2383   @Contract(pure = true)
2384   public static boolean isJavaIdentifierStart(char c) {
2385     return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierStart(c);
2386   }
2387
2388   @Contract(pure = true)
2389   public static boolean isJavaIdentifierPart(char c) {
2390     return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierPart(c);
2391   }
2392
2393   @Contract(pure = true)
2394   public static boolean isJavaIdentifier(@NotNull String text) {
2395     int len = text.length();
2396     if (len == 0) return false;
2397
2398     if (!isJavaIdentifierStart(text.charAt(0))) return false;
2399
2400     for (int i = 1; i < len; i++) {
2401       if (!isJavaIdentifierPart(text.charAt(i))) return false;
2402     }
2403
2404     return true;
2405   }
2406
2407   /**
2408    * Escape property name or key in property file. Unicode characters are escaped as well.
2409    *
2410    * @param input an input to escape
2411    * @param isKey if true, the rules for key escaping are applied. The leading space is escaped in that case.
2412    * @return an escaped string
2413    */
2414   @NotNull
2415   @Contract(pure = true)
2416   public static String escapeProperty(@NotNull String input, final boolean isKey) {
2417     final StringBuilder escaped = new StringBuilder(input.length());
2418     for (int i = 0; i < input.length(); i++) {
2419       final char ch = input.charAt(i);
2420       switch (ch) {
2421         case ' ':
2422           if (isKey && i == 0) {
2423             // only the leading space has to be escaped
2424             escaped.append('\\');
2425           }
2426           escaped.append(' ');
2427           break;
2428         case '\t':
2429           escaped.append("\\t");
2430           break;
2431         case '\r':
2432           escaped.append("\\r");
2433           break;
2434         case '\n':
2435           escaped.append("\\n");
2436           break;
2437         case '\f':
2438           escaped.append("\\f");
2439           break;
2440         case '\\':
2441         case '#':
2442         case '!':
2443         case ':':
2444         case '=':
2445           escaped.append('\\');
2446           escaped.append(ch);
2447           break;
2448         default:
2449           if (20 < ch && ch < 0x7F) {
2450             escaped.append(ch);
2451           }
2452           else {
2453             escaped.append("\\u");
2454             escaped.append(Character.forDigit((ch >> 12) & 0xF, 16));
2455             escaped.append(Character.forDigit((ch >> 8) & 0xF, 16));
2456             escaped.append(Character.forDigit((ch >> 4) & 0xF, 16));
2457             escaped.append(Character.forDigit((ch) & 0xF, 16));
2458           }
2459           break;
2460       }
2461     }
2462     return escaped.toString();
2463   }
2464
2465   @Contract(pure = true)
2466   public static String getQualifiedName(@Nullable String packageName, String className) {
2467     if (packageName == null || packageName.isEmpty()) {
2468       return className;
2469     }
2470     return packageName + '.' + className;
2471   }
2472
2473   @Contract(pure = true)
2474   public static int compareVersionNumbers(@Nullable String v1, @Nullable String v2) {
2475     // todo duplicates com.intellij.util.text.VersionComparatorUtil.compare
2476     // todo please refactor next time you make changes here
2477     if (v1 == null && v2 == null) {
2478       return 0;
2479     }
2480     if (v1 == null) {
2481       return -1;
2482     }
2483     if (v2 == null) {
2484       return 1;
2485     }
2486
2487     String[] part1 = v1.split("[\\.\\_\\-]");
2488     String[] part2 = v2.split("[\\.\\_\\-]");
2489
2490     int idx = 0;
2491     for (; idx < part1.length && idx < part2.length; idx++) {
2492       String p1 = part1[idx];
2493       String p2 = part2[idx];
2494
2495       int cmp;
2496       if (p1.matches("\\d+") && p2.matches("\\d+")) {
2497         cmp = new Integer(p1).compareTo(new Integer(p2));
2498       }
2499       else {
2500         cmp = part1[idx].compareTo(part2[idx]);
2501       }
2502       if (cmp != 0) return cmp;
2503     }
2504
2505     if (part1.length == part2.length) {
2506       return 0;
2507     }
2508     else {
2509       boolean left = part1.length > idx;
2510       String[] parts = left ? part1 : part2;
2511
2512       for (; idx < parts.length; idx++) {
2513         String p = parts[idx];
2514         int cmp;
2515         if (p.matches("\\d+")) {
2516           cmp = new Integer(p).compareTo(0);
2517         }
2518         else {
2519           cmp = 1;
2520         }
2521         if (cmp != 0) return left ? cmp : -cmp;
2522       }
2523       return 0;
2524     }
2525   }
2526
2527   @Contract(pure = true)
2528   public static int getOccurrenceCount(@NotNull String text, final char c) {
2529     int res = 0;
2530     int i = 0;
2531     while (i < text.length()) {
2532       i = text.indexOf(c, i);
2533       if (i >= 0) {
2534         res++;
2535         i++;
2536       }
2537       else {
2538         break;
2539       }
2540     }
2541     return res;
2542   }
2543
2544   @Contract(pure = true)
2545   public static int getOccurrenceCount(@NotNull String text, @NotNull String s) {
2546     int res = 0;
2547     int i = 0;
2548     while (i < text.length()) {
2549       i = text.indexOf(s, i);
2550       if (i >= 0) {
2551         res++;
2552         i++;
2553       }
2554       else {
2555         break;
2556       }
2557     }
2558     return res;
2559   }
2560
2561   @NotNull
2562   @Contract(pure = true)
2563   public static String fixVariableNameDerivedFromPropertyName(@NotNull String name) {
2564     if (isEmptyOrSpaces(name)) return name;
2565     char c = name.charAt(0);
2566     if (isVowel(c)) {
2567       return "an" + Character.toUpperCase(c) + name.substring(1);
2568     }
2569     return "a" + Character.toUpperCase(c) + name.substring(1);
2570   }
2571
2572   @NotNull
2573   @Contract(pure = true)
2574   public static String sanitizeJavaIdentifier(@NotNull String name) {
2575     final StringBuilder result = new StringBuilder(name.length());
2576
2577     for (int i = 0; i < name.length(); i++) {
2578       final char ch = name.charAt(i);
2579       if (Character.isJavaIdentifierPart(ch)) {
2580         if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
2581           result.append("_");
2582         }
2583         result.append(ch);
2584       }
2585     }
2586
2587     return result.toString();
2588   }
2589
2590   public static void assertValidSeparators(@NotNull CharSequence s) {
2591     char[] chars = CharArrayUtil.fromSequenceWithoutCopying(s);
2592     int slashRIndex = -1;
2593
2594     if (chars != null) {
2595       for (int i = 0, len = s.length(); i < len; ++i) {
2596         if (chars[i] == '\r') {
2597           slashRIndex = i;
2598           break;
2599         }
2600       }
2601     }
2602     else {
2603       for (int i = 0, len = s.length(); i < len; i++) {
2604         if (s.charAt(i) == '\r') {
2605           slashRIndex = i;
2606           break;
2607         }
2608       }
2609     }
2610
2611     if (slashRIndex != -1) {
2612       String context =
2613         String.valueOf(last(s.subSequence(0, slashRIndex), 10, true)) + first(s.subSequence(slashRIndex, s.length()), 10, true);
2614       context = escapeStringCharacters(context);
2615       LOG.error("Wrong line separators: '" + context + "' at offset " + slashRIndex);
2616     }
2617   }
2618
2619   @NotNull
2620   @Contract(pure = true)
2621   public static String tail(@NotNull String s, final int idx) {
2622     return idx >= s.length() ? "" : s.substring(idx, s.length());
2623   }
2624
2625   /**
2626    * Splits string by lines.
2627    *
2628    * @param string String to split
2629    * @return array of strings
2630    */
2631   @NotNull
2632   @Contract(pure = true)
2633   public static String[] splitByLines(@NotNull String string) {
2634     return splitByLines(string, true);
2635   }
2636
2637   /**
2638    * Splits string by lines. If several line separators are in a row corresponding empty lines
2639    * are also added to result if {@code excludeEmptyStrings} is {@code false}.
2640    *
2641    * @param string String to split
2642    * @return array of strings
2643    */
2644   @NotNull
2645   @Contract(pure = true)
2646   public static String[] splitByLines(@NotNull String string, boolean excludeEmptyStrings) {
2647     return (excludeEmptyStrings ? EOL_SPLIT_PATTERN : EOL_SPLIT_PATTERN_WITH_EMPTY).split(string);
2648   }
2649
2650   @NotNull
2651   @Contract(pure = true)
2652   public static String[] splitByLinesDontTrim(@NotNull String string) {
2653     return EOL_SPLIT_DONT_TRIM_PATTERN.split(string);
2654   }
2655
2656   /**
2657    * Splits string by lines, keeping all line separators at the line ends and in the empty lines.
2658    * <br> E.g. splitting text
2659    * <blockquote>
2660    *   foo\r\n<br>
2661    *   \n<br>
2662    *   bar\n<br>
2663    *   \r\n<br>
2664    *   baz\r<br>
2665    *   \r<br>
2666    * </blockquote>
2667    * will return the following array: foo\r\n, \n, bar\n, \r\n, baz\r, \r
2668    *
2669    */
2670   @NotNull
2671   @Contract(pure = true)
2672   public static String[] splitByLinesKeepSeparators(@NotNull String string) {
2673     return EOL_SPLIT_KEEP_SEPARATORS.split(string);
2674   }
2675
2676   @NotNull
2677   @Contract(pure = true)
2678   public static List<Pair<String, Integer>> getWordsWithOffset(@NotNull String s) {
2679     List<Pair<String, Integer>> res = ContainerUtil.newArrayList();
2680     s += " ";
2681     StringBuilder name = new StringBuilder();
2682     int startInd = -1;
2683     for (int i = 0; i < s.length(); i++) {
2684       if (Character.isWhitespace(s.charAt(i))) {
2685         if (name.length() > 0) {
2686           res.add(Pair.create(name.toString(), startInd));
2687           name.setLength(0);
2688           startInd = -1;
2689         }
2690       }
2691       else {
2692         if (startInd == -1) {
2693           startInd = i;
2694         }
2695         name.append(s.charAt(i));
2696       }
2697     }
2698     return res;
2699   }
2700
2701   /**
2702    * Implementation of <a href="http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html"/>
2703    * "Sorting for Humans: Natural Sort Order"</a>
2704    */
2705   @Contract(pure = true)
2706   public static int naturalCompare(@Nullable String string1, @Nullable String string2) {
2707     return naturalCompare(string1, string2, false);
2708   }
2709
2710   @Contract(pure = true)
2711   private static int naturalCompare(@Nullable String string1, @Nullable String string2, boolean caseSensitive) {
2712     //noinspection StringEquality
2713     if (string1 == string2) {
2714       return 0;
2715     }
2716     if (string1 == null) {
2717       return -1;
2718     }
2719     if (string2 == null) {
2720       return 1;
2721     }
2722
2723     final int string1Length = string1.length();
2724     final int string2Length = string2.length();
2725     int i = 0;
2726     int j = 0;
2727     for (; i < string1Length && j < string2Length; i++, j++) {
2728       char ch1 = string1.charAt(i);
2729       char ch2 = string2.charAt(j);
2730       if ((isDecimalDigit(ch1) || ch1 == ' ') && (isDecimalDigit(ch2) || ch2 == ' ')) {
2731         int startNum1 = i;
2732         while (ch1 == ' ' || ch1 == '0') { // skip leading spaces and zeros
2733           startNum1++;
2734           if (startNum1 >= string1Length) break;
2735           ch1 = string1.charAt(startNum1);
2736         }
2737         int startNum2 = j;
2738         while (ch2 == ' ' || ch2 == '0') { // skip leading spaces and zeros
2739           startNum2++;
2740           if (startNum2 >= string2Length) break;
2741           ch2 = string2.charAt(startNum2);
2742         }
2743         i = startNum1;
2744         j = startNum2;
2745         // find end index of number
2746         while (i < string1Length && isDecimalDigit(string1.charAt(i))) i++;
2747         while (j < string2Length && isDecimalDigit(string2.charAt(j))) j++;
2748         final int lengthDiff = (i - startNum1) - (j - startNum2);
2749         if (lengthDiff != 0) {
2750           // numbers with more digits are always greater than shorter numbers
2751           return lengthDiff;
2752         }
2753         for (; startNum1 < i; startNum1++, startNum2++) {
2754           // compare numbers with equal digit count
2755           final int diff = string1.charAt(startNum1) - string2.charAt(startNum2);
2756           if (diff != 0) {
2757             return diff;
2758           }
2759         }
2760         i--;
2761         j--;
2762       }
2763       else {
2764         if (caseSensitive) {
2765           return ch1 - ch2;
2766         }
2767         else {
2768           // similar logic to charsMatch() below
2769           if (ch1 != ch2) {
2770             final int diff1 = StringUtilRt.toUpperCase(ch1) - StringUtilRt.toUpperCase(ch2);
2771             if (diff1 != 0) {
2772               final int diff2 = StringUtilRt.toLowerCase(ch1) - StringUtilRt.toLowerCase(ch2);
2773               if (diff2 != 0) {
2774                 return diff2;
2775               }
2776             }
2777           }
2778         }
2779       }
2780     }
2781     // After the loop the end of one of the strings might not have been reached, if the other
2782     // string ends with a number and the strings are equal until the end of that number. When
2783     // there are more characters in the string, then it is greater.
2784     if (i < string1Length) {
2785       return 1;
2786     }
2787     if (j < string2Length) {
2788       return -1;
2789     }
2790     if (!caseSensitive && string1Length == string2Length) {
2791       // do case sensitive compare if case insensitive strings are equal
2792       return naturalCompare(string1, string2, true);
2793     }
2794     return string1Length - string2Length;
2795   }
2796
2797   @Contract(pure = true)
2798   public static boolean isDecimalDigit(char c) {
2799     return c >= '0' && c <= '9';
2800   }
2801
2802   @Contract(pure = true)
2803   public static int compare(@Nullable String s1, @Nullable String s2, boolean ignoreCase) {
2804     //noinspection StringEquality
2805     if (s1 == s2) return 0;
2806     if (s1 == null) return -1;
2807     if (s2 == null) return 1;
2808     return ignoreCase ? s1.compareToIgnoreCase(s2) : s1.compareTo(s2);
2809   }
2810
2811   @Contract(pure = true)
2812   public static int comparePairs(@Nullable String s1, @Nullable String t1, @Nullable String s2, @Nullable String t2, boolean ignoreCase) {
2813     final int compare = compare(s1, s2, ignoreCase);
2814     return compare != 0 ? compare : compare(t1, t2, ignoreCase);
2815   }
2816
2817   @Contract(pure = true)
2818   public static int hashCode(@NotNull CharSequence s) {
2819     return stringHashCode(s);
2820   }
2821
2822   @Contract(pure = true)
2823   public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2824     if (s1 == null ^ s2 == null) {
2825       return false;
2826     }
2827
2828     if (s1 == null) {
2829       return true;
2830     }
2831
2832     if (s1.length() != s2.length()) {
2833       return false;
2834     }
2835     for (int i = 0; i < s1.length(); i++) {
2836       if (s1.charAt(i) != s2.charAt(i)) {
2837         return false;
2838       }
2839     }
2840     return true;
2841   }
2842
2843   @Contract(pure = true)
2844   public static boolean equalsIgnoreCase(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2845     if (s1 == null ^ s2 == null) {
2846       return false;
2847     }
2848
2849     if (s1 == null) {
2850       return true;
2851     }
2852
2853     if (s1.length() != s2.length()) {
2854       return false;
2855     }
2856     for (int i = 0; i < s1.length(); i++) {
2857       if (!charsMatch(s1.charAt(i), s2.charAt(i), true)) {
2858         return false;
2859       }
2860     }
2861     return true;
2862   }
2863
2864   @Contract(pure = true)
2865   public static boolean equalsIgnoreWhitespaces(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2866     if (s1 == null ^ s2 == null) {
2867       return false;
2868     }
2869
2870     if (s1 == null) {
2871       return true;
2872     }
2873
2874     int len1 = s1.length();
2875     int len2 = s2.length();
2876
2877     int index1 = 0;
2878     int index2 = 0;
2879     while (index1 < len1 && index2 < len2) {
2880       if (s1.charAt(index1) == s2.charAt(index2)) {
2881         index1++;
2882         index2++;
2883         continue;
2884       }
2885
2886       boolean skipped = false;
2887       while (index1 != len1 && isWhiteSpace(s1.charAt(index1))) {
2888         skipped = true;
2889         index1++;
2890       }
2891       while (index2 != len2 && isWhiteSpace(s2.charAt(index2))) {
2892         skipped = true;
2893         index2++;
2894       }
2895
2896       if (!skipped) return false;
2897     }
2898
2899     for (; index1 != len1; index1++) {
2900       if (!isWhiteSpace(s1.charAt(index1))) return false;
2901     }
2902     for (; index2 != len2; index2++) {
2903       if (!isWhiteSpace(s2.charAt(index2))) return false;
2904     }
2905
2906     return true;
2907   }
2908
2909   @Contract(pure = true)
2910   public static boolean equalsTrimWhitespaces(@NotNull CharSequence s1, @NotNull CharSequence s2) {
2911     int start1 = 0;
2912     int end1 = s1.length();
2913     int start2 = 0;
2914     int end2 = s2.length();
2915
2916     while (start1 < end1) {
2917       char c = s1.charAt(start1);
2918       if (!isWhiteSpace(c)) break;
2919       start1++;
2920     }
2921
2922     while (start1 < end1) {
2923       char c = s1.charAt(end1 - 1);
2924       if (!isWhiteSpace(c)) break;
2925       end1--;
2926     }
2927
2928     while (start2 < end2) {
2929       char c = s2.charAt(start2);
2930       if (!isWhiteSpace(c)) break;
2931       start2++;
2932     }
2933
2934     while (start2 < end2) {
2935       char c = s2.charAt(end2 - 1);
2936       if (!isWhiteSpace(c)) break;
2937       end2--;
2938     }
2939
2940     CharSequence ts1 = new CharSequenceSubSequence(s1, start1, end1);
2941     CharSequence ts2 = new CharSequenceSubSequence(s2, start2, end2);
2942
2943     return equals(ts1, ts2);
2944   }
2945
2946   @Contract(pure = true)
2947   public static int compare(char c1, char c2, boolean ignoreCase) {
2948     // duplicating String.equalsIgnoreCase logic
2949     int d = c1 - c2;
2950     if (d == 0 || !ignoreCase) {
2951       return d;
2952     }
2953     // If characters don't match but case may be ignored,
2954     // try converting both characters to uppercase.
2955     // If the results match, then the comparison scan should
2956     // continue.
2957     char u1 = StringUtilRt.toUpperCase(c1);
2958     char u2 = StringUtilRt.toUpperCase(c2);
2959     d = u1 - u2;
2960     if (d != 0) {
2961       // Unfortunately, conversion to uppercase does not work properly
2962       // for the Georgian alphabet, which has strange rules about case
2963       // conversion.  So we need to make one last check before
2964       // exiting.
2965       d = StringUtilRt.toLowerCase(u1) - StringUtilRt.toLowerCase(u2);
2966     }
2967     return d;
2968   }
2969
2970   @Contract(pure = true)
2971   public static boolean charsMatch(char c1, char c2, boolean ignoreCase) {
2972     return compare(c1, c2, ignoreCase) == 0;
2973   }
2974
2975   @NotNull
2976   @Contract(pure = true)
2977   public static String formatLinks(@NotNull String message) {
2978     Pattern linkPattern = Pattern.compile("http://[a-zA-Z0-9\\./\\-\\+]+");
2979     StringBuffer result = new StringBuffer();
2980     Matcher m = linkPattern.matcher(message);
2981     while (m.find()) {
2982       m.appendReplacement(result, "<a href=\"" + m.group() + "\">" + m.group() + "</a>");
2983     }
2984     m.appendTail(result);
2985     return result.toString();
2986   }
2987
2988   @Contract(pure = true)
2989   public static boolean isHexDigit(char c) {
2990     return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F';
2991   }
2992
2993   @Contract(pure = true)
2994   public static boolean isOctalDigit(char c) {
2995     return '0' <= c && c <= '7';
2996   }
2997
2998   @NotNull
2999   @Contract(pure = true)
3000   public static String shortenTextWithEllipsis(@NotNull final String text, final int maxLength, final int suffixLength) {
3001     return shortenTextWithEllipsis(text, maxLength, suffixLength, false);
3002   }
3003
3004   @NotNull