fix(llm): strip inline HTML + entities from rule text (refs #595)

This commit is contained in:
kert
2026-08-13 12:32:42 -04:00
parent ad6d3f05c1
commit e85147e33f
2 changed files with 46 additions and 8 deletions

View File

@@ -15,6 +15,8 @@ so parsing the comment id is the reliable path to a comment's docket.
from __future__ import annotations from __future__ import annotations
import html
import re
from pathlib import Path from pathlib import Path
from typing import Iterator from typing import Iterator
@@ -123,23 +125,38 @@ def _attachment_text(store: Store, item_key: str) -> str:
return "\n\n".join(parts) return "\n\n".join(parts)
_TAG_RE = re.compile(r"<[^>]+>")
def _strip_html_wrapper(text: str) -> str: def _strip_html_wrapper(text: str) -> str:
"""Drop the FR ``.txt`` attachment's HTML wrapper, if present. """Drop the FR ``.txt`` attachment's HTML wrapper and inline markup.
FR TXT attachments are HTML-wrapped: ``<html><head>...<body><pre>`` FR TXT attachments are HTML-wrapped: ``<html><head>...<body><pre>``
then the plain text, closed with ``</pre></body></html>``. Strip then the plain text, closed with ``</pre></body></html>``. Strip
everything through the first ``<pre>`` and any trailing close tags; everything through the first ``<pre>`` and any trailing close tags;
text without that wrapper passes through unchanged. text without that wrapper passes through unchanged.
The FR text itself also carries inline HTML — e.g. Cloudflare's
obfuscated ``<a href="/cdn-cgi/l/email-protection#...">`` mailto
anchors and page-break anchors. Those are stripped with a plain
``<[^>]+>`` regex (this is a controlled, known corpus — not
attacker-supplied HTML — so no HTML-parsing dependency is needed),
keeping the anchor's inner text. HTML entities (``&amp;`` etc.) are
then unescaped. Plain-text markers like ``[[Page NNNNN]]`` contain no
angle brackets and pass through untouched.
""" """
idx = text.find("<pre>") idx = text.find("<pre>")
if idx == -1: if idx == -1:
return text body = text
body = text[idx + len("<pre>") :] else:
stripped = body.rstrip() body = text[idx + len("<pre>") :]
for suffix in ("</pre></body></html>", "</pre>"): stripped = body.rstrip()
if stripped.endswith(suffix): for suffix in ("</pre></body></html>", "</pre>"):
return stripped[: -len(suffix)] if stripped.endswith(suffix):
return body body = stripped[: -len(suffix)]
break
body = _TAG_RE.sub("", body)
return html.unescape(body)
def _rule_text(store: Store, item_key: str) -> str: def _rule_text(store: Store, item_key: str) -> str:

View File

@@ -145,6 +145,27 @@ class TestRuleDocs:
"item_key": rule_key, "item_key": rule_key,
} }
def test_inline_html_and_entities_stripped(self, store, rule_key, tmp_path):
txt = tmp_path / "2025-13271.txt"
txt.write_text(
"<html><head><title>x</title></head><body><pre>\n"
"[[Page 12345]]\n"
'Contact us at <a href="/cdn-cgi/l/email-protection#x">'
"someone</a> regarding CMS &amp; PFS.\n"
"</pre></body></html>"
)
store.attach_file(rule_key, txt)
docs = list(iter_rule_docs(store))
assert len(docs) == 1
text = docs[0].text
assert "<a href=" not in text
assert "</a>" not in text
assert "someone" in text
assert "CMS & PFS" in text
assert "[[Page 12345]]" in text
def test_pdf_only_falls_back_to_extract_attachment(self, store, rule_key, tmp_path): def test_pdf_only_falls_back_to_extract_attachment(self, store, rule_key, tmp_path):
pdf = tmp_path / "rule.pdf" pdf = tmp_path / "rule.pdf"
pdf.write_bytes(b"%PDF-1.4 fake") pdf.write_bytes(b"%PDF-1.4 fake")