render-engine 2026.1.1a1__py3-none-any.whl → 2026.2.1a1__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.
- render_engine/__version__.py +2 -2
- render_engine/collection.py +26 -12
- render_engine/content_managers/base_content_manager.py +33 -2
- render_engine/content_managers/file_content_manager.py +28 -2
- render_engine/engine.py +21 -12
- render_engine/feeds.py +7 -0
- render_engine/page.py +27 -17
- render_engine/plugins.py +1 -1
- render_engine/render_engine_templates/base_collection_path.md +1 -1
- render_engine/site.py +14 -23
- render_engine/site_map.py +3 -2
- render_engine/themes.py +24 -6
- {render_engine-2026.1.1a1.dist-info → render_engine-2026.2.1a1.dist-info}/METADATA +23 -30
- {render_engine-2026.1.1a1.dist-info → render_engine-2026.2.1a1.dist-info}/RECORD +16 -18
- {render_engine-2026.1.1a1.dist-info → render_engine-2026.2.1a1.dist-info}/WHEEL +1 -1
- render_engine/__main__.py +0 -6
- render_engine/extras/__init__.py +0 -3
- {render_engine-2026.1.1a1.dist-info → render_engine-2026.2.1a1.dist-info}/top_level.txt +0 -0
render_engine/__version__.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2026.
|
|
32
|
-
__version_tuple__ = version_tuple = (2026,
|
|
31
|
+
__version__ = version = '2026.2.1a1'
|
|
32
|
+
__version_tuple__ = version_tuple = (2026, 2, 1, 'a1')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
render_engine/collection.py
CHANGED
|
@@ -2,20 +2,19 @@ import copy
|
|
|
2
2
|
import datetime
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from collections.abc import Callable, Generator
|
|
5
|
+
from collections.abc import Callable, Generator, Iterable
|
|
6
6
|
from multiprocessing.pool import ThreadPool
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
import dateutil.parser as dateparse
|
|
11
11
|
from more_itertools import batched
|
|
12
12
|
from render_engine_parser import BasePageParser
|
|
13
13
|
from slugify import slugify
|
|
14
14
|
|
|
15
|
-
from render_engine.content_managers import ContentManager, FileContentManager
|
|
16
|
-
|
|
17
15
|
from ._base_object import BaseObject
|
|
18
16
|
from .archive import Archive
|
|
17
|
+
from .content_managers import ContentManager, FileContentManager
|
|
19
18
|
from .feeds import RSSFeed
|
|
20
19
|
from .page import Page
|
|
21
20
|
from .plugins import PluginManager
|
|
@@ -82,16 +81,17 @@ class Collection(BaseObject):
|
|
|
82
81
|
feed_title: str
|
|
83
82
|
include_suffixes: list[str] = ["*.md", "*.html"]
|
|
84
83
|
items_per_page: int | None
|
|
85
|
-
Parser: BasePageParser = BasePageParser
|
|
84
|
+
Parser: type[BasePageParser] = BasePageParser
|
|
86
85
|
parser_extras: dict[str, Any]
|
|
87
86
|
required_themes: list[Callable]
|
|
88
87
|
routes: list[str | Path] = ["./"]
|
|
88
|
+
site = None
|
|
89
89
|
sort_by: str | list = "_title"
|
|
90
90
|
sort_reverse: bool = False
|
|
91
91
|
template_vars: dict[str, Any]
|
|
92
92
|
template: str | None
|
|
93
93
|
plugin_manager: PluginManager | None
|
|
94
|
-
ContentManager: type[ContentManager]
|
|
94
|
+
ContentManager: type[ContentManager] = FileContentManager
|
|
95
95
|
content_manager_extras: dict[str, Any]
|
|
96
96
|
|
|
97
97
|
def __init__(
|
|
@@ -120,7 +120,7 @@ class Collection(BaseObject):
|
|
|
120
120
|
cm_extras.update(self.content_manager_extras)
|
|
121
121
|
self.content_manager = self.ContentManager(**cm_extras)
|
|
122
122
|
if hasattr(self, "pages"):
|
|
123
|
-
self.content_manager.pages = self.pages
|
|
123
|
+
self.content_manager.pages = cast(Iterable, self.pages)
|
|
124
124
|
|
|
125
125
|
def get_page(
|
|
126
126
|
self,
|
|
@@ -247,6 +247,9 @@ class Collection(BaseObject):
|
|
|
247
247
|
def __iter__(self):
|
|
248
248
|
yield from self.content_manager
|
|
249
249
|
|
|
250
|
+
def __len__(self):
|
|
251
|
+
return len(self.content_manager)
|
|
252
|
+
|
|
250
253
|
@property
|
|
251
254
|
def all_content(self) -> Generator:
|
|
252
255
|
"""All of the content that is associated with the Collection including Pages, Feed, and Archives"""
|
|
@@ -273,6 +276,7 @@ class Collection(BaseObject):
|
|
|
273
276
|
:param site: The site object triggering the call
|
|
274
277
|
:param hook_type: The hook to run
|
|
275
278
|
"""
|
|
279
|
+
self.plugin_manager = cast(PluginManager, self.plugin_manager)
|
|
276
280
|
if not getattr(self.plugin_manager, "_pm", None) or not self.plugin_manager.plugins:
|
|
277
281
|
return
|
|
278
282
|
try:
|
|
@@ -282,18 +286,24 @@ class Collection(BaseObject):
|
|
|
282
286
|
return
|
|
283
287
|
method(collection=self, site=site, settings=self.plugin_manager.plugin_settings)
|
|
284
288
|
|
|
285
|
-
def _render(self, entry):
|
|
289
|
+
def _render(self, entry: BaseObject):
|
|
286
290
|
"""
|
|
287
291
|
Renders 1 entry in the Collection
|
|
288
292
|
|
|
289
293
|
:param entry: The entry to process
|
|
290
294
|
"""
|
|
291
295
|
if not isinstance(entry, RSSFeed) and not isinstance(entry, Archive):
|
|
292
|
-
entry.plugin_manager = copy.deepcopy(self.plugin_manager)
|
|
296
|
+
entry.plugin_manager: PluginManager = copy.deepcopy(self.plugin_manager)
|
|
293
297
|
|
|
298
|
+
# Circular imports. Need to be handled here.
|
|
299
|
+
from .page import BasePage
|
|
300
|
+
from .site import Site
|
|
301
|
+
|
|
302
|
+
entry = cast(BasePage, entry)
|
|
303
|
+
self = cast(Collection, self)
|
|
304
|
+
self.site = cast(Site, self.site)
|
|
294
305
|
entry.site = self.site
|
|
295
|
-
|
|
296
|
-
entry.render(route, self.site.theme_manager)
|
|
306
|
+
entry.render(self.site.theme_manager)
|
|
297
307
|
|
|
298
308
|
def render(self) -> None:
|
|
299
309
|
"""Iterate through Pages and Check for Archives and Feeds"""
|
|
@@ -307,7 +317,11 @@ class Collection(BaseObject):
|
|
|
307
317
|
pass
|
|
308
318
|
|
|
309
319
|
def create_entry(
|
|
310
|
-
self,
|
|
320
|
+
self,
|
|
321
|
+
filepath: Path | None = None,
|
|
322
|
+
editor: str | None = None,
|
|
323
|
+
content: str | None = None,
|
|
324
|
+
metadata: dict | None = None,
|
|
311
325
|
) -> str:
|
|
312
326
|
"""
|
|
313
327
|
Create a new entry for the Collection
|
|
@@ -12,11 +12,42 @@ class ContentManager(ABC):
|
|
|
12
12
|
"""The Page objects managed by the content manager"""
|
|
13
13
|
...
|
|
14
14
|
|
|
15
|
+
@pages.setter
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def pages(self, value: Iterable):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def __len__(self):
|
|
21
|
+
return len(list(self.pages))
|
|
22
|
+
|
|
15
23
|
def __iter__(self) -> Generator:
|
|
16
24
|
"""Iterator for the ContentManager"""
|
|
17
25
|
yield from self.pages
|
|
18
26
|
|
|
19
27
|
@abstractmethod
|
|
20
|
-
def create_entry(
|
|
28
|
+
def create_entry(
|
|
29
|
+
self,
|
|
30
|
+
filepath: Path | None = None,
|
|
31
|
+
editor: str | None = None,
|
|
32
|
+
metadata: dict | None = None,
|
|
33
|
+
content: str | None = None,
|
|
34
|
+
):
|
|
21
35
|
"""Create a new entry"""
|
|
22
|
-
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def find_entry(self, **kwargs):
|
|
39
|
+
"""
|
|
40
|
+
Find an entry
|
|
41
|
+
|
|
42
|
+
:param kwargs: List of attributes to search by
|
|
43
|
+
:return: Page if it was found otherwise None
|
|
44
|
+
"""
|
|
45
|
+
for page in self:
|
|
46
|
+
if all(getattr(page, attr, None) == value for attr, value in kwargs.items()):
|
|
47
|
+
return page
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def update_entry(self, *, page, **kwargs):
|
|
52
|
+
"""Update an entry"""
|
|
53
|
+
pass
|
|
@@ -37,7 +37,12 @@ class FileContentManager(ContentManager):
|
|
|
37
37
|
self._pages = value
|
|
38
38
|
|
|
39
39
|
def create_entry(
|
|
40
|
-
self,
|
|
40
|
+
self,
|
|
41
|
+
filepath: Path | None = None,
|
|
42
|
+
editor: str | None = None,
|
|
43
|
+
metadata: dict | None = None,
|
|
44
|
+
content: str | None = None,
|
|
45
|
+
update: bool = False,
|
|
41
46
|
) -> str:
|
|
42
47
|
"""
|
|
43
48
|
Create a new entry for the Collection
|
|
@@ -46,12 +51,33 @@ class FileContentManager(ContentManager):
|
|
|
46
51
|
:param editor: Editor to open to edit the entry.
|
|
47
52
|
:param content: The content for the entry
|
|
48
53
|
:param metadata: Metadata for the new entry
|
|
54
|
+
:param update: Allow overwriting the existing file
|
|
49
55
|
"""
|
|
50
56
|
if not filepath:
|
|
51
57
|
raise ValueError("filepath needs to be specified.")
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
if not update and filepath.exists():
|
|
60
|
+
raise RuntimeError(f"File at {filepath} exists and update is disabled.")
|
|
61
|
+
|
|
62
|
+
parsed_content = self.collection.Parser.create_entry(content=content, **(metadata or {}))
|
|
54
63
|
filepath.write_text(parsed_content)
|
|
55
64
|
if editor:
|
|
56
65
|
subprocess.run([editor, filepath])
|
|
57
66
|
return f"New entry created at {filepath} ."
|
|
67
|
+
|
|
68
|
+
def update_entry(self, page, *, content: str | None = None, **kwargs) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Update an entry
|
|
71
|
+
|
|
72
|
+
:param page: Page object to update
|
|
73
|
+
:param content: Content for the updated page
|
|
74
|
+
:param kwargs: Attributes to be included in the updated page
|
|
75
|
+
:return: String indicating that the page was updated.
|
|
76
|
+
"""
|
|
77
|
+
self.create_entry(filepath=page.content_path, metadata=kwargs, content=content, update=True)
|
|
78
|
+
if self._pages:
|
|
79
|
+
self._pages = [
|
|
80
|
+
existing_page for existing_page in self._pages if page.content_path != existing_page.content_path
|
|
81
|
+
]
|
|
82
|
+
self._pages.append(self.collection.get_page(page.content_path))
|
|
83
|
+
return f"Entry at {page.content_path} updated."
|
render_engine/engine.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from email.utils import format_datetime as fmt_datetime
|
|
3
|
+
from typing import cast
|
|
3
4
|
from urllib.parse import urljoin
|
|
4
5
|
|
|
5
6
|
from dateutil.parser import parse
|
|
@@ -13,7 +14,9 @@ from jinja2 import (
|
|
|
13
14
|
select_autoescape,
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
from ._base_object import BaseObject
|
|
16
18
|
from .collection import Collection
|
|
19
|
+
from .page import BasePage
|
|
17
20
|
|
|
18
21
|
render_engine_templates_loader = ChoiceLoader(
|
|
19
22
|
[
|
|
@@ -56,10 +59,11 @@ def format_datetime(
|
|
|
56
59
|
datetime_format: str | None = None,
|
|
57
60
|
) -> str:
|
|
58
61
|
"""Parse information from the given class object."""
|
|
62
|
+
format: str
|
|
59
63
|
if datetime_format:
|
|
60
64
|
format = datetime_format
|
|
61
65
|
else:
|
|
62
|
-
format = env.globals.get("DATETIME_FORMAT", "%Y-%m-%d")
|
|
66
|
+
format = cast(str, env.globals.get("DATETIME_FORMAT", "%Y-%m-%d"))
|
|
63
67
|
|
|
64
68
|
return value.strftime(format)
|
|
65
69
|
|
|
@@ -69,7 +73,8 @@ engine.filters["format_datetime"] = format_datetime
|
|
|
69
73
|
|
|
70
74
|
@pass_environment
|
|
71
75
|
def to_absolute(env: Environment, url: str) -> str:
|
|
72
|
-
|
|
76
|
+
site_url: str = cast(str, env.globals.get("SITE_URL"))
|
|
77
|
+
return str(urljoin(site_url, url))
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
engine.filters["to_absolute"] = to_absolute
|
|
@@ -78,10 +83,11 @@ engine.filters["to_absolute"] = to_absolute
|
|
|
78
83
|
@pass_environment
|
|
79
84
|
def feed_url(env: Environment, value: str) -> str:
|
|
80
85
|
"""Returns the URL for the collections feed"""
|
|
81
|
-
routes = env.globals.get("routes")
|
|
86
|
+
routes = cast(dict[str, BaseObject], env.globals.get("routes"))
|
|
82
87
|
|
|
83
88
|
if routes:
|
|
84
|
-
|
|
89
|
+
route = cast(Collection, routes[value])
|
|
90
|
+
return route.feed.url_for()
|
|
85
91
|
|
|
86
92
|
else:
|
|
87
93
|
raise ValueError("No Route Found")
|
|
@@ -93,21 +99,24 @@ engine.filters["feed_url"] = feed_url
|
|
|
93
99
|
@pass_environment
|
|
94
100
|
def url_for(env: Environment, value: str, page: int = 0) -> str:
|
|
95
101
|
"""Look for the route in the route_list and return the url for the page."""
|
|
96
|
-
routes = env.globals.get("routes")
|
|
97
|
-
|
|
102
|
+
routes = cast(dict[str, BaseObject], env.globals.get("routes"))
|
|
103
|
+
|
|
104
|
+
if "." in value:
|
|
105
|
+
collection, route = value.split(".", maxsplit=1)
|
|
98
106
|
|
|
99
|
-
if len(route) == 2 and isinstance(route, list):
|
|
100
|
-
collection, route = route
|
|
101
107
|
if collection := routes.get(collection, None):
|
|
108
|
+
collection = cast(Collection, collection)
|
|
102
109
|
for page in collection:
|
|
103
110
|
if getattr(page, page._reference) == route:
|
|
104
111
|
return page.url_for()
|
|
105
112
|
|
|
106
113
|
else:
|
|
107
|
-
route
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
route: BaseObject | None
|
|
115
|
+
match route := routes.get(value):
|
|
116
|
+
case Collection():
|
|
117
|
+
return list(route.archives)[page].url_for()
|
|
118
|
+
case BasePage():
|
|
119
|
+
return route.url_for()
|
|
111
120
|
|
|
112
121
|
raise ValueError(f"{value} is not a valid route.")
|
|
113
122
|
|
render_engine/feeds.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Feed Objects for Generating RSS Feeds
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from render_engine_parser import BasePageParser
|
|
6
|
+
|
|
5
7
|
from .page import BasePage
|
|
6
8
|
|
|
7
9
|
|
|
@@ -35,3 +37,8 @@ class RSSFeed(BasePage):
|
|
|
35
37
|
|
|
36
38
|
template = "rss2.0.xml"
|
|
37
39
|
extension: str = ".rss"
|
|
40
|
+
Parser: type[BasePageParser] = BasePageParser
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.pages: list = list()
|
|
44
|
+
self.slug: str = self._slug or self.__class__.__name__
|
render_engine/page.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
from jinja2 import Environment, Template
|
|
7
7
|
from render_engine_parser.base_parsers import BasePageParser
|
|
@@ -43,14 +43,15 @@ class BasePage(BaseObject):
|
|
|
43
43
|
plugin_manager: PluginManager | None
|
|
44
44
|
site = None # This is a Site but circular imports so we can't actually type hint it.
|
|
45
45
|
no_prerender: bool = False
|
|
46
|
+
collection: dict | None = None
|
|
46
47
|
|
|
47
48
|
@property
|
|
48
|
-
def _content(self) ->
|
|
49
|
+
def _content(self) -> Any:
|
|
49
50
|
"""returns the content of the page."""
|
|
50
51
|
return getattr(self, "content", None)
|
|
51
52
|
|
|
52
53
|
@property
|
|
53
|
-
def _data(self) ->
|
|
54
|
+
def _data(self) -> Any:
|
|
54
55
|
"""returns the content of the page."""
|
|
55
56
|
return getattr(self, "data", None)
|
|
56
57
|
|
|
@@ -149,20 +150,27 @@ class BasePage(BaseObject):
|
|
|
149
150
|
def __repr__(self) -> str:
|
|
150
151
|
return f"<Page: {self._title}>"
|
|
151
152
|
|
|
152
|
-
def render(self,
|
|
153
|
+
def render(self, theme_manager: ThemeManager) -> int:
|
|
153
154
|
"""Render the page to the file system"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
settings = {**self.site.plugin_manager.plugin_settings, "route": route}
|
|
159
|
-
pm.hook.render_content(page=self, settings=settings, site=self.site)
|
|
160
|
-
self.rendered_content = self._render_content(theme_manager.engine)
|
|
161
|
-
# pass the route to the plugin settings
|
|
162
|
-
if pm is not None:
|
|
163
|
-
pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
|
|
155
|
+
rc = 0
|
|
156
|
+
from .site import Site
|
|
157
|
+
|
|
158
|
+
site: Site = cast(Site, self.site)
|
|
164
159
|
|
|
165
|
-
|
|
160
|
+
for route in self.routes:
|
|
161
|
+
path = Path(site.output_path) / Path(route) / Path(self.path_name)
|
|
162
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
settings = dict()
|
|
164
|
+
if (pm := getattr(self, "plugin_manager", None)) and pm is not None:
|
|
165
|
+
settings = {**site.plugin_manager.plugin_settings, "route": route}
|
|
166
|
+
pm.hook.render_content(page=self, settings=settings, site=self.site)
|
|
167
|
+
self.rendered_content = self._render_content(theme_manager.engine)
|
|
168
|
+
# pass the route to the plugin settings
|
|
169
|
+
if pm is not None:
|
|
170
|
+
pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
|
|
171
|
+
|
|
172
|
+
rc += path.write_text(self.rendered_content)
|
|
173
|
+
return rc
|
|
166
174
|
|
|
167
175
|
|
|
168
176
|
class Page(BasePage):
|
|
@@ -225,11 +233,11 @@ class Page(BasePage):
|
|
|
225
233
|
Defaults to `BasePageParser`.
|
|
226
234
|
"""
|
|
227
235
|
if Parser:
|
|
228
|
-
self.Parser = Parser
|
|
236
|
+
self.Parser = cast(type[BasePageParser], Parser)
|
|
229
237
|
|
|
230
238
|
# Parse Content from the Content Path or the Content
|
|
231
239
|
if content_path := (content_path or getattr(self, "content_path", None)):
|
|
232
|
-
self.metadata, self.content = self.Parser.parse_content_path(content_path)
|
|
240
|
+
self.metadata, self.content = self.Parser.parse_content_path(str(content_path))
|
|
233
241
|
|
|
234
242
|
elif content := (content or getattr(self, "content", None)):
|
|
235
243
|
self.metadata, self.content = self.Parser.parse_content(content)
|
|
@@ -242,6 +250,8 @@ class Page(BasePage):
|
|
|
242
250
|
for key, val in self.metadata.items():
|
|
243
251
|
setattr(self, key.lower(), val)
|
|
244
252
|
|
|
253
|
+
self.content_path = content_path
|
|
254
|
+
|
|
245
255
|
@property
|
|
246
256
|
def _content(self) -> Any:
|
|
247
257
|
"""
|
render_engine/plugins.py
CHANGED
render_engine/site.py
CHANGED
|
@@ -65,7 +65,7 @@ class Site:
|
|
|
65
65
|
def __init__(
|
|
66
66
|
self,
|
|
67
67
|
) -> None:
|
|
68
|
-
self.plugin_manager = PluginManager()
|
|
68
|
+
self.plugin_manager: PluginManager = PluginManager()
|
|
69
69
|
self.theme_manager = ThemeManager(
|
|
70
70
|
engine=engine,
|
|
71
71
|
output_path=self._output_path,
|
|
@@ -75,8 +75,7 @@ class Site:
|
|
|
75
75
|
self.site_settings: dict = {}
|
|
76
76
|
self.subcollections: dict[str, list] = {"pages": []}
|
|
77
77
|
self.theme_manager.engine.globals.update(self.site_vars)
|
|
78
|
-
|
|
79
|
-
self.theme_manager.engine.loader.loaders.insert(0, FileSystemLoader(self._template_path))
|
|
78
|
+
self.theme_manager.add_loader(0, FileSystemLoader(self._template_path))
|
|
80
79
|
self._site_map = None
|
|
81
80
|
|
|
82
81
|
@property
|
|
@@ -96,7 +95,7 @@ class Site:
|
|
|
96
95
|
self.theme_manager.static_paths = static_paths
|
|
97
96
|
|
|
98
97
|
@property
|
|
99
|
-
def site_map(self) -> SiteMap:
|
|
98
|
+
def site_map(self) -> SiteMap | None:
|
|
100
99
|
return self._site_map
|
|
101
100
|
|
|
102
101
|
def update_site_vars(self, **kwargs) -> None:
|
|
@@ -179,7 +178,7 @@ class Site:
|
|
|
179
178
|
self.route_list[_Collection._slug] = _Collection
|
|
180
179
|
return _Collection
|
|
181
180
|
|
|
182
|
-
def page(self, _page: Page) -> Page:
|
|
181
|
+
def page(self, _page: type[Page]) -> Page:
|
|
183
182
|
"""
|
|
184
183
|
Add the page to the route list to be rendered later.
|
|
185
184
|
Also remaps `title` in case the user wants to use it in the template rendering.
|
|
@@ -228,22 +227,17 @@ class Site:
|
|
|
228
227
|
# load themes in the ChoiceLoader/FileLoader
|
|
229
228
|
for theme_prefix, theme_loader in self.theme_manager.prefix.items():
|
|
230
229
|
logging.info(f"loading theme: {theme_prefix}")
|
|
231
|
-
|
|
232
|
-
self.theme_manager.engine.loader.loaders.insert(-1, theme_loader)
|
|
230
|
+
self.theme_manager.add_loader(-1, theme_loader)
|
|
233
231
|
# load themes in the PrefixLoader
|
|
234
|
-
|
|
235
|
-
self.theme_manager.engine.loader.loaders.insert(-1, PrefixLoader(self.theme_manager.prefix))
|
|
232
|
+
self.theme_manager.add_loader(-1, PrefixLoader(self.theme_manager.prefix))
|
|
236
233
|
|
|
237
234
|
@property
|
|
238
235
|
def template_path(self) -> str:
|
|
239
|
-
|
|
240
|
-
return self.theme_manager.engine.loader.loaders[0].searchpath[0]
|
|
241
|
-
return ""
|
|
236
|
+
return self.theme_manager.template_path
|
|
242
237
|
|
|
243
238
|
@template_path.setter
|
|
244
239
|
def template_path(self, template_path: str) -> None:
|
|
245
|
-
|
|
246
|
-
self.theme_manager.engine.loader.loaders.insert(0, FileSystemLoader(template_path))
|
|
240
|
+
self.theme_manager.add_loader(0, FileSystemLoader(template_path))
|
|
247
241
|
|
|
248
242
|
def render(self) -> None:
|
|
249
243
|
"""
|
|
@@ -287,7 +281,7 @@ class Site:
|
|
|
287
281
|
self.plugin_manager.hook.pre_build_site(
|
|
288
282
|
site=self,
|
|
289
283
|
settings=self.plugin_manager.plugin_settings,
|
|
290
|
-
)
|
|
284
|
+
)
|
|
291
285
|
|
|
292
286
|
self.load_themes()
|
|
293
287
|
self.theme_manager.engine.globals.update(self.site_vars)
|
|
@@ -306,14 +300,11 @@ class Site:
|
|
|
306
300
|
args = []
|
|
307
301
|
match entry:
|
|
308
302
|
case Page():
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# self._render_output(route, entry)
|
|
316
|
-
args = [route, self.theme_manager]
|
|
303
|
+
progress.update(
|
|
304
|
+
task_add_route,
|
|
305
|
+
description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
|
|
306
|
+
)
|
|
307
|
+
args = [self.theme_manager]
|
|
317
308
|
case Collection():
|
|
318
309
|
progress.update(
|
|
319
310
|
task_add_route,
|
render_engine/site_map.py
CHANGED
|
@@ -16,13 +16,14 @@ class SiteMapEntry:
|
|
|
16
16
|
self.slug = entry._slug
|
|
17
17
|
self.title = entry._title
|
|
18
18
|
self.path_name = entry.path_name
|
|
19
|
+
route = str(route)
|
|
19
20
|
match entry:
|
|
20
21
|
case Page():
|
|
21
22
|
# For a base page the _route created if we use the route is invalid - just use the path_name
|
|
22
23
|
self._route = f"/{route.lstrip('/')}/{self.path_name}" if from_collection else f"/{self.path_name}"
|
|
23
24
|
self.entries = list()
|
|
24
25
|
case Collection():
|
|
25
|
-
self._route = f"/{entry.routes[0].lstrip('/')}"
|
|
26
|
+
self._route = f"/{str(entry.routes[0]).lstrip('/')}"
|
|
26
27
|
self.entries = [
|
|
27
28
|
SiteMapEntry(collection_entry, self._route, from_collection=True) for collection_entry in entry
|
|
28
29
|
]
|
|
@@ -72,7 +73,7 @@ class SiteMap:
|
|
|
72
73
|
self,
|
|
73
74
|
value: str,
|
|
74
75
|
attr: str = "slug",
|
|
75
|
-
collection: str = None,
|
|
76
|
+
collection: str | None = None,
|
|
76
77
|
full_search: bool = False,
|
|
77
78
|
) -> SiteMapEntry | None:
|
|
78
79
|
"""
|
render_engine/themes.py
CHANGED
|
@@ -3,9 +3,10 @@ import logging
|
|
|
3
3
|
import pathlib
|
|
4
4
|
import shutil
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
6
7
|
|
|
7
8
|
import slugify
|
|
8
|
-
from jinja2 import BaseLoader, Environment
|
|
9
|
+
from jinja2 import BaseLoader, ChoiceLoader, Environment, FileSystemLoader
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclasses.dataclass
|
|
@@ -95,11 +96,15 @@ class ThemeManager:
|
|
|
95
96
|
if theme.template_globals:
|
|
96
97
|
for key, value in theme.template_globals.items():
|
|
97
98
|
if isinstance(value, set) and isinstance(self.engine.globals.get(key), set):
|
|
98
|
-
self.engine.globals.
|
|
99
|
-
|
|
100
|
-
self.engine.globals[key]
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
entry: set = cast(set, self.engine.globals.get(key, set()))
|
|
100
|
+
entry.update(value)
|
|
101
|
+
self.engine.globals[key] = entry
|
|
102
|
+
match self.engine.globals.get(key):
|
|
103
|
+
case set():
|
|
104
|
+
entry: set = cast(set, self.engine.globals[key])
|
|
105
|
+
entry.add(value)
|
|
106
|
+
case _:
|
|
107
|
+
self.engine.globals[key] = value
|
|
103
108
|
|
|
104
109
|
def _render_static(self) -> None:
|
|
105
110
|
"""Copies a Static Directory to the output folder"""
|
|
@@ -111,3 +116,16 @@ class ThemeManager:
|
|
|
111
116
|
pathlib.Path(self.output_path) / pathlib.Path(static_path).name,
|
|
112
117
|
dirs_exist_ok=True,
|
|
113
118
|
)
|
|
119
|
+
|
|
120
|
+
def add_loader(self, idx: int, loader: BaseLoader):
|
|
121
|
+
"""Add a loader to the list of loaders"""
|
|
122
|
+
if self.engine.loader is not None and isinstance(self.engine.loader, ChoiceLoader):
|
|
123
|
+
loaders: list[BaseLoader] = cast(list[BaseLoader], self.engine.loader.loaders)
|
|
124
|
+
loaders.insert(idx, loader)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def template_path(self) -> str:
|
|
128
|
+
if self.engine.loader is not None and isinstance(self.engine.loader, ChoiceLoader):
|
|
129
|
+
loader: FileSystemLoader = cast(FileSystemLoader, self.engine.loader.loaders[0])
|
|
130
|
+
return loader.searchpath[0]
|
|
131
|
+
return ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: render_engine
|
|
3
|
-
Version: 2026.
|
|
3
|
+
Version: 2026.2.1a1
|
|
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/
|
|
@@ -21,22 +21,6 @@ Provides-Extra: cli
|
|
|
21
21
|
Requires-Dist: render-engine-cli; extra == "cli"
|
|
22
22
|
Provides-Extra: extras
|
|
23
23
|
Requires-Dist: render-engine-sitemap; extra == "extras"
|
|
24
|
-
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: cookiecutter; extra == "dev"
|
|
26
|
-
Requires-Dist: ephemeral-port-reserve; extra == "dev"
|
|
27
|
-
Requires-Dist: httpx; extra == "dev"
|
|
28
|
-
Requires-Dist: mkdocs; extra == "dev"
|
|
29
|
-
Requires-Dist: mkdocs-material; extra == "dev"
|
|
30
|
-
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
31
|
-
Requires-Dist: mypy; extra == "dev"
|
|
32
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
33
|
-
Requires-Dist: pymdown-extensions; extra == "dev"
|
|
34
|
-
Requires-Dist: pytest; extra == "dev"
|
|
35
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
36
|
-
Requires-Dist: pytest-mock; extra == "dev"
|
|
37
|
-
Requires-Dist: ruff; extra == "dev"
|
|
38
|
-
Requires-Dist: toml; extra == "dev"
|
|
39
|
-
Requires-Dist: watchfiles; extra == "dev"
|
|
40
24
|
|
|
41
25
|
<!-- markdownlint-disable -->
|
|
42
26
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
@@ -47,6 +31,7 @@ Requires-Dist: watchfiles; extra == "dev"
|
|
|
47
31
|
# Render Engine
|
|
48
32
|
|
|
49
33
|
[](https://github.com/kjaymiller/render_engine/actions/workflows/test.yml)
|
|
34
|
+

|
|
50
35
|
[](https://discord.gg/2xMQ4j4d8m)
|
|
51
36
|
|
|
52
37
|
## Learn More
|
|
@@ -59,9 +44,12 @@ Requires-Dist: watchfiles; extra == "dev"
|
|
|
59
44
|
|
|
60
45
|
## The _3 layer_ Architecture
|
|
61
46
|
|
|
62
|
-
- **[Page]
|
|
63
|
-
|
|
64
|
-
- **[
|
|
47
|
+
- **[Page][page-docs]** - A single webpage item built from content, a template,
|
|
48
|
+
raw data, or a combination of those things.
|
|
49
|
+
- **[Collection][collection-docs]** - A group of webpages built from the same
|
|
50
|
+
template, organized in a single directory
|
|
51
|
+
- **[Site][site-docs]** - The container that helps to render all Pages and
|
|
52
|
+
Collections with uniform settings and variables
|
|
65
53
|
|
|
66
54
|
## Installing Render Engine
|
|
67
55
|
|
|
@@ -88,13 +76,20 @@ Check out the [Getting Started](https://render-engine.readthedocs.io/en/latest/p
|
|
|
88
76
|
|
|
89
77
|
## Finding Awesome Add-Ons
|
|
90
78
|
|
|
91
|
-
We've compiled a set of [awesome add-ons]
|
|
79
|
+
We've compiled a set of [awesome add-ons][awesome-list] that you can use to
|
|
80
|
+
make your site even better!
|
|
81
|
+
|
|
82
|
+
## CONTRIBUTING
|
|
83
|
+
|
|
84
|
+
We encourage contributors of all skill levels.
|
|
85
|
+
Please review the [contributing][contributing] section of our docs for more information.
|
|
92
86
|
|
|
93
87
|
## Contributors
|
|
94
88
|
|
|
89
|
+
<!-- markdownlint-disable -->
|
|
95
90
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
96
91
|
<!-- prettier-ignore-start -->
|
|
97
|
-
|
|
92
|
+
|
|
98
93
|
<table>
|
|
99
94
|
<tbody>
|
|
100
95
|
<tr>
|
|
@@ -126,20 +121,18 @@ We've compiled a set of [awesome add-ons](https://github.com/render-engine/rende
|
|
|
126
121
|
</tbody>
|
|
127
122
|
</table>
|
|
128
123
|
|
|
129
|
-
<!-- markdownlint-restore -->
|
|
130
124
|
<!-- prettier-ignore-end -->
|
|
131
|
-
|
|
132
125
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
133
|
-
<!-- prettier-ignore-start -->
|
|
134
|
-
<!-- markdownlint-disable -->
|
|
135
|
-
|
|
136
126
|
<!-- markdownlint-restore -->
|
|
137
|
-
<!-- prettier-ignore-end -->
|
|
138
|
-
|
|
139
|
-
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
140
127
|
|
|
141
128
|
### Sponsors at the $20/month and Higher Level
|
|
142
129
|
|
|
143
130
|
- [Brian Douglas](https://github.com/bdougie)
|
|
144
131
|
|
|
145
132
|
Thank you to them and all of those who continue to support this project!
|
|
133
|
+
|
|
134
|
+
[contributing]: https://render-engine.readthedocs.io/en/latest/contributing/CONTRIBUTING/
|
|
135
|
+
[page-docs]: https://render-engine.readthedocs.io/en/latest/page/
|
|
136
|
+
[collection-docs]: https://render-engine.readthedocs.io/en/latest/collection/
|
|
137
|
+
[site-docs]: https://render-engine.readthedocs.io/en/latest/site/
|
|
138
|
+
[awesome-list]: https://github.com/render-engine/render-engine-awesome-list
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
render_engine/.gitignore,sha256=74oa8YR8gNxKxB6lCtoqmgtB2xpZnWM539Qsl4wI_lg,12
|
|
2
2
|
render_engine/__init__.py,sha256=3fgua4ZA9o1pvQ5unhY1gRARLXFqAu019NEYqZTjP20,154
|
|
3
|
-
render_engine/
|
|
4
|
-
render_engine/__version__.py,sha256=hfZmZGRgu1zYwAQDJZKkcPJnWnfiuAJLIE-fSAXX7c8,718
|
|
3
|
+
render_engine/__version__.py,sha256=Adg32837GXzmF14xSZl3K1spnzU9xP6UbqfY6CVoOyg,718
|
|
5
4
|
render_engine/_base_object.py,sha256=B05rDWJUe5GM5FF94NeYY71KweBHY-IYel1lSBwLOrU,3574
|
|
6
5
|
render_engine/archive.py,sha256=S3-kCmDNVKkEfKDKxcEk-sXkBD0vS0RDnFfPunYkU8g,2072
|
|
7
6
|
render_engine/blog.py,sha256=f9GqFUFsta0KZnFhCiajobpfQyALqvgI5sbLm6zt1zw,1571
|
|
8
|
-
render_engine/collection.py,sha256=
|
|
9
|
-
render_engine/engine.py,sha256=
|
|
10
|
-
render_engine/feeds.py,sha256=
|
|
7
|
+
render_engine/collection.py,sha256=3CdT7Q56beYwjEYwA0scaQoLFfF54o8A3l-p21bBS6s,12633
|
|
8
|
+
render_engine/engine.py,sha256=CZ3ruTOsnwDw9O90O4Wy9HSWl1sysGFc8KIJTX_9v34,3173
|
|
9
|
+
render_engine/feeds.py,sha256=hI6jXeMchdZh85vUmOaRkI228CChRNU_cRKg1vaxTAQ,1142
|
|
11
10
|
render_engine/hookspecs.py,sha256=GhOpw0zTQjfwWOFYYbJ4P7Cvq-oy1MmTPHmd90dr0kg,2292
|
|
12
11
|
render_engine/links.py,sha256=pKmQMTz8-yGX8IecHcrlF3Dkejk7cptaO3qCkQiHB9I,2560
|
|
13
|
-
render_engine/page.py,sha256=
|
|
14
|
-
render_engine/plugins.py,sha256=
|
|
12
|
+
render_engine/page.py,sha256=gAcb8XxknAUfMMgG71wsDpI24z_dz0d2C3Ks7ST7RXU,9293
|
|
13
|
+
render_engine/plugins.py,sha256=QT_jvbKVlUz7GcL13f38Agli7t9flFDvG-qVj3yBRRU,4696
|
|
15
14
|
render_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
render_engine/site.py,sha256=
|
|
17
|
-
render_engine/site_map.py,sha256=
|
|
18
|
-
render_engine/themes.py,sha256=
|
|
15
|
+
render_engine/site.py,sha256=pbwEdgR2TN0pzAuABFB8Z5bKRnmjETKXZZb3S6CJXz4,12774
|
|
16
|
+
render_engine/site_map.py,sha256=aiW9CdlPrGanSirQi_C2tbDr6281OAxlx9HHAhSucb4,5461
|
|
17
|
+
render_engine/themes.py,sha256=4ZYueERJAehz0CrhMV9LNuOBCWeNvP9bJSaOyGQGs2I,5069
|
|
19
18
|
render_engine/content_managers/__init__.py,sha256=z1x99J0GNcfqYFrugD0EleiZR6b-sfM6zViDTH1iF0s,161
|
|
20
|
-
render_engine/content_managers/base_content_manager.py,sha256=
|
|
21
|
-
render_engine/content_managers/file_content_manager.py,sha256=
|
|
22
|
-
render_engine/extras/__init__.py,sha256=L4jr4A7Jl-ODnSx1q2fP3_dBo37Dw6yepNRddu1nFNo,72
|
|
19
|
+
render_engine/content_managers/base_content_manager.py,sha256=Bgmk9LCRe0hdomkw_gm-4SR5v1hdE5srQGiDR25mZxY,1314
|
|
20
|
+
render_engine/content_managers/file_content_manager.py,sha256=QmpCz1qncmXmSY_SdpMCdvtCB5fbpKkONont7wVAPaI,2945
|
|
23
21
|
render_engine/parsers/markdown.py,sha256=0jpixCaoHaL0IRSvFIljJIRCvFkXoKTEYQNK38LwMDU,287
|
|
24
22
|
render_engine/render_engine_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
23
|
render_engine/render_engine_templates/archive.html,sha256=Ipxtj2vUSMk09pcjiGpNRIH3dWTc6naomVQ0Otp1O00,45
|
|
26
24
|
render_engine/render_engine_templates/base.html,sha256=IJv85-lU8Fq4HBe1p1PWl3-M3SvqxVYvQvfRwp9viTI,42
|
|
27
|
-
render_engine/render_engine_templates/base_collection_path.md,sha256=
|
|
25
|
+
render_engine/render_engine_templates/base_collection_path.md,sha256=nWY3B8tqbuJ0wdROntmtYmyX7KBbgiq7QvDY-msvS7s,431
|
|
28
26
|
render_engine/render_engine_templates/page.html,sha256=p9aZ6_6mouPT1c8FVg-f14MeSt4OB-sH3hcXTY6jGmA,42
|
|
29
27
|
render_engine/render_engine_templates/rss2.0.xml,sha256=Z5LGUdPRr3fQhXtJCjR8uxiPQ_GqYHELleDaS8Tr1oU,1803
|
|
30
28
|
render_engine/render_engine_templates/rss2.0_items.xml,sha256=jxhDG9K2R112-upqq4NZnahDGZcvlWIn_sl9b4ArVEI,1306
|
|
@@ -36,7 +34,7 @@ render_engine/render_engine_templates/base_templates/_page.html,sha256=jjrY2BAwl
|
|
|
36
34
|
render_engine/render_engine_templates/components/footer.html,sha256=HkPGGhfN0HcYm7t8zgXWCQ3bsCbT8FxT4_n2-9e1zUE,74
|
|
37
35
|
render_engine/render_engine_templates/components/page_title.html,sha256=l8aE1TY94UPHXHqAyy6jv4IoN2Hv9cbrTPh7ILkMyxg,137
|
|
38
36
|
render_engine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
render_engine-2026.
|
|
40
|
-
render_engine-2026.
|
|
41
|
-
render_engine-2026.
|
|
42
|
-
render_engine-2026.
|
|
37
|
+
render_engine-2026.2.1a1.dist-info/METADATA,sha256=zF9jFepRfk-g7IPXiDHZMgU2VbOBuL-t4w1IiNj5o8g,11482
|
|
38
|
+
render_engine-2026.2.1a1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
39
|
+
render_engine-2026.2.1a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
|
|
40
|
+
render_engine-2026.2.1a1.dist-info/RECORD,,
|
render_engine/__main__.py
DELETED
render_engine/extras/__init__.py
DELETED
|
File without changes
|