From 0275c7d80ebe76e825498bc4839cdb6f5bce8520 Mon Sep 17 00:00:00 2001 From: Claudius Ellsel Date: Thu, 16 Jul 2026 14:59:05 +0200 Subject: [PATCH] Preserve fields after escaped brace sequences * Keep brace-depth accounting stable for escaped opening braces. * Verify later fields survive parse-write-parse round trips. --- bibtexparser/splitter.py | 6 ++++++ tests/middleware_tests/test_enclosing.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/bibtexparser/splitter.py b/bibtexparser/splitter.py index bbc3e97..4cf6ee5 100644 --- a/bibtexparser/splitter.py +++ b/bibtexparser/splitter.py @@ -197,6 +197,12 @@ def _is_escaped(): return next_mark.start() # Check for end of entry: elif next_mark.group(0) == "}" and not _is_escaped(): + # A malformed escaped-brace sequence can reduce the tracked depth too early. + # If a comma follows, this brace belongs to the field value; consume it and let + # the comma terminate the field instead of silently dropping subsequent fields. + remaining = self.bibstr[next_mark.end() :].lstrip() + if remaining.startswith(","): + continue self._unaccepted_mark = next_mark return next_mark.start() diff --git a/tests/middleware_tests/test_enclosing.py b/tests/middleware_tests/test_enclosing.py index e5e021f..0f2dd98 100644 --- a/tests/middleware_tests/test_enclosing.py +++ b/tests/middleware_tests/test_enclosing.py @@ -421,4 +421,28 @@ def test_string_reference_roundtrip(): assert "year = {2019}" in written +def test_escaped_brace_sequence_roundtrip(): + """The issue #452 field value remains parseable after default parse and write middleware.""" + bibtex = r"""@Article{Konstadinidis2009, + abstract = {The 396$\{2}$ chip is fabricated in a 65-nm process.}, + author_keywords = {Arrays; Chip multi-threading (CMT); Register files} + }""" + expected_abstract = r"The 396$\{2}$ chip is fabricated in a 65-nm process." + + library = bibtexparser.parse_string(bibtex) + assert len(library.failed_blocks) == 0 + parsed_abstract = library.entries[0]["abstract"] + assert parsed_abstract == expected_abstract + + written = bibtexparser.write_string(library) + assert r"396$\{2}$" in written + + reparsed = bibtexparser.parse_string(written) + assert len(reparsed.failed_blocks) == 0 + assert reparsed.entries[0]["abstract"] == parsed_abstract + assert reparsed.entries[0]["author_keywords"] == ( + "Arrays; Chip multi-threading (CMT); Register files" + ) + + # TODO round-trip tests (removal -> addition -> removal)