Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/httk/web/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ def create_asgi_app(
srcdir: str | Path,
*,
baseurl: str | None = None,
compatibility_mode: bool = False,
debug: bool = False,
) -> Starlette:
config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl)
config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode)
engine = SiteEngine(config)
return create_app(engine=engine, debug=debug)

Expand All @@ -27,17 +28,20 @@ def serve(
host: str = "127.0.0.1",
port: int = 8080,
baseurl: str | None = None,
compatibility_mode: bool = False,
debug: bool = False,
) -> None:
app = create_asgi_app(srcdir=srcdir, baseurl=baseurl, debug=debug)
app = create_asgi_app(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode, debug=debug)
run_dev_server(app=app, host=host, port=port)


def publish(
srcdir: str | Path,
outdir: str | Path,
baseurl: str,
*,
compatibility_mode: bool = False,
) -> PublishReport:
config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl)
config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode)
engine = SiteEngine(config)
return publish_site(engine=engine, outdir=outdir)
13 changes: 11 additions & 2 deletions src/httk/web/engine/site_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@
from httk.web.model.page import PageResult, ResolvedRoute
from httk.web.model.request import HttpRequestContext
from httk.web.renderers import RENDERERS_BY_SUFFIX
from httk.web.templating import JinjaTemplateEngine, TemplateRenderInput
from httk.web.templating import (
HttkCompatTemplateEngine,
JinjaTemplateEngine,
TemplateEngine,
TemplateRenderInput,
)


class SiteEngine:
def __init__(self, config: SiteConfig) -> None:
self.config = config
self.template_engine = JinjaTemplateEngine(template_dir=config.template_dir)
self.template_engine: TemplateEngine
if config.compatibility_mode:
self.template_engine = HttkCompatTemplateEngine(template_dir=config.template_dir)
else:
self.template_engine = JinjaTemplateEngine(template_dir=config.template_dir)
self.function_handler = PythonFunctionHandler(functions_dir=config.functions_dir)

def resolve(self, route: str) -> ResolvedRoute:
Expand Down
11 changes: 9 additions & 2 deletions src/httk/web/model/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ class SiteConfig:
template_subdir: str = "templates"
functions_subdir: str = "functions"
baseurl: str | None = None
compatibility_mode: bool = False

@classmethod
def from_srcdir(cls, srcdir: str | Path, *, baseurl: str | None = None) -> Self:
return cls(srcdir=Path(srcdir).resolve(), baseurl=baseurl)
def from_srcdir(
cls,
srcdir: str | Path,
*,
baseurl: str | None = None,
compatibility_mode: bool = False,
) -> Self:
return cls(srcdir=Path(srcdir).resolve(), baseurl=baseurl, compatibility_mode=compatibility_mode)

@property
def content_dir(self) -> Path:
Expand Down
5 changes: 3 additions & 2 deletions src/httk/web/templating/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .base import TemplateRenderInput
from .base import TemplateEngine, TemplateRenderInput
from .httk_compat import HttkCompatTemplateEngine
from .jinja2_engine import JinjaTemplateEngine

__all__ = ["TemplateRenderInput", "JinjaTemplateEngine"]
__all__ = ["TemplateRenderInput", "TemplateEngine", "JinjaTemplateEngine", "HttkCompatTemplateEngine"]
7 changes: 7 additions & 0 deletions src/httk/web/templating/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
Expand All @@ -7,3 +8,9 @@ class TemplateRenderInput:
template_name: str | None
base_template_name: str | None
context: dict[str, object]


class TemplateEngine(Protocol):
def render(self, render_input: TemplateRenderInput) -> str: ...

def render_fragment(self, *, template_name: str, context: dict[str, object]) -> str | None: ...
23 changes: 23 additions & 0 deletions src/httk/web/templating/httk_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path

from .jinja2_engine import JinjaTemplateEngine

LEGACY_TEMPLATE_SUFFIXES: tuple[str, ...] = (
".httkweb.html",
".html",
".html.j2",
".jinja",
".j2",
)


class HttkCompatTemplateEngine(JinjaTemplateEngine):
"""
Legacy-oriented template resolution for old httkweb projects.

The compatibility engine keeps Jinja rendering but prioritizes legacy
template suffixes so old `.httkweb.html` files resolve first.
"""

def __init__(self, template_dir: Path) -> None:
super().__init__(template_dir, template_suffixes=LEGACY_TEMPLATE_SUFFIXES)
12 changes: 9 additions & 3 deletions src/httk/web/templating/jinja2_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@


class JinjaTemplateEngine:
def __init__(self, template_dir: Path) -> None:
def __init__(
self,
template_dir: Path,
*,
template_suffixes: tuple[str, ...] = TEMPLATE_SUFFIXES,
) -> None:
self.template_dir = template_dir
self.template_suffixes = template_suffixes
self._environment = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=True,
Expand Down Expand Up @@ -69,7 +75,7 @@ def _resolve_template(self, name: str | None) -> str | None:
if path_candidate.suffix:
return None

for suffix in TEMPLATE_SUFFIXES:
for suffix in self.template_suffixes:
with_suffix = self.template_dir / f"{candidate}{suffix}"
if with_suffix.exists() and with_suffix.is_file():
return f"{candidate}{suffix}"
Expand Down Expand Up @@ -135,7 +141,7 @@ def _resolve_fragment_template(self, name: str) -> str | None:
# For bare names, probe the same suffix families as standard template
# resolution plus legacy html-style names used by old function metadata.
if not path_candidate.suffix:
for suffix in TEMPLATE_SUFFIXES:
for suffix in self.template_suffixes:
template_key = self._resolve_template(f"{candidate}{suffix}")
if template_key is not None:
return template_key
Expand Down
38 changes: 38 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,41 @@ def test_publish_writes_html_output(tmp_path: Path) -> None:
index_out = out / "index.html"
assert index_out.exists()
assert any(path == index_out for path in report.written_files)


def test_create_asgi_app_compatibility_mode_prefers_httkweb_templates(tmp_path: Path) -> None:
src = tmp_path / "src"
(src / "content").mkdir(parents=True)
(src / "static").mkdir(parents=True)
(src / "templates").mkdir(parents=True)

(src / "content" / "index.md").write_text("---\ntemplate: default\n---\n\nhello", encoding="utf-8")
(src / "templates" / "default.html.j2").write_text("modern={{ content }}", encoding="utf-8")
(src / "templates" / "default.httkweb.html").write_text("legacy={{ content }}", encoding="utf-8")

app = create_asgi_app(src, compatibility_mode=True)

with TestClient(app) as client:
response = client.get("/")

assert response.status_code == 200
assert "legacy=" in response.text
assert "modern=" not in response.text


def test_publish_compatibility_mode_prefers_httkweb_templates(tmp_path: Path) -> None:
src = tmp_path / "src"
out = tmp_path / "public"
(src / "content").mkdir(parents=True)
(src / "static").mkdir(parents=True)
(src / "templates").mkdir(parents=True)

(src / "content" / "index.md").write_text("---\ntemplate: default\n---\n\nhello", encoding="utf-8")
(src / "templates" / "default.html.j2").write_text("modern={{ content }}", encoding="utf-8")
(src / "templates" / "default.httkweb.html").write_text("legacy={{ content }}", encoding="utf-8")

publish(src, out, "http://localhost/", compatibility_mode=True)

rendered = (out / "index.html").read_text(encoding="utf-8")
assert "legacy=" in rendered
assert "modern=" not in rendered
Loading