From 7f3631a624e4ef3b3201c98a9c8df63f555f0597 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 7 Jul 2026 11:34:54 -0400 Subject: [PATCH] Add Micron renderer and AMPGateway site target --- ampg/build.py | 48 ++++++++++++++++- ampg/renderers.py | 4 ++ ampg/status.py | 2 +- docs/configuration.md | 7 +-- examples/ampgateway-online.gateway.toml | 70 +++++++++++++++++++++++-- tests/test_gemini_build.py | 55 +++++++++++++++++++ tests/test_gemtext_renderer.py | 23 +++++++- tests/test_status.py | 2 +- 8 files changed, 200 insertions(+), 11 deletions(-) diff --git a/ampg/build.py b/ampg/build.py index 10c7b87..e18d56f 100644 --- a/ampg/build.py +++ b/ampg/build.py @@ -5,7 +5,7 @@ from pathlib import Path import shutil from .config import GatewayConfig, ProtocolConfig, SiteConfig -from .renderers import PrivacyRenderStats, render_gemtext, render_privacy_html +from .renderers import PrivacyRenderStats, render_gemtext, render_micron, render_privacy_html IGNORED_DIRS = {".git", ".hg", ".svn", ".vscode", "__pycache__"} @@ -43,6 +43,8 @@ def build_protocol(site: SiteConfig, protocol: ProtocolConfig) -> BuildResult: return _render_privacy_html_tree(site, protocol, output_root) if protocol.renderer == "gemtext": return _render_gemtext_tree(site, protocol, output_root) + if protocol.renderer == "micron": + return _render_micron_tree(site, protocol, output_root) raise ValueError(f"{site.id}/{protocol.name}: unsupported renderer {protocol.renderer!r}") @@ -135,6 +137,39 @@ def _render_gemtext_tree( ) +def _render_micron_tree( + site: SiteConfig, protocol: ProtocolConfig, output_root: Path +) -> BuildResult: + files_written = 0 + files_skipped = 0 + max_asset_bytes = int(protocol.options.get("max_asset_bytes", 512 * 1024)) + for source_path in _iter_source_files(site.source.path): + if source_path.suffix.lower() in {".html", ".htm"}: + target_path = _target_with_suffix(site.source.path, output_root, source_path, ".mu") + target_path.parent.mkdir(parents=True, exist_ok=True) + html = source_path.read_text(encoding="utf-8", errors="replace") + target_path.write_text(render_micron(html, rewrite_link=_rewrite_micron_link), encoding="utf-8") + elif source_path.suffix.lower() in {".js", ".css", ".map"}: + files_skipped += 1 + continue + elif source_path.stat().st_size > max_asset_bytes: + files_skipped += 1 + continue + else: + target_path = output_root / source_path.relative_to(site.source.path) + target_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, target_path) + files_written += 1 + return BuildResult( + site_id=site.id, + protocol=protocol.name, + renderer=protocol.renderer, + output_root=output_root, + files_written=files_written, + files_skipped=files_skipped, + ) + + def _iter_source_files(root: Path): for path in sorted(root.rglob("*")): if not path.is_file(): @@ -182,3 +217,14 @@ def _rewrite_gemtext_link(href: str) -> str: if path.endswith(".html") or path.endswith(".htm"): path = path.rsplit(".", 1)[0] + ".gmi" return path + (sep + fragment if sep else "") + + +def _rewrite_micron_link(href: str) -> str: + if href.startswith("#"): + return href + if href.startswith(("http://", "https://", "gemini://", "mailto:")): + return href + path, sep, fragment = href.partition("#") + if path.endswith(".html") or path.endswith(".htm"): + path = path.rsplit(".", 1)[0] + ".mu" + return path + (sep + fragment if sep else "") diff --git a/ampg/renderers.py b/ampg/renderers.py index bc51b99..e77460e 100644 --- a/ampg/renderers.py +++ b/ampg/renderers.py @@ -272,6 +272,10 @@ def render_gemtext(html: str, rewrite_link: Callable[[str], str] | None = None) return GemtextRenderer(rewrite_link=rewrite_link).render(html) +def render_micron(html: str, rewrite_link: Callable[[str], str] | None = None) -> str: + return GemtextRenderer(rewrite_link=rewrite_link).render(html) + + def _format_attrs(attrs: list[tuple[str, str | None]]) -> str: if not attrs: return "" diff --git a/ampg/status.py b/ampg/status.py index c7f8021..7d7df15 100644 --- a/ampg/status.py +++ b/ampg/status.py @@ -16,7 +16,7 @@ from .transports import ( VALID_DAEMON_POLICIES = {"external", "adopt", "manage", "auto"} -SUPPORTED_RENDERERS = {"clearnet", "privacy-html", "gemtext"} +SUPPORTED_RENDERERS = {"clearnet", "privacy-html", "gemtext", "micron"} @dataclass(frozen=True) diff --git a/docs/configuration.md b/docs/configuration.md index 3079c32..3d70be8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -198,9 +198,10 @@ outside AMPG's managed state layout. `micron` defaults: -- one `.mu` page per high-value page. -- headings converted to terminal-safe emphasis. -- images omitted unless the site config opts into small downloadable assets. +- one `.mu` page per HTML page. +- headings, lists, links, and image references converted to terminal-safe text. +- local `.html` links rewritten to `.mu`. +- JavaScript, CSS, source maps, and oversized assets skipped. ## Daemon policy values diff --git a/examples/ampgateway-online.gateway.toml b/examples/ampgateway-online.gateway.toml index cac04c9..cfff877 100644 --- a/examples/ampgateway-online.gateway.toml +++ b/examples/ampgateway-online.gateway.toml @@ -5,8 +5,8 @@ run_dir = "./.ampg/ampgateway-online/run" user = "ampg" [profiles.visual-vps] -description = "Full VPS deployment for the AMPGateway visual docs site." -protocols = ["clearnet", "tor", "i2p", "gemini"] +description = "Full VPS deployment for the AMPGateway visual docs and live example sites." +protocols = ["clearnet", "tor", "i2p", "gemini", "reticulum"] platform = "linux-systemd" write_artifacts = true @@ -44,7 +44,7 @@ renderer = "privacy-html" daemon = "tor" daemon_policy = "auto" local_port = 18180 -hidden_service_dir = "/var/lib/ampg/tor/ampgateway-online" +hidden_service_dir = "/var/lib/tor/ampg/ampgateway-online" onion_location = "auto" max_asset_bytes = 1048576 script_policy = "strip" @@ -69,8 +69,70 @@ port = 1965 max_asset_bytes = 1048576 [site.protocols.reticulum] -enabled = false +enabled = true renderer = "micron" daemon = "rnsd" daemon_policy = "auto" aspect = "web" +max_asset_bytes = 524288 + +[[site]] +id = "ampgateway-site" +domain = "ampgateway.site" + +[site.source] +kind = "static-html" +path = "../sites/ampgateway-online" +canonical_url = "https://ampgateway.site" + +[site.outputs] +root = "../dist/ampgateway-site" +plan_root = "../dist/ampg-plan" + +[site.interactions] +default_tier = "static" +deny_routes = ["/admin/*", "/internal/*", "/webhooks/*"] + +[site.protocols.clearnet] +enabled = true +renderer = "clearnet" +daemon = "nginx" +daemon_policy = "external" + +[site.protocols.tor] +enabled = true +renderer = "privacy-html" +daemon = "tor" +daemon_policy = "auto" +local_port = 18280 +hidden_service_dir = "/var/lib/tor/ampg/ampgateway-site" +onion_location = "auto" +max_asset_bytes = 1048576 +script_policy = "strip" + +[site.protocols.i2p] +enabled = true +renderer = "privacy-html" +daemon = "i2pd" +daemon_policy = "auto" +local_port = 18281 +keys_file = "ampgateway-site-web.dat" +tunnel_name = "ampgateway-site-web" +max_asset_bytes = 1048576 +script_policy = "strip" + +[site.protocols.gemini] +enabled = true +renderer = "gemtext" +daemon = "agate" +daemon_policy = "auto" +port = 1966 +max_asset_bytes = 1048576 + +[site.protocols.reticulum] +enabled = true +renderer = "micron" +daemon = "rnsd" +daemon_policy = "auto" +aspect = "web" +max_asset_bytes = 524288 diff --git a/tests/test_gemini_build.py b/tests/test_gemini_build.py index 58eb895..c5a1036 100644 --- a/tests/test_gemini_build.py +++ b/tests/test_gemini_build.py @@ -65,6 +65,61 @@ max_asset_bytes = 1024 self.assertIn("daemon = agate", agate_plan) self.assertIn("content_root =", agate_plan) + def test_existing_html_source_can_build_only_reticulum(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + source.mkdir() + (source / "index.html").write_text( + '

Home

About

', + encoding="utf-8", + ) + (source / "about.html").write_text("

About

", encoding="utf-8") + (source / "app.js").write_text("bad()", encoding="utf-8") + (source / "styles.css").write_text("body {}", encoding="utf-8") + config_path = root / "gateway.toml" + config_path.write_text( + """ +[[site]] +id = "reticulum_only" +domain = "example.test" + +[site.source] +kind = "static-html" +path = "./source" + +[site.outputs] +root = "./out" +plan_root = "./plan" + +[site.protocols.reticulum] +enabled = true +renderer = "micron" +daemon = "rnsd" +daemon_policy = "auto" +aspect = "web" +max_asset_bytes = 1024 +""", + encoding="utf-8", + ) + + config = load_config(config_path) + plan = plan_gateway(config) + artifacts = write_plan_artifacts(config) + results = build_gateway(config) + + self.assertEqual([line.protocol for line in plan], ["reticulum"]) + self.assertEqual([result.protocol for result in results], ["reticulum"]) + self.assertEqual(len(artifacts), 0) + self.assertTrue((root / "out/reticulum/index.mu").exists()) + self.assertTrue((root / "out/reticulum/about.mu").exists()) + self.assertFalse((root / "out/reticulum/app.js").exists()) + self.assertFalse((root / "out/reticulum/styles.css").exists()) + micron = (root / "out/reticulum/index.mu").read_text(encoding="utf-8") + self.assertIn("# Home", micron) + self.assertIn("=> about.mu About", micron) + self.assertNotIn("bad", micron) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_gemtext_renderer.py b/tests/test_gemtext_renderer.py index 379461c..f423b13 100644 --- a/tests/test_gemtext_renderer.py +++ b/tests/test_gemtext_renderer.py @@ -1,6 +1,6 @@ import unittest -from ampg.renderers import render_gemtext +from ampg.renderers import render_gemtext, render_micron class GemtextRendererTest(unittest.TestCase): @@ -31,6 +31,27 @@ class GemtextRendererTest(unittest.TestCase): self.assertIn("=> logo.png Image: Logo", gemtext) self.assertNotIn("ignored", gemtext) + def test_converts_basic_html_to_micron(self): + html = """ + + +

Hello

+

Read the about page.

+ + + +""" + + micron = render_micron( + html, + rewrite_link=lambda href: href.replace(".html", ".mu"), + ) + + self.assertIn("# Hello", micron) + self.assertIn("Read the about page.", micron) + self.assertIn("=> about.mu about page", micron) + self.assertNotIn("ignored", micron) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_status.py b/tests/test_status.py index d4cccd5..e0f4e0a 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -154,7 +154,7 @@ daemon_policy = "auto" """ [site.protocols.tor] enabled = true -renderer = "micron" +renderer = "unbuilt-renderer" daemon = "tor" daemon_policy = "adopt" """,