465 lines
17 KiB
Python
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())
|