Merge commit 'edcd53bbbba9f94e21f43fd03d3a2febcc2b1564'
[fanfix.git] / src / jexer / bits / StringUtils.java
index a98756ed22950a5f3eb359f36f24438448078d0e..fffce206875cf663480d2041aac121f88b58d01a 100644 (file)
@@ -80,14 +80,14 @@ public class StringUtils {
                         // We have just transitioned from a word to
                         // whitespace.  See if we have enough space to add
                         // the word to the line.
-                        if (word.length() + line.length() > n) {
+                        if (width(word.toString()) + width(line.toString()) > n) {
                             // This word will exceed the line length.  Wrap
                             // at it instead.
                             result.add(line.toString());
                             line = new StringBuilder();
                         }
                         if ((word.toString().startsWith(" "))
-                            && (line.length() == 0)
+                            && (width(line.toString()) == 0)
                         ) {
                             line.append(word.substring(1));
                         } else {
@@ -112,14 +112,14 @@ public class StringUtils {
                 }
             } // for (int j = 0; j < rawLines[i].length(); j++)
 
-            if (word.length() + line.length() > n) {
+            if (width(word.toString()) + width(line.toString()) > n) {
                 // This word will exceed the line length.  Wrap at it
                 // instead.
                 result.add(line.toString());
                 line = new StringBuilder();
             }
             if ((word.toString().startsWith(" "))
-                && (line.length() == 0)
+                && (width(line.toString()) == 0)
             ) {
                 line.append(word.substring(1));
             } else {
@@ -148,7 +148,7 @@ public class StringUtils {
         List<String> lines = left(str, n);
         for (String line: lines) {
             StringBuilder sb = new StringBuilder();
-            for (int i = 0; i < n - line.length(); i++) {
+            for (int i = 0; i < n - width(line); i++) {
                 sb.append(' ');
             }
             sb.append(line);
@@ -175,8 +175,8 @@ public class StringUtils {
         List<String> lines = left(str, n);
         for (String line: lines) {
             StringBuilder sb = new StringBuilder();
-            int l = (n - line.length()) / 2;
-            int r = n - line.length() - l;
+            int l = (n - width(line)) / 2;
+            int r = n - width(line) - l;
             for (int i = 0; i < l; i++) {
                 sb.append(' ');
             }
@@ -404,4 +404,95 @@ public class StringUtils {
         return result.toString();
     }
 
+    /**
+     * Determine display width of a Unicode code point.
+     *
+     * @param ch the code point, can be char
+     * @return the number of text cell columns required to display this code
+     * point, one of 0, 1, or 2
+     */
+    public static int width(final int ch) {
+        /*
+         * This routine is a modified version of mk_wcwidth() available
+         * at: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+         *
+         * The combining characters list has been omitted from this
+         * implementation.  Hopefully no users will be impacted.
+         */
+
+        // 8-bit control characters: width 0
+        if (ch == 0) {
+            return 0;
+        }
+        if ((ch < 32) || ((ch >= 0x7f) && (ch < 0xa0))) {
+            return 0;
+        }
+
+        // All others: either 1 or 2
+        if ((ch >= 0x1100)
+            && ((ch <= 0x115f)
+                // Hangul Jamo init. consonants
+                || (ch == 0x2329)
+                || (ch == 0x232a)
+                // CJK ... Yi
+                || ((ch >= 0x2e80) && (ch <= 0xa4cf) && (ch != 0x303f))
+                // Hangul Syllables
+                || ((ch >= 0xac00) && (ch <= 0xd7a3))
+                // CJK Compatibility Ideographs
+                || ((ch >= 0xf900) && (ch <= 0xfaff))
+                // Vertical forms
+                || ((ch >= 0xfe10) && (ch <= 0xfe19))
+                // CJK Compatibility Forms
+                || ((ch >= 0xfe30) && (ch <= 0xfe6f))
+                // Fullwidth Forms
+                || ((ch >= 0xff00) && (ch <= 0xff60))
+                || ((ch >= 0xffe0) && (ch <= 0xffe6))
+                || ((ch >= 0x20000) && (ch <= 0x2fffd))
+                || ((ch >= 0x30000) && (ch <= 0x3fffd))
+                // emoji
+                || ((ch >= 0x1f004) && (ch <= 0x1fffd))
+            )
+        ) {
+            return 2;
+        }
+        return 1;
+    }
+
+    /**
+     * Determine display width of a string.  This ASSUMES that no characters
+     * are combining.  Hopefully no users will be impacted.
+     *
+     * @param str the string
+     * @return the number of text cell columns required to display this string
+     */
+    public static int width(final String str) {
+        int n = 0;
+        for (int i = 0; i < str.length();) {
+            int ch = str.codePointAt(i);
+            n += width(ch);
+            i += Character.charCount(ch);
+        }
+        return n;
+    }
+
+    /**
+     * Check if character is in the CJK range.
+     *
+     * @param ch character to check
+     * @return true if this character is in the CJK range
+     */
+    public static boolean isCjk(final int ch) {
+        return ((ch >= 0x2e80) && (ch <= 0x9fff));
+    }
+
+    /**
+     * Check if character is in the emoji range.
+     *
+     * @param ch character to check
+     * @return true if this character is in the emoji range
+     */
+    public static boolean isEmoji(final int ch) {
+        return ((ch >= 0x1f004) && (ch <= 0x1fffd));
+    }
+
 }