feat: merge user templates with defaults
This commit is contained in:
parent
c489a6f076
commit
1f2b736815
5 changed files with 102 additions and 15 deletions
11
README.md
11
README.md
|
@ -172,8 +172,8 @@ public/
|
||||||
|
|
||||||
The **root** of the zona **project** _must_ contain the configuration file,
|
The **root** of the zona **project** _must_ contain the configuration file,
|
||||||
`config.yml`, and a directory called `content`. A directory called `templates`
|
`config.yml`, and a directory called `content`. A directory called `templates`
|
||||||
is optional, and prioritized if it exists. `public` is the built site output —
|
is optional, and merged with the defaults if it exists. `public` is the built
|
||||||
it's recommended to add this path to your `.gitignore`.
|
site output — it's recommended to add this path to your `.gitignore`.
|
||||||
|
|
||||||
The `content` directory is the **root of the website**. Think of it as the
|
The `content` directory is the **root of the website**. Think of it as the
|
||||||
**content root**. For example, suppose your website is hosted at `example.com`.
|
**content root**. For example, suppose your website is hosted at `example.com`.
|
||||||
|
@ -191,9 +191,10 @@ site using the `post_list` template.
|
||||||
### Templates
|
### Templates
|
||||||
|
|
||||||
The `templates` directory may contain any `jinja2` template files. You may
|
The `templates` directory may contain any `jinja2` template files. You may
|
||||||
modify the existing templates or create your own. To apply a certain template to
|
modify the existing templates or create your own. Your templates are merged with
|
||||||
a page, set the `template` option in its [frontmatter](#frontmatter). The
|
the packaged defaults. To apply a certain template to a page, set the `template`
|
||||||
following public variables are made available to the template engine:
|
option in its [frontmatter](#frontmatter). The following public variables are
|
||||||
|
made available to the template engine:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ---------- | ------------------------------------------------------ |
|
| ---------- | ------------------------------------------------------ |
|
||||||
|
|
|
@ -30,6 +30,7 @@ class ZonaBuilder:
|
||||||
self.config.build.include_drafts = True
|
self.config.build.include_drafts = True
|
||||||
self.items: list[Item] = []
|
self.items: list[Item] = []
|
||||||
self.item_map: dict[Path, Item] = {}
|
self.item_map: dict[Path, Item] = {}
|
||||||
|
self.fresh: bool = True
|
||||||
|
|
||||||
def _discover(self):
|
def _discover(self):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
@ -48,7 +49,9 @@ class ZonaBuilder:
|
||||||
destination=destination,
|
destination=destination,
|
||||||
url=str(destination.relative_to(layout.output)),
|
url=str(destination.relative_to(layout.output)),
|
||||||
)
|
)
|
||||||
if path.name.endswith(".md") and not path.is_relative_to(
|
if path.name.endswith(
|
||||||
|
".md"
|
||||||
|
) and not path.is_relative_to(
|
||||||
layout.root / "content" / "static"
|
layout.root / "content" / "static"
|
||||||
):
|
):
|
||||||
logger.debug(f"Parsing {path.name}.")
|
logger.debug(f"Parsing {path.name}.")
|
||||||
|
@ -69,11 +72,13 @@ class ZonaBuilder:
|
||||||
item.copy = False
|
item.copy = False
|
||||||
name = destination.stem
|
name = destination.stem
|
||||||
if name == "index":
|
if name == "index":
|
||||||
item.destination = item.destination.with_suffix(
|
item.destination = (
|
||||||
".html"
|
item.destination.with_suffix(".html")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
relative = path.relative_to(base).with_suffix("")
|
relative = path.relative_to(base).with_suffix(
|
||||||
|
""
|
||||||
|
)
|
||||||
name = relative.stem
|
name = relative.stem
|
||||||
item.destination = (
|
item.destination = (
|
||||||
layout.output
|
layout.output
|
||||||
|
@ -85,7 +90,9 @@ class ZonaBuilder:
|
||||||
layout.output
|
layout.output
|
||||||
)
|
)
|
||||||
item.url = (
|
item.url = (
|
||||||
"" if rel_url == Path(".") else rel_url.as_posix()
|
""
|
||||||
|
if rel_url == Path(".")
|
||||||
|
else rel_url.as_posix()
|
||||||
)
|
)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
self.items = items
|
self.items = items
|
||||||
|
@ -118,7 +125,9 @@ class ZonaBuilder:
|
||||||
# write code highlighting stylesheet
|
# write code highlighting stylesheet
|
||||||
if self.config.markdown.syntax_highlighting.enabled:
|
if self.config.markdown.syntax_highlighting.enabled:
|
||||||
pygments_style = zmd.get_style_defs(self.config)
|
pygments_style = zmd.get_style_defs(self.config)
|
||||||
pygments_path = self.layout.output / "static" / "pygments.css"
|
pygments_path = (
|
||||||
|
self.layout.output / "static" / "pygments.css"
|
||||||
|
)
|
||||||
util.ensure_parents(pygments_path)
|
util.ensure_parents(pygments_path)
|
||||||
pygments_path.write_text(pygments_style)
|
pygments_path.write_text(pygments_style)
|
||||||
for item in self.item_map.values():
|
for item in self.item_map.values():
|
||||||
|
@ -159,7 +168,10 @@ class ZonaBuilder:
|
||||||
child.unlink()
|
child.unlink()
|
||||||
elif child.is_dir():
|
elif child.is_dir():
|
||||||
shutil.rmtree(child)
|
shutil.rmtree(child)
|
||||||
|
if not self.fresh:
|
||||||
|
self.layout = self.layout.refresh()
|
||||||
logger.debug("Discovering...")
|
logger.debug("Discovering...")
|
||||||
self._discover()
|
self._discover()
|
||||||
logger.debug("Building...")
|
logger.debug("Building...")
|
||||||
self._build()
|
self._build()
|
||||||
|
self.fresh = False
|
||||||
|
|
|
@ -16,6 +16,22 @@ class Layout:
|
||||||
content: Path
|
content: Path
|
||||||
templates: Path
|
templates: Path
|
||||||
output: Path
|
output: Path
|
||||||
|
shared_templates: util.TempDir | None
|
||||||
|
_validate: bool
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
logger.debug("Refreshing layout...")
|
||||||
|
if (
|
||||||
|
self.shared_templates
|
||||||
|
and not self.shared_templates.removed
|
||||||
|
):
|
||||||
|
logger.debug("Removing stale templates tempdir...")
|
||||||
|
self.shared_templates.remove()
|
||||||
|
return self.__class__.from_input(
|
||||||
|
root=self.root,
|
||||||
|
output=self.output,
|
||||||
|
validate=self._validate,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_input(
|
def from_input(
|
||||||
|
@ -31,6 +47,8 @@ class Layout:
|
||||||
output=(root / "public").resolve()
|
output=(root / "public").resolve()
|
||||||
if not output
|
if not output
|
||||||
else output,
|
else output,
|
||||||
|
shared_templates=None,
|
||||||
|
_validate=validate,
|
||||||
)
|
)
|
||||||
if validate:
|
if validate:
|
||||||
logger.debug("Validating site layout...")
|
logger.debug("Validating site layout...")
|
||||||
|
@ -39,10 +57,29 @@ class Layout:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
"Missing required content directory!"
|
"Missing required content directory!"
|
||||||
)
|
)
|
||||||
if not layout.templates.is_dir():
|
internal_templates = util.get_resource_dir("templates")
|
||||||
|
user_templates = layout.templates
|
||||||
|
if not user_templates.is_dir() or util.is_empty(
|
||||||
|
user_templates
|
||||||
|
):
|
||||||
logger.debug("Using default template directory.")
|
logger.debug("Using default template directory.")
|
||||||
# use the included defaults
|
# use the included defaults
|
||||||
layout.templates = util.get_resource_dir("templates")
|
layout.templates = internal_templates
|
||||||
|
else:
|
||||||
|
seen: set[str] = set()
|
||||||
|
temp = util.TempDir()
|
||||||
|
logger.debug(
|
||||||
|
f"Creating shared template directory at {temp}"
|
||||||
|
)
|
||||||
|
for f in user_templates.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
util.copy_static_file(f, temp.path)
|
||||||
|
seen.add(f.name)
|
||||||
|
for f in internal_templates.iterdir():
|
||||||
|
if f.is_file() and f.name not in seen:
|
||||||
|
util.copy_static_file(f, temp.path)
|
||||||
|
layout.shared_templates = temp
|
||||||
|
layout.templates = temp.path
|
||||||
|
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ class Templater:
|
||||||
template_dir: Path,
|
template_dir: Path,
|
||||||
post_list: list[Item],
|
post_list: list[Item],
|
||||||
):
|
):
|
||||||
|
# build temporary template dir
|
||||||
self.env: Environment = Environment(
|
self.env: Environment = Environment(
|
||||||
loader=FileSystemLoader(template_dir),
|
loader=FileSystemLoader(template_dir),
|
||||||
autoescape=select_autoescape(["html", "xml"]),
|
autoescape=select_autoescape(["html", "xml"]),
|
||||||
|
@ -78,7 +79,9 @@ class Templater:
|
||||||
header=header,
|
header=header,
|
||||||
footer=footer,
|
footer=footer,
|
||||||
is_post=item.post,
|
is_post=item.post,
|
||||||
next=util.normalize_url(item.next.url) if item.next else None,
|
next=util.normalize_url(item.next.url)
|
||||||
|
if item.next
|
||||||
|
else None,
|
||||||
previous=util.normalize_url(item.previous.url)
|
previous=util.normalize_url(item.previous.url)
|
||||||
if item.previous
|
if item.previous
|
||||||
else None,
|
else None,
|
||||||
|
|
|
@ -1,11 +1,36 @@
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import string
|
import string
|
||||||
|
import tempfile
|
||||||
|
import weakref
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
from importlib.resources.abc import Traversable
|
from importlib.resources.abc import Traversable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copy2
|
from shutil import copy2
|
||||||
from typing import NamedTuple
|
from typing import Any, NamedTuple, override
|
||||||
|
|
||||||
|
|
||||||
|
class TempDir:
|
||||||
|
"""Temporary directory that cleans up when it's garbage collected."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._tempdir: str = tempfile.mkdtemp()
|
||||||
|
self.path: Path = Path(self._tempdir)
|
||||||
|
self._finalizer: weakref.finalize[Any, Any] = (
|
||||||
|
weakref.finalize(self, shutil.rmtree, self._tempdir)
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
self._finalizer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def removed(self):
|
||||||
|
return not self._finalizer.alive
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<TempDir {self.path}>"
|
||||||
|
|
||||||
|
|
||||||
class ZonaResource(NamedTuple):
|
class ZonaResource(NamedTuple):
|
||||||
|
@ -65,6 +90,15 @@ def copy_static_file(src: Path, dst: Path):
|
||||||
copy2(src, dst)
|
copy2(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(path: Path) -> bool:
|
||||||
|
"""If given a file, check if it has any non-whitespace content.
|
||||||
|
If given a directory, check if it has any children."""
|
||||||
|
if path.is_file():
|
||||||
|
return path.read_text().strip() == ""
|
||||||
|
else:
|
||||||
|
return not any(path.iterdir())
|
||||||
|
|
||||||
|
|
||||||
def filename_to_title(path: Path) -> str:
|
def filename_to_title(path: Path) -> str:
|
||||||
name = path.stem
|
name = path.stem
|
||||||
words = name.replace("-", " ").replace("_", " ")
|
words = name.replace("-", " ").replace("_", " ")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue