Decode self from percent (or URL) encoding to a clear string

Invalid '%' are not decoded.

assert "aBc09-._~".from_percent_encoding == "aBc09-._~"
assert "%25%28%29%3c%20%3e".from_percent_encoding == "%()< >"
assert ".com%2fpost%3fe%3dasdf%26f%3d123".from_percent_encoding == ".com/post?e=asdf&f=123"
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 == "éあいう"
assert "%1 %A %C3%A9A9".from_percent_encoding == "%1 %A éA9"

Property definitions

core $ Text :: from_percent_encoding
	# Decode `self` from percent (or URL) encoding to a clear string
	#
	# Invalid '%' are not decoded.
	#
	# ~~~
	# assert "aBc09-._~".from_percent_encoding == "aBc09-._~"
	# assert "%25%28%29%3c%20%3e".from_percent_encoding == "%()< >"
	# assert ".com%2fpost%3fe%3dasdf%26f%3d123".from_percent_encoding == ".com/post?e=asdf&f=123"
	# 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 == "éあいう"
	# assert "%1 %A %C3%A9A9".from_percent_encoding == "%1 %A éA9"
	# ~~~
	fun from_percent_encoding: String
	do
		var len = byte_length
		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 CString(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[l] = u'%'
				else
					i += 1
					var hex_s = substring(i, 2)
					if hex_s.is_hex then
						var hex_i = hex_s.to_hex
						buf[l] = hex_i
						i += 1
					else
						# What follows a % is not Hex
						buf[l] = u'%'
						i -= 1
					end
				end
			else buf[l] = c.code_point

			i += 1
			l += 1
		end

		return buf.to_s_unsafe(l, copy=false)
	end
lib/core/text/abstract_text.nit:925,2--982,4