///|
/// Decode a `data:` URL to its payload text.
/// Supports `data:text/html,<payload>` and `data:text/html;base64,<payload>`
/// (base64 payload is interpreted as UTF-8, per the data URL spec).
/// Returns `None` for non-data URLs or undecodable payloads.
pub fn decode_data_url(url : String) -> String? {
if !url.has_prefix("data:") {
return None
}
let rest = url.unsafe_substring(start=5, end=url.length())
let comma = rest.find(",")
match comma {
None => None
Some(comma) => {
let meta = rest.unsafe_substring(start=0, end=comma)
let payload = rest.unsafe_substring(start=comma + 1, end=rest.length())
if meta.contains(";base64") {
let bytes = Some(@base64.decode(payload)) catch { _ => None }
match bytes {
None => None
Some(b) => {
let text = Some(@utf8.decode(b[:], ignore_bom=true)) catch {
_ => None
}
text
}
}
} else {
Some(percent_decode(payload))
}
}
}
}
///|
/// Decode percent-encoded payload (`%XX`). Consecutive `%XX` bytes are
/// accumulated and decoded as UTF-8 so multi-byte characters survive.
fn percent_decode(s : String) -> String {
let buf = StringBuilder()
let len = s.length()
let mut i = 0
let pending : Array[Byte] = []
while i < len {
if s[i] == '%' && i + 2 < len {
match parse_hex_byte(s, i + 1) {
Some(byte) => {
pending.push(byte.to_byte())
i = i + 3
continue
}
None => ()
}
}
if pending.length() > 0 {
let text = Some(
@utf8.decode(Bytes::from_array(pending)[:], ignore_bom=true),
) catch {
_ => None
}
match text {
Some(t) => buf.write_string(t)
None => ()
}
pending.clear()
}
buf.write_char(s[i].to_int().unsafe_to_char())
i = i + 1
}
if pending.length() > 0 {
let text = Some(
@utf8.decode(Bytes::from_array(pending)[:], ignore_bom=true),
) catch {
_ => None
}
match text {
Some(t) => buf.write_string(t)
None => ()
}
}
buf.to_string()
}
///|
/// Parse two hex digits at `start` as a byte value.
fn parse_hex_byte(s : String, start : Int) -> Int? {
fn digit(c : UInt16) -> Int? {
let v = c.to_int()
if v >= '0'.to_int() && v <= '9'.to_int() {
return Some(v - '0'.to_int())
}
if v >= 'a'.to_int() && v <= 'f'.to_int() {
return Some(v - 'a'.to_int() + 10)
}
if v >= 'A'.to_int() && v <= 'F'.to_int() {
return Some(v - 'A'.to_int() + 10)
}
None
}
let hi = digit(s[start])
let lo = digit(s[start + 1])
match (hi, lo) {
(Some(h), Some(l)) => Some(h * 16 + l)
_ => None
}
}