Resolve string concatenation expressions in field values (#396) - #565
Resolve string concatenation expressions in field values (#396)#565gaoflow wants to merge 2 commits into
Conversation
ResolveStringReferencesMiddleware only resolved a field whose entire value was a single string reference. A concatenation expression such as `10 # "~" # jan` was looked up as one key, failed, and was left raw (issue sciunto-org#396); an all-quoted expression like `"a" # "b"` was skipped by the enclosed-value guard and later mangled by RemoveEnclosingMiddleware. Detect top-level `#` concatenation (respecting quotes and braces), resolve each token (number, quoted/braced literal, or string reference) and join the results. The resolved value is kept enclosed in braces so the downstream enclosing middlewares treat it as a plain string and it round-trips to valid BibTeX. If any reference is unknown, the original expression is left untouched.
|
I independently explored a broader version of this fix while auditing the parser's round-trip behavior. I have published the result as a comparison branch rather than opening a competing PR: In addition to resolving ordinary entry-field concatenations, the branch tests and handles:
The branch has two commits so the expression resolver and the enclosing-middleware safeguard can be reviewed independently. Validation is clean: For transparency, this comparison implementation and its tests were prepared with ChatGPT Codex using GPT-5.6 Sol with high reasoning effort. They should still receive normal maintainer review, especially because the work deliberately covers more cases than the original report. |
…intact The remaining places where a value that is a `#` expression is treated as a single token: - A string definition was substituted verbatim, so a definition that is itself an expression or another reference was spliced into the field unevaluated (`asiacrypt91name = asiacryptname # "'91"` from sciunto-org#396). Definitions are now resolved up front, iteratively, so chains resolve and a reference cycle simply never resolves instead of producing a partial value. - `RemoveEnclosingMiddleware._strip_enclosing` took the first and last character of an expression for one enclosing pair, so an expression bounded by delimiters was corrupted, both in a field (`"a" # nope # "b"`, when nothing resolves it first) and in a `@string` block, where a valid file was rewritten as `@string{s = {Asia" # "crypt}}`. Both middlewares now share one expression scanner, which follows the escaping the splitter applies (a delimiter after a backslash is content). Cases and the comparison branch that prompted them: @claell on sciunto-org#565. Co-authored-by: Claudius Ellsel <claudius.ellsel@live.de>
|
Thanks @claell — the comparison branch was useful and I've folded the cases in, with credit on the commit. Concatenation inside I went a different way twice on the implementation. Per-reference recursion trades the cycle case for a new one on chains — 2588 passed, 12 skipped (2579 before), full pre-commit suite clean. |
Fixes #396. Three places treated a value that is a
#expression as a single token.Concatenation in an entry field was not interpolated.
ResolveStringReferencesMiddlewareonly handled a value that was one whole reference, somonth = 10 # "~" # janwas looked up as a single key and left raw.A string definition was substituted verbatim. A definition that is itself an expression or another reference was spliced into the field unevaluated, so
asiacrypt91name = asiacryptname # "'91"(the case @kmccurley raised in the issue) gavebooktitlethe valueasiacryptname # "'91".RemoveEnclosingMiddlewaretook the first and last character of an expression for one enclosing pair.title = "hello" # " " # "world"becamehello" # " " # "world, and@string{s = "Asia" # "crypt"}was rewritten as@string{s = {Asia" # "crypt}}— a valid file corrupted by a round trip.Both middlewares now share one expression scanner, which follows the escaping the splitter applies (a delimiter after a backslash is content). String definitions are resolved up front and iteratively, so chains resolve and a definition on a reference cycle simply never resolves instead of yielding a partial value. A resolved expression is stored brace-enclosed, since unenclosed it would be read back as a reference; an expression that cannot be resolved keeps its exact text and is written back unchanged.
Tests cover the issue repro, definitions that are expressions / chains / cycles, a 500-link chain, and the unresolved-expression round trip. 2588 passed, 12 skipped; full pre-commit suite clean.
@claell published a comparison branch covering several of these cases; credited on the commit, details in a comment below.
This pull request was prepared with the assistance of AI, under my direction and review.