diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c18dd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/derivation.nix b/derivation.nix
new file mode 100644
index 0000000..d4ec9f6
--- /dev/null
+++ b/derivation.nix
@@ -0,0 +1,10 @@
+{ python3Packages }:
+with python3Packages;
+buildPythonApplication {
+ pname = "sssg-py";
+ version = "1.0";
+
+ propagatedBuildInputs = [ markdown jinja2 watchdog ];
+
+ src = ./.;
+}
diff --git a/sample_project/.gitignore b/sample_project/.gitignore
new file mode 100644
index 0000000..ea1472e
--- /dev/null
+++ b/sample_project/.gitignore
@@ -0,0 +1 @@
+output/
diff --git a/sample_project/src/main.css b/sample_project/src/main.css
new file mode 100644
index 0000000..e0a53ad
--- /dev/null
+++ b/sample_project/src/main.css
@@ -0,0 +1,3 @@
+thsi file doesn't have to be css'
+
+it's just a test'
diff --git a/sample_project/src/main.md b/sample_project/src/main.md
new file mode 100644
index 0000000..3bb7143
--- /dev/null
+++ b/sample_project/src/main.md
@@ -0,0 +1,9 @@
+---
+title: Page root
+---
+
+# This is a test!
+
+testingk
+
+[to the testing subdirectory](./testing)
diff --git a/sample_project/src/testing/index.md b/sample_project/src/testing/index.md
new file mode 100644
index 0000000..0d4929f
--- /dev/null
+++ b/sample_project/src/testing/index.md
@@ -0,0 +1,5 @@
+this is tsate
+
+this is another test
+
+tseting
diff --git a/sample_project/templates/article.html b/sample_project/templates/article.html
new file mode 100644
index 0000000..8992131
--- /dev/null
+++ b/sample_project/templates/article.html
@@ -0,0 +1,4 @@
+{% extends 'base.html' %}
+{% block body_content %}
+{{ md_html|safe }}
+{% endblock body_content %}
diff --git a/sample_project/templates/base.html b/sample_project/templates/base.html
new file mode 100644
index 0000000..98ff6a2
--- /dev/null
+++ b/sample_project/templates/base.html
@@ -0,0 +1,9 @@
+
+
+ testing
+
+
+ {% block body_content %}
+ {% endblock %}
+
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..9e038a8
--- /dev/null
+++ b/setup.py
@@ -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
+)
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..994bed6
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,10 @@
+{ pkgs ? import {} }:
+ pkgs.mkShell {
+ nativeBuildInputs = [
+ pkgs.python3
+ pkgs.python311Packages.markdown
+ pkgs.python311Packages.jinja2
+ pkgs.python311Packages.watchdog
+ ];
+ }
+
diff --git a/sssg.sh b/sssg.sh
deleted file mode 100755
index 616bb65..0000000
--- a/sssg.sh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/bin/sh
-
-if [ "$1" = "-h" ]; then
- cat < 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
diff --git a/sssg/__main__.py b/sssg/__main__.py
new file mode 100644
index 0000000..9ae637f
--- /dev/null
+++ b/sssg/__main__.py
@@ -0,0 +1,4 @@
+from .cli import main
+
+if __name__ == "__main__":
+ main()
diff --git a/sssg/cli.py b/sssg/cli.py
new file mode 100644
index 0000000..e58e77e
--- /dev/null
+++ b/sssg/cli.py
@@ -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()
diff --git a/sssg/converters.py b/sssg/converters.py
new file mode 100644
index 0000000..3aa3d34
--- /dev/null
+++ b/sssg/converters.py
@@ -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,
+ }
diff --git a/sssg/document.py b/sssg/document.py
new file mode 100644
index 0000000..2360868
--- /dev/null
+++ b/sssg/document.py
@@ -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
+ )
diff --git a/sssg/liveupdate.py b/sssg/liveupdate.py
new file mode 100644
index 0000000..f137974
--- /dev/null
+++ b/sssg/liveupdate.py
@@ -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()
diff --git a/sssg/site.py b/sssg/site.py
new file mode 100644
index 0000000..7ede628
--- /dev/null
+++ b/sssg/site.py
@@ -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
+ )
diff --git a/testing.nix b/testing.nix
new file mode 100644
index 0000000..9b4f265
--- /dev/null
+++ b/testing.nix
@@ -0,0 +1,7 @@
+{ pkgs ? import {} }:
+ pkgs.mkShell {
+ nativeBuildInputs = [
+ ( pkgs.callPackage ./derivation.nix {} )
+ ];
+ }
+