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"") + + 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"") @@ -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"") 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"") + 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"") + 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 &amp; + assert 'data-value="test&value"' in result + assert '&amp;' 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):