Installation
Install with CLI
Recommended
gh skills-hub install publish-to-pages Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/publish-to-pages/ Extract the ZIP to .github/skills/ in your repo. The folder name must match publish-to-pages for Copilot to auto-discover it.
Skill Files (4)
SKILL.md 4.4 KB
---
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
### Large File Handling
Both conversion scripts automatically detect large files and switch to **external assets mode**:
- **PPTX:** Files >20MB or with >50 images โ images saved as separate files in `assets/`
- **PDF:** Files >20MB or with >50 pages โ page PNGs saved in `assets/`
- Files >150MB print a warning (PPTX suggests PDF path instead)
This keeps individual files well under GitHub's 100MB limit. Small files still produce a single self-contained HTML.
You can force the behavior with `--external-assets` or `--no-external-assets`.
### 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
# For large files, force external assets:
python3 SKILL_DIR/scripts/convert-pptx.py INPUT_FILE /tmp/output.html --external-assets
```
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
# For large files, force external assets:
python3 SKILL_DIR/scripts/convert-pdf.py INPUT_FILE /tmp/output.html --external-assets
```
Each page is rendered as a PNG and embedded into 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` (plus `assets/` if present), and enables GitHub Pages.
**Note:** When external assets mode is used, the output HTML references files in `assets/`. The publish script automatically detects and copies the `assets/` directory alongside the HTML file. Make sure the HTML file and its `assets/` directory are in the same parent directory.
## 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.
scripts/
convert-pdf.py 6.6 KB
#!/usr/bin/env python3
"""Convert a PDF to an HTML presentation.
Each page is rendered as a PNG image (via pdftoppm). Supports external assets
mode for large files to avoid huge single-file HTML.
Requirements: poppler-utils (pdftoppm)
"""
import argparse
import base64
import glob
import os
import subprocess
import sys
import tempfile
from pathlib import Path
def get_page_count(pdf_path):
"""Get page count using pdfinfo if available."""
try:
result = subprocess.run(["pdfinfo", pdf_path], capture_output=True, text=True)
for line in result.stdout.splitlines():
if line.startswith("Pages:"):
return int(line.split(":")[1].strip())
except:
pass
return None
def convert(pdf_path: str, output_path: str | None = None, dpi: int = 150, external_assets=None):
pdf_path = str(Path(pdf_path).resolve())
if not Path(pdf_path).exists():
print(f"Error: {pdf_path} not found")
sys.exit(1)
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)
file_size_mb = os.path.getsize(pdf_path) / (1024 * 1024)
if file_size_mb > 150:
print(f"WARNING: PDF is {file_size_mb:.0f}MB โ conversion may be slow and memory-intensive.")
page_count = get_page_count(pdf_path)
# Auto-detect external assets mode
if external_assets is None:
external_assets = file_size_mb > 20 or (page_count is not None and page_count > 50)
if external_assets:
print(f"Auto-enabling external assets mode (file: {file_size_mb:.1f}MB, pages: {page_count or 'unknown'})")
output = output_path or str(Path(pdf_path).with_suffix('.html'))
output_dir = Path(output).parent
if external_assets:
assets_dir = output_dir / "assets"
assets_dir.mkdir(parents=True, exist_ok=True)
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:
page_bytes = f.read()
if external_assets:
img_name = f"img-{i:03d}.png"
(assets_dir / img_name).write_bytes(page_bytes)
src = f"assets/{img_name}"
else:
b64 = base64.b64encode(page_bytes).decode()
src = f"data:image/png;base64,{b64}"
slides_html.append(
f'<section class="slide">'
f'<div class="slide-inner">'
f'<img src="{src}" alt="Page {i}">'
f'</div></section>'
)
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>'''
Path(output).write_text(html, encoding='utf-8')
output_size = os.path.getsize(output)
print(f"Converted to: {output}")
print(f"Pages: {len(slides_html)}")
print(f"Output size: {output_size / (1024*1024):.1f}MB")
print(f"External assets: {'yes' if external_assets else 'no'}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert PDF to HTML presentation")
parser.add_argument("input", help="Path to .pdf file")
parser.add_argument("output", nargs="?", help="Output HTML path (default: same name with .html)")
parser.add_argument("--external-assets", action="store_true", default=None,
help="Save page images as separate files in assets/ directory (auto-detected for large files)")
parser.add_argument("--no-external-assets", action="store_true",
help="Force inline base64 even for large files")
parser.add_argument("--dpi", type=int, default=150, help="Render DPI (default: 150)")
args = parser.parse_args()
ext_assets = None
if args.external_assets:
ext_assets = True
elif args.no_external_assets:
ext_assets = False
convert(args.input, args.output, dpi=args.dpi, external_assets=ext_assets)
convert-pptx.py 13.1 KB
#!/usr/bin/env python3
"""Convert a PPTX file to an HTML presentation with formatting preserved.
Supports external assets mode for large files to avoid huge single-file HTML.
"""
import argparse
import base64
import io
import os
import re
import sys
from pathlib import Path
def _ensure_pptx():
try:
from pptx import Presentation
from pptx.enum.text import PP_ALIGN
return True
except ImportError:
print("ERROR: python-pptx not installed. Install with: pip install python-pptx")
sys.exit(1)
def rgb_to_hex(rgb_color):
if rgb_color is None:
return None
try:
return f"#{rgb_color}"
except:
return None
def get_text_style(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):
from pptx.enum.text import PP_ALIGN
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 get_shape_position(shape, slide_width, slide_height):
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):
from pptx.oxml.ns import qn
for source in [slide, slide.slide_layout]:
try:
bg_el = source.background._element
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):
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:
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):
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 extract_image_data(shape):
"""Extract raw image bytes and content type from a shape."""
try:
image = shape.image
return image.blob, image.content_type
except:
return None, None
def count_images(prs):
"""Count total images across all slides."""
count = 0
for slide in prs.slides:
for shape in slide.shapes:
if shape.shape_type == 13 or hasattr(shape, "image"):
try:
_ = shape.image
count += 1
except:
pass
return count
CONTENT_TYPE_TO_EXT = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/jpg': '.jpg',
'image/gif': '.gif',
'image/bmp': '.bmp',
'image/tiff': '.tiff',
'image/svg+xml': '.svg',
'image/webp': '.webp',
}
def convert(pptx_path, output_path=None, external_assets=None):
_ensure_pptx()
from pptx import Presentation
file_size_mb = os.path.getsize(pptx_path) / (1024 * 1024)
# Pre-flight warning for very large files
if file_size_mb > 150:
print(f"WARNING: File is {file_size_mb:.0f}MB โ consider using PDF conversion (convert-pdf.py) for better performance.")
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
total_images = count_images(prs)
# Auto-detect external assets mode
if external_assets is None:
external_assets = file_size_mb > 20 or total_images > 50
if external_assets:
print(f"Auto-enabling external assets mode (file: {file_size_mb:.1f}MB, images: {total_images})")
output = output_path or str(Path(pptx_path).with_suffix('.html'))
output_dir = Path(output).parent
if external_assets:
assets_dir = output_dir / "assets"
assets_dir.mkdir(parents=True, exist_ok=True)
img_counter = 0
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"):
blob, content_type = extract_image_data(shape)
if blob:
img_counter += 1
if external_assets:
ext = CONTENT_TYPE_TO_EXT.get(content_type, '.png')
img_name = f"img-{img_counter:03d}{ext}"
(assets_dir / img_name).write_bytes(blob)
src = f"assets/{img_name}"
else:
b64 = base64.b64encode(blob).decode('utf-8')
src = f"data:{content_type};base64,{b64}"
elements.append(
f'<div style="{pos_style};display:flex;align-items:center;justify-content:center">'
f'<img src="{src}" 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
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"
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>'''
Path(output).write_text(html, encoding='utf-8')
output_size = os.path.getsize(output)
# Summary
print(f"Converted to: {output}")
print(f"Slides: {len(slides_html)}")
print(f"Images: {img_counter}")
print(f"Output size: {output_size / (1024*1024):.1f}MB")
print(f"External assets: {'yes' if external_assets else 'no'}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert PPTX to HTML presentation")
parser.add_argument("input", help="Path to .pptx file")
parser.add_argument("output", nargs="?", help="Output HTML path (default: same name with .html)")
parser.add_argument("--external-assets", action="store_true", default=None,
help="Save images as separate files in assets/ directory (auto-detected for large files)")
parser.add_argument("--no-external-assets", action="store_true",
help="Force inline base64 even for large files")
args = parser.parse_args()
ext_assets = None # auto-detect
if args.external_assets:
ext_assets = True
elif args.no_external_assets:
ext_assets = False
convert(args.input, args.output, external_assets=ext_assets)
publish.sh 1.4 KB
#!/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"
HTML_DIR=$(dirname "$HTML_FILE")
# Copy HTML file as index.html
cp "$HTML_FILE" "$TMPDIR/index.html"
# Copy assets directory if it exists alongside the HTML file
if [ -d "$HTML_DIR/assets" ]; then
cp -r "$HTML_DIR/assets" "$TMPDIR/assets"
echo "Copied assets/ directory ($(find "$HTML_DIR/assets" -type f | wc -l) files)"
fi
cd "$TMPDIR"
git add -A
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"
License (MIT)
View full license text
MIT License Copyright GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.