--- /dev/null
+#!/bin/python3
+# BSD 2-Clause License
+#
+# Copyright (c) 2023, Georgios Atheridis <georgios@atheridis.org>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import argparse
+import os
+import re
+import tomllib
+
+import markdown
+
+
+regex_extend = re.compile(r"^{%\s?extend (\"|')(.+)\1\s?%}$")
+regex_variable = re.compile(r"{{\s?(\S+)\s?}}")
+regex_execute = re.compile(r"{%\s?file (\"|')(.+)\1\s?%}")
+
+
+def md_to_html(md: str) -> str:
+ return markdown.markdown(md, extensions=["tables"])
+
+
+def initialize_values(data: dict):
+ pass
+
+
+def get_value(data: dict, namespace: tuple, key: str):
+ value = data.get(key)
+ for namespace_item in namespace:
+ data = data.get(namespace_item, data)
+ value = data.get(key, value)
+ if value:
+ return value
+ if key == "_name":
+ return namespace[-1]
+
+
+def update_value(data: dict, namespace: tuple, key: str, value):
+ for namespace_item in namespace:
+ data = data.setdefault(namespace_item, {})
+ data[key] = value
+
+
+def split_path(path: str) -> tuple:
+ rest, tail = os.path.split(path)
+ if rest in ("", os.path.sep):
+ return (tail,)
+ return split_path(rest) + (tail,)
+
+
+def interpret(file_value: str, data: dict, namespace: tuple) -> str:
+ while regex_execute.search(file_value) or regex_variable.search(file_value):
+ while file_to_run := regex_execute.search(file_value):
+ file_to_run = file_to_run.group(2)
+ _, ext = os.path.splitext(file_to_run)
+ _value = ""
+ with open(os.path.join(data["_TEMPLATES"], file_to_run), "r") as f:
+ if ext == ".py":
+ d = {"data": data, "namespace": namespace, "get_value": get_value}
+ exec(f.read(), d)
+ _value = d["_value"]
+ elif ext == ".md":
+ _value = md_to_html(f.read())
+ else:
+ _value = f.read()
+ file_value = regex_execute.sub(_value, file_value, 1)
+ while variable := regex_variable.search(file_value):
+ varspace = variable.group(1).split(".")
+ try:
+ varspace.remove("self")
+ except ValueError:
+ varspace = tuple(varspace)
+ else:
+ varspace = namespace + tuple(varspace)
+ file_value = regex_variable.sub(
+ str(get_value(data, varspace[:-1], varspace[-1])), file_value, 1
+ )
+ return file_value
+
+
+def generate_output(file: str, data: dict, namespace: tuple) -> str:
+ _, file_type = os.path.splitext(file)
+ with open(file, "r") as in_file:
+ _value = in_file.read()
+ if result := regex_extend.search(_value.splitlines()[0]):
+ update_value(data, namespace, "_extend", result.group(2))
+ _value = _value.removeprefix(result.group(0) + "\n")
+ _value = interpret(_value, data, namespace)
+ if file_type == ".md":
+ _value = md_to_html(_value)
+ update_value(data, namespace, "_value", _value)
+
+ if _extend := get_value(data, namespace, "_extend"):
+ update_value(data, namespace, "_extend", "")
+ generate_output(
+ os.path.join(data["_TEMPLATES"], _extend), data, namespace
+ )
+ return str(get_value(data, namespace, "_value"))
+
+
+def main(args):
+ # Load toml file
+ data = tomllib.load(args.data)
+ args.data.close()
+
+ # Assign default values if not set
+ if args.templates:
+ data["_TEMPLATES"] = args.templates
+ elif not data.get("_TEMPLATES"):
+ data["_TEMPLATES"] = "templates"
+
+ if args.page_root:
+ data["_PAGE_ROOT"] = args.page_root
+ elif not data.get("_PAGE_ROOT"):
+ data["_PAGE_ROOT"] = "pages"
+
+ if args.out:
+ data["_OUT"] = args.out
+ elif not data.get("_OUT"):
+ data["_OUT"] = "out"
+
+ rel_path = os.path.relpath(args.page, data["_PAGE_ROOT"])
+ rel_path_no_type, file_type = os.path.splitext(rel_path)
+ namespace = split_path(rel_path_no_type)
+ os.makedirs(os.path.join(data["_OUT"], os.path.split(rel_path)[0]), exist_ok=True)
+ if file_type == ".md":
+ rel_path = rel_path_no_type + ".html"
+ with open(os.path.join(data["_OUT"], rel_path), "w") as out_file:
+ out_file.write(generate_output(args.page, data, namespace))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--page", type=str)
+ parser.add_argument("--page-root", type=str)
+ parser.add_argument("--templates", type=str)
+ parser.add_argument("--data", default="data.toml", type=argparse.FileType("rb"))
+ parser.add_argument("--out", type=str)
+ args = parser.parse_args()
+ main(args)