mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 07:24:31 +00:00
v0.4 Phase 6: fetch-deps command for offline/local dependency mode
- Add _http_get() general HTTP helper (SSL via certifi, 30s timeout) - Add CDN_DEPS table: 6 jsDelivr assets (js-yaml, marked, fuse.js, highlight.js, 2x highlight CSS) - Add _fetch_bunny_fonts(): reads theme.yml font-body/heading/code keys, fetches CSS from fonts.bunny.net, downloads woff2 files to assets/fonts/, rewrites CSS to use relative local paths, writes per-font CSS file - Add _patch_index_html(): replaces CDN URLs with local vendor paths, injects <link data-mdcms-fonts> tags for locally downloaded fonts - Add fetch-deps CLI command: downloads vendors, fetches fonts if theme.yml present, patches index.html — site makes no external network requests - index.html loadFonts(): skip if data-mdcms-fonts link already present (set by patched index.html after fetch-deps) - Update CLAUDE.md CLI command table with fetch-deps entries https://claude.ai/code/session_015XtsgTMi8UtmgxEgb5Qt2c
This commit is contained in:
parent
321202e5e0
commit
9b7639cc62
3 changed files with 160 additions and 0 deletions
|
|
@ -59,6 +59,8 @@ During development, run directly: `python3 mdcms.py <command>`
|
|||
| `mdcms build <name>` | Build `nav.yml` and `search.json` for a registered site. |
|
||||
| `mdcms build --path <path>` | Build using an explicit path — no registry needed. Intended for CI/CD. |
|
||||
| `mdcms build` | Build using current working directory. Simplest form for GitHub Actions. |
|
||||
| `mdcms fetch-deps [name]` | Download all external JS/CSS deps to `assets/required/vendors/` and Bunny Fonts to `assets/fonts/`. Patches `index.html` to use local paths — no CDN requests after this. |
|
||||
| `mdcms fetch-deps --path <path>` | Same, using an explicit path. |
|
||||
|
||||
**Local preview:** Run `python3 -m http.server 8800` in the site directory and open `http://localhost:8800`. Do not open `index.html` directly — browsers block local file access due to CORS.
|
||||
|
||||
|
|
|
|||
|
|
@ -1321,6 +1321,7 @@ body {
|
|||
|
||||
// ─── Fonts ────────────────────────────────────────────────
|
||||
function loadFonts(tc) {
|
||||
if (document.querySelector('link[data-mdcms-fonts]')) return;
|
||||
function parseFont(spec) {
|
||||
if (!spec) return null;
|
||||
const parts = spec.split(':');
|
||||
|
|
|
|||
157
mdcms.py
157
mdcms.py
|
|
@ -492,6 +492,130 @@ def run_build(site_path: Path):
|
|||
))
|
||||
|
||||
|
||||
# ─── Dependency fetching ──────────────────────────────────────
|
||||
|
||||
CDN_DEPS = [
|
||||
(
|
||||
"https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js",
|
||||
"assets/required/vendors/js-yaml.min.js",
|
||||
),
|
||||
(
|
||||
"https://cdn.jsdelivr.net/npm/marked@12.0.0/marked.min.js",
|
||||
"assets/required/vendors/marked.min.js",
|
||||
),
|
||||
(
|
||||
"https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js",
|
||||
"assets/required/vendors/fuse.min.js",
|
||||
),
|
||||
(
|
||||
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js",
|
||||
"assets/required/vendors/highlight.min.js",
|
||||
),
|
||||
(
|
||||
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css",
|
||||
"assets/required/vendors/github.min.css",
|
||||
),
|
||||
(
|
||||
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css",
|
||||
"assets/required/vendors/github-dark.min.css",
|
||||
),
|
||||
]
|
||||
|
||||
_WOFF2_URL_RE = re.compile(
|
||||
r"""url\(\s*['"]?(https://fonts\.bunny\.net/[^'"\s)]+\.woff2)['"]?\s*\)""",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _http_get(url: str, timeout: int = 30) -> bytes:
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
req = urllib.request.Request(url, headers={"User-Agent": f"mdcms/{CLI_VERSION}"})
|
||||
with urllib.request.urlopen(req, context=ssl_ctx, timeout=timeout) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def _fetch_bunny_fonts(site_path: Path, theme_file: str) -> list:
|
||||
"""Download Bunny Fonts from theme.yml to assets/fonts/. Returns list of local CSS paths."""
|
||||
theme_path = site_path / theme_file
|
||||
if not theme_path.exists():
|
||||
return []
|
||||
try:
|
||||
theme_data = yaml.safe_load(theme_path.read_text(encoding="utf-8")) or {}
|
||||
except (OSError, yaml.YAMLError):
|
||||
return []
|
||||
|
||||
fonts_dir = site_path / "assets" / "fonts"
|
||||
fonts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
seen: set = set()
|
||||
local_css_paths: list = []
|
||||
|
||||
for key in ("font-body", "font-heading", "font-code"):
|
||||
spec = theme_data.get(key)
|
||||
if not spec:
|
||||
continue
|
||||
parts = str(spec).split(":")
|
||||
if len(parts) < 3 or parts[0].strip().lower() != "bunny":
|
||||
continue
|
||||
name = parts[1].strip()
|
||||
weight = parts[-1].strip()
|
||||
font_id = f"{name}:{weight}"
|
||||
if font_id in seen:
|
||||
continue
|
||||
seen.add(font_id)
|
||||
|
||||
bunny_url = f"https://fonts.bunny.net/css?family={name.replace(' ', '+')}:{weight}"
|
||||
click.echo(f" Fetching font: {name} {weight}")
|
||||
try:
|
||||
css_text = _http_get(bunny_url).decode("utf-8")
|
||||
except Exception as e:
|
||||
click.echo(click.style(f" Warning: could not fetch {bunny_url}: {e}", fg="yellow"))
|
||||
continue
|
||||
|
||||
def _rewrite(m: re.Match) -> str:
|
||||
woff2_url = m.group(1)
|
||||
filename = woff2_url.split("/")[-1].split("?")[0]
|
||||
dest = fonts_dir / filename
|
||||
if not dest.exists():
|
||||
try:
|
||||
dest.write_bytes(_http_get(woff2_url))
|
||||
except Exception as e:
|
||||
click.echo(click.style(f" Warning: could not fetch {woff2_url}: {e}", fg="yellow"))
|
||||
return m.group(0)
|
||||
return f"url('../fonts/{filename}')"
|
||||
|
||||
local_css = _WOFF2_URL_RE.sub(_rewrite, css_text)
|
||||
safe_name = name.lower().replace(" ", "-")
|
||||
css_filename = f"{safe_name}-{weight}.css"
|
||||
(fonts_dir / css_filename).write_text(local_css, encoding="utf-8")
|
||||
local_css_paths.append(f"assets/fonts/{css_filename}")
|
||||
click.echo(f" Wrote assets/fonts/{css_filename}")
|
||||
|
||||
return local_css_paths
|
||||
|
||||
|
||||
def _patch_index_html(site_path: Path, local_font_css: list):
|
||||
"""Replace CDN tags with local paths and inject font link tags."""
|
||||
index_path = site_path / "index.html"
|
||||
if not index_path.exists():
|
||||
raise click.ClickException("index.html not found in site directory.")
|
||||
|
||||
html = index_path.read_text(encoding="utf-8")
|
||||
|
||||
for cdn_url, local_path in CDN_DEPS:
|
||||
html = html.replace(cdn_url, local_path)
|
||||
|
||||
if local_font_css:
|
||||
links = "\n".join(
|
||||
f'<link rel="stylesheet" href="{p}" data-mdcms-fonts="1">'
|
||||
for p in local_font_css
|
||||
)
|
||||
html = html.replace("</head>", f"{links}\n</head>", 1)
|
||||
|
||||
index_path.write_text(html, encoding="utf-8")
|
||||
click.echo(" Patched index.html")
|
||||
|
||||
|
||||
# ─── GitHub template download ─────────────────────────────────
|
||||
|
||||
def _github_get(url: str) -> bytes:
|
||||
|
|
@ -732,6 +856,39 @@ def build(name, path_override):
|
|||
click.echo(click.style("Build complete.", fg="green"))
|
||||
|
||||
|
||||
@cli.command("fetch-deps")
|
||||
@click.argument("name", required=False, default=None)
|
||||
@click.option("--path", "path_override", default=None, type=click.Path(),
|
||||
help="Explicit site path (no registry lookup).")
|
||||
def fetch_deps(name, path_override):
|
||||
"""Download external JS/CSS dependencies and patch index.html for offline use."""
|
||||
site_path = resolve_site_path(name, path_override)
|
||||
if not (site_path / "index.html").exists():
|
||||
raise click.ClickException(f"No index.html found at {site_path}")
|
||||
|
||||
click.echo(f"Fetching dependencies for {site_path} ...")
|
||||
|
||||
vendors_dir = site_path / "assets" / "required" / "vendors"
|
||||
vendors_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for cdn_url, rel_dest in CDN_DEPS:
|
||||
dest = site_path / rel_dest
|
||||
click.echo(f" {rel_dest}")
|
||||
try:
|
||||
dest.write_bytes(_http_get(cdn_url))
|
||||
except Exception as e:
|
||||
raise click.ClickException(f"Failed to download {cdn_url}: {e}")
|
||||
|
||||
cfg = read_config(site_path)
|
||||
local_font_css: list = []
|
||||
if cfg.get("theme"):
|
||||
local_font_css = _fetch_bunny_fonts(site_path, cfg["theme"])
|
||||
|
||||
_patch_index_html(site_path, local_font_css)
|
||||
|
||||
click.echo(click.style("Done. Site is ready for offline use.", fg="green"))
|
||||
|
||||
|
||||
# ─── Entry point ─────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
|
|
|
|||
Loading…
Reference in a new issue