refactor: Extract individual features into toggleable modules

This commit is contained in:
sigmasternchen 2025-01-07 20:14:49 +01:00
parent 8fe474561a
commit b2d8198d1d
12 changed files with 221 additions and 132 deletions

View file

@ -1,131 +0,0 @@
import argparse
import glob
import os
import markdown
import yaml
from jinja2 import Environment, FileSystemLoader
from yaml import Loader
jinja_env = Environment(
loader=FileSystemLoader("/")
)
def to_relative(path):
trimmed = path.removeprefix(os.getcwd())
if trimmed != path:
trimmed = "." + trimmed
return trimmed
def compile_markdown(data):
for entry in data:
if "markdown" in entry:
print(f"Compiling markdown for {entry['relative_filename']}...")
entry["markdown_compiled"] = markdown.markdown(entry["markdown"])
def render(data, tags, output_dir):
files_written = 0
for entry in data:
if "template" in entry:
template_path = os.path.realpath(os.path.dirname(entry["filename"]) + "/" + entry["template"])
template_dir = os.path.dirname(template_path)
print(f"Rendering template for {entry['relative_filename']}...")
template = jinja_env.get_template(template_path)
entry["rendered"] = template.render(current=entry, all=data, tags=tags, template_dir=template_dir)
if "rendered" in entry and "output" in entry:
files_written += 1
filename = os.path.realpath(output_dir + "/" + entry["output"])
print(f" ... writing to {to_relative(filename)}")
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w") as file:
file.write(entry["rendered"])
return files_written
def extract_tags(data):
tags = {}
for entry in data:
for tag in entry.get("tags", []):
entry_list = tags.get(tag, [])
entry_list.append(entry)
tags[tag] = entry_list
print(f"Found tags: " + repr(list(tags.keys())))
return tags
def handle_file_or_glob(globname):
results = []
for filename in glob.glob(os.path.realpath(globname)):
results.extend(handle_file(filename))
return results
def handle_file(filename):
print(f"Reading {to_relative(filename)}...")
with open(filename, "r") as file:
data = yaml.load(file, Loader)
data["filename"] = filename
data["relative_filename"] = to_relative(filename)
results = [data]
relative_dir = os.path.dirname(filename)
for filename in data.get("include", []):
filename = relative_dir + "/" + filename
sub_data = handle_file_or_glob(filename)
results.extend(sub_data)
return results
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--output", default="./output/")
args, filenames = parser.parse_known_args()
return args.output, filenames
def main():
output_dir, filenames = parse_arguments()
print(f"Output directory: {output_dir}")
print(f"Initial filenames: {filenames}")
print()
if len(filenames) == 0:
print("error: at least one filename needed")
exit(1)
data = []
for filename in filenames:
data.extend(handle_file_or_glob(filename))
print(f"Total number of entries: {len(data)}")
print()
compile_markdown(data)
tags = extract_tags(data)
files_written = render(data, tags, output_dir)
print(f"Total files written: {files_written}")
print()
print("Done.")
if __name__ == "__main__":
main()

25
grimoiressg/__main__.py Normal file
View file

@ -0,0 +1,25 @@
from grimoiressg.arguments import parse_arguments_to_initial_context
from grimoiressg.config import read_config
from grimoiressg.content_files import recursively_read_files
from grimoiressg.modules import available_modules
def apply_modules(data, config, context):
for module in config.get("enabled_modules", []):
print(f"Applying module {module}...")
available_modules[module](data, context)
print("")
def main():
context = parse_arguments_to_initial_context()
config = read_config(context)
data = recursively_read_files(context)
apply_modules(data, config, context)
print("Done.")
if __name__ == "__main__":
main()

31
grimoiressg/arguments.py Normal file
View file

@ -0,0 +1,31 @@
import argparse
def parse_arguments_to_initial_context():
parser = argparse.ArgumentParser(
description='''
Grimoire is a minimalistic Static Site Generator.
In the simplest case the only argument needed is at least one content file. \
The rest of the flags is used to customize the behavior.
'''
)
parser.add_argument("content_file", nargs='+', help="one or more content files")
parser.add_argument("-o", "--output", default="./output/", help="the output directory (default: ./output/)")
parser.add_argument("-c", "--config", help="the config file to use")
args, _ = parser.parse_known_args()
context = {
"output_dir": args.output,
"config_file": args.config,
"filenames": args.content_file
}
print(f"Output directory: {context['output_dir']}")
print(f"Config file: {context['config_file']}")
print("Content files:")
for filename in context["filenames"]:
print(f" - {filename}")
print()
return context

35
grimoiressg/config.py Normal file
View file

@ -0,0 +1,35 @@
import yaml
from yaml import Loader
from grimoiressg.modules import available_modules
def default_config():
return {
"enabled_modules": [
"tags",
"markdown",
"templating"
]
}
def read_config(context):
config_file = context.get("config_file", None)
if not config_file:
print("No config file given; using default config")
config = default_config()
else:
with open(config_file, "r") as file:
config = yaml.load(file, Loader) or {}
print("Enabled modules:")
for module in config.get("enabled_modules", []):
print(f" - {module}")
if module not in available_modules:
print(f" ERROR: Module does not exist")
exit(1)
print()
return config

View file

@ -0,0 +1,39 @@
import os
import yaml
from yaml import Loader
from grimoiressg.utils.files import for_each_glob, to_relative
def handle_file(filename):
print(f" Reading {to_relative(filename)}...")
with open(filename, "r") as file:
data = yaml.load(file, Loader)
data["filename"] = filename
data["relative_filename"] = to_relative(filename)
results = [data]
relative_dir = os.path.dirname(filename)
for filename in data.get("include", []):
filename = relative_dir + "/" + filename
sub_data = for_each_glob(filename, handle_file)
results.extend(sub_data)
return results
def recursively_read_files(context):
data = []
print("Reading content files...")
for filename in context["filenames"]:
data.extend(for_each_glob(filename, handle_file))
print(f"Read {len(data)} files in total.")
print()
return data

View file

@ -0,0 +1,9 @@
from grimoiressg.modules.markdown import compile_markdown
from grimoiressg.modules.tags import extract_tags
from grimoiressg.modules.templating import render_templates
available_modules = {
"tags": extract_tags,
"markdown": compile_markdown,
"templating": render_templates
}

View file

@ -0,0 +1,8 @@
import markdown
def compile_markdown(data, context):
for entry in data:
if "markdown" in entry:
print(f"Compiling markdown for {entry['relative_filename']}...")
entry["markdown_compiled"] = markdown.markdown(entry["markdown"])

View file

@ -0,0 +1,18 @@
def extract_tags(data, context):
tags = {}
for entry in data:
for tag in entry.get("tags", []):
entry_list = tags.get(tag, [])
entry_list.append(entry)
tags[tag] = entry_list
if tags:
print("Found tags:")
for tag in tags.keys():
print(f" - {tag} ({len(tags[tag])} files)")
else:
print("No tags found.")
context["tags"] = tags

View file

@ -0,0 +1,36 @@
import os
from grimoiressg.utils import to_relative
from jinja2 import Environment, FileSystemLoader
jinja_env = Environment(
loader=FileSystemLoader("/")
)
def render_templates(data, context):
files_written = 0
for entry in data:
if "template" in entry:
template_path = os.path.realpath(os.path.dirname(entry["filename"]) + "/" + entry["template"])
template_dir = os.path.dirname(template_path)
print(f"Rendering template for {entry['relative_filename']}...")
template = jinja_env.get_template(template_path)
entry["rendered"] = template.render(
**context,
current=entry,
all=data,
template_dir=template_dir
)
if "rendered" in entry and "output" in entry:
files_written += 1
filename = os.path.realpath(context["output_dir"] + "/" + entry["output"])
print(f" writing to {to_relative(filename)}")
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w") as file:
file.write(entry["rendered"])
print(f"{files_written} rendered")

View file

@ -0,0 +1 @@
from .files import to_relative

View file

@ -0,0 +1,18 @@
import glob
import os
def to_relative(path):
trimmed = path.removeprefix(os.getcwd())
if trimmed != path:
trimmed = "." + trimmed
return trimmed
def for_each_glob(glob_path, callback):
results = []
for filename in glob.glob(os.path.realpath(glob_path)):
results.extend(callback(filename))
return results

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "grimoire-ssg"
packages = [
{ include = "grimoire-ssg" }
{ include = "grimoiressg" }
]
version = "0.1.0"
description = "A minimalistic Static Site Generator"