Skip to content

Commit a78a5b1

Browse files
committed
Server side handling of relative URLs
1 parent 88d1f46 commit a78a5b1

File tree

2 files changed

+27
-4
lines changed

2 files changed

+27
-4
lines changed

src/reactpy_router/components.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from pathlib import Path
44
from typing import Any
5+
from urllib.parse import urljoin
56
from uuid import uuid4
67

7-
from reactpy import component, event, html
8+
from reactpy import component, event, html, use_connection
89
from reactpy.backend.types import Location
910
from reactpy.core.types import VdomChild, VdomDict
1011
from reactpy.web.module import export, module_from_file
@@ -26,12 +27,30 @@ def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict:
2627
# properly sets the location. When a client-server communication layer is added to a \
2728
# future ReactPy release, this component will need to be rewritten to use that instead. \
2829
set_location = _use_route_state().set_location
30+
current_path = use_connection().location.pathname
2931

3032
@event(prevent_default=True)
3133
def on_click(_event: dict[str, Any]) -> None:
3234
pathname, search = to.split("?", 1) if "?" in to else (to, "")
3335
if search:
3436
search = f"?{search}"
37+
38+
# Resolve relative paths that match `../foo`
39+
if pathname.startswith("../"):
40+
pathname = urljoin(current_path, pathname)
41+
42+
# Resolve relative paths that match `foo`
43+
if not pathname.startswith("/"):
44+
pathname = urljoin(current_path, pathname)
45+
46+
# Resolve relative paths that match `/foo/../bar`
47+
while "/../" in pathname:
48+
part_1, part_2 = pathname.split("/../", 1)
49+
pathname = urljoin(f"{part_1}/", f"../{part_2}")
50+
51+
# Resolve relative paths that match `foo/./bar`
52+
pathname = pathname.replace("/./", "/")
53+
3554
set_location(Location(pathname, search))
3655

3756
uuid_string = f"link-{uuid4().hex}"

tests/test_core.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,22 +193,26 @@ async def test_relative_links(display: DisplayFixture):
193193
def sample():
194194
return browser_router(
195195
route("/", link("Root", to="/a", id="root")),
196-
route("/a", link("A", to="/a/b", id="a")),
196+
route("/a", link("A", to="/a/a/../b", id="a")),
197197
route("/a/b", link("B", to="../a/b/c", id="b")),
198198
route("/a/b/c", link("C", to="../d", id="c")),
199199
route("/a/d", link("D", to="e", id="d")),
200-
route("/a/e", link("E", to="../default", id="e")),
200+
route("/a/e", link("E", to="/a/./f", id="e")),
201+
route("/a/f", link("F", to="../default", id="f")),
201202
route("{default:any}", html.h1({"id": "default"}, "Default")),
202203
)
203204

204205
await display.show(sample)
205206

206-
for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e"]:
207+
for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e", "#f"]:
207208
lnk = await display.page.wait_for_selector(link_selector)
208209
await lnk.click()
209210

210211
await display.page.wait_for_selector("#default")
211212

213+
await display.page.go_back()
214+
await display.page.wait_for_selector("#f")
215+
212216
await display.page.go_back()
213217
await display.page.wait_for_selector("#e")
214218

0 commit comments

Comments
 (0)