chore: import upstream snapshot with attribution
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
This commit is contained in:
Executable
+516
@@ -0,0 +1,516 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-commit hook to ensure all LDR-specific CSS class names are prefixed with 'ldr-'.
|
||||
This prevents CSS class name conflicts when Vite bundles CSS from dependencies.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
# Allowed patterns - ONLY third-party framework classes
|
||||
# Everything else MUST have ldr- prefix
|
||||
ALLOWED_PATTERNS = {
|
||||
# Already prefixed with ldr-
|
||||
r"^ldr-",
|
||||
# Bootstrap 5 specific classes (no wildcards unless necessary)
|
||||
# Layout
|
||||
r"^(container|container-fluid|container-sm|container-md|container-lg|container-xl|container-xxl)$",
|
||||
r"^row$",
|
||||
r"^col$",
|
||||
r"^col-(1|2|3|4|5|6|7|8|9|10|11|12)$",
|
||||
r"^col-(sm|md|lg|xl|xxl)-(1|2|3|4|5|6|7|8|9|10|11|12)$",
|
||||
# Bootstrap components (with specific prefixes)
|
||||
r"^btn(-primary|-secondary|-success|-danger|-warning|-info|-light|-dark|-link|-outline-primary|-outline-secondary|-outline-success|-outline-danger|-outline-warning|-outline-info|-outline-light|-outline-dark|-sm|-lg|-block|-close|-close-white)?$",
|
||||
r"^alert(-primary|-secondary|-success|-danger|-warning|-info|-light|-dark|-dismissible)?$",
|
||||
r"^badge(-primary|-secondary|-success|-danger|-warning|-info|-light|-dark|-pill)?$",
|
||||
r"^card(-body|-header|-footer|-title|-subtitle|-text|-link|-img-top|-img-bottom)?$",
|
||||
r"^navbar(-brand|-nav|-toggler|-collapse|-expand|-expand-sm|-expand-md|-expand-lg|-expand-xl|-dark|-light)?$",
|
||||
r"^nav(-link|-item|-pills|-tabs|-fill|-justified)?$",
|
||||
r"^dropdown(-toggle|-menu|-menu-end|-menu-start|-item|-divider|-header)?$",
|
||||
r"^modal(-dialog|-dialog-centered|-dialog-scrollable|-content|-header|-body|-footer|-title|-backdrop|-static|-sm|-lg|-xl|-fullscreen)?$",
|
||||
r"^form(-control|-label|-text|-check|-check-input|-check-label|-check-inline|-switch|-select|-range|-floating|-group|-row)?$",
|
||||
r"^input-group(-text|-prepend|-append)?$",
|
||||
r"^list-group(-item|-item-action|-flush)?$",
|
||||
r"^table(-dark|-striped|-bordered|-borderless|-hover|-sm|-responsive)?$",
|
||||
# Bootstrap utilities with exact values
|
||||
r"^text-(start|end|center|primary|secondary|success|danger|warning|info|light|dark|body|muted|white|black-50|white-50)$",
|
||||
r"^text-(lowercase|uppercase|capitalize|nowrap|truncate|break|monospace)$",
|
||||
r"^bg-(primary|secondary|success|danger|warning|info|light|dark|body|white|transparent)$",
|
||||
r"^border(-top|-end|-bottom|-start|-primary|-secondary|-success|-danger|-warning|-info|-light|-dark|-white)?$",
|
||||
r"^border-(0|1|2|3|4|5)$",
|
||||
r"^rounded(-top|-end|-bottom|-start|-circle|-pill|-0|1|2|3)?$",
|
||||
r"^shadow(-sm|-lg|-none)?$",
|
||||
# Display utilities
|
||||
r"^d-(none|inline|inline-block|block|table|table-row|table-cell|flex|inline-flex|grid)$",
|
||||
r"^d-(sm|md|lg|xl|xxl)-(none|inline|inline-block|block|table|table-row|table-cell|flex|inline-flex|grid)$",
|
||||
# Flexbox utilities
|
||||
r"^flex-(row|row-reverse|column|column-reverse|wrap|nowrap|wrap-reverse)$",
|
||||
r"^justify-content-(start|end|center|between|around|evenly)$",
|
||||
r"^align-items-(start|end|center|baseline|stretch)$",
|
||||
r"^align-self-(start|end|center|baseline|stretch)$",
|
||||
r"^flex-(fill|grow-0|grow-1|shrink-0|shrink-1)$",
|
||||
# Spacing utilities (margins and padding)
|
||||
r"^m-(0|1|2|3|4|5|auto)$",
|
||||
r"^mt-(0|1|2|3|4|5|auto)$",
|
||||
r"^mb-(0|1|2|3|4|5|auto)$",
|
||||
r"^ms-(0|1|2|3|4|5|auto)$",
|
||||
r"^me-(0|1|2|3|4|5|auto)$",
|
||||
r"^mx-(0|1|2|3|4|5|auto)$",
|
||||
r"^my-(0|1|2|3|4|5|auto)$",
|
||||
r"^p-(0|1|2|3|4|5)$",
|
||||
r"^pt-(0|1|2|3|4|5)$",
|
||||
r"^pb-(0|1|2|3|4|5)$",
|
||||
r"^ps-(0|1|2|3|4|5)$",
|
||||
r"^pe-(0|1|2|3|4|5)$",
|
||||
r"^px-(0|1|2|3|4|5)$",
|
||||
r"^py-(0|1|2|3|4|5)$",
|
||||
# Sizing utilities
|
||||
r"^w-(25|50|75|100|auto)$",
|
||||
r"^h-(25|50|75|100|auto)$",
|
||||
r"^mw-100$",
|
||||
r"^mh-100$",
|
||||
# Position utilities
|
||||
r"^position-(static|relative|absolute|fixed|sticky)$",
|
||||
r"^top-(0|50|100)$",
|
||||
r"^bottom-(0|50|100)$",
|
||||
r"^start-(0|50|100)$",
|
||||
r"^end-(0|50|100)$",
|
||||
# Typography utilities
|
||||
r"^fs-(1|2|3|4|5|6)$",
|
||||
r"^fw-(light|lighter|normal|bold|bolder)$",
|
||||
r"^fst-(normal|italic)$",
|
||||
r"^lh-(1|sm|base|lg)$",
|
||||
r"^font-monospace$",
|
||||
r"^text-decoration-(none|underline|line-through)$",
|
||||
# Visibility utilities
|
||||
r"^(visible|invisible)$",
|
||||
r"^visually-hidden$",
|
||||
r"^visually-hidden-focusable$",
|
||||
r"^overflow-(auto|hidden|visible|scroll)$",
|
||||
r"^user-select-(all|auto|none)$",
|
||||
# Interactive utilities
|
||||
r"^pe-(none|auto)$",
|
||||
r"^(active|disabled|show|hide|collapse|collapsed|fade|collapsing|close)$",
|
||||
# Form validation states (Bootstrap 5)
|
||||
r"^(is-valid|is-invalid|valid-feedback|invalid-feedback|valid-tooltip|invalid-tooltip)$",
|
||||
# Toast component (Bootstrap 5)
|
||||
r"^toast(-header|-body)?$",
|
||||
# Progress bars (Bootstrap 5)
|
||||
r"^progress(-bar|-bar-striped|-bar-animated|-stacked)?$",
|
||||
r"^btn-group(-vertical|-sm|-lg)?$",
|
||||
# Image utilities (Bootstrap 5)
|
||||
r"^img-(fluid|thumbnail)$",
|
||||
# Other Bootstrap utilities
|
||||
r"^(clearfix|sr-only|sr-only-focusable|small)$",
|
||||
r"^spinner-(border|border-sm|grow|grow-sm)$",
|
||||
r"^placeholder(-glow|-wave)?$",
|
||||
# Bootstrap Icons (bi-*)
|
||||
r"^bi(-[a-z0-9-]+)?$",
|
||||
# Font Awesome icons
|
||||
r"^(fa|fas|far|fab|fal|fad)$",
|
||||
r"^fa-[a-z0-9-]+$",
|
||||
# KaTeX math rendering
|
||||
r"^katex(-[a-z0-9-]+)?$",
|
||||
}
|
||||
|
||||
|
||||
def is_allowed_class(class_name: str) -> bool:
|
||||
"""Check if a class name is allowed without ldr- prefix."""
|
||||
# Remove any leading/trailing whitespace
|
||||
class_name = class_name.strip()
|
||||
|
||||
# Empty class names are invalid
|
||||
if not class_name:
|
||||
return False
|
||||
|
||||
# Check against allowed patterns
|
||||
for pattern in ALLOWED_PATTERNS:
|
||||
if re.match(pattern, class_name, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_css_file(file_path: Path) -> List[Tuple[int, str, str]]:
|
||||
"""Check CSS file for non-prefixed class definitions."""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
# Pattern to match CSS class selectors, including compound
|
||||
# selectors like .class1.class2
|
||||
# First match the leading dot (not preceded by a word char or /)
|
||||
# then capture everything including subsequent .class segments
|
||||
class_pattern = re.compile(
|
||||
r"(?<![\w/])\.([a-zA-Z][a-zA-Z0-9\-_]*(?:\.[a-zA-Z][a-zA-Z0-9\-_]*)*)"
|
||||
)
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Skip comments
|
||||
if "/*" in line or "*/" in line or line.strip().startswith("//"):
|
||||
continue
|
||||
|
||||
# Skip @import statements
|
||||
if "@import" in line:
|
||||
continue
|
||||
|
||||
matches = class_pattern.findall(line)
|
||||
for match in matches:
|
||||
# Split compound selectors like "class1.class2" into
|
||||
# individual class names for separate validation
|
||||
for class_name in match.split("."):
|
||||
if not class_name:
|
||||
continue
|
||||
if not is_allowed_class(class_name):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
class_name,
|
||||
f"CSS class '.{class_name}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_html_file(file_path: Path) -> List[Tuple[int, str, str]]:
|
||||
"""Check HTML file for non-prefixed class usage and CSS definitions in style tags."""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
# Pattern to match class attributes in HTML
|
||||
# Simplified pattern to avoid exponential backtracking
|
||||
# Matches: class="..." or class='...'
|
||||
# Uses two separate patterns to handle each quote type correctly
|
||||
class_attr_patterns = [
|
||||
re.compile(r'class\s*=\s*"([^"]*)"', re.IGNORECASE),
|
||||
re.compile(r"class\s*=\s*'([^']*)'", re.IGNORECASE),
|
||||
]
|
||||
|
||||
# Pattern to match CSS class definitions (same as in check_css_file)
|
||||
css_class_pattern = re.compile(
|
||||
r"(?<![\w/])\.([a-zA-Z][a-zA-Z0-9\-_]*(?:\.[a-zA-Z][a-zA-Z0-9\-_]*)*)"
|
||||
)
|
||||
|
||||
in_style_tag = False
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Skip HTML comments
|
||||
if "<!--" in line or "-->" in line:
|
||||
continue
|
||||
|
||||
# Skip Jinja2 template comments
|
||||
if "{#" in line or "#}" in line:
|
||||
continue
|
||||
|
||||
# Check for style tag boundaries
|
||||
if "<style" in line.lower():
|
||||
in_style_tag = True
|
||||
if "</style>" in line.lower():
|
||||
in_style_tag = False
|
||||
|
||||
# If we're inside a style tag, check for CSS class definitions
|
||||
if in_style_tag:
|
||||
# Skip CSS comments
|
||||
if (
|
||||
"/*" in line
|
||||
or "*/" in line
|
||||
or line.strip().startswith("//")
|
||||
):
|
||||
continue
|
||||
|
||||
# Check for CSS class definitions
|
||||
css_matches = css_class_pattern.findall(line)
|
||||
for match in css_matches:
|
||||
# Split compound selectors like "class1.class2"
|
||||
for class_name in match.split("."):
|
||||
if not class_name:
|
||||
continue
|
||||
if not is_allowed_class(class_name):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
class_name,
|
||||
f"CSS class '.{class_name}' in style tag should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
continue # Skip HTML class checking when in style tag
|
||||
|
||||
# Try both patterns to handle different quote types
|
||||
matches = []
|
||||
for pattern in class_attr_patterns:
|
||||
matches.extend(pattern.findall(line))
|
||||
|
||||
for class_attr in matches:
|
||||
# Skip if this is a JavaScript template literal (contains ${...})
|
||||
if "${" in class_attr:
|
||||
continue
|
||||
|
||||
# Skip if the entire class attribute contains Jinja2 template variables
|
||||
# This handles cases like class="alert alert-{{ category }}"
|
||||
if "{{" in class_attr or "{%" in class_attr:
|
||||
# Extract only the static class names (those not part of Jinja2 expressions)
|
||||
# Remove the Jinja2 parts and check remaining static classes
|
||||
# Remove Jinja2 expressions but keep the rest
|
||||
cleaned_attr = re.sub(
|
||||
r"\{\{[^}]*\}\}|\{%[^%]*%\}", "", class_attr
|
||||
)
|
||||
classes = cleaned_attr.split()
|
||||
else:
|
||||
# No Jinja2 in this attribute, check all classes
|
||||
classes = class_attr.split()
|
||||
|
||||
for class_name in classes:
|
||||
# Additional safety check for individual class names
|
||||
if (
|
||||
"{{" in class_name
|
||||
or "{%" in class_name
|
||||
or "{" in class_name
|
||||
or "}" in class_name
|
||||
):
|
||||
continue
|
||||
|
||||
# Skip JavaScript template literals and variables
|
||||
if any(
|
||||
char in class_name
|
||||
for char in [
|
||||
"$",
|
||||
"=",
|
||||
"!",
|
||||
"<",
|
||||
">",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
"||",
|
||||
"&&",
|
||||
"?",
|
||||
":",
|
||||
]
|
||||
):
|
||||
continue
|
||||
|
||||
# Skip common Jinja2 patterns
|
||||
if class_name in [
|
||||
"if",
|
||||
"else",
|
||||
"endif",
|
||||
"for",
|
||||
"endfor",
|
||||
"block",
|
||||
"endblock",
|
||||
"include",
|
||||
"extends",
|
||||
]:
|
||||
continue
|
||||
|
||||
# Skip Bootstrap icon classes
|
||||
if class_name.startswith("bi-") or class_name == "bi":
|
||||
continue
|
||||
|
||||
if not is_allowed_class(class_name):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
class_name,
|
||||
f"HTML class '{class_name}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_js_file(file_path: Path) -> List[Tuple[int, str, str]]:
|
||||
"""Check JavaScript file for non-prefixed class usage."""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
# Patterns to match class usage in JavaScript (excluding querySelector/jQuery)
|
||||
patterns = [
|
||||
# classList.add('classname')
|
||||
re.compile(
|
||||
r'classList\.(add|remove|toggle|contains)\s*\(\s*["\']([^"\']+)["\']'
|
||||
),
|
||||
# className = 'classname' or className: 'classname'
|
||||
re.compile(r'className\s*[:=]\s*["\']([^"\']+)["\']'),
|
||||
# getElementsByClassName('classname')
|
||||
re.compile(r'getElementsByClassName\s*\(\s*["\']([^"\']+)["\']'),
|
||||
# hasClass('classname'), addClass('classname'), removeClass('classname')
|
||||
re.compile(
|
||||
r'\.(hasClass|addClass|removeClass|toggleClass)\s*\(\s*["\']([^"\']+)["\']'
|
||||
),
|
||||
]
|
||||
|
||||
# Pattern for template literal className assignments:
|
||||
# className = `some-class ${dynamic}` or className: `some-class`
|
||||
template_literal_pattern = re.compile(r"className\s*[:=]\s*`([^`]+)`")
|
||||
|
||||
# Special patterns for querySelector and jQuery that need different handling
|
||||
querySelector_pattern = re.compile(
|
||||
r'querySelector(?:All)?\s*\(\s*["\']([^"\']+)["\']'
|
||||
)
|
||||
jquery_pattern = re.compile(r'\$\s*\(\s*["\']([^"\']+)["\']')
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Skip comments
|
||||
if "//" in line or "/*" in line or "*/" in line:
|
||||
# Simple comment detection (not perfect but good enough)
|
||||
comment_start = line.find("//")
|
||||
if comment_start >= 0:
|
||||
line = line[:comment_start]
|
||||
|
||||
# Handle querySelector and jQuery selectors specially
|
||||
for selector_match in querySelector_pattern.findall(line):
|
||||
# Extract class names from CSS selectors (e.g., '.class1 .class2', '.class1.class2')
|
||||
# Only match class selectors (starting with .)
|
||||
class_matches = re.findall(
|
||||
r"\.([a-zA-Z0-9_-]+)", selector_match
|
||||
)
|
||||
for cls in class_matches:
|
||||
if not is_allowed_class(cls):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
cls,
|
||||
f"JavaScript class '.{cls}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
for jquery_match in jquery_pattern.findall(line):
|
||||
# Extract class names from jQuery selectors
|
||||
class_matches = re.findall(r"\.([a-zA-Z0-9_-]+)", jquery_match)
|
||||
for cls in class_matches:
|
||||
if not is_allowed_class(cls):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
cls,
|
||||
f"JavaScript class '.{cls}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
# Handle template literal className assignments
|
||||
for tl_match in template_literal_pattern.findall(line):
|
||||
# Extract static class tokens by removing ${...} expressions
|
||||
static_part = re.sub(r"\$\{[^}]*\}", " ", tl_match)
|
||||
for cls in static_part.split():
|
||||
# Skip partial tokens that are fragments of dynamic
|
||||
# class names (e.g. "alert-" from "alert-${type}")
|
||||
if cls.endswith("-") or cls.startswith("-"):
|
||||
continue
|
||||
if not is_allowed_class(cls):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
cls,
|
||||
f"JavaScript class '{cls}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
# Handle other patterns
|
||||
for pattern in patterns:
|
||||
matches = pattern.findall(line)
|
||||
for match in matches:
|
||||
# Handle different capture groups
|
||||
if isinstance(match, tuple):
|
||||
# For patterns with multiple groups, get the class name
|
||||
class_name = match[-1] if len(match) > 1 else match[0]
|
||||
else:
|
||||
class_name = match
|
||||
|
||||
# Split multiple classes if present
|
||||
classes = class_name.split()
|
||||
for cls in classes:
|
||||
if not is_allowed_class(cls):
|
||||
errors.append(
|
||||
(
|
||||
line_num,
|
||||
cls,
|
||||
f"JavaScript class '{cls}' should be prefixed with 'ldr-'",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to check files passed as arguments."""
|
||||
if len(sys.argv) < 2:
|
||||
print("No files to check")
|
||||
return 0
|
||||
|
||||
has_errors = False
|
||||
|
||||
for file_arg in sys.argv[1:]:
|
||||
file_path = Path(file_arg)
|
||||
|
||||
if not file_path.exists():
|
||||
continue
|
||||
|
||||
# Match path components, not substrings: "lib" must be an actual
|
||||
# directory segment, not a fragment of "library.html" etc.
|
||||
if any(
|
||||
vendor in file_path.parts
|
||||
for vendor in [
|
||||
"vendor",
|
||||
"dist",
|
||||
"build",
|
||||
"lib",
|
||||
"libs",
|
||||
"node_modules",
|
||||
]
|
||||
):
|
||||
continue
|
||||
|
||||
errors = []
|
||||
|
||||
# Check based on file extension
|
||||
if file_path.suffix == ".css":
|
||||
errors = check_css_file(file_path)
|
||||
elif file_path.suffix in [".html", ".htm"]:
|
||||
errors = check_html_file(file_path)
|
||||
elif file_path.suffix in [".js", ".jsx", ".ts", ".tsx", ".mjs"]:
|
||||
errors = check_js_file(file_path)
|
||||
|
||||
if errors:
|
||||
has_errors = True
|
||||
print(f"\n❌ CSS class prefix errors in {file_path}:")
|
||||
for line_num, class_name, message in errors:
|
||||
print(f" Line {line_num}: {message}")
|
||||
|
||||
if has_errors:
|
||||
print("\n" + "=" * 60)
|
||||
print("CSS Class Naming Convention:")
|
||||
print(" All LDR-specific CSS classes must be prefixed with 'ldr-'")
|
||||
print(
|
||||
" This prevents conflicts when Vite bundles CSS from dependencies"
|
||||
)
|
||||
print(" Example: .custom-button → .ldr-custom-button")
|
||||
print("=" * 60)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user