rewrite in python -- mostly feature complete
This commit is contained in:
parent
dabe830008
commit
ae6a433484
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
10
derivation.nix
Normal file
10
derivation.nix
Normal 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
1
sample_project/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
output/
|
3
sample_project/src/main.css
Normal file
3
sample_project/src/main.css
Normal file
@ -0,0 +1,3 @@
|
||||
thsi file doesn't have to be css'
|
||||
|
||||
it's just a test'
|
9
sample_project/src/main.md
Normal file
9
sample_project/src/main.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Page root
|
||||
---
|
||||
|
||||
# This is a test!
|
||||
|
||||
testingk
|
||||
|
||||
[to the testing subdirectory](./testing)
|
5
sample_project/src/testing/index.md
Normal file
5
sample_project/src/testing/index.md
Normal file
@ -0,0 +1,5 @@
|
||||
this is tsate
|
||||
|
||||
this is another test
|
||||
|
||||
tseting
|
4
sample_project/templates/article.html
Normal file
4
sample_project/templates/article.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block body_content %}
|
||||
{{ md_html|safe }}
|
||||
{% endblock body_content %}
|
9
sample_project/templates/base.html
Normal file
9
sample_project/templates/base.html
Normal file
@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>testing</title>
|
||||
</head>
|
||||
<body>
|
||||
{% block body_content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
24
setup.py
Normal file
24
setup.py
Normal 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
10
shell.nix
Normal 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
77
sssg.sh
@ -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
4
sssg/__main__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
17
sssg/cli.py
Normal file
17
sssg/cli.py
Normal 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
56
sssg/converters.py
Normal 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
71
sssg/document.py
Normal 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
31
sssg/liveupdate.py
Normal 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
72
sssg/site.py
Normal 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
7
testing.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
( pkgs.callPackage ./derivation.nix {} )
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user