///|
/// Represents URL search parameters per the WHATWG URL Standard.
/// See: https://url.spec.whatwg.org/#interface-urlsearchparams
/// Provides methods to work with the query string of a URL using
/// application/x-www-form-urlencoded format.
pub struct UrlSearchParams {
mut list : Array[(String, String)]
}
///|
/// Create a new empty UrlSearchParams instance.
pub fn UrlSearchParams::new() -> UrlSearchParams {
{ list: [] }
}
///|
/// Parse a query string into UrlSearchParams.
/// Strips a leading "?" if present.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
pub fn UrlSearchParams::from_string(input : String) -> UrlSearchParams {
// Strip leading "?" if present
let input = match input[:] {
['?', .. rest] => rest
s => s
}
let params = UrlSearchParams::new()
urlencoded_parse(input, params.list)
params
}
///|
/// Create UrlSearchParams from an array of (name, value) pairs.
pub fn UrlSearchParams::from_pairs(
pairs : Array[(String, String)],
) -> UrlSearchParams {
{ list: pairs.copy() }
}
///|
/// application/x-www-form-urlencoded percent-encode set.
/// See: https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
/// Encodes everything EXCEPT: 0-9, A-Z, a-z, *, -, ., _
fn urlencoded_percent_encode_set(char : Char) -> Bool {
match char {
'0'..='9' | 'A'..='Z' | 'a'..='z' | '*' | '-' | '.' | '_' => false
_ => true
}
}
///|
/// Parse application/x-www-form-urlencoded input.
/// See: https://url.spec.whatwg.org/#concept-urlencoded-parser
fn urlencoded_parse(
input : StringView,
output : Array[(String, String)],
) -> Unit {
// 1. Let sequences be the result of splitting input on `&`.
for sequence in input.split("&") {
// 2. For each byte sequence bytes in sequences:
// 2.1. If bytes is the empty byte sequence, then continue.
if sequence.is_empty() {
continue
}
// 2.2. If bytes contains a `=`, then let name be the bytes from the start
// of bytes up to but excluding its first `=`, and let value be the
// bytes, if any, after the first `=` up to the end of bytes.
// Otherwise, let name be bytes and let value be the empty byte sequence.
let (name_raw, value_raw) = find_first_equals(sequence)
// 2.3. Replace any `+` in name and value with U+0020 SPACE.
let name_plus_decoded = replace_plus_with_space(name_raw)
let value_plus_decoded = replace_plus_with_space(value_raw)
// 2.4. Let nameString and valueString be the result of running UTF-8 decode
// without BOM on the percent-decoding of name and value, respectively.
let name = @encoding/utf8.decode_lossy(
percent_decode_string(name_plus_decoded[:]),
)
let value = @encoding/utf8.decode_lossy(
percent_decode_string(value_plus_decoded[:]),
)
// 2.5. Append (nameString, valueString) to output.
output.push((name, value))
}
}
///|
/// Find first '=' in StringView and split into (name, value).
fn find_first_equals(input : StringView) -> (String, String) {
let name_builder = StringBuilder::new()
let value_builder = StringBuilder::new()
let mut found_equals = false
for c in input {
if !found_equals && c == '=' {
found_equals = true
} else if found_equals {
value_builder.write_char(c)
} else {
name_builder.write_char(c)
}
}
(name_builder.to_string(), value_builder.to_string())
}
///|
/// Replace all `+` characters with spaces.
fn replace_plus_with_space(input : String) -> String {
let output = StringBuilder::new()
for c in input {
if c == '+' {
output.write_char(' ')
} else {
output.write_char(c)
}
}
output.to_string()
}
///|
/// Serialize UrlSearchParams to application/x-www-form-urlencoded string.
/// See: https://url.spec.whatwg.org/#concept-urlencoded-serializer
pub fn UrlSearchParams::to_string(self : UrlSearchParams) -> String {
let output = StringBuilder::new()
for i, pair in self.list {
if i > 0 {
output.write_char('&')
}
let name = pair.0
let value = pair.1
// Percent-encode name and value, encoding space as +
let encoded_name = utf8_percent_encode(
name,
urlencoded_percent_encode_set,
space_as_plus=true,
)
let encoded_value = utf8_percent_encode(
value,
urlencoded_percent_encode_set,
space_as_plus=true,
)
output.write_string(encoded_name)
output.write_char('=')
output.write_string(encoded_value)
}
output.to_string()
}
///|
/// Return the number of name-value pairs.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-size
pub fn UrlSearchParams::size(self : UrlSearchParams) -> Int {
self.list.length()
}
///|
/// Append a new name-value pair.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-append
pub fn UrlSearchParams::append(
self : UrlSearchParams,
name : String,
value : String,
) -> Unit {
self.list.push((name, value))
}
///|
/// Delete all pairs with the given name.
/// If value is provided, only delete pairs where both name and value match.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-delete
pub fn UrlSearchParams::delete(
self : UrlSearchParams,
name : String,
value? : String,
) -> Unit {
match value {
Some(v) =>
self.list = self.list.filter(fn(pair) { pair.0 != name || pair.1 != v })
None => self.list = self.list.filter(fn(pair) { pair.0 != name })
}
}
///|
/// Get the first value for the given name, or None if not found.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-get
pub fn UrlSearchParams::get(self : UrlSearchParams, name : String) -> String? {
for pair in self.list {
if pair.0 == name {
return Some(pair.1)
}
}
None
}
///|
/// Get all values for the given name.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-getall
pub fn UrlSearchParams::get_all(
self : UrlSearchParams,
name : String,
) -> Array[String] {
let result : Array[String] = []
for pair in self.list {
if pair.0 == name {
result.push(pair.1)
}
}
result
}
///|
/// Check if a name exists in the search params.
/// If value is provided, check if a pair with both name and value exists.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-has
pub fn UrlSearchParams::has(
self : UrlSearchParams,
name : String,
value? : String,
) -> Bool {
match value {
Some(v) => self.list.iter().any(fn(pair) { pair.0 == name && pair.1 == v })
None => self.list.iter().any(fn(pair) { pair.0 == name })
}
}
///|
/// Set a value for the given name.
/// If name already exists, replaces the first occurrence and removes any others.
/// Otherwise, appends a new pair.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-set
pub fn UrlSearchParams::set(
self : UrlSearchParams,
name : String,
value : String,
) -> Unit {
// Find first matching pair
let mut found = false
let new_list : Array[(String, String)] = []
for pair in self.list {
if pair.0 == name {
if !found {
// Replace first occurrence
new_list.push((name, value))
found = true
}
// Skip subsequent occurrences
} else {
new_list.push(pair)
}
}
if !found {
// Name not found, append new pair
new_list.push((name, value))
}
self.list = new_list
}
///|
/// Compare two strings by UTF-16 code unit order.
/// WHATWG URL Standard requires sorting by "code unit less than".
/// See: https://infra.spec.whatwg.org/#code-unit-less-than
fn compare_utf16_code_units(a : String, b : String) -> Int {
let iter_a = a.iter()
let iter_b = b.iter()
while true {
let char_a = iter_a.next()
let char_b = iter_b.next()
match (char_a, char_b) {
(None, None) => return 0
(None, Some(_)) => return -1
(Some(_), None) => return 1
(Some(ca), Some(cb)) => {
let code_a = ca.to_int()
let code_b = cb.to_int()
// For BMP characters (< 0x10000), code point == code unit
// For supplementary characters, compare by high surrogate first
let (unit_a, next_a) : (Int, Int?) = if code_a >= 0x10000 {
// Compute high surrogate: 0xD800 + ((code - 0x10000) >> 10)
let high = 0xD800 + ((code_a - 0x10000) >> 10)
let low = 0xDC00 + ((code_a - 0x10000) & 0x3FF)
(high, Some(low))
} else {
(code_a, None)
}
let (unit_b, next_b) : (Int, Int?) = if code_b >= 0x10000 {
let high = 0xD800 + ((code_b - 0x10000) >> 10)
let low = 0xDC00 + ((code_b - 0x10000) & 0x3FF)
(high, Some(low))
} else {
(code_b, None)
}
// Compare first code units
if unit_a < unit_b {
return -1
}
if unit_a > unit_b {
return 1
}
// First units equal, compare second units if present
match (next_a, next_b) {
(Some(la), Some(lb)) =>
if la < lb {
return -1
} else if la > lb {
return 1
}
(Some(_), None) =>
// a has more code units for this character
return 1
(None, Some(_)) =>
// b has more code units for this character
return -1
(None, None) => ()
}
}
}
}
0 // unreachable
}
///|
/// Sort all name-value pairs by name using stable sort.
/// Uses UTF-16 code unit order per WHATWG URL Standard.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-sort
pub fn UrlSearchParams::sort(self : UrlSearchParams) -> Unit {
self.list.sort_by(fn(a, b) { compare_utf16_code_units(a.0, b.0) })
}
///|
/// Iterate over all (name, value) pairs.
/// See: https://url.spec.whatwg.org/#urlsearchparams-iteration
pub fn UrlSearchParams::iter(self : UrlSearchParams) -> Iter[(String, String)] {
self.list.iter()
}
///|
/// Iterate over all (name, value) pairs (alias for iter).
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-entries
pub fn UrlSearchParams::entries(
self : UrlSearchParams,
) -> Iter[(String, String)] {
self.iter()
}
///|
/// Iterate over all names.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-keys
pub fn UrlSearchParams::keys(self : UrlSearchParams) -> Iter[String] {
self.list.iter().map(fn(pair) { pair.0 })
}
///|
/// Iterate over all values.
/// See: https://url.spec.whatwg.org/#dom-urlsearchparams-values
pub fn UrlSearchParams::values(self : UrlSearchParams) -> Iter[String] {
self.list.iter().map(fn(pair) { pair.1 })
}
///|
/// Implement Show trait for UrlSearchParams, outputting the serialized string.
pub impl Show for UrlSearchParams with fn output(
self : UrlSearchParams,
logger : &Logger,
) -> Unit {
logger.write_string(self.to_string())
}
///|
/// Serialize UrlSearchParams to JSON as a string.
pub impl ToJson for UrlSearchParams with fn to_json(self : UrlSearchParams) -> Json {
self.to_string().to_json()
}