nitc&lib: MapIterator keys can be nullable
[nit.git] / lib / standard / string.nit
index aeaa736..f1d3288 100644 (file)
@@ -30,7 +30,6 @@ intrude import collection::array
 # High-level abstraction for all text representations
 abstract class Text
        super Comparable
-       super StringCapable
 
        redef type OTHER: Text
 
@@ -386,6 +385,40 @@ abstract class Text
        #     assert "\na\nb\tc\t".trim          == "a\nb\tc"
        fun trim: SELFTYPE do return (self.l_trim).r_trim
 
+       # Returns `self` removed from its last line terminator (if any).
+       #
+       #    assert "Hello\n".chomp == "Hello"
+       #    assert "Hello".chomp   == "Hello"
+       #
+       #    assert "\n".chomp == ""
+       #    assert "".chomp   == ""
+       #
+       # Line terminators are `"\n"`, `"\r\n"` and `"\r"`.
+       # A single line terminator, the last one, is removed.
+       #
+       #    assert "\r\n".chomp     == ""
+       #    assert "\r\n\n".chomp   == "\r\n"
+       #    assert "\r\n\r\n".chomp == "\r\n"
+       #    assert "\r\n\r".chomp   == "\r\n"
+       #
+       # Note: unlike with most IO methods like `IStream::read_line`,
+       # a single `\r` is considered here to be a line terminator and will be removed.
+       fun chomp: SELFTYPE
+       do
+               var len = length
+               if len == 0 then return self
+               var l = self.chars.last
+               if l == '\r' then
+                       return substring(0, len-1)
+               else if l != '\n' then
+                       return self
+               else if len > 1 and self.chars[len-2] == '\r' then
+                       return substring(0, len-2)
+               else
+                       return substring(0, len-1)
+               end
+       end
+
        # Justify a self in a space of `length`
        #
        # `left` is the space ratio on the left side.
@@ -393,6 +426,8 @@ abstract class Text
        # * 1.0 for right-justified (all spaces at the left)
        # * 0.5 for centered (half the spaces at the left)
        #
+       # Examples
+       #
        #     assert "hello".justify(10, 0.0)  == "hello     "
        #     assert "hello".justify(10, 1.0)  == "     hello"
        #     assert "hello".justify(10, 0.5)  == "  hello   "
@@ -401,9 +436,9 @@ abstract class Text
        #
        #     assert "hello".justify(2, 0.0)   == "hello"
        #
-       # REQUIRE: left >= 0.0 and left <= 1.0
+       # REQUIRE: `left >= 0.0 and left <= 1.0`
        # ENSURE: `self.length <= length implies result.length == length`
-       # ENSURE: `self.length >= length implies result == self
+       # ENSURE: `self.length >= length implies result == self`
        fun justify(length: Int, left: Float): SELFTYPE
        do
                var diff = length - self.length
@@ -413,13 +448,39 @@ abstract class Text
                return " " * before + self + " " * (diff-before)
        end
 
-       # Mangle a string to be a unique string only made of alphanumeric characters
+       # Mangle a string to be a unique string only made of alphanumeric characters and underscores.
+       #
+       # This method is injective (two different inputs never produce the same
+       # output) and the returned string always respect the following rules:
+       #
+       # * Contains only US-ASCII letters, digits and underscores.
+       # * Never starts with a digit.
+       # * Never ends with an underscore.
+       # * Never contains two contiguous underscores.
+       #
+       #     assert "42_is/The answer!".to_cmangle == "_52d2_is_47dThe_32danswer_33d"
+       #     assert "__".to_cmangle == "_95d_95d"
+       #     assert "__d".to_cmangle == "_95d_d"
+       #     assert "_d_".to_cmangle == "_d_95d"
+       #     assert "_42".to_cmangle == "_95d42"
+       #     assert "foo".to_cmangle == "foo"
+       #     assert "".to_cmangle == ""
        fun to_cmangle: String
        do
+               if is_empty then return ""
                var res = new FlatBuffer
                var underscore = false
-               for i in [0..length[ do
-                       var c = chars[i]
+               var start = 0
+               var c = chars[0]
+
+               if c >= '0' and c <= '9' then
+                       res.add('_')
+                       res.append(c.ascii.to_s)
+                       res.add('d')
+                       start = 1
+               end
+               for i in [start..length[ do
+                       c = chars[i]
                        if (c >= 'a' and c <= 'z') or (c >='A' and c <= 'Z') then
                                res.add(c)
                                underscore = false
@@ -442,6 +503,10 @@ abstract class Text
                                underscore = false
                        end
                end
+               if underscore then
+                       res.append('_'.ascii.to_s)
+                       res.add('d')
+               end
                return res.to_s
        end
 
@@ -641,9 +706,11 @@ abstract class Text
                return buf.to_s
        end
 
-       # Escape the four characters `<`, `>`, `&`, and `"` with their html counterpart
+       # Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references.
+       #
+       #     assert "a&b-<>\"x\"/'".html_escape      ==  "a&amp;b-&lt;&gt;&#34;x&#34;&#47;&#39;"
        #
-       #     assert "a&b->\"x\"".html_escape      ==  "a&amp;b-&gt;&quot;x&quot;"
+       # SEE: <https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content>
        fun html_escape: SELFTYPE
        do
                var buf = new FlatBuffer
@@ -657,7 +724,11 @@ abstract class Text
                        else if c == '>' then
                                buf.append "&gt;"
                        else if c == '"' then
-                               buf.append "&quot;"
+                               buf.append "&#34;"
+                       else if c == '\'' then
+                               buf.append "&#39;"
+                       else if c == '/' then
+                               buf.append "&#47;"
                        else buf.add c
                end
 
@@ -929,9 +1000,9 @@ abstract class String
        #
        # SEE : `Char::is_letter` for the definition of letter.
        #
-       #    assert "jAVASCRIPT".capitalized == "Javascript"
-       #    assert "i am root".capitalized == "I Am Root"
-       #    assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC"
+       #     assert "jAVASCRIPT".capitalized == "Javascript"
+       #     assert "i am root".capitalized == "I Am Root"
+       #     assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC"
        fun capitalized: SELFTYPE do
                if length == 0 then return self
 
@@ -998,7 +1069,7 @@ class FlatString
 
        redef fun reversed
        do
-               var native = calloc_string(self.length + 1)
+               var native = new NativeString(self.length + 1)
                var length = self.length
                var items = self.items
                var pos = 0
@@ -1036,7 +1107,7 @@ class FlatString
 
        redef fun to_upper
        do
-               var outstr = calloc_string(self.length + 1)
+               var outstr = new NativeString(self.length + 1)
                var out_index = 0
 
                var myitems = self.items
@@ -1056,7 +1127,7 @@ class FlatString
 
        redef fun to_lower
        do
-               var outstr = calloc_string(self.length + 1)
+               var outstr = new NativeString(self.length + 1)
                var out_index = 0
 
                var myitems = self.items
@@ -1101,7 +1172,7 @@ class FlatString
                if real_items != null then
                        return real_items.as(not null)
                else
-                       var newItems = calloc_string(length + 1)
+                       var newItems = new NativeString(length + 1)
                        self.items.copy_to(newItems, length, index_from, 0)
                        newItems[length] = '\0'
                        self.real_items = newItems
@@ -1179,7 +1250,7 @@ class FlatString
 
                var total_length = my_length + its_length
 
-               var target_string = calloc_string(my_length + its_length + 1)
+               var target_string = new NativeString(my_length + its_length + 1)
 
                self.items.copy_to(target_string, my_length, index_from, 0)
                if s isa FlatString then
@@ -1210,7 +1281,7 @@ class FlatString
 
                var my_items = self.items
 
-               var target_string = calloc_string((final_length) + 1)
+               var target_string = new NativeString(final_length + 1)
 
                target_string[final_length] = '\0'
 
@@ -1406,20 +1477,20 @@ abstract class Buffer
        #
        # SEE: `Char::is_letter` for the definition of a letter.
        #
-       #    var b = new FlatBuffer.from("jAVAsCriPt")"
-       #    b.capitalize
-       #    assert b == "Javascript"
-       #    b = new FlatBuffer.from("i am root")
-       #    b.capitalize
-       #    assert b == "I Am Root"
-       #    b = new FlatBuffer.from("ab_c -ab0c ab\nc")
-       #    b.capitalize
-       #    assert b == "Ab_C -Ab0C Ab\nC"
+       #     var b = new FlatBuffer.from("jAVAsCriPt")
+       #     b.capitalize
+       #     assert b == "Javascript"
+       #     b = new FlatBuffer.from("i am root")
+       #     b.capitalize
+       #     assert b == "I Am Root"
+       #     b = new FlatBuffer.from("ab_c -ab0c ab\nc")
+       #     b.capitalize
+       #     assert b == "Ab_C -Ab0C Ab\nC"
        fun capitalize do
                if length == 0 then return
                var c = self[0].to_upper
                self[0] = c
-               var prev: Char = c
+               var prev = c
                for i in [1 .. length[ do
                        prev = c
                        c = self[i]
@@ -1509,7 +1580,7 @@ class FlatBuffer
                # The COW flag can be set at false here, since
                # it does a copy of the current `Buffer`
                written = false
-               var a = calloc_string(c+1)
+               var a = new NativeString(c+1)
                if length > 0 then items.copy_to(a, length, 0, 0)
                items = a
                capacity = c
@@ -1525,7 +1596,7 @@ class FlatBuffer
        redef fun to_cstring
        do
                if is_dirty then
-                       var new_native = calloc_string(length + 1)
+                       var new_native = new NativeString(length + 1)
                        new_native[length] = '\0'
                        if length > 0 then items.copy_to(new_native, length, 0, 0)
                        real_items = new_native
@@ -1542,7 +1613,7 @@ class FlatBuffer
        do
                capacity = s.length + 1
                length = s.length
-               items = calloc_string(capacity)
+               items = new NativeString(capacity)
                if s isa FlatString then
                        s.items.copy_to(items, length, s.index_from, 0)
                else if s isa FlatBuffer then
@@ -1562,7 +1633,7 @@ class FlatBuffer
        do
                assert cap >= 0
                # _items = new NativeString.calloc(cap)
-               items = calloc_string(cap+1)
+               items = new NativeString(cap+1)
                capacity = cap
                length = 0
        end
@@ -1619,7 +1690,7 @@ class FlatBuffer
        redef fun reverse
        do
                written = false
-               var ns = calloc_string(capacity)
+               var ns = new NativeString(capacity)
                var si = length - 1
                var ni = 0
                var it = items
@@ -1690,7 +1761,6 @@ end
 
 private class FlatBufferCharView
        super BufferCharView
-       super StringCapable
 
        redef type SELFTYPE: FlatBuffer
 
@@ -1772,7 +1842,7 @@ redef class Object
 
        # The class name of the object.
        #
-       #    assert 5.class_name == "Int"
+       #     assert 5.class_name == "Int"
        fun class_name: String do return native_class_name.to_s
 
        # Developer readable representation of `self`.
@@ -1967,10 +2037,10 @@ redef class Char
 
        # Returns true if the char is a numerical digit
        #
-       #      assert '0'.is_numeric
-       #      assert '9'.is_numeric
-       #      assert not 'a'.is_numeric
-       #      assert not '?'.is_numeric
+       #     assert '0'.is_numeric
+       #     assert '9'.is_numeric
+       #     assert not 'a'.is_numeric
+       #     assert not '?'.is_numeric
        fun is_numeric: Bool
        do
                return self >= '0' and self <= '9'
@@ -1978,10 +2048,10 @@ redef class Char
 
        # Returns true if the char is an alpha digit
        #
-       #      assert 'a'.is_alpha
-       #      assert 'Z'.is_alpha
-       #      assert not '0'.is_alpha
-       #      assert not '?'.is_alpha
+       #     assert 'a'.is_alpha
+       #     assert 'Z'.is_alpha
+       #     assert not '0'.is_alpha
+       #     assert not '?'.is_alpha
        fun is_alpha: Bool
        do
                return (self >= 'a' and self <= 'z') or (self >= 'A' and self <= 'Z')
@@ -1989,11 +2059,11 @@ redef class Char
 
        # Returns true if the char is an alpha or a numeric digit
        #
-       #      assert 'a'.is_alphanumeric
-       #      assert 'Z'.is_alphanumeric
-       #      assert '0'.is_alphanumeric
-       #      assert '9'.is_alphanumeric
-       #      assert not '?'.is_alphanumeric
+       #     assert 'a'.is_alphanumeric
+       #     assert 'Z'.is_alphanumeric
+       #     assert '0'.is_alphanumeric
+       #     assert '9'.is_alphanumeric
+       #     assert not '?'.is_alphanumeric
        fun is_alphanumeric: Bool
        do
                return self.is_numeric or self.is_alpha
@@ -2104,7 +2174,7 @@ redef class Map[K,V]
                var i = iterator
                var k = i.key
                var e = i.item
-               s.append("{k}{couple_sep}{e or else "<null>"}")
+               s.append("{k or else "<null>"}{couple_sep}{e or else "<null>"}")
 
                # Concat other items
                i.next
@@ -2112,7 +2182,7 @@ redef class Map[K,V]
                        s.append(sep)
                        k = i.key
                        e = i.item
-                       s.append("{k}{couple_sep}{e or else "<null>"}")
+                       s.append("{k or else "<null>"}{couple_sep}{e or else "<null>"}")
                        i.next
                end
                return s.to_s
@@ -2125,7 +2195,6 @@ end
 
 # Native strings are simple C char *
 extern class NativeString `{ char* `}
-       super StringCapable
        # Creates a new NativeString with a capacity of `length`
        new(length: Int) is intern
 
@@ -2169,7 +2238,7 @@ extern class NativeString `{ char* `}
        fun to_s_with_copy: FlatString
        do
                var length = cstring_length
-               var new_self = calloc_string(length + 1)
+               var new_self = new NativeString(length + 1)
                copy_to(new_self, length, 0, 0)
                var str = new FlatString.with_infos(new_self, length, 0, length - 1)
                new_self[length] = '\0'
@@ -2178,13 +2247,6 @@ extern class NativeString `{ char* `}
        end
 end
 
-# StringCapable objects can create native strings
-interface StringCapable
-
-       # Allocate a string of `size`.
-       protected fun calloc_string(size: Int): NativeString is intern
-end
-
 redef class Sys
        private var args_cache: nullable Sequence[String]