mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Count em-dash HTML entities in em-dash-overuse
The em-dash-overuse text analyzer ran stripHtmlToText over raw markup, which drops tags but leaves character entities intact. A model that wrote —, —, or — rendered a real em-dash the counter never saw, so 12 entity-escaped dashes on a live page slipped through. Decode the em-dash entities (named, zero-padded decimal, upper/lower hex) to the literal glyph before counting. En-dash entities stay untouched: the rule counts em-dashes, and the literal en-dash was never counted either. The gap lived only in the regex / static-HTML path (detectText and detect-html's runTextContentAnalyzers, both over raw HTML). The browser adapter never ran this analyzer, so build:browser and build:extension produce no diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0376145a46
commit
7dcca2bb36
@@ -309,8 +309,16 @@ const REGEX_ANALYZERS = [
|
||||
// Em-dash overuse: 5+ em-dashes or "--" in body text content
|
||||
// (occasional em-dash use in prose is fine; the pattern fires only
|
||||
// when count crosses into AI-cadence territory).
|
||||
//
|
||||
// stripHtmlToText drops tags but leaves character-entity escapes intact, so
|
||||
// a model that writes `—`, `—`, or `—` renders an em-dash
|
||||
// the counter never saw. Decode the em-dash entities (named, zero-padded
|
||||
// decimal, upper/lower hex) to the literal glyph first. En-dash entities are
|
||||
// deliberately left alone: the rule counts em-dashes, and the literal `–`
|
||||
// was never counted either.
|
||||
(content, filePath) => {
|
||||
const text = stripHtmlToText(content);
|
||||
const text = stripHtmlToText(content)
|
||||
.replace(/—|�*8212;|�*2014;/gi, '—');
|
||||
let count = 0;
|
||||
const re = /[—]|--(?=\S)/g;
|
||||
while (re.exec(text) !== null) count++;
|
||||
|
||||
@@ -917,3 +917,80 @@ describe('detectHtml — generated-UI tells', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('em-dash overuse — HTML entity escapes', () => {
|
||||
// Build a full page so the page-level text-content analyzer runs. `body` is the
|
||||
// prose that carries the dashes; the doctype/html scaffold is required by
|
||||
// isFullPage(). Each dash spelling is a separate case because the rule counts
|
||||
// per page, not per element.
|
||||
const page = (body) =>
|
||||
`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>t</title></head>` +
|
||||
`<body><main><h1>A real page heading of ordinary length</h1><p>${body}</p></main></body></html>`;
|
||||
|
||||
// Six dashes clears the 5+ threshold. Sentence fragments keep the surrounding
|
||||
// prose realistic so nothing else in the pipeline objects.
|
||||
const sixNamed = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
const sixNumeric = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
const sixHex = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
const sixHexUpper = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
const sixNumericPadded = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
// Three literal glyphs + three named entities render identically; the count
|
||||
// must see all six.
|
||||
const mixed = 'fast — cheap — honest — simple — quiet — kind — done';
|
||||
|
||||
const SHOULD_FLAG = {
|
||||
'named —': sixNamed,
|
||||
'numeric —': sixNumeric,
|
||||
'hex —': sixHex,
|
||||
'uppercase-hex —': sixHexUpper,
|
||||
'zero-padded decimal —': sixNumericPadded,
|
||||
'mixed literal + entity': mixed,
|
||||
};
|
||||
|
||||
// False-positive shapes: none of these should trip the em-dash counter.
|
||||
const SHOULD_PASS = {
|
||||
// Below the 5+ threshold: occasional em-dash entity use is legitimate prose.
|
||||
'two entities below threshold': 'fast — cheap — done, otherwise plain sentences fill the paragraph body',
|
||||
// En-dashes are a different character and a different job (ranges); the em-dash
|
||||
// rule must not decode or count them.
|
||||
'en-dash entities': 'pages 10–20 and 30–40 and 50–60 and 70–80 and 90–100 and 1–2',
|
||||
'numeric en-dash entities': 'pages 10–20 and 30–40 and 50–60 and 70–80 and 90–100 and 1–2',
|
||||
// Double-escaped: the visible text is the literal string "—", not a dash.
|
||||
'double-escaped ampersand': 'write &mdash; and &mdash; and &mdash; and &mdash; and &mdash; and &mdash; literally',
|
||||
// Unrelated entities must never be miscounted as dashes.
|
||||
'non-dash entities': 'a b © c … d & e ™ f ® g ° h § i ¶',
|
||||
// Ordinary hyphenated compounds are single hyphens, not the double-hyphen tell.
|
||||
'hyphenated compounds': 'state-of-the-art, well-being, high-quality, self-service, end-to-end, at-a-glance copy',
|
||||
};
|
||||
|
||||
const emDashCount = (findings) =>
|
||||
findings.filter((r) => r.antipattern === 'em-dash-overuse').length;
|
||||
|
||||
for (const [label, body] of Object.entries(SHOULD_FLAG)) {
|
||||
it(`flags em-dash overuse spelled as ${label}`, () => {
|
||||
const findings = detectText(page(body), 'em-dash.html');
|
||||
assert.equal(
|
||||
emDashCount(findings), 1,
|
||||
`expected em-dash-overuse for "${label}", got: ${findings.map((r) => r.antipattern).join(', ') || 'none'}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, body] of Object.entries(SHOULD_PASS)) {
|
||||
it(`does not flag ${label}`, () => {
|
||||
const findings = detectText(page(body), 'em-dash.html');
|
||||
assert.equal(
|
||||
emDashCount(findings), 0,
|
||||
`"${label}" should not flag em-dash overuse`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it('static-HTML path decodes entity em-dashes too (fixture file)', async () => {
|
||||
const findings = await detectHtml(path.join(FIXTURES, 'em-dash-entities.html'));
|
||||
assert.equal(
|
||||
findings.filter((r) => r.antipattern === 'em-dash-overuse').length, 1,
|
||||
'em-dash-entities.html should flag em-dash overuse via the static-HTML path',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Em-dash entity overuse</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>A page whose em-dashes hide inside HTML entities</h1>
|
||||
<p>
|
||||
The product is fast — it is also cheap — and it is honest
|
||||
— which matters — more than speed — or price
|
||||
— in the long run.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user