Files
Neo-ZQYY/tools/h5-to-mp-checker/check_h5_to_mp.py
2026-03-15 10:15:02 +08:00

301 lines
9.9 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
H5 到小程序样式对比检查工具
用法:
python check_h5_to_mp.py --h5 h5.html --wxss mp.wxss --output report.md
"""
import re
import argparse
from pathlib import Path
from typing import Dict, List, Set
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
class IssueLevel(Enum):
"""问题级别"""
CRITICAL = "❌ 严重" # 差异 > 20%
WARNING = "⚠️ 警告" # 差异 10-20%
INFO = " 提示" # 差异 < 10%
OK = "✅ 正确" # 差异 < 2%
@dataclass
class StyleIssue:
"""样式问题"""
level: IssueLevel
category: str
tailwind_class: str
property_name: str
h5_value: str
expected_rpx: str
actual_rpx: str
difference: float
location: str
# Tailwind CSS v3 配置
TAILWIND_CONFIG = {
'text': {
'text-xs': {'font-size': '12px', 'line-height': '16px'},
'text-sm': {'font-size': '14px', 'line-height': '20px'},
'text-base': {'font-size': '16px', 'line-height': '24px'},
'text-lg': {'font-size': '18px', 'line-height': '28px'},
'text-xl': {'font-size': '20px', 'line-height': '28px'},
'text-2xl': {'font-size': '24px', 'line-height': '32px'},
},
'spacing': {
'p-2': '8px', 'p-3': '12px', 'p-4': '16px',
'm-2': '8px', 'm-3': '12px', 'm-4': '16px',
'gap-2': '8px', 'gap-3': '12px',
'ml-11': '44px', 'mb-2': '8px', 'mb-3': '12px',
'pt-2': '8px', 'pt-3': '12px',
},
}
PX_TO_RPX = 1.8204 # 412px 设备
def px_to_rpx(px_value: str) -> float:
"""将 px 值转换为 rpx"""
if not px_value or px_value == '0':
return 0
match = re.search(r'([\d.]+)', px_value)
if match:
return round(float(match.group(1)) * PX_TO_RPX, 2)
return 0
def rpx_to_px(rpx_value: str) -> float:
"""将 rpx 值转换为 px"""
if not rpx_value or rpx_value == '0':
return 0
match = re.search(r'([\d.]+)', rpx_value)
if match:
return round(float(match.group(1)) / PX_TO_RPX, 2)
return 0
def extract_tailwind_classes(html_content: str) -> Dict[str, Set[str]]:
"""提取 HTML 中所有 Tailwind 类"""
classes = {'text': set(), 'spacing': set(), 'custom': set()}
class_attrs = re.findall(r'class="([^"]+)"', html_content)
for class_str in class_attrs:
for cls in class_str.split():
if cls.startswith('text-'):
if '[' in cls:
classes['custom'].add(cls)
else:
classes['text'].add(cls)
elif re.match(r'^[pm][tblrxy]?-', cls) or cls.startswith('gap-'):
classes['spacing'].add(cls)
return classes
def extract_wxss_styles(wxss_content: str) -> Dict[str, Dict[str, str]]:
"""提取 WXSS 中所有样式"""
styles = {}
pattern = r'\.([a-zA-Z0-9_-]+(?:--[a-zA-Z0-9_-]+)?)\s*\{([^}]+)\}'
matches = re.findall(pattern, wxss_content, re.MULTILINE)
for class_name, rules in matches:
styles[class_name] = {}
for rule in rules.split(';'):
rule = rule.strip()
if ':' in rule:
prop, value = rule.split(':', 1)
value = re.sub(r'/\*.*?\*/', '', value).strip()
styles[class_name][prop.strip()] = value
return styles
def check_global_styles(wxss_content: str) -> List[StyleIssue]:
"""检查全局样式"""
issues = []
page_match = re.search(r'page\s*\{([^}]+)\}', wxss_content)
if page_match and 'line-height:' in page_match.group(1):
lh_match = re.search(r'line-height:\s*([\d.]+)', page_match.group(1))
if lh_match:
issues.append(StyleIssue(
level=IssueLevel.CRITICAL,
category='global',
tailwind_class='page',
property_name='line-height',
h5_value='无全局设置',
expected_rpx='',
actual_rpx=lh_match.group(1),
difference=100.0,
location='page 全局样式'
))
return issues
def check_text_classes(tailwind_classes: Set[str], wxss_styles: Dict) -> List[StyleIssue]:
"""检查文本类"""
issues = []
for tw_class in tailwind_classes:
if tw_class not in TAILWIND_CONFIG['text']:
continue
config = TAILWIND_CONFIG['text'][tw_class]
h5_font_size = config['font-size']
h5_line_height = config['line-height']
expected_fs_rpx = px_to_rpx(h5_font_size)
expected_lh_rpx = px_to_rpx(h5_line_height)
for class_name, props in wxss_styles.items():
if 'font-size' not in props:
continue
actual_fs = props['font-size']
actual_px = rpx_to_px(actual_fs)
expected_px = float(h5_font_size.replace('px', ''))
if abs(actual_px - expected_px) < 2:
# 检查 line-height
if 'line-height' not in props:
issues.append(StyleIssue(
level=IssueLevel.CRITICAL,
category='text',
tailwind_class=tw_class,
property_name='line-height',
h5_value=h5_line_height,
expected_rpx=f'{expected_lh_rpx}rpx',
actual_rpx='缺失',
difference=100.0,
location=f'.{class_name}'
))
else:
actual_lh = props['line-height']
actual_lh_px = rpx_to_px(actual_lh)
expected_lh_px = float(h5_line_height.replace('px', ''))
diff = abs(actual_lh_px - expected_lh_px) / expected_lh_px * 100
if diff > 2:
level = IssueLevel.CRITICAL if diff > 20 else (
IssueLevel.WARNING if diff > 10 else IssueLevel.INFO)
issues.append(StyleIssue(
level=level,
category='text',
tailwind_class=tw_class,
property_name='line-height',
h5_value=h5_line_height,
expected_rpx=f'{expected_lh_rpx}rpx',
actual_rpx=actual_lh,
difference=diff,
location=f'.{class_name}'
))
return issues
def generate_report(issues: List[StyleIssue], output_file: Path):
"""生成 Markdown 报告"""
critical = [i for i in issues if i.level == IssueLevel.CRITICAL]
warning = [i for i in issues if i.level == IssueLevel.WARNING]
info = [i for i in issues if i.level == IssueLevel.INFO]
report = f"""# H5 到小程序样式对比报告
> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
## 📊 问题统计
- ❌ 严重问题:{len(critical)}
- ⚠️ 警告:{len(warning)}
- 提示:{len(info)}
- **总计**{len(issues)}
---
## ❌ 严重问题({len(critical)} 个)
"""
for issue in critical:
report += f"""### {issue.level.value} {issue.tailwind_class} - {issue.property_name}
- **位置**{issue.location}
- **H5 值**{issue.h5_value}
- **应该是**{issue.expected_rpx}
- **实际是**{issue.actual_rpx}
- **差异**{issue.difference:.1f}%
"""
if warning:
report += f"\n## ⚠️ 警告({len(warning)} 个)\n\n"
for issue in warning:
report += f"- {issue.tailwind_class} - {issue.property_name}: 差异 {issue.difference:.1f}% ({issue.location})\n"
if info:
report += f"\n## 提示({len(info)} 个)\n\n"
for issue in info:
report += f"- {issue.tailwind_class} - {issue.property_name}: 差异 {issue.difference:.1f}% ({issue.location})\n"
output_file.write_text(report, encoding='utf-8')
def main():
import sys
import io
# 修复 Windows 控制台编码问题
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
parser = argparse.ArgumentParser(description='H5 到小程序样式对比检查工具')
parser.add_argument('--h5', required=True, help='H5 HTML 文件路径')
parser.add_argument('--wxss', required=True, help='小程序 WXSS 文件路径')
parser.add_argument('--output', default='report.md', help='输出报告路径')
args = parser.parse_args()
h5_file = Path(args.h5)
wxss_file = Path(args.wxss)
output_file = Path(args.output)
if not h5_file.exists():
print(f"[ERROR] H5 文件不存在:{h5_file}")
return
if not wxss_file.exists():
print(f"[ERROR] WXSS 文件不存在:{wxss_file}")
return
print("[1/6] 读取文件...")
h5_content = h5_file.read_text(encoding='utf-8')
wxss_content = wxss_file.read_text(encoding='utf-8')
print("[2/6] 提取 Tailwind 类...")
tailwind_classes = extract_tailwind_classes(h5_content)
print(f" - text 类:{len(tailwind_classes['text'])}")
print("[3/6] 提取 WXSS 样式...")
wxss_styles = extract_wxss_styles(wxss_content)
print(f" - 样式类:{len(wxss_styles)}")
print("[4/6] 检查全局样式...")
issues = check_global_styles(wxss_content)
print("[5/6] 检查文本类...")
issues.extend(check_text_classes(tailwind_classes['text'], wxss_styles))
print(f"\n[统计] 发现 {len(issues)} 个问题")
print("[6/6] 生成报告...")
generate_report(issues, output_file)
print(f"[完成] 报告已生成:{output_file}")
if __name__ == '__main__':
main()