roundabout,
created on Saturday, 20 April 2024, 12:39:59 (1713616799),
received on Tuesday, 23 April 2024, 11:54:33 (1713873273)
Author identity: vlad <vlad.muntoiu@gmail.com>
9923c5c13fbd0bb737507f08e27fb97a3212b20e
ampoule_ssg/__init__.py
@@ -4,6 +4,7 @@ import os
import re import shutil import contextlib import typingfrom datetime import datetime import jinja2 from ruamel.yaml import YAML
@@ -140,7 +141,7 @@ def parse_date_string(date_string):
class Document: """A type representing a document, which can be text or binary.""" def __init__(self, file_name, url_transform=lambda x: x):def __init__(self, file_name: typing.Union[str, bytes, os.PathLike], url_transform: typing.Callable = lambda x: x, front_matter_enabled: bool = True):"""Create a new document object. :param file_name: The name of the file to read.
@@ -159,24 +160,25 @@ class Document:
# Parse front matter if available. front_matter = "" initial_line = f.readline()if initial_line == "---\n":print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Front matter found", sep="")line = ""while line != "---\n":line = f.readline()if line != "---\n":front_matter += lineprint(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Front matter loaded", sep="")if front_matter:if front_matter_enabled: initial_line = f.readline() if initial_line == "---\n": print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Front matter found", sep="") line = "" while line != "---\n": line = f.readline() if line != "---\n": front_matter += line print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Front matter loaded", sep="") if front_matter and front_matter_enabled:self.front_matter = self.front_matter.load(front_matter) print(self.front_matter, type(self.front_matter)) if "DATE" in self.front_matter: self.date = parse_date_string(self.front_matter["DATE"]) else: # put it backelif front_matter_enabled: # put it backself.content = initial_line print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Reading content", sep="")
@@ -204,14 +206,28 @@ class Document:
def __str__(self): return self.content def __getitem__(self, item):def __getitem__(self, item: str):"""Get an item from the front matter of the document""" return self.front_matter[item] def __setitem__(self, item: str, value: typing.Any): """Set an item in the front matter of the document""" self.front_matter[item] = value def __delitem__(self, item: str): """Delete an item from the front matter of the document""" del self.front_matter[item] def __contains__(self, item: str): """Check if an item is in the front matter of the document""" return item in self.front_matter class Index: """A type representing an index of documents.""" def __init__(self, directory, recursive=False, url_transform=lambda x: x, sort_by=lambda x: x.file_name, exclude=None):def __init__(self, directory: typing.Union[str, bytes, os.PathLike], recursive: bool = False, url_transform: typing.Callable = lambda x: x, sort_by: typing.Callable = lambda x: x.file_name, exclude: typing.Union[str, None] = None, static: bool = False):"""Create a new index object. :param directory: The directory to read the files from.
@@ -221,6 +237,7 @@ class Index:
:param exclude: Regular expression to exclude files from the index. """ self.directory = directory self.static = static# Temporarily move to the specified directory in order to read the files. if exclude: regex = re.compile(exclude)
@@ -232,7 +249,7 @@ class Index:
else: self.file_names = [i for i in os.listdir() if os.path.isfile(i) and not regex.search(i)] self.documents = sorted([Document(i, url_transform) for i in self.file_names], key=sort_by)self.documents = sorted([Document(i, url_transform, front_matter_enabled=not static) for i in self.file_names], key=sort_by)self.__current_index = 0 def __iter__(self):
@@ -255,18 +272,18 @@ class Index:
class Site: """A type representing a website.""" def __init__(self, build_dir, template_dir="templates"):def __init__(self, build_dir: typing.Union[str, bytes, os.PathLike], template_dir: typing.Union[str, bytes, os.PathLike] = "templates"):"""Create a new site object. :param build_dir: The directory to build the site in. :param template_dir: The directory to read the templates from. """ self.build_dir = build_dirself.template_engine = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))self.pages = {}self.context = {}self.build_dir: str = build_dir self.template_engine: jinja2.Environment = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) self.pages: dict[str, typing.Union[Static, Page]] = {} self.context: dict[str, typing.Any] = {}def add_page(self, location, page):def add_page(self, location: typing.Union[str, bytes, os.PathLike], page: typing.Union["Static", "Page"]):"""Add a page to the site. :param location: The location the page should be saved to.
@@ -277,7 +294,7 @@ class Site:
location = location.lstrip("/") # interpret it as site root, not OS root self.pages[location] = page def add_from_index(self, index, location, template=None, static=False, **kwargs):def add_from_index(self, index: Index, location: typing.Union[str, bytes, os.PathLike], template: typing.Union[str, None] = None, **kwargs):"""Add pages to the site from an index. :param index: The index to read the documents from.
@@ -288,14 +305,14 @@ class Site:
""" location = location.lstrip("/") # interpret it as site root, not OS root kwargs = {**self.context, **kwargs} if static:if index.static:for document in index: self.pages[os.path.join(location, document.file_name)] = Static(self, document) else: for document in index: self.pages[os.path.join(location, document.file_name)] = Page(self, template, document, **kwargs) def filter(self, name):def filter(self, name: str):"""Decorator to add a filter to the template engine. :param name: The name the filter will be used with in Jinja2.
@@ -306,7 +323,7 @@ class Site:
return decorator def test(self, name):def test(self, name: str):"""Decorator to add a test to the template engine. :param name: The name the test will be used with in Jinja2.
@@ -337,12 +354,12 @@ class Site:
class Page(str): """A type representing a page, which is a rendered template.""" def __new__(cls, site, template, document=None, **kwargs):def __new__(cls, site: Site, template: str, document: Document = None, **kwargs):kwargs = {**site.context, **kwargs} return site.template_engine.get_template(template).render(document=document, **kwargs) class Static(bytes): """A type representing a static file, which is binary content that is not templated.""" def __new__(cls, site, document):def __new__(cls, site: Site, document: Document):return document.content
ampoule_ssg/extra_jinja.py
@@ -0,0 +1,211 @@
import math import datetime def init_filters(site): @site.filter("first_paragraph") def first_paragraph(value): return value.split("\n\n")[0] @site.filter("split") def split(value, sep, maxsplit=-1): return value.split(sep, maxsplit) @site.filter("rsplit") def rsplit(value, sep, maxsplit=-1): return value.rsplit(sep, maxsplit) @site.filter("partition") def partition(value, sep): return value.partition(sep) @site.filter("rpartition") def rpartition(value, sep): return value.rpartition(sep) @site.filter("lstrip") def lstrip(value, chars=None): return value.lstrip(chars) @site.filter("rstrip") def rstrip(value, chars=None): return value.rstrip(chars) @site.filter("strip") def strip(value, chars=None): return value.strip(chars) @site.filter("removeprefix") def removeprefix(value, prefix): return value.removeprefix(prefix) @site.filter("removesuffix") def removesuffix(value, suffix): return value.removesuffix(suffix) @site.filter("remove") def remove(value, string): return value.replace(string, "") @site.filter("strftime") def strftime(value: datetime.datetime | datetime.date | datetime.time, format_): return value.strftime(format_) @site.filter("unixtime") def unixtime(value: datetime.datetime | datetime.date | datetime.time): return round(value.timestamp()) @site.filter("strptime") def strptime(value, format_): return datetime.datetime.strptime(value, format_) @site.filter("round") def round_(value, decimals=0): return round(value, decimals) @site.filter("floor") def floor(value): return math.floor(value) @site.filter("ceiling") def ceiling(value): return math.ceil(value) @site.filter("units") def units(value, decimals=2, scale=1024, suffixes=("B", "kiB", "MiB", "GiB", "TiB", "PiB")): for unit in suffixes: if value < scale: break value /= scale if int(value) == value: return int(value) + "\u202f" + unit return round(value * 10 ** decimals) / 10 ** decimals + "\u202f" + unit @site.filter("conditional") def conditional(value, true_value, false_value): return true_value if value else false_value @site.filter("debug_log_value") def debug_log_value(value): print(value) return value @site.filter("harvester_protection") def harvester_protection(value): return "".join(f"&#x{ord(char):x};" for char in value) @site.filter("pretty_number") def pretty_number(value, separator="\u202f"): return f"{value:,}".replace(",", separator) @site.filter("hex") def hex_(value): return hex(value).removeprefix("0x") @site.filter("oct") def oct_(value): return oct(value).removeprefix("0o") @site.filter("bin") def bin_(value): return bin(value).removeprefix("0b") @site.filter("join") def join(value, separator=" "): return separator.join(value) @site.filter("replace") def replace(value, old, new): return value.replace(old, new) @site.filter("file_stat") def file_stat(value): return value.stat() @site.filter("path_cat") def path_cat(*value): return os.path.join(*value) @site.filter("path_dirname") def path_dirname(value): return os.path.dirname(value) @site.filter("path_basename") def path_basename(value): return os.path.basename(value) @site.filter("path_splitext") def path_splitext(value): return os.path.splitext(value) @site.filter("type") def type_(value): return type(value) @site.filter("type_name") def type_name(value): return type(value).__name__ @site.filter("each_nth") def nth(value, step, start=0): return value[start::step] @site.filter("key_list") def key_list(value): return list(value.keys()) @site.filter("value_list") def value_list(value): return list(value.values()) @site.filter("item_list") def item_list(value): return list(value.items()) @site.filter("remove_dupes") def remove_dupes(value): list = [] for i in value: if i not in list: list.append(i) return list @site.filter("percent") def percent(value, maximum, decimals=2): return round(value * maximum / 100, decimals) + "%" @site.filter("percent_of") def percent_of(value, total, decimals=2): return round(value / total * 100, decimals) + "%" @site.filter("permille") def permille(value, maximum, decimals=2): return round(value * maximum / 1000, decimals) + "‰" @site.filter("permille_of") def permille_of(value, total, decimals=2): return round(value / total * 1000, decimals) + "‰" @site.filter("timezone") def timezone(value, timezone): return value.astimezone(timezone) def init_tests(site): @site.test("instance_of") def isinstance_(value, type_): return isinstance(value, type_) @site.test("only_chars") def only_chars(value, chars): return set(value).issubset(set(chars)) @site.test("empty") def is_empty(value): return not len(value) @site.test("not_empty") def is_not_empty(value): return len(value) @site.test("past_date") def past_date(value): return value < datetime.datetime.now() @site.test("future_date") def future_date(value): return value > datetime.datetime.now() @site.test("numeric") def is_numeric(value): try: float(value) return True except ValueError: return False @site.test("startswith") def startswith(value, prefix): return value.startswith(prefix) @site.test("endswith") def endswith(value, suffix): return value.endswith(suffix) @site.test("matches_regex") def matches_regex(value, pattern): return re.match(pattern, value) is not None @site.test("is_callable") def is_callable(value): return callable(value) @site.test("all") def all_(value): return all(value) @site.test("any") def any_(value): return any(value) @site.test("longer_than") def longer_than(value, length): return len(value) > length @site.test("shorter_than") def shorter_than(value, length): return len(value) < length @site.test("weekend") def weekend(value): return value.weekday() >= 5 @site.test("weekday") def weekday(value): return value.weekday() < 5 @site.test("leap_year") def leap_year(value): return value.year % 4 == 0 and (value.year % 100 != 0 or value.year % 400 == 0) @site.test("almost_equal") def almost_equal(value, other, tolerance=1e-6): return abs(value - other) < tolerance
extra_jinja.py
@@ -1,211 +0,0 @@
import mathimport datetimedef init_filters(site):@site.filter("first_paragraph")def first_paragraph(value):return value.split("\n\n")[0]@site.filter("split")def split(value, sep, maxsplit=-1):return value.split(sep, maxsplit)@site.filter("rsplit")def rsplit(value, sep, maxsplit=-1):return value.rsplit(sep, maxsplit)@site.filter("partition")def partition(value, sep):return value.partition(sep)@site.filter("rpartition")def rpartition(value, sep):return value.rpartition(sep)@site.filter("lstrip")def lstrip(value, chars=None):return value.lstrip(chars)@site.filter("rstrip")def rstrip(value, chars=None):return value.rstrip(chars)@site.filter("strip")def strip(value, chars=None):return value.strip(chars)@site.filter("removeprefix")def removeprefix(value, prefix):return value.removeprefix(prefix)@site.filter("removesuffix")def removesuffix(value, suffix):return value.removesuffix(suffix)@site.filter("remove")def remove(value, string):return value.replace(string, "")@site.filter("strftime")def strftime(value: datetime.datetime | datetime.date | datetime.time, format_):return value.strftime(format_)@site.filter("unixtime")def unixtime(value: datetime.datetime | datetime.date | datetime.time):return round(value.timestamp())@site.filter("strptime")def strptime(value, format_):return datetime.datetime.strptime(value, format_)@site.filter("round")def round_(value, decimals=0):return round(value, decimals)@site.filter("floor")def floor(value):return math.floor(value)@site.filter("ceiling")def ceiling(value):return math.ceil(value)@site.filter("units")def units(value, decimals=2, scale=1024,suffixes=("B", "kiB", "MiB", "GiB", "TiB", "PiB")):for unit in suffixes:if value < scale:breakvalue /= scaleif int(value) == value:return int(value) + "\u202f" + unitreturn round(value * 10 ** decimals) / 10 ** decimals + "\u202f" + unit@site.filter("conditional")def conditional(value, true_value, false_value):return true_value if value else false_value@site.filter("debug_log_value")def debug_log_value(value):print(value)return value@site.filter("harvester_protection")def harvester_protection(value):return "".join(f"&#x{ord(char):x};" for char in value)@site.filter("pretty_number")def pretty_number(value, separator="\u202f"):return f"{value:,}".replace(",", separator)@site.filter("hex")def hex_(value):return hex(value).removeprefix("0x")@site.filter("oct")def oct_(value):return oct(value).removeprefix("0o")@site.filter("bin")def bin_(value):return bin(value).removeprefix("0b")@site.filter("join")def join(value, separator=" "):return separator.join(value)@site.filter("replace")def replace(value, old, new):return value.replace(old, new)@site.filter("file_stat")def file_stat(value):return value.stat()@site.filter("path_cat")def path_cat(*value):return os.path.join(*value)@site.filter("path_dirname")def path_dirname(value):return os.path.dirname(value)@site.filter("path_basename")def path_basename(value):return os.path.basename(value)@site.filter("path_splitext")def path_splitext(value):return os.path.splitext(value)@site.filter("type")def type_(value):return type(value)@site.filter("type_name")def type_name(value):return type(value).__name__@site.filter("each_nth")def nth(value, step, start=0):return value[start::step]@site.filter("key_list")def key_list(value):return list(value.keys())@site.filter("value_list")def value_list(value):return list(value.values())@site.filter("item_list")def item_list(value):return list(value.items())@site.filter("remove_dupes")def remove_dupes(value):list = []for i in value:if i not in list:list.append(i)return list@site.filter("percent")def percent(value, maximum, decimals=2):return round(value * maximum / 100, decimals) + "%"@site.filter("percent_of")def percent_of(value, total, decimals=2):return round(value / total * 100, decimals) + "%"@site.filter("permille")def permille(value, maximum, decimals=2):return round(value * maximum / 1000, decimals) + "‰"@site.filter("permille_of")def permille_of(value, total, decimals=2):return round(value / total * 1000, decimals) + "‰"@site.filter("timezone")def timezone(value, timezone):return value.astimezone(timezone)def init_tests(site):@site.test("instance_of")def isinstance_(value, type_):return isinstance(value, type_)@site.test("only_chars")def only_chars(value, chars):return set(value).issubset(set(chars))@site.test("empty")def is_empty(value):return not len(value)@site.test("not_empty")def is_not_empty(value):return len(value)@site.test("past_date")def past_date(value):return value < datetime.datetime.now()@site.test("future_date")def future_date(value):return value > datetime.datetime.now()@site.test("numeric")def is_numeric(value):try:float(value)return Trueexcept ValueError:return False@site.test("startswith")def startswith(value, prefix):return value.startswith(prefix)@site.test("endswith")def endswith(value, suffix):return value.endswith(suffix)@site.test("matches_regex")def matches_regex(value, pattern):return re.match(pattern, value) is not None@site.test("is_callable")def is_callable(value):return callable(value)@site.test("all")def all_(value):return all(value)@site.test("any")def any_(value):return any(value)@site.test("longer_than")def longer_than(value, length):return len(value) > length@site.test("shorter_than")def shorter_than(value, length):return len(value) < length@site.test("weekend")def weekend(value):return value.weekday() >= 5@site.test("weekday")def weekday(value):return value.weekday() < 5@site.test("leap_year")def leap_year(value):return value.year % 4 == 0 and (value.year % 100 != 0 or value.year % 400 == 0)@site.test("almost_equal")def almost_equal(value, other, tolerance=1e-6):return abs(value - other) < tolerance