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")