feat(diff_report): render formatting_changes as a per-pair block
Adds a "🎨 Formatting changes" block to the per-page diff report
when the deterministic formatting layer finds typographic flips.
Distinguishes page-wide style shifts from local span flips, lists up
to three example quotes per aggregated finding, and HTML-escapes all
user-controlled strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b1bb9ccf0
commit
7eaac85df3
2 changed files with 106 additions and 0 deletions
|
|
@ -75,6 +75,46 @@ def _render_diff_list(items: List[str], css_class: str, label: str, icon: str) -
|
|||
"""
|
||||
|
||||
|
||||
def _render_formatting_block(findings: List[Dict]) -> str:
|
||||
if not findings:
|
||||
return ''
|
||||
|
||||
def _fmt_value(v):
|
||||
if isinstance(v, bool):
|
||||
return 'Bold' if v else 'Regular'
|
||||
return str(v)
|
||||
|
||||
items = []
|
||||
for f in findings:
|
||||
attr = f.get('attribute', '')
|
||||
old_v = _fmt_value(f.get('old_value'))
|
||||
new_v = _fmt_value(f.get('new_value'))
|
||||
total = f.get('total_span_count', 0)
|
||||
page_wide = f.get('page_wide', False)
|
||||
quotes = f.get('example_quotes', []) or []
|
||||
|
||||
if page_wide:
|
||||
prefix = f"<strong>Page-wide {html.escape(attr)} change</strong>: {html.escape(old_v)} → {html.escape(new_v)}"
|
||||
else:
|
||||
prefix = f"<strong>{html.escape(attr).capitalize()}: {html.escape(old_v)} → {html.escape(new_v)}</strong>"
|
||||
|
||||
quote_html = ''
|
||||
if quotes:
|
||||
quoted = ', '.join(f'“{html.escape(q)}”' for q in quotes[:3])
|
||||
extra = total - len(quotes[:3])
|
||||
extra_html = f" <span class='muted'>…and {extra} more</span>" if extra > 0 else ''
|
||||
quote_html = f" ({total} span{'s' if total != 1 else ''}): {quoted}{extra_html}"
|
||||
|
||||
items.append(f"<li>{prefix}{quote_html}</li>")
|
||||
|
||||
return f"""
|
||||
<div class='diff-block block-style'>
|
||||
<div class='diff-label'>🎨 Formatting changes</div>
|
||||
<ul>{''.join(items)}</ul>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _render_pair_card(entry: Dict, pair_diffs: Dict) -> str:
|
||||
old = entry['old_page']
|
||||
new = entry['new_page']
|
||||
|
|
@ -132,6 +172,7 @@ def _render_pair_card(entry: Dict, pair_diffs: Dict) -> str:
|
|||
blocks.append(_render_diff_list(pair.get('modified') or [], 'block-modified', 'Modified', '✎'))
|
||||
blocks.append(_render_diff_list(pair.get('moved') or [], 'block-moved', 'Moved', '↔'))
|
||||
blocks.append(_render_diff_list(pair.get('style_changes') or [], 'block-style', 'Style changes', '🎨'))
|
||||
blocks.append(_render_formatting_block(pair.get('formatting_changes') or []))
|
||||
|
||||
error_block = ''
|
||||
if pair.get('error'):
|
||||
|
|
|
|||
65
backend/tests/test_diff_report_formatting_block.py
Normal file
65
backend/tests/test_diff_report_formatting_block.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Smoke test for the new formatting-changes rendering block."""
|
||||
|
||||
from document_mode.diff_report_writer import _render_formatting_block
|
||||
|
||||
|
||||
def test_empty_findings_render_nothing():
|
||||
assert _render_formatting_block([]) == ''
|
||||
|
||||
|
||||
def test_single_bold_flip_renders_with_quote():
|
||||
findings = [{
|
||||
'attribute': 'bold',
|
||||
'old_value': True,
|
||||
'new_value': False,
|
||||
'example_quotes': ['Theft of personal belongings'],
|
||||
'total_span_count': 1,
|
||||
'page_wide': False,
|
||||
}]
|
||||
html_out = _render_formatting_block(findings)
|
||||
assert '🎨 Formatting changes' in html_out
|
||||
assert 'Theft of personal belongings' in html_out
|
||||
assert 'Bold' in html_out
|
||||
assert 'Regular' in html_out
|
||||
assert 'block-style' in html_out
|
||||
|
||||
|
||||
def test_page_wide_flag_changes_label():
|
||||
findings = [{
|
||||
'attribute': 'font',
|
||||
'old_value': 'AXASans-Regular',
|
||||
'new_value': 'Helvetica',
|
||||
'example_quotes': ['Some body text'],
|
||||
'total_span_count': 17,
|
||||
'page_wide': True,
|
||||
}]
|
||||
html_out = _render_formatting_block(findings)
|
||||
assert 'Page-wide font change' in html_out
|
||||
|
||||
|
||||
def test_html_escape_in_quotes():
|
||||
findings = [{
|
||||
'attribute': 'bold',
|
||||
'old_value': True,
|
||||
'new_value': False,
|
||||
'example_quotes': ['<script>alert("xss")</script>'],
|
||||
'total_span_count': 1,
|
||||
'page_wide': False,
|
||||
}]
|
||||
html_out = _render_formatting_block(findings)
|
||||
assert '<script>' not in html_out
|
||||
assert '<script>' in html_out
|
||||
|
||||
|
||||
def test_aggregated_finding_shows_and_x_more():
|
||||
findings = [{
|
||||
'attribute': 'bold',
|
||||
'old_value': True,
|
||||
'new_value': False,
|
||||
'example_quotes': ['First quote', 'Second quote', 'Third quote'],
|
||||
'total_span_count': 12,
|
||||
'page_wide': False,
|
||||
}]
|
||||
html_out = _render_formatting_block(findings)
|
||||
assert '12 spans' in html_out
|
||||
assert 'and 9 more' in html_out
|
||||
Loading…
Add table
Reference in a new issue