From 62520bc0e66c32375ad2e71a4c802050ac14bcf2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 18 Dec 2025 19:33:44 +0000
Subject: [PATCH 1/9] Initial plan
From 5a3a1f18842cb4a1265b116a3e4cd731327b2461 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 18 Dec 2025 19:41:53 +0000
Subject: [PATCH 2/9] Replace fragile string replacement with HTML parser for
render method
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
s3file/forms.py | 68 +++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 63 insertions(+), 5 deletions(-)
diff --git a/s3file/forms.py b/s3file/forms.py
index 0565602..16f2462 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -1,7 +1,9 @@
import base64
+import html
import logging
import pathlib
import uuid
+from html.parser import HTMLParser
from django.conf import settings
from django.templatetags.static import static
@@ -16,6 +18,63 @@
logger = logging.getLogger("s3file")
+class InputToS3FileRewriter(HTMLParser):
+ """
+ HTML parser that rewrites tags to custom elements.
+
+ This provides a robust way to transform Django's rendered file input widgets
+ into custom elements, handling various attribute orderings and formats.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.output = []
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "input":
+ attrs_dict = dict(attrs)
+ if attrs_dict.get("type") == "file":
+ # Replace with s3-file custom element
+ self._write_s3_file_tag(attrs)
+ return
+
+ # For all other tags, preserve as-is
+ self.output.append(self.get_starttag_text())
+
+ def handle_endtag(self, tag):
+ self.output.append(f"{tag}>")
+
+ def handle_data(self, data):
+ self.output.append(data)
+
+ def handle_startendtag(self, tag, attrs):
+ # For self-closing tags
+ if tag == "input":
+ attrs_dict = dict(attrs)
+ if attrs_dict.get("type") == "file":
+ # Replace with s3-file custom element
+ self._write_s3_file_tag(attrs)
+ return
+
+ self.output.append(self.get_starttag_text())
+
+ def _write_s3_file_tag(self, attrs):
+ """Write the s3-file opening tag with all attributes except type."""
+ self.output.append("")
+
+ def get_html(self):
+ """Return the transformed HTML."""
+ return "".join(self.output)
+
+
@html_safe
class Asset:
"""A generic asset that can be included in a template."""
@@ -99,11 +158,10 @@ def build_attrs(self, *args, **kwargs):
def render(self, name, value, attrs=None, renderer=None):
"""Render the widget as a custom element for Safari compatibility."""
- return mark_safe( # noqa: S308
- str(super().render(name, value, attrs=attrs, renderer=renderer)).replace(
- f'
Date: Thu, 18 Dec 2025 19:43:46 +0000
Subject: [PATCH 3/9] Refactor HTML parser to reduce code duplication and
improve documentation
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
s3file/forms.py | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/s3file/forms.py b/s3file/forms.py
index 16f2462..ca68a58 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -30,13 +30,16 @@ def __init__(self):
super().__init__()
self.output = []
+ def _is_file_input(self, attrs):
+ """Check if attributes indicate a file input element."""
+ attrs_dict = dict(attrs)
+ return attrs_dict.get("type") == "file"
+
def handle_starttag(self, tag, attrs):
- if tag == "input":
- attrs_dict = dict(attrs)
- if attrs_dict.get("type") == "file":
- # Replace with s3-file custom element
- self._write_s3_file_tag(attrs)
- return
+ if tag == "input" and self._is_file_input(attrs):
+ # Replace with s3-file custom element
+ self._write_s3_file_tag(attrs)
+ return
# For all other tags, preserve as-is
self.output.append(self.get_starttag_text())
@@ -49,17 +52,19 @@ def handle_data(self, data):
def handle_startendtag(self, tag, attrs):
# For self-closing tags
- if tag == "input":
- attrs_dict = dict(attrs)
- if attrs_dict.get("type") == "file":
- # Replace with s3-file custom element
- self._write_s3_file_tag(attrs)
- return
+ if tag == "input" and self._is_file_input(attrs):
+ # Replace with s3-file custom element
+ self._write_s3_file_tag(attrs)
+ return
self.output.append(self.get_starttag_text())
def _write_s3_file_tag(self, attrs):
- """Write the s3-file opening tag with all attributes except type."""
+ """
+ Write the s3-file opening tag with all attributes except type.
+
+ Note: This creates an opening tag that requires a corresponding closing tag.
+ """
self.output.append("
Date: Thu, 18 Dec 2025 19:59:34 +0000
Subject: [PATCH 4/9] Compress parser code and add comprehensive unit tests
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
s3file/forms.py | 59 ++++++++++++------------------------------
tests/test_forms.py | 63 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 80 insertions(+), 42 deletions(-)
diff --git a/s3file/forms.py b/s3file/forms.py
index ca68a58..dc1ddd6 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -19,30 +19,21 @@
class InputToS3FileRewriter(HTMLParser):
- """
- HTML parser that rewrites tags to custom elements.
-
- This provides a robust way to transform Django's rendered file input widgets
- into custom elements, handling various attribute orderings and formats.
- """
+ """HTML parser that rewrites to custom elements."""
def __init__(self):
super().__init__()
self.output = []
- def _is_file_input(self, attrs):
- """Check if attributes indicate a file input element."""
- attrs_dict = dict(attrs)
- return attrs_dict.get("type") == "file"
-
def handle_starttag(self, tag, attrs):
- if tag == "input" and self._is_file_input(attrs):
- # Replace with s3-file custom element
- self._write_s3_file_tag(attrs)
- return
-
- # For all other tags, preserve as-is
- self.output.append(self.get_starttag_text())
+ if tag == "input" and dict(attrs).get("type") == "file":
+ self.output.append("")
+ else:
+ self.output.append(self.get_starttag_text())
def handle_endtag(self, tag):
self.output.append(f"{tag}>")
@@ -51,32 +42,16 @@ def handle_data(self, data):
self.output.append(data)
def handle_startendtag(self, tag, attrs):
- # For self-closing tags
- if tag == "input" and self._is_file_input(attrs):
- # Replace with s3-file custom element
- self._write_s3_file_tag(attrs)
- return
-
- self.output.append(self.get_starttag_text())
-
- def _write_s3_file_tag(self, attrs):
- """
- Write the s3-file opening tag with all attributes except type.
-
- Note: This creates an opening tag that requires a corresponding closing tag.
- """
- self.output.append("")
+ if tag == "input" and dict(attrs).get("type") == "file":
+ self.output.append("")
+ else:
+ self.output.append(self.get_starttag_text())
def get_html(self):
- """Return the transformed HTML."""
return "".join(self.output)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 7afca9a..546ca23 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -72,6 +72,54 @@ def test_str(self, settings):
assert str(js) == ''
+class TestInputToS3FileRewriter:
+ def test_transforms_file_input(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ assert parser.get_html() == ''
+
+ def test_preserves_non_file_input(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ assert parser.get_html() == ''
+
+ def test_handles_attribute_ordering(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ assert result.startswith('')
+ result = parser.get_html()
+ assert result.startswith('')
+ result = parser.get_html()
+ assert 'data-value="test&value"' in result
+
+ def test_handles_self_closing_tag(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ assert parser.get_html() == ''
+
+ def test_preserves_surrounding_elements(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ assert result == '
'
+
+
@contextmanager
def wait_for_page_load(driver, timeout=30):
old_page = driver.find_element(By.TAG_NAME, "html")
@@ -186,6 +234,21 @@ def test_render_wraps_in_s3_file_element(self, freeze_upload_folder):
# Check that the output is the s3-file custom element
assert html.startswith("
Date: Thu, 18 Dec 2025 20:06:52 +0000
Subject: [PATCH 5/9] Add test for non-file input with self-closing tag to
cover line 52
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
tests/test_forms.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 546ca23..44d9b0c 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -113,6 +113,11 @@ def test_handles_self_closing_tag(self):
parser.feed('')
assert parser.get_html() == ''
+ def test_preserves_non_file_self_closing_tag(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ assert parser.get_html() == ''
+
def test_preserves_surrounding_elements(self):
parser = forms.InputToS3FileRewriter()
parser.feed('')
From b67064b7ab144f9850c549e370164e3740874035 Mon Sep 17 00:00:00 2001
From: Johannes Maron
Date: Thu, 18 Dec 2025 21:09:08 +0100
Subject: [PATCH 6/9] Update s3file/forms.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
s3file/forms.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/s3file/forms.py b/s3file/forms.py
index dc1ddd6..030c0f9 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -51,6 +51,17 @@ def handle_startendtag(self, tag, attrs):
else:
self.output.append(self.get_starttag_text())
+ def handle_comment(self, data):
+ # Preserve HTML comments in the output
+ self.output.append(f"")
+
+ def handle_decl(self, decl):
+ # Preserve declarations such as in the output
+ self.output.append(f"")
+
+ def handle_pi(self, data):
+ # Preserve processing instructions such as in the output
+ self.output.append(f"{data}>")
def get_html(self):
return "".join(self.output)
From 634f4bd0e8e7455cc4f94b65e190db13b6d24943 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 18 Dec 2025 20:09:15 +0000
Subject: [PATCH 7/9] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
s3file/forms.py | 13 +++++++++++--
tests/test_forms.py | 12 +++++++-----
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/s3file/forms.py b/s3file/forms.py
index 030c0f9..c671de3 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -30,7 +30,11 @@ def handle_starttag(self, tag, attrs):
self.output.append("")
else:
self.output.append(self.get_starttag_text())
@@ -46,7 +50,11 @@ def handle_startendtag(self, tag, attrs):
self.output.append("")
else:
self.output.append(self.get_starttag_text())
@@ -62,6 +70,7 @@ def handle_decl(self, decl):
def handle_pi(self, data):
# Preserve processing instructions such as in the output
self.output.append(f"{data}>")
+
def get_html(self):
return "".join(self.output)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 44d9b0c..033d670 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -87,20 +87,22 @@ def test_handles_attribute_ordering(self):
parser = forms.InputToS3FileRewriter()
parser.feed('')
result = parser.get_html()
- assert result.startswith('')
+ parser.feed(
+ ''
+ )
result = parser.get_html()
- assert result.startswith('
Date: Thu, 18 Dec 2025 20:16:07 +0000
Subject: [PATCH 8/9] Add entity and character reference handlers with
comprehensive tests
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
s3file/forms.py | 8 ++++++++
tests/test_forms.py | 19 +++++++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/s3file/forms.py b/s3file/forms.py
index c671de3..21e7976 100644
--- a/s3file/forms.py
+++ b/s3file/forms.py
@@ -71,6 +71,14 @@ def handle_pi(self, data):
# Preserve processing instructions such as in the output
self.output.append(f"{data}>")
+ def handle_entityref(self, name):
+ # Preserve HTML entities like &, <, >
+ self.output.append(f"&{name};")
+
+ def handle_charref(self, name):
+ # Preserve character references like ', '
+ self.output.append(f"{name};")
+
def get_html(self):
return "".join(self.output)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 033d670..d462cbc 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -110,6 +110,25 @@ def test_escapes_html_entities(self):
result = parser.get_html()
assert 'data-value="test&value"' in result
+ def test_preserves_existing_html_entities(self):
+ # Test that already-escaped entities in input are preserved (not double-escaped)
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ # Should preserve the & entity, not convert to &
+ assert 'data-value="test&value"' in result
+ assert '&' not in result
+
+ def test_preserves_character_references(self):
+ # Test that character references are preserved (may be in decimal or hex format)
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ # The character reference should be preserved (either ' or ' both represent ')
+ assert ('data-value="test's"' in result or 'data-value="test's"' in result)
+ # Verify the actual apostrophe character is NOT directly in the output (should be a reference)
+ assert 'data-value="test\'s"' not in result or '' in result
+
def test_handles_self_closing_tag(self):
parser = forms.InputToS3FileRewriter()
parser.feed('')
From e736b3746e8428223b4adb7433e5c3ba054d9f79 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 18 Dec 2025 20:20:15 +0000
Subject: [PATCH 9/9] Add tests for comment, declaration, and processing
instruction handlers
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
---
tests/test_forms.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index d462cbc..eab1b5c 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -145,6 +145,24 @@ def test_preserves_surrounding_elements(self):
result = parser.get_html()
assert result == '
'
+ def test_preserves_html_comments(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ assert result == ''
+
+ def test_preserves_declarations(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ assert result == ''
+
+ def test_preserves_processing_instructions(self):
+ parser = forms.InputToS3FileRewriter()
+ parser.feed('')
+ result = parser.get_html()
+ assert result == ''
+
@contextmanager
def wait_for_page_load(driver, timeout=30):