diff --git a/backend/document_mode/diff_report_writer.py b/backend/document_mode/diff_report_writer.py
index 9770590..93c730b 100644
--- a/backend/document_mode/diff_report_writer.py
+++ b/backend/document_mode/diff_report_writer.py
@@ -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"Page-wide {html.escape(attr)} change: {html.escape(old_v)} β {html.escape(new_v)}"
+ else:
+ prefix = f"{html.escape(attr).capitalize()}: {html.escape(old_v)} β {html.escape(new_v)}"
+
+ quote_html = ''
+ if quotes:
+ quoted = ', '.join(f'“{html.escape(q)}”' for q in quotes[:3])
+ extra = total - len(quotes[:3])
+ extra_html = f" β¦and {extra} more" if extra > 0 else ''
+ quote_html = f" ({total} span{'s' if total != 1 else ''}): {quoted}{extra_html}"
+
+ items.append(f"
{prefix}{quote_html}")
+
+ return f"""
+
+
π¨ Formatting changes
+
+
+ """
+
+
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'):
diff --git a/backend/tests/test_diff_report_formatting_block.py b/backend/tests/test_diff_report_formatting_block.py
new file mode 100644
index 0000000..938a83c
--- /dev/null
+++ b/backend/tests/test_diff_report_formatting_block.py
@@ -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': [''],
+ 'total_span_count': 1,
+ 'page_wide': False,
+ }]
+ html_out = _render_formatting_block(findings)
+ assert '