// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0
///|
/// `uri-template` (RFC 6570). The template is a run of literals and
/// expressions; literals are restricted to the RFC 3986/3987 character sets
/// plus the two braces that delimit expressions, and each expression is an
/// operator-free or operator-led variable list of varspecs.
fn format_uri_template(value : String) -> Bool {
let text = value[:]
let mut index = 0
while index < text.length() {
let next = text.get_char(index)
if next is Some('{') {
let rest = text[index:].to_owned()
guard format_template_expression(rest) is Some(consumed) else {
return false
}
index = index + consumed
} else if next is Some('}') {
return false
} else if next is Some('%') {
guard index + 2 < text.length() &&
text.get_char(index + 1) is Some(high) &&
format_is_hex(high) &&
text.get_char(index + 2) is Some(low) &&
format_is_hex(low) else {
return false
}
index = index + 3
} else if next is Some(ch) {
if !format_template_literal_char(ch) {
return false
}
index = index + char_width(ch)
} else {
return false
}
}
true
}
///|
/// Literal characters: `unreserved / reserved / ucschar / iprivate`, with
/// percent-encoded triplets handled by the caller.
fn format_template_literal_char(ch : Char) -> Bool {
if is_unreserved_byte(ch.to_int()) ||
uri_subdelimiter(ch) ||
ch is (':' | '/' | '?' | '#' | '[' | ']' | '@') {
return true
}
ch.to_int() > 0x7F && (format_is_ucschar(ch) || format_is_iprivate(ch))
}
///|
/// One `{ ... }` expression starting at the start of `text`. Returns the
/// number of code units consumed, including both braces.
fn format_template_expression(text : StringView) -> Int32? {
guard text.has_prefix("{") else { return None }
let mut index = 1
if index < text.length() {
let next = text.get_char(index)
if next is Some(ch) && format_template_operator(ch) {
index = index + 1
}
}
let mut varspecs = 0
while index < text.length() {
match text.get_char(index) {
Some('}') =>
if varspecs == 0 {
return None
} else {
return Some(index + 1)
}
Some(',') => if varspecs == 0 { return None } else { index = index + 1 }
_ => {
guard format_template_varspec(text, index) is Some(end) else {
return None
}
index = end
varspecs = varspecs + 1
}
}
}
None
}
///|
fn format_template_operator(ch : Char) -> Bool {
ch is ('+' | '#' | '.' | '/' | ';' | '?' | '&')
}
///|
/// `varspec = varname [ ":" max-length ] [ "*" ]`, where
/// `varname = varchar *( varchar / "." )` and `varchar = ALPHA / DIGIT / "_" /
/// pct-encoded`.
fn format_template_varspec(text : StringView, start : Int32) -> Int32? {
let mut index = start
while index < text.length() {
let next = text.get_char(index)
if next is Some('%') {
guard index + 2 < text.length() &&
text.get_char(index + 1) is Some(high) &&
format_is_hex(high) &&
text.get_char(index + 2) is Some(low) &&
format_is_hex(low) else {
return None
}
index = index + 3
} else if next is Some(ch) && (format_is_alnum(ch) || ch == '_') {
index = index + 1
} else {
break
}
}
if index == start {
return None
}
// A dot may only separate two non-empty name characters.
while index < text.length() {
if text.get_char(index) != Some('.') {
break
}
let after_dot = index + 1
guard text.get_char(after_dot) is Some(ch) &&
(format_is_alnum(ch) || ch == '_') else {
return None
}
index = after_dot
while index < text.length() {
let next = text.get_char(index)
if next is Some('%') {
guard index + 2 < text.length() &&
text.get_char(index + 1) is Some(high) &&
format_is_hex(high) &&
text.get_char(index + 2) is Some(low) &&
format_is_hex(low) else {
return None
}
index = index + 3
} else if next is Some(ch) && (format_is_alnum(ch) || ch == '_') {
index = index + 1
} else {
break
}
}
}
if text.get_char(index) == Some(':') {
index = index + 1
guard text.get_char(index) is Some(first) && first is ('1'..<='9') else {
return None
}
index = index + 1
let mut digits = 1
while digits < 4 {
guard text.get_char(index) is Some(ch) && format_is_digit(ch) else {
break
}
index = index + 1
digits = digits + 1
}
// `{v:10000}` has a fifth digit, which is not part of max-length.
guard text.get_char(index) is Some(ch) && format_is_digit(ch) else {
return Some(index)
}
return None
}
if text.get_char(index) == Some('*') {
return Some(index + 1)
}
Some(index)
}