Compare commits
7 Commits
ded054147b
...
8e64e48097
Author | SHA1 | Date | |
---|---|---|---|
8e64e48097 | |||
fab58de71d | |||
0b7bfddd64 | |||
9e2ee75927 | |||
3503d1d42f | |||
6d1ea43089 | |||
d68cfe15fe |
4
.gitignore
vendored
4
.gitignore
vendored
@ -171,4 +171,6 @@ cython_debug/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
dist
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"stsg"
|
||||||
|
]
|
||||||
|
}
|
0
dist/example_page_1/en/index.html
vendored
0
dist/example_page_1/en/index.html
vendored
0
dist/example_page_2/de/index.html
vendored
0
dist/example_page_2/de/index.html
vendored
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[project]
|
||||||
|
name = "stsg"
|
||||||
|
dependencies = [
|
||||||
|
"watchdog~=6.0.0"
|
||||||
|
]
|
||||||
|
dynamic = []
|
||||||
|
authors = []
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
classifiers = []
|
||||||
|
version = "0.0.0"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
stsg = "stsg.__main__:build"
|
||||||
|
stsg_dev = "stsg.__main__:hot_reload"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling", "hatch-requirements-txt"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["stsg/*.py"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["stsg"]
|
||||||
|
|
||||||
|
[tool.hatch.metadata]
|
||||||
|
allow-direct-references = true
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
|
37
src/index.html
Normal file
37
src/index.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>STSG</title>
|
||||||
|
<link rel="stylesheet" href="static/bulma.min.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header (Navbar) -->
|
||||||
|
<nav
|
||||||
|
class="navbar is-primary"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="main navigation"
|
||||||
|
>
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="#">
|
||||||
|
<strong>Static Translated Site Generator</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<content />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="content has-text-centered">
|
||||||
|
<p><strong>STSG</strong> by Hazel. © 2025</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
21551
src/static/bulma.css
vendored
Normal file
21551
src/static/bulma.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
src/static/bulma.min.css
vendored
Normal file
3
src/static/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
99
stsg/__main__.py
Normal file
99
stsg/__main__.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import logging
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .config import SOURCE_DIRECTORY, CODE_DIRECTORY
|
||||||
|
from .build import build as complete_build
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("stsg")
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
complete_build()
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownChangeHandler(FileSystemEventHandler):
|
||||||
|
def on_modified(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = os.path.basename(event.src_path)
|
||||||
|
if filename.startswith('.') or filename.endswith(('~', '.tmp', '.swp')):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("%s changed, building", event.src_path)
|
||||||
|
build()
|
||||||
|
|
||||||
|
|
||||||
|
class PythonChangeHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, command):
|
||||||
|
self.logger = logging.getLogger("stsg.hot_reload")
|
||||||
|
|
||||||
|
self.env = os.environ.copy()
|
||||||
|
self.env["IS_CHILD"] = "true"
|
||||||
|
|
||||||
|
self.command = command
|
||||||
|
self.process = self.start_process()
|
||||||
|
|
||||||
|
def start_process(self):
|
||||||
|
self.logger.info("Starting process...")
|
||||||
|
return subprocess.Popen(self.command, env=self.env)
|
||||||
|
|
||||||
|
def restart_process(self):
|
||||||
|
self.logger.info("Restarting process...")
|
||||||
|
self.process.kill()
|
||||||
|
self.process = self.start_process()
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
|
||||||
|
filename = os.path.basename(event.src_path)
|
||||||
|
if filename.startswith('.') or filename.endswith(('~', '.tmp', '.swp')):
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.src_path.endswith(".py"):
|
||||||
|
self.logger.info(f"Detected change: {event.src_path}")
|
||||||
|
self.restart_process()
|
||||||
|
|
||||||
|
def stop_process(self):
|
||||||
|
if self.process:
|
||||||
|
self.process.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def build_on_change():
|
||||||
|
build()
|
||||||
|
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(MarkdownChangeHandler(), path=SOURCE_DIRECTORY, recursive=True)
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1) # Keeps the thread alive
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
|
||||||
|
observer.join()
|
||||||
|
|
||||||
|
|
||||||
|
def hot_reload():
|
||||||
|
if os.environ.get("IS_CHILD") == "true":
|
||||||
|
build_on_change()
|
||||||
|
return
|
||||||
|
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(PythonChangeHandler(["stsg_dev"]), path=CODE_DIRECTORY, recursive=True)
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1) # Keeps the thread alive
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
|
||||||
|
observer.join()
|
84
stsg/build.py
Normal file
84
stsg/build.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, STATIC_DIRECTORY
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("stsg.build")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_static():
|
||||||
|
src = str(Path(SOURCE_DIRECTORY, STATIC_DIRECTORY))
|
||||||
|
dst = str(Path(DIST_DIRECTORY, STATIC_DIRECTORY))
|
||||||
|
|
||||||
|
if not os.path.exists(src):
|
||||||
|
logger.warn("The static folder '%s' wasn't defined.", src)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("copying static files from '%s' to '%r'", src, dst)
|
||||||
|
|
||||||
|
os.makedirs(dst, exist_ok=True)
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(src):
|
||||||
|
if any(p.startswith(".") for p in Path(root).parts):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compute relative path from the source root
|
||||||
|
rel_path = os.path.relpath(root, src)
|
||||||
|
dest_dir = os.path.join(dst, rel_path)
|
||||||
|
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if file.startswith("."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_file = os.path.join(root, file)
|
||||||
|
dest_file = os.path.join(dest_dir, file)
|
||||||
|
shutil.copy2(src_file, dest_file)
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
def __init__(self, root: str = SOURCE_DIRECTORY):
|
||||||
|
self.file = Path(root, "index.html")
|
||||||
|
|
||||||
|
current_root = Path(root)
|
||||||
|
while current_root.parts and self.file is None:
|
||||||
|
current_file = Path(current_root, "index.html")
|
||||||
|
if current_file.exists() and current_file.is_file:
|
||||||
|
self.file = current_file
|
||||||
|
|
||||||
|
current_root = current_root.parent
|
||||||
|
|
||||||
|
if self.file is None:
|
||||||
|
logger.error("couldn't find context for %s", root)
|
||||||
|
exit(1)
|
||||||
|
logger.info("%s found context %r", root, str(self.file))
|
||||||
|
|
||||||
|
def get_text(self, placeholder_values: dict):
|
||||||
|
text = self.file.read_text()
|
||||||
|
|
||||||
|
for key, value in placeholder_values.items():
|
||||||
|
text = text.replace(f"<{key}/>", value)
|
||||||
|
text = text.replace(f"<{key} />", value)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
logger.info("building static page")
|
||||||
|
copy_static()
|
||||||
|
|
||||||
|
context = Context()
|
||||||
|
|
||||||
|
# debug
|
||||||
|
t = context.get_text({
|
||||||
|
"content": """
|
||||||
|
<h1 class="title">Hello World</h1>
|
||||||
|
<p class="subtitle">My first website with <strong>Bulma</strong>!</p>
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
with Path(DIST_DIRECTORY, "index.html").open("w") as f:
|
||||||
|
f.write(t)
|
9
stsg/config.py
Normal file
9
stsg/config.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
SOURCE_DIRECTORY = "src"
|
||||||
|
DIST_DIRECTORY = "dist"
|
||||||
|
|
||||||
|
# relative to SOURCE_DIRECTORY / DIST_DIRECTORY
|
||||||
|
STATIC_DIRECTORY = "static"
|
||||||
|
|
||||||
|
# FOR DEVELOPMENT
|
||||||
|
|
||||||
|
CODE_DIRECTORY = "stsg"
|
Loading…
x
Reference in New Issue
Block a user