mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add Micron renderer and AMPGateway site target
This commit is contained in:
+47
-1
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
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__"}
|
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)
|
return _render_privacy_html_tree(site, protocol, output_root)
|
||||||
if protocol.renderer == "gemtext":
|
if protocol.renderer == "gemtext":
|
||||||
return _render_gemtext_tree(site, protocol, output_root)
|
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}")
|
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):
|
def _iter_source_files(root: Path):
|
||||||
for path in sorted(root.rglob("*")):
|
for path in sorted(root.rglob("*")):
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
@@ -182,3 +217,14 @@ def _rewrite_gemtext_link(href: str) -> str:
|
|||||||
if path.endswith(".html") or path.endswith(".htm"):
|
if path.endswith(".html") or path.endswith(".htm"):
|
||||||
path = path.rsplit(".", 1)[0] + ".gmi"
|
path = path.rsplit(".", 1)[0] + ".gmi"
|
||||||
return path + (sep + fragment if sep else "")
|
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 "")
|
||||||
|
|||||||
@@ -272,6 +272,10 @@ def render_gemtext(html: str, rewrite_link: Callable[[str], str] | None = None)
|
|||||||
return GemtextRenderer(rewrite_link=rewrite_link).render(html)
|
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:
|
def _format_attrs(attrs: list[tuple[str, str | None]]) -> str:
|
||||||
if not attrs:
|
if not attrs:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ from .transports import (
|
|||||||
|
|
||||||
|
|
||||||
VALID_DAEMON_POLICIES = {"external", "adopt", "manage", "auto"}
|
VALID_DAEMON_POLICIES = {"external", "adopt", "manage", "auto"}
|
||||||
SUPPORTED_RENDERERS = {"clearnet", "privacy-html", "gemtext"}
|
SUPPORTED_RENDERERS = {"clearnet", "privacy-html", "gemtext", "micron"}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -198,9 +198,10 @@ outside AMPG's managed state layout.
|
|||||||
|
|
||||||
`micron` defaults:
|
`micron` defaults:
|
||||||
|
|
||||||
- one `.mu` page per high-value page.
|
- one `.mu` page per HTML page.
|
||||||
- headings converted to terminal-safe emphasis.
|
- headings, lists, links, and image references converted to terminal-safe text.
|
||||||
- images omitted unless the site config opts into small downloadable assets.
|
- local `.html` links rewritten to `.mu`.
|
||||||
|
- JavaScript, CSS, source maps, and oversized assets skipped.
|
||||||
|
|
||||||
## Daemon policy values
|
## Daemon policy values
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ run_dir = "./.ampg/ampgateway-online/run"
|
|||||||
user = "ampg"
|
user = "ampg"
|
||||||
|
|
||||||
[profiles.visual-vps]
|
[profiles.visual-vps]
|
||||||
description = "Full VPS deployment for the AMPGateway visual docs site."
|
description = "Full VPS deployment for the AMPGateway visual docs and live example sites."
|
||||||
protocols = ["clearnet", "tor", "i2p", "gemini"]
|
protocols = ["clearnet", "tor", "i2p", "gemini", "reticulum"]
|
||||||
platform = "linux-systemd"
|
platform = "linux-systemd"
|
||||||
write_artifacts = true
|
write_artifacts = true
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ renderer = "privacy-html"
|
|||||||
daemon = "tor"
|
daemon = "tor"
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
local_port = 18180
|
local_port = 18180
|
||||||
hidden_service_dir = "/var/lib/ampg/tor/ampgateway-online"
|
hidden_service_dir = "/var/lib/tor/ampg/ampgateway-online"
|
||||||
onion_location = "auto"
|
onion_location = "auto"
|
||||||
max_asset_bytes = 1048576
|
max_asset_bytes = 1048576
|
||||||
script_policy = "strip"
|
script_policy = "strip"
|
||||||
@@ -69,8 +69,70 @@ port = 1965
|
|||||||
max_asset_bytes = 1048576
|
max_asset_bytes = 1048576
|
||||||
|
|
||||||
[site.protocols.reticulum]
|
[site.protocols.reticulum]
|
||||||
enabled = false
|
enabled = true
|
||||||
renderer = "micron"
|
renderer = "micron"
|
||||||
daemon = "rnsd"
|
daemon = "rnsd"
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
aspect = "web"
|
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
|
||||||
|
|||||||
@@ -65,6 +65,61 @@ max_asset_bytes = 1024
|
|||||||
self.assertIn("daemon = agate", agate_plan)
|
self.assertIn("daemon = agate", agate_plan)
|
||||||
self.assertIn("content_root =", 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(
|
||||||
|
'<h1>Home</h1><p><a href="about.html">About</a></p><script>bad()</script>',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(source / "about.html").write_text("<h1>About</h1>", 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from ampg.renderers import render_gemtext
|
from ampg.renderers import render_gemtext, render_micron
|
||||||
|
|
||||||
|
|
||||||
class GemtextRendererTest(unittest.TestCase):
|
class GemtextRendererTest(unittest.TestCase):
|
||||||
@@ -31,6 +31,27 @@ class GemtextRendererTest(unittest.TestCase):
|
|||||||
self.assertIn("=> logo.png Image: Logo", gemtext)
|
self.assertIn("=> logo.png Image: Logo", gemtext)
|
||||||
self.assertNotIn("ignored", gemtext)
|
self.assertNotIn("ignored", gemtext)
|
||||||
|
|
||||||
|
def test_converts_basic_html_to_micron(self):
|
||||||
|
html = """
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<p>Read the <a href="about.html">about page</a>.</p>
|
||||||
|
<script>ignored()</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ daemon_policy = "auto"
|
|||||||
"""
|
"""
|
||||||
[site.protocols.tor]
|
[site.protocols.tor]
|
||||||
enabled = true
|
enabled = true
|
||||||
renderer = "micron"
|
renderer = "unbuilt-renderer"
|
||||||
daemon = "tor"
|
daemon = "tor"
|
||||||
daemon_policy = "adopt"
|
daemon_policy = "adopt"
|
||||||
""",
|
""",
|
||||||
|
|||||||
Reference in New Issue
Block a user