Skip page-level checks on partials/components

Page-level typography checks (flat hierarchy, single font, overused font)
now only run on files that look like full pages (have <!DOCTYPE, <html>,
or <head> tags). Partials and components still get element-level border
checks.

isFullPage() strips HTML comments before checking to avoid false matches
on prose that mentions tag names.

Added partial-component.html fixture that has Inter, flat sizes, and a
side-tab border — verifies only the border is flagged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-17 11:49:32 -07:00
co-authored by Claude Opus 4.6
parent 37393f1793
commit ed1579ee37
4 changed files with 101 additions and 15 deletions
@@ -79,6 +79,13 @@ const ANTIPATTERNS = [
},
];
/** Check if content looks like a full page (not a component/partial) */
function isFullPage(content) {
// Strip HTML comments before checking — they might mention <html>/<head> in prose
const stripped = content.replace(/<!--[\s\S]*?-->/g, '');
return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripped);
}
function getAP(id) {
return ANTIPATTERNS.find(a => a.id === id);
}
@@ -289,9 +296,11 @@ async function detectHtml(filePath) {
}
}
// Page-level typography checks
for (const f of checkPageTypography(document, window)) {
findings.push(finding(f.id, filePath, f.snippet));
// Page-level typography checks (only for full pages, not partials)
if (isFullPage(html)) {
for (const f of checkPageTypography(document, window)) {
findings.push(finding(f.id, filePath, f.snippet));
}
}
window.close();
@@ -540,8 +549,11 @@ function detectText(content, filePath) {
}
}
for (const analyzer of REGEX_ANALYZERS) {
findings.push(...analyzer(content, filePath));
// Page-level analyzers only run on full pages
if (isFullPage(content)) {
for (const analyzer of REGEX_ANALYZERS) {
findings.push(...analyzer(content, filePath));
}
}
return findings;
@@ -712,7 +724,7 @@ if (isMainModule) main();
export {
ANTIPATTERNS, SAFE_TAGS, OVERUSED_FONTS, GENERIC_FONTS,
checkElementBorders, checkPageTypography, isNeutralColor,
checkElementBorders, checkPageTypography, isNeutralColor, isFullPage,
detectHtml, detectUrl, detectText,
walkDir, formatFindings, SCANNABLE_EXTENSIONS, SKIP_DIRS,
};
@@ -79,6 +79,13 @@ const ANTIPATTERNS = [
},
];
/** Check if content looks like a full page (not a component/partial) */
function isFullPage(content) {
// Strip HTML comments before checking — they might mention <html>/<head> in prose
const stripped = content.replace(/<!--[\s\S]*?-->/g, '');
return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripped);
}
function getAP(id) {
return ANTIPATTERNS.find(a => a.id === id);
}
@@ -289,9 +296,11 @@ async function detectHtml(filePath) {
}
}
// Page-level typography checks
for (const f of checkPageTypography(document, window)) {
findings.push(finding(f.id, filePath, f.snippet));
// Page-level typography checks (only for full pages, not partials)
if (isFullPage(html)) {
for (const f of checkPageTypography(document, window)) {
findings.push(finding(f.id, filePath, f.snippet));
}
}
window.close();
@@ -540,8 +549,11 @@ function detectText(content, filePath) {
}
}
for (const analyzer of REGEX_ANALYZERS) {
findings.push(...analyzer(content, filePath));
// Page-level analyzers only run on full pages
if (isFullPage(content)) {
for (const analyzer of REGEX_ANALYZERS) {
findings.push(...analyzer(content, filePath));
}
}
return findings;
@@ -712,7 +724,7 @@ if (isMainModule) main();
export {
ANTIPATTERNS, SAFE_TAGS, OVERUSED_FONTS, GENERIC_FONTS,
checkElementBorders, checkPageTypography, isNeutralColor,
checkElementBorders, checkPageTypography, isNeutralColor, isFullPage,
detectHtml, detectUrl, detectText,
walkDir, formatFindings, SCANNABLE_EXTENSIONS, SKIP_DIRS,
};
+54 -3
View File
@@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
import {
ANTIPATTERNS, checkElementBorders, isNeutralColor,
ANTIPATTERNS, checkElementBorders, isNeutralColor, isFullPage,
detectHtml, detectText,
walkDir, SCANNABLE_EXTENSIONS,
} from '../source/skills/critique/scripts/detect-antipatterns.mjs';
@@ -145,12 +145,14 @@ describe('detectText — overused fonts', () => {
describe('detectText — flat type hierarchy', () => {
test('flags sizes too close together', () => {
const f = detectText('h1{font-size:18px}h2{font-size:16px}h3{font-size:15px}p{font-size:14px}.s{font-size:13px}', 'test.css');
const page = '<!DOCTYPE html><html><style>h1{font-size:18px}h2{font-size:16px}h3{font-size:15px}p{font-size:14px}.s{font-size:13px}</style></html>';
const f = detectText(page, 'test.html');
expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true);
});
test('passes good hierarchy', () => {
const f = detectText('h1{font-size:48px}h2{font-size:32px}p{font-size:16px}.s{font-size:12px}', 'test.css');
const page = '<!DOCTYPE html><html><style>h1{font-size:48px}h2{font-size:32px}p{font-size:16px}.s{font-size:12px}</style></html>';
const f = detectText(page, 'test.html');
expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0);
});
});
@@ -192,6 +194,14 @@ describe('detectHtml — jsdom', () => {
expect(cleanFindings).toHaveLength(0);
});
test('partial-component: flags borders, skips page-level', async () => {
const f = await detectHtml(path.join(FIXTURES, 'partial-component.html'));
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0);
expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0);
expect(f.filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
});
test('legitimate-borders has minimal false positives', async () => {
const f = await detectHtml(path.join(FIXTURES, 'legitimate-borders.html'));
const borderFindings = f.filter(r => r.antipattern === 'side-tab' || r.antipattern === 'border-accent-on-rounded');
@@ -212,6 +222,47 @@ describe('detectHtml — jsdom', () => {
});
});
// ---------------------------------------------------------------------------
// Full page vs partial detection
// ---------------------------------------------------------------------------
describe('isFullPage', () => {
test('detects DOCTYPE', () => expect(isFullPage('<!DOCTYPE html><html>')).toBe(true));
test('detects <html>', () => expect(isFullPage('<html><head></head>')).toBe(true));
test('detects <head>', () => expect(isFullPage('<head><meta charset="UTF-8"></head>')).toBe(true));
test('rejects component/partial', () => expect(isFullPage('<div class="card">content</div>')).toBe(false));
test('rejects JSX', () => expect(isFullPage('export default function Card() { return <div>hi</div> }')).toBe(false));
});
describe('partials skip page-level checks', () => {
test('regex: partial with flat hierarchy is not flagged', () => {
const partial = '<div style="font-size: 14px">text</div>\n<div style="font-size: 16px">text</div>\n<div style="font-size: 15px">text</div>';
const f = detectText(partial, 'card.tsx');
expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0);
});
test('regex: partial with single overused font is not flagged for single-font', () => {
const partial = `<div style="font-family: 'Inter', sans-serif; font-size: 14px">text</div>\n`.repeat(25);
const f = detectText(partial, 'card.tsx');
expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0);
});
test('regex: partial still flags border anti-patterns', () => {
const partial = '<div class="border-l-4 border-blue-500 rounded-lg">card</div>';
const f = detectText(partial, 'card.tsx');
expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
});
test('regex: full page with flat hierarchy IS flagged', () => {
const page = '<!DOCTYPE html><html><head></head><body>\n' +
'<h1 style="font-size: 18px">h1</h1>\n<h2 style="font-size: 16px">h2</h2>\n' +
'<p style="font-size: 14px">p</p>\n<span style="font-size: 15px">s</span>\n' +
'<small style="font-size: 13px">sm</small>\n</body></html>';
const f = detectText(page, 'index.html');
expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true);
});
});
// ---------------------------------------------------------------------------
// ANTIPATTERNS registry
// ---------------------------------------------------------------------------
+11
View File
@@ -0,0 +1,11 @@
<!-- This is a partial/component — no DOCTYPE, no <html>, no <head> -->
<!-- Page-level checks (flat hierarchy, single font) should NOT run -->
<!-- But element-level border checks SHOULD still run -->
<div class="card" style="font-family: 'Inter', sans-serif;">
<div style="font-size: 14px; border-left: 4px solid #3b82f6; border-radius: 8px; padding: 1rem; background: white;">
<h3 style="font-size: 16px; font-weight: 600;">Card Title</h3>
<p style="font-size: 15px; color: #6b7280;">Card description with close font sizes.</p>
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>