1import os
2
3import frontmatter
4
5from plain.templates import Template
6from plain.utils.functional import cached_property
7
8from .markdown import render_markdown
9
10
11class PageRenderError(Exception):
12 pass
13
14
15class Page:
16 def __init__(self, relative_path, absolute_path):
17 self.relative_path = relative_path
18 self.absolute_path = absolute_path
19 self._template_context = {}
20
21 def set_template_context(self, context):
22 self._template_context = context
23
24 @cached_property
25 def _frontmatter(self):
26 with open(self.absolute_path) as f:
27 return frontmatter.load(f)
28
29 @cached_property
30 def vars(self):
31 return self._frontmatter.metadata
32
33 @cached_property
34 def title(self):
35 default_title = os.path.splitext(os.path.basename(self.relative_path))[0]
36 return self.vars.get("title", default_title)
37
38 @cached_property
39 def content(self):
40 # Strip the frontmatter
41 content = self._frontmatter.content
42
43 if not self.vars.get("render_plain", False):
44 template = Template(os.path.join("pages", self.relative_path))
45
46 try:
47 content = template.render(self._template_context)
48 except Exception as e:
49 # Throw our own error so we don't get shadowed by the Jinja error
50 raise PageRenderError(f"Error rendering page {self.relative_path}: {e}")
51
52 # Strip the frontmatter again, since it was in the template file itself
53 _, content = frontmatter.parse(content)
54
55 if self.is_markdown():
56 content = render_markdown(content)
57
58 return content
59
60 def is_markdown(self):
61 extension = os.path.splitext(self.absolute_path)[1]
62 return extension == ".md"
63
64 def is_template(self):
65 return ".template." in os.path.basename(self.absolute_path)
66
67 def is_asset(self):
68 extension = os.path.splitext(self.absolute_path)[1]
69 # Anything that we don't specifically recognize for pages
70 # gets treated as an asset
71 return extension.lower() not in (
72 ".html",
73 ".md",
74 ".redirect",
75 )
76
77 def is_redirect(self):
78 extension = os.path.splitext(self.absolute_path)[1]
79 return extension == ".redirect"
80
81 def get_url_path(self) -> str | None:
82 if self.is_template():
83 return None
84
85 if self.is_asset():
86 return self.relative_path
87
88 url_path = os.path.splitext(self.relative_path)[0]
89
90 # If it's an index.html or something, the url is the parent dir
91 if os.path.basename(url_path) == "index":
92 url_path = os.path.dirname(url_path)
93
94 # The root url should stay an empty string
95 if not url_path:
96 return ""
97
98 # Everything else should get a trailing slash
99 return url_path + "/"
100
101 def get_url_name(self):
102 url_path = self.get_url_path()
103 if url_path is None:
104 return None
105
106 if not url_path:
107 return "index"
108
109 return url_path.rstrip("/")
110
111 def get_template_name(self):
112 if template_name := self.vars.get("template_name"):
113 return template_name
114
115 return ""
116
117 def get_view_class(self):
118 from .views import PageAssetView, PageRedirectView, PageView
119
120 if self.is_redirect():
121 return PageRedirectView
122
123 if self.is_asset():
124 return PageAssetView
125
126 return PageView