import re from typing import List, Dict, Tuple, Optional class ManifestParser: def __init__(self): self.tasks = [] # 存储解析后的任务指令 def parse(self, content: str): """解析 manifest.txt 或邮件正文,生成内部指令结构""" lines = [line.strip() for line in content.splitlines() if line.strip() and not line.strip().startswith('#')] self.tasks = [] current_scope = None # 当前压缩包作用域,None表示根目录 i = 0 while i < len(lines): line = lines[i] # 处理压缩包作用域开始 if line.startswith('in ') and line.endswith(':'): archive_name = line[3:-1].strip() current_scope = archive_name i += 1 continue elif line == 'in .': current_scope = None i += 1 continue # 检查行内是否包含 "in xxx" 临时作用域 inline_archive = None if ' in ' in line: parts = line.split(' in ') cmd_part = parts[0].strip() archive_part = parts[1].strip() # 如果archive_part还有更多内容(如"to: xxx"),需重新组合?假设格式严格:"指令 in archive.zip" if ' ' in archive_part: # 复杂情况,暂不支持,要求用户使用单独行 pass else: inline_archive = archive_part line = cmd_part # 解析指令 if line.startswith('to:'): # 全局默认 targets = self._parse_targets(line[3:]) self.tasks.append({ 'type': 'global', 'targets': targets, 'scope': current_scope, 'inline_archive': inline_archive }) elif line.startswith('from:'): # 按格式批量转换 match = re.match(r'from:\s*(\S+)\s+to:\s*(.+)', line) if match: src_format = match.group(1).lower() targets = self._parse_targets(match.group(2)) self.tasks.append({ 'type': 'by_format', 'src_format': src_format, 'targets': targets, 'scope': current_scope, 'inline_archive': inline_archive }) elif line.startswith('from.name:'): # 精准文件名匹配 rest = line[10:].strip() if ' to.name:' in rest: # 重命名 parts = rest.split(' to.name:') src_name = parts[0].strip() dst_spec = parts[1].strip() # 解析 dst_spec: "newname.ext(size)" dst_name, size = self._parse_dst_with_size(dst_spec) self.tasks.append({ 'type': 'rename', 'src_name': src_name, 'dst_name': dst_name, 'size': size, 'scope': current_scope, 'inline_archive': inline_archive }) else: # to: 格式 parts = rest.split(' to:') src_name = parts[0].strip() targets = self._parse_targets(parts[1]) self.tasks.append({ 'type': 'by_name', 'src_name': src_name, 'targets': targets, 'scope': current_scope, 'inline_archive': inline_archive }) elif line.startswith('to.name:'): # 多版本导出中的 to.name 行,需要结合前面的 from.name 处理 # 我们在主流程中采用累积方式,这里简单处理,实际在main中会连续读取 # 为简化,不在此处实现多行组合,而是在主程序中按行处理上下文 # 让主程序负责收集连续的 to.name pass else: logger.warning(f"无法识别的指令: {line}") i += 1 return self.tasks def _parse_targets(self, target_str: str) -> List[Tuple[str, Optional[Tuple]]]: """解析 "webp, png(16:9), jpg(800x600)" -> [('webp',None), ('png',('16:9')), ('jpg',('800x600'))]""" items = [t.strip() for t in target_str.split(',')] result = [] for item in items: size = None if '(' in item and item.endswith(')'): fmt, size_part = item.split('(', 1) size_str = size_part.rstrip(')') if ':' in size_str: # 比例 w_ratio, h_ratio = map(int, size_str.split(':')) size = ('ratio', w_ratio, h_ratio) elif 'x' in size_str: w, h = map(int, size_str.split('x')) size = ('pixel', w, h) result.append((fmt.lower(), size)) else: result.append((item.lower(), None)) return result def _parse_dst_with_size(self, dst_spec: str) -> Tuple[str, Optional[Tuple]]: """解析 "banner.webp(800x600)" -> ('banner.webp', ('pixel',800,600))""" size = None if '(' in dst_spec and dst_spec.endswith(')'): name, size_part = dst_spec.split('(', 1) size_str = size_part.rstrip(')') if ':' in size_str: w, h = map(int, size_str.split(':')) size = ('ratio', w, h) elif 'x' in size_str: w, h = map(int, size_str.split('x')) size = ('pixel', w, h) return name.strip(), size else: return dst_spec.strip(), None