Skip to content

Commit 9aa0b94

Browse files
codingjoeCopilotCopilotpre-commit-ci[bot]
authored
Fix #327 -- Switch to autonomous custom element for Safari support (#357)
Apple decided they would rather not support customized built-in elements, see also: WebKit/standards-positions#97 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 52cd30b commit 9aa0b94

File tree

5 files changed

+495
-51
lines changed

5 files changed

+495
-51
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ jobs:
6464
runs-on: ${{ matrix.os }}
6565
steps:
6666
- uses: actions/checkout@v6
67-
- name: Install Selenium
68-
run: |
69-
curl -LsSfO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
70-
sudo dpkg -i google-chrome-stable_current_amd64.deb || sudo apt-get -f install -y
71-
if: matrix.os == 'ubuntu-latest'
7267
- uses: astral-sh/setup-uv@v7
7368
- run: uv run pytest -m selenium
7469
- uses: codecov/codecov-action@v5

s3file/forms.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import base64
2+
import html
23
import logging
34
import pathlib
45
import uuid
6+
from html.parser import HTMLParser
57

68
from django.conf import settings
79
from django.templatetags.static import static
810
from django.utils.functional import cached_property
911
from django.utils.html import format_html, html_safe
12+
from django.utils.safestring import mark_safe
1013
from storages.utils import safe_join
1114

1215
from s3file.middleware import S3FileMiddleware
@@ -15,6 +18,71 @@
1518
logger = logging.getLogger("s3file")
1619

1720

21+
class InputToS3FileRewriter(HTMLParser):
22+
"""HTML parser that rewrites <input type="file"> to <s3-file> custom elements."""
23+
24+
def __init__(self):
25+
super().__init__()
26+
self.output = []
27+
28+
def handle_starttag(self, tag, attrs):
29+
if tag == "input" and dict(attrs).get("type") == "file":
30+
self.output.append("<s3-file")
31+
for name, value in attrs:
32+
if name != "type":
33+
self.output.append(
34+
f' {name}="{html.escape(value, quote=True)}"'
35+
if value
36+
else f" {name}"
37+
)
38+
self.output.append(">")
39+
else:
40+
self.output.append(self.get_starttag_text())
41+
42+
def handle_endtag(self, tag):
43+
self.output.append(f"</{tag}>")
44+
45+
def handle_data(self, data):
46+
self.output.append(data)
47+
48+
def handle_startendtag(self, tag, attrs):
49+
if tag == "input" and dict(attrs).get("type") == "file":
50+
self.output.append("<s3-file")
51+
for name, value in attrs:
52+
if name != "type":
53+
self.output.append(
54+
f' {name}="{html.escape(value, quote=True)}"'
55+
if value
56+
else f" {name}"
57+
)
58+
self.output.append(">")
59+
else:
60+
self.output.append(self.get_starttag_text())
61+
62+
def handle_comment(self, data):
63+
# Preserve HTML comments in the output
64+
self.output.append(f"<!--{data}-->")
65+
66+
def handle_decl(self, decl):
67+
# Preserve declarations such as <!DOCTYPE ...> in the output
68+
self.output.append(f"<!{decl}>")
69+
70+
def handle_pi(self, data):
71+
# Preserve processing instructions such as <?xml ...?> in the output
72+
self.output.append(f"<?{data}>")
73+
74+
def handle_entityref(self, name):
75+
# Preserve HTML entities like &amp;, &lt;, &gt;
76+
self.output.append(f"&{name};")
77+
78+
def handle_charref(self, name):
79+
# Preserve character references like &#39;, &#x27;
80+
self.output.append(f"&#{name};")
81+
82+
def get_html(self):
83+
return "".join(self.output)
84+
85+
1886
@html_safe
1987
class Asset:
2088
"""A generic asset that can be included in a template."""
@@ -75,7 +143,6 @@ def client(self):
75143

76144
def build_attrs(self, *args, **kwargs):
77145
attrs = super().build_attrs(*args, **kwargs)
78-
attrs["is"] = "s3-file"
79146

80147
accept = attrs.get("accept")
81148
response = self.client.generate_presigned_post(
@@ -97,6 +164,13 @@ def build_attrs(self, *args, **kwargs):
97164

98165
return defaults
99166

167+
def render(self, name, value, attrs=None, renderer=None):
168+
"""Render the widget as a custom element for Safari compatibility."""
169+
html_output = str(super().render(name, value, attrs=attrs, renderer=renderer))
170+
parser = InputToS3FileRewriter()
171+
parser.feed(html_output)
172+
return mark_safe(parser.get_html()) # noqa: S308
173+
100174
def get_conditions(self, accept):
101175
conditions = [
102176
{"bucket": self.bucket_name},

0 commit comments

Comments
 (0)