use deno_core::op2;
use serde::Serialize;
use urlpattern::quirks;
use urlpattern::quirks::StringOrInit;
use urlpattern::quirks::UrlPatternInit;
#[derive(Debug, thiserror::Error, deno_error::JsError)]
#[class(type)]
#[error("{0}")]
pub struct UrlPatternError(String);
impl From<urlpattern::Error> for UrlPatternError {
fn from(err: urlpattern::Error) -> Self {
UrlPatternError(err.to_string())
}
}
fn enrich_url_pattern_error(
err: urlpattern::Error,
input: &StringOrInit,
) -> UrlPatternError {
let message = err.to_string();
let pos = match err {
urlpattern::Error::Tokenizer(_, pos) => Some(pos),
_ => None,
};
let (pattern, caret_pos) = match input {
StringOrInit::String(s) => (Some(("URLPattern", s.as_str())), None),
StringOrInit::Init(init) => (single_init_component(init), pos),
};
let Some((name, pattern)) = pattern else {
return UrlPatternError(message);
};
let mut out = format!("Failed to parse {name} from \"{pattern}\": {message}");
if let Some(pos) = caret_pos {
out.push_str("\n\n ");
out.push_str(pattern);
out.push_str("\n ");
for _ in 0..pos {
out.push(' ');
}
out.push('^');
}
if message.contains("invalid name") {
out.push_str(
"\n\n hint: \":\" starts a named group and must be followed by a name \
(a letter or \"_\", then letters, digits or \"_\"). To match a literal \
\":\", escape it as \"\\:\".",
);
}
UrlPatternError(out)
}
fn single_init_component(
init: &UrlPatternInit,
) -> Option<(&'static str, &str)> {
let components: [(&'static str, &Option<String>); 8] = [
("protocol", &init.protocol),
("username", &init.username),
("password", &init.password),
("hostname", &init.hostname),
("port", &init.port),
("pathname", &init.pathname),
("search", &init.search),
("hash", &init.hash),
];
let mut set = components
.iter()
.filter_map(|(name, value)| value.as_deref().map(|value| (*name, value)));
match (set.next(), set.next()) {
(Some(only), None) => Some(only),
_ => None,
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UrlPatternComponent {
pattern_string: String,
regexp_string: String,
group_name_list: Vec<String>,
}
impl From<urlpattern::quirks::UrlPatternComponent> for UrlPatternComponent {
fn from(c: urlpattern::quirks::UrlPatternComponent) -> Self {
Self {
pattern_string: c.pattern_string,
regexp_string: c.regexp_string,
group_name_list: c.group_name_list,
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UrlPatternResult {
protocol: UrlPatternComponent,
username: UrlPatternComponent,
password: UrlPatternComponent,
hostname: UrlPatternComponent,
port: UrlPatternComponent,
pathname: UrlPatternComponent,
search: UrlPatternComponent,
hash: UrlPatternComponent,
has_regexp_groups: bool,
}
#[op2]
#[serde]
pub fn op_urlpattern_parse(
#[serde] input: StringOrInit,
#[string] base_url: Option<String>,
#[serde] options: urlpattern::UrlPatternOptions,
) -> Result<UrlPatternResult, UrlPatternError> {
let init =
quirks::process_construct_pattern_input(input.clone(), base_url.as_deref())
.map_err(|e| enrich_url_pattern_error(e, &input))?;
let pattern = quirks::parse_pattern(init, options)
.map_err(|e| enrich_url_pattern_error(e, &input))?;
Ok(UrlPatternResult {
protocol: pattern.protocol.into(),
username: pattern.username.into(),
password: pattern.password.into(),
hostname: pattern.hostname.into(),
port: pattern.port.into(),
pathname: pattern.pathname.into(),
search: pattern.search.into(),
hash: pattern.hash.into(),
has_regexp_groups: pattern.has_regexp_groups,
})
}
#[op2]
#[string]
pub fn op_urlpattern_process_match_input(
#[serde] input: StringOrInit,
#[string] base_url: Option<String>,
#[buffer] buf: &mut [u32],
) -> Result<Option<String>, UrlPatternError> {
let res = quirks::process_match_input(input, base_url.as_deref())?;
let (input, _inputs) = match res {
Some((input, inputs)) => (input, inputs),
None => return Ok(None),
};
let match_input = match quirks::parse_match_input(input) {
Some(mi) => mi,
None => return Ok(None),
};
let fields = [
&match_input.protocol,
&match_input.username,
&match_input.password,
&match_input.hostname,
&match_input.port,
&match_input.pathname,
&match_input.search,
&match_input.hash,
];
let total_len: usize = fields.iter().map(|f| f.len()).sum();
let mut concat = String::with_capacity(total_len);
let mut offset = 0u32;
for (i, field) in fields.iter().enumerate() {
buf[i] = offset;
offset += field.len() as u32;
concat.push_str(field);
}
buf[8] = offset;
Ok(Some(concat))
}