Compare commits
No commits in common. "b028477371d815402979829d112916353000c7ec" and "8591ae53fa82d76cbf31321d5ce7409018b3637b" have entirely different histories.
b028477371
...
8591ae53fa
|
|
@ -1,4 +1,3 @@
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
.coverage
|
.coverage
|
||||||
jean_web.egg-info
|
jean_website.egg-info
|
||||||
tests/**/*.mo
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
TData = TypeVar("TData")
|
|
||||||
|
|
||||||
|
|
||||||
class Content(Generic[TData]):
|
|
||||||
def __init__(self, path: Path, data: TData, language: str | None = None) -> None:
|
|
||||||
self.__path = path
|
|
||||||
self.__data = data
|
|
||||||
self.__language = language
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Path:
|
|
||||||
return self.__path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def language(self) -> str | None:
|
|
||||||
return self.__language
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self) -> TData:
|
|
||||||
return self.__data
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
if isinstance(self.__data, bytes):
|
|
||||||
return self.__data.decode("utf-8")
|
|
||||||
return str(self.__data)
|
|
||||||
120
jweb/context.py
120
jweb/context.py
|
|
@ -1,120 +0,0 @@
|
||||||
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 jweb.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, glob=self.__glob)
|
|
||||||
self.add_globals(load=self.__load, write=self.__write, glob=self.__glob)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_language(self) -> str | None:
|
|
||||||
return self.__current_language
|
|
||||||
|
|
||||||
def add_filters(self, **filters: Any) -> None:
|
|
||||||
self.__environment.filters.update(**filters)
|
|
||||||
|
|
||||||
def add_globals(self, **globals: Any) -> None:
|
|
||||||
self.__environment.globals.update(**globals)
|
|
||||||
|
|
||||||
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[bytes]:
|
|
||||||
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("rb") as content_file:
|
|
||||||
return Content(localized_path, content_file.read(), current_language)
|
|
||||||
|
|
||||||
@pass_context
|
|
||||||
def __glob(
|
|
||||||
self, context: JinjaContext, pattern: str, include_base_language: bool = False
|
|
||||||
) -> Iterator[Content[bytes]]:
|
|
||||||
roots: list[Path] = []
|
|
||||||
current_language = self.current_language
|
|
||||||
if current_language is None:
|
|
||||||
roots.append(self.__content_directory)
|
|
||||||
else:
|
|
||||||
if include_base_language:
|
|
||||||
roots.append(self.__content_directory / _DEFAULT_LANGUAGE)
|
|
||||||
roots.append(self.__content_directory / current_language)
|
|
||||||
|
|
||||||
paths: set[Path] = set()
|
|
||||||
for root in roots:
|
|
||||||
paths = paths | set(it.relative_to(root) for it in root.glob(pattern))
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
yield self.__load(context, path)
|
|
||||||
|
|
||||||
def __write(self, content: Content[bytes]) -> Path:
|
|
||||||
relative_path = content.path.relative_to(self.__content_directory)
|
|
||||||
output_path = self.__output_directory / relative_path
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with output_path.open("wb") 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)
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from subprocess import check_output
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from jweb.context import Context
|
|
||||||
|
|
||||||
|
|
||||||
def load_extension(context: Context) -> None:
|
|
||||||
context.add_filters(git_creation_date=_git_creation_date)
|
|
||||||
|
|
||||||
|
|
||||||
def _git_creation_date(content: Any) -> 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])
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from markdown import Markdown
|
|
||||||
|
|
||||||
from jweb.content import Content
|
|
||||||
from jweb.context import Context
|
|
||||||
|
|
||||||
|
|
||||||
def load_extension(context: Context) -> None:
|
|
||||||
context.add_filters(markdown=_MarkdownDocument)
|
|
||||||
|
|
||||||
|
|
||||||
class _MarkdownDocument(Content[str]):
|
|
||||||
def __init__(self, content: Content[Any]) -> 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"])
|
|
||||||
super().__init__(content.path, self.__markdown.convert(data), content.language)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def meta(self) -> Any:
|
|
||||||
return self.__markdown.Meta # type: ignore
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from yaml import Loader, load
|
|
||||||
|
|
||||||
from jweb.content import Content
|
|
||||||
from jweb.context import Context
|
|
||||||
|
|
||||||
|
|
||||||
def load_extension(context: Context) -> None:
|
|
||||||
context.add_filters(yaml=_load_yaml)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_yaml(content: Any) -> Any:
|
|
||||||
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 load(data, Loader)
|
|
||||||
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from click import group
|
from click import group
|
||||||
|
|
||||||
from jweb.context import Context
|
from jwebsite.site import Site
|
||||||
|
|
||||||
|
|
||||||
@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": Context(cwd)})
|
exec(site_config.read(), {"site": Site(cwd)})
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
from functools import cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Iterator
|
||||||
|
|
||||||
|
from markdown import Markdown
|
||||||
|
from yaml import Loader, load
|
||||||
|
|
||||||
|
|
||||||
|
class Content:
|
||||||
|
current_language: str | None = None
|
||||||
|
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
self.__path = path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> Path:
|
||||||
|
return self.__path
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDirectory(Content):
|
||||||
|
def load(self, subpath: str | Path) -> Content:
|
||||||
|
subpath = Path(subpath)
|
||||||
|
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:
|
||||||
|
return str(self.__value)
|
||||||
|
|
||||||
|
def __getitem__(self, key: Any) -> Any:
|
||||||
|
return DataField(self.__file_path, self.__value.get(key))
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Any]:
|
||||||
|
for it in self.__value:
|
||||||
|
yield DataField(self.__file_path, it)
|
||||||
|
|
||||||
|
|
||||||
|
class DataFile(Content):
|
||||||
|
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
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import check_output
|
from subprocess import check_output
|
||||||
from typing import Any
|
|
||||||
|
from jwebsite.content import Content
|
||||||
|
|
||||||
|
|
||||||
def git_creation_date(content: Any) -> datetime:
|
def git_creation_date(content: Content) -> datetime:
|
||||||
git_dir = check_output(["git", "rev-parse", "--show-toplevel"], encoding="utf-8")
|
git_dir = check_output(["git", "rev-parse", "--show-toplevel"], encoding="utf-8")
|
||||||
git_dir = git_dir.strip()
|
git_dir = git_dir.strip()
|
||||||
log = check_output(
|
log = check_output(
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
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}"
|
||||||
34
noxfile.py
34
noxfile.py
|
|
@ -1,12 +1,7 @@
|
||||||
"""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:
|
||||||
|
|
@ -21,38 +16,11 @@ 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")
|
session.run("python", "-m", "pytest", "--cov=jwebsite", "--cov-report=html")
|
||||||
for directory in _LOCALIZED_TESTS:
|
|
||||||
session.run("pybabel", "compile", "--domain=tests", f"--directory={directory}/locale", "--use-fuzzy")
|
|
||||||
session.run("python", "-m", "pytest", "--cov=jweb", "--cov-report=html")
|
|
||||||
|
|
||||||
|
|
||||||
@session()
|
@session()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
[project]
|
[project]
|
||||||
name = "jean-web"
|
name = "jean-website"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Jean-Web", email ="team@collectivit.org"}
|
{name = "Jean-Website", email ="team@collectivit.org"}
|
||||||
]
|
]
|
||||||
|
|
||||||
description = "Static site generator"
|
description = "Static site generator"
|
||||||
|
|
@ -29,14 +29,14 @@ dev = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
jweb = "jweb.cli:main"
|
jwebsite = "jwebsite.cli:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45"]
|
requires = ["setuptools>=45"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
strict = true
|
strict = true
|
||||||
files = "jweb/**/*.py,tests/**/*.py,noxfile.py"
|
files = "jwebsite/**/*.py,noxfile.py"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 110
|
line-length = 110
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pytest import mark, raises
|
|
||||||
|
|
||||||
from jweb.content import Content
|
|
||||||
from jweb.context import Context
|
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize("content", ["# Otters", b"# Otters"])
|
|
||||||
def test_load(datadir: Path, content: bytes | str) -> None:
|
|
||||||
context = Context(datadir)
|
|
||||||
context.load_extensions("jweb.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("jweb.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("jweb.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"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{% with document = content | markdown %}
|
|
||||||
{{- document }}
|
|
||||||
{%- endwith -%}
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{% with document = content | markdown %}
|
|
||||||
{{- document.meta.title }}
|
|
||||||
{%- endwith -%}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pytest import mark, raises
|
|
||||||
|
|
||||||
from jweb.content import Content
|
|
||||||
from jweb.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("jweb.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("jweb.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)
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{% for otter in content | yaml -%}
|
|
||||||
{{- otter }}
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
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"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
["steven", "peter"]
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
steven: ./relative-steven
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: Steven
|
||||||
|
mood: Angry
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Titre
|
||||||
|
---
|
||||||
|
|
||||||
|
Contenu
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Title
|
||||||
|
---
|
||||||
|
|
||||||
|
Content
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pytest import mark
|
|
||||||
|
|
||||||
from jweb.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"
|
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize("path", ["content.txt", Path("content.txt")])
|
|
||||||
def test_load(datadir: Path, path: str | Path) -> None:
|
|
||||||
context = Context(datadir)
|
|
||||||
context.render("test-load.html", "output.html", path=path)
|
|
||||||
|
|
||||||
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/subdir/content.txt", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "Weasel\n"
|
|
||||||
|
|
||||||
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "/content.txt\n/subdir/content.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def test_glob(datadir: Path) -> None:
|
|
||||||
context = Context(datadir)
|
|
||||||
context.render("test-glob.html", "output.html")
|
|
||||||
|
|
||||||
with open(datadir / "build" / "output.html", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "Otters\nCaiman\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_glob_localized(datadir: Path) -> None:
|
|
||||||
context = Context(datadir)
|
|
||||||
context.load_translations("tests", datadir / "locale", "fr")
|
|
||||||
context.render("test-glob.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\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_glob_localized_include_base_language(datadir: Path) -> None:
|
|
||||||
context = Context(datadir)
|
|
||||||
context.load_translations("tests", datadir / "locale", "fr")
|
|
||||||
context.render("test-glob-include-base-language.html", "output.html")
|
|
||||||
|
|
||||||
with open(datadir / "build" / "fr" / "output.html", encoding="utf-8") as ouput_file:
|
|
||||||
assert ouput_file.read() == "Caimans\nLoutres\n"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[jinja2: src/**.html]
|
|
||||||
encoding = utf-8
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Otters
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Otters
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Caimans
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Loutres
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Caiman
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Weasel
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
# 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-22 00:10+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"
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{% for content in glob("*.txt", include_base_language=True) | sort(attribute='path') -%}
|
|
||||||
{{ content -}}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{% for content in glob("*.txt") | sort(attribute='path') -%}
|
|
||||||
{{ content -}}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{{ gettext('Otters') }}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{{ 'content.txt' | load -}}
|
|
||||||
{{ 'not-localized-content.txt' | load -}}
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{{ path | load }}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{{animal}}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
{{ 'content.txt' | load | write }}
|
|
||||||
{{ 'subdir/content.txt' | load | write }}
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Yipee
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[Peter, Steven]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{%- for otter in site.content.load('otters.yml') -%}
|
||||||
|
<p>{{ otter }}</p>
|
||||||
|
{%- endfor %}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{ "assets/steven-avatar" | output }}
|
||||||
|
|
||||||
Loading…
Reference in New Issue