zcbot/skills/ppt/scripts/error_helper.py

465 lines
17 KiB
Python

#!/usr/bin/env python3
"""
PPT Master - Error Message Helper
Provides user-friendly error messages and specific fix suggestions.
"""
import argparse
from typing import Dict, List, Optional
class ErrorHelper:
"""Error message helper."""
# Error types and their corresponding fix suggestions
ERROR_SOLUTIONS = {
'missing_readme': {
'message': 'Missing README.md file',
'solutions': [
'Create a README.md file with project description, usage instructions, etc.',
'Reference template: examples/google_annual_report_ppt169_20251116/README.md',
'Or use command: cp examples/google_annual_report_ppt169_20251116/README.md <your_project>/'
],
'severity': 'error'
},
'missing_spec': {
'message': 'Missing design specification file',
'solutions': [
'Create a design_spec.md file',
'Include: canvas specs, color scheme, font specs, layout specs, content outline',
'Refer to the design specification generated by the Strategist role'
],
'severity': 'warning'
},
'missing_svg_output': {
'message': 'Missing svg_output directory',
'solutions': [
'Create the svg_output directory: mkdir svg_output',
'Place generated SVG files in this directory',
'Ensure SVG files follow naming convention: slide_XX_name.svg'
],
'severity': 'error'
},
'empty_svg_output': {
'message': 'svg_output directory is empty',
'solutions': [
'Use the AI role (Executor) to generate SVG files',
'Save SVG files to the svg_output directory',
'Ensure file naming format: slide_01_cover.svg, slide_02_content.svg, etc.'
],
'severity': 'warning'
},
'invalid_svg_naming': {
'message': 'Non-standard SVG file naming',
'solutions': [
'Rename SVG files using format: slide_XX_name.svg',
'XX should be a two-digit number (01, 02, ...)',
'name should use English or pinyin, separated by underscores',
'Example: slide_01_cover.svg, slide_02_overview.svg'
],
'severity': 'warning'
},
'missing_project_date': {
'message': 'Project directory missing date suffix',
'solutions': [
'Rename the project directory to add a date suffix: _YYYYMMDD',
'Format: {project_name}_{format}_{YYYYMMDD}',
'Example: my_project_ppt169_20251116',
'Command: mv old_name new_name_ppt169_20251116'
],
'severity': 'warning'
},
'viewbox_mismatch': {
'message': 'SVG viewBox does not match canvas format',
'solutions': [
'Check the viewBox attribute of SVG files',
'Ensure it matches the project canvas format',
'PPT 16:9 should be: viewBox="0 0 1280 720"',
'PPT 4:3 should be: viewBox="0 0 1024 768"',
'Reference: references/canvas-formats.md'
],
'severity': 'warning'
},
'multiple_viewboxes': {
'message': 'Multiple different viewBox settings detected',
'solutions': [
'Unify the viewBox across all SVG files',
'All pages in the same project should use the same canvas size',
'Use find-and-replace tools for batch correction',
'Reference the viewBox setting of the first page'
],
'severity': 'warning'
},
'no_viewbox': {
'message': 'SVG file missing viewBox attribute',
'solutions': [
'Add the viewBox attribute to the SVG root element',
'Format: <svg viewBox="0 0 1280 720" ...>',
'Ensure width, height are consistent with viewBox',
'This is a mandatory requirement for SVG generation'
],
'severity': 'error'
},
'foreignobject_detected': {
'message': 'Forbidden <foreignObject> element detected',
'solutions': [
'Remove <foreignObject> elements',
'Use <text> + <tspan> for manual line wrapping',
'This is a project technical specification requirement',
'Reference: references/shared-standards.md'
],
'severity': 'error'
},
'clippath_on_non_image': {
'message': 'clip-path is only allowed on <image> elements',
'solutions': [
'Remove clip-path from shapes / groups / text',
'Draw the target geometry directly with the matching native element: <circle> / <ellipse> / <rect rx="..."> / <polygon> / <path>. A rect clipped to a circle is just a <circle>.',
'clip-path on <image> is conditionally allowed — see references/shared-standards.md §1.2'
],
'severity': 'error'
},
'clippath_def_missing': {
'message': 'clip-path references a <clipPath> id that does not exist in <defs>',
'solutions': [
'Define the referenced <clipPath id="..."> inside <defs>',
'The clipPath must contain exactly one shape child (circle / ellipse / rect with rx,ry / path / polygon)',
'Reference: references/shared-standards.md §1.2'
],
'severity': 'error'
},
'mask_detected': {
'message': 'Forbidden <mask> element detected',
'solutions': [
'Remove <mask> elements',
'PPT does not support SVG masks',
'Use opacity (opacity/fill-opacity) as an alternative'
],
'severity': 'error'
},
'style_element_detected': {
'message': 'Forbidden <style> element detected',
'solutions': [
'Remove <style> elements',
'Convert CSS styles to inline attributes',
'Example: fill="#000" instead of class="text-black"'
],
'severity': 'error'
},
'class_attribute_detected': {
'message': 'Forbidden class attribute detected',
'solutions': [
'Remove all class attributes',
'Use inline styles instead',
'Example: fill="#000" stroke="#333" directly on the element'
],
'severity': 'error'
},
'id_attribute_detected': {
'message': 'Forbidden id attribute detected',
'solutions': [
'Remove all id attributes',
'Use inline styles instead',
'Avoid relying on selectors for positioning or style reuse'
],
'severity': 'error'
},
'external_css_detected': {
'message': 'Forbidden external CSS reference detected',
'solutions': [
'Remove <?xml-stylesheet?> declarations',
'Remove <link rel="stylesheet"> references',
'Remove @import external styles',
'Convert styles to inline attributes'
],
'severity': 'error'
},
'symbol_use_detected': {
'message': 'Forbidden <symbol> + <use> complex usage detected',
'solutions': [
'Expand <symbol> into actual SVG code',
'Avoid <symbol> + <use> reuse structures',
'Embed SVG paths directly when icons are needed'
],
'severity': 'error'
},
# Note: <marker> and marker-end are NO LONGER forbidden — they are
# conditionally allowed (see references/shared-standards.md §1.1).
# The converter maps qualifying markers to native DrawingML arrow heads.
'marker_orphan_ref': {
'message': 'marker-start/marker-end references a marker id, but no <marker> element is defined',
'solutions': [
'Define the <marker> inside <defs>',
'Or remove the marker-start/marker-end attribute',
'See shared-standards.md §1.1 for marker constraints',
],
'severity': 'error'
},
'rgba_detected': {
'message': 'Forbidden rgba() color detected',
'solutions': [
'Replace rgba() with hex + opacity notation',
'Example: fill="#FFFFFF" fill-opacity="0.1"',
'Use stroke-opacity for strokes'
],
'severity': 'error'
},
'group_opacity_detected': {
'message': 'Forbidden <g opacity> detected',
'solutions': [
'Remove group-level opacity',
'Set opacity individually on each child element',
'Use fill-opacity / stroke-opacity for control'
],
'severity': 'error'
},
'image_opacity_detected': {
'message': 'Forbidden <image opacity> detected',
'solutions': [
'Remove image opacity attribute',
'Add a <rect> overlay to control transparency',
'Ensure overlay color matches the background'
],
'severity': 'error'
},
'event_attribute_detected': {
'message': 'Forbidden event attribute detected',
'solutions': [
'Remove onclick/onload and other event attributes',
'Scripts and event handling are forbidden in SVG',
'Implement interactivity in PPT instead'
],
'severity': 'error'
},
'set_detected': {
'message': 'Forbidden <set> element detected',
'solutions': [
'Remove <set> elements',
'SVG animations will not be exported to PPT',
'Use PPT native animations for animation effects'
],
'severity': 'error'
},
'iframe_detected': {
'message': 'Forbidden <iframe> element detected',
'solutions': [
'Remove <iframe> elements',
'External pages should not be embedded in SVG'
],
'severity': 'error'
},
'textpath_detected': {
'message': 'Forbidden <textPath> element detected',
'solutions': [
'Remove <textPath> elements',
'Text on path is not compatible with PPT',
'Use regular <text> elements and adjust position manually'
],
'severity': 'error'
},
'webfont_detected': {
'message': 'Forbidden web font (@font-face) detected',
'solutions': [
'Remove @font-face declarations',
'End every font-family stack with a PPT-safe pre-installed family',
'Example: font-family: "Microsoft YaHei", Arial, sans-serif'
],
'severity': 'error'
},
'animation_detected': {
'message': 'Forbidden SMIL animation element detected',
'solutions': [
'Remove all <animate>, <animateMotion>, <animateTransform> and similar elements',
'SVG animations will not be exported to PPT',
'Use PPT native animations for animation effects'
],
'severity': 'error'
},
'script_detected': {
'message': 'Forbidden <script> element detected',
'solutions': [
'Remove <script> elements',
'Scripts and event handling are forbidden',
'JavaScript in SVG will not execute in PPT'
],
'severity': 'error'
},
'invalid_font': {
'message': 'Font stack does not end on a PPT-safe family',
'solutions': [
'End the stack with a cross-platform pre-installed family',
'CJK: "Microsoft YaHei", sans-serif | SimSun, serif',
'Latin: Arial, sans-serif | "Times New Roman", serif',
'Mono: Consolas, "Courier New", monospace',
'See strategist.md §g for the full PPT-safe discipline'
],
'severity': 'warning'
}
}
@classmethod
def get_solution(cls, error_type: str, context: Optional[Dict] = None) -> Dict:
"""
Get the solution for an error.
Args:
error_type: Error type
context: Context information (optional)
Returns:
Dictionary containing message, solutions, severity
"""
if error_type in cls.ERROR_SOLUTIONS:
solution = cls.ERROR_SOLUTIONS[error_type].copy()
# Customize message based on context
if context:
solution = cls._customize_solution(solution, context)
return solution
# Unknown error type
return {
'message': 'Unknown error',
'solutions': ['Please check the documentation or contact the maintainer'],
'severity': 'error'
}
@classmethod
def _customize_solution(cls, solution: Dict, context: Dict) -> Dict:
"""
Customize solution based on context.
Args:
solution: Original solution
context: Context information
Returns:
Customized solution
"""
customized = solution.copy()
# Customize based on project path
if 'project_path' in context:
project_path = context['project_path']
customized['solutions'] = [
s.replace('<project_path>', project_path).replace(
'<your_project>', project_path)
for s in customized['solutions']
]
# Customize based on filename
if 'file_name' in context:
file_name = context['file_name']
customized['message'] = f"{customized['message']}: {file_name}"
# Customize based on expected/actual values
if 'expected' in context and 'actual' in context:
customized['message'] += f" (expected: {context['expected']}, actual: {context['actual']})"
return customized
@classmethod
def format_error_message(cls, error_type: str, context: Optional[Dict] = None) -> str:
"""
Format error message (for terminal output).
Args:
error_type: Error type
context: Context information
Returns:
Formatted error message string
"""
solution = cls.get_solution(error_type, context)
lines = []
# Error message
severity_icon = "[ERROR]" if solution['severity'] == 'error' else "[WARN]"
lines.append(f"{severity_icon} {solution['message']}")
# Solutions
if solution['solutions']:
lines.append("\nSuggested fixes:")
for i, sol in enumerate(solution['solutions'], 1):
lines.append(f" {i}. {sol}")
return "\n".join(lines)
@classmethod
def print_error(cls, error_type: str, context: Optional[Dict] = None):
"""
Print formatted error message.
Args:
error_type: Error type
context: Context information
"""
print(cls.format_error_message(error_type, context))
@classmethod
def get_all_error_types(cls) -> List[str]:
"""Get all supported error types."""
return list(cls.ERROR_SOLUTIONS.keys())
@classmethod
def print_help(cls):
"""Print all error types and solutions."""
print("PPT Master - Error Types and Solutions\n")
print("=" * 80)
for error_type, info in cls.ERROR_SOLUTIONS.items():
print(f"\n[{error_type}]")
print(f"Message: {info['message']}")
print(f"Severity: {info['severity']}")
print("Solutions:")
for i, sol in enumerate(info['solutions'], 1):
print(f" {i}. {sol}")
print("-" * 80)
def build_parser() -> argparse.ArgumentParser:
"""Build the command-line parser."""
parser = argparse.ArgumentParser(
description="Look up PPT Master error messages and suggested fixes.",
)
parser.add_argument(
"error_type",
nargs="?",
choices=sorted(ErrorHelper.ERROR_SOLUTIONS),
help="Error type to explain",
)
parser.add_argument(
"context",
nargs="*",
metavar="key=value",
help="Optional context values used by templates",
)
return parser
def main(argv: list[str] | None = None) -> int:
"""Run the CLI entry point for error lookup."""
parser = build_parser()
args = parser.parse_args(argv)
if not args.error_type:
ErrorHelper.print_help()
return 0
context = {}
for item in args.context:
if '=' not in item:
parser.error(f"context values must use key=value syntax: {item}")
key, value = item.split('=', 1)
context[key] = value
print(ErrorHelper.format_error_message(args.error_type, context))
return 0
if __name__ == '__main__':
raise SystemExit(main())