1from __future__ import annotations
  2
  3from functools import cached_property
  4from typing import Any
  5
  6from plain.assets.views import AssetView
  7from plain.http import (
  8    NotFoundError404,
  9    RedirectResponse,
 10    Response,
 11)
 12from plain.runtime import settings
 13from plain.utils.cache import patch_vary_headers
 14from plain.views import TemplateView, View
 15
 16from .exceptions import PageNotFoundError, RedirectPageError
 17from .pages import Page
 18from .registry import pages_registry
 19
 20__all__ = ["PageView"]
 21
 22
 23class PageViewMixin:
 24    @cached_property
 25    def page(self) -> Page:
 26        url_name = self.request.resolver_match.url_name  # type: ignore[attr-defined]
 27
 28        try:
 29            return pages_registry.get_page_from_name(url_name)
 30        except PageNotFoundError:
 31            raise NotFoundError404()
 32
 33    def get_template_context(self) -> dict[str, Any]:
 34        context = super().get_template_context()  # type: ignore[misc]
 35        context["page"] = self.page
 36        self.page.set_template_context(context)  # Pass the standard context through
 37        return context
 38
 39
 40class PageView(PageViewMixin, TemplateView):
 41    template_name = "page.html"
 42
 43    def _markdown_response(self, page: Page) -> Response:
 44        """Build a plain-text markdown response with Vary header."""
 45        context = {**self.get_template_context(), "page": page}
 46        page.set_template_context(context)
 47        markdown_content = page.rendered_source(context)
 48        response = Response(markdown_content, content_type="text/plain; charset=utf-8")
 49        patch_vary_headers(response, ["Accept"])
 50        return response
 51
 52    def _prefers_markdown(self, *types: str) -> bool:
 53        """Check if the request prefers markdown over the given types."""
 54        preferred = self.request.get_preferred_type(*types)
 55        return preferred in ("text/markdown", "text/plain")
 56
 57    def get(self) -> Response:
 58        """Check Accept header and serve markdown if requested."""
 59        if not settings.PAGES_SERVE_MARKDOWN:
 60            return super().get()
 61
 62        # Standalone markdown page -- markdown is preferred by default.
 63        # Type order matters: first type listed wins ties, so markdown
 64        # types come first here to default to raw markdown.
 65        if self.page.is_markdown():
 66            if self._prefers_markdown("text/markdown", "text/plain", "text/html"):
 67                return self._markdown_response(self.page)
 68            response = super().get()
 69            patch_vary_headers(response, ["Accept"])
 70            return response
 71
 72        # HTML page -- only serve markdown if a companion exists and is explicitly preferred.
 73        # Type order matters: text/html first so HTML wins ties.
 74        companion = pages_registry.get_markdown_companion(self.page.get_url_name())
 75        if not companion:
 76            return super().get()
 77
 78        if self._prefers_markdown("text/html", "text/markdown", "text/plain"):
 79            return self._markdown_response(companion)
 80
 81        # HTML response varies by Accept when a companion exists
 82        response = super().get()
 83        patch_vary_headers(response, ["Accept"])
 84        return response
 85
 86    def get_template_names(self) -> list[str]:
 87        """
 88        Allow for more specific user templates like
 89        markdown.html or html.html
 90        """
 91        if template_name := self.page.get_template_name():
 92            return [template_name]
 93
 94        return super().get_template_names()
 95
 96
 97class PageRedirectView(PageViewMixin, View):
 98    def get(self) -> RedirectResponse:
 99        url = self.page.vars.get("url")
100
101        if not url:
102            raise RedirectPageError("Redirect page is missing a url")
103
104        status_code = self.page.vars.get("status_code", 302)
105        return RedirectResponse(url, status_code=status_code)
106
107
108class PageAssetView(PageViewMixin, AssetView):
109    def get_url_path(self) -> str | None:
110        return self.page.get_url_path()
111
112    def get_asset_path(self, path: str) -> str:
113        return self.page.absolute_path
114
115    def get_debug_asset_path(self, path: str) -> str:
116        return self.page.absolute_path
117
118
119class PageMarkdownView(PageViewMixin, TemplateView):
120    def get(self) -> Response:
121        """Serve the markdown content without frontmatter."""
122        context = self.get_template_context()
123        markdown_content = self.page.rendered_source(context)
124        return Response(markdown_content, content_type="text/plain; charset=utf-8")