|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +TinyEngine CSS Syntax Checker |
| 4 | +
|
| 5 | +检查DSL中的CSS字段是否有语法错误。 |
| 6 | +
|
| 7 | +支持三种模式: |
| 8 | +1. basic: 基础语法检查(默认,无需额外依赖) |
| 9 | +2. tinycss2: 使用 tinycss2 库(需要安装:pip install tinycss2) |
| 10 | +3. postcss: 使用 postcss 命令行工具(需要安装:npm install -g postcss) |
| 11 | +
|
| 12 | +用法: |
| 13 | + python3 check_css.py <dsl-file> [mode] |
| 14 | + python3 check_css.py <dsl-file> postcss # 使用postcss检查 |
| 15 | + python3 check_css.py <dsl-file> tinycss2 # 使用tinycss2检查 |
| 16 | +""" |
| 17 | + |
| 18 | +import json |
| 19 | +import re |
| 20 | +import subprocess |
| 21 | +import sys |
| 22 | +from typing import Dict, List, Any |
| 23 | + |
| 24 | + |
| 25 | +class CSSChecker: |
| 26 | + """CSS语法检查器基类""" |
| 27 | + |
| 28 | + def __init__(self, dsl_data: Dict[str, Any]): |
| 29 | + self.dsl = dsl_data |
| 30 | + self.errors = [] |
| 31 | + self.warnings = [] |
| 32 | + |
| 33 | + def check(self) -> bool: |
| 34 | + """检查CSS语法""" |
| 35 | + page_content = self.dsl.get('page_content', self.dsl) |
| 36 | + css_string = page_content.get('css', '') |
| 37 | + |
| 38 | + if not css_string: |
| 39 | + self.warnings.append("No CSS field found") |
| 40 | + return True |
| 41 | + |
| 42 | + return self._check_css(css_string) |
| 43 | + |
| 44 | + def _check_css(self, css: str) -> bool: |
| 45 | + """子类实现具体的检查逻辑""" |
| 46 | + raise NotImplementedError |
| 47 | + |
| 48 | + def report(self) -> str: |
| 49 | + """生成报告""" |
| 50 | + lines = [] |
| 51 | + if self.errors: |
| 52 | + lines.append("❌ CSS Errors:") |
| 53 | + for error in self.errors: |
| 54 | + lines.append(f" - {error}") |
| 55 | + if self.warnings: |
| 56 | + lines.append("⚠️ CSS Warnings:") |
| 57 | + for warning in self.warnings: |
| 58 | + lines.append(f" - {warning}") |
| 59 | + if not self.errors and not self.warnings: |
| 60 | + lines.append("✅ CSS check passed!") |
| 61 | + return "\n".join(lines) |
| 62 | + |
| 63 | + |
| 64 | +class BasicCSSChecker(CSSChecker): |
| 65 | + """基础CSS语法检查器(无需额外依赖)""" |
| 66 | + |
| 67 | + def _check_css(self, css: str) -> bool: |
| 68 | + """基础检查:括号匹配、基本语法""" |
| 69 | + # 检查括号匹配 |
| 70 | + stack = [] |
| 71 | + i = 0 |
| 72 | + while i < len(css): |
| 73 | + char = css[i] |
| 74 | + if char in '{': |
| 75 | + stack.append((char, i)) |
| 76 | + elif char in '}': |
| 77 | + if not stack or stack[-1][0] != '{': |
| 78 | + self.errors.append(f"Unmatched '}}' at position {i}") |
| 79 | + return False |
| 80 | + stack.pop() |
| 81 | + elif char in '(': |
| 82 | + stack.append((char, i)) |
| 83 | + elif char in ')': |
| 84 | + if not stack or stack[-1][0] != '(': |
| 85 | + self.errors.append(f"Unmatched ')' at position {i}") |
| 86 | + return False |
| 87 | + stack.pop() |
| 88 | + i += 1 |
| 89 | + |
| 90 | + if stack: |
| 91 | + for char, pos in stack: |
| 92 | + self.errors.append(f"Unclosed '{char}' at position {pos}") |
| 93 | + return False |
| 94 | + |
| 95 | + # 移除注释进行检查 |
| 96 | + css_no_comments = re.sub(r'/\*.*?\*/', '', css, flags=re.DOTALL) |
| 97 | + |
| 98 | + # 检查是否有CSS规则 |
| 99 | + if '{' not in css_no_comments: |
| 100 | + self.warnings.append("CSS may not contain any rules") |
| 101 | + |
| 102 | + # 检查分号使用 |
| 103 | + rules = re.findall(r'\{([^}]*)\}', css_no_comments) |
| 104 | + for rule in rules: |
| 105 | + properties = rule.split(';') |
| 106 | + for prop in properties[:-1]: # 最后一个可能为空 |
| 107 | + prop = prop.strip() |
| 108 | + if prop and ':' not in prop: |
| 109 | + self.warnings.append(f"Property without colon: {prop[:50]}") |
| 110 | + |
| 111 | + return len(self.errors) == 0 |
| 112 | + |
| 113 | + |
| 114 | +class TinyCSS2Checker(CSSChecker): |
| 115 | + """使用tinycss2库的CSS检查器""" |
| 116 | + |
| 117 | + def _check_css(self, css: str) -> bool: |
| 118 | + try: |
| 119 | + import tinycss2 |
| 120 | + except ImportError: |
| 121 | + self.errors.append( |
| 122 | + "tinycss2 not installed. Install with: pip install tinycss2" |
| 123 | + ) |
| 124 | + return False |
| 125 | + |
| 126 | + # 使用tinycss2解析CSS |
| 127 | + try: |
| 128 | + # 解析CSS规则列表 |
| 129 | + rules = tinycss2.parse_stylesheet(css, skip_comments=False) |
| 130 | + |
| 131 | + # tinycss2会抛出解析错误 |
| 132 | + for rule in rules: |
| 133 | + if rule.type == 'error': |
| 134 | + self.errors.append(f"Parse error: {rule.message}") |
| 135 | + |
| 136 | + return len(self.errors) == 0 |
| 137 | + |
| 138 | + except Exception as e: |
| 139 | + self.errors.append(f"Failed to parse CSS: {e}") |
| 140 | + return False |
| 141 | + |
| 142 | + |
| 143 | +class PostCSSChecker(CSSChecker): |
| 144 | + """使用postcss命令行工具的CSS检查器""" |
| 145 | + |
| 146 | + def _check_css(self, css: str) -> bool: |
| 147 | + # 检查postcss是否可用 |
| 148 | + try: |
| 149 | + result = subprocess.run( |
| 150 | + ['postcss', '--version'], |
| 151 | + capture_output=True, |
| 152 | + text=True, |
| 153 | + timeout=5 |
| 154 | + ) |
| 155 | + if result.returncode != 0: |
| 156 | + self.errors.append("postcss command failed. Install with: npm install -g postcss") |
| 157 | + return False |
| 158 | + except FileNotFoundError: |
| 159 | + self.errors.append("postcss not found. Install with: npm install -g postcss") |
| 160 | + return False |
| 161 | + except subprocess.TimeoutExpired: |
| 162 | + self.errors.append("postcss command timed out") |
| 163 | + return False |
| 164 | + |
| 165 | + # 将CSS写入临时文件 |
| 166 | + import tempfile |
| 167 | + with tempfile.NamedTemporaryFile(mode='w', suffix='.css', delete=False) as f: |
| 168 | + f.write(css) |
| 169 | + temp_file = f.name |
| 170 | + |
| 171 | + try: |
| 172 | + # 使用postcss解析CSS |
| 173 | + result = subprocess.run( |
| 174 | + ['postcss', temp_file], |
| 175 | + capture_output=True, |
| 176 | + text=True, |
| 177 | + timeout=10 |
| 178 | + ) |
| 179 | + |
| 180 | + # postcss的错误输出 |
| 181 | + if result.stderr: |
| 182 | + # 过滤掉警告(如<css input>:3:3: Yellow color) |
| 183 | + errors = [] |
| 184 | + warnings = [] |
| 185 | + for line in result.stderr.strip().split('\n'): |
| 186 | + if line: |
| 187 | + if any(w in line.lower() for w in ['warning', 'yellow']): |
| 188 | + warnings.append(line) |
| 189 | + else: |
| 190 | + errors.append(line) |
| 191 | + |
| 192 | + self.errors.extend(errors) |
| 193 | + self.warnings.extend(warnings) |
| 194 | + |
| 195 | + return len(self.errors) == 0 |
| 196 | + |
| 197 | + except subprocess.TimeoutExpired: |
| 198 | + self.errors.append("postcss command timed out") |
| 199 | + return False |
| 200 | + finally: |
| 201 | + import os |
| 202 | + try: |
| 203 | + os.unlink(temp_file) |
| 204 | + except: |
| 205 | + pass |
| 206 | + |
| 207 | + |
| 208 | +def main(): |
| 209 | + """主函数""" |
| 210 | + if len(sys.argv) < 2: |
| 211 | + print("Usage: check_css.py <dsl-file> [mode]") |
| 212 | + print(" mode: basic (default), tinycss2, postcss") |
| 213 | + sys.exit(1) |
| 214 | + |
| 215 | + file_path = sys.argv[1] |
| 216 | + mode = sys.argv[2] if len(sys.argv) > 2 else 'basic' |
| 217 | + |
| 218 | + # 读取DSL文件 |
| 219 | + try: |
| 220 | + with open(file_path, 'r', encoding='utf-8') as f: |
| 221 | + dsl_data = json.load(f) |
| 222 | + except json.JSONDecodeError as e: |
| 223 | + print(f"❌ Invalid JSON: {e}") |
| 224 | + sys.exit(1) |
| 225 | + except FileNotFoundError: |
| 226 | + print(f"❌ File not found: {file_path}") |
| 227 | + sys.exit(1) |
| 228 | + |
| 229 | + # 选择检查器 |
| 230 | + checkers = { |
| 231 | + 'basic': BasicCSSChecker, |
| 232 | + 'tinycss2': TinyCSS2Checker, |
| 233 | + 'postcss': PostCSSChecker |
| 234 | + } |
| 235 | + |
| 236 | + checker_class = checkers.get(mode) |
| 237 | + if not checker_class: |
| 238 | + print(f"❌ Unknown mode: {mode}") |
| 239 | + print(f"Available modes: {', '.join(checkers.keys())}") |
| 240 | + sys.exit(1) |
| 241 | + |
| 242 | + checker = checker_class(dsl_data) |
| 243 | + is_valid = checker.check() |
| 244 | + print(checker.report()) |
| 245 | + sys.exit(0 if is_valid else 1) |
| 246 | + |
| 247 | + |
| 248 | +if __name__ == '__main__': |
| 249 | + main() |
0 commit comments