refactor: organize code in context & extensions
This commit is contained in:
parent
8591ae53fa
commit
2729af8e39
|
|
@ -1,3 +1,4 @@
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
.coverage
|
.coverage
|
||||||
jean_website.egg-info
|
jean_website.egg-info
|
||||||
|
tests/**/*.mo
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from click import group
|
from click import group
|
||||||
|
|
||||||
from jwebsite.site import Site
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
@group
|
@group
|
||||||
|
|
@ -13,4 +13,4 @@ def main() -> None: ...
|
||||||
def build() -> None:
|
def build() -> None:
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
with open(cwd / "site.py", encoding="utf-8") as site_config:
|
with open(cwd / "site.py", encoding="utf-8") as site_config:
|
||||||
exec(site_config.read(), {"site": Site(cwd)})
|
exec(site_config.read(), {"site": Context(cwd)})
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,36 @@
|
||||||
from functools import cache
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Iterator
|
from typing import Any, Iterator
|
||||||
|
|
||||||
from markdown import Markdown
|
|
||||||
from yaml import Loader, load
|
|
||||||
|
|
||||||
|
|
||||||
class Content:
|
class Content:
|
||||||
current_language: str | None = None
|
def __init__(self, path: Path, data: Any, language: str | None = None) -> None:
|
||||||
|
|
||||||
def __init__(self, path: Path) -> None:
|
|
||||||
self.__path = path
|
self.__path = path
|
||||||
|
self.__data = data
|
||||||
|
self.__language = language
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> Path:
|
def path(self) -> Path:
|
||||||
return self.__path
|
return self.__path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def language(self) -> str | None:
|
||||||
|
return self.__language
|
||||||
|
|
||||||
class ContentDirectory(Content):
|
@property
|
||||||
def load(self, subpath: str | Path) -> Content:
|
def data(self) -> Any:
|
||||||
subpath = Path(subpath)
|
return self.__data
|
||||||
current: Content = self
|
|
||||||
for part in subpath.parts:
|
|
||||||
if not isinstance(current, ContentDirectory):
|
|
||||||
raise NotADirectoryError(self.path)
|
|
||||||
|
|
||||||
current = current.__load_children(part)
|
|
||||||
|
|
||||||
return current
|
|
||||||
|
|
||||||
def glob(self, pattern: str) -> Iterable[Content]:
|
|
||||||
for item in self.path.glob(pattern):
|
|
||||||
yield self.load(str(item.relative_to(self.path)))
|
|
||||||
|
|
||||||
def __load_children(self, name: str) -> Content:
|
|
||||||
child_path = self.__get_localized_path(self.path / name)
|
|
||||||
return self.__load_path(child_path)
|
|
||||||
|
|
||||||
@cache # noqa: B019
|
|
||||||
def __load_path(self, child_path: Path) -> Content:
|
|
||||||
if not child_path.exists():
|
|
||||||
raise FileNotFoundError(child_path)
|
|
||||||
|
|
||||||
if child_path.is_dir():
|
|
||||||
return ContentDirectory(child_path)
|
|
||||||
|
|
||||||
if child_path.is_file():
|
|
||||||
if child_path.suffix in [".yml", ".yaml", ".json"]:
|
|
||||||
return DataFile(child_path)
|
|
||||||
if child_path.suffix == ".md":
|
|
||||||
return MarkdownFile(child_path)
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_localized_path(path: Path) -> Path:
|
|
||||||
if Content.current_language is None:
|
|
||||||
return path
|
|
||||||
localized_path = path.with_name(f"{path.stem}-{Content.current_language}{path.suffix}")
|
|
||||||
print(localized_path)
|
|
||||||
if not localized_path.exists():
|
|
||||||
return path
|
|
||||||
|
|
||||||
return localized_path
|
|
||||||
|
|
||||||
|
|
||||||
class DataField:
|
|
||||||
def __init__(self, file_path: Path, value: Any) -> None:
|
|
||||||
self.__file_path = file_path
|
|
||||||
self.__value = value
|
|
||||||
|
|
||||||
def as_path(self) -> Path:
|
|
||||||
return self.__file_path.parent / str(self.__value)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.__value)
|
return str(self.__data)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentField(Content):
|
||||||
def __getitem__(self, key: Any) -> Any:
|
def __getitem__(self, key: Any) -> Any:
|
||||||
return DataField(self.__file_path, self.__value.get(key))
|
return ContentField(self.path, self.data.get(key), self.language)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Any]:
|
def __iter__(self) -> Iterator[Any]:
|
||||||
for it in self.__value:
|
for it in self.data:
|
||||||
yield DataField(self.__file_path, it)
|
yield ContentField(self.path, it, self.language)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
class DataFile(Content):
|
return str(self.data)
|
||||||
def __init__(self, path: Path) -> None:
|
|
||||||
super().__init__(path)
|
|
||||||
with path.open("r", encoding="utf-8") as data_file:
|
|
||||||
self.__data = load(data_file, Loader)
|
|
||||||
|
|
||||||
def __getitem__(self, key: Any) -> Any:
|
|
||||||
return DataField(self.path, self.__data.get(key))
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Any]:
|
|
||||||
for it in self.__data:
|
|
||||||
yield DataField(self.path, it)
|
|
||||||
|
|
||||||
|
|
||||||
class MarkdownFile(Content):
|
|
||||||
def __init__(self, path: Path) -> None:
|
|
||||||
super().__init__(path)
|
|
||||||
with path.open("r", encoding="utf-8") as markdown_file:
|
|
||||||
self.__markdown = Markdown(extensions=["full_yaml_metadata"])
|
|
||||||
self.__html = self.__markdown.convert(markdown_file.read())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def html(self) -> str:
|
|
||||||
return self.__html
|
|
||||||
|
|
||||||
@property
|
|
||||||
def meta(self) -> Any:
|
|
||||||
return DataField(self.path, self.__markdown.Meta) # type: ignore
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import gettext
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from gettext import GNUTranslations, NullTranslations
|
||||||
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
from jinja2 import pass_context
|
||||||
|
from jinja2.environment import Environment
|
||||||
|
from jinja2.loaders import FileSystemLoader
|
||||||
|
from jinja2.runtime import Context as JinjaContext
|
||||||
|
|
||||||
|
from jwebsite.content import Content
|
||||||
|
|
||||||
|
_DEFAULT_LANGUAGE = "en"
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
def __init__(self, root_directory: Path) -> None:
|
||||||
|
self.__root_directory = root_directory
|
||||||
|
self.__output_directory = root_directory / "build"
|
||||||
|
self.__content_directory = root_directory / "content"
|
||||||
|
self.__environment = Environment(
|
||||||
|
loader=FileSystemLoader(searchpath=root_directory / "src"),
|
||||||
|
)
|
||||||
|
self.__translations: dict[str, GNUTranslations | NullTranslations] = {}
|
||||||
|
self.__current_language: str | None = None
|
||||||
|
|
||||||
|
self.add_filters(load=self.__load, write=self.__write)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_language(self) -> str | None:
|
||||||
|
return self.__current_language
|
||||||
|
|
||||||
|
def add_filters(self, **filters: Any) -> None:
|
||||||
|
self.__environment.filters.update(**filters)
|
||||||
|
|
||||||
|
def load_extensions(self, *extensions: str) -> None:
|
||||||
|
for extension in extensions:
|
||||||
|
module = import_module(extension)
|
||||||
|
module.load_extension(self)
|
||||||
|
|
||||||
|
def load_translations(self, domain: str, locale_dir: str | Path, *languages: str) -> None:
|
||||||
|
locale_dir = str(locale_dir)
|
||||||
|
self.__environment.add_extension("jinja2.ext.i18n")
|
||||||
|
self.__translations[_DEFAULT_LANGUAGE] = NullTranslations()
|
||||||
|
for language in languages:
|
||||||
|
self.__translations[language] = gettext.translation(
|
||||||
|
domain, localedir=str(locale_dir), languages=[language]
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def set_language(self, language: str) -> Iterator[None]:
|
||||||
|
translation = self.__translations[language]
|
||||||
|
old_language = self.__current_language
|
||||||
|
self.__environment.install_gettext_translations(translation, newstyle=True) # type: ignore
|
||||||
|
self.__current_language = language
|
||||||
|
yield
|
||||||
|
self.__current_language = old_language
|
||||||
|
self.__environment.uninstall_gettext_translations(translation) # type: ignore
|
||||||
|
|
||||||
|
def render(self, source: str, output: str | Path, **context: Any) -> None:
|
||||||
|
if self.__translations:
|
||||||
|
for language in self.__translations:
|
||||||
|
with self.set_language(language):
|
||||||
|
self.__render(source, self.__output_directory / language / output, **context)
|
||||||
|
else:
|
||||||
|
self.__render(source, self.__output_directory / output, **context)
|
||||||
|
|
||||||
|
@pass_context
|
||||||
|
def __load(self, context: JinjaContext, path: str | Path) -> Content:
|
||||||
|
current_language = self.current_language
|
||||||
|
if current_language:
|
||||||
|
localized_path = self.__content_directory / current_language / path
|
||||||
|
if not localized_path.exists():
|
||||||
|
localized_path = self.__content_directory / _DEFAULT_LANGUAGE / path
|
||||||
|
else:
|
||||||
|
localized_path = self.__content_directory / path
|
||||||
|
|
||||||
|
with localized_path.open("r", encoding="utf-8") as content_file:
|
||||||
|
return Content(localized_path, content_file.read(), current_language)
|
||||||
|
|
||||||
|
def __write(self, content: Content) -> Path:
|
||||||
|
relative_path = content.path.relative_to(self.__content_directory)
|
||||||
|
output_path = self.__output_directory / relative_path
|
||||||
|
with output_path.open("w") as output_file:
|
||||||
|
output_file.write(content.data)
|
||||||
|
|
||||||
|
return Path(f"/{relative_path}")
|
||||||
|
|
||||||
|
def __render(self, source: str, output_path: Path, **context: Any) -> None:
|
||||||
|
output_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
template = self.__environment.get_template(source)
|
||||||
|
content = template.render(**context)
|
||||||
|
|
||||||
|
with open(output_path, "w") as output_file:
|
||||||
|
output_file.write(content)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from subprocess import check_output
|
||||||
|
|
||||||
|
from jwebsite.content import Content
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
def load_extension(context: Context) -> None:
|
||||||
|
context.add_filters(git_creation_date=_git_creation_date)
|
||||||
|
|
||||||
|
|
||||||
|
def _git_creation_date(content: Content) -> datetime:
|
||||||
|
git_dir = check_output(["git", "rev-parse", "--show-toplevel"], encoding="utf-8")
|
||||||
|
git_dir = git_dir.strip()
|
||||||
|
log = check_output(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"log",
|
||||||
|
"--pretty=format:%ad",
|
||||||
|
"--date=iso-strict",
|
||||||
|
"--diff-filter=A",
|
||||||
|
"--",
|
||||||
|
str(content.path.relative_to(git_dir)),
|
||||||
|
],
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
log_lines = log.splitlines()
|
||||||
|
if len(log_lines) == 0:
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
return datetime.fromisoformat(log_lines[0])
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from markdown import Markdown
|
||||||
|
|
||||||
|
from jwebsite.content import Content, ContentField
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
def load_extension(context: Context) -> None:
|
||||||
|
context.add_filters(markdown=_MarkdownDocument)
|
||||||
|
|
||||||
|
|
||||||
|
class _MarkdownDocument:
|
||||||
|
def __init__(self, content: Content) -> None:
|
||||||
|
if not isinstance(content, Content) or not isinstance(content.data, (str, bytes)):
|
||||||
|
raise ValueError("markdown filter can only accept byte or string content")
|
||||||
|
|
||||||
|
data = content.data
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
|
||||||
|
assert isinstance(data, str)
|
||||||
|
|
||||||
|
self.__content = content
|
||||||
|
self.__markdown = Markdown(extensions=["full_yaml_metadata"])
|
||||||
|
self.__html = self.__markdown.convert(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self) -> str:
|
||||||
|
return self.__html
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta(self) -> Any:
|
||||||
|
return ContentField(self.__content.path, self.__markdown.Meta, self.__content.language) # type: ignore
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from yaml import Loader, load
|
||||||
|
|
||||||
|
from jwebsite.content import Content, ContentField
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
def load_extension(context: Context) -> None:
|
||||||
|
context.add_filters(yaml=_load_yaml)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml(content: Any) -> ContentField:
|
||||||
|
if not isinstance(content, Content) or not isinstance(content.data, (str, bytes)):
|
||||||
|
raise ValueError("yaml filter can only accept byte or string content")
|
||||||
|
|
||||||
|
data = content.data
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
|
||||||
|
assert isinstance(data, str)
|
||||||
|
|
||||||
|
return ContentField(content.path, load(data, Loader), content.language)
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import gettext
|
|
||||||
from gettext import GNUTranslations, NullTranslations
|
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copy
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from jinja2.environment import Environment
|
|
||||||
from jinja2.loaders import FileSystemLoader
|
|
||||||
|
|
||||||
from jwebsite.content import Content, ContentDirectory
|
|
||||||
from jwebsite.git import git_creation_date
|
|
||||||
|
|
||||||
|
|
||||||
class Site:
|
|
||||||
def __init__(self, root_directory: Path) -> None:
|
|
||||||
self.__root_directory = root_directory
|
|
||||||
self.__output_directory = root_directory / "build"
|
|
||||||
self.__environment = Environment(loader=FileSystemLoader(searchpath=root_directory / "src"))
|
|
||||||
self.__environment.filters.update({"output": self.__output, "git_creation_date": git_creation_date})
|
|
||||||
self.__content = ContentDirectory(root_directory / "content")
|
|
||||||
self.__translations: dict[str, GNUTranslations | NullTranslations] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self) -> ContentDirectory:
|
|
||||||
return self.__content
|
|
||||||
|
|
||||||
def set_translations(self, domain: str, locale_dir: str, languages: list[str]) -> None:
|
|
||||||
self.__environment.add_extension("jinja2.ext.i18n")
|
|
||||||
self.__translations["en"] = NullTranslations()
|
|
||||||
for language in languages:
|
|
||||||
self.__translations[language] = gettext.translation(
|
|
||||||
domain, localedir=str(locale_dir), languages=[language]
|
|
||||||
)
|
|
||||||
|
|
||||||
def render(self, source: str, output: str | Path, **context: Any) -> None:
|
|
||||||
if self.__translations:
|
|
||||||
for language, translation in self.__translations.items():
|
|
||||||
Content.current_language = language
|
|
||||||
self.__environment.install_gettext_translations(translation, newstyle=True) # type: ignore
|
|
||||||
self.__render(source, self.__output_directory / language / output, **context)
|
|
||||||
self.__environment.uninstall_gettext_translations(translation) # type: ignore
|
|
||||||
else:
|
|
||||||
self.__render(source, self.__output_directory / output, **context)
|
|
||||||
|
|
||||||
def __render(self, source: str, output_path: Path, **context: Any) -> None:
|
|
||||||
output_path.parent.mkdir(exist_ok=True, parents=True)
|
|
||||||
template = self.__environment.get_template(source)
|
|
||||||
content = template.render(site=self, **context)
|
|
||||||
|
|
||||||
with open(output_path, "w") as output_file:
|
|
||||||
output_file.write(content)
|
|
||||||
|
|
||||||
def __output(self, path: str | Path) -> str:
|
|
||||||
path = Path(path)
|
|
||||||
if path.is_absolute():
|
|
||||||
src_path = path
|
|
||||||
relative_src_path = src_path.relative_to(self.__content.path)
|
|
||||||
else:
|
|
||||||
src_path = self.__content.path / path
|
|
||||||
relative_src_path = path
|
|
||||||
dst_path = self.__output_directory / relative_src_path
|
|
||||||
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
copy(src_path, dst_path, follow_symlinks=True)
|
|
||||||
return f"/{relative_src_path}"
|
|
||||||
32
noxfile.py
32
noxfile.py
|
|
@ -1,7 +1,12 @@
|
||||||
"""Nox configuration file."""
|
"""Nox configuration file."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from nox import Session, session
|
from nox import Session, session
|
||||||
|
|
||||||
|
_LOCALIZED_TESTS = ["tests/test_context"]
|
||||||
|
|
||||||
|
|
||||||
@session()
|
@session()
|
||||||
def lint(session: Session) -> None:
|
def lint(session: Session) -> None:
|
||||||
|
|
@ -16,10 +21,37 @@ def mypy(session: Session) -> None:
|
||||||
session.run("mypy")
|
session.run("mypy")
|
||||||
|
|
||||||
|
|
||||||
|
@session()
|
||||||
|
def update_messages(session: Session) -> None:
|
||||||
|
session.install("babel", "jinja2")
|
||||||
|
for directory in _LOCALIZED_TESTS:
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
messages_file = Path(tmp_dir) / "messages.po"
|
||||||
|
session.run(
|
||||||
|
"pybabel",
|
||||||
|
"extract",
|
||||||
|
"--mapping",
|
||||||
|
f"{directory}/babel.cfg",
|
||||||
|
f"--output-file={messages_file}",
|
||||||
|
str(directory),
|
||||||
|
)
|
||||||
|
|
||||||
|
session.run(
|
||||||
|
"pybabel",
|
||||||
|
"update",
|
||||||
|
"--domain=tests",
|
||||||
|
f"--input-file={messages_file}",
|
||||||
|
f"--output-dir={directory}/locale",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@session(python=["3.10", "3.11"])
|
@session(python=["3.10", "3.11"])
|
||||||
def unit_tests(session: Session) -> None:
|
def unit_tests(session: Session) -> None:
|
||||||
"""Run unit tests."""
|
"""Run unit tests."""
|
||||||
devenv(session)
|
devenv(session)
|
||||||
|
session.install("babel", "jinja2")
|
||||||
|
for directory in _LOCALIZED_TESTS:
|
||||||
|
session.run("pybabel", "compile", "--domain=tests", f"--directory={directory}/locale", "--use-fuzzy")
|
||||||
session.run("python", "-m", "pytest", "--cov=jwebsite", "--cov-report=html")
|
session.run("python", "-m", "pytest", "--cov=jwebsite", "--cov-report=html")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ requires = ["setuptools>=45"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
strict = true
|
strict = true
|
||||||
files = "jwebsite/**/*.py,noxfile.py"
|
files = "jwebsite/**/*.py,tests/**/*.py,noxfile.py"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 110
|
line-length = 110
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pytest import mark, raises
|
||||||
|
|
||||||
|
from jwebsite.content import Content
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize("content", ["# Otters", b"# Otters"])
|
||||||
|
def test_load(datadir: Path, content: bytes | str) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_extensions("jwebsite.extensions.markdown")
|
||||||
|
|
||||||
|
context.render("test-load.html", "output.html", content=Content(Path("content.yml"), content, None))
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "<h1>Otters</h1>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_type_error(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_extensions("jwebsite.extensions.markdown")
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
context.render("test-load.html", "output.html", content=Content(Path("content.yml"), 10, None))
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
context.render("test-load.html", "output.html", content=10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_extensions("jwebsite.extensions.markdown")
|
||||||
|
|
||||||
|
markdown = "---\n" "title: Steven\n" "---\n" "\n" "Content\n"
|
||||||
|
|
||||||
|
context.render("test-metadata.html", "output.html", content=Content(Path("content.yml"), markdown, None))
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Steven"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% with document = content | markdown %}
|
||||||
|
{{- document.html }}
|
||||||
|
{%- endwith -%}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% with document = content | markdown %}
|
||||||
|
{{- document.meta.title }}
|
||||||
|
{%- endwith -%}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pytest import mark, raises
|
||||||
|
|
||||||
|
from jwebsite.content import Content
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize("content", ["[Peter, Steven]", b"[Peter, Steven]"])
|
||||||
|
def test_load(datadir: Path, content: bytes | str) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_extensions("jwebsite.extensions.yaml")
|
||||||
|
|
||||||
|
context.render("test-load.html", "output.html", content=Content(Path("content.yml"), content, None))
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "PeterSteven"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_type_error(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_extensions("jwebsite.extensions.yaml")
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
context.render("test-load.html", "output.html", content=Content(Path("content.yml"), 10, None))
|
||||||
|
|
||||||
|
with raises(ValueError):
|
||||||
|
context.render("test-load.html", "output.html", content=10)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% for otter in content | yaml -%}
|
||||||
|
{{- otter }}
|
||||||
|
{%- endfor %}
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pytest import raises
|
|
||||||
|
|
||||||
from jwebsite.content import Content, ContentDirectory
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_directory(datadir: Path) -> None:
|
|
||||||
content = ContentDirectory(datadir)
|
|
||||||
otters = content.load(Path("otters"))
|
|
||||||
assert isinstance(otters, ContentDirectory)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_errors(datadir: Path) -> None:
|
|
||||||
directory = ContentDirectory(datadir)
|
|
||||||
|
|
||||||
with raises(FileNotFoundError):
|
|
||||||
directory.load(Path("otters/i-dont-exist"))
|
|
||||||
|
|
||||||
with raises(NotADirectoryError):
|
|
||||||
directory.load(Path("otters/steven.yml/child"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_data(datadir: Path) -> None:
|
|
||||||
content = ContentDirectory(datadir)
|
|
||||||
steven = content.load(Path("otters/steven.yml"))
|
|
||||||
|
|
||||||
assert str(steven["name"]) == "Steven"
|
|
||||||
assert str(steven["mood"]) == "Angry"
|
|
||||||
|
|
||||||
otter_list = content.load(Path("otters/otter_list.json"))
|
|
||||||
assert [str(it) for it in otter_list] == ["steven", "peter"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_data_as_path(datadir: Path) -> None:
|
|
||||||
content = ContentDirectory(datadir)
|
|
||||||
paths = content.load(Path("otters/paths.yml"))
|
|
||||||
|
|
||||||
assert paths["steven"].as_path() == datadir / "otters" / "relative-steven"
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_markdown(datadir: Path) -> None:
|
|
||||||
content = ContentDirectory(datadir)
|
|
||||||
page = content.load(Path("page.md"))
|
|
||||||
|
|
||||||
assert page.html == "<p>Content</p>"
|
|
||||||
assert str(page.meta["title"]) == "Title"
|
|
||||||
|
|
||||||
|
|
||||||
def test_localized_content(datadir: Path) -> None:
|
|
||||||
content = ContentDirectory(datadir)
|
|
||||||
Content.current_language = "fr"
|
|
||||||
page = content.load(Path("page.md"))
|
|
||||||
|
|
||||||
assert page.html == "<p>Contenu</p>"
|
|
||||||
assert str(page.meta["title"]) == "Titre"
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
["steven", "peter"]
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
steven: ./relative-steven
|
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
name: Steven
|
|
||||||
mood: Angry
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
title: Titre
|
|
||||||
---
|
|
||||||
|
|
||||||
Contenu
|
|
||||||
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Title
|
|
||||||
---
|
|
||||||
|
|
||||||
Content
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from jwebsite.context import Context
|
||||||
|
|
||||||
|
|
||||||
|
def test_render(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.render("test-render.html", "output.html", animal="Otters")
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Otters"
|
||||||
|
|
||||||
|
|
||||||
|
def test_jinja_localization(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_translations("tests", datadir / "locale", "fr")
|
||||||
|
context.render("test-jinja-localization.html", "output.html")
|
||||||
|
|
||||||
|
with open(datadir / "build" / "en" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Otters"
|
||||||
|
|
||||||
|
with open(datadir / "build" / "fr" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Loutres"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.render("test-load.html", "output.html")
|
||||||
|
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Otters\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_localized(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.load_translations("tests", datadir / "locale", "fr")
|
||||||
|
context.render("test-load-localized.html", "output.html")
|
||||||
|
|
||||||
|
with open(datadir / "build" / "en" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Otters\nCaimans\n"
|
||||||
|
|
||||||
|
with open(datadir / "build" / "fr" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Loutres\nCaimans\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_write(datadir: Path) -> None:
|
||||||
|
context = Context(datadir)
|
||||||
|
context.render("test-write.html", "output.html")
|
||||||
|
|
||||||
|
with open(datadir / "build" / "content.txt", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "Otters\n"
|
||||||
|
|
||||||
|
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
||||||
|
assert ouput_file.read() == "/content.txt"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[jinja2: src/**.html]
|
||||||
|
encoding = utf-8
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Otters
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Otters
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Caimans
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Loutres
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# French translations for PROJECT.
|
||||||
|
# Copyright (C) 2024 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2024-05-21 21:13+0200\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: fr\n"
|
||||||
|
"Language-Team: fr <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.15.0\n"
|
||||||
|
|
||||||
|
#: tests/test_context/src/test-jinja-localization.html:1
|
||||||
|
msgid "Otters"
|
||||||
|
msgstr "Loutres"
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{{ gettext('Otters') }}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ 'content.txt' | load -}}
|
||||||
|
{{ 'not-localized-content.txt' | load -}}
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{{ 'content.txt' | load }}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{{animal}}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{{ 'content.txt' | load | write }}
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pyfakefs.fake_filesystem import FakeFilesystem
|
|
||||||
from pytest import fixture
|
|
||||||
|
|
||||||
from jwebsite.site import Site
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def site_dir(datadir: Path, fs: FakeFilesystem):
|
|
||||||
fs.add_real_directory(datadir)
|
|
||||||
yield fs
|
|
||||||
|
|
||||||
|
|
||||||
def test_render_page(datadir: Path, site_dir: FakeFilesystem):
|
|
||||||
site = Site(datadir)
|
|
||||||
site.render("index.j2", "index.html")
|
|
||||||
with open(datadir / "build" / "index.html", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "<p>Peter</p><p>Steven</p>"
|
|
||||||
|
|
||||||
|
|
||||||
def test_output_file(datadir: Path, site_dir: FakeFilesystem):
|
|
||||||
site = Site(datadir)
|
|
||||||
build_dir = datadir / "build"
|
|
||||||
|
|
||||||
site.render("output-file.j2", "output-file.html")
|
|
||||||
|
|
||||||
with open(build_dir / "assets/steven-avatar", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "Yipee\n"
|
|
||||||
|
|
||||||
with open(build_dir / "output-file.html", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "/assets/steven-avatar\n"
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Yipee
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
[Peter, Steven]
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{%- for otter in site.content.load('otters.yml') -%}
|
|
||||||
<p>{{ otter }}</p>
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
{{ "assets/steven-avatar" | output }}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue