next_core/next_edge/
route_regex.rs

1//! The following code was mostly generated using GTP-4 from
2//! next.js/packages/next/src/shared/lib/router/utils/route-regex.ts
3
4use once_cell::sync::Lazy;
5use regex::Regex;
6use rustc_hash::FxHashMap;
7
8const INTERCEPTION_ROUTE_MARKERS: [&str; 4] = ["(..)(..)", "(.)", "(..)", "(...)"];
9const NEXT_QUERY_PARAM_PREFIX: &str = "nxtP";
10const NEXT_INTERCEPTION_MARKER_PREFIX: &str = "nxtI";
11
12#[derive(Debug, Clone)]
13pub struct Group {
14    pub pos: usize,
15    pub repeat: bool,
16    pub optional: bool,
17}
18
19#[derive(Debug)]
20pub struct RouteRegex {
21    pub groups: FxHashMap<String, Group>,
22    pub regex: String,
23}
24
25#[derive(Debug)]
26pub struct NamedRouteRegex {
27    pub regex: RouteRegex,
28    pub named_regex: String,
29    pub route_keys: FxHashMap<String, String>,
30}
31
32#[derive(Debug)]
33pub struct NamedMiddlewareRegex {
34    pub named_regex: String,
35}
36
37struct ParsedParameter {
38    key: String,
39    repeat: bool,
40    optional: bool,
41}
42
43/// Parses a given parameter from a route to a data structure that can be used
44/// to generate the parametrized route. Examples:
45///   - `[...slug]` -> `{ key: 'slug', repeat: true, optional: true }`
46///   - `...slug` -> `{ key: 'slug', repeat: true, optional: false }`
47///   - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }`
48///   - `bar` -> `{ key: 'bar', repeat: false, optional: false }`
49fn parse_parameter(param: &str) -> ParsedParameter {
50    let mut key = param.to_string();
51    let optional = key.starts_with('[') && key.ends_with(']');
52    if optional {
53        key = key[1..key.len() - 1].to_string();
54    }
55    let repeat = key.starts_with("...");
56    if repeat {
57        key = key[3..].to_string();
58    }
59    ParsedParameter {
60        key,
61        repeat,
62        optional,
63    }
64}
65
66fn escape_string_regexp(segment: &str) -> String {
67    regex::escape(segment)
68}
69
70/// Removes the trailing slash for a given route or page path. Preserves the
71/// root page. Examples:
72///  - `/foo/bar/` -> `/foo/bar`
73///  - `/foo/bar` -> `/foo/bar`
74///  - `/` -> `/`
75fn remove_trailing_slash(route: &str) -> &str {
76    if route == "/" {
77        route
78    } else {
79        route.trim_end_matches('/')
80    }
81}
82
83static PARAM_MATCH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
84
85fn get_parametrized_route(route: &str) -> (String, FxHashMap<String, Group>) {
86    let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
87    let mut groups: FxHashMap<String, Group> = FxHashMap::default();
88    let mut group_index = 1;
89    let parameterized_route = segments
90        .iter()
91        .map(|segment| {
92            let marker_match = INTERCEPTION_ROUTE_MARKERS
93                .iter()
94                .find(|&&m| segment.starts_with(m))
95                .copied();
96            let param_matches = PARAM_MATCH_REGEX.captures(segment);
97            if let Some(matches) = param_matches {
98                let ParsedParameter {
99                    key,
100                    optional,
101                    repeat,
102                } = parse_parameter(&matches[1]);
103                groups.insert(
104                    key,
105                    Group {
106                        pos: group_index,
107                        repeat,
108                        optional,
109                    },
110                );
111                group_index += 1;
112                if let Some(marker) = marker_match {
113                    return format!("/{}([^/]+?)", escape_string_regexp(marker));
114                } else {
115                    return match (repeat, optional) {
116                        (true, true) => "(?:/(.+?))?",
117                        (true, false) => "/(.+?)",
118                        (false, true) => "(?:/([^/]+?))?",
119                        (false, false) => "/([^/]+?)",
120                    }
121                    .to_string();
122                }
123            }
124            format!("/{}", escape_string_regexp(segment))
125        })
126        .collect::<Vec<String>>()
127        .join("");
128    (parameterized_route, groups)
129}
130
131/// From a normalized route this function generates a regular expression and
132/// a corresponding groups object intended to be used to store matching groups
133/// from the regular expression.
134pub fn get_route_regex(normalized_route: &str) -> RouteRegex {
135    let (parameterized_route, groups) = get_parametrized_route(normalized_route);
136    RouteRegex {
137        regex: format!("^{parameterized_route}(?:/)?$"),
138        groups,
139    }
140}
141
142/// Builds a function to generate a minimal routeKey using only a-z and minimal
143/// number of characters.
144fn build_get_safe_route_key() -> impl FnMut() -> String {
145    let mut i = 0;
146
147    move || {
148        let mut route_key = String::new();
149        i += 1;
150        let mut j = i;
151
152        while j > 0 {
153            route_key.push((97 + ((j - 1) % 26)) as u8 as char);
154            j = (j - 1) / 26;
155        }
156
157        i += 1;
158        route_key
159    }
160}
161
162fn get_safe_key_from_segment(
163    get_safe_route_key: &mut impl FnMut() -> String,
164    segment: &str,
165    route_keys: &mut FxHashMap<String, String>,
166    key_prefix: Option<&'static str>,
167) -> String {
168    let ParsedParameter {
169        key,
170        optional,
171        repeat,
172    } = parse_parameter(segment);
173
174    // replace any non-word characters since they can break
175    // the named regex
176    let mut cleaned_key = key.replace(|c: char| !c.is_alphanumeric(), "");
177    if let Some(prefix) = key_prefix {
178        cleaned_key = format!("{prefix}{cleaned_key}");
179    }
180    let mut invalid_key = false;
181
182    // check if the key is still invalid and fallback to using a known
183    // safe key
184    if cleaned_key.is_empty() || cleaned_key.len() > 30 {
185        invalid_key = true;
186    }
187    if cleaned_key.chars().next().unwrap().is_numeric() {
188        invalid_key = true;
189    }
190    if invalid_key {
191        cleaned_key = get_safe_route_key();
192    }
193    if let Some(prefix) = key_prefix {
194        route_keys.insert(cleaned_key.clone(), format!("{prefix}{key}"));
195    } else {
196        route_keys.insert(cleaned_key.clone(), key);
197    }
198    match (repeat, optional) {
199        (true, true) => format!(r"(?:/(?P<{cleaned_key}>.+?))?"),
200        (true, false) => format!(r"/(?P<{cleaned_key}>.+?)"),
201        (false, true) => format!(r"(?:/(?P<{cleaned_key}>[^/]+?))?"),
202        (false, false) => format!(r"/(?P<{cleaned_key}>[^/]+?)"),
203    }
204}
205
206fn get_named_parametrized_route(
207    route: &str,
208    prefix_route_keys: bool,
209) -> (String, FxHashMap<String, String>) {
210    let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect();
211    let get_safe_route_key = &mut build_get_safe_route_key();
212    let mut route_keys: FxHashMap<String, String> = FxHashMap::default();
213    let parameterized_route = segments
214        .iter()
215        .map(|segment| {
216            let key_prefix = if prefix_route_keys {
217                let has_interception_marker = INTERCEPTION_ROUTE_MARKERS
218                    .iter()
219                    .any(|&m| segment.starts_with(m));
220                if has_interception_marker {
221                    Some(NEXT_INTERCEPTION_MARKER_PREFIX)
222                } else {
223                    Some(NEXT_QUERY_PARAM_PREFIX)
224                }
225            } else {
226                None
227            };
228            static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap());
229            let param_matches = RE.captures(segment);
230            if let Some(matches) = param_matches {
231                return get_safe_key_from_segment(
232                    get_safe_route_key,
233                    &matches[1],
234                    &mut route_keys,
235                    key_prefix,
236                );
237            }
238            format!("/{}", escape_string_regexp(segment))
239        })
240        .collect::<Vec<String>>()
241        .join("");
242    (parameterized_route, route_keys)
243}
244
245/// This function extends `getRouteRegex` generating also a named regexp where
246/// each group is named along with a routeKeys object that indexes the assigned
247/// named group with its corresponding key.
248///
249/// When the routeKeys need to be prefixed to uniquely identify internally the
250/// "prefixRouteKey" arg should be "true" currently this is only the case when
251/// creating the routes-manifest during the build
252pub fn get_named_route_regex(normalized_route: &str) -> NamedRouteRegex {
253    let (parameterized_route, route_keys) = get_named_parametrized_route(normalized_route, false);
254    let regex = get_route_regex(normalized_route);
255    NamedRouteRegex {
256        regex,
257        named_regex: format!("^{parameterized_route}(?:/)?$"),
258        route_keys,
259    }
260}
261
262/// Generates a named regexp.
263/// This is intended to be using for build time only.
264pub fn get_named_middleware_regex(normalized_route: &str) -> String {
265    let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true);
266    format!("^{parameterized_route}(?:/)?$")
267}