feat(gui): visualize leading/trailing whitespace in analyzer findings
The analyzer's "Run Analysis" panel rendered sample cells via st.dataframe,
which (a) silently collapses leading/trailing ASCII whitespace and (b)
displays NBSP/ZWSP/control chars as nothing. The user couldn't see the
exact pollution they were being told about.
visualize_hidden_html gains a mark_outer_whitespace=True option that
wraps each leading and trailing ASCII space/tab in its own badge with a
"SP LEAD" / "SP TRAIL" tooltip. The badges are per-character so the
user can count exactly how much padding the cleaner will strip.
components.render_findings_panel now:
- injects hidden_char_css() once at the top of the panel
- replaces st.dataframe(samples) with a custom HTML table
- renders the value column with mark_outer_whitespace=True
- applies white-space: pre-wrap on value cells so any internal ASCII
whitespace also stays visible (browsers collapse runs by default)
Four new tests cover: leading+trailing badge counts, default-off
behaviour, leading tab badge, all-whitespace string treated entirely
as leading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -750,11 +750,16 @@ def render_findings_panel(findings, *, header: str = "Detected issues") -> None:
|
||||
the user can decide which tool to open first.
|
||||
"""
|
||||
from src.core.analyze import findings_by_tool # local import to avoid cycle
|
||||
from src.core.text_clean import hidden_char_css
|
||||
|
||||
if not findings:
|
||||
st.success("No issues detected. Open any tool below to start working.")
|
||||
return
|
||||
|
||||
# Inject the hidden-char badge styles once so every sample value below
|
||||
# can render leading/trailing whitespace and invisibles as visible badges.
|
||||
st.markdown(hidden_char_css() + _SAMPLE_TABLE_CSS, unsafe_allow_html=True)
|
||||
|
||||
by_sev: dict[str, int] = {}
|
||||
for f in findings:
|
||||
by_sev[f.severity] = by_sev.get(f.severity, 0) + 1
|
||||
@@ -792,7 +797,35 @@ def render_findings_panel(findings, *, header: str = "Detected issues") -> None:
|
||||
_render_one_finding(f)
|
||||
|
||||
|
||||
_SAMPLE_TABLE_CSS = """
|
||||
<style>
|
||||
.findings-sample-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.findings-sample-table th,
|
||||
.findings-sample-table td {
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.findings-sample-table td.value {
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
/* pre-wrap so any ASCII whitespace inside the value is preserved
|
||||
visually (browsers collapse adjacent spaces by default). */
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.findings-sample-table tbody tr:hover { background: #fafafa; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def _render_one_finding(f) -> None:
|
||||
from src.core.text_clean import visualize_hidden_html
|
||||
|
||||
color = _SEVERITY_COLOR[f.severity]
|
||||
icon = _SEVERITY_ICON[f.severity]
|
||||
column_part = f" in `{f.column}`" if getattr(f, "column", None) else ""
|
||||
@@ -800,10 +833,34 @@ def _render_one_finding(f) -> None:
|
||||
f"{icon} :{color}[**{f.id}**]{column_part} — {f.description}"
|
||||
)
|
||||
if f.samples:
|
||||
sample_df = pd.DataFrame(
|
||||
f.samples, columns=["row", "column", "value"],
|
||||
# Render samples as an HTML table so leading/trailing whitespace
|
||||
# and invisible characters in the value column show up as badges.
|
||||
# A plain st.dataframe collapses outer whitespace and renders
|
||||
# NBSP/ZWSP as nothing, defeating the point of the audit.
|
||||
rows_html = []
|
||||
for row, col, value in f.samples:
|
||||
rendered_value = visualize_hidden_html(
|
||||
str(value), mark_outer_whitespace=True,
|
||||
)
|
||||
rendered_col = visualize_hidden_html(
|
||||
str(col), mark_outer_whitespace=True,
|
||||
)
|
||||
rows_html.append(
|
||||
"<tr>"
|
||||
f"<td>{int(row) + 1 if isinstance(row, int) else row}</td>"
|
||||
f"<td><code>{rendered_col}</code></td>"
|
||||
f"<td class='value'>{rendered_value}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
st.markdown(
|
||||
"<table class='findings-sample-table'>"
|
||||
"<thead><tr>"
|
||||
"<th>Row</th><th>Column</th><th>Value</th>"
|
||||
"</tr></thead>"
|
||||
f"<tbody>{''.join(rows_html)}</tbody>"
|
||||
"</table>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
st.dataframe(sample_df, use_container_width=True, hide_index=True)
|
||||
|
||||
|
||||
def upload_and_analyze_section() -> None:
|
||||
|
||||
Reference in New Issue
Block a user