mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-18 07:05:12 +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
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