22
33import re
44from dataclasses import dataclass
5- from fnmatch import translate as fnmatch_translate
65from pathlib import Path
7- from typing import Any , Callable , Iterator , Sequence
6+ from urllib .parse import parse_qs
7+ from typing import Any , Callable , Iterator , Sequence , TypeVar , overload
88
9- from idom import component , create_context , use_context , use_state
9+ from idom import component , create_context , use_context , use_state , use_memo
1010from idom .core .types import VdomAttributesAndChildren , VdomDict
1111from idom .core .vdom import coalesce_attributes_and_children
1212from idom .types import BackendImplementation , ComponentType , Context , Location
1313from idom .web .module import export , module_from_file
14+ from starlette .routing import compile_path
1415
1516try :
1617 from typing import Protocol
17- except ImportError :
18+ except ImportError : # pragma: no cover
1819 from typing_extensions import Protocol
1920
2021
@@ -32,39 +33,42 @@ def configure(
3233 use_location = implementation
3334 else :
3435 raise TypeError (
35- "Expected a BackendImplementation or "
36- f"` use_location` hook, not { implementation } "
36+ "Expected a ' BackendImplementation' or "
37+ f"' use_location' hook, not { implementation } "
3738 )
3839
3940 @component
40- def Router (* routes : Route ) -> ComponentType | None :
41+ def Router (* routes : Route | Sequence [ Route ] ) -> ComponentType | None :
4142 initial_location = use_location ()
4243 location , set_location = use_state (initial_location )
43- for p , r in _compile_routes (routes ):
44- match = p .match (location .pathname )
44+ compiled_routes = use_memo (lambda : _compile_routes (routes ), dependencies = routes )
45+ for r in compiled_routes :
46+ match = r .pattern .match (location .pathname )
4547 if match :
4648 return _LocationStateContext (
4749 r .element ,
48- value = _LocationState (location , set_location , match ),
49- key = p .pattern ,
50+ value = _LocationState (
51+ location ,
52+ set_location ,
53+ {r .converters [k ](v ) for k , v in match .groupdict ().items ()},
54+ ),
55+ key = r .pattern .pattern ,
5056 )
5157 return None
5258
5359 return Router
5460
5561
56- def use_location () -> Location :
57- return _use_location_state ().location
58-
59-
60- def use_match () -> re .Match [str ]:
61- return _use_location_state ().match
62-
63-
6462@dataclass
6563class Route :
66- path : str | re . Pattern [ str ]
64+ path : str
6765 element : Any
66+ routes : Sequence [Route ]
67+
68+ def __init__ (self , path : str , element : Any | None , * routes : Route ) -> None :
69+ self .path = path
70+ self .element = element
71+ self .routes = routes
6872
6973
7074@component
@@ -79,15 +83,54 @@ def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
7983 return _Link (attrs , * children )
8084
8185
82- def _compile_routes (routes : Sequence [Route ]) -> Iterator [tuple [re .Pattern [str ], Route ]]:
86+ def use_location () -> Location :
87+ """Get the current route location"""
88+ return _use_location_state ().location
89+
90+
91+ def use_params () -> dict [str , Any ]:
92+ """Get parameters from the currently matching route pattern"""
93+ return _use_location_state ().params
94+
95+
96+ def use_query (
97+ keep_blank_values : bool = False ,
98+ strict_parsing : bool = False ,
99+ errors : str = "replace" ,
100+ max_num_fields : int | None = None ,
101+ separator : str = "&" ,
102+ ) -> dict [str , list [str ]]:
103+ """See :func:`urllib.parse.parse_qs` for parameter info."""
104+ return parse_qs (
105+ use_location ().search ,
106+ keep_blank_values = keep_blank_values ,
107+ strict_parsing = strict_parsing ,
108+ errors = errors ,
109+ max_num_fields = max_num_fields ,
110+ separator = separator ,
111+ )
112+
113+
114+ def _compile_routes (routes : Sequence [Route ]) -> list [_CompiledRoute ]:
115+ for path , element in _iter_routes (routes ):
116+ pattern , _ , converters = compile_path (path )
117+ yield _CompiledRoute (
118+ pattern , {k : v .convert for k , v in converters .items ()}, element
119+ )
120+
121+
122+ def _iter_routes (routes : Sequence [Route ]) -> Iterator [tuple [str , Any ]]:
83123 for r in routes :
84- if isinstance (r .path , re .Pattern ):
85- yield r .path , r
86- continue
87- if not r .path .startswith ("/" ):
88- raise ValueError ("Path pattern must begin with '/'" )
89- pattern = re .compile (fnmatch_translate (r .path ))
90- yield pattern , r
124+ for path , element in _iter_routes (r .routes ):
125+ yield r .path + path , element
126+ yield r .path , r .element
127+
128+
129+ @dataclass
130+ class _CompiledRoute :
131+ pattern : re .Pattern [str ]
132+ converters : dict [str , Callable [[Any ], Any ]]
133+ element : Any
91134
92135
93136def _use_location_state () -> _LocationState :
@@ -100,7 +143,7 @@ def _use_location_state() -> _LocationState:
100143class _LocationState :
101144 location : Location
102145 set_location : Callable [[Location ], None ]
103- match : re . Match [str ]
146+ params : dict [str , Any ]
104147
105148
106149_LocationStateContext : Context [_LocationState | None ] = create_context (None )
0 commit comments