mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-17 06:35:11 +00:00
Add publish-to-pages agent skill (#1035)
* Add publish-to-pages agent skill Agent skill that publishes presentations and web content to GitHub Pages. Works with any AI coding agent (Copilot CLI, Claude Code, Gemini CLI, etc.) Features: - Converts PPTX and PDF with full formatting preservation - Creates repo, enables Pages, returns live URL - Zero config — just needs gh CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update README.skills.md --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e2c763df88
commit
4ad31b665f
@@ -196,6 +196,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
|
||||
| [prd](../skills/prd/SKILL.md) | Generate high-quality Product Requirements Documents (PRDs) for software systems and AI-powered features. Includes executive summaries, user stories, technical specifications, and risk analysis. | None |
|
||||
| [project-workflow-analysis-blueprint-generator](../skills/project-workflow-analysis-blueprint-generator/SKILL.md) | Comprehensive technology-agnostic prompt generator for documenting end-to-end application workflows. Automatically detects project architecture patterns, technology stacks, and data flow patterns to generate detailed implementation blueprints covering entry points, service layers, data access, error handling, and testing approaches across multiple technologies including .NET, Java/Spring, React, and microservices architectures. | None |
|
||||
| [prompt-builder](../skills/prompt-builder/SKILL.md) | Guide users through creating high-quality GitHub Copilot prompts with proper structure, tools, and best practices. | None |
|
||||
| [publish-to-pages](../skills/publish-to-pages/SKILL.md) | Publish presentations and web content to GitHub Pages. Converts PPTX, PDF, HTML, or Google Slides to a live GitHub Pages URL. Handles repo creation, file conversion, Pages enablement, and returns the live URL. Use when the user wants to publish, deploy, or share a presentation or HTML file via GitHub Pages. | `scripts/convert-pdf.py`<br />`scripts/convert-pptx.py`<br />`scripts/publish.sh` |
|
||||
| [pytest-coverage](../skills/pytest-coverage/SKILL.md) | Run pytest tests with coverage, discover lines missing coverage, and increase coverage to 100%. | None |
|
||||
| [python-mcp-server-generator](../skills/python-mcp-server-generator/SKILL.md) | Generate a complete MCP server project in Python with tools, resources, and proper configuration | None |
|
||||
| [quasi-coder](../skills/quasi-coder/SKILL.md) | Expert 10x engineer skill for interpreting and implementing code from shorthand, quasi-code, and natural language descriptions. Use when collaborators provide incomplete code snippets, pseudo-code, or descriptions with potential typos or incorrect terminology. Excels at translating non-technical or semi-technical descriptions into production-quality code. | None |
|
||||
|
||||
90
skills/publish-to-pages/SKILL.md
Normal file
90
skills/publish-to-pages/SKILL.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: publish-to-pages
|
||||
description: 'Publish presentations and web content to GitHub Pages. Converts PPTX, PDF, HTML, or Google Slides to a live GitHub Pages URL. Handles repo creation, file conversion, Pages enablement, and returns the live URL. Use when the user wants to publish, deploy, or share a presentation or HTML file via GitHub Pages.'
|
||||
---
|
||||
|
||||
# publish-to-pages
|
||||
|
||||
Publish any presentation or web content to GitHub Pages in one shot.
|
||||
|
||||
## 1. Prerequisites Check
|
||||
|
||||
Run these silently. Only surface errors:
|
||||
|
||||
```bash
|
||||
command -v gh >/dev/null || echo "MISSING: gh CLI — install from https://cli.github.com"
|
||||
gh auth status &>/dev/null || echo "MISSING: gh not authenticated — run 'gh auth login'"
|
||||
command -v python3 >/dev/null || echo "MISSING: python3 (needed for PPTX conversion)"
|
||||
```
|
||||
|
||||
`poppler-utils` is optional (PDF conversion via `pdftoppm`). Don't block on it.
|
||||
|
||||
## 2. Input Detection
|
||||
|
||||
Determine input type from what the user provides:
|
||||
|
||||
| Input | Detection |
|
||||
|-------|-----------|
|
||||
| HTML file | Extension `.html` or `.htm` |
|
||||
| PPTX file | Extension `.pptx` |
|
||||
| PDF file | Extension `.pdf` |
|
||||
| Google Slides URL | URL contains `docs.google.com/presentation` |
|
||||
|
||||
Ask the user for a **repo name** if not provided. Default: filename without extension.
|
||||
|
||||
## 3. Conversion
|
||||
|
||||
### HTML
|
||||
No conversion needed. Use the file directly as `index.html`.
|
||||
|
||||
### PPTX
|
||||
Run the conversion script:
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/convert-pptx.py INPUT_FILE /tmp/output.html
|
||||
```
|
||||
If `python-pptx` is missing, tell the user: `pip install python-pptx`
|
||||
|
||||
### PDF
|
||||
Convert with the included script (requires `poppler-utils` for `pdftoppm`):
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/convert-pdf.py INPUT_FILE /tmp/output.html
|
||||
```
|
||||
Each page is rendered as a PNG and base64-embedded into a self-contained HTML with slide navigation.
|
||||
If `pdftoppm` is missing, tell the user: `apt install poppler-utils` (or `brew install poppler` on macOS).
|
||||
|
||||
### Google Slides
|
||||
1. Extract the presentation ID from the URL (the long string between `/d/` and `/`)
|
||||
2. Download as PPTX:
|
||||
```bash
|
||||
curl -L "https://docs.google.com/presentation/d/PRESENTATION_ID/export/pptx" -o /tmp/slides.pptx
|
||||
```
|
||||
3. Then convert the PPTX using the convert script above.
|
||||
|
||||
## 4. Publishing
|
||||
|
||||
### Visibility
|
||||
Repos are created **public** by default. If the user specifies `private` (or wants a private repo), use `--private` — but note that GitHub Pages on private repos requires a Pro, Team, or Enterprise plan.
|
||||
|
||||
### Publish
|
||||
```bash
|
||||
bash SKILL_DIR/scripts/publish.sh /path/to/index.html REPO_NAME public "Description"
|
||||
```
|
||||
|
||||
Pass `private` instead of `public` if the user requests it.
|
||||
|
||||
The script creates the repo, pushes `index.html`, and enables GitHub Pages.
|
||||
|
||||
## 5. Output
|
||||
|
||||
Tell the user:
|
||||
- **Repository:** `https://github.com/USERNAME/REPO_NAME`
|
||||
- **Live URL:** `https://USERNAME.github.io/REPO_NAME/`
|
||||
- **Note:** Pages takes 1-2 minutes to go live.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Repo already exists:** Suggest appending a number (`my-slides-2`) or a date (`my-slides-2026`).
|
||||
- **Pages enablement fails:** Still return the repo URL. User can enable Pages manually in repo Settings.
|
||||
- **PPTX conversion fails:** Tell user to run `pip install python-pptx`.
|
||||
- **PDF conversion fails:** Suggest installing `poppler-utils` (`apt install poppler-utils` or `brew install poppler`).
|
||||
- **Google Slides download fails:** The presentation may not be publicly accessible. Ask user to make it viewable or download the PPTX manually.
|
||||
121
skills/publish-to-pages/scripts/convert-pdf.py
Executable file
121
skills/publish-to-pages/scripts/convert-pdf.py
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a PDF to a self-contained HTML presentation.
|
||||
|
||||
Each page is rendered as a PNG image (via pdftoppm) and base64-embedded
|
||||
into a single HTML file with slide navigation (arrows, swipe, click).
|
||||
|
||||
Requirements: poppler-utils (pdftoppm)
|
||||
Usage: python3 convert-pdf.py input.pdf [output.html]
|
||||
"""
|
||||
|
||||
import base64
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def convert(pdf_path: str, output_path: str | None = None, dpi: int = 150):
|
||||
pdf_path = str(Path(pdf_path).resolve())
|
||||
if not Path(pdf_path).exists():
|
||||
print(f"Error: {pdf_path} not found")
|
||||
sys.exit(1)
|
||||
|
||||
# Check for pdftoppm
|
||||
if subprocess.run(["which", "pdftoppm"], capture_output=True).returncode != 0:
|
||||
print("Error: pdftoppm not found. Install poppler-utils:")
|
||||
print(" apt install poppler-utils # Debian/Ubuntu")
|
||||
print(" brew install poppler # macOS")
|
||||
sys.exit(1)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
prefix = os.path.join(tmpdir, "page")
|
||||
result = subprocess.run(
|
||||
["pdftoppm", "-png", "-r", str(dpi), pdf_path, prefix],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"Error converting PDF: {result.stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
pages = sorted(glob.glob(f"{prefix}-*.png"))
|
||||
if not pages:
|
||||
print("Error: No pages rendered from PDF")
|
||||
sys.exit(1)
|
||||
|
||||
slides_html = []
|
||||
for i, page_path in enumerate(pages, 1):
|
||||
with open(page_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
slides_html.append(
|
||||
f'<section class="slide">'
|
||||
f'<div class="slide-inner">'
|
||||
f'<img src="data:image/png;base64,{b64}" alt="Page {i}">'
|
||||
f'</div></section>'
|
||||
)
|
||||
|
||||
# Try to extract title from filename
|
||||
title = Path(pdf_path).stem.replace("-", " ").replace("_", " ")
|
||||
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
html, body {{ height: 100%; overflow: hidden; background: #000; }}
|
||||
.slide {{ width: 100vw; height: 100vh; display: none; align-items: center; justify-content: center; }}
|
||||
.slide.active {{ display: flex; }}
|
||||
.slide-inner {{ display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }}
|
||||
.slide-inner img {{ max-width: 100%; max-height: 100%; object-fit: contain; }}
|
||||
.progress {{ position: fixed; bottom: 0; left: 0; height: 4px; background: #0366d6; transition: width 0.3s; z-index: 100; }}
|
||||
.counter {{ position: fixed; bottom: 12px; right: 20px; font-size: 14px; color: rgba(255,255,255,0.4); z-index: 100; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{chr(10).join(slides_html)}
|
||||
<div class="progress" id="progress"></div>
|
||||
<div class="counter" id="counter"></div>
|
||||
<script>
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
let current = 0;
|
||||
function show(n) {{
|
||||
slides.forEach(s => s.classList.remove('active'));
|
||||
current = Math.max(0, Math.min(n, slides.length - 1));
|
||||
slides[current].classList.add('active');
|
||||
document.getElementById('progress').style.width = ((current + 1) / slides.length * 100) + '%';
|
||||
document.getElementById('counter').textContent = (current + 1) + ' / ' + slides.length;
|
||||
}}
|
||||
document.addEventListener('keydown', e => {{
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') {{ e.preventDefault(); show(current + 1); }}
|
||||
if (e.key === 'ArrowLeft') {{ e.preventDefault(); show(current - 1); }}
|
||||
}});
|
||||
let touchStartX = 0;
|
||||
document.addEventListener('touchstart', e => {{ touchStartX = e.changedTouches[0].screenX; }});
|
||||
document.addEventListener('touchend', e => {{
|
||||
const diff = e.changedTouches[0].screenX - touchStartX;
|
||||
if (Math.abs(diff) > 50) {{ diff > 0 ? show(current - 1) : show(current + 1); }}
|
||||
}});
|
||||
document.addEventListener('click', e => {{
|
||||
if (e.clientX > window.innerWidth / 2) show(current + 1);
|
||||
else show(current - 1);
|
||||
}});
|
||||
show(0);
|
||||
</script>
|
||||
</body></html>'''
|
||||
|
||||
output = output_path or str(Path(pdf_path).with_suffix('.html'))
|
||||
Path(output).write_text(html, encoding='utf-8')
|
||||
print(f"Converted to: {output}")
|
||||
print(f"Pages: {len(slides_html)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: convert-pdf.py <file.pdf> [output.html]")
|
||||
sys.exit(1)
|
||||
convert(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
|
||||
306
skills/publish-to-pages/scripts/convert-pptx.py
Executable file
306
skills/publish-to-pages/scripts/convert-pptx.py
Executable file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a PPTX file to a self-contained HTML presentation with formatting preserved."""
|
||||
import sys
|
||||
import base64
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
from pptx.dml.color import RGBColor
|
||||
except ImportError:
|
||||
print("ERROR: python-pptx not installed. Install with: pip install python-pptx")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def rgb_to_hex(rgb_color):
|
||||
"""Convert RGBColor to hex string."""
|
||||
if rgb_color is None:
|
||||
return None
|
||||
try:
|
||||
return f"#{rgb_color}"
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_text_style(run):
|
||||
"""Extract inline text styling from a run."""
|
||||
styles = []
|
||||
try:
|
||||
if run.font.bold:
|
||||
styles.append("font-weight:bold")
|
||||
if run.font.italic:
|
||||
styles.append("font-style:italic")
|
||||
if run.font.underline:
|
||||
styles.append("text-decoration:underline")
|
||||
if run.font.size:
|
||||
styles.append(f"font-size:{run.font.size.pt}pt")
|
||||
if run.font.color and run.font.color.rgb:
|
||||
styles.append(f"color:{rgb_to_hex(run.font.color.rgb)}")
|
||||
if run.font.name:
|
||||
styles.append(f"font-family:'{run.font.name}',sans-serif")
|
||||
except:
|
||||
pass
|
||||
return ";".join(styles)
|
||||
|
||||
|
||||
def get_alignment(paragraph):
|
||||
"""Get CSS text-align from paragraph alignment."""
|
||||
try:
|
||||
align = paragraph.alignment
|
||||
if align == PP_ALIGN.CENTER:
|
||||
return "center"
|
||||
elif align == PP_ALIGN.RIGHT:
|
||||
return "right"
|
||||
elif align == PP_ALIGN.JUSTIFY:
|
||||
return "justify"
|
||||
except:
|
||||
pass
|
||||
return "left"
|
||||
|
||||
|
||||
def extract_image(shape):
|
||||
"""Extract image from shape as base64 data URI."""
|
||||
try:
|
||||
image = shape.image
|
||||
content_type = image.content_type
|
||||
image_bytes = image.blob
|
||||
b64 = base64.b64encode(image_bytes).decode('utf-8')
|
||||
return f"data:{content_type};base64,{b64}"
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_shape_position(shape, slide_width, slide_height):
|
||||
"""Get shape position as percentages."""
|
||||
try:
|
||||
left = (shape.left / slide_width) * 100 if shape.left else 0
|
||||
top = (shape.top / slide_height) * 100 if shape.top else 0
|
||||
width = (shape.width / slide_width) * 100 if shape.width else 50
|
||||
height = (shape.height / slide_height) * 100 if shape.height else 30
|
||||
return left, top, width, height
|
||||
except:
|
||||
return 5, 5, 90, 40
|
||||
|
||||
|
||||
def get_slide_background(slide, prs):
|
||||
"""Extract slide background color from XML."""
|
||||
from pptx.oxml.ns import qn
|
||||
for source in [slide, slide.slide_layout]:
|
||||
try:
|
||||
bg_el = source.background._element
|
||||
# Look for solidFill > srgbClr inside bgPr
|
||||
for sf in bg_el.iter(qn('a:solidFill')):
|
||||
clr = sf.find(qn('a:srgbClr'))
|
||||
if clr is not None and clr.get('val'):
|
||||
return f"background-color:#{clr.get('val')}"
|
||||
except:
|
||||
pass
|
||||
return "background-color:#ffffff"
|
||||
|
||||
|
||||
def get_shape_fill(shape):
|
||||
"""Extract shape fill color from XML."""
|
||||
from pptx.oxml.ns import qn
|
||||
try:
|
||||
sp_pr = shape._element.find(qn('p:spPr'))
|
||||
if sp_pr is None:
|
||||
sp_pr = shape._element.find(qn('a:spPr'))
|
||||
if sp_pr is None:
|
||||
# Try direct child
|
||||
for tag in ['{http://schemas.openxmlformats.org/drawingml/2006/main}spPr',
|
||||
'{http://schemas.openxmlformats.org/presentationml/2006/main}spPr']:
|
||||
sp_pr = shape._element.find(tag)
|
||||
if sp_pr is not None:
|
||||
break
|
||||
if sp_pr is not None:
|
||||
sf = sp_pr.find(qn('a:solidFill'))
|
||||
if sf is not None:
|
||||
clr = sf.find(qn('a:srgbClr'))
|
||||
if clr is not None and clr.get('val'):
|
||||
return f"#{clr.get('val')}"
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def render_paragraph(paragraph):
|
||||
"""Render a paragraph with inline formatting."""
|
||||
align = get_alignment(paragraph)
|
||||
parts = []
|
||||
for run in paragraph.runs:
|
||||
text = run.text
|
||||
if not text:
|
||||
continue
|
||||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
style = get_text_style(run)
|
||||
if style:
|
||||
parts.append(f'<span style="{style}">{text}</span>')
|
||||
else:
|
||||
parts.append(text)
|
||||
if not parts:
|
||||
return ""
|
||||
content = "".join(parts)
|
||||
return f'<p style="text-align:{align};margin:0.3em 0;line-height:1.4">{content}</p>'
|
||||
|
||||
|
||||
def convert(pptx_path, output_path=None):
|
||||
prs = Presentation(pptx_path)
|
||||
slide_width = prs.slide_width
|
||||
slide_height = prs.slide_height
|
||||
aspect_ratio = slide_width / slide_height if slide_height else 16/9
|
||||
|
||||
slides_html = []
|
||||
|
||||
for i, slide in enumerate(prs.slides, 1):
|
||||
bg_style = get_slide_background(slide, prs)
|
||||
elements = []
|
||||
|
||||
for shape in sorted(slide.shapes, key=lambda s: (s.top or 0, s.left or 0)):
|
||||
left, top, width, height = get_shape_position(shape, slide_width, slide_height)
|
||||
pos_style = f"position:absolute;left:{left:.1f}%;top:{top:.1f}%;width:{width:.1f}%;height:{height:.1f}%"
|
||||
|
||||
# Image
|
||||
if shape.shape_type == 13 or hasattr(shape, "image"):
|
||||
data_uri = extract_image(shape)
|
||||
if data_uri:
|
||||
elements.append(
|
||||
f'<div style="{pos_style};display:flex;align-items:center;justify-content:center">'
|
||||
f'<img src="{data_uri}" style="max-width:100%;max-height:100%;object-fit:contain" alt="">'
|
||||
f'</div>'
|
||||
)
|
||||
continue
|
||||
|
||||
# Table
|
||||
if shape.has_table:
|
||||
table = shape.table
|
||||
table_html = '<table style="width:100%;border-collapse:collapse;font-size:0.9em">'
|
||||
for row in table.rows:
|
||||
table_html += "<tr>"
|
||||
for cell in row.cells:
|
||||
cell_text = cell.text.replace("&", "&").replace("<", "<")
|
||||
table_html += f'<td style="border:1px solid #ccc;padding:6px 10px">{cell_text}</td>'
|
||||
table_html += "</tr>"
|
||||
table_html += "</table>"
|
||||
elements.append(f'<div style="{pos_style};overflow:auto">{table_html}</div>')
|
||||
continue
|
||||
|
||||
# Text
|
||||
if shape.has_text_frame:
|
||||
text_parts = []
|
||||
for para in shape.text_frame.paragraphs:
|
||||
rendered = render_paragraph(para)
|
||||
if rendered:
|
||||
text_parts.append(rendered)
|
||||
if text_parts:
|
||||
content = "".join(text_parts)
|
||||
fill = get_shape_fill(shape)
|
||||
fill_style = f"background-color:{fill};padding:1em;border-radius:8px;" if fill else ""
|
||||
elements.append(
|
||||
f'<div style="{pos_style};{fill_style}overflow:hidden;display:flex;flex-direction:column;justify-content:center">'
|
||||
f'{content}</div>'
|
||||
)
|
||||
continue
|
||||
|
||||
# Decorative shape with fill (colored rectangles, bars, etc.)
|
||||
fill = get_shape_fill(shape)
|
||||
if fill:
|
||||
elements.append(
|
||||
f'<div style="{pos_style};background-color:{fill};border-radius:4px"></div>'
|
||||
)
|
||||
|
||||
slide_content = "\n".join(elements)
|
||||
slides_html.append(
|
||||
f'<section class="slide" style="{bg_style}">\n<div class="slide-inner">\n{slide_content}\n</div>\n</section>'
|
||||
)
|
||||
|
||||
title = "Presentation"
|
||||
# Try to get title from first slide
|
||||
if prs.slides:
|
||||
for shape in prs.slides[0].shapes:
|
||||
if hasattr(shape, "text") and shape.text.strip() and len(shape.text.strip()) < 150:
|
||||
title = shape.text.strip()
|
||||
break
|
||||
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
html, body {{ height: 100%; overflow: hidden; background: #000; }}
|
||||
.slide {{
|
||||
width: 100vw; height: 100vh;
|
||||
display: none;
|
||||
align-items: center; justify-content: center;
|
||||
overflow: hidden;
|
||||
}}
|
||||
.slide.active {{ display: flex; }}
|
||||
.slide-inner {{
|
||||
position: relative;
|
||||
width: 1280px; height: {int(1280 / aspect_ratio)}px;
|
||||
transform-origin: center center;
|
||||
flex-shrink: 0;
|
||||
}}
|
||||
.progress {{ position: fixed; bottom: 0; left: 0; height: 4px; background: #0366d6; transition: width 0.3s; z-index: 100; }}
|
||||
.counter {{ position: fixed; bottom: 12px; right: 20px; font-size: 14px; color: rgba(255,255,255,0.4); z-index: 100; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{chr(10).join(slides_html)}
|
||||
<div class="progress" id="progress"></div>
|
||||
<div class="counter" id="counter"></div>
|
||||
<script>
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
let current = 0;
|
||||
function show(n) {{
|
||||
slides.forEach(s => s.classList.remove('active'));
|
||||
current = Math.max(0, Math.min(n, slides.length - 1));
|
||||
slides[current].classList.add('active');
|
||||
document.getElementById('progress').style.width = ((current + 1) / slides.length * 100) + '%';
|
||||
document.getElementById('counter').textContent = (current + 1) + ' / ' + slides.length;
|
||||
}}
|
||||
document.addEventListener('keydown', e => {{
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') {{ e.preventDefault(); show(current + 1); }}
|
||||
if (e.key === 'ArrowLeft') {{ e.preventDefault(); show(current - 1); }}
|
||||
}});
|
||||
let touchStartX = 0;
|
||||
document.addEventListener('touchstart', e => {{ touchStartX = e.changedTouches[0].screenX; }});
|
||||
document.addEventListener('touchend', e => {{
|
||||
const diff = e.changedTouches[0].screenX - touchStartX;
|
||||
if (Math.abs(diff) > 50) {{ diff > 0 ? show(current - 1) : show(current + 1); }}
|
||||
}});
|
||||
document.addEventListener('click', e => {{
|
||||
if (e.clientX > window.innerWidth / 2) show(current + 1);
|
||||
else show(current - 1);
|
||||
}});
|
||||
show(0);
|
||||
function scaleSlides() {{
|
||||
document.querySelectorAll('.slide-inner').forEach(inner => {{
|
||||
const scaleX = window.innerWidth / inner.offsetWidth;
|
||||
const scaleY = window.innerHeight / inner.offsetHeight;
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
inner.style.transform = 'scale(' + scale + ')';
|
||||
}});
|
||||
}}
|
||||
window.addEventListener('resize', scaleSlides);
|
||||
scaleSlides();
|
||||
</script>
|
||||
</body></html>'''
|
||||
|
||||
output = output_path or str(Path(pptx_path).with_suffix('.html'))
|
||||
Path(output).write_text(html, encoding='utf-8')
|
||||
print(f"Converted to: {output}")
|
||||
print(f"Slides: {len(slides_html)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: convert-pptx.py <file.pptx> [output.html]")
|
||||
sys.exit(1)
|
||||
convert(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
|
||||
40
skills/publish-to-pages/scripts/publish.sh
Executable file
40
skills/publish-to-pages/scripts/publish.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Main publish script
|
||||
# Args: $1 = path to index.html, $2 = repo name, $3 = visibility (private|public), $4 = description
|
||||
set -euo pipefail
|
||||
|
||||
HTML_FILE="$1"
|
||||
REPO_NAME="$2"
|
||||
VISIBILITY="${3:-public}"
|
||||
DESCRIPTION="${4:-Published via publish-to-pages}"
|
||||
|
||||
USERNAME=$(gh api user --jq '.login')
|
||||
|
||||
# Check if repo exists
|
||||
if gh repo view "$USERNAME/$REPO_NAME" &>/dev/null; then
|
||||
echo "ERROR: Repository $USERNAME/$REPO_NAME already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create repo
|
||||
gh repo create "$REPO_NAME" --"$VISIBILITY" --description "$DESCRIPTION"
|
||||
|
||||
# Clone, push, enable pages
|
||||
TMPDIR=$(mktemp -d)
|
||||
git clone "https://github.com/$USERNAME/$REPO_NAME.git" "$TMPDIR"
|
||||
cp "$HTML_FILE" "$TMPDIR/index.html"
|
||||
cd "$TMPDIR"
|
||||
git add index.html
|
||||
git commit -m "Publish content"
|
||||
git push origin main
|
||||
|
||||
# Enable GitHub Pages
|
||||
gh api "repos/$USERNAME/$REPO_NAME/pages" -X POST -f source[branch]=main -f source[path]=/ 2>/dev/null || true
|
||||
|
||||
echo "REPO_URL=https://github.com/$USERNAME/$REPO_NAME"
|
||||
echo "PAGES_URL=https://$USERNAME.github.io/$REPO_NAME/"
|
||||
echo ""
|
||||
echo "GitHub Pages may take 1-2 minutes to deploy."
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$TMPDIR"
|
||||
Reference in New Issue
Block a user