char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport URLPattern extension to typescript5553f00

main
6.2 KiB150 linesraw
1use std::convert::Infallible;
2
3use avril_core::{Extension, Op, v8};
4use scoped_tls_hkt::scoped_thread_local;
5use urlpattern::{RegexSyntax, UrlPatternOptions, quirks, regexp::RegExp};
6
7// The parser's RegExp trait has no context parameter. Borrow the current op's evaluator
8// only for the synchronous parse, without retaining V8 handles or erasing scope lifetimes.
9type Evaluator<'a> = dyn FnMut(&str, &str, Option<&str>) -> Result<bool, ()> + 'a;
10scoped_thread_local!(static mut EVALUATOR: for<'a> &'a mut Evaluator<'a>);
11
12struct JsRegExp {
13    source: String,
14    flags: String,
15}
16
17impl RegExp for JsRegExp {
18    fn syntax() -> RegexSyntax {
19        RegexSyntax::EcmaScript
20    }
21
22    fn parse(source: &str, flags: &str, _force_eval: bool) -> Result<Self, ()> {
23        EVALUATOR.with(|evaluate| evaluate(source, flags, None))?;
24        Ok(Self {
25            source: source.into(),
26            flags: flags.into(),
27        })
28    }
29
30    fn matches<'a>(&self, text: &'a str) -> Option<Vec<Option<&'a str>>> {
31        // During parsing, only the existence of a protocol match is used.
32        EVALUATOR
33            .with(|evaluate| evaluate(&self.source, &self.flags, Some(text)))
34            .ok()?
35            .then(Vec::new)
36    }
37
38    fn pattern_string(&self) -> &str {
39        &self.source
40    }
41}
42
43pub fn init() -> Extension {
44    Extension {
45        name: "ext:urlpattern",
46        dependencies: &["ext:web"],
47        ops: vec![
48            Op::with_scope(
49                "op_urlpattern_parse",
50                |scope,
51                 (input, base, ignore_case): (
52                    quirks::StringOrInit<'static>,
53                    Option<String>,
54                    bool,
55                )| {
56                    let mut evaluate = |source: &str, flags: &str, text: Option<&str>| {
57                        v8::tc_scope!(let scope, scope);
58                        let source = v8::String::new(scope, source).ok_or(())?;
59                        let mut options = v8::RegExpCreationFlags::UNICODE_SETS;
60                        if flags.contains('i') {
61                            options |= v8::RegExpCreationFlags::IGNORE_CASE;
62                        }
63                        let regex = v8::RegExp::new(scope, source, options).ok_or(())?;
64                        // V8's Exec API otherwise dispatches through the public exec property.
65                        let null = v8::null(scope).into();
66                        regex.set_prototype(scope, null).ok_or(())?;
67                        match text {
68                            Some(text) => {
69                                let text = v8::String::new(scope, text).ok_or(())?;
70                                Ok(regex
71                                    .exec(scope, text)
72                                    .is_some_and(|result| !result.is_null()))
73                            }
74                            None => Ok(true),
75                        }
76                    };
77                    let parsed = EVALUATOR.set(&mut evaluate, || {
78                        let init = match input {
79                            quirks::StringOrInit::String(input) => {
80                                let base = base
81                                    .as_deref()
82                                    .map(url::Url::parse)
83                                    .transpose()
84                                    .map_err(urlpattern::Error::Url)?;
85                                urlpattern::UrlPatternInit::parse_constructor_string::<JsRegExp>(
86                                    &input, base,
87                                )?
88                            }
89                            input => {
90                                quirks::process_construct_pattern_input(input, base.as_deref())?
91                            }
92                        };
93                        urlpattern::UrlPattern::<JsRegExp>::parse(
94                            init,
95                            UrlPatternOptions {
96                                regex_syntax: RegexSyntax::EcmaScript,
97                                ignore_case,
98                            },
99                        )
100                    });
101                    let parsed = parsed.and_then(|pattern| {
102                        let has_regexp_groups = pattern.has_regexp_groups();
103                        let components = [
104                            pattern.protocol,
105                            pattern.username,
106                            pattern.password,
107                            pattern.hostname,
108                            pattern.port,
109                            pattern.pathname,
110                            pattern.search,
111                            pattern.hash,
112                        ]
113                        .into_iter()
114                        .map(|c| {
115                            let regex = c.regexp?;
116                            let flags = if regex.flags.contains('i') { "vi" } else { "v" };
117                            Ok((c.pattern_string, regex.source, c.group_name_list, flags))
118                        })
119                        .collect::<Result<Vec<_>, urlpattern::Error>>()?;
120                        Ok((components, has_regexp_groups))
121                    });
122                    let (pattern, error) = match parsed {
123                        Ok(pattern) => (Some(pattern), None),
124                        Err(error) => (None, Some(error.to_string())),
125                    };
126                    Ok::<_, Infallible>((pattern, error))
127                },
128            ),
129            Op::new(
130                "op_urlpattern_match_input",
131                |(input, base): (quirks::StringOrInit<'static>, Option<String>)| {
132                    let parsed = quirks::process_match_input(input, base.as_deref());
133                    let (input, error) = match parsed {
134                        Ok(input) => (
135                            input.and_then(|(input, _)| quirks::parse_match_input(input)),
136                            None,
137                        ),
138                        Err(error) => (None, Some(error.to_string())),
139                    };
140                    Ok::<_, Infallible>((input, error))
141                },
142            ),
143        ],
144        modules: &[(
145            "ext:urlpattern/main.js",
146            include_str!(concat!(env!("OUT_DIR"), "/urlpattern.js")),
147        )],
148        entry_point: Some("ext:urlpattern/main.js"),
149    }
150}