Skip to main content

ferrovue_router/
lib.rs

1//! Vue Router's location matching, for `<RouterLink>` and `useRoute()` on the server: part of
2//! [ferrovue](https://docs.rs/ferrovue)'s runtime.
3//!
4//! # Example
5//!
6//! ```
7//! # use ferrovue_router as ferrovue;
8//! let router = ferrovue::Router::new(&["/", "/blog/:slug"]);
9//! let route = router.at("/blog/hello");
10//! let link = route.link("/blog/hello");
11//! assert_eq!((link.href.as_str(), link.active, link.exact), ("/blog/hello", true, true));
12//! assert!(!route.link("/").active);
13//! ```
14#![warn(rustdoc::missing_crate_level_docs)]
15
16/// The routes, in the order vue-router tries them.
17///
18/// # Example
19///
20/// ```
21/// # use ferrovue_router as ferrovue;
22/// use ferrovue::Router;
23///
24/// let router = Router::new(&["/", "/blog/:slug", "/docs/:path(.*)"]);
25/// let route = router.at("/docs/guide/routing");
26/// assert_eq!(route.param("path"), Some("guide/routing"));
27///
28/// // One router serves every request, from any thread.
29/// fn shared<T: Send + Sync>(_: &T) {}
30/// shared(&router);
31/// ```
32pub struct Router {
33    routes: Vec<Pattern>,
34    base: String,
35}
36
37/// The base, and each route's full path with its name, in the order vue-router tries them.
38///
39/// # Example
40///
41/// ```
42/// # use ferrovue_router as ferrovue;
43/// let router = ferrovue::Router::named(&[("/", Some("home")), ("/users/:id", None)]).with_base("/app/");
44/// assert_eq!(
45///     format!("{router:?}"),
46///     r#"Router { base: "/app", routes: {"/": Some("home"), "/users/:id": None} }"#
47/// );
48/// ```
49impl std::fmt::Debug for Router {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        let routes = self.routes.iter().map(|r| (&r.path, &r.name));
52        f.debug_struct("Router")
53            .field("base", &self.base)
54            .field("routes", &DebugMap(routes))
55            .finish()
56    }
57}
58
59struct DebugMap<I>(I);
60
61impl<K: std::fmt::Debug, V: std::fmt::Debug, I: Iterator<Item = (K, V)> + Clone> std::fmt::Debug
62    for DebugMap<I>
63{
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_map().entries(self.0.clone()).finish()
66    }
67}
68
69struct Pattern {
70    path: String,
71    parent: Option<usize>,
72    tokens: Vec<Token>,
73    score: Vec<i32>,
74    name: Option<String>,
75    matchable: bool,
76}
77
78enum Token {
79    Static(String),
80    Param {
81        name: String,
82        wildcard: bool,
83        optional: bool,
84    },
85}
86
87type Values = Vec<Option<String>>;
88
89const ROOT: i32 = 90;
90const STATIC: i32 = 80;
91const PARAM: i32 = 60;
92const OPTIONAL: i32 = 52;
93const WILDCARD: i32 = 20;
94
95impl Router {
96    /// A router over these paths, written as vue-router writes them: `/users/:id`, `/:lang?/about`,
97    /// `/docs/:section/:page(.*)`.
98    ///
99    /// # Panics
100    ///
101    /// On a path that does not start with `/`, a parameter that is not `:name`, `:name?` or
102    /// `:name(.*)`, a `(.*)` anywhere but the last segment, or a static segment that is not plain
103    /// ASCII text. The paths are part of the program, so this is a programming error, found the
104    /// first time the router is built.
105    ///
106    /// # Example
107    ///
108    /// ```
109    /// # use ferrovue_router as ferrovue;
110    /// let router = ferrovue::Router::new(&["/", "/users/:id"]);
111    /// assert_eq!(router.at("/users/7").param("id"), Some("7"));
112    /// assert_eq!(router.at("/nowhere").param("id"), None);
113    /// ```
114    #[must_use]
115    pub fn new(paths: &[&str]) -> Router {
116        Router::named(&paths.iter().map(|p| (*p, None)).collect::<Vec<_>>())
117    }
118
119    /// A router over these routes, each a path and, if it has one, its name.
120    ///
121    /// # Panics
122    ///
123    /// As [`Router::new`] does, and on two routes with the same name.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// # use ferrovue_router as ferrovue;
129    /// use ferrovue::Router;
130    ///
131    /// let router = Router::named(&[("/", Some("home")), ("/users/:id", Some("user"))]).with_base("/app/");
132    /// let route = router.at("/users/7?tab=posts#bio");
133    /// assert_eq!(route.param("id"), Some("7"));
134    /// assert_eq!(route.name(), Some("user"));
135    /// assert_eq!(route.hash(), "#bio");
136    ///
137    /// let link = route.link_named("user", &[("id", "7")], "", "");
138    /// assert_eq!(link.href, "/app/users/7");
139    /// assert!(link.active);
140    /// ```
141    #[must_use]
142    pub fn named(routes: &[(&str, Option<&str>)]) -> Router {
143        let defs: Vec<RouteDef<'_>> = routes
144            .iter()
145            .map(|(path, name)| RouteDef {
146                name: *name,
147                ..RouteDef::new(path)
148            })
149            .collect();
150        Router::tree(&defs)
151    }
152
153    /// A router over nested routes: each child's path is joined to its parent's, unless it starts
154    /// with `/`, and an empty one is its parent's own, its default child.
155    ///
156    /// # Panics
157    ///
158    /// As [`Router::named`] does.
159    ///
160    /// # Example
161    ///
162    /// ```
163    /// # use ferrovue_router as ferrovue;
164    /// use ferrovue::{RouteDef, Router};
165    ///
166    /// let router = Router::tree(&[RouteDef::new("/users/:id")
167    ///     .with_name("user")
168    ///     .with_children(&[RouteDef::new("posts").with_name("user-posts")])]);
169    /// let here = router.at("/users/7/posts");
170    /// let parent = here.link("/users/7");
171    /// // The parent is active on its child's page, but not exactly.
172    /// assert!(parent.active && !parent.exact);
173    /// ```
174    #[must_use]
175    pub fn tree(defs: &[RouteDef<'_>]) -> Router {
176        fn add(defs: &[RouteDef<'_>], parent: Option<usize>, records: &mut Vec<Pattern>) {
177            for d in defs {
178                let path = match parent {
179                    Some(p) if !d.path.starts_with('/') => {
180                        let parent_path = &records[p].path;
181                        let slash = if parent_path.ends_with('/') || d.path.is_empty() {
182                            ""
183                        } else {
184                            "/"
185                        };
186                        format!("{parent_path}{slash}{}", d.path)
187                    }
188                    _ => d.path.to_owned(),
189                };
190                let mut pattern = Pattern::new(&path, d.name.map(str::to_owned));
191                pattern.parent = parent;
192                pattern.matchable = d.view || d.name.is_some();
193                records.push(pattern);
194                let me = records.len() - 1;
195                add(d.children, Some(me), records);
196            }
197        }
198        let mut records: Vec<Pattern> = Vec::new();
199        add(defs, None, &mut records);
200        for (i, a) in records.iter().enumerate() {
201            if let Some(name) = &a.name {
202                let again = records[i + 1..]
203                    .iter()
204                    .any(|b| b.name.as_ref() == Some(name));
205                assert!(!again, "two routes are called {name:?}");
206            }
207        }
208        let mut order: Vec<usize> = Vec::new();
209        for i in (0..records.len()).filter(|&i| records[i].matchable) {
210            let (mut lower, mut upper) = (0, order.len());
211            while lower != upper {
212                let mid = (lower + upper) / 2;
213                if compare(&records[i].score, &records[order[mid]].score)
214                    == std::cmp::Ordering::Less
215                {
216                    upper = mid;
217                } else {
218                    lower = mid + 1;
219                }
220            }
221            let mut ancestor = records[i].parent;
222            while let Some(a) = ancestor {
223                if records[a].matchable
224                    && compare(&records[i].score, &records[a].score) == std::cmp::Ordering::Equal
225                {
226                    if let Some(at) = order[..upper].iter().rposition(|&x| x == a) {
227                        upper = at;
228                    }
229                    break;
230                }
231                ancestor = records[a].parent;
232            }
233            order.insert(upper, i);
234        }
235        order.extend((0..records.len()).filter(|&i| !records[i].matchable));
236        let mut position = vec![0; records.len()];
237        for (at, &i) in order.iter().enumerate() {
238            position[i] = at;
239        }
240        let mut slots: Vec<Option<Pattern>> = records.into_iter().map(Some).collect();
241        let routes = order
242            .iter()
243            .map(|&i| {
244                let mut p = slots[i].take().expect("each record once");
245                p.parent = p.parent.map(|parent| position[parent]);
246                p
247            })
248            .collect();
249        Router {
250            routes,
251            base: String::new(),
252        }
253    }
254
255    fn chain(&self, mut i: usize) -> Vec<usize> {
256        let mut chain = vec![i];
257        while let Some(p) = self.routes[i].parent {
258            chain.push(p);
259            i = p;
260        }
261        chain.reverse();
262        chain
263    }
264
265    fn params<'a>(&'a self, i: usize, values: &'a [Option<String>]) -> Vec<(&'a str, &'a str)> {
266        self.routes[i]
267            .tokens
268            .iter()
269            .filter_map(|t| match t {
270                Token::Param { name, .. } => Some(name.as_str()),
271                Token::Static(_) => None,
272            })
273            .zip(values)
274            .filter_map(|(name, value)| Some((name, value.as_deref()?)))
275            .collect()
276    }
277
278    /// The same routes under a base path, as `createWebHistory(base)` serves them: every `href`
279    /// starts with it. `/app/` and `app` both mean `/app`.
280    ///
281    /// # Example
282    ///
283    /// ```
284    /// # use ferrovue_router as ferrovue;
285    /// let router = ferrovue::Router::new(&["/", "/blog/:slug"]).with_base("/app/");
286    /// // A request for /app/blog/intro, with the base taken off.
287    /// let route = router.at("/blog/intro");
288    /// let link = route.link("/blog/intro");
289    /// assert_eq!(link.href, "/app/blog/intro");
290    /// assert!(link.exact);
291    /// ```
292    #[must_use]
293    pub fn with_base(mut self, base: &str) -> Router {
294        let mut b = if base.is_empty() || base.starts_with('/') || base.starts_with('#') {
295            base.to_owned()
296        } else {
297            format!("/{base}")
298        };
299        if b.ends_with('/') {
300            b.pop();
301        }
302        if let Some(i) = b.find('#')
303            && i > 0
304        {
305            b = b[i..].to_owned();
306        }
307        self.base = b;
308        self
309    }
310
311    /// The reader's location: a path, with any query and hash, and without the history's base.
312    ///
313    /// Pass the request's path and query as they arrived, percent-encoding included. A location
314    /// that matches no route still gives a [`Route`], whose [`Route::name`] and [`Route::param`]
315    /// are `None` and whose links are never active.
316    ///
317    /// # Example
318    ///
319    /// ```
320    /// # use ferrovue_router as ferrovue;
321    /// let router = ferrovue::Router::new(&["/", "/search"]);
322    /// let route = router.at("/search?q=caf%C3%A9&page=2#results");
323    /// assert_eq!(route.path(), "/search");
324    /// assert_eq!(route.query("q").attr_value(), Some("café"));
325    /// assert_eq!(route.hash(), "#results");
326    /// ```
327    #[must_use]
328    pub fn at(&self, location: &str) -> Route<'_> {
329        let (path, full_path) = parse_url(location, "/");
330        let hash_pos = location.find('#');
331        let hash = hash_pos.map_or_else(String::new, |h| decode(&location[h..]));
332        let search = location
333            .find('?')
334            .filter(|&s| hash_pos.is_none_or(|h| s < h))
335            .map_or("", |s| {
336                &location[s + 1..hash_pos.filter(|&h| h > 0).unwrap_or(location.len())]
337            });
338        Route {
339            router: self,
340            matched: self.matched(&path),
341            path,
342            hash,
343            query: parse_query(search),
344            full_path,
345        }
346    }
347
348    fn matched(&self, path: &str) -> Option<(usize, Values)> {
349        self.routes
350            .iter()
351            .enumerate()
352            .filter(|(_, r)| r.matchable)
353            .find_map(|(i, r)| r.parse(path).map(|params| (i, params)))
354    }
355}
356
357/// A route as the routes file lists it: a path, its name if it has one, whether it shows a
358/// component, and the routes nested in it, rendered by the `<RouterView>` in its component.
359///
360/// Build one with [`RouteDef::new`] and its `with_` methods, which are `const`, so a route table
361/// can be a `const` or a `static`.
362///
363/// # Example
364///
365/// ```
366/// # use ferrovue_router as ferrovue;
367/// use ferrovue::{RouteDef, Router};
368///
369/// const ROUTES: &[RouteDef<'static>] = &[
370///     RouteDef::new("/").with_name("home"),
371///     RouteDef::new("/books").with_view(false).with_children(&[
372///         RouteDef::new(":id").with_name("book"),
373///     ]),
374/// ];
375///
376/// let router = Router::tree(ROUTES);
377/// assert_eq!(router.at("/books/dune").name(), Some("book"));
378/// assert_eq!(router.at("/books").name(), None);
379/// ```
380#[derive(Clone, Copy, Debug)]
381#[non_exhaustive]
382pub struct RouteDef<'a> {
383    /// The path, relative to the parent's unless it starts with `/`; empty for a default child.
384    pub path: &'a str,
385    /// The route's name.
386    pub name: Option<&'a str>,
387    /// Whether it shows a component. A route with neither a view nor a name only groups the routes
388    /// nested in it, as a folder of file-based routes does, and vue-router never matches it.
389    pub view: bool,
390    /// The routes nested in it.
391    pub children: &'a [RouteDef<'a>],
392}
393
394impl<'a> RouteDef<'a> {
395    /// A route at `path` that shows a component, with no name and no nested routes.
396    ///
397    /// # Example
398    ///
399    /// ```
400    /// # use ferrovue_router as ferrovue;
401    /// let def = ferrovue::RouteDef::new("/about");
402    /// assert_eq!(def.path, "/about");
403    /// assert_eq!(def.name, None);
404    /// assert!(def.view && def.children.is_empty());
405    /// ```
406    #[must_use]
407    pub const fn new(path: &'a str) -> RouteDef<'a> {
408        RouteDef {
409            path,
410            name: None,
411            view: true,
412            children: &[],
413        }
414    }
415
416    /// The route, named `name`.
417    ///
418    /// # Example
419    ///
420    /// ```
421    /// # use ferrovue_router as ferrovue;
422    /// let def = ferrovue::RouteDef::new("/users/:id").with_name("user");
423    /// assert_eq!(def.name, Some("user"));
424    /// ```
425    #[must_use]
426    pub const fn with_name(self, name: &'a str) -> RouteDef<'a> {
427        RouteDef {
428            name: Some(name),
429            ..self
430        }
431    }
432
433    /// The route, showing a component when `view` is true. A route with neither a view nor a name
434    /// only groups the routes nested in it.
435    ///
436    /// # Example
437    ///
438    /// ```
439    /// # use ferrovue_router as ferrovue;
440    /// let def = ferrovue::RouteDef::new("/books").with_view(false);
441    /// assert!(!def.view);
442    /// ```
443    #[must_use]
444    pub const fn with_view(self, view: bool) -> RouteDef<'a> {
445        RouteDef { view, ..self }
446    }
447
448    /// The route, with `children` nested in it.
449    ///
450    /// # Example
451    ///
452    /// ```
453    /// # use ferrovue_router as ferrovue;
454    /// use ferrovue::RouteDef;
455    ///
456    /// const USER: RouteDef<'static> = RouteDef::new("/users/:id").with_children(&[
457    ///     RouteDef::new(""),
458    ///     RouteDef::new("posts"),
459    /// ]);
460    /// assert_eq!(USER.children[1].path, "posts");
461    /// ```
462    #[must_use]
463    pub const fn with_children(self, children: &'a [RouteDef<'a>]) -> RouteDef<'a> {
464        RouteDef { children, ..self }
465    }
466}
467
468/// Where the reader is, which is what every link on the page is compared with.
469///
470/// # Example
471///
472/// ```
473/// # use ferrovue_router as ferrovue;
474/// let router = ferrovue::Router::named(&[("/", Some("home")), ("/blog/:slug", Some("post"))]);
475/// let route = router.at("/blog/intro?sort=new#comments");
476///
477/// assert_eq!(route.name(), Some("post"));
478/// assert_eq!(route.param("slug"), Some("intro"));
479/// assert_eq!(route.path(), "/blog/intro");
480/// assert_eq!(route.full_path(), "/blog/intro?sort=new#comments");
481/// assert!(route.query("sort").is("new"));
482/// assert_eq!(route.hash(), "#comments");
483///
484/// // What `<RouterLink to="/">` writes from here.
485/// let home = route.link("/");
486/// assert_eq!(home.href, "/");
487/// assert!(!home.active);
488/// ```
489pub struct Route<'r> {
490    router: &'r Router,
491    path: String,
492    hash: String,
493    matched: Option<(usize, Values)>,
494    query: Vec<(String, Vec<Option<String>>)>,
495    full_path: String,
496}
497
498/// One value of `route.query`, as vue-router parses it: a key may be absent, given without a value
499/// (`?flag`, which is `null`), given once, or given more than once, which makes an array.
500///
501/// # Example
502///
503/// ```
504/// # use ferrovue_router as ferrovue;
505/// use ferrovue::{Query, Router};
506///
507/// let router = Router::new(&["/search"]);
508/// let route = router.at("/search?q=rust&tag=a&tag=b&flag");
509///
510/// assert_eq!(route.query("q"), Query::One("rust"));
511/// assert_eq!(route.query("flag"), Query::Null);
512/// assert_eq!(route.query("nothing"), Query::Absent);
513/// assert!(route.query("tag").is_array());
514/// ```
515#[derive(Clone, Copy, Debug, PartialEq)]
516#[non_exhaustive]
517pub enum Query<'r> {
518    /// No such key: `undefined`.
519    Absent,
520    /// `?flag`: `null`.
521    Null,
522    /// `?q=text`, decoded.
523    One(&'r str),
524    /// `?q=a&q=b`: an array, in which a value written without `=` is `null`.
525    Many(&'r [Option<String>]),
526}
527
528impl<'r> Query<'r> {
529    /// `{{ route.query.q }}`, escaped: nothing for `undefined` and `null`, the text for a string,
530    /// and an array as `toDisplayString` writes one: `JSON.stringify(value, null, 2)`.
531    ///
532    /// # Example
533    ///
534    /// ```
535    /// # use ferrovue_router as ferrovue;
536    /// let router = ferrovue::Router::new(&["/search"]);
537    /// let route = router.at("/search?q=a%3Cb&tag=x&tag=y");
538    ///
539    /// let mut out = String::new();
540    /// route.query("q").write_display(&mut out);
541    /// assert_eq!(out, "a&lt;b");
542    ///
543    /// out.clear();
544    /// route.query("tag").write_display(&mut out);
545    /// assert_eq!(out, "[\n  &quot;x&quot;,\n  &quot;y&quot;\n]");
546    /// ```
547    pub fn write_display(&self, out: &mut String) {
548        match self {
549            Query::Absent | Query::Null => {}
550            Query::One(s) => ferrovue_core::escape_into(out, s),
551            Query::Many(values) => {
552                let mut json = String::from("[\n");
553                for (i, v) in values.iter().enumerate() {
554                    if i > 0 {
555                        json.push_str(",\n");
556                    }
557                    json.push_str("  ");
558                    match v {
559                        Some(s) => json.push_str(&serde_json::to_string(s).unwrap_or_default()),
560                        None => json.push_str("null"),
561                    }
562                }
563                json.push_str("\n]");
564                ferrovue_core::escape_into(out, &json);
565            }
566        }
567    }
568
569    /// JavaScript's truthiness: a non-empty string or an array.
570    ///
571    /// # Example
572    ///
573    /// ```
574    /// # use ferrovue_router as ferrovue;
575    /// use ferrovue::Query;
576    ///
577    /// assert!(Query::One("x").truthy());
578    /// assert!(!Query::One("").truthy());
579    /// assert!(!Query::Null.truthy() && !Query::Absent.truthy());
580    /// ```
581    #[must_use]
582    pub fn truthy(&self) -> bool {
583        match self {
584            Query::One(s) => !s.is_empty(),
585            Query::Many(_) => true,
586            Query::Absent | Query::Null => false,
587        }
588    }
589
590    /// `route.query.q === text`: only a single value can equal a string.
591    #[must_use]
592    pub fn is(&self, text: &str) -> bool {
593        matches!(self, Query::One(s) if *s == text)
594    }
595
596    /// `route.query.q === undefined`.
597    #[must_use]
598    pub fn is_undefined(&self) -> bool {
599        matches!(self, Query::Absent)
600    }
601
602    /// `Array.isArray(route.query.q)`.
603    #[must_use]
604    pub fn is_array(&self) -> bool {
605        matches!(self, Query::Many(_))
606    }
607
608    /// `route.query.q ?? fallback`: the fallback for `undefined` and `null`.
609    ///
610    /// # Example
611    ///
612    /// ```
613    /// # use ferrovue_router as ferrovue;
614    /// use ferrovue::Query;
615    ///
616    /// assert_eq!(Query::Absent.or("none"), Query::One("none"));
617    /// assert_eq!(Query::One("").or("none"), Query::One("")); // `??` keeps the empty string
618    /// ```
619    #[must_use]
620    pub fn or<'a>(self, fallback: &'a str) -> Query<'a>
621    where
622        'r: 'a,
623    {
624        match self {
625            Query::Absent | Query::Null => Query::One(fallback),
626            other => other,
627        }
628    }
629
630    /// The single string, when the value is one: `typeof route.query.q === "string"`, which narrows
631    /// it to what an attribute can be bound to. `null`, `undefined` and an array are `None`.
632    ///
633    /// # Example
634    ///
635    /// ```
636    /// # use ferrovue_router as ferrovue;
637    /// use ferrovue::Query;
638    ///
639    /// assert_eq!(Query::One("rust").attr_value(), Some("rust"));
640    /// assert_eq!(Query::Null.attr_value(), None);
641    /// ```
642    #[must_use]
643    pub fn attr_value(&self) -> Option<&'r str> {
644        match self {
645            Query::One(s) => Some(s),
646            _ => None,
647        }
648    }
649}
650
651fn parse_query(search: &str) -> Vec<(String, Vec<Option<String>>)> {
652    let mut query: Vec<(String, Vec<Option<String>>)> = Vec::new();
653    if search.is_empty() {
654        return query;
655    }
656    for pair in search.split('&') {
657        let pair = pair.replace('+', " ");
658        let (key, value) = match pair.find('=') {
659            Some(i) => (decode(&pair[..i]), Some(decode(&pair[i + 1..]))),
660            None => (decode(&pair), None),
661        };
662        match query.iter_mut().find(|(k, _)| *k == key) {
663            Some((_, values)) => values.push(value),
664            None => query.push((key, vec![value])),
665        }
666    }
667    query
668}
669
670/// A `<RouterLink>`'s `to`, resolved: what [`Route::link`], [`Route::link_named`] and
671/// [`Route::link_path`] return.
672///
673/// The `href` is not escaped.
674///
675/// # Example
676///
677/// ```
678/// # use ferrovue_router as ferrovue;
679/// let router = ferrovue::Router::new(&["/", "/blog/:slug"]);
680/// let link = router.at("/blog/intro").link("/blog/intro#top");
681/// assert_eq!(link.href, "/blog/intro#top");
682/// assert!(link.active && link.exact);
683/// ```
684#[derive(Debug, Clone, PartialEq, Eq)]
685#[non_exhaustive]
686pub struct Link {
687    /// What the anchor's `href` is.
688    pub href: String,
689    /// `isActive`: it points at the route the reader is on, or one the reader's route is nested in,
690    /// with parameters the reader's location has too. The `router-link-active` class.
691    pub active: bool,
692    /// `isExactActive`: it points at exactly the reader's route, with the same parameters. The
693    /// `router-link-exact-active` class, and `aria-current`.
694    pub exact: bool,
695}
696
697/// The location as written, and the route it matched with its parameters; the router it borrows
698/// is left out.
699///
700/// # Example
701///
702/// ```
703/// # use ferrovue_router as ferrovue;
704/// let router = ferrovue::Router::named(&[("/blog/:slug", Some("post"))]);
705/// assert_eq!(
706///     format!("{:?}", router.at("/blog/intro?sort=new#comments")),
707///     r#"Route { full_path: "/blog/intro?sort=new#comments", name: Some("post"), params: {"slug": "intro"}, .. }"#
708/// );
709/// assert_eq!(
710///     format!("{:?}", router.at("/nowhere")),
711///     r#"Route { full_path: "/nowhere", name: None, params: {}, .. }"#
712/// );
713/// ```
714impl std::fmt::Debug for Route<'_> {
715    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716        let params = match &self.matched {
717            Some((i, values)) => self.router.params(*i, values),
718            None => Vec::new(),
719        };
720        f.debug_struct("Route")
721            .field("full_path", &self.full_path)
722            .field("name", &self.name())
723            .field("params", &DebugMap(params.into_iter()))
724            .finish_non_exhaustive()
725    }
726}
727
728impl Route<'_> {
729    /// The location's path, without its query or hash.
730    #[must_use]
731    pub fn path(&self) -> &str {
732        &self.path
733    }
734
735    /// `route.hash`: the location's hash, with its `#`, decoded; empty when there is none.
736    #[must_use]
737    pub fn hash(&self) -> &str {
738        &self.hash
739    }
740
741    /// `route.query.<key>`.
742    #[must_use]
743    pub fn query(&self, key: &str) -> Query<'_> {
744        match self.query.iter().find(|(k, _)| k == key) {
745            None => Query::Absent,
746            Some((_, values)) => match values.as_slice() {
747                [None] => Query::Null,
748                [Some(v)] => Query::One(v),
749                many => Query::Many(many),
750            },
751        }
752    }
753
754    /// `route.fullPath`: the path, then the query and hash as they were written.
755    #[must_use]
756    pub fn full_path(&self) -> &str {
757        &self.full_path
758    }
759
760    /// `route.name`: the name of the route the location matched, if it has one.
761    #[must_use]
762    pub fn name(&self) -> Option<&str> {
763        let (i, _) = self.matched.as_ref()?;
764        self.router.routes[*i].name.as_deref()
765    }
766
767    /// `route.params.<name>`: that parameter of the route the location matched, decoded; `None`
768    /// when the route has no such parameter, an optional one the location leaves out, or the
769    /// location matched no route.
770    #[must_use]
771    pub fn param(&self, name: &str) -> Option<&str> {
772        let (i, values) = self.matched.as_ref()?;
773        let at = self.router.routes[*i]
774            .tokens
775            .iter()
776            .filter_map(|t| match t {
777                Token::Param { name, .. } => Some(name.as_str()),
778                Token::Static(_) => None,
779            })
780            .position(|n| n == name)?;
781        values.get(at)?.as_deref()
782    }
783
784    /// Resolve a link from here: a string `to`, absolute or relative to the current path, with any
785    /// query and hash, which are kept as written.
786    ///
787    /// # Example
788    ///
789    /// ```
790    /// # use ferrovue_router as ferrovue;
791    /// let router = ferrovue::Router::new(&["/", "/blog/:slug"]);
792    /// let here = router.at("/blog/intro");
793    /// let link = here.link("/blog/intro?sort=new");
794    /// assert_eq!(link.href, "/blog/intro?sort=new");
795    /// assert!(link.active, "the query does not change which route it is");
796    /// assert!(!here.link("/").active);
797    /// ```
798    #[must_use]
799    pub fn link(&self, to: &str) -> Link {
800        let (path, full) = parse_url(to, &self.path);
801        let (active, exact) = self.state(self.router.matched(&path));
802        Link {
803            href: format!("{}{full}", self.router.base),
804            active,
805            exact,
806        }
807    }
808
809    fn state(&self, target: Option<(usize, Values)>) -> (bool, bool) {
810        let (Some((t, link_values)), Some((c, here_values))) = (target, self.matched.as_ref())
811        else {
812            return (false, false);
813        };
814        let router = self.router;
815        let link_chain = router.chain(t);
816        let here_chain = router.chain(*c);
817        let routed = *link_chain.last().expect("a chain holds its route");
818        let mut index = here_chain.iter().position(|&r| r == routed);
819        if index.is_none() && link_chain.len() > 1 {
820            let parent = link_chain[link_chain.len() - 2];
821            let parent_path = &router.routes[parent].path;
822            let here_last = *here_chain.last().expect("a chain holds its route");
823            if router.routes[routed].path == *parent_path
824                && router.routes[here_last].path != *parent_path
825            {
826                index = here_chain.iter().position(|&r| r == parent);
827            }
828        }
829        let Some(index) = index else {
830            return (false, false);
831        };
832        let link_params = router.params(t, &link_values);
833        let here_params = router.params(*c, here_values);
834        let includes = link_params
835            .iter()
836            .all(|(k, v)| here_params.iter().any(|(hk, hv)| hk == k && hv == v));
837        let same = includes && link_params.len() == here_params.len();
838        (includes, index == here_chain.len() - 1 && same)
839    }
840
841    /// Resolve `{ name, params, query, hash }`: the named route's path with each parameter
842    /// encoded, then `search` (a query [`query_into`] built, without its `?`) and `hash`.
843    ///
844    /// An optional parameter left out of `params` is left out of the path, unless the route is
845    /// nested in one that has it optional and the current location gives it, which vue-router
846    /// then keeps.
847    ///
848    /// # Panics
849    ///
850    /// On a name no route has. The compiler checks the names a template uses against the routes.
851    /// A debug build also panics on a required parameter that is missing or empty, as vue-router
852    /// throws on one; a release build writes the link without it.
853    ///
854    /// # Example
855    ///
856    /// ```
857    /// # use ferrovue_router as ferrovue;
858    /// let router = ferrovue::Router::named(&[("/", Some("home")), ("/books/:id", Some("book"))]);
859    /// let route = router.at("/books/dune");
860    ///
861    /// // <RouterLink :to="{ name: 'book', params: { id: 'dune' }, query: { tab: 'reviews' }, hash: '#top' }">
862    /// let mut search = String::new();
863    /// ferrovue::query_into(&mut search, "tab", "reviews");
864    /// let link = route.link_named("book", &[("id", "dune")], &search, "#top");
865    /// assert_eq!(link.href, "/books/dune?tab=reviews#top");
866    /// assert!(link.active && link.exact);
867    ///
868    /// // Parameters are encoded as vue-router encodes them.
869    /// assert_eq!(route.link_named("book", &[("id", "a/b c")], "", "").href, "/books/a%2Fb%20c");
870    /// ```
871    #[must_use]
872    pub fn link_named(
873        &self,
874        name: &str,
875        params: &[(&str, &str)],
876        search: &str,
877        hash: &str,
878    ) -> Link {
879        let i = self
880            .router
881            .routes
882            .iter()
883            .position(|r| r.name.as_deref() == Some(name))
884            .unwrap_or_else(|| panic!("no route is called {name:?}"));
885        let router = self.router;
886        let route = &router.routes[i];
887        let current = match &self.matched {
888            Some((c, values)) => router.params(*c, values),
889            None => Vec::new(),
890        };
891        let inherited: Vec<&str> = route.parent.map_or_else(Vec::new, |p| {
892            router.routes[p]
893                .tokens
894                .iter()
895                .filter_map(|t| match t {
896                    Token::Param {
897                        name,
898                        optional: true,
899                        ..
900                    } => Some(name.as_str()),
901                    _ => None,
902                })
903                .collect()
904        });
905        let mut path = String::new();
906        let mut values = Vec::new();
907        for token in &route.tokens {
908            path.push('/');
909            match token {
910                Token::Static(text) => path.push_str(text),
911                Token::Param { name, optional, .. } => {
912                    let given = params.iter().find(|(k, _)| k == name).map(|(_, v)| *v);
913                    let value = given.or_else(|| {
914                        let pick = !optional || inherited.contains(&name.as_str());
915                        pick.then(|| current.iter().find(|(k, _)| k == name).map(|(_, v)| *v))
916                            .flatten()
917                    });
918                    let text = value.unwrap_or("");
919                    if text.is_empty() && *optional {
920                        path.pop();
921                    }
922                    debug_assert!(
923                        *optional || !text.is_empty(),
924                        "missing required param {name:?} for the route called {:?}",
925                        route.name
926                    );
927                    let encoded = encode_param(text);
928                    values.push(value.map(|_| decode(&encoded)));
929                    path.push_str(&encoded);
930                }
931            }
932        }
933        if path.is_empty() {
934            path.push('/');
935        }
936        let (active, exact) = self.state(Some((i, values)));
937        Link {
938            href: self.href(&path, search, hash),
939            active,
940            exact,
941        }
942    }
943
944    /// Resolve `{ path, query, hash }`: the path resolved from here, as a string `to` would be,
945    /// then `search` and `hash`.
946    ///
947    /// # Example
948    ///
949    /// ```
950    /// # use ferrovue_router as ferrovue;
951    /// let router = ferrovue::Router::new(&["/", "/search"]);
952    /// let route = router.at("/");
953    ///
954    /// // <RouterLink :to="{ path: '/search', query: { q: 'a b' } }">
955    /// let mut search = String::new();
956    /// ferrovue::query_into(&mut search, "q", "a b");
957    /// let link = route.link_path("/search", &search, "");
958    /// assert_eq!(link.href, "/search?q=a+b");
959    /// assert!(!link.active);
960    /// ```
961    #[must_use]
962    pub fn link_path(&self, path: &str, search: &str, hash: &str) -> Link {
963        let (path, _) = parse_url(path, &self.path);
964        let (active, exact) = self.state(self.router.matched(&path));
965        Link {
966            href: self.href(&path, search, hash),
967            active,
968            exact,
969        }
970    }
971
972    fn href(&self, path: &str, search: &str, hash: &str) -> String {
973        let mut href = format!("{}{path}", self.router.base);
974        if !search.is_empty() {
975            href.push('?');
976            href.push_str(search);
977        }
978        href.push_str(&encode_url(hash, Part::Hash));
979        href
980    }
981}
982
983/// `stringifyQuery`, one key and value at a time: `key=value`, each encoded as vue-router encodes
984/// them, after a `&` when `search` already holds a pair.
985///
986/// # Example
987///
988/// ```
989/// # use ferrovue_router as ferrovue;
990/// let mut search = String::new();
991/// ferrovue::query_into(&mut search, "q", "a b&c");
992/// ferrovue::query_into(&mut search, "page", "2");
993/// assert_eq!(search, "q=a+b%26c&page=2");
994/// ```
995pub fn query_into(search: &mut String, key: &str, value: &str) {
996    if !search.is_empty() {
997        search.push('&');
998    }
999    search.push_str(&encode_url(key, Part::QueryKey));
1000    search.push('=');
1001    search.push_str(&encode_url(value, Part::QueryValue));
1002}
1003
1004#[derive(Clone, Copy, PartialEq)]
1005enum Part {
1006    Param,
1007    QueryValue,
1008    QueryKey,
1009    Hash,
1010}
1011
1012fn encode_param(text: &str) -> String {
1013    encode_url(text, Part::Param)
1014}
1015
1016fn encode_url(text: &str, part: Part) -> String {
1017    use std::fmt::Write;
1018    let mut out = String::with_capacity(text.len());
1019    for c in text.chars() {
1020        let uri = c.is_ascii_alphanumeric() || ";,/?:@&=+$-_.!~*'()#|[]".contains(c);
1021        let keep = match (part, c) {
1022            (Part::Param, '#' | '?' | '/') => false,
1023            (Part::QueryValue | Part::QueryKey, '+' | '#' | '&') => false,
1024            (Part::QueryKey, '=') => false,
1025            (Part::QueryValue | Part::QueryKey, ' ') => {
1026                out.push('+');
1027                continue;
1028            }
1029            (Part::QueryValue | Part::QueryKey, '`' | '{' | '}' | '^') => true,
1030            (Part::Hash, '{' | '}' | '^') => true,
1031            _ => uri,
1032        };
1033        if keep {
1034            out.push(c);
1035        } else {
1036            let mut buf = [0; 4];
1037            for b in c.encode_utf8(&mut buf).bytes() {
1038                let _ = write!(out, "%{b:02X}");
1039            }
1040        }
1041    }
1042    out
1043}
1044
1045impl Pattern {
1046    fn new(path: &str, name: Option<String>) -> Pattern {
1047        let Some(rest) = path.strip_prefix('/') else {
1048            panic!("a route path starts with `/`: {path:?}");
1049        };
1050        if rest.is_empty() {
1051            return Pattern {
1052                path: path.to_owned(),
1053                parent: None,
1054                tokens: Vec::new(),
1055                score: vec![ROOT],
1056                name,
1057                matchable: true,
1058            };
1059        }
1060        let segments: Vec<&str> = rest.split('/').collect();
1061        let mut tokens = Vec::new();
1062        let mut score = Vec::new();
1063        for (i, seg) in segments.iter().enumerate() {
1064            if let Some(param) = seg.strip_prefix(':') {
1065                let (param, optional) = match param.strip_suffix('?') {
1066                    Some(param) => (param, true),
1067                    None => (param, false),
1068                };
1069                let (name, wildcard) = match param.strip_suffix("(.*)") {
1070                    Some(name) => (name, true),
1071                    None => (param, false),
1072                };
1073                let named = !name.is_empty()
1074                    && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
1075                    && !(wildcard && optional);
1076                assert!(
1077                    named,
1078                    "a parameter is `:name`, `:name?` or `:name(.*)`: {path:?}"
1079                );
1080                assert!(
1081                    !wildcard || i == segments.len() - 1,
1082                    "`(.*)` is supported on the last segment only: {path:?}"
1083                );
1084                tokens.push(Token::Param {
1085                    name: name.to_owned(),
1086                    wildcard,
1087                    optional,
1088                });
1089                score.push(match (wildcard, optional) {
1090                    (true, _) => WILDCARD,
1091                    (false, true) => OPTIONAL,
1092                    (false, false) => PARAM,
1093                });
1094            } else {
1095                let plain = !seg.is_empty()
1096                    && seg.is_ascii()
1097                    && !seg.contains([':', '(', ')', '*', '?', '+', '\\']);
1098                assert!(plain, "a static segment is plain ASCII text: {path:?}");
1099                tokens.push(Token::Static((*seg).to_owned()));
1100                score.push(STATIC);
1101            }
1102        }
1103        Pattern {
1104            path: path.to_owned(),
1105            parent: None,
1106            tokens,
1107            score,
1108            name,
1109            matchable: true,
1110        }
1111    }
1112
1113    fn parse(&self, path: &str) -> Option<Values> {
1114        let mut params = Vec::new();
1115        self.parse_from(&self.tokens, path, &mut params)
1116            .then_some(params)
1117    }
1118
1119    fn parse_from(&self, tokens: &[Token], rest: &str, params: &mut Values) -> bool {
1120        let Some((token, after)) = tokens.split_first() else {
1121            return matches!(rest, "" | "/");
1122        };
1123        match token {
1124            Token::Static(text) => {
1125                let Some(rest) = rest.strip_prefix('/') else {
1126                    return false;
1127                };
1128                match rest.get(..text.len()) {
1129                    Some(head) if head.eq_ignore_ascii_case(text) => {
1130                        self.parse_from(after, &rest[text.len()..], params)
1131                    }
1132                    _ => false,
1133                }
1134            }
1135            Token::Param {
1136                wildcard: false,
1137                optional,
1138                ..
1139            } => {
1140                if let Some(inner) = rest.strip_prefix('/') {
1141                    let end = inner.find('/').unwrap_or(inner.len());
1142                    if end > 0 {
1143                        params.push(Some(decode(&inner[..end])));
1144                        if self.parse_from(after, &inner[end..], params) {
1145                            return true;
1146                        }
1147                        params.pop();
1148                    }
1149                }
1150                if *optional {
1151                    params.push(None);
1152                    if self.parse_from(after, rest, params) {
1153                        return true;
1154                    }
1155                    params.pop();
1156                }
1157                false
1158            }
1159            Token::Param { wildcard: true, .. } => {
1160                let Some(rest) = rest.strip_prefix('/') else {
1161                    return false;
1162                };
1163                if rest.contains(['\n', '\r', '\u{2028}', '\u{2029}']) {
1164                    return false;
1165                }
1166                params.push(Some(decode(rest)));
1167                self.parse_from(after, "", params)
1168            }
1169        }
1170    }
1171}
1172
1173fn compare(a: &[i32], b: &[i32]) -> std::cmp::Ordering {
1174    for (x, y) in a.iter().zip(b) {
1175        if x != y {
1176            return y.cmp(x);
1177        }
1178    }
1179    b.len().cmp(&a.len())
1180}
1181
1182fn parse_url(location: &str, current: &str) -> (String, String) {
1183    let hash_pos = location.find('#');
1184    let search_pos = location
1185        .find('?')
1186        .filter(|&s| hash_pos.is_none_or(|h| s < h));
1187    let mut path: Option<&str> = None;
1188    let mut search = "";
1189    let mut hash = "";
1190    if let Some(s) = search_pos {
1191        path = Some(&location[..s]);
1192        let end = hash_pos.filter(|&h| h > 0).unwrap_or(location.len());
1193        search = &location[s..end];
1194    }
1195    if let Some(h) = hash_pos {
1196        path = Some(path.filter(|p| !p.is_empty()).unwrap_or(&location[..h]));
1197        hash = &location[h..];
1198    }
1199    let path = resolve_relative_path(path.unwrap_or(location), current);
1200    let full = format!("{path}{search}{hash}");
1201    (path, full)
1202}
1203
1204fn resolve_relative_path(to: &str, from: &str) -> String {
1205    if to.starts_with('/') {
1206        return to.to_owned();
1207    }
1208    if to.is_empty() {
1209        return from.to_owned();
1210    }
1211    let from_segments: Vec<&str> = from.split('/').collect();
1212    let mut to_segments: Vec<&str> = to.split('/').collect();
1213    if matches!(to_segments.last(), Some(&("." | ".."))) {
1214        to_segments.push("");
1215    }
1216    let mut position = from_segments.len() - 1;
1217    let mut to_position = 0;
1218    while to_position < to_segments.len() {
1219        match to_segments[to_position] {
1220            "." => {}
1221            ".." => {
1222                if position > 1 {
1223                    position -= 1;
1224                }
1225            }
1226            _ => break,
1227        }
1228        to_position += 1;
1229    }
1230    format!(
1231        "{}/{}",
1232        from_segments[..position].join("/"),
1233        to_segments[to_position..].join("/")
1234    )
1235}
1236
1237fn decode(text: &str) -> String {
1238    if !text.contains('%') {
1239        return text.to_owned();
1240    }
1241    let bytes = text.as_bytes();
1242    let mut out = Vec::with_capacity(bytes.len());
1243    let mut i = 0;
1244    while i < bytes.len() {
1245        if bytes[i] == b'%' {
1246            let hex = |b: u8| (b as char).to_digit(16);
1247            match (
1248                bytes.get(i + 1).and_then(|&b| hex(b)),
1249                bytes.get(i + 2).and_then(|&b| hex(b)),
1250            ) {
1251                (Some(hi), Some(lo)) => {
1252                    out.push((hi * 16 + lo) as u8);
1253                    i += 3;
1254                    continue;
1255                }
1256                _ => return text.to_owned(),
1257            }
1258        }
1259        out.push(bytes[i]);
1260        i += 1;
1261    }
1262    String::from_utf8(out).unwrap_or_else(|_| text.to_owned())
1263}
1264
1265#[cfg(test)]
1266mod tests;