Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c6594215e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -169,5 +169,4 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
public
|
public
|
||||||
content/*
|
content/*
|
||||||
config.yml
|
|
||||||
16
argparser.py
16
argparser.py
@@ -1,16 +0,0 @@
|
|||||||
import argparse
|
|
||||||
from config import config
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser(
|
|
||||||
prog = config.app_name,
|
|
||||||
description = config.app_description,
|
|
||||||
epilog = f"See {config.app_src_url} for more info.")
|
|
||||||
|
|
||||||
subparsers = argparser.add_subparsers(dest='command', required=True)
|
|
||||||
|
|
||||||
subparsers.add_parser('init', help = 'Initialize new site.')
|
|
||||||
subparsers.add_parser('build', help = "Build the site from existing content.")
|
|
||||||
new_content = subparsers.add_parser('create', aliases = ['new'], help = "Create a new content file.")
|
|
||||||
new_content.add_argument('filename', type=str, help = "Name of the content file to create.")
|
|
||||||
edit_content = subparsers.add_parser('edit', help = "Edit a content file.")
|
|
||||||
edit_content.add_argument('filename', type=str, help = "Name of the content file to edit.")
|
|
||||||
203
classes.py
203
classes.py
@@ -1,203 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
from collections import defaultdict
|
|
||||||
import markdown
|
|
||||||
import frontmatter
|
|
||||||
from pathlib import Path
|
|
||||||
from logger import logger
|
|
||||||
from config import config
|
|
||||||
|
|
||||||
class ContentItem:
|
|
||||||
def render_content(self, categories, target_file = False):
|
|
||||||
if target_file:
|
|
||||||
self.target_file = Path(target_file)
|
|
||||||
logger.debug(f"Rendering {self.source_file} to {self.target_file}")
|
|
||||||
if hasattr(config, "footer"):
|
|
||||||
footer_data = config.footer
|
|
||||||
else:
|
|
||||||
footer_data = ''
|
|
||||||
try:
|
|
||||||
if self.image_file and Path(self.image_file).exists():
|
|
||||||
self.image_file = Path(self.image_file)
|
|
||||||
image_targetfile = Path(f"{config.output_dir}/{config.static_dir}/images/{self.source_file.stem}.jpg")
|
|
||||||
logger.debug(f"Copying {self.image_file} to {image_targetfile}")
|
|
||||||
shutil.copyfile(self.image_file, image_targetfile)
|
|
||||||
self.image_file = f"{self.image_file.stem}.jpg"
|
|
||||||
else:
|
|
||||||
self.image_file = False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Couldn't render image file {self.image_file}: {e}")
|
|
||||||
try:
|
|
||||||
if self.audio_file and Path(self.audio_file).exists():
|
|
||||||
self.audio_file = Path(self.audio_file)
|
|
||||||
audio_targetfile = Path(f"{config.output_dir}/{config.static_dir}/audio/{self.audio_file.name}")
|
|
||||||
logger.debug(f"Copying {self.audio_file} to {audio_targetfile}")
|
|
||||||
shutil.copyfile(self.audio_file, audio_targetfile)
|
|
||||||
self.audio_file = f"{self.audio_file.stem}.mp3"
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Couldn't render audio file {self.audio_file}: {e}")
|
|
||||||
try:
|
|
||||||
if self.css_file and Path(self.css_file).exists():
|
|
||||||
self.css_file = Path(self.css_file)
|
|
||||||
css_targetfile = Path(f"{config.output_dir}/{config.static_dir}/css/{self.css_file.name}")
|
|
||||||
logger.debug(f"Copying {self.css_file} to {css_targetfile}")
|
|
||||||
shutil.copyfile(self.css_file, css_targetfile)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Couldn't render CSS file {self.css_file}: {e}")
|
|
||||||
try:
|
|
||||||
if self.js_file and Path(self.js_file).exists():
|
|
||||||
self.js_file = Path(self.js_file)
|
|
||||||
js_targetfile = Path(f"{config.output_dir}/{config.static_dir}/js/{self.js_file.name}")
|
|
||||||
logger.debug(f"Copying {self.js_file} to {js_targetfile}")
|
|
||||||
shutil.copyfile(self.js_file, js_targetfile)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Couldn't render JS file {self.js_file}: {e}")
|
|
||||||
try:
|
|
||||||
with self.target_file.open("w", encoding="utf-8") as f:
|
|
||||||
f.write(config.content_item_template.render(content_item = self, page_title = f"{config.site_name}: {self.title}", parent_path = '../', categories = categories, footer_data = footer_data))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Couldn't render content file: {e}")
|
|
||||||
|
|
||||||
def parse_content(self):
|
|
||||||
logger.debug(f"Parsing file {self.source_file}")
|
|
||||||
try:
|
|
||||||
self.source_file = Path(self.source_file)
|
|
||||||
self.parent_dir = self.source_file.parent # Most likely './content'
|
|
||||||
self.slug = self.source_file.stem
|
|
||||||
self.target_file = Path(f"{config.output_dir}/{self.source_file.parent}/{self.source_file.stem}.html")
|
|
||||||
self.data = frontmatter.load(self.source_file)
|
|
||||||
self.preview = self.data.content.replace('\n', '<br>')[:300]
|
|
||||||
self.title = self.data.get("title", self.slug)
|
|
||||||
self.omit_second_title = self.data.get("omit_second_title", False)
|
|
||||||
self.date = self.data.get("date", "2000-01-01T00:00:00+03:00")
|
|
||||||
self.categories = [c for c in self.data.get("categories", []) if c != 'default']
|
|
||||||
self.hidden = self.data.get("hidden", str(False))
|
|
||||||
self.data.content = self.data.content.replace('\n', ' \n') # For markdown newline rendering
|
|
||||||
self.html = markdown.markdown(self.data.content)
|
|
||||||
cover_image_path = f"{self.source_file.parent}/{self.source_file.stem}.jpg"
|
|
||||||
self.image_file = cover_image_path if Path(cover_image_path).exists() else ""
|
|
||||||
audio_filepath = f"{self.source_file.parent}/{self.source_file.stem}.mp3"
|
|
||||||
self.audio_file = audio_filepath if Path(audio_filepath).exists() else ""
|
|
||||||
css_filepath = f"{self.source_file.parent}/{self.source_file.stem}.css"
|
|
||||||
self.css_file = css_filepath if Path(css_filepath).exists() else ""
|
|
||||||
js_filepath = f"{self.source_file.parent}/{self.source_file.stem}.js"
|
|
||||||
self.js_file = js_filepath if Path(js_filepath).exists() else ""
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Parser error, {e}")
|
|
||||||
|
|
||||||
def create_content(self):
|
|
||||||
with open(self.source_file, mode="w", encoding="utf-8") as f:
|
|
||||||
f.writelines([
|
|
||||||
"---\n",
|
|
||||||
f"title: {self.source_file.stem}\n",
|
|
||||||
f"date: '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}'\n",
|
|
||||||
"description: ''\n",
|
|
||||||
"author: ''\n",
|
|
||||||
"categories: ['default']\n",
|
|
||||||
"hidden: False\n",
|
|
||||||
"omit_second_title: False\n",
|
|
||||||
"---\n",
|
|
||||||
"\n\n\n"
|
|
||||||
])
|
|
||||||
|
|
||||||
def __init__(self, file):
|
|
||||||
self.source_file = file
|
|
||||||
|
|
||||||
class Site:
|
|
||||||
def __init__(self):
|
|
||||||
self.output_dir = Path(config.output_dir)
|
|
||||||
self.content_dir = Path(config.content_dir)
|
|
||||||
self.static_dir = Path(config.static_dir)
|
|
||||||
self.templates_dir = Path(config.templates_dir)
|
|
||||||
self.images_dir = Path(f"{config.static_dir}/images")
|
|
||||||
self.css_dir = Path(f"{config.static_dir}/css")
|
|
||||||
self.js_dir = Path(f"{config.static_dir}/js")
|
|
||||||
self.output_dir = Path(config.output_dir)
|
|
||||||
self.content_items = []
|
|
||||||
self.categories = defaultdict(list)
|
|
||||||
|
|
||||||
def init_site(self):
|
|
||||||
# Exit if current directory not empty
|
|
||||||
if os.path.isdir('.') and os.listdir('.'):
|
|
||||||
logger.error("Current directory is not empty.")
|
|
||||||
sys.exit(1)
|
|
||||||
logger.info("Initializing new site")
|
|
||||||
config.create_default_config()
|
|
||||||
# Create directories
|
|
||||||
for subdir in [self.content_dir]:
|
|
||||||
os.makedirs(subdir, exist_ok=True)
|
|
||||||
|
|
||||||
# Copy default theme
|
|
||||||
shutil.copytree(f"{config.base_dir}/themes/default", "themes/default", dirs_exist_ok=True)
|
|
||||||
|
|
||||||
def get_content_items(self):
|
|
||||||
logger.debug("Getting content items")
|
|
||||||
self.get_content_items = []
|
|
||||||
logger.debug(f"Scanning {Path(config.content_dir)}")
|
|
||||||
for md_file in Path(config.content_dir).glob("*.md"):
|
|
||||||
content_item = ContentItem(md_file)
|
|
||||||
content_item.parse_content()
|
|
||||||
self.content_items.append(content_item)
|
|
||||||
self.content_items.sort(key=lambda i: (-datetime.fromisoformat(i.date).timestamp(), i.title))
|
|
||||||
|
|
||||||
def map_categories(self):
|
|
||||||
for content_item in self.content_items:
|
|
||||||
for category in content_item.categories:
|
|
||||||
if not category == "default":
|
|
||||||
self.categories[category].append(content_item.slug)
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
# Recreate the output dir if exists
|
|
||||||
if self.output_dir.exists():
|
|
||||||
shutil.rmtree(self.output_dir)
|
|
||||||
self.output_dir.mkdir()
|
|
||||||
|
|
||||||
# Create public subdirs
|
|
||||||
subdirs = ["categories", "content", "static"]
|
|
||||||
for subdir in subdirs:
|
|
||||||
subdir = self.output_dir / subdir
|
|
||||||
subdir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Copy theme's static dir to 'public/static
|
|
||||||
shutil.copytree(f"{config.base_dir}/themes/{config.theme}/static", f"{self.output_dir}/static", dirs_exist_ok=True)
|
|
||||||
|
|
||||||
# Get content items
|
|
||||||
self.get_content_items()
|
|
||||||
|
|
||||||
# Build categories map
|
|
||||||
self.map_categories()
|
|
||||||
|
|
||||||
# Render the content files
|
|
||||||
for content_item in self.content_items:
|
|
||||||
content_item.render_content(categories = self.categories)
|
|
||||||
|
|
||||||
# Render the about file
|
|
||||||
about_content = ContentItem(Path(f'themes/{config.theme}/content/about.md'))
|
|
||||||
about_content.parse_content()
|
|
||||||
about_content.render_content(categories = self.categories, target_file='public/static/about.html')
|
|
||||||
|
|
||||||
# Render the index file
|
|
||||||
if hasattr(config, "footer"):
|
|
||||||
footer_data = config.footer
|
|
||||||
else:
|
|
||||||
footer_data = ''
|
|
||||||
visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True]
|
|
||||||
with (self.output_dir / "index.html").open("w", encoding="utf-8") as f:
|
|
||||||
f.write(config.index_template.render(page_title = config.site_name, content_items=visible_content_items, categories = self.categories, footer_data = footer_data))
|
|
||||||
|
|
||||||
# Render the categories indices
|
|
||||||
if hasattr(config, "footer"):
|
|
||||||
footer_data = config.footer
|
|
||||||
else:
|
|
||||||
footer_data = ''
|
|
||||||
visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True]
|
|
||||||
for category in self.categories:
|
|
||||||
category_index = Path(f"{self.output_dir}/categories/{category}.html")
|
|
||||||
category_items = [i for i in visible_content_items if category in i.data.get("categories", "default")]
|
|
||||||
with (category_index).open(mode="w", encoding="utf-8") as f:
|
|
||||||
f.write(config.index_template.render(page_title=f"{config.site_name}: [ {category} ]", content_items=category_items, categories = self.categories, parent_path = '../', footer_data = footer_data))
|
|
||||||
|
|
||||||
logger.info(f"Created {len(self.content_items)} content items.")
|
|
||||||
|
|
||||||
87
config.py
87
config.py
@@ -1,69 +1,24 @@
|
|||||||
import os, sys, yaml
|
import logging, os, sys
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
# Main config section
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self):
|
APP_NAME = "hydrogen"
|
||||||
self.config_path = "config.yml"
|
MAIN_PAGE_TITLE = "Selected poems"
|
||||||
self.app_name = "microgen"
|
OUTPUT_DIR = os.environ.get('OUTPUT_DIR') or 'public'
|
||||||
self.app_description = "Simplistic static site generator"
|
HEADER_IMAGE = 'static/header.jpg'
|
||||||
self.app_src_url = f"https://git.exocortex.ru/Exocortex/{self.app_name}"
|
LOG_TO = sys.stdout
|
||||||
self.content_dir = 'content'
|
LOG_LEVEL = logging.DEBUG
|
||||||
self.templates_dir = 'templates'
|
|
||||||
self.static_dir = 'static'
|
|
||||||
self.output_dir = 'public'
|
|
||||||
self.header_image = 'static/header.jpg'
|
|
||||||
self.site_name = f"{self.app_name} library"
|
|
||||||
self.theme = "default"
|
|
||||||
self.debug = False
|
|
||||||
if os.path.isfile (self.config_path):
|
|
||||||
with open(self.config_path, "r") as f:
|
|
||||||
config = yaml.safe_load(f)
|
|
||||||
if config:
|
|
||||||
self.update_from_dict(config)
|
|
||||||
|
|
||||||
|
# Logging config section
|
||||||
# Get the script base_dir and check for templates dir
|
logger = logging.getLogger(Config.APP_NAME)
|
||||||
if Path(f'themes/{self.theme}/templates').exists():
|
logger.setLevel(Config.LOG_LEVEL)
|
||||||
self.base_dir = "."
|
stdout_handler = logging.StreamHandler(Config.LOG_TO)
|
||||||
elif getattr(sys, 'frozen', False):
|
stdout_handler.setLevel(Config.LOG_LEVEL)
|
||||||
# Running fron Pyinstaller-packed binary
|
formatter = logging.Formatter(
|
||||||
self.base_dir = Path(sys._MEIPASS)
|
'[%(asctime)s] %(name)s: %(levelname)s: %(message)s',
|
||||||
else:
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
# Running as a plain Python app
|
)
|
||||||
self.base_dir = Path(__file__).resolve().parent
|
stdout_handler.setFormatter(formatter)
|
||||||
|
if not logger.hasHandlers():
|
||||||
# Prepare template rendering engine
|
logger.addHandler(stdout_handler)
|
||||||
if Path(f"themes/{self.theme}/templates").exists():
|
logger.propagate = False
|
||||||
# Use the templates from the initialized site instance and the installed theme
|
|
||||||
jinja_env_dir = f"themes/{self.theme}/templates"
|
|
||||||
#logger.debug(f"Using locally initialized templates from {jinja_env_dir}")
|
|
||||||
else:
|
|
||||||
# Use default templates from the default theme shipped with the app
|
|
||||||
# i.e. PyInstaller-packed single executable
|
|
||||||
#logger.debug("Using shipped default temlpates.")
|
|
||||||
jinja_env_dir = f"{self.base_dir}/themes/default/templates"
|
|
||||||
self.env = Environment(loader=FileSystemLoader(f"{jinja_env_dir}"))
|
|
||||||
self.env.globals['header_image'] = f"{self.base_dir}/themes/{self.theme}/static/images/header.jpg"
|
|
||||||
self.index_template = self.env.get_template("index.html")
|
|
||||||
self.content_item_template = self.env.get_template("content_item.html")
|
|
||||||
|
|
||||||
|
|
||||||
def update_from_dict(self, config_dict):
|
|
||||||
for key, value in config_dict.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__dict__}"
|
|
||||||
|
|
||||||
def create_default_config(self):
|
|
||||||
defaults = {
|
|
||||||
"site_name": self.site_name,
|
|
||||||
"theme": "default",
|
|
||||||
"debug": False,
|
|
||||||
"footer": "'' # Author / copyright / date / links, can be plaintext or valid HTML"
|
|
||||||
}
|
|
||||||
with open(self.config_path, mode="w", encoding="utf-8") as f:
|
|
||||||
yaml.safe_dump(defaults, f)
|
|
||||||
|
|
||||||
config = Config()
|
|
||||||
0
content/.Placeholder
Normal file
0
content/.Placeholder
Normal file
29
helpers.py
29
helpers.py
@@ -1,29 +0,0 @@
|
|||||||
import os, subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
from classes import *
|
|
||||||
from config import config
|
|
||||||
from logger import logger
|
|
||||||
|
|
||||||
def create_content(filename):
|
|
||||||
filename = Path(f"{config.content_dir}/{filename}.md")
|
|
||||||
if not filename.exists():
|
|
||||||
logger.debug(f"Creating new content {filename}")
|
|
||||||
new_content_item = ContentItem(filename)
|
|
||||||
new_content_item.create_content()
|
|
||||||
|
|
||||||
def edit_content(filename):
|
|
||||||
logger.debug(f"Editing content {filename}")
|
|
||||||
editor = os.environ.get('EDITOR', 'vim')
|
|
||||||
# Create the content file if not exists
|
|
||||||
if not Path(f"{config.content_dir}/{filename}.md").exists():
|
|
||||||
create_content(filename)
|
|
||||||
filename = Path(f"{config.content_dir}/{filename}.md")
|
|
||||||
subprocess.call([editor, filename])
|
|
||||||
|
|
||||||
def init_site():
|
|
||||||
site = Site()
|
|
||||||
site.init_site()
|
|
||||||
|
|
||||||
def build_site():
|
|
||||||
site = Site()
|
|
||||||
site.build()
|
|
||||||
106
hydrogen.py
106
hydrogen.py
@@ -1,18 +1,96 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from argparser import argparser
|
from pathlib import Path
|
||||||
from helpers import *
|
import frontmatter
|
||||||
|
import markdown
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
import shutil
|
||||||
|
from config import Config, logger
|
||||||
|
|
||||||
def main():
|
content_dir = Path('content')
|
||||||
args = argparser.parse_args()
|
template_dir = Path('templates')
|
||||||
match args.command:
|
output_dir = Path(Config.OUTPUT_DIR)
|
||||||
case "build":
|
img_dir = Path('public/images')
|
||||||
build_site()
|
assets_dir = Path('public/assets')
|
||||||
case "init":
|
assets_css_dir = Path('public/assets/css')
|
||||||
init_site()
|
assets_js_dir = Path('public/assets/js')
|
||||||
return
|
static_dir = Path('static')
|
||||||
case "new" | "create" | "edit":
|
header_image = Config.HEADER_IMAGE or '/static/header.jpg'
|
||||||
edit_content(args.filename)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
class ContentItemPrototype:
|
||||||
main()
|
def render_content(self):
|
||||||
|
if self.image_src_file.exists():
|
||||||
|
shutil.copyfile(self.image_src_file, output_dir / self.image)
|
||||||
|
if self.custom_css_src_file.exists():
|
||||||
|
shutil.copyfile(self.custom_css_src_file, output_dir / self.custom_css)
|
||||||
|
if self.custom_js_src_file.exists():
|
||||||
|
shutil.copyfile(self.custom_js_src_file, output_dir / self.custom_js)
|
||||||
|
with self.content_output_path.open("w", encoding="utf-8") as f:
|
||||||
|
f.write(content_item_template.render(content_item = self, page_title = self.title))
|
||||||
|
|
||||||
|
def __init__(self, md_file):
|
||||||
|
self.slug = md_file.stem
|
||||||
|
self.data = frontmatter.load(md_file)
|
||||||
|
self.html = markdown.markdown(self.data.content)
|
||||||
|
self.preview = self.html[:300] + "<a href=" + f"{self.slug}.html" + ">... read more</a>"
|
||||||
|
self.title = self.data.get("title", self.slug)
|
||||||
|
self.omit_second_title = self.data.get("omit_second_title", False)
|
||||||
|
self.date = self.data.get("data", "2000-01-01T00:00:00+03:00")
|
||||||
|
self.content_output_path = output_dir / f"{self.slug}.html"
|
||||||
|
self.image_src_file = Path(f"{content_dir}/{md_file.stem}.jpg")
|
||||||
|
self.image = f"images/{md_file.stem}.jpg" if self.image_src_file.exists() else None
|
||||||
|
self.custom_css_src_file = Path(f"{content_dir}/{md_file.stem}.css")
|
||||||
|
self.custom_css = f"assets/css/{md_file.stem}.css" if self.custom_css_src_file.exists() else None
|
||||||
|
self.custom_js_src_file = Path(f"{content_dir}/{md_file.stem}.js")
|
||||||
|
self.custom_js = f"assets/js/{md_file.stem}.js" if self.custom_js_src_file.exists() else None
|
||||||
|
# Special handling for the 'about' item
|
||||||
|
if self.slug == 'about':
|
||||||
|
self.image = 'static/about.jpg'
|
||||||
|
|
||||||
|
logger.info(f"Building the '{Config.MAIN_PAGE_TITLE}' site.")
|
||||||
|
# Recreate the output dir if needed
|
||||||
|
if output_dir.exists():
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
# Create public subdirs
|
||||||
|
img_dir.mkdir()
|
||||||
|
assets_dir.mkdir()
|
||||||
|
assets_css_dir.mkdir()
|
||||||
|
assets_js_dir.mkdir()
|
||||||
|
|
||||||
|
# Copy static files if exist
|
||||||
|
if static_dir.exists():
|
||||||
|
shutil.copytree(static_dir, output_dir / "static")
|
||||||
|
|
||||||
|
# Prepare template rendering engine
|
||||||
|
env = Environment(loader=FileSystemLoader(str(template_dir)))
|
||||||
|
env.globals['header_image'] = header_image
|
||||||
|
index_template = env.get_template("index.html")
|
||||||
|
content_item_template = env.get_template("content_item.html")
|
||||||
|
|
||||||
|
# Parse the content files
|
||||||
|
content_items = []
|
||||||
|
for md_file in content_dir.glob("*.md"):
|
||||||
|
current_content_item = ContentItemPrototype(md_file)
|
||||||
|
current_content_item.render_content()
|
||||||
|
content_items.append({
|
||||||
|
"slug": current_content_item.slug,
|
||||||
|
"title": current_content_item.title,
|
||||||
|
"date": current_content_item.date,
|
||||||
|
"preview": markdown.markdown(current_content_item.preview),
|
||||||
|
"image": current_content_item.image,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Render the index file
|
||||||
|
with (output_dir / "index.html").open("w", encoding="utf-8") as f:
|
||||||
|
f.write(index_template.render(page_title = Config.MAIN_PAGE_TITLE, content_items=content_items))
|
||||||
|
|
||||||
|
# Render the about file
|
||||||
|
about_content = ContentItemPrototype(Path('static/about.md'))
|
||||||
|
about_content.render_content()
|
||||||
|
|
||||||
|
# Move 'robots.txt' into output_dir
|
||||||
|
shutil.copyfile(static_dir / 'robots.txt', output_dir / 'robots.txt')
|
||||||
|
|
||||||
|
logger.info(f"Created {len(content_items)} content items.")
|
||||||
19
jinja_env.py
19
jinja_env.py
@@ -1,19 +0,0 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
# get the scrip directory
|
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
# Running fron Pyinstaller-packed binary
|
|
||||||
base_dir = Path(sys._MEIPASS)
|
|
||||||
else:
|
|
||||||
# Running as a plain Python app
|
|
||||||
base_dir = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
# Prepare template rendering engine
|
|
||||||
jinja_env_dir = f"{base_dir}/themes/{Config.THEME}/templates"
|
|
||||||
env = Environment(loader=FileSystemLoader(f"{jinja_env_dir}"))
|
|
||||||
env.globals['header_image'] = f"{base_dir}/static/header.jpg"
|
|
||||||
index_template = env.get_template("index.html")
|
|
||||||
content_item_template = env.get_template("content_item.html")
|
|
||||||
22
logger.py
22
logger.py
@@ -1,22 +0,0 @@
|
|||||||
import logging, sys
|
|
||||||
from config import config
|
|
||||||
|
|
||||||
# Logging config section
|
|
||||||
|
|
||||||
LOG_TO = sys.stdout
|
|
||||||
LOG_LEVEL = logging.INFO
|
|
||||||
if config.debug == True:
|
|
||||||
LOG_LEVEL = logging.DEBUG
|
|
||||||
|
|
||||||
logger = logging.getLogger(config.app_name)
|
|
||||||
logger.setLevel(LOG_LEVEL)
|
|
||||||
stdout_handler = logging.StreamHandler(LOG_TO)
|
|
||||||
stdout_handler.setLevel(LOG_LEVEL)
|
|
||||||
formatter = logging.Formatter(
|
|
||||||
'[%(asctime)s] %(name)s: %(levelname)s: %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
stdout_handler.setFormatter(formatter)
|
|
||||||
if not logger.hasHandlers():
|
|
||||||
logger.addHandler(stdout_handler)
|
|
||||||
logger.propagate = False
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
argparse
|
|
||||||
bs4
|
|
||||||
python-frontmatter
|
python-frontmatter
|
||||||
jinja2
|
jinja2
|
||||||
markdown
|
markdown
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
6
static/about.md
Normal file
6
static/about.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: "About"
|
||||||
|
omit_second_title: True
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
2
static/robots.txt
Normal file
2
static/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
24
templates/content_item.html
Normal file
24
templates/content_item.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "default.html" %}
|
||||||
|
|
||||||
|
{% block head_includes %}
|
||||||
|
{% if content_item.custom_css %}
|
||||||
|
<link href="{{ content_item.custom_css }}">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4 mb-5">
|
||||||
|
{% if content_item.image %}<img src="{{ content_item.image }}" class="img-fluid mb-5" style="border-radius: 5px;">{% endif %}
|
||||||
|
{% if not content_item.omit_second_title %}
|
||||||
|
<h1>{{ content_item.title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
<article>{{ content_item.html | safe }}</article>
|
||||||
|
<a href="index.html" class="btn btn-secondary mt-4">← Back</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_includes %}
|
||||||
|
{% if content_item.custom_js %}
|
||||||
|
<script src="{{ content_item.custom_js }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
90
templates/default.html
Normal file
90
templates/default.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% set base = "" %}
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
|
||||||
|
<title>{{ page_title }}</title>
|
||||||
|
<style>
|
||||||
|
.top_header {
|
||||||
|
position: relative;
|
||||||
|
height: 250px;
|
||||||
|
background: rgb(70, 70, 124);
|
||||||
|
background-image: url("{{ header_image }}");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top_header_text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.top_menu a {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top_menu a:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.top_header_text {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
{% block head_includes %}
|
||||||
|
{% endblock %}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top_header">
|
||||||
|
<div >
|
||||||
|
<h1 class="top_header_text"><a style="border-radius: 5px; padding: 10px; opacity: 0.7; background: black">{{ page_title }}</a></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top_menu d-flex justify-content-end" style="margin-bottom: 20px;">
|
||||||
|
|
||||||
|
<a href="index.html">Home</a>
|
||||||
|
<a href="about.html">About</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">{% block content %}{% endblock %}</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.min.js" integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" crossorigin="anonymous"></script>
|
||||||
|
{% block footer_includes %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
19
templates/index.html
Normal file
19
templates/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "default.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-3 g-4 px-1 py-2">
|
||||||
|
{% for content_item in content_items %}
|
||||||
|
<div class="col mb-3">
|
||||||
|
<div class="card h-100" style="border-radius: 5px;">
|
||||||
|
<img src="{{ content_item.image }}" alt="{{ content_item.title }}" class="content-cover-img card-img-top img-fluid" style="object-fit: cover; height: 200px; border-radius: 5px;">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h5 class="card-title"><a class="text-decoration-none text-body" href="{{ content_item.slug}}.html ">{{ content_item.title }}</a></h5>
|
||||||
|
<p class="card-text mb-3">{{ content_item.preview | safe}}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: "About"
|
|
||||||
omit_second_title: True
|
|
||||||
---
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
|
||||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
||||||
|
|
||||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
"Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif,
|
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
}
|
|
||||||
|
|
||||||
.top_header {
|
|
||||||
position: relative;
|
|
||||||
height: 150px;
|
|
||||||
/*background: rgb(70, 70, 124);*/
|
|
||||||
overflow: hidden;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top_header_text {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
color: rgba(255,255,255,0.75);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 2em;
|
|
||||||
/*background: rgba(0, 0, 0, 0.75);*/
|
|
||||||
background: #121212;
|
|
||||||
opacity: 0.95;
|
|
||||||
width: 100%;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer-data, #footer-data-secondary {
|
|
||||||
line-height: 2em;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 B |
@@ -1,20 +0,0 @@
|
|||||||
console.log('Default theme loaded.')
|
|
||||||
|
|
||||||
const saved_color_theme = localStorage.getItem('saved_color_theme');
|
|
||||||
|
|
||||||
if (saved_color_theme) {
|
|
||||||
document.documentElement.setAttribute('data-bs-theme', saved_color_theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("categories_toggle").onclick = function() {
|
|
||||||
categories_menu_item = document.getElementById("categories_menu");
|
|
||||||
categories_menu_item.style.display = categories_menu_item.style.display === "none" ? "block" : "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("dark_theme_toggle").onclick = function() {
|
|
||||||
html = document.documentElement;
|
|
||||||
current_color_theme = html.getAttribute('data-bs-theme');
|
|
||||||
new_color_theme = current_color_theme === 'dark' ? 'light' : 'dark';
|
|
||||||
html.setAttribute('data-bs-theme', new_color_theme);
|
|
||||||
localStorage.setItem('saved_color_theme', new_color_theme);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
{% extends "default.html" %}
|
|
||||||
|
|
||||||
{% block head_includes %}
|
|
||||||
{% if content_item.css_file %}
|
|
||||||
<link rel="stylesheet" href="{{ parent_path }}static/css/{{ content_item.slug }}.css">
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mt-4 mb-5">
|
|
||||||
{% if content_item.image_file %}
|
|
||||||
<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" alt="{{ content_item.image_file }}" class="img-fluid mb-5 rounded w-100 h-auto dotr">
|
|
||||||
{% endif %}
|
|
||||||
{% if not content_item.omit_second_title %}
|
|
||||||
<h1>{{ content_item.title }}</h1>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<article>{{ content_item.html | safe }}</article>
|
|
||||||
|
|
||||||
{% if content_item.audio_file %}
|
|
||||||
<div id="audio" class="mb-4 mt-4">
|
|
||||||
<audio controls>
|
|
||||||
<source src="{{ parent_path }}static/audio/{{ content_item.audio_file }}" type="audio/mpeg">
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div id="categories">
|
|
||||||
{% for category in content_item.categories %}
|
|
||||||
<a href="{{ parent_path }}categories/{{ category }}.html" class="mx-1 text-decoration-none small text-muted">{{ category }} </a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<a href="../index.html" class="btn btn-secondary mt-4">← Back</a>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block footer_includes %}
|
|
||||||
{% if content_item.js_file %}
|
|
||||||
<script src="{{ parent_path}}static/js/{{ content_item.slug }}.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html data-bs-theme="light">
|
|
||||||
<head>
|
|
||||||
{% set base = "" %}
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
|
||||||
<link href="{{ parent_path }}static/css/theme.css" rel="stylesheet">
|
|
||||||
<title>{{ page_title }}</title>
|
|
||||||
{% block head_includes %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="top_wrapper">
|
|
||||||
<div id= "top_header" class="row top_header">
|
|
||||||
<img id="top_header_image" src="{{ parent_path }}static/images/header.jpg" class="img-fluid">
|
|
||||||
<div id="top_title" class="row">
|
|
||||||
<a class="top_header_text ms-1 text-decoration-none">{{ page_title }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="menu_container" class="row justify-content-end pe-2 py-2" >
|
|
||||||
<div id="menu_wrapper" class="col-auto fw-bold">
|
|
||||||
<a class="link-body-emphasis mx-2 text-decoration-none" href="{{ parent_path }}index.html">Home</a>
|
|
||||||
<a class="link-body-emphasis mx-2 text-decoration-none" id="categories_toggle" href="#none">Categories</a>
|
|
||||||
<a class="link-body-emphasis mx-2 text-decoration-none" href="{{ parent_path }}static/about.html">About</a>
|
|
||||||
<a class="link-body-emphasis mx-2 text-decoration-none" id="dark_theme_toggle" href="#none">🌓︎</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="categories_container" class="container">
|
|
||||||
<div id="categories_menu" class="row justify-content-center mx-auto mb-2" style="display: none">
|
|
||||||
{% for category in categories %}
|
|
||||||
<a href="{{ parent_path }}categories/{{ category }}.html" class="mx-1 text-decoration-none">{{ category }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content_wrapper" class="container-fluid mb-2">
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
<div id="footer_wrapper" class="container-fluid">
|
|
||||||
<div id="footer-data" class="row d-flex text-muted justify-content-end">
|
|
||||||
{% block footer_includes %}
|
|
||||||
{% endblock %}
|
|
||||||
{% if footer_data %}
|
|
||||||
<div class="d-flex align-items-center justify-content-center">
|
|
||||||
<p class="my-0">{{ footer_data }}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="footer-data-secondary" class="row d-flex text-muted justify-content-end">
|
|
||||||
<div class="d-flex align-items-center justify-content-center">
|
|
||||||
<p class="small my-0" style="font-size: 0.75rem;">Page created with <a class="text-decoration-none" href="#microgen">microgen</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.min.js" integrity="sha384-RuyvpeZCxMJCqVUGFI0Do1mQrods/hhxYlcVfGPOfQtPJh0JCw12tUAZ/Mv10S7D" crossorigin="anonymous"></script>
|
|
||||||
<script defer src="{{ parent_path }}static/js/theme.js"></script>
|
|
||||||
</body>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
{% extends "default.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="row justify-content-center g-4 mb-4 py-4">
|
|
||||||
{% for content_item in content_items %}
|
|
||||||
<div class="col-auto align-items-stretch d-flex mx-2" style="width: 100%; width: 24em;">
|
|
||||||
<div class="card h-100 px-0 rounded mx-1 my-3" style="width: 100%; width: 22em;">
|
|
||||||
<div class="card-header">
|
|
||||||
{% if content_item.image_file %}
|
|
||||||
<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" alt="{{ content_item.title }}" class="card-img-top img-fluid mx-auto d-block object-fit-fill rounded" style="height: 200px; width: auto;">
|
|
||||||
{% else %}
|
|
||||||
<img src="{{ parent_path }}static/images/1x1.png" alt="{{ content_item.title }}" class="card-img-top img-fluid mx-auto d-block object-fit-scale rounded" style="height: 200px; width: auto;">
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="fw-bold"><a class="text-decoration-none text-body" href="{{ parent_path }}content/{{ content_item.slug}}.html">{{ content_item.title }}</a></h5>
|
|
||||||
<p class="card-text mb-2">{{ content_item.preview | safe}}<a class="text-decoration-none" href="{{ parent_path }}content/{{ content_item.slug}}.html">... read more</a> </p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer" style="height: 4em;">
|
|
||||||
{% for item_category in content_item.categories %}
|
|
||||||
<small><a href="{{ parent_path }}categories/{{ item_category }}.html" class="text-muted text-decoration-none mx-1">{{ item_category }}</a></small>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user