// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A media type: `text/plain; charset=utf-8`.
///
/// `essence` is the lowercased `type/subtype`; `params` keeps the parameters in
/// order, names lowercased and quoted values unquoted. A malformed parameter is
/// dropped rather than guessed at.
pub(all) struct MediaType {
essence : String
params : Array[(String, String)]
} derive(Eq, Debug)
///|
pub extend MediaType with Eq::{equal, not_equal}
///|
pub extend MediaType with Debug::{to_repr}
///|
/// Parse a header value into a media type. Lenient: anything that is not a
/// parameter is left out, so an empty or foreign value yields an empty essence
/// rather than an error.
pub fn media_type(s : StringView) -> MediaType {
let semi = index_char(s, 0x3B, 0)
let head = if semi < 0 { s } else { s[0:semi] }
let params : Array[(String, String)] = if semi < 0 {
[]
} else {
parse_params(s[semi + 1:])
}
{ essence: trim(head).to_lower(), params, }
}
///|
/// The type half: `text` in `text/plain`.
pub fn MediaType::type_(self : MediaType) -> String {
match index_char(self.essence[:], 0x2F, 0) {
-1 => self.essence
at => self.essence[0:at].to_owned()
}
}
///|
/// The subtype half: `plain` in `text/plain`, empty when there is no slash.
pub fn MediaType::subtype(self : MediaType) -> String {
match index_char(self.essence[:], 0x2F, 0) {
-1 => ""
at => self.essence[at + 1:].to_owned()
}
}
///|
/// A parameter by lowercased name, quoted or bare.
pub fn MediaType::param(self : MediaType, name : StringView) -> String? {
for pair in self.params {
if pair.0[:] == name {
return Some(pair.1)
}
}
None
}
///|
/// Back to a header value. A value that is not a token goes out quoted, which
/// keeps a boundary with spaces or quotes in one piece.
pub impl Show for MediaType with fn to_string(self) {
let out = StringBuilder()
out.write_string(self.essence)
for pair in self.params {
out.write_char(';')
out.write_char(' ')
out.write_string(pair.0)
out.write_char('=')
if is_token(pair.1[:]) {
out.write_string(pair.1)
} else {
out.write_char('"')
for i in 0.. Array[(String, String)] {
let out : Array[(String, String)] = []
let n = s.length()
let mut i = 0
while i < n {
while i < n && (is_space(s.at(i).to_int()) || s.at(i).to_int() == 0x3B) {
i += 1
}
let name_start = i
while i < n &&
s.at(i).to_int() != 0x3D &&
s.at(i).to_int() != 0x3B &&
!is_space(s.at(i).to_int()) {
i += 1
}
let mut name_end = i
while name_end > name_start && is_space(s.at(name_end - 1).to_int()) {
name_end -= 1
}
while i < n && is_space(s.at(i).to_int()) {
i += 1
}
guard i < n && s.at(i).to_int() == 0x3D else {
while i < n && s.at(i).to_int() != 0x3B {
i += 1
}
continue
}
let name = s[name_start:name_end].to_owned()
i += 1
while i < n && is_space(s.at(i).to_int()) {
i += 1
}
if i < n && s.at(i).to_int() == 0x22 {
i += 1
let value = StringBuilder()
while i < n && s.at(i).to_int() != 0x22 {
if s.at(i).to_int() == 0x5C && i + 1 < n {
i += 1
}
value.write_view(s[i:i + 1])
i += 1
}
if i < n {
i += 1
}
out.push((name.to_lower(), value.to_string()))
} else {
let value_start = i
while i < n && s.at(i).to_int() != 0x3B {
i += 1
}
out.push((name.to_lower(), trim(s[value_start:i])))
}
}
out
}
///|
/// Whether a value can stand bare in a parameter.
fn is_token(s : StringView) -> Bool {
if s.is_empty() {
return false
}
for i in 0.. Bool {
match c {
'!'
| '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '.'
| '^'
| '_'
| '`'
| '|'
| '~'
| '0'..<='9'
| 'A'..<='Z'
| 'a'..<='z' => true
_ => false
}
}
///|
fn trim(s : StringView) -> String {
let mut from = 0
let mut to = s.length()
while from < to && is_space(s.at(from).to_int()) {
from += 1
}
while to > from && is_space(s.at(to - 1).to_int()) {
to -= 1
}
s[from:to].to_owned()
}
///|
fn is_space(c : Int32) -> Bool {
c == 0x20 || c == 0x09 || c == 0x0D || c == 0x0A
}
///|
fn index_char(s : StringView, needle : Int32, from : Int32) -> Int32 {
for i = from; i < s.length(); i = i + 1 {
if s.at(i).to_int() == needle {
return i
}
}
-1
}