lib/core: Added new `is_hexdigit` service on `Char`
[nit.git] / lib / core / text / abstract_text.nit
index 5cd317c..cacee99 100644 (file)
@@ -68,7 +68,7 @@ abstract class Text
        fun substring(from: Int, count: Int): SELFTYPE is abstract
 
        # Iterates on the substrings of self if any
-       fun substrings: Iterator[FlatText] is abstract
+       private fun substrings: Iterator[FlatText] is abstract
 
        # Is the current Text empty (== "")
        #
@@ -146,15 +146,7 @@ abstract class Text
        # Returns -1 if not found
        #
        # DEPRECATED : Use self.chars.last_index_of_from instead
-       fun last_index_of_from(item: Char, pos: Int): Int
-       do
-               var iter = self.chars.reverse_iterator_from(pos)
-               while iter.is_ok do
-                       if iter.item == item then return iter.index
-                       iter.next
-               end
-               return -1
-       end
+       fun last_index_of_from(item: Char, pos: Int): Int do return chars.last_index_of_from(item, pos)
 
        # Gets an iterator on the chars of self
        #
@@ -543,7 +535,7 @@ abstract class Text
 
                if c >= '0' and c <= '9' then
                        res.add('_')
-                       res.append(c.ascii.to_s)
+                       res.append(c.code_point.to_s)
                        res.add('d')
                        start = 1
                end
@@ -555,7 +547,7 @@ abstract class Text
                                continue
                        end
                        if underscore then
-                               res.append('_'.ascii.to_s)
+                               res.append('_'.code_point.to_s)
                                res.add('d')
                        end
                        if c >= '0' and c <= '9' then
@@ -566,13 +558,13 @@ abstract class Text
                                underscore = true
                        else
                                res.add('_')
-                               res.append(c.ascii.to_s)
+                               res.append(c.code_point.to_s)
                                res.add('d')
                                underscore = false
                        end
                end
                if underscore then
-                       res.append('_'.ascii.to_s)
+                       res.append('_'.code_point.to_s)
                        res.add('d')
                end
                return res.to_s
@@ -587,7 +579,7 @@ abstract class Text
        # Three digits are always used to avoid following digits to be interpreted as an element
        # of the octal sequence.
        #
-       #     assert "{0.ascii}{1.ascii}{8.ascii}{31.ascii}{32.ascii}".escape_to_c == "\\000\\001\\010\\037 "
+       #     assert "{0.code_point}{1.code_point}{8.code_point}{31.code_point}{32.code_point}".escape_to_c == "\\000\\001\\010\\037 "
        #
        # The exceptions are the common `\t` and `\n`.
        fun escape_to_c: String
@@ -599,17 +591,15 @@ abstract class Text
                                b.append("\\n")
                        else if c == '\t' then
                                b.append("\\t")
-                       else if c == '\0' then
-                               b.append("\\000")
                        else if c == '"' then
                                b.append("\\\"")
                        else if c == '\'' then
                                b.append("\\\'")
                        else if c == '\\' then
                                b.append("\\\\")
-                       else if c.ascii < 32 then
+                       else if c.code_point < 32 then
                                b.add('\\')
-                               var oct = c.ascii.to_base(8, false)
+                               var oct = c.code_point.to_base(8, false)
                                # Force 3 octal digits since it is the
                                # maximum allowed in the C specification
                                if oct.length == 1 then
@@ -682,8 +672,8 @@ abstract class Text
                        else if c == ':' or c == ' ' or c == '#' then
                                b.add('\\')
                                b.add(c)
-                       else if c.ascii < 32 or c == ';' or c == '|' or c == '\\' or c == '=' then
-                               b.append("?{c.ascii.to_base(16, false)}")
+                       else if c.code_point < 32 or c == ';' or c == '|' or c == '\\' or c == '=' then
+                               b.append("?{c.code_point.to_base(16, false)}")
                        else
                                b.add(c)
                        end
@@ -697,7 +687,7 @@ abstract class Text
        #     assert s.length        ==  2
        #     var u = s.unescape_nit
        #     assert u.length        ==  1
-       #     assert u.chars[0].ascii      ==  10 # (the ASCII value of the "new line" character)
+       #     assert u.chars[0].code_point      ==  10 # (the ASCII value of the "new line" character)
        fun unescape_nit: String
        do
                var res = new Buffer.with_cap(self.length)
@@ -728,11 +718,44 @@ abstract class Text
                return res.to_s
        end
 
+       # Returns `self` with all characters escaped with their UTF-16 representation
+       #
+       #     assert "Aรจใ‚๐“".escape_to_utf16 == "\\u0041\\u00e8\\u3042\\ud800\\udfd3"
+       fun escape_to_utf16: String do
+               var buf = new Buffer
+               for i in chars do buf.append i.escape_to_utf16
+               return buf.to_s
+       end
+
+       # Returns the Unicode char escaped by `self`
+       #
+       #     assert "\\u0041".from_utf16_escape == 'A'
+       #     assert "\\ud800\\udfd3".from_utf16_escape == '๐“'
+       #     assert "\\u00e8".from_utf16_escape == 'รจ'
+       #     assert "\\u3042".from_utf16_escape == 'ใ‚'
+       fun from_utf16_escape: Char do
+               var ln = length
+               if ln != 6 and ln != 12 then return 0xFFFD.code_point
+               var cphi = substring(2, 4).to_hex
+               if cphi < 0xD800 then return cphi.code_point
+               if cphi > 0xDFFF then return cphi.code_point
+               if cphi > 0xDBFF then return 0xFFFD.code_point
+               var cp = 0
+               cp += (cphi - 0xD800) << 10
+               var cplo = substring(8, 4).to_hex
+               if cplo < 0xDC00 then return 0xFFFD.code_point
+               if cplo > 0xDFFF then return 0xFFFD.code_point
+               cp += cplo - 0xDC00
+               cp += 0x10000
+               return cp.code_point
+       end
+
        # Encode `self` to percent (or URL) encoding
        #
        #     assert "aBc09-._~".to_percent_encoding == "aBc09-._~"
        #     assert "%()< >".to_percent_encoding == "%25%28%29%3c%20%3e"
        #     assert ".com/post?e=asdf&f=123".to_percent_encoding == ".com%2fpost%3fe%3dasdf%26f%3d123"
+       #     assert "รฉใ‚ใ„ใ†".to_percent_encoding == "%c3%a9%e3%81%82%e3%81%84%e3%81%86"
        fun to_percent_encoding: String
        do
                var buf = new Buffer
@@ -746,7 +769,10 @@ abstract class Text
                           c == '_' or c == '~'
                        then
                                buf.add c
-                       else buf.append "%{c.ascii.to_hex}"
+                       else
+                               var bytes = c.to_s.bytes
+                               for b in bytes do buf.append "%{b.to_i.to_hex}"
+                       end
                end
 
                return buf.to_s
@@ -762,36 +788,50 @@ abstract class Text
        #     assert "%25%28%29%3C%20%3E".from_percent_encoding == "%()< >"
        #     assert "incomplete %".from_percent_encoding == "incomplete ?"
        #     assert "invalid % usage".from_percent_encoding == "invalid ? usage"
+       #     assert "%c3%a9%e3%81%82%e3%81%84%e3%81%86".from_percent_encoding == "รฉใ‚ใ„ใ†"
        fun from_percent_encoding: String
        do
-               var buf = new Buffer
+               var len = bytelen
+               var has_percent = false
+               for c in chars do
+                       if c == '%' then
+                               len -= 2
+                               has_percent = true
+                       end
+               end
+
+               # If no transformation is needed, return self as a string
+               if not has_percent then return to_s
 
+               var buf = new NativeString(len)
                var i = 0
+               var l = 0
                while i < length do
                        var c = chars[i]
                        if c == '%' then
                                if i + 2 >= length then
                                        # What follows % has been cut off
-                                       buf.add '?'
+                                       buf[l] = '?'.ascii
                                else
                                        i += 1
                                        var hex_s = substring(i, 2)
                                        if hex_s.is_hex then
                                                var hex_i = hex_s.to_hex
-                                               buf.add hex_i.ascii
+                                               buf[l] = hex_i.to_b
                                                i += 1
                                        else
                                                # What follows a % is not Hex
-                                               buf.add '?'
+                                               buf[l] = '?'.ascii
                                                i -= 1
                                        end
                                end
-                       else buf.add c
+                       else buf[l] = c.ascii
 
                        i += 1
+                       l += 1
                end
 
-               return buf.to_s
+               return buf.to_s_with_length(l)
        end
 
        # Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references.
@@ -889,7 +929,7 @@ abstract class Text
 
                        for i in [0..length[ do
                                var char = chars[i]
-                               h = (h << 5) + h + char.ascii
+                               h = (h << 5) + h + char.code_point
                        end
 
                        hash_cache = h
@@ -965,10 +1005,7 @@ abstract class FlatText
        #
        # Warning : Might be void in some subclasses, be sure to check
        # if set before using it.
-       private var items: NativeString is noinit
-
-       # Real items, used as cache for to_cstring is called
-       private var real_items: nullable NativeString = null
+       var items: NativeString is noinit
 
        # Returns a char* starting at position `first_byte`
        #
@@ -985,7 +1022,7 @@ abstract class FlatText
        #
        # As always, do not modify the content of the String in C code, if this is what you want
        # copy locally the char* as Nit Strings are immutable.
-       private fun fast_cstring: NativeString is abstract
+       fun fast_cstring: NativeString is abstract
 
        redef var length = 0
 
@@ -1034,7 +1071,7 @@ private abstract class StringByteView
 
        redef fun is_empty do return target.is_empty
 
-       redef fun length do return target.length
+       redef fun length do return target.bytelen
 
        redef fun iterator do return self.iterator_from(0)
 
@@ -1557,9 +1594,15 @@ end
 
 redef class Char
 
+       # Returns a sequence with the UTF-8 bytes of `self`
+       #
+       #     assert 'a'.bytes == [0x61u8]
+       #     assert 'ใพ'.bytes == [0xE3u8, 0x81u8, 0xBEu8]
+       fun bytes: SequenceRead[Byte] do return to_s.bytes
+
        # Length of `self` in a UTF-8 String
        private fun u8char_len: Int do
-               var c = self.ascii
+               var c = self.code_point
                if c < 0x80 then return 1
                if c <= 0x7FF then return 2
                if c <= 0xFFFF then return 3
@@ -1576,6 +1619,46 @@ redef class Char
                return ns.to_s_with_length(ln)
        end
 
+       # Returns `self` escaped to UTF-16
+       #
+       # i.e. Represents `self`.`code_point` using UTF-16 codets escaped
+       # with a `\u`
+       #
+       #     assert 'A'.escape_to_utf16 == "\\u0041"
+       #     assert 'รจ'.escape_to_utf16 == "\\u00e8"
+       #     assert 'ใ‚'.escape_to_utf16 == "\\u3042"
+       #     assert '๐“'.escape_to_utf16 == "\\ud800\\udfd3"
+       fun escape_to_utf16: String do
+               var cp = code_point
+               var buf: Buffer
+               if cp < 0xD800 or (cp >= 0xE000 and cp <= 0xFFFF) then
+                       buf = new Buffer.with_cap(6)
+                       buf.append("\\u0000")
+                       var hx = cp.to_hex
+                       var outid = 5
+                       for i in hx.chars.reverse_iterator do
+                               buf[outid] = i
+                               outid -= 1
+                       end
+               else
+                       buf = new Buffer.with_cap(12)
+                       buf.append("\\u0000\\u0000")
+                       var lo = (((cp - 0x10000) & 0x3FF) + 0xDC00).to_hex
+                       var hi = ((((cp - 0x10000) & 0xFFC00) >> 10) + 0xD800).to_hex
+                       var out = 2
+                       for i in hi do
+                               buf[out] = i
+                               out += 1
+                       end
+                       out = 8
+                       for i in lo do
+                               buf[out] = i
+                               out += 1
+                       end
+               end
+               return buf.to_s
+       end
+
        private fun u8char_tos(r: NativeString, len: Int) `{
                r[len] = '\0';
                switch(len){
@@ -1626,6 +1709,16 @@ redef class Char
                return (self >= 'a' and self <= 'z') or (self >= 'A' and self <= 'Z')
        end
 
+       # Is `self` an hexadecimal digit ?
+       #
+       #     assert 'A'.is_hexdigit
+       #     assert not 'G'.is_hexdigit
+       #     assert 'a'.is_hexdigit
+       #     assert not 'g'.is_hexdigit
+       #     assert '5'.is_hexdigit
+       fun is_hexdigit: Bool do return (self >= '0' and self <= '9') or (self >= 'A' and self <= 'F') or
+                                       (self >= 'a' and self <= 'f')
+
        # Returns true if the char is an alpha or a numeric digit
        #
        #     assert 'a'.is_alphanumeric