Fix HTML entity encoding in graph tooltips

(cherry picked from commit 88025d0a93)
This commit is contained in:
himanshupatro-334 authored and safishamsi committed 2026-09-20 18:01:00 +01:00
1 parent 9a06e95408
commit 785ee5a426
2 files changed
+76 -3

No files matched your search

+3 -3
View File
@@ -519,7 +519,7 @@ def to_html(
"color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}},
"size": round(size, 1),
"font": {"size": font_size, "color": "#ffffff"},
"title": _html.escape(label),
"title": label,
"community": cid,
"community_name": sanitize_label((community_labels or {}).get(cid, f"Community {cid}")),
"source_file": sanitize_label(str(data.get("source_file") or "")),
@@ -555,7 +555,7 @@ def to_html(
lesson = f"Lesson: {status} ({entry.get('uses', 0)} useful)"
if stale:
lesson += " [code changed — re-verify]"
node["title"] = _html.escape(label) + "\n" + _html.escape(sanitize_label(lesson))
node["title"] = f"{label}\n{sanitize_label(lesson)}"
vis_nodes.append(node)
# Build edges list. Restore original edge direction from _src/_tgt
@@ -572,7 +572,7 @@ def to_html(
"from": true_src,
"to": true_tgt,
"label": relation,
"title": _html.escape(f"{relation} [{confidence}]"),
"title": sanitize_label(f"{relation} [{confidence}]"),
"dashes": confidence != "EXTRACTED",
"width": 2 if confidence == "EXTRACTED" else 1,
"color": {"opacity": 0.7 if confidence == "EXTRACTED" else 0.35},
+73
View File
@@ -432,6 +432,13 @@ def _vis_nodes_from_html(content: str) -> list:
return json.loads(m.group(1).replace("<\\/", "</"))
def _vis_edges_from_html(content: str) -> list:
"""Extract the RAW_EDGES JSON array embedded in the generated HTML."""
m = re.search(r"const RAW_EDGES = (\[.*?\]);", content, re.DOTALL)
assert m, "RAW_EDGES not found in HTML"
return json.loads(m.group(1).replace("<\\/", "</"))
def test_to_html_annotated_node_gets_learning_status_and_ring():
"""A node with an overlay entry gets learning_status + learning_stale fields,
a status-colored ring (border), and a Lesson line in its hover title."""
@@ -494,6 +501,72 @@ def test_to_html_unannotated_identical_to_pre_feature():
assert "learning_status" not in ca
def test_to_html_tooltips_preserve_special_characters_without_html_entities(tmp_path):
"""Issue #3664: Node and edge hover tooltips (vis-network title) must preserve
raw characters (&, ', ", <, >) without HTML entity escaping (&amp;, &#x27;, etc.)
because vis-network renders title via innerText rather than innerHTML."""
import networkx as nx
G = nx.Graph()
G.add_node("n1", label="B-Roll & Filming", source_file="src/a.py", community=0)
G.add_node("n2", label="Anna's content batching schedule", source_file="src/b.py", community=0)
G.add_node("n3", label='<Widget prop="value" & count > 0>', source_file="src/c.py", community=0)
G.add_edge("n1", "n2", relation="reviews & approves", confidence="EXTRACTED")
G.add_edge("n2", "n3", relation="calls <indirect>", confidence="INFERRED")
labels = {0: "Team A & Team B"}
out = tmp_path / "graph.html"
to_html(G, {0: ["n1", "n2", "n3"]}, str(out), community_labels=labels)
content = out.read_text(encoding="utf-8")
nodes = {n["id"]: n for n in _vis_nodes_from_html(content)}
assert nodes["n1"]["title"] == "B-Roll & Filming"
assert "&amp;" not in nodes["n1"]["title"]
assert nodes["n2"]["title"] == "Anna's content batching schedule"
assert "&#x27;" not in nodes["n2"]["title"]
assert "&#39;" not in nodes["n2"]["title"]
assert nodes["n3"]["title"] == '<Widget prop="value" & count > 0>'
for entity in ("&lt;", "&gt;", "&quot;", "&amp;"):
assert entity not in nodes["n3"]["title"]
edges = _vis_edges_from_html(content)
edge_map = {(e["from"], e["to"]): e for e in edges}
e1 = edge_map[("n1", "n2")]
assert e1["title"] == "reviews & approves [EXTRACTED]"
assert "&amp;" not in e1["title"]
e2 = edge_map[("n2", "n3")]
assert e2["title"] == "calls <indirect> [INFERRED]"
assert "&lt;" not in e2["title"]
assert "&gt;" not in e2["title"]
# In contrast, legend items are injected into innerHTML and MUST remain HTML-escaped.
assert "Team A &amp; Team B" in content
def test_to_html_learning_overlay_tooltip_preserves_special_characters(tmp_path):
"""Issue #3664: Node with learning overlay preserves special characters in both
the label and lesson text without HTML entity escaping."""
import networkx as nx
G = nx.Graph()
G.add_node("n1", label="Anna's & Bob's <Pipeline>", source_file="src/a.py", community=0)
overlay = {
"n1": {"status": "contested", "uses": 2, "neg": 1, "stale": True}
}
out = tmp_path / "graph.html"
to_html(G, {0: ["n1"]}, str(out), learning_overlay=overlay)
content = out.read_text(encoding="utf-8")
nodes = {n["id"]: n for n in _vis_nodes_from_html(content)}
title = nodes["n1"]["title"]
assert "Anna's & Bob's <Pipeline>\n" in title
assert "Lesson: contested (useful 2 / dead-end 1) [code changed — re-verify]" in title
for entity in ("&amp;", "&#x27;", "&#39;", "&lt;", "&gt;"):
assert entity not in title
def test_to_canvas_file_paths_relative_to_vault():
"""Node file paths in canvas must be vault-root-relative (just fname.md), not hardcoded."""
G = make_graph()