83 lines
2.8 KiB
Text
83 lines
2.8 KiB
Text
|
|
#!/usr/bin/env -S uv run --script
|
||
|
|
# /// script
|
||
|
|
# requires-python = ">=3.12"
|
||
|
|
# dependencies = ["rich>=13.0.0", "typer>=0.24.0"]
|
||
|
|
# ///
|
||
|
|
#MISE description="Serve a docs release tarball locally in a browser"
|
||
|
|
#USAGE arg "<tarball>" help="Path to docs tarball (e.g. ~/Downloads/docs-v0.0.2.tar.gz)"
|
||
|
|
#USAGE flag "--port <port>" default="8484" help="Port for preview server (default 8484)"
|
||
|
|
"""Extract a docs release tarball and serve it locally.
|
||
|
|
|
||
|
|
Downloads the docs tarball from a Forgejo release page manually, then
|
||
|
|
point this task at it to preview the site in your browser.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
mise run docs-preview ~/Downloads/docs-v0.0.2.tar.gz
|
||
|
|
mise run docs-preview ~/Downloads/docs-v0.0.2.tar.gz --port 9090
|
||
|
|
"""
|
||
|
|
|
||
|
|
import http.server
|
||
|
|
import tarfile
|
||
|
|
import tempfile
|
||
|
|
import threading
|
||
|
|
import webbrowser
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Annotated
|
||
|
|
|
||
|
|
import typer
|
||
|
|
from rich.console import Console
|
||
|
|
|
||
|
|
console = Console()
|
||
|
|
|
||
|
|
|
||
|
|
def main(
|
||
|
|
tarball: Annotated[Path, typer.Argument(help="Path to docs tarball")],
|
||
|
|
port: Annotated[int, typer.Option(help="Port for preview server")] = 8484,
|
||
|
|
) -> None:
|
||
|
|
tarball = tarball.expanduser().resolve()
|
||
|
|
if not tarball.exists():
|
||
|
|
console.print(f"[bold red]File not found:[/bold red] {tarball}")
|
||
|
|
raise typer.Exit(code=1)
|
||
|
|
|
||
|
|
docroot = Path(tempfile.mkdtemp(prefix="docs-preview-"))
|
||
|
|
console.print(f"[dim]Extracting {tarball.name} to {docroot}...[/dim]")
|
||
|
|
with tarfile.open(tarball, "r:gz") as tf:
|
||
|
|
tf.extractall(docroot, filter="data")
|
||
|
|
|
||
|
|
url = f"http://localhost:{port}"
|
||
|
|
console.print(f"\n[bold green]Serving docs at {url}[/bold green]")
|
||
|
|
console.print(f"[yellow]Press Ctrl+C to stop.[/yellow]\n")
|
||
|
|
|
||
|
|
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
|
||
|
|
|
||
|
|
class QuartzHandler(http.server.SimpleHTTPRequestHandler):
|
||
|
|
"""Handler that resolves clean URLs to index.html files."""
|
||
|
|
|
||
|
|
def __init__(self, *args, **kwargs):
|
||
|
|
super().__init__(*args, directory=str(docroot), **kwargs)
|
||
|
|
|
||
|
|
def do_GET(self):
|
||
|
|
# Quartz outputs foo.html, not foo/index.html, so clean URLs
|
||
|
|
# like /tutorials/tutorials need to resolve to tutorials.html
|
||
|
|
path = self.path.split("?")[0].split("#")[0]
|
||
|
|
file = docroot / path.lstrip("/")
|
||
|
|
if not file.suffix and not file.is_file():
|
||
|
|
html_candidate = file.with_suffix(".html")
|
||
|
|
if html_candidate.is_file():
|
||
|
|
self.path = path + ".html"
|
||
|
|
elif (file / "index.html").is_file():
|
||
|
|
self.path = path.rstrip("/") + "/index.html"
|
||
|
|
super().do_GET()
|
||
|
|
|
||
|
|
handler = QuartzHandler
|
||
|
|
server = http.server.HTTPServer(("localhost", port), handler)
|
||
|
|
try:
|
||
|
|
server.serve_forever()
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
pass
|
||
|
|
finally:
|
||
|
|
console.print("\n[dim]Stopped.[/dim]")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
typer.run(main)
|