Compare commits
3 commits
404e951651
...
1f2b736815
Author | SHA1 | Date | |
---|---|---|---|
1f2b736815 | |||
c489a6f076 | |||
933210c93b |
11 changed files with 152 additions and 29 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
|
||||||
|
@ -99,6 +106,13 @@ class ZonaBuilder:
|
||||||
else date.min,
|
else date.min,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
posts = len(post_list)
|
||||||
|
for i, item in enumerate(post_list):
|
||||||
|
prev = post_list[i - 1] if i > 0 else None
|
||||||
|
next = post_list[i + 1] if i < posts - 2 else None
|
||||||
|
item.previous = prev
|
||||||
|
item.next = next
|
||||||
|
|
||||||
templater = Templater(
|
templater = Templater(
|
||||||
config=self.config,
|
config=self.config,
|
||||||
template_dir=self.layout.templates,
|
template_dir=self.layout.templates,
|
||||||
|
@ -111,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():
|
||||||
|
@ -152,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
|
||||||
|
|
|
@ -31,6 +31,18 @@ header {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-nav {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav .symbol {
|
||||||
|
color: var(--main-bullet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav a {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.site-logo {
|
.site-logo {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
{% extends "base.html" %} {% block content %}
|
{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {%
|
||||||
|
include "title.html" %} {% if metadata.date %}
|
||||||
{% if metadata.show_title %}
|
|
||||||
{% include "title.html" %}
|
|
||||||
{% if metadata.date %}
|
|
||||||
<center>
|
<center>
|
||||||
<time class="post-date" datetime="{{ metadata.date | safe }}"
|
<time class="post-date" datetime="{{ metadata.date | safe }}"
|
||||||
>{{ metadata.date | safe}}</time>
|
>{{ metadata.date | safe}}</time>
|
||||||
</center>
|
</center>
|
||||||
{% endif %}
|
{% endif %} {% endif %} {% if is_post %} {% include "post_nav.html" %} {% endif
|
||||||
|
%}
|
||||||
<hr>
|
<hr>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<article>{{ content | safe }}</article>
|
<article>{{ content | safe }}</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
9
src/zona/data/templates/post_nav.html
Normal file
9
src/zona/data/templates/post_nav.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div class="post-nav">
|
||||||
|
<center>
|
||||||
|
<span class="symbol"><</span>{% if previous %}<a
|
||||||
|
href="{{ previous.url }}"
|
||||||
|
>prev</a>{% endif %}{% if previous and next %}<span class="symbol"
|
||||||
|
>|</span>{% endif %}{% if next %}<a href="{{ next.url }}">next</a>{% endif
|
||||||
|
%}<span class="symbol">></span>
|
||||||
|
</center>
|
||||||
|
</div>
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ class Metadata:
|
||||||
date: date
|
date: date
|
||||||
description: str | None
|
description: str | None
|
||||||
show_title: bool = True
|
show_title: bool = True
|
||||||
|
show_date: bool = True
|
||||||
|
show_nav: bool = True
|
||||||
style: str | None = "/static/style.css"
|
style: str | None = "/static/style.css"
|
||||||
header: bool = True
|
header: bool = True
|
||||||
footer: bool = True
|
footer: bool = True
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -21,6 +23,8 @@ class Item:
|
||||||
type: ItemType | None = None
|
type: ItemType | None = None
|
||||||
copy: bool = True
|
copy: bool = True
|
||||||
post: bool = False
|
post: bool = False
|
||||||
|
next: Item | None = None
|
||||||
|
previous: Item | None = None
|
||||||
|
|
||||||
|
|
||||||
# @dataclass
|
# @dataclass
|
||||||
|
|
|
@ -237,9 +237,11 @@ def serve(
|
||||||
observer.schedule(
|
observer.schedule(
|
||||||
event_handler, path=str(root / "content"), recursive=True
|
event_handler, path=str(root / "content"), recursive=True
|
||||||
)
|
)
|
||||||
|
templates = root / "templates"
|
||||||
|
if templates.is_dir():
|
||||||
observer.schedule(
|
observer.schedule(
|
||||||
event_handler,
|
event_handler,
|
||||||
path=str(root / "templates"),
|
path=str(templates),
|
||||||
recursive=True,
|
recursive=True,
|
||||||
)
|
)
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|
|
@ -3,6 +3,7 @@ from typing import Literal
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
|
from zona import util
|
||||||
from zona.config import ZonaConfig
|
from zona.config import ZonaConfig
|
||||||
from zona.markdown import md_to_html
|
from zona.markdown import md_to_html
|
||||||
from zona.models import Item
|
from zona.models import Item
|
||||||
|
@ -35,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"]),
|
||||||
|
@ -76,5 +78,12 @@ class Templater:
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
header=header,
|
header=header,
|
||||||
footer=footer,
|
footer=footer,
|
||||||
|
is_post=item.post,
|
||||||
|
next=util.normalize_url(item.next.url)
|
||||||
|
if item.next
|
||||||
|
else None,
|
||||||
|
previous=util.normalize_url(item.previous.url)
|
||||||
|
if item.previous
|
||||||
|
else None,
|
||||||
post_list=self.post_list,
|
post_list=self.post_list,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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