diff --git a/.claude/skills/design-system/scripts/generate-slide.py b/.claude/skills/design-system/scripts/generate-slide.py index 228a50a..2de390d 100644 --- a/.claude/skills/design-system/scripts/generate-slide.py +++ b/.claude/skills/design-system/scripts/generate-slide.py @@ -8,9 +8,26 @@ NO hardcoded colors, fonts, or spacing allowed import argparse import json +from html import escape from pathlib import Path from datetime import datetime + +def _e(value, default=''): + """HTML-escape a user-supplied value for safe embedding in HTML content.""" + return escape(str(value if value is not None else default)) + + +def _safe_url(url, default='#'): + """Validate and escape a URL for use in href attributes. + + Only allows http://, https://, #, and / schemes to prevent + javascript: URI injection (CWE-79). + """ + if url and str(url).strip().lower().startswith(('http://', 'https://', '#', '/')): + return escape(str(url), quote=True) + return default + # Paths SCRIPT_DIR = Path(__file__).parent DATA_DIR = SCRIPT_DIR.parent / "data" @@ -412,16 +429,16 @@ def generate_title_slide(data): """Title slide with gradient headline""" return f'''
-
{data.get('badge', 'Pitch Deck')}
-

{data.get('title', 'Your Title Here')}

-

{data.get('subtitle', 'Your compelling subtitle')}

+
{_e(data.get('badge', 'Pitch Deck'))}
+

{_e(data.get('title', 'Your Title Here'))}

+

{_e(data.get('subtitle', 'Your compelling subtitle'))}

- {data.get('cta', 'Get Started')} - {data.get('secondary_cta', 'Learn More')} + {_e(data.get('cta', 'Get Started'))} + {_e(data.get('secondary_cta', 'Learn More'))}
''' @@ -432,27 +449,27 @@ def generate_problem_slide(data): return f'''
The Problem
-

{data.get('headline', 'The problem your audience faces')}

+

{_e(data.get('headline', 'The problem your audience faces'))}

01
-

{data.get('pain_1_title', 'Pain Point 1')}

-

{data.get('pain_1_desc', 'Description of the first pain point')}

+

{_e(data.get('pain_1_title', 'Pain Point 1'))}

+

{_e(data.get('pain_1_desc', 'Description of the first pain point'))}

02
-

{data.get('pain_2_title', 'Pain Point 2')}

-

{data.get('pain_2_desc', 'Description of the second pain point')}

+

{_e(data.get('pain_2_title', 'Pain Point 2'))}

+

{_e(data.get('pain_2_desc', 'Description of the second pain point'))}

03
-

{data.get('pain_3_title', 'Pain Point 3')}

-

{data.get('pain_3_desc', 'Description of the third pain point')}

+

{_e(data.get('pain_3_title', 'Pain Point 3'))}

+

{_e(data.get('pain_3_desc', 'Description of the third pain point'))}

''' @@ -463,28 +480,28 @@ def generate_solution_slide(data): return f'''
The Solution
-

{data.get('headline', 'How we solve this')}

+

{_e(data.get('headline', 'How we solve this'))}

-

{data.get('feature_1_title', 'Feature 1')}

-

{data.get('feature_1_desc', 'Description of feature 1')}

+

{_e(data.get('feature_1_title', 'Feature 1'))}

+

{_e(data.get('feature_1_desc', 'Description of feature 1'))}

-

{data.get('feature_2_title', 'Feature 2')}

-

{data.get('feature_2_desc', 'Description of feature 2')}

+

{_e(data.get('feature_2_title', 'Feature 2'))}

+

{_e(data.get('feature_2_desc', 'Description of feature 2'))}

-

{data.get('feature_3_title', 'Feature 3')}

-

{data.get('feature_3_desc', 'Description of feature 3')}

+

{_e(data.get('feature_3_title', 'Feature 3'))}

+

{_e(data.get('feature_3_desc', 'Description of feature 3'))}

@@ -496,8 +513,8 @@ def generate_solution_slide(data):
''' @@ -514,21 +531,21 @@ def generate_metrics_slide(data): metrics_html = ''.join([f'''
-
{m['value']}
-
{m['label']}
+
{_e(m.get('value', ''))}
+
{_e(m.get('label', ''))}
''' for m in metrics[:4]]) return f'''
Traction
-

{data.get('headline', 'Our Growth')}

+

{_e(data.get('headline', 'Our Growth'))}

{metrics_html}
''' @@ -544,25 +561,25 @@ def generate_chart_slide(data): ]) bars_html = ''.join([f''' -
- {b.get('display', str(b['value']) + '%')} - {b['label']} +
+ {_e(b.get('display', str(b.get('value', 0)) + '%'))} + {_e(b.get('label', ''))}
''' for b in bars]) return f'''
-
{data.get('badge', 'Growth')}
-

{data.get('headline', 'Revenue Growth')}

+
{_e(data.get('badge', 'Growth'))}
+

{_e(data.get('headline', 'Revenue Growth'))}

-
{data.get('chart_title', 'Quarterly Revenue')}
+
{_e(data.get('chart_title', 'Quarterly Revenue'))}
{bars_html}
''' @@ -574,13 +591,13 @@ def generate_testimonial_slide(data):
What They Say
-

"{data.get('quote', 'This product changed how we work. Incredible results.')}"

-

{data.get('author', 'Jane Doe')}

-

{data.get('role', 'CEO, Example Company')}

+

"{_e(data.get('quote', 'This product changed how we work. Incredible results.'))}"

+

{_e(data.get('author', 'Jane Doe'))}

+

{_e(data.get('role', 'CEO, Example Company'))}

''' @@ -590,14 +607,14 @@ def generate_cta_slide(data): """Closing CTA slide""" return f'''
-

{data.get('headline', 'Ready to get started?')}

-

{data.get('subheadline', 'Join thousands of teams already using our solution.')}

+

{_e(data.get('headline', 'Ready to get started?'))}

+

{_e(data.get('subheadline', 'Join thousands of teams already using our solution.'))}

''' @@ -632,7 +649,7 @@ def generate_deck(slides_data, title="Pitch Deck"): tokens_rel_path = "../../../assets/design-tokens.css" return SLIDE_TEMPLATE.format( - title=title, + title=escape(str(title)), tokens_css_path=tokens_rel_path, slides_content=slides_html )