Files
wehub-resource-sync 7a0da7932b
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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

517 lines
20 KiB
Python
Executable File

#!/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())