Preserve whitespace after inline document citations - #1291
Merged
Paul Lizer (paullizer) merged 1 commit intoAug 19, 2026
Merged
Conversation
parseCitations() matched the trailing [#citation-id] marker together with the whitespace that followed it, but rebuilt the citation without putting that whitespace back. Because it runs on raw markdown before marked.parse(), a deleted blank line changed how the rest of the block parsed, so a paragraph after a cited list item was absorbed into the list item and sentences ran together with no space after ")". The callback now captures the trailing whitespace off the matched bracket group and re-emits it exactly as the model wrote it, so a citation followed immediately by punctuation still renders byte-identically. The leftover [#guid] cleanup pass had the same defect in reverse via its leading greedy \s*, which could swallow the blank line before a stray marker. It is now three ordered passes: whole-line bracket runs are removed with their line, line-opening runs with their trailing spacing, and inline runs consume only spaces and tabs. Fixes #1289 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
force-pushed
the
paullizer-fix-citation-whitespace-collapse
branch
from
August 19, 2026 00:31
a1e62eb to
2cc66a1
Compare
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.
Fixes #1289
What was wrong
Text following an inline document citation was jammed onto the end of the closing parenthesis instead of starting a new paragraph:
...support cited answers in chat. (Source: application_workflows.md, Page: 1)Admins can configure......warrants it (Source: document-intelligence.md, Page: 1)For best results......(Source: uploading_documents.md, Page: 1)Thank you, Paul.Root cause
parseCitations()inchat-citations.jsmatches citations with:/\(Source:\s*(...),\s*(Page(?:s)?|Sheet(?:s)?|Location):\s*(...)\)\s*((?:\[#.*?\]\s*)+)/giThe trailing
\s*inside the repeated bracket group is greedy and matches newlines, so it consumed the whitespace after the last[#citation-id]marker. The replacement callback returned only the rebuilt(Source: ...)string and never put that whitespace back.parseCitations()runs on raw markdown beforemarked.parse()inrenderAiMessageContent(), so a deleted\n\ndidn't just remove a space — it changed how markdown parsed the remainder of the block. That's why the paragraph after the first citation was absorbed into numbered list item 5.Reproduced in isolation against the production regex:
The leftover
[#guid]cleanup pass a few lines below had the same class of defect in the opposite direction — its leading greedy\s*could swallow the blank line before a stray marker that opened a paragraph.The fix
[#guid]cleanup is now three ordered passes so line structure survives: a bracket run occupying a whole line is removed with its line, a run opening a line is removed with its trailing spacing, and any remaining inline run consumes only spaces and tabs. Consecutive runs like[#id-a] [#id-b]are handled as a unit.These are whitespace-only changes. The emitted citation HTML is unchanged, so escaping, sanitization, and the XSS surface are untouched.
Before / after
Rendered HTML for the reported message, before:
After:
Testing
functional_tests/test_chat_citation_whitespace_preservation.pyexecutes the realparseCitations()in a Node sandbox and renders the result with the vendoredmarkedbundle, so it asserts user-visible block structure rather than regex output.[#guid]cleanup keeps line structureAll four fail against the pre-fix source. Each half of the fix was reverted independently to confirm both are covered.
ui_tests/test_chat_citation_paragraph_spacing.pyseeds the reported message into the chat page and asserts the DOM: the numbered list item ends at its citation, the following text renders as its own<p>, and no sentence collides with a citation's closing parenthesis. Env-gated like the other UI tests.Related suites re-run clean:
test_agent_document_search_citations.py,test_markdown_citation_lookup_fallback.py,test_chat_cited_source_tracking.py,test_unicode_table_conversion.py,test_comprehensive_table_support.py,test_stored_xss_chat_workspace_rendering_fix.py,test_external_links_new_window.py,test_azure_maps_tile_token_refresh_fix.py.Three unrelated tests fail (
test_agent_citation_full_results_modal.py,test_stored_xss_chat_modal_filename_fix.py,test_file_upload_document_ingestion_security_audit.py), verified as already failing on the base branch before this change.A review pass measured ReDoS behaviour on adversarial input (no new blowup; the pre-existing
[^\]]*\]cost is unchanged) and probed CRLF, fenced code blocks, tables, blockquotes, headings, and list markers against the^/manchors. Every case where the new cleanup differs from the old is strictly less destructive — the old pass could break a fenced code block by pulling its closing fence onto the previous line.Side effects
copyMarkdownis built from the same parsed output, so copied and exported message text keeps its line breaks too.Docs and version
docs/explanation/fixes/CHAT_CITATION_WHITESPACE_COLLAPSE_FIX.mdconfig.pybumped to0.250.229Rebased onto
Developmentafter #1290 landed, which had claimed v0.250.228. The release notes conflict was resolved by moving this entry into a new v0.250.229 section above it; the diff againstDevelopmentis these 7 files only.