Tokenize backslash escapes by parity in the splitter - #598
Open
gaoflow wants to merge 1 commit into
Open
Conversation
The mark iterator used a fixed-width `(?<!\\)` look-behind, which cannot tell an escaped delimiter from one that merely follows an escaped backslash. A delimiter behind an even-length backslash run was therefore treated as escaped, so the field scan ran past the end of the value: the block failed outright for braced, quoted, `@string`, `@preamble` and `@comment` values, and silently swallowed the following fields for bare and nested ones. Match the escape pairs and drop them instead of looking behind; pairing them left to right gives the parity for free. Newlines are left out of the escapable set, since those marks only advance the line counter and BibTeX has no line continuation - a line ending in a backslash was shifting every later start_line by one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Splitter.splitfinds the structural marks withThat look-behind is fixed width, so all it can ask is "is the previous character a backslash?" — it cannot ask whether that backslash is itself escaped. A delimiter sitting behind an even-length backslash run is treated as escaped when it is not, and the field scan runs past the end of the value.
The library also cannot read back what it writes:
write_stringon a field value ofC:\\emitstitle = {C:\\},, and parsing that gives a failed block.The escape model the rest of the package uses is already parity-based.
parse_single_name_into_partsconsumes the escaped character withnext(nameiter), andNameParts.merge_last_name_firstcounts the trailing backslashes explicitly ("Even number: everything is escaped"). Only the splitter's tokenizer disagrees.Extent
I went over the whole grid rather than the one case I hit: runs of 0-4 backslashes before
},",,and{, in braced / quoted / bare / nested-group values, and in@string,@preambleand@commentblocks. Every even-length run of 2 or more is wrong, in every one of those contexts - 12 divergent rows. The bare-value and nested-group ones are the nasty ones: the entry parses, nothing lands infailed_blocks, and the fields after the value are simply gone. pybtex accepts all 12.The same look-behind also covers
\n, where escaping means nothing at all - newline marks exist only to advance the line counter, and BibTeX has no line continuation. So any line ending in a backslash, andabstract = {First line.\\is ordinary LaTeX, makes the counter skip a line and everystart_lineafter it is off by one.Fix
Match the escape pairs and drop them instead of looking behind. Pairing left to right gives the parity for free:
\\consumes both backslashes so the delimiter after them is a mark again, while\\\}consumes two and then escapes the brace. Newlines stay out of the escapable set.Odd runs still escape the delimiter. That is this parser's deliberate divergence from BibTeX proper, which counts braces literally and has no escape in its scanner at all, and I have not touched it - the new tests pin it in both directions so it cannot be dropped by accident either.
What this does not cover
A value with an odd trailing backslash still writes out as something this parser cannot read back:
title = {C:\},, where the closing brace is escaped under the rule above. Fixing that means deciding whether the writer should brace-protect the value the wayescape_last_slashdoes for names, which is a writer-side call I would rather not make on my own. Happy to follow up if you want it.This is orthogonal to the other splitter work in the queue: #572 is brace-depth accounting inside
_move_to_comma_or_closing_curly_bracket, #573 isnames.py. I checked that the #452 abstract from #572 parses identically before and after this change.One overlap worth flagging: my own #565 adds an
_is_escapedhelper to the middleware layer that deliberately mirrors the splitter's current single-backslash rule. Whichever of the two lands second should bring that helper along to parity so the layers do not disagree - I will do that on the other branch.Validation
tests/splitter_tests/test_splitter_backslash_parity.py, 40 cases, 20 of which fail onmain..pre-commit-config.yamlrevisions.This pull request was prepared with the assistance of AI, under my direction and review.