rewrite in python -- mostly feature complete

This commit is contained in:
stupidcomputer 2025-01-05 15:16:17 -06:00
parent dabe830008
commit ae6a433484
18 changed files with 334 additions and 77 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

10
derivation.nix Normal file
View File

@ -0,0 +1,10 @@
{ python3Packages }:
with python3Packages;
buildPythonApplication {
pname = "sssg-py";
version = "1.0";
propagatedBuildInputs = [ markdown jinja2 watchdog ];
src = ./.;
}

1
sample_project/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output/

View File

@ -0,0 +1,3 @@
thsi file doesn't have to be css'
it's just a test'

View File

@ -0,0 +1,9 @@
---
title: Page root
---
# This is a test!
testingk
[to the testing subdirectory](./testing)

View File

@ -0,0 +1,5 @@
this is tsate
this is another test
tseting

View File

@ -0,0 +1,4 @@
{% extends 'base.html' %}
{% block body_content %}
{{ md_html|safe }}
{% endblock body_content %}

View File

@ -0,0 +1,9 @@
<html>
<head>
<title>testing</title>
</head>
<body>
{% block body_content %}
{% endblock %}
</body>
</html>

24
setup.py Normal file
View File

@ -0,0 +1,24 @@
from setuptools import setup, find_packages
setup(
name = 'sssg',
version = '1.0.0',
author = 'stupidcomputer',
author_email = 'ryan@beepboop.systems',
url = 'https://git.beepboop.systems/stupidcomputer/sssg',
description = 'the stupid static site generator',
license = 'GPLv3',
entry_points = {
'console_scripts': [
'sssg = sssg.__main__:main'
]
},
packages=["sssg"],
classifiers = (
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
"Environment :: Console"
),
zip_safe = False
)

10
shell.nix Normal file
View File

@ -0,0 +1,10 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [
pkgs.python3
pkgs.python311Packages.markdown
pkgs.python311Packages.jinja2
pkgs.python311Packages.watchdog
];
}

77
sssg.sh
View File

@ -1,77 +0,0 @@
#!/bin/sh
if [ "$1" = "-h" ]; then
cat <<EOF
ssg.sh
-h: print help
-s: spin up an http server with python3 -m http.server
-d: deploy website to beepboop.systems
EOF
exit
fi
# if we're trying to do anything, check if the directory has a valid root
if [ ! -f ".sssg_generated" ]; then
printf "it doesn't seem like I'm in a static site directory -- is that true?\n"
exit 1
fi
if [ "$1" = "-s" ]; then
python3 -m http.server -d output
exit
fi
if [ "$1" = "-d" ]; then
if [ -f ".deploy" ]; then
sh ./.deploy
else
printf "configure a deploy script first!\n"
exit 1
fi
exit
fi
files=$(find -type f | grep -v "output/")
directories=$(find -type d | grep -v "output/")
IFS='
'
mkdir -p ./output
# if there's special things that need to run, run them
if [ -f ".special_commands" ]; then
sh ./.special_commands
fi
for i in $directories; do
if [ ! "$i" = "./output" ]; then
mkdir -p "./output/$i"
fi
done
# only commits with 'CHANGE' in them go into the changelog
git log --grep 'CHANGE' > output/changelog.rst
pandoc -s --template=./template.html -f rst -t html -o "output/changelog.html" "output/changelog.rst"
rm changelog.rst
set -x
for i in $files; do
without_extension=${i%.*}
case $i in
*.rst)
pandoc -s --template=./template.html -f rst -t html -o "output/$without_extension.html" "$without_extension.rst"
;;
"./ssg.sh") # don't copy this file
;;
"./shell.nix") # ditto
;;
"./template.html")
;;
*)
cp "$i" "output/$i"
;;
esac
done
set +x

4
sssg/__main__.py Normal file
View File

@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()

17
sssg/cli.py Normal file
View File

@ -0,0 +1,17 @@
from sssg.site import Site
import sssg.liveupdate # circular dependency resolution
import sys
import os
def main(args=sys.argv):
command = args[1]
if command == "rebuild":
cwd = os.getcwd()
site = Site.from_directory(cwd)
site.rebuild()
elif command == "deploy":
os.execv('/bin/sh', ['/bin/sh', '.deploy'])
elif command == "update":
sssg.liveupdate.liveupdate()

56
sssg/converters.py Normal file
View File

@ -0,0 +1,56 @@
from sssg.document import DocumentType, Document
from markdown import Markdown
from jinja2 import Environment
from typing import Self
import os
class Converter:
document: Document
jinja_environment: Environment
def __init__(self: Self, document: Document, environment: Environment):
self.document = document
self.jinja_environment = environment
def realize_to_output(self: Self):
# should be subclassed
pass
class ConverterConfiguration:
converters = dict[DocumentType, Converter]
jinja_environment: Environment
def __init__(self: Self, converters: dict[DocumentType, Converter], environment: Environment):
self.converters = converters
self.jinja_environment = environment
def convert(self: Self, document: Document):
self.converters[document.doc_type](document, self.jinja_environment).realize_to_output()
class BasicConverter(Converter):
def realize_to_output(self: Self):
with open(self.document.file_path, "r") as file:
contents = file.read()
md = Markdown(extensions=["meta", "fenced_code", "tables"])
html = md.convert(contents)
template = self.jinja_environment.get_template("article.html")
with open(self.document.result_path, "w") as file:
file.write(template.render(md_html=html))
BlogConverter = BasicConverter
class BinaryConverter(Converter):
def realize_to_output(self: Self):
with open(self.document.file_path, "rb") as origin:
with open(self.document.result_path, "wb") as target:
target.write(origin.read())
class SensibleConverterConfiguration(ConverterConfiguration):
def __init__(self: Self, environment: Environment):
self.jinja_environment = environment
self.converters = {
DocumentType.generic_markdown: BasicConverter,
DocumentType.blog: BlogConverter,
DocumentType.binary: BinaryConverter,
}

71
sssg/document.py Normal file
View File

@ -0,0 +1,71 @@
import markdown
from dataclasses import dataclass
from typing import Self
from enum import auto, Enum
class DocumentType(Enum):
generic_markdown = auto()
blog = auto()
binary = auto()
@dataclass
class Document:
doc_type: DocumentType
file_path: str
@property
def result_path(self: Self) -> str:
# change the 'src' to 'output' -- loop backwards and replace
splitted = self.file_path.split('/')
found_src = False
# can't use reversed(enumerate(...)) -- so use a workaround
for index, value in sorted(enumerate(splitted), reverse=True):
if value == "src":
splitted[index] = "output"
found_src = True
break
if not found_src:
raise ValueError("Your source directory must be named 'src/'")
file_path = '/'.join(splitted)
if self.doc_type == DocumentType.binary:
return file_path
elif (
self.doc_type == DocumentType.generic_markdown or
self.doc_type == DocumentType.blog
):
splitted = file_path.split('.')
return '.'.join(splitted[:-1] + ["html"])
@classmethod
def from_filepath(cls: Self, file_path: str) -> Self:
# this is stupid, but try to detect DocumentType from the file
# extension, and then disambiguate into the DocumentType.blog and
# DocumentType.generic_markdown cases as needed
is_markdown = file_path.endswith(".md")
if is_markdown:
with open(file_path, "r") as file:
contents = file.read()
md = markdown.Markdown(extensions=["meta"])
md.convert(contents)
try:
is_blog = md.Meta["blog"] == "yes"
except KeyError:
is_blog = False
if is_blog:
doc_type = DocumentType.blog
else:
doc_type = DocumentType.generic_markdown
else:
doc_type = DocumentType.binary
return cls(
doc_type=doc_type,
file_path=file_path
)

31
sssg/liveupdate.py Normal file
View File

@ -0,0 +1,31 @@
import time
import importlib
import os
from watchdog.observers import Observer
from watchdog.events import LoggingEventHandler
import sssg.cli # circular dependency resolution
class Handler(LoggingEventHandler):
def on_modified(self, event):
if os.path.isdir(event.src_path):
print("{} was ignored (directory)".format(event.src_path))
return
print("{} triggered reload".format(event.src_path))
time.sleep(0.25) # reduce some nvim race conditions
sssg.cli.main(["", "rebuild"])
on_created = on_modified
def liveupdate():
event_handler = Handler()
observer = Observer()
observer.schedule(event_handler, './src', recursive=True)
observer.schedule(event_handler, './templates', recursive=True)
observer.start()
print("observer started")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()

72
sssg/site.py Normal file
View File

@ -0,0 +1,72 @@
import os
from jinja2 import Environment, FileSystemLoader
from typing import Self
from pathlib import Path
from sssg.document import Document, DocumentType
from sssg.converters import SensibleConverterConfiguration
def dir_walk(dirpath: str) -> list[str]:
filepaths: list[str] = []
for root, dirs, files in os.walk(dirpath):
for filename in files:
complete = os.path.join(root, filename)
print(complete)
filepaths.append(complete)
for dirname in dirs:
complete = os.path.join(root, dirname)
print(complete)
filepaths += dir_walk(complete)
filepaths = [path for path in filepaths if '~' not in path]
print(filepaths)
return filepaths
class Site:
dir_path: str
documents: list[Document]
jinja_environment: Environment
def __init__(
self: Self,
dir_path: str,
documents: list[Document],
):
self.dir_path = dir_path
self.documents = documents
self.jinja_environment = Environment(
loader=FileSystemLoader(
os.path.join(self.dir_path, "templates")
)
)
def _ensure_output_directories(self: Self):
output_dirs = [os.path.dirname(doc.result_path) for doc in self.documents]
# remove duplicates
output_dirs = list(set(output_dirs))
for path in output_dirs:
Path(path).mkdir(parents=True, exist_ok=True)
def rebuild(
self: Self,
):
self._ensure_output_directories()
config = SensibleConverterConfiguration(self.jinja_environment)
for document in self.documents:
config.convert(document)
@classmethod
def from_directory(cls: Self, dir_path: str):
src_dir = os.path.join(dir_path, "src")
paths = dir_walk(src_dir)
paths = list(set(paths))
documents = [
Document.from_filepath(path)
for path in paths
]
return cls(
dir_path=dir_path,
documents=documents
)

7
testing.nix Normal file
View File

@ -0,0 +1,7 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [
( pkgs.callPackage ./derivation.nix {} )
];
}