///|
/// Strip spaces and tabs from both ends (plus a trailing CR, so Windows
/// line endings survive a `split("\n")`).
pub fn trim_spaces(text : StringView) -> String {
let mut start = 0
let end = text.length()
while start < end {
let ch = text[start].to_int()
if ch == 0x20 || ch == 0x09 {
start += 1
} else {
break
}
}
let mut stop = end
while stop > start {
let ch = text[stop - 1].to_int()
if ch == 0x20 || ch == 0x09 || ch == 0x0D {
stop -= 1
} else {
break
}
}
if start == 0 && stop == end {
text.to_owned()
} else if start >= stop {
""
} else {
text[start:stop].to_owned()
}
}
///|
/// Strict non-negative integer: every character must be an ASCII digit,
/// empty input rejected. Returns `None` instead of a partial number.
pub fn parse_int(text : String) -> Int? {
if text.is_empty() {
return None
}
let mut value = 0
for i in 0.. 0x39 {
return None
}
value = value * 10 + (ch - 0x30)
}
Some(value)
}
///|
/// Text of the first double-quoted segment in a line, or `""` when the
/// line quotes nothing. Built for `key = "value"` manifest lines.
pub fn quoted_value(line : String) -> String {
let mut start = -1
let mut stop = line.length()
for i, ch in line {
if ch == '"' {
if start < 0 {
start = i
} else {
stop = i
break
}
}
}
if start >= 0 && stop > start {
line[start + 1:stop].to_owned()
} else {
""
}
}
///|
const HEX_DIGITS : String = "0123456789ABCDEF"
///|
fn hex_pair(value : Int) -> String {
let high = value >> 4
let low = value & 15
HEX_DIGITS[high:high + 1].to_owned() + HEX_DIGITS[low:low + 1].to_owned()
}
///|
/// Escape a string for embedding inside a JSON double-quoted literal:
/// quotes, backslashes and the C/JSON control escapes; other control
/// characters become `\u00XX`.
pub fn json_escape(text : String) -> String {
let buf = StringBuilder()
for c in text {
match c {
'"' => buf.write_string("\\\"")
'\\' => buf.write_string("\\\\")
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\t' => buf.write_string("\\t")
_ =>
if c.to_int() < 0x20 {
buf.write_string("\\u00" + hex_pair(c.to_int()))
} else {
buf.write_char(c)
}
}
}
buf.to_string()
}
///|
/// A JSON string literal: the text, escaped and wrapped in quotes.
pub fn jstr(text : String) -> String {
"\"" + json_escape(text) + "\""
}
///|
/// Build a JSON object from already-encoded value fragments, so an optional
/// key simply does not appear instead of turning into a null the platform
/// may reject. Keys are quoted by `jstr`; values must arrive pre-encoded
/// (e.g. `jstr(...)` for strings, raw digits for numbers).
pub fn jobj(fields : Array[(String, String)]) -> String {
let buf = StringBuilder()
buf.write_char('{')
let mut first = true
for pair in fields {
let (key, value) = pair
if !first {
buf.write_char(',')
}
first = false
buf.write_string(jstr(key))
buf.write_char(':')
buf.write_string(value)
}
buf.write_char('}')
buf.to_string()
}
///|
/// Build a JSON array from already-encoded item fragments.
pub fn jarr(items : Array[String]) -> String {
let buf = StringBuilder()
buf.write_char('[')
let mut first = true
for item in items {
if !first {
buf.write_char(',')
}
first = false
buf.write_string(item)
}
buf.write_char(']')
buf.to_string()
}
///|
fn is_unreserved(value : Int) -> Bool {
(value >= 48 && value <= 57) ||
(value >= 65 && value <= 90) ||
(value >= 97 && value <= 122) ||
value == 45 ||
value == 46 ||
value == 95 ||
value == 126
}
///|
/// Percent-encode a value for a URL query string or a form body: everything
/// outside the RFC 3986 unreserved set becomes `%XX` over UTF-8 bytes.
pub fn url_encode(text : String) -> String {
let buf = StringBuilder()
for byte in @utf8.encode(text) {
let value = byte.to_int()
if is_unreserved(value) {
// Unreserved bytes are ASCII, so decoding one byte back gives the char.
buf.write_string(@utf8.decode_lossy(Bytes::from_array([byte])))
} else {
buf.write_char('%')
buf.write_string(hex_pair(value))
}
}
buf.to_string()
}