mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
v0.3.8 — Phase 6: fetch-deps offline/local dependency mode
v0.3.8 — Phase 6: fetch-deps offline/local dependency mode
This commit is contained in:
commit
8993a4285c
5 changed files with 165 additions and 5 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(':');
|
||||
|
|
|
|||
163
mdcms.py
163
mdcms.py
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mdcms v0.3.7 — CLI companion
|
||||
# mdcms v0.3.8 — CLI companion
|
||||
#
|
||||
# Copyright 2026 Kristian Benestad
|
||||
# Apache License, Version 2.0 — https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
"""MD-CMS v0.3.7 — CLI tool for managing and building MD-CMS sites."""
|
||||
"""MD-CMS v0.3.8 — CLI tool for managing and building MD-CMS sites."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
|
@ -21,7 +21,7 @@ import certifi
|
|||
import click
|
||||
import yaml
|
||||
|
||||
CLI_VERSION = "0.3.7"
|
||||
CLI_VERSION = "0.3.8"
|
||||
CLI_RELEASE_DATE = "17 May 2026"
|
||||
MIN_SUPPORTED_VERSION = "0.3"
|
||||
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "mdcms"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
description = "MD-CMS — Markdown-based CMS companion CLI"
|
||||
readme = "README.md"
|
||||
license = { text = "Apache-2.0" }
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ PHASES = {
|
|||
3: ("v0.4_phase3", "Asset validation in mdcms build"),
|
||||
4: ("claude/debug-api-errors-gd730", "Callout tags"),
|
||||
5: ("claude/toc-tag-phase5", "Table of contents tag"),
|
||||
6: ("v0.4_phase6", "Offline / fetch-deps"),
|
||||
6: ("claude/fetch-deps-phase6", "Offline / fetch-deps"),
|
||||
7: ("v0.4_phase7", "PWA — service worker and manifest"),
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue