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        url_name = self.page.get_url_name()
 75        if not url_name:
 76            return super().get()
 77
 78        companion = pages_registry.get_markdown_companion(url_name)
 79        if not companion:
 80            return super().get()
 81
 82        if self._prefers_markdown("text/html", "text/markdown", "text/plain"):
 83            return self._markdown_response(companion)
 84
 85        # HTML response varies by Accept when a companion exists
 86        response = super().get()
 87        patch_vary_headers(response, ["Accept"])
 88        return response
 89
 90    def get_template_names(self) -> list[str]:
 91        """
 92        Allow for more specific user templates like
 93        markdown.html or html.html
 94        """
 95        if template_name := self.page.get_template_name():
 96            return [template_name]
 97
 98        return super().get_template_names()
 99
100
101class PageRedirectView(PageViewMixin, View):
102    def get(self) -> RedirectResponse:
103        url = self.page.vars.get("url")
104
105        if not url:
106            raise RedirectPageError("Redirect page is missing a url")
107
108        status_code = self.page.vars.get("status_code", 302)
109        return RedirectResponse(url, status_code=status_code, allow_external=True)
110
111
112class PageAssetView(PageViewMixin, AssetView):
113    def get_url_path(self) -> str | None:
114        return self.page.get_url_path()
115
116    def get_asset_path(self, path: str) -> str:
117        return self.page.absolute_path
118
119    def get_debug_asset_path(self, path: str) -> str:
120        return self.page.absolute_path
121
122
123class PageMarkdownView(PageViewMixin, TemplateView):
124    def get(self) -> Response:
125        """Serve the markdown content without frontmatter."""
126        context = self.get_template_context()
127        markdown_content = self.page.rendered_source(context)
128        return Response(markdown_content, content_type="text/plain; charset=utf-8")