Skip to content

Commit 28bf247

Browse files
add noscript option
1 parent 613b256 commit 28bf247

File tree

6 files changed

+100
-2
lines changed

6 files changed

+100
-2
lines changed

src/reactpy/executors/asgi/pyscript.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
pyscript_component_html,
1818
pyscript_setup_html,
1919
)
20-
from reactpy.executors.utils import vdom_head_to_html
20+
from reactpy.executors.utils import html_noscript_path_to_html, vdom_head_to_html
2121
from reactpy.types import ReactPyConfig, VdomDict
2222

2323

@@ -32,6 +32,7 @@ def __init__(
3232
initial: str | VdomDict = "",
3333
http_headers: dict[str, str] | None = None,
3434
html_head: VdomDict | None = None,
35+
html_noscript_path: str | Path | None = None,
3536
html_lang: str = "en",
3637
**settings: Unpack[ReactPyConfig],
3738
) -> None:
@@ -59,6 +60,8 @@ def __init__(
5960
commonly used to render a loading animation.
6061
http_headers: Additional headers to include in the HTTP response for the base HTML document.
6162
html_head: Additional head elements to include in the HTML response.
63+
html_noscript_path: Path to an HTML file whose contents are rendered within a
64+
`<noscript>` tag in the HTML body.
6265
html_lang: The language of the HTML document.
6366
settings:
6467
Global ReactPy configuration settings that affect behavior and performance. Most settings
@@ -78,6 +81,7 @@ def __init__(
7881
self.extra_headers = http_headers or {}
7982
self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?")
8083
self.html_head = html_head or html.head()
84+
self.html_noscript_path = html_noscript_path
8185
self.html_lang = html_lang
8286

8387
def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool: # nocov
@@ -97,6 +101,11 @@ class ReactPyPyscriptApp(ReactPyApp):
97101
def render_index_html(self) -> None:
98102
"""Process the index.html and store the results in this class."""
99103
head_content = vdom_head_to_html(self.parent.html_head)
104+
noscript = (
105+
html_noscript_path_to_html(self.parent.html_noscript_path)
106+
if self.parent.html_noscript_path
107+
else ""
108+
)
100109
pyscript_setup = pyscript_setup_html(
101110
extra_py=self.parent.extra_py,
102111
extra_js=self.parent.extra_js,
@@ -114,6 +123,7 @@ def render_index_html(self) -> None:
114123
f'<html lang="{self.parent.html_lang}">'
115124
f"{head_content}"
116125
"<body>"
126+
f"{noscript}"
117127
f"{pyscript_component}"
118128
"</body>"
119129
"</html>"

src/reactpy/executors/asgi/standalone.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from datetime import UTC, datetime
88
from email.utils import formatdate
99
from logging import getLogger
10+
from pathlib import Path
1011
from typing import Literal, Unpack, cast, overload
1112

1213
from asgi_tools import ResponseHTML
@@ -24,7 +25,11 @@
2425
AsgiWebsocketScope,
2526
)
2627
from reactpy.executors.pyscript.utils import pyscript_setup_html
27-
from reactpy.executors.utils import server_side_component_html, vdom_head_to_html
28+
from reactpy.executors.utils import (
29+
html_noscript_path_to_html,
30+
server_side_component_html,
31+
vdom_head_to_html,
32+
)
2833
from reactpy.types import (
2934
PyScriptOptions,
3035
ReactPyConfig,
@@ -45,6 +50,7 @@ def __init__(
4550
*,
4651
http_headers: dict[str, str] | None = None,
4752
html_head: VdomDict | None = None,
53+
html_noscript_path: str | Path | None = None,
4854
html_lang: str = "en",
4955
pyscript_setup: bool = False,
5056
pyscript_options: PyScriptOptions | None = None,
@@ -56,6 +62,8 @@ def __init__(
5662
root_component: The root component to render. This app is typically a single page application.
5763
http_headers: Additional headers to include in the HTTP response for the base HTML document.
5864
html_head: Additional head elements to include in the HTML response.
65+
html_noscript_path: Path to an HTML file whose contents are rendered within a
66+
`<noscript>` tag in the HTML body.
5967
html_lang: The language of the HTML document.
6068
pyscript_setup: Whether to automatically load PyScript within your HTML head.
6169
pyscript_options: Options to configure PyScript behavior.
@@ -66,6 +74,7 @@ def __init__(
6674
self.extra_headers = http_headers or {}
6775
self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?")
6876
self.html_head = html_head or html.head()
77+
self.html_noscript_path = html_noscript_path
6978
self.html_lang = html_lang
7079

7180
if pyscript_setup:
@@ -229,11 +238,17 @@ async def __call__(
229238

230239
def render_index_html(self) -> None:
231240
"""Process the index.html and store the results in this class."""
241+
noscript = (
242+
html_noscript_path_to_html(self.parent.html_noscript_path)
243+
if self.parent.html_noscript_path
244+
else ""
245+
)
232246
self._index_html = (
233247
"<!doctype html>"
234248
f'<html lang="{self.parent.html_lang}">'
235249
f"{vdom_head_to_html(self.parent.html_head)}"
236250
"<body>"
251+
f"{noscript}"
237252
f"{server_side_component_html(element_id='app', class_='', component_path='')}"
238253
"</body>"
239254
"</html>"

src/reactpy/executors/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from collections.abc import Iterable
5+
from pathlib import Path
56
from typing import Any
67

78
from reactpy._option import Option
@@ -46,6 +47,10 @@ def vdom_head_to_html(head: VdomDict) -> str:
4647
raise ValueError("Head element must be constructed with `html.head`.")
4748

4849

50+
def html_noscript_path_to_html(path: str | Path) -> str:
51+
return f"<noscript>{Path(path).read_text(encoding='utf-8')}</noscript>"
52+
53+
4954
def process_settings(settings: ReactPyConfig) -> None:
5055
"""Process the settings and return the final configuration."""
5156
from reactpy import config

tests/test_asgi/test_pyscript.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# ruff: noqa: S701
2+
import asyncio
23
from pathlib import Path
34

45
import pytest
6+
from requests import request
57
from jinja2 import Environment as JinjaEnvironment
68
from jinja2 import FileSystemLoader as JinjaFileSystemLoader
79
from starlette.applications import Starlette
@@ -11,6 +13,7 @@
1113
from reactpy import html
1214
from reactpy.executors.asgi.pyscript import ReactPyCsr
1315
from reactpy.testing import BackendFixture, DisplayFixture
16+
from reactpy.testing.common import REACTPY_TESTS_DEFAULT_TIMEOUT
1417

1518

1619
@pytest.fixture(scope="module")
@@ -98,6 +101,30 @@ def test_bad_file_path():
98101
ReactPyCsr()
99102

100103

104+
async def test_customized_noscript(tmp_path: Path):
105+
noscript_file = tmp_path / "noscript.html"
106+
noscript_file.write_text(
107+
'<p id="noscript-message">Please enable JavaScript.</p>',
108+
encoding="utf-8",
109+
)
110+
111+
app = ReactPyCsr(
112+
Path(__file__).parent / "pyscript_components" / "root.py",
113+
html_noscript_path=noscript_file,
114+
)
115+
116+
async with BackendFixture(app) as server:
117+
url = f"http://{server.host}:{server.port}"
118+
response = await asyncio.to_thread(
119+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
120+
)
121+
assert response.status_code == 200
122+
assert (
123+
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
124+
in response.text
125+
)
126+
127+
101128
async def test_jinja_template_tag(jinja_display: DisplayFixture):
102129
await jinja_display.goto("/")
103130

tests/test_asgi/test_standalone.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from collections.abc import MutableMapping
3+
from pathlib import Path
34

45
import pytest
56
from asgi_tools import ResponseText
@@ -138,6 +139,34 @@ def sample():
138139
assert (await new_display.page.title()) == custom_title
139140

140141

142+
async def test_customized_noscript(tmp_path: Path):
143+
@reactpy.component
144+
def sample():
145+
return html.h1("Hello World")
146+
147+
noscript_file = tmp_path / "noscript.html"
148+
noscript_file.write_text(
149+
'<p id="noscript-message">Please enable JavaScript.</p>',
150+
encoding="utf-8",
151+
)
152+
153+
app = ReactPy(
154+
sample,
155+
html_noscript_path=noscript_file,
156+
)
157+
158+
async with BackendFixture(app) as server:
159+
url = f"http://{server.host}:{server.port}"
160+
response = await asyncio.to_thread(
161+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
162+
)
163+
assert response.status_code == 200
164+
assert (
165+
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
166+
in response.text
167+
)
168+
169+
141170
async def test_head_request():
142171
@reactpy.component
143172
def sample():

tests/test_asgi/test_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pathlib import Path
2+
13
import pytest
24

35
from reactpy import config
@@ -9,6 +11,16 @@ def test_invalid_vdom_head():
911
utils.vdom_head_to_html({"tagName": "invalid"})
1012

1113

14+
def test_html_noscript_path_to_html(tmp_path: Path):
15+
noscript_file = tmp_path / "noscript.html"
16+
noscript_file.write_text("<p>Please enable JavaScript.</p>", encoding="utf-8")
17+
18+
assert (
19+
utils.html_noscript_path_to_html(noscript_file)
20+
== "<noscript><p>Please enable JavaScript.</p></noscript>"
21+
)
22+
23+
1224
def test_process_settings():
1325
utils.process_settings({"async_rendering": False})
1426
assert config.REACTPY_ASYNC_RENDERING.current is False

0 commit comments

Comments
 (0)