quiver-cli 0.8.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +666 -436
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { GENERIC_FONTS } from '../../shared/constants.mjs';
|
|
1
|
+
import { GENERIC_FONTS, OVERUSED_FONTS, EM_DASH_FLOOR, EM_DASH_CHARS_PER_DASH } from '../../shared/constants.mjs';
|
|
2
2
|
import { isNeutralColor } from '../../shared/color.mjs';
|
|
3
|
+
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
|
3
4
|
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
|
5
|
+
import { scanCssTextForGlow, scanCssTextForGridBackground, scanCssTextForMarquee, scanCssTextForPseudoStripe, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
|
4
6
|
import { isFullPage } from '../../shared/page.mjs';
|
|
5
7
|
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
|
6
8
|
import { finding } from '../../findings.mjs';
|
|
7
|
-
import { filterByProviders } from '../../registry/antipatterns.mjs';
|
|
8
9
|
import { profileFindings, profileStep } from '../../profile/profiler.mjs';
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Regex fallback (non-HTML files: CSS, JSX, TSX, etc.)
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
|
|
14
|
-
const hasRounded = (line) =>
|
|
15
|
+
const hasRounded = (line) =>
|
|
16
|
+
/\brounded(?:-\w+)?\b/.test(line.replace(/\brounded-none\b/g, ''));
|
|
15
17
|
const hasBorderRadius = (line) => /border-radius/i.test(line);
|
|
16
18
|
const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|input[\s>]|span[\s>])/i.test(line);
|
|
17
19
|
|
|
20
|
+
|
|
18
21
|
/** Strip HTML to plain text — drops script/style/comments/tags so
|
|
19
22
|
* content-text analyzers don't false-positive on code or CSS. */
|
|
20
23
|
function stripHtmlToText(html) {
|
|
@@ -38,25 +41,100 @@ function shouldRunPageAnalyzers(content, filePath) {
|
|
|
38
41
|
return !ext || PAGE_ANALYZER_EXTS.has(ext);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
function firstOverusedGoogleFont(text) {
|
|
45
|
+
return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CSS named colors whose channels are equal (achromatic). Anything outside
|
|
49
|
+
// this set falls through to the format parsers, and an unrecognized spelling
|
|
50
|
+
// stays non-neutral so a real accent is never skipped.
|
|
51
|
+
const NEUTRAL_COLOR_KEYWORDS = new Set([
|
|
52
|
+
'transparent', 'currentcolor',
|
|
53
|
+
'black', 'white', 'gray', 'grey', 'silver',
|
|
54
|
+
'dimgray', 'dimgrey', 'darkgray', 'darkgrey', 'lightgray', 'lightgrey',
|
|
55
|
+
'gainsboro', 'whitesmoke',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function hexChannels(color) {
|
|
59
|
+
const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
|
|
60
|
+
if (long) return [parseInt(long[1], 16), parseInt(long[2], 16), parseInt(long[3], 16)];
|
|
61
|
+
const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
|
|
62
|
+
if (short) return [1, 2, 3].map((i) => parseInt(short[i] + short[i], 16));
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Split one box-shadow layer into top-level tokens.
|
|
68
|
+
*
|
|
69
|
+
* Whitespace inside parens does not separate tokens: `rgb(0 0 0)` and
|
|
70
|
+
* `var(--x, 4px)` are each a single value, and splitting them on spaces would
|
|
71
|
+
* read their innards as separate lengths.
|
|
72
|
+
*/
|
|
73
|
+
function tokenizeShadowLayer(layer) {
|
|
74
|
+
const tokens = [];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let current = '';
|
|
77
|
+
for (const char of String(layer || '')) {
|
|
78
|
+
if (char === '(') depth++;
|
|
79
|
+
else if (char === ')') depth--;
|
|
80
|
+
else if (depth === 0 && /\s/.test(char)) {
|
|
81
|
+
if (current) tokens.push(current);
|
|
82
|
+
current = '';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
current += char;
|
|
51
86
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
if (current) tokens.push(current);
|
|
88
|
+
return tokens;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function lastMatch(text, re) {
|
|
92
|
+
const all = [...String(text || '').matchAll(re)];
|
|
93
|
+
return all.length ? all[all.length - 1] : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isShadowLength(token) {
|
|
97
|
+
return /^-?\d*\.?\d+(?:px)?$/i.test(String(token || ''));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Neutrality test for colors as written in source CSS.
|
|
102
|
+
*
|
|
103
|
+
* shared/color.mjs's isNeutralColor only parses the computed function forms a
|
|
104
|
+
* browser or jsdom emits (rgb/oklch/lab/...) and deliberately reports every
|
|
105
|
+
* other spelling as chromatic so an unknown format is never silently skipped.
|
|
106
|
+
* That default is wrong for authored CSS, where `#000` and `black` are the
|
|
107
|
+
* normal spellings: calling it directly reports a plain black hairline as a
|
|
108
|
+
* colored stripe. Handle hex and named neutrals here, then defer.
|
|
109
|
+
*/
|
|
110
|
+
function isNeutralAuthoredColor(rawColor) {
|
|
111
|
+
const c = String(rawColor || '').trim().toLowerCase();
|
|
112
|
+
if (!c) return false;
|
|
113
|
+
if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
|
|
114
|
+
// Modern rgb() takes space-separated channels (`rgb(0 0 0)`). shared/color.mjs
|
|
115
|
+
// parses only the comma form a browser's getComputedStyle emits, so authored
|
|
116
|
+
// space-separated neutrals fell through it and reported as chromatic — the
|
|
117
|
+
// exemption this function exists for, missed. Normalize before delegating.
|
|
118
|
+
if (/^rgba?\(/i.test(c)) {
|
|
119
|
+
const channels = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
120
|
+
if (channels) {
|
|
121
|
+
const values = [1, 2, 3].map((i) => Number(channels[i]));
|
|
122
|
+
return (Math.max(...values) - Math.min(...values)) < 30;
|
|
123
|
+
}
|
|
124
|
+
return isNeutralColor(c);
|
|
56
125
|
}
|
|
126
|
+
if (/^(?:hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
|
|
127
|
+
const channels = hexChannels(c);
|
|
128
|
+
if (channels) return (Math.max(...channels) - Math.min(...channels)) < 30;
|
|
57
129
|
return false;
|
|
58
130
|
}
|
|
59
131
|
|
|
132
|
+
function isNeutralBorderColor(str) {
|
|
133
|
+
const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
|
|
134
|
+
if (!m) return false;
|
|
135
|
+
return isNeutralAuthoredColor(m[1]);
|
|
136
|
+
}
|
|
137
|
+
|
|
60
138
|
const REGEX_MATCHERS = [
|
|
61
139
|
// --- Side-tab ---
|
|
62
140
|
{ id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g,
|
|
@@ -88,9 +166,12 @@ const REGEX_MATCHERS = [
|
|
|
88
166
|
{ id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi,
|
|
89
167
|
test: () => true,
|
|
90
168
|
fmt: (m) => m[0] },
|
|
91
|
-
{ id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?
|
|
92
|
-
test: () =>
|
|
93
|
-
|
|
169
|
+
{ id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi,
|
|
170
|
+
test: (m) => {
|
|
171
|
+
m.overusedGoogleFont = firstOverusedGoogleFont(m[0]);
|
|
172
|
+
return Boolean(m.overusedGoogleFont);
|
|
173
|
+
},
|
|
174
|
+
fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` },
|
|
94
175
|
// --- Gradient text ---
|
|
95
176
|
{ id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi,
|
|
96
177
|
test: (m, line) => /gradient/i.test(line),
|
|
@@ -160,27 +241,6 @@ const REGEX_MATCHERS = [
|
|
|
160
241
|
];
|
|
161
242
|
|
|
162
243
|
const REGEX_ANALYZERS = [
|
|
163
|
-
// Single font
|
|
164
|
-
(content, filePath) => {
|
|
165
|
-
const fontFamilyRe = /font-family\s*:\s*([^;}]+)/gi;
|
|
166
|
-
const fonts = new Set();
|
|
167
|
-
let m;
|
|
168
|
-
while ((m = fontFamilyRe.exec(content)) !== null) {
|
|
169
|
-
for (const f of m[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
|
|
170
|
-
if (f && !GENERIC_FONTS.has(f)) fonts.add(f);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi;
|
|
174
|
-
while ((m = gfRe.exec(content)) !== null) {
|
|
175
|
-
for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f);
|
|
176
|
-
}
|
|
177
|
-
if (fonts.size !== 1 || content.split('\n').length < 20) return [];
|
|
178
|
-
const name = [...fonts][0];
|
|
179
|
-
const lines = content.split('\n');
|
|
180
|
-
let line = 1;
|
|
181
|
-
for (let i = 0; i < lines.length; i++) { if (lines[i].toLowerCase().includes(name)) { line = i + 1; break; } }
|
|
182
|
-
return [finding('single-font', filePath, `only font used is ${name}`, line)];
|
|
183
|
-
},
|
|
184
244
|
// Flat type hierarchy
|
|
185
245
|
(content, filePath) => {
|
|
186
246
|
const sizes = new Set();
|
|
@@ -230,15 +290,34 @@ const REGEX_ANALYZERS = [
|
|
|
230
290
|
const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
|
|
231
291
|
return [finding('monotonous-spacing', filePath, `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(pct * 100)}%)`)];
|
|
232
292
|
},
|
|
233
|
-
// Em-dash overuse:
|
|
234
|
-
//
|
|
235
|
-
//
|
|
293
|
+
// Em-dash overuse (ADVISORY): the AI cadence tell is em-dash *saturation*,
|
|
294
|
+
// not the occasional dash. Humans use em-dashes legitimately, so this rule is
|
|
295
|
+
// advisory (surfaced separately, never a failure, hook-skipped by default) and
|
|
296
|
+
// its threshold is deliberately conservative. Two gates must both hold:
|
|
297
|
+
// 1. Absolute floor of EM_DASH_FLOOR (8) dashes — a page with a handful
|
|
298
|
+
// never fires, no matter how short.
|
|
299
|
+
// 2. Density: at least one dash per EM_DASH_CHARS_PER_DASH (500) characters
|
|
300
|
+
// of body text, so a long article that uses eight across several thousand
|
|
301
|
+
// words is left alone while a short, dash-per-clause landing page is not.
|
|
302
|
+
// Raised from the old flat 5-dash floor, which fired on ordinary long prose.
|
|
303
|
+
//
|
|
304
|
+
// stripHtmlToText drops tags but leaves character-entity escapes intact, so
|
|
305
|
+
// a model that writes `—`, `—`, or `—` renders an em-dash
|
|
306
|
+
// the counter never saw. Decode the em-dash entities (named, zero-padded
|
|
307
|
+
// decimal, upper/lower hex) to the literal glyph first. En-dash entities are
|
|
308
|
+
// deliberately left alone: the rule counts em-dashes, and the literal `–`
|
|
309
|
+
// was never counted either.
|
|
236
310
|
(content, filePath) => {
|
|
237
|
-
const text = stripHtmlToText(content)
|
|
311
|
+
const text = stripHtmlToText(content)
|
|
312
|
+
.replace(/—|�*8212;|�*2014;/gi, '—');
|
|
238
313
|
let count = 0;
|
|
239
314
|
const re = /[—]|--(?=\S)/g;
|
|
240
315
|
while (re.exec(text) !== null) count++;
|
|
241
|
-
if (count <
|
|
316
|
+
if (count < EM_DASH_FLOOR) return [];
|
|
317
|
+
// Saturation gate: dashes must be dense in the prose, not sprinkled through
|
|
318
|
+
// a long document. textLength <= count * chars-per-dash means the density is
|
|
319
|
+
// at or above the threshold.
|
|
320
|
+
if (text.length > count * EM_DASH_CHARS_PER_DASH) return [];
|
|
242
321
|
return [finding('em-dash-overuse', filePath, `${count} em-dashes in body text`)];
|
|
243
322
|
},
|
|
244
323
|
// Marketing buzzwords: SaaS phrase list
|
|
@@ -274,22 +353,6 @@ const REGEX_ANALYZERS = [
|
|
|
274
353
|
if (count === 0) return [];
|
|
275
354
|
return [finding('marketing-buzzword', filePath, `${count} buzzword phrase${count === 1 ? '' : 's'}: "${firstSample}"`)];
|
|
276
355
|
},
|
|
277
|
-
// Numbered section markers (01 / 02 / 03 ...)
|
|
278
|
-
(content, filePath) => {
|
|
279
|
-
const text = stripHtmlToText(content);
|
|
280
|
-
const re = /\b(0[1-9]|1[0-2])\b/g;
|
|
281
|
-
const seen = new Set();
|
|
282
|
-
let m;
|
|
283
|
-
while ((m = re.exec(text)) !== null) seen.add(m[1]);
|
|
284
|
-
if (seen.size < 3) return [];
|
|
285
|
-
const sorted = [...seen].sort();
|
|
286
|
-
let sequential = 0;
|
|
287
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
288
|
-
if (parseInt(sorted[i], 10) === parseInt(sorted[i - 1], 10) + 1) sequential++;
|
|
289
|
-
}
|
|
290
|
-
if (sequential < 2) return [];
|
|
291
|
-
return [finding('numbered-section-markers', filePath, `Sequence: ${sorted.slice(0, 6).join(', ')}`)];
|
|
292
|
-
},
|
|
293
356
|
// Aphoristic cadence: manufactured-contrast + short-rebuttal
|
|
294
357
|
(content, filePath) => {
|
|
295
358
|
const text = stripHtmlToText(content);
|
|
@@ -311,41 +374,143 @@ const REGEX_ANALYZERS = [
|
|
|
311
374
|
if (count < 3) return [];
|
|
312
375
|
return [finding('aphoristic-cadence', filePath, `${count} aphoristic constructions: "${firstSample}"`)];
|
|
313
376
|
},
|
|
314
|
-
// Dark glow (page-level
|
|
377
|
+
// Dark glow / chromatic halo shadows (page-level). Shared scanner handles
|
|
378
|
+
// any color format, single-level var() resolution, zero-offset halos on
|
|
379
|
+
// any background, and text-shadow glows.
|
|
315
380
|
(content, filePath) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (!colorMatch) continue;
|
|
329
|
-
const [r, g, b] = [+colorMatch[1], +colorMatch[2], +colorMatch[3]];
|
|
330
|
-
if ((Math.max(r, g, b) - Math.min(r, g, b)) < 30) continue; // skip gray
|
|
331
|
-
// Check blur: look for pattern like "0 0 20px" (third number > 4)
|
|
332
|
-
const pxVals = [...val.matchAll(/(\d+)px|(?<![.\d])\b(0)\b(?![.\d])/g)].map(p => +(p[1] || p[2]));
|
|
333
|
-
if (pxVals.length >= 3 && pxVals[2] > 4) {
|
|
334
|
-
const lines = content.substring(0, m.index).split('\n');
|
|
335
|
-
return [finding('dark-glow', filePath, `Colored glow (rgb(${r},${g},${b})) on dark page`, lines.length)];
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return [];
|
|
381
|
+
const hits = scanCssTextForGlow(content);
|
|
382
|
+
if (hits.length === 0) return [];
|
|
383
|
+
const lines = content.substring(0, hits[0].index).split('\n');
|
|
384
|
+
return [finding('dark-glow', filePath, hits[0].snippet, lines.length)];
|
|
385
|
+
},
|
|
386
|
+
// Radial-gradient background halo on a dark page (the gradient sibling
|
|
387
|
+
// of the dark-glow shadow tell).
|
|
388
|
+
(content, filePath) => {
|
|
389
|
+
const hits = scanCssTextForRadialHalo(content);
|
|
390
|
+
if (hits.length === 0) return [];
|
|
391
|
+
const lines = content.substring(0, hits[0].index).split('\n');
|
|
392
|
+
return [finding('radial-halo', filePath, hits[0].snippet, lines.length)];
|
|
339
393
|
},
|
|
394
|
+
// Auto-scrolling marquees (<marquee> or infinite horizontal loop
|
|
395
|
+
// animations).
|
|
396
|
+
(content, filePath) => scanCssTextForMarquee(content).map(hit => finding('marquee', filePath, hit.snippet)),
|
|
340
397
|
];
|
|
341
398
|
|
|
342
399
|
// ---------------------------------------------------------------------------
|
|
343
|
-
//
|
|
400
|
+
// Structural CSS checks used by source files whose styles are not parsed by
|
|
401
|
+
// the static HTML engine.
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
const CHROMATIC_SHADOW_TOKEN_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
|
405
|
+
|
|
406
|
+
function insetStripeColorIsChromatic(rawColor) {
|
|
407
|
+
const color = String(rawColor || '').trim().replace(/\s*!important\s*$/i, '');
|
|
408
|
+
if (/^(?:currentcolor|transparent|inherit|unset)$/i.test(color)) return false;
|
|
409
|
+
const variable = color.match(/^var\(\s*(--[\w-]+)/i);
|
|
410
|
+
if (variable) return CHROMATIC_SHADOW_TOKEN_RE.test(variable[1]);
|
|
411
|
+
if (!/^(?:#|rgba?\(|hsla?\(|hwb\(|oklch\(|oklab\(|lch\(|lab\(|color\(|[a-z]+$)/i.test(color)) return false;
|
|
412
|
+
return !isNeutralAuthoredColor(color);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Blank out comment bodies while preserving every byte offset (and therefore
|
|
417
|
+
* every line number) so commented-out CSS is not scanned as live rules.
|
|
418
|
+
*/
|
|
419
|
+
function blankCssComments(css) {
|
|
420
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function scanInsetStripeCss(rawContent, filePath, lineOffset = 0) {
|
|
424
|
+
const content = blankCssComments(rawContent);
|
|
425
|
+
const findings = [];
|
|
426
|
+
const ruleRe = /([^{};]+)\{([^{}]*)\}/g;
|
|
427
|
+
let match;
|
|
428
|
+
// Deriving each line with content.slice(0, offset).split('\n') re-scans the
|
|
429
|
+
// whole prefix per rule, which is O(n^2) on a large stylesheet. Rule matches
|
|
430
|
+
// arrive in source order, so carry a monotonic cursor instead: one pass total.
|
|
431
|
+
let scanOffset = 0;
|
|
432
|
+
let scanLine = 1;
|
|
433
|
+
const lineAtOffset = (offset) => {
|
|
434
|
+
while (scanOffset < offset) {
|
|
435
|
+
if (content[scanOffset] === '\n') scanLine++;
|
|
436
|
+
scanOffset++;
|
|
437
|
+
}
|
|
438
|
+
return scanLine;
|
|
439
|
+
};
|
|
440
|
+
while ((match = ruleRe.exec(content)) !== null) {
|
|
441
|
+
// The selector group is `[^{};]+`, which greedily absorbs the whitespace and
|
|
442
|
+
// newlines trailing the previous rule. Advance past that run before deriving
|
|
443
|
+
// the line, or every rule after the first reports the preceding line.
|
|
444
|
+
const selectorStart = match.index + (match[1].length - match[1].trimStart().length);
|
|
445
|
+
const selector = match[1].trim().replace(/\s+/g, ' ');
|
|
446
|
+
if (!selector) continue;
|
|
447
|
+
if (/:(?:hover|focus|focus-visible|focus-within|active|checked|target)\b/i.test(selector)) continue;
|
|
448
|
+
if (/\[aria-selected\s*[*^$|~]?=\s*["']?true/i.test(selector)) continue;
|
|
449
|
+
if (/\[aria-current(?!\s*[*^$|~]?=\s*["']?false)/i.test(selector)) continue;
|
|
450
|
+
if (/(?:^|[\s._[-])(?:active|current|selected)(?![\w])/i.test(selector)) continue;
|
|
451
|
+
if (/(?:^|[\s>+~,(])(?:button|hr|tr|td|th|table|blockquote|pre|code)(?![\w-])/i.test(selector)) continue;
|
|
452
|
+
|
|
453
|
+
// Read the last of a repeated declaration, not the first: that is what the
|
|
454
|
+
// cascade paints. Taking the first both flagged stripes that a later
|
|
455
|
+
// `box-shadow: none` had cancelled and missed stripes that overrode an
|
|
456
|
+
// earlier value, and mis-skipped rules whose narrow width was overridden.
|
|
457
|
+
const width = lastMatch(match[2], /(?:^|;)\s*(?:width|inline-size)\s*:\s*(\d+(?:\.\d+)?)px/gi);
|
|
458
|
+
if (width && Number(width[1]) <= 40) continue;
|
|
459
|
+
const declaration = lastMatch(match[2], /(?:^|;)\s*box-shadow\s*:\s*([^;]+)/gi);
|
|
460
|
+
if (!declaration || !/\binset\b/i.test(declaration[1])) continue;
|
|
461
|
+
// `!important` qualifies the declaration, not the shadow value, so strip it
|
|
462
|
+
// before the layers are read. Tokenizing split it into its own token, which
|
|
463
|
+
// made the color count wrong and silently stopped flagging stripes declared
|
|
464
|
+
// with it — a shape the previous regex handled.
|
|
465
|
+
const shadowValue = declaration[1].replace(/\s*!\s*important\s*$/i, '').trim();
|
|
466
|
+
|
|
467
|
+
for (const rawLayer of shadowValue.split(/,(?![^(]*\))/)) {
|
|
468
|
+
const layer = rawLayer.trim();
|
|
469
|
+
// Parse the layer by its grammar rather than by one spelling of it.
|
|
470
|
+
// A box-shadow layer is `inset? && <length>{2,4} && <color>?` in any
|
|
471
|
+
// order, so `inset 4px 0 red`, `4px 0 0 red inset`, and `red 4px 0 inset`
|
|
472
|
+
// all paint the same stripe. Matching a fixed token order missed three
|
|
473
|
+
// valid spellings in a row; enumerate the tokens instead. Tokenizing must
|
|
474
|
+
// respect parens: `rgb(0 0 0)` is one color token, and splitting it on
|
|
475
|
+
// whitespace would read its channels as lengths.
|
|
476
|
+
const tokens = tokenizeShadowLayer(layer);
|
|
477
|
+
if (!tokens.some((token) => /^inset$/i.test(token))) continue;
|
|
478
|
+
const rest = tokens.filter((token) => !/^inset$/i.test(token));
|
|
479
|
+
const lengths = rest.filter(isShadowLength);
|
|
480
|
+
const colors = rest.filter((token) => !isShadowLength(token));
|
|
481
|
+
// Only the two offsets are required; omitted blur/spread default to 0,
|
|
482
|
+
// which is exactly the stripe shape. More than one non-length token is a
|
|
483
|
+
// layer shape we do not claim to understand, so leave it alone.
|
|
484
|
+
if (lengths.length < 2 || lengths.length > 4 || colors.length !== 1) continue;
|
|
485
|
+
const values = lengths.map((token) => ({
|
|
486
|
+
n: Number(token.replace(/px$/i, '')),
|
|
487
|
+
hasPx: /px$/i.test(token),
|
|
488
|
+
}));
|
|
489
|
+
const x = values[0];
|
|
490
|
+
const y = values[1];
|
|
491
|
+
const blur = values[2] ? values[2].n : 0;
|
|
492
|
+
const spread = values[3] ? values[3].n : 0;
|
|
493
|
+
if ((x.n !== 0 && !x.hasPx) || (y.n !== 0 && !y.hasPx) || blur !== 0 || spread !== 0) continue;
|
|
494
|
+
const ax = Math.abs(x.n);
|
|
495
|
+
const ay = Math.abs(y.n);
|
|
496
|
+
if (!((ax >= 3 && ax <= 12 && ay === 0) || (ay >= 3 && ay <= 12 && ax === 0))) continue;
|
|
497
|
+
if (!insetStripeColorIsChromatic(colors[0])) continue;
|
|
498
|
+
const edge = ay === 0 ? (x.n > 0 ? 'left' : 'right') : (y.n > 0 ? 'top' : 'bottom');
|
|
499
|
+
const line = lineOffset + lineAtOffset(selectorStart);
|
|
500
|
+
findings.push(finding('side-tab', filePath, `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`, line));
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return findings;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
|
344
509
|
// ---------------------------------------------------------------------------
|
|
345
510
|
|
|
346
511
|
function extractStyleBlocks(content, ext) {
|
|
347
512
|
ext = ext.toLowerCase();
|
|
348
|
-
if (ext !== '.vue' && ext !== '.svelte') return [];
|
|
513
|
+
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
|
349
514
|
const blocks = [];
|
|
350
515
|
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
351
516
|
let m;
|
|
@@ -430,24 +595,24 @@ function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null,
|
|
|
430
595
|
}
|
|
431
596
|
|
|
432
597
|
/** Page-level analyzers that scan rendered text content (em-dash use,
|
|
433
|
-
* buzzword phrases,
|
|
598
|
+
* buzzword phrases, aphoristic cadence).
|
|
434
599
|
* These are detector-agnostic — they work on any HTML/text source
|
|
435
600
|
* and don't need a parsed DOM. Exported so detectHtml can call them
|
|
436
601
|
* for `.html` files (which otherwise skip the regex engine). */
|
|
437
602
|
const TEXT_CONTENT_ANALYZER_IDS = [
|
|
438
603
|
'em-dash-overuse',
|
|
439
604
|
'marketing-buzzword',
|
|
440
|
-
'numbered-section-markers',
|
|
441
605
|
'aphoristic-cadence',
|
|
442
606
|
];
|
|
443
607
|
|
|
444
608
|
function runTextContentAnalyzers(content, filePath, options = {}) {
|
|
445
609
|
const profile = options?.profile;
|
|
446
610
|
if (!shouldRunPageAnalyzers(content, filePath)) return [];
|
|
447
|
-
// The
|
|
611
|
+
// The 3 text-content analyzers are at indices 2-4 in REGEX_ANALYZERS
|
|
612
|
+
// (single-font's removal on 2026-07-29 shifted every index down one).
|
|
448
613
|
const findings = [];
|
|
449
614
|
for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
|
|
450
|
-
const analyzer = REGEX_ANALYZERS[
|
|
615
|
+
const analyzer = REGEX_ANALYZERS[2 + i];
|
|
451
616
|
const ruleId = TEXT_CONTENT_ANALYZER_IDS[i];
|
|
452
617
|
findings.push(...profileFindings(profile, {
|
|
453
618
|
engine: 'regex',
|
|
@@ -472,8 +637,36 @@ function detectText(content, filePath, options = {}) {
|
|
|
472
637
|
profile,
|
|
473
638
|
phase: 'source',
|
|
474
639
|
}));
|
|
640
|
+
// Pseudo-element stripes (::before/::after absolute bars) carry the same
|
|
641
|
+
// side-tab silhouette without any border token, so the line matchers can't
|
|
642
|
+
// see them (issue #394). The shared scanner already runs on full HTML pages
|
|
643
|
+
// via checkHtmlPatterns; give standalone stylesheets, component style
|
|
644
|
+
// blocks, and CSS-in-JS templates the same coverage. Each hit carries the
|
|
645
|
+
// rule's source offset, so the finding gets a real line and line-scoped
|
|
646
|
+
// inline ignores keep working.
|
|
647
|
+
const pseudoStripeFindings = (text, lineOffset) =>
|
|
648
|
+
scanCssTextForPseudoStripe(text).map(hit =>
|
|
649
|
+
finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
|
|
650
|
+
|
|
651
|
+
if (cssLike.has(ext)) {
|
|
652
|
+
findings.push(...scanInsetStripeCss(content, filePath));
|
|
653
|
+
findings.push(...pseudoStripeFindings(content, 0));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Block-level CSS checks that need multiple declarations must run over the
|
|
657
|
+
// complete source, not line-by-line. This covers standalone stylesheets,
|
|
658
|
+
// component style blocks, inline styles, and CSS-in-JS templates.
|
|
659
|
+
findings.push(...profileFindings(profile, {
|
|
660
|
+
engine: 'regex',
|
|
661
|
+
phase: 'source',
|
|
662
|
+
ruleId: 'codex-grid-background',
|
|
663
|
+
target: filePath,
|
|
664
|
+
}, () => scanCssTextForGridBackground(content).map(hit => {
|
|
665
|
+
const line = content.substring(0, hit.index).split('\n').length;
|
|
666
|
+
return finding('codex-grid-background', filePath, hit.snippet, line);
|
|
667
|
+
})));
|
|
475
668
|
|
|
476
|
-
// Extract and scan <style> blocks from Vue/Svelte
|
|
669
|
+
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
|
477
670
|
const styleBlocks = profile
|
|
478
671
|
? profileStep(profile, {
|
|
479
672
|
engine: 'regex',
|
|
@@ -488,6 +681,14 @@ function detectText(content, filePath, options = {}) {
|
|
|
488
681
|
profile,
|
|
489
682
|
phase: 'style-block',
|
|
490
683
|
}));
|
|
684
|
+
// block.startLine is the first line *after* the <style> tag, but block.content
|
|
685
|
+
// begins at the character right after that tag — so its own line 1 sits on the
|
|
686
|
+
// tag's line, whether or not a newline follows immediately. lineAtOffset is
|
|
687
|
+
// 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
|
|
688
|
+
// reported every selector one line low. runRegexMatchers keeps startLine - 1
|
|
689
|
+
// because it indexes its split lines from zero.
|
|
690
|
+
findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
|
|
691
|
+
findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
|
|
491
692
|
}
|
|
492
693
|
|
|
493
694
|
// Extract and scan CSS-in-JS template literals
|
|
@@ -505,6 +706,8 @@ function detectText(content, filePath, options = {}) {
|
|
|
505
706
|
profile,
|
|
506
707
|
phase: 'css-in-js',
|
|
507
708
|
}));
|
|
709
|
+
findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 1));
|
|
710
|
+
findings.push(...pseudoStripeFindings(block.content, block.startLine - 1));
|
|
508
711
|
}
|
|
509
712
|
|
|
510
713
|
if (options?.designSystem) {
|
|
@@ -530,12 +733,10 @@ function detectText(content, filePath, options = {}) {
|
|
|
530
733
|
// Page-level analyzers only run on full pages
|
|
531
734
|
if (shouldRunPageAnalyzers(content, filePath)) {
|
|
532
735
|
const analyzerIds = [
|
|
533
|
-
'single-font',
|
|
534
736
|
'flat-type-hierarchy',
|
|
535
737
|
'monotonous-spacing',
|
|
536
738
|
'em-dash-overuse',
|
|
537
739
|
'marketing-buzzword',
|
|
538
|
-
'numbered-section-markers',
|
|
539
740
|
'aphoristic-cadence',
|
|
540
741
|
'dark-glow',
|
|
541
742
|
];
|
|
@@ -550,10 +751,9 @@ function detectText(content, filePath, options = {}) {
|
|
|
550
751
|
}
|
|
551
752
|
}
|
|
552
753
|
|
|
553
|
-
const byProvider = filterByProviders(deduped, options?.providers);
|
|
554
754
|
// Inline `impeccable-disable*` waivers travel with the file; honor them unless
|
|
555
755
|
// explicitly bypassed (`--no-config` / `--no-inline-ignores`).
|
|
556
|
-
return options?.inlineIgnores === false ?
|
|
756
|
+
return options?.inlineIgnores === false ? deduped : applyInlineIgnores(deduped, content);
|
|
557
757
|
}
|
|
558
758
|
|
|
559
759
|
export {
|