1import os
2from functools import cached_property
3
4import frontmatter
5
6from plain.runtime import settings
7from plain.templates import Template
8from plain.urls import path, reverse
9
10from .markdown import render_markdown
11
12
13class PageRenderError(Exception):
14 pass
15
16
17class Page:
18 def __init__(self, relative_path, absolute_path):
19 self.relative_path = relative_path
20 self.absolute_path = absolute_path
21 self._template_context = {}
22
23 def set_template_context(self, context):
24 self._template_context = context
25
26 @cached_property
27 def _frontmatter(self):
28 with open(self.absolute_path) as f:
29 return frontmatter.load(f)
30
31 @cached_property
32 def vars(self):
33 return self._frontmatter.metadata
34
35 @cached_property
36 def title(self):
37 default_title = os.path.splitext(os.path.basename(self.relative_path))[0]
38 return self.vars.get("title", default_title)
39
40 @cached_property
41 def content(self):
42 # Strip the frontmatter
43 content = self._frontmatter.content
44
45 if not self.vars.get("render_plain", False):
46 template = Template(os.path.join("pages", self.relative_path))
47
48 try:
49 content = template.render(self._template_context)
50 except Exception as e:
51 # Throw our own error so we don't get shadowed by the Jinja error
52 raise PageRenderError(f"Error rendering page {self.relative_path}: {e}")
53
54 # Strip the frontmatter again, since it was in the template file itself
55 _, content = frontmatter.parse(content)
56
57 if self.is_markdown():
58 content = render_markdown(content, current_page_path=self.relative_path)
59
60 return content
61
62 def is_markdown(self):
63 extension = os.path.splitext(self.absolute_path)[1]
64 return extension == ".md"
65
66 def is_template(self):
67 return ".template." in os.path.basename(self.absolute_path)
68
69 def is_asset(self):
70 extension = os.path.splitext(self.absolute_path)[1]
71 # Anything that we don't specifically recognize for pages
72 # gets treated as an asset
73 return extension.lower() not in (
74 ".html",
75 ".md",
76 ".redirect",
77 )
78
79 def is_redirect(self):
80 extension = os.path.splitext(self.absolute_path)[1]
81 return extension == ".redirect"
82
83 def get_template_name(self):
84 if template_name := self.vars.get("template_name"):
85 return template_name
86
87 return ""
88
89 def get_url_path(self):
90 """Generate the primary URL path for this page."""
91 if self.is_template():
92 return None
93
94 if self.is_asset():
95 return self.relative_path
96
97 url_path = os.path.splitext(self.relative_path)[0]
98
99 # If it's an index.html or something, the url is the parent dir
100 if os.path.basename(url_path) == "index":
101 url_path = os.path.dirname(url_path)
102
103 # The root url should stay an empty string
104 if not url_path:
105 return ""
106
107 # Everything else should get a trailing slash
108 return url_path + "/"
109
110 def get_url_name(self):
111 """Generate the URL name from the URL path."""
112 url_path = self.get_url_path()
113 if url_path is None:
114 return None
115
116 if not url_path:
117 return "index"
118
119 return url_path.rstrip("/")
120
121 def get_view_class(self):
122 """Get the appropriate view class for this page."""
123 from .views import PageAssetView, PageRedirectView, PageView
124
125 if self.is_redirect():
126 return PageRedirectView
127
128 if self.is_asset():
129 return PageAssetView
130
131 return PageView
132
133 def get_markdown_url(self):
134 """Get the markdown URL for this page if it exists."""
135 if not self.is_markdown():
136 return None
137
138 if not settings.PAGES_MARKDOWN_URLS:
139 return None
140
141 url_name = self.get_url_name()
142 if not url_name:
143 return None
144
145 return reverse(f"pages:{url_name}-md")
146
147 def get_urls(self):
148 """Get all URL path objects for this page."""
149 urls = []
150
151 # Generate primary URL
152 url_path = self.get_url_path()
153 url_name = self.get_url_name()
154 view_class = self.get_view_class()
155
156 if url_path is not None and url_name is not None:
157 urls.append(
158 path(
159 url_path,
160 view_class,
161 name=url_name,
162 )
163 )
164
165 # For markdown files, optionally add .md URL
166 if self.is_markdown() and settings.PAGES_MARKDOWN_URLS:
167 from .views import PageMarkdownView
168
169 urls.append(
170 path(
171 self.relative_path,
172 PageMarkdownView,
173 name=f"{url_name}-md",
174 )
175 )
176
177 return urls