| 182 | |
| 183 | |
| 184 | def lt_findings(text, rel, lt_ctx): |
| 185 | if not text.strip(): |
| 186 | return [] |
| 187 | mod, tool, accept_re = lt_ctx["mod"], lt_ctx["tool"], lt_ctx["accept_re"] |
| 188 | try: |
| 189 | html = blog_text.body_to_html(text) |
| 190 | except ImportError as exc: |
| 191 | # python-markdown not installed — degrade gracefully (Vale + cap run). |
| 192 | sys.stderr.write(f"python-markdown unavailable; skipping LanguageTool: {exc}\n") |
| 193 | return None |
| 194 | with tempfile.NamedTemporaryFile( |
| 195 | "w", suffix=".html", delete=False, encoding="utf-8" |
| 196 | ) as tf: |
| 197 | tf.write(html) |
| 198 | tmp = tf.name |
| 199 | try: |
| 200 | extracted = mod.extract_text(tmp) |
| 201 | finally: |
| 202 | os.unlink(tmp) |
| 203 | disabled = set(mod.DISABLED_RULES) |
| 204 | out = [] |
| 205 | for global_offset, chunk in mod.chunk_text(extracted): |
| 206 | try: |
| 207 | matches = tool.check(chunk) |
| 208 | except Exception as exc: # noqa: BLE001 — advisory check must never crash the gate |
| 209 | sys.stderr.write(f"LanguageTool failed on {rel}: {exc}\n") |
| 210 | continue |
| 211 | for m in matches: |
| 212 | rid = mod._attr(m, "rule_id", "ruleId", default="") |
| 213 | # Skip rules disabled in run_languagetool AND the blog stylistic |
| 214 | # deny-list, so the gate agrees with the findings collector the sweep |
| 215 | # used (otherwise a grammar fix like "all of" -> "all of the" trips |
| 216 | # ALL_OF_THE and the gate fails on the sweep's own improvements). |
| 217 | if rid in disabled or rid in cbf.LT_STYLISTIC_DENY or any( |
| 218 | rid.startswith(p) for p in cbf.LT_STYLISTIC_DENY_PREFIXES |
| 219 | ): |
| 220 | continue |
| 221 | flagged = mod._flagged_text(m) |
| 222 | loff = mod._attr(m, "offset", default=0) |
| 223 | llen = mod._attr(m, "error_length", "errorLength", default=0) |
| 224 | after = chunk[loff + llen:loff + llen + 64] |
| 225 | if mod.is_accepted(flagged, accept_re, surrounding_after=after): |
| 226 | continue |
| 227 | out.append({ |
| 228 | # Key on the flagged token, NOT the surrounding context snippet: |
| 229 | # a nearby edit shifts the snippet and would otherwise make a |
| 230 | # pre-existing, deliberately-kept finding (e.g. "thru", a proper |
| 231 | # name, a British spelling) look "net-new". |
| 232 | "signature": ("lt", rid, flagged), |
| 233 | "file": rel, |
| 234 | "line": extracted.count("\n", 0, global_offset + m.offset) + 1, |
| 235 | "message": f"{rid}: {mod._attr(m, 'message', default='')} [{flagged}]", |
| 236 | }) |
| 237 | return out |
| 238 | |
| 239 | |
| 240 | # --------------------------------------------------------------------------- # |