Plain is headed towards 1.0! Subscribe for development updates →

  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