render-engine 2025.10.1a1__py3-none-any.whl → 2025.10.3a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,12 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  import dateutil.parser as dateparse
9
- from more_itertools import batched, flatten
9
+ from more_itertools import batched
10
10
  from render_engine_parser import BasePageParser
11
11
  from slugify import slugify
12
12
 
13
+ from render_engine.content_managers import ContentManager, FileContentManager
14
+
13
15
  from ._base_object import BaseObject
14
16
  from .archive import Archive
15
17
  from .feeds import RSSFeed
@@ -56,6 +58,9 @@ class Collection(BaseObject):
56
58
  title: str
57
59
  template: str | None
58
60
  archive_template str | None: The template to use for the archive pages.
61
+ ContentManager: type[ContentManager] | None = FileContentManager
62
+ content_manager: ContentManager
63
+ content_manager_extras: dict[str, Any]: kwargs to pass to the ContentManager when instantiating
59
64
 
60
65
  Methods:
61
66
 
@@ -84,6 +89,8 @@ class Collection(BaseObject):
84
89
  template_vars: dict[str, Any]
85
90
  template: str | None
86
91
  plugin_manager: PluginManager | None
92
+ ContentManager: type[ContentManager] | None = FileContentManager
93
+ content_manager_extras: dict[str, Any]
87
94
 
88
95
  def __init__(
89
96
  self,
@@ -102,9 +109,16 @@ class Collection(BaseObject):
102
109
  self.title = self._title
103
110
  self.template_vars = getattr(self, "template_vars", {})
104
111
 
105
- def iter_content_path(self):
106
- """Iterate through in the collection's content path."""
107
- return flatten([Path(self.content_path).glob(suffix) for suffix in self.include_suffixes])
112
+ cm_extras = {
113
+ "content_path": getattr(self, "content_path", None),
114
+ "include_suffixes": getattr(self, "include_suffixes", None),
115
+ "collection": self,
116
+ }
117
+ if hasattr(self, "content_manager_extras"):
118
+ cm_extras.update(self.content_manager_extras)
119
+ self.content_manager = self.ContentManager(**cm_extras)
120
+ if hasattr(self, "pages"):
121
+ self.content_manager.pages = self.pages
108
122
 
109
123
  def get_page(
110
124
  self,
@@ -116,8 +130,6 @@ class Collection(BaseObject):
116
130
  Parser=self.Parser,
117
131
  )
118
132
 
119
- if getattr(self, "_pm", None):
120
- _page.register_plugins(self.plugins, **self.plugin_settings)
121
133
  _page.parser_extras = getattr(self, "parser_extras", {})
122
134
  _page.routes = self.routes
123
135
  _page.template = getattr(self, "template", None)
@@ -162,7 +174,7 @@ class Collection(BaseObject):
162
174
  """
163
175
  try:
164
176
  return sorted(
165
- (page for page in self.__iter__()),
177
+ (page for page in self),
166
178
  key=self._sort_key(self.sort_by),
167
179
  reverse=self.sort_reverse,
168
180
  )
@@ -231,10 +243,7 @@ class Collection(BaseObject):
231
243
  return f"{__name__}"
232
244
 
233
245
  def __iter__(self):
234
- if not hasattr(self, "pages"):
235
- self.pages = [self.get_page(page) for page in self.iter_content_path()]
236
- for page in self.pages: # noqa: UP028
237
- yield page
246
+ yield from self.content_manager
238
247
 
239
248
  def _run_collection_plugins(self, site, hook_type: str):
240
249
  """
@@ -0,0 +1,7 @@
1
+ from .base_content_manager import ContentManager
2
+ from .file_content_manager import FileContentManager
3
+
4
+ __all__ = [
5
+ ContentManager,
6
+ FileContentManager,
7
+ ]
@@ -0,0 +1,16 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Generator, Iterable
3
+
4
+
5
+ class ContentManager(ABC):
6
+ """Base ContentManager abstract class"""
7
+
8
+ @property
9
+ @abstractmethod
10
+ def pages(self) -> Iterable:
11
+ """The Page objects managed by the content manager"""
12
+ ...
13
+
14
+ def __iter__(self) -> Generator:
15
+ """Iterator for the ContentManager"""
16
+ yield from self.pages
@@ -0,0 +1,36 @@
1
+ from collections.abc import Iterable
2
+ from pathlib import Path
3
+
4
+ from more_itertools import flatten
5
+
6
+ from render_engine.content_managers import ContentManager
7
+
8
+
9
+ class FileContentManager(ContentManager):
10
+ """Content manager for content stored on the file system as individual files"""
11
+
12
+ def __init__(
13
+ self,
14
+ content_path: Path | str,
15
+ collection,
16
+ include_suffixes: Iterable[str] = ("*.md", "*.html"),
17
+ **kwargs,
18
+ ):
19
+ self.content_path = content_path
20
+ self.include_suffixes = include_suffixes
21
+ self.collection = collection
22
+ self._pages = None
23
+
24
+ def iter_content_path(self):
25
+ """Iterate through in the collection's content path."""
26
+ return flatten([Path(self.content_path).glob(suffix) for suffix in self.include_suffixes])
27
+
28
+ @property
29
+ def pages(self) -> Iterable:
30
+ if self._pages is None:
31
+ self._pages = [self.collection.get_page(page) for page in self.iter_content_path()]
32
+ yield from self._pages
33
+
34
+ @pages.setter
35
+ def pages(self, value: Iterable):
36
+ self._pages = value
render_engine/page.py CHANGED
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  from pathlib import Path
3
4
  from typing import Any
@@ -10,6 +11,8 @@ from render_engine.themes import ThemeManager
10
11
  from ._base_object import BaseObject
11
12
  from .plugins import PluginManager
12
13
 
14
+ logger = logging.getLogger("Page")
15
+
13
16
 
14
17
  class BasePage(BaseObject):
15
18
  """
@@ -28,6 +31,8 @@ class BasePage(BaseObject):
28
31
  extension (str): The file extension for the page. Defaults to ".html".
29
32
  routes (list[str] | Path): The list of routes for the page. Defaults to ["./"].
30
33
  template (str | Template | None): The template to use for rendering the page.
34
+ site: The Site object that owns the page.
35
+ no_prerender: Flag to not prerender the content
31
36
  """
32
37
 
33
38
  extension: str = ".html"
@@ -36,7 +41,8 @@ class BasePage(BaseObject):
36
41
  rendered_content: str | None
37
42
  _reference: str = "_slug"
38
43
  plugin_manager: PluginManager | None
39
- site = None
44
+ site = None # This is a Site but circular imports so we can't actually type hint it.
45
+ no_prerender: bool = False
40
46
 
41
47
  @property
42
48
  def _content(self) -> any:
@@ -69,20 +75,35 @@ class BasePage(BaseObject):
69
75
  return f"/{route}/{self.path_name}"
70
76
 
71
77
  def _render_from_template(self, template: Template, **kwargs) -> str:
72
- """Renders the page from a template."""
78
+ """
79
+ Renders the page from a template.
80
+
81
+ If the content looks like a template that
82
+
83
+ :param template: Template to render
84
+ :param **kwargs: Data to pass into the template for rendering.
85
+ :return: The rendered page
86
+ """
73
87
  template_data = {"data": self._data, "content": self._content}
74
88
  if site := getattr(self, "site", None):
75
89
  template_data["site_map"] = site.site_map
76
- if isinstance(self._content, str) and re.search(r"{{.*}}", self._content):
90
+ if not self.no_prerender and isinstance(self._content, str) and re.search(r"{{.*?site_map.*?}}", self._content):
77
91
  # If the content looks like a template, try to render it.
78
- content_template = Template(self._content)
79
- template_data["content"] = content_template.render(
80
- **{
81
- **self.to_dict(),
82
- **template_data,
83
- **kwargs,
84
- }
85
- )
92
+ try:
93
+ content_template = Template(self._content)
94
+ except Exception:
95
+ logger.info(f"Failed to parse {repr(self.path_name)} as a template.", exc_info=True)
96
+ else:
97
+ try:
98
+ template_data["content"] = content_template.render(
99
+ **{
100
+ **self.to_dict(),
101
+ **template_data,
102
+ **kwargs,
103
+ }
104
+ )
105
+ except Exception:
106
+ logger.info(f"Failed to pre-render {repr(self.path_name)}.", exc_info=True)
86
107
 
87
108
  return template.render(
88
109
  **{
render_engine/site_map.py CHANGED
@@ -22,7 +22,7 @@ class SiteMapEntry:
22
22
  self._route = f"/{route.lstrip('/')}/{self.path_name}" if from_collection else f"/{self.path_name}"
23
23
  self.entries = list()
24
24
  case Collection():
25
- self._route = f"/{route.lstrip('/')}"
25
+ self._route = f"/{entry.routes[0].lstrip('/')}"
26
26
  self.entries = [
27
27
  SiteMapEntry(collection_entry, self._route, from_collection=True) for collection_entry in entry
28
28
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2025.10.1a1
3
+ Version: 2025.10.3a1
4
4
  Summary: A Flexible Static Site Generator for Python
5
5
  Project-URL: homepage, https://github.com/render-engine/render-engine/
6
6
  Project-URL: repository, https://github.com/render-engine/render-engine/
@@ -4,17 +4,20 @@ render_engine/__main__.py,sha256=uI7aBBZz0qSDwwwD11nS5oltWsuLw9hStfYo8O1aNws,144
4
4
  render_engine/_base_object.py,sha256=DIyLdQ6gS4a0DP46zwGYkYIyewh8uRFznJmaUch7d8M,3546
5
5
  render_engine/archive.py,sha256=S3-kCmDNVKkEfKDKxcEk-sXkBD0vS0RDnFfPunYkU8g,2072
6
6
  render_engine/blog.py,sha256=f9GqFUFsta0KZnFhCiajobpfQyALqvgI5sbLm6zt1zw,1571
7
- render_engine/collection.py,sha256=rubgKV6z3LN1gCGdYsbwd023SVQdBNnSHnHqXhkn83g,10209
7
+ render_engine/collection.py,sha256=dAGyWdIoXr6S4GMSbPPin4Y5vLJR_onO_85-NEqRaWM,10599
8
8
  render_engine/engine.py,sha256=GOtUiq4ny5GHaLSCeH5u1Zk1JnWJVh63vK7etJiwS20,2843
9
9
  render_engine/feeds.py,sha256=i-VHsb6pRplMzaenBn6oeqh9yI_N4WVUAExPox6iJgw,921
10
10
  render_engine/hookspecs.py,sha256=GhOpw0zTQjfwWOFYYbJ4P7Cvq-oy1MmTPHmd90dr0kg,2292
11
11
  render_engine/links.py,sha256=pKmQMTz8-yGX8IecHcrlF3Dkejk7cptaO3qCkQiHB9I,2560
12
- render_engine/page.py,sha256=mIKdOmyQ1zRcaoZej9hMV2xajH_frXllaO-ZpZMzCtM,8091
12
+ render_engine/page.py,sha256=l6sKWNJ4gBtC_ONEc0u479q3znL-8Q7U_phNqXqmh6w,8988
13
13
  render_engine/plugins.py,sha256=NXM8QTbbRV-DwgpQRoIhILijJBN4SyYg2Rkk1LUAuZM,4703
14
14
  render_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  render_engine/site.py,sha256=URvG7OQSgeFOcrZwrD8vHLUWa6UxDBxIP-UMeLLqhZM,12936
16
- render_engine/site_map.py,sha256=cVnCekhDqPKXCSslI8eg-_4cRHN_yWuWo82sjwKzAGI,5412
16
+ render_engine/site_map.py,sha256=I1p_yMDMy1jpIivgNgZptnsxZa8NkcrpciVJE3DlFlQ,5422
17
17
  render_engine/themes.py,sha256=TFG1rd34QCBvBWfeDbawgsn6kprmjsDTa1pdDSwDMic,4207
18
+ render_engine/content_managers/__init__.py,sha256=z1x99J0GNcfqYFrugD0EleiZR6b-sfM6zViDTH1iF0s,161
19
+ render_engine/content_managers/base_content_manager.py,sha256=LSGBRkGbBHMI-oRyoXVTfr5Me-8vYws7NV961VPOnxI,414
20
+ render_engine/content_managers/file_content_manager.py,sha256=yMLT7xBhCw4NLfKuwx-6T_U9I03UTsyTAFz4uxE1uu8,1102
18
21
  render_engine/extras/__init__.py,sha256=L4jr4A7Jl-ODnSx1q2fP3_dBo37Dw6yepNRddu1nFNo,72
19
22
  render_engine/parsers/markdown.py,sha256=0jpixCaoHaL0IRSvFIljJIRCvFkXoKTEYQNK38LwMDU,287
20
23
  render_engine/render_engine_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -32,7 +35,7 @@ render_engine/render_engine_templates/base_templates/_page.html,sha256=jjrY2BAwl
32
35
  render_engine/render_engine_templates/components/footer.html,sha256=HkPGGhfN0HcYm7t8zgXWCQ3bsCbT8FxT4_n2-9e1zUE,74
33
36
  render_engine/render_engine_templates/components/page_title.html,sha256=l8aE1TY94UPHXHqAyy6jv4IoN2Hv9cbrTPh7ILkMyxg,137
34
37
  render_engine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- render_engine-2025.10.1a1.dist-info/METADATA,sha256=X7uol4Ld4scOzJSZ0jYobZmf_AyTwJ53DAwt1hM488s,11895
36
- render_engine-2025.10.1a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- render_engine-2025.10.1a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
38
- render_engine-2025.10.1a1.dist-info/RECORD,,
38
+ render_engine-2025.10.3a1.dist-info/METADATA,sha256=906rgnnlpsL8upgcEtC4-JYstfkIHiqBK2QU6f_fMaw,11895
39
+ render_engine-2025.10.3a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
+ render_engine-2025.10.3a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
41
+ render_engine-2025.10.3a1.dist-info/RECORD,,