11from __future__ import annotations
2- from dataclasses import dataclass
32
4- from fnmatch import translate as fnmatch_translate
5- from pathlib import Path
63import re
7- from typing import Any , Iterator , Protocol , Callable , Sequence
4+ from dataclasses import dataclass
5+ from pathlib import Path
6+ from typing import Any , Callable , Iterator , Sequence
7+ from urllib .parse import parse_qs
88
9- from idom import create_context , component , use_context , use_state
10- from idom .web . module import export , module_from_file
11- from idom .core .vdom import coalesce_attributes_and_children , VdomAttributesAndChildren
9+ from idom import component , create_context , use_context , use_memo , use_state
10+ from idom .core . types import VdomAttributesAndChildren , VdomDict
11+ from idom .core .vdom import coalesce_attributes_and_children
1212from idom .types import BackendImplementation , ComponentType , Context , Location
13+ from idom .web .module import export , module_from_file
14+ from starlette .routing import compile_path
15+
16+ try :
17+ from typing import Protocol
18+ except ImportError : # pragma: no cover
19+ from typing_extensions import Protocol # type: ignore
1320
1421
15- class Router (Protocol ):
22+ class RoutesConstructor (Protocol ):
1623 def __call__ (self , * routes : Route ) -> ComponentType :
1724 ...
1825
1926
20- def bind (backend : BackendImplementation ) -> Router :
27+ def configure (
28+ implementation : BackendImplementation [Any ] | Callable [[], Location ]
29+ ) -> RoutesConstructor :
30+ if isinstance (implementation , BackendImplementation ):
31+ use_location = implementation .use_location
32+ elif callable (implementation ):
33+ use_location = implementation
34+ else :
35+ raise TypeError (
36+ "Expected a 'BackendImplementation' or "
37+ f"'use_location' hook, not { implementation } "
38+ )
39+
2140 @component
22- def Router (* routes : Route ):
23- initial_location = backend . use_location ()
41+ def routes (* routes : Route ) -> ComponentType | None :
42+ initial_location = use_location ()
2443 location , set_location = use_state (initial_location )
25- for p , r in _compile_routes (routes ):
26- if p .match (location .pathname ):
44+ compiled_routes = use_memo (
45+ lambda : _iter_compile_routes (routes ), dependencies = routes
46+ )
47+ for r in compiled_routes :
48+ match = r .pattern .match (location .pathname )
49+ if match :
2750 return _LocationStateContext (
2851 r .element ,
29- value = (location , set_location ),
30- key = r .path ,
52+ value = _LocationState (
53+ location ,
54+ set_location ,
55+ {k : r .converters [k ](v ) for k , v in match .groupdict ().items ()},
56+ ),
57+ key = r .pattern .pattern ,
3158 )
3259 return None
3360
34- return Router
35-
36-
37- def use_location () -> str :
38- return _use_location_state ()[0 ]
61+ return routes
3962
4063
4164@dataclass
4265class Route :
43- path : str | re . Pattern
66+ path : str
4467 element : Any
68+ routes : Sequence [Route ]
69+
70+ def __init__ (self , path : str , element : Any | None , * routes : Route ) -> None :
71+ self .path = path
72+ self .element = element
73+ self .routes = routes
4574
4675
4776@component
48- def Link (* attributes_or_children : VdomAttributesAndChildren , to : str ) -> None :
77+ def link (* attributes_or_children : VdomAttributesAndChildren , to : str ) -> VdomDict :
4978 attributes , children = coalesce_attributes_and_children (attributes_or_children )
50- set_location = _use_location_state ()[1 ]
51- return _Link (
52- {
53- ** attributes ,
54- "to" : to ,
55- "onClick" : lambda event : set_location (Location (** event )),
56- },
57- * children ,
79+ set_location = _use_location_state ().set_location
80+ attrs = {
81+ ** attributes ,
82+ "to" : to ,
83+ "onClick" : lambda event : set_location (Location (** event )),
84+ }
85+ return _Link (attrs , * children )
86+
87+
88+ def use_location () -> Location :
89+ """Get the current route location"""
90+ return _use_location_state ().location
91+
92+
93+ def use_params () -> dict [str , Any ]:
94+ """Get parameters from the currently matching route pattern"""
95+ return _use_location_state ().params
96+
97+
98+ def use_query (
99+ keep_blank_values : bool = False ,
100+ strict_parsing : bool = False ,
101+ errors : str = "replace" ,
102+ max_num_fields : int | None = None ,
103+ separator : str = "&" ,
104+ ) -> dict [str , list [str ]]:
105+ """See :func:`urllib.parse.parse_qs` for parameter info."""
106+ return parse_qs (
107+ use_location ().search [1 :],
108+ keep_blank_values = keep_blank_values ,
109+ strict_parsing = strict_parsing ,
110+ errors = errors ,
111+ max_num_fields = max_num_fields ,
112+ separator = separator ,
58113 )
59114
60115
61- def _compile_routes (routes : Sequence [Route ]) -> Iterator [tuple [re .Pattern , Route ]]:
116+ def _iter_compile_routes (routes : Sequence [Route ]) -> Iterator [_CompiledRoute ]:
117+ for path , element in _iter_routes (routes ):
118+ pattern , _ , converters = compile_path (path )
119+ yield _CompiledRoute (
120+ pattern , {k : v .convert for k , v in converters .items ()}, element
121+ )
122+
123+
124+ def _iter_routes (routes : Sequence [Route ]) -> Iterator [tuple [str , Any ]]:
62125 for r in routes :
63- if isinstance (r .path , re .Pattern ):
64- yield r .path , r
65- continue
66- if not r .path .startswith ("/" ):
67- raise ValueError ("Path pattern must begin with '/'" )
68- pattern = re .compile (fnmatch_translate (r .path ))
69- yield pattern , r
126+ for path , element in _iter_routes (r .routes ):
127+ yield r .path + path , element
128+ yield r .path , r .element
129+
130+
131+ @dataclass
132+ class _CompiledRoute :
133+ pattern : re .Pattern [str ]
134+ converters : dict [str , Callable [[Any ], Any ]]
135+ element : Any
70136
71137
72138def _use_location_state () -> _LocationState :
@@ -75,9 +141,14 @@ def _use_location_state() -> _LocationState:
75141 return location_state
76142
77143
78- _LocationSetter = Callable [[str ], None ]
79- _LocationState = tuple [Location , _LocationSetter ]
80- _LocationStateContext : type [Context [_LocationState | None ]] = create_context (None )
144+ @dataclass
145+ class _LocationState :
146+ location : Location
147+ set_location : Callable [[Location ], None ]
148+ params : dict [str , Any ]
149+
150+
151+ _LocationStateContext : Context [_LocationState | None ] = create_context (None )
81152
82153_Link = export (
83154 module_from_file (
0 commit comments