///|
pub fn parse_attribute_list(
raw : String,
line~ : Int = 0,
) -> Result[Array[HlsAttribute], ParseError] {
let attrs : Array[HlsAttribute] = []
for piece in split_quoted(raw, ',') {
let item = clean(piece[:])
if item == "" {
continue
}
match item.split_once("=") {
Some((name_view, value_view)) => {
let name = clean(name_view).to_upper()
let raw_value = clean(value_view)
if name == "" {
return Err(InvalidAttribute(line, "attribute name is empty"))
}
attrs.push({
name,
value: unquote(raw_value),
quoted: is_quoted(raw_value),
})
}
None =>
return Err(InvalidAttribute(line, "expected KEY=VALUE, got \{item}"))
}
}
Ok(attrs)
}
///|
fn is_quoted(input : String) -> Bool {
input.length() >= 2 &&
input.get_char(0) == Some('"') &&
input.get_char(input.length() - 1) == Some('"')
}
///|
fn unquote(input : String) -> String {
if !is_quoted(input) {
return input
}
let inner = input[1:input.length() - 1].to_owned()
let out = StringBuilder(size_hint=inner.length())
let mut escaped = false
for c in inner.iter() {
if escaped {
out.write_char(c)
escaped = false
} else if c == '\\' {
escaped = true
} else {
out.write_char(c)
}
}
if escaped {
out.write_char('\\')
}
out.to_string()
}
///|
fn quote_attr(value : String) -> String {
let out = StringBuilder(size_hint=value.length() + 2)
out.write_char('"')
for c in value.iter() {
if c == '"' || c == '\\' {
out.write_char('\\')
}
out.write_char(c)
}
out.write_char('"')
out.to_string()
}
///|
fn write_attr(
attrs : Array[String],
name : String,
value : String,
quoted~ : Bool = false,
) -> Unit {
if quoted {
attrs.push(name + "=" + quote_attr(value))
} else {
attrs.push(name + "=" + value)
}
}
///|
fn join_attrs(attrs : Array[String]) -> String {
let buf = StringBuilder()
for i, attr in attrs {
if i > 0 {
buf.write_char(',')
}
buf.write_string(attr)
}
buf.to_string()
}
///|
pub fn attributes_to_string(attrs : Array[HlsAttribute]) -> String {
let parts : Array[String] = []
for attr in attrs {
write_attr(parts, attr.name, attr.value, quoted=attr.quoted)
}
join_attrs(parts)
}
///|
fn parse_byte_range(raw : String, line : Int) -> Result[ByteRange, ParseError] {
match clean(raw[:]).split_once("@") {
Some((length_view, offset_view)) =>
match parse_unsigned_int(clean(length_view), line, "BYTERANGE.length") {
Ok(length) =>
match parse_unsigned_int(clean(offset_view), line, "BYTERANGE.offset") {
Ok(offset) => Ok({ length, offset: Some(offset) })
Err(err) => Err(err)
}
Err(err) => Err(err)
}
None =>
match parse_unsigned_int(clean(raw[:]), line, "BYTERANGE.length") {
Ok(length) => Ok({ length, offset: None })
Err(err) => Err(err)
}
}
}
///|
fn byte_range_to_string(range : ByteRange) -> String {
match range.offset {
Some(offset) => range.length.to_string() + "@" + offset.to_string()
None => range.length.to_string()
}
}
///|
fn parse_resolution(raw : String, line : Int) -> Result[Resolution, ParseError] {
match clean(raw[:]).split_once("x") {
Some((w, h)) =>
match parse_unsigned_int(clean(w), line, "RESOLUTION.width") {
Ok(width) =>
match parse_unsigned_int(clean(h), line, "RESOLUTION.height") {
Ok(height) => Ok({ width, height })
Err(err) => Err(err)
}
Err(err) => Err(err)
}
None => Err(InvalidNumber(line, "RESOLUTION", raw))
}
}
///|
fn resolution_to_string(resolution : Resolution) -> String {
resolution.width.to_string() + "x" + resolution.height.to_string()
}