34 Commits

Author SHA1 Message Date
SG
808aab3609 updates 2025-06-30 20:57:01 +03:00
sg
162b45809b hopefully fixed bad images in content_item
template
2025-06-30 20:21:03 +03:00
SG
664a6adb77 updates 2025-06-22 14:08:45 +03:00
sg
b633c21ef2 updates 2025-06-22 14:06:52 +03:00
SG
802f8022d0 audio player added 2025-06-22 12:12:36 +03:00
SG
d58d9464a9 updates 2025-06-20 15:18:28 +03:00
SG
dca4aa7a7e updates 2025-06-19 11:17:01 +03:00
SG
8813d91493 updates 2025-06-19 08:56:37 +03:00
SG
9fa58d4700 updates 2025-06-18 15:55:52 +03:00
SG
15879e66b7 updates 2025-06-18 15:26:11 +03:00
SG
3abe3f2fac updates 2025-06-18 14:59:23 +03:00
SG
d3b369be46 updates 2025-06-18 14:00:00 +03:00
SG
66d3132e53 updates 2025-06-18 13:27:14 +03:00
SG
4136a5684a updates 2025-06-18 13:26:29 +03:00
SG
39b5b64e77 updates 2025-06-18 12:14:54 +03:00
SG
0a2d621a1d updates 2025-06-18 10:50:03 +03:00
SG
6ad76fc082 updates 2025-06-18 10:28:56 +03:00
SG
fd6e8d15c0 updates 2025-06-17 18:20:41 +03:00
SG
00f1c1a9ae updates 2025-06-17 18:13:47 +03:00
SG
0ea2fbeda6 updates 2025-06-17 17:07:00 +03:00
SG
326e5c5edd updates 2025-06-17 16:22:47 +03:00
SG
4ea54eaa91 Merge branch 'themes' of https://git.exocortex.ru/Exocortex/hydrogen into themes 2025-06-17 13:46:14 +03:00
SG
438a8d6a14 updates 2025-06-17 13:46:08 +03:00
sg
83e6b7f0a5 updates 2025-06-17 00:21:01 +03:00
SG
0583eb07ee updates 2025-06-16 22:11:24 +03:00
SG
7d81226d83 updates 2025-06-16 18:06:11 +03:00
SG
cfe8e9a799 updates 2025-06-16 17:55:58 +03:00
SG
87484fe341 updates 2025-06-15 01:37:30 +03:00
SG
3dfc802509 updates 2025-06-15 01:09:10 +03:00
SG
3ddf1a97d0 updates 2025-06-15 01:09:05 +03:00
SG
2072bf966f updates 2025-06-14 18:49:56 +03:00
SG
fe74be81c6 updates 2025-06-14 18:46:31 +03:00
SG
793388d4ba moved static files into themes 2025-06-14 18:14:24 +03:00
SG
ed9d1dade2 added script path detection
prepare for PyInstaller
2025-06-14 17:37:33 +03:00
23 changed files with 368 additions and 448 deletions

1
.gitignore vendored
View File

@@ -170,3 +170,4 @@ cython_debug/
public public
content/* content/*
config.yml

View File

@@ -1,15 +1,13 @@
import argparse import argparse
from config import Config from config import config
argparser = argparse.ArgumentParser( argparser = argparse.ArgumentParser(
prog = Config.APP_NAME, prog = config.app_name,
description = Config.APP_DESCRIPTION, description = config.app_description,
epilog = f"See {Config.APP_SRC_URL} for more info.") epilog = f"See {config.app_src_url} for more info.")
subparsers = argparser.add_subparsers(dest='command', required=True) subparsers = argparser.add_subparsers(dest='command', required=True)
#argparser.add_argument('--init', action='store_true', help = 'Initialize new site')
#argparser.add_argument('--content', '--edit', type = str, help = 'Create new content')
subparsers.add_parser('init', help = 'Initialize new site.') subparsers.add_parser('init', help = 'Initialize new site.')
subparsers.add_parser('build', help = "Build the site from existing content.") 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 = subparsers.add_parser('create', aliases = ['new'], help = "Create a new content file.")

View File

@@ -1,67 +1,97 @@
from datetime import datetime from datetime import datetime
import os import os
import sys
import shutil import shutil
from collections import defaultdict from collections import defaultdict
import markdown import markdown
import frontmatter import frontmatter
from bs4 import BeautifulSoup
from pathlib import Path from pathlib import Path
from logger import logger from logger import logger
from config import Config from config import config
from jinja_env import env, content_item_template, index_template
class ContentItem: class ContentItem:
def render_content(self, categories): def render_content(self, categories, target_file = False):
logger.debug(f"Rendering {self.source_filename} to {self.target_filename}") 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: try:
if self.image_file and self.image_file.exists(): if self.image_file and Path(self.image_file).exists():
image_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/images/{self.image_file.name}") 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}") logger.debug(f"Copying {self.image_file} to {image_targetfile}")
shutil.copyfile(self.image_file, image_targetfile) shutil.copyfile(self.image_file, image_targetfile)
self.image_file = f"{self.image_file.stem}.jpg" self.image_file = f"{self.image_file.stem}.jpg"
if self.css_file and self.css_file.exists(): else:
css_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/css/{self.css_file.name}") 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}") logger.debug(f"Copying {self.css_file} to {css_targetfile}")
shutil.copyfile(self.css_file, css_targetfile) shutil.copyfile(self.css_file, css_targetfile)
if self.js_file and self.js_file.exists(): except Exception as e:
js_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/js/{self.js_file.name}") 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}") logger.debug(f"Copying {self.js_file} to {js_targetfile}")
shutil.copyfile(self.js_file, js_targetfile) shutil.copyfile(self.js_file, js_targetfile)
with self.target_filename.open("w", encoding="utf-8") as f:
f.write(content_item_template.render(content_item = self, page_title = self.title, parent_path = '../', categories = categories))
except Exception as e: except Exception as e:
logger.error(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): def parse_content(self):
logger.debug(f"Parsing file {self.source_filename}") logger.debug(f"Parsing file {self.source_file}")
try: try:
self.source_filename = Path(self.source_filename) self.source_file = Path(self.source_file)
self.subdir = self.source_filename.parent self.parent_dir = self.source_file.parent # Most likely './content'
self.slug = self.source_filename.stem self.slug = self.source_file.stem
self.target_filename = Path(f"{Config.OUTPUT_DIR}/{self.source_filename.parent}/{self.source_filename.stem}.html") self.target_file = Path(f"{config.output_dir}/{self.source_file.parent}/{self.source_file.stem}.html")
self.data = frontmatter.load(self.source_filename) self.data = frontmatter.load(self.source_file)
self.preview = self.data.content.replace('\n', '<br>')[:300] self.preview = self.data.content.replace('\n', '<br>')[:300]
self.title = self.data.get("title", self.slug) self.title = self.data.get("title", self.slug)
self.omit_second_title = self.data.get("omit_second_title", False) 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.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.categories = [c for c in self.data.get("categories", []) if c != 'default']
self.hidden = self.data.get("hidden", str(False)) self.hidden = self.data.get("hidden", str(False))
self.data.content = self.data.content.replace('\n', ' \n') self.data.content = self.data.content.replace('\n', ' \n') # For markdown newline rendering
self.html = markdown.markdown(self.data.content) self.html = markdown.markdown(self.data.content)
cover_image_path = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.jpg") cover_image_path = f"{self.source_file.parent}/{self.source_file.stem}.jpg"
self.image_file = cover_image_path if cover_image_path.exists() else "" self.image_file = cover_image_path if Path(cover_image_path).exists() else ""
css_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.css") audio_filepath = f"{self.source_file.parent}/{self.source_file.stem}.mp3"
self.css_file = css_filepath if css_filepath.exists() else "" self.audio_file = audio_filepath if Path(audio_filepath).exists() else ""
js_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.js") css_filepath = f"{self.source_file.parent}/{self.source_file.stem}.css"
self.js_file = js_filepath if js_filepath.exists() else "" 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: except Exception as e:
logger.error(e) logger.error(f"Parser error, {e}")
def create_content(self): def create_content(self):
with open(self.source_filename, mode="w", encoding="utf-8") as f: with open(self.source_file, mode="w", encoding="utf-8") as f:
f.writelines([ f.writelines([
"---\n", "---\n",
f"title: {self.source_filename.stem}\n", f"title: {self.source_file.stem}\n",
f"date: '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}'\n", f"date: '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}'\n",
"description: ''\n", "description: ''\n",
"author: ''\n", "author: ''\n",
@@ -72,42 +102,41 @@ class ContentItem:
"\n\n\n" "\n\n\n"
]) ])
def __init__(self, filename): def __init__(self, file):
self.source_filename = filename self.source_file = file
class Site: class Site:
def __init__(self): def __init__(self):
self.output_dir = Path(Config.OUTPUT_DIR) self.output_dir = Path(config.output_dir)
self.content_dir = Path(Config.CONTENT_DIR) self.content_dir = Path(config.content_dir)
self.static_dir = Path(Config.STATIC_DIR) self.static_dir = Path(config.static_dir)
self.templates_dir = Path(Config.TEMPLATES_DIR) self.templates_dir = Path(config.templates_dir)
self.images_dir = Path(f"{Config.STATIC_DIR}/images") self.images_dir = Path(f"{config.static_dir}/images")
self.css_dir = Path(f"{Config.STATIC_DIR}/css") self.css_dir = Path(f"{config.static_dir}/css")
self.js_dir = Path(f"{Config.STATIC_DIR}/js") self.js_dir = Path(f"{config.static_dir}/js")
self.output_dir = Path(Config.OUTPUT_DIR) self.output_dir = Path(config.output_dir)
self.content_items = [] self.content_items = []
self.categories = defaultdict(list) self.categories = defaultdict(list)
def init_site(self): 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") logger.info("Initializing new site")
config.create_default_config()
# Create directories # Create directories
for subdir in [self.content_dir, self.static_dir, self.templates_dir, for subdir in [self.content_dir]:
self.images_dir, self.css_dir, self.js_dir]:
os.makedirs(subdir, exist_ok=True) os.makedirs(subdir, exist_ok=True)
# Create templates from literals
import templates # Copy default theme
template_names = [t for t in dir(templates) if not t.startswith('_')] shutil.copytree(f"{config.base_dir}/themes/default", "themes/default", dirs_exist_ok=True)
for template_name in template_names:
template_content = getattr(templates, template_name)
with open(self.templates_dir / f"{template_name}.html", "w", encoding="utf8") as f:
f.write(template_content)
# Create static/about.md
def get_content_items(self): def get_content_items(self):
logger.debug("Getting content items") logger.debug("Getting content items")
self.get_content_items = [] self.get_content_items = []
logger.debug(f"Scanning {Path(Config.CONTENT_DIR)}") logger.debug(f"Scanning {Path(config.content_dir)}")
for md_file in Path(Config.CONTENT_DIR).glob("*.md"): for md_file in Path(config.content_dir).glob("*.md"):
content_item = ContentItem(md_file) content_item = ContentItem(md_file)
content_item.parse_content() content_item.parse_content()
self.content_items.append(content_item) self.content_items.append(content_item)
@@ -120,20 +149,19 @@ class Site:
self.categories[category].append(content_item.slug) self.categories[category].append(content_item.slug)
def build(self): def build(self):
# Recreate the output dir if needed # Recreate the output dir if exists
if self.output_dir.exists(): if self.output_dir.exists():
shutil.rmtree(self.output_dir) shutil.rmtree(self.output_dir)
self.output_dir.mkdir() self.output_dir.mkdir()
# Create public subdirs # Create public subdirs
subdirs = [self.images_dir, self.css_dir, self.js_dir, self.content_dir, "categories"] subdirs = ["categories", "content", "static"]
for subdir in subdirs: for subdir in subdirs:
subdir = self.output_dir / subdir subdir = self.output_dir / subdir
subdir.mkdir(parents=True, exist_ok=True) subdir.mkdir(parents=True, exist_ok=True)
# Copy static files if exist # Copy theme's static dir to 'public/static
if self.static_dir.exists(): shutil.copytree(f"{config.base_dir}/themes/{config.theme}/static", f"{self.output_dir}/static", dirs_exist_ok=True)
shutil.copytree(self.static_dir, self.output_dir / self.static_dir, dirs_exist_ok=True)
# Get content items # Get content items
self.get_content_items() self.get_content_items()
@@ -146,22 +174,30 @@ class Site:
content_item.render_content(categories = self.categories) content_item.render_content(categories = self.categories)
# Render the about file # Render the about file
about_content = ContentItem(Path('static/about.md')) about_content = ContentItem(Path(f'themes/{config.theme}/content/about.md'))
about_content.parse_content() about_content.parse_content()
about_content.render_content(categories = self.categories) about_content.render_content(categories = self.categories, target_file='public/static/about.html')
# Render the index file # 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] 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: with (self.output_dir / "index.html").open("w", encoding="utf-8") as f:
f.write(index_template.render(page_title = Config.MAIN_PAGE_TITLE, content_items=visible_content_items, categories = self.categories)) 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 # 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] visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True]
for category in self.categories: for category in self.categories:
category_index = Path(f"{self.output_dir}/categories/{category}.html") 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")] 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: with (category_index).open(mode="w", encoding="utf-8") as f:
f.write(index_template.render(page_title=category, content_items=category_items, categories = self.categories, parent_path = '../')) 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.") logger.info(f"Created {len(self.content_items)} content items.")

View File

@@ -1,14 +1,69 @@
import os import os, sys, yaml
# Main config section from jinja2 import Environment, FileSystemLoader
class Config: from pathlib import Path
MAIN_PAGE_TITLE = "microgen library"
APP_NAME = "hydrogen" class Config:
APP_DESCRIPTION = "Simplistic static site generator" def __init__(self):
APP_SRC_URL = f"https://git.exocortex.ru/Exocortex/{APP_NAME}" self.config_path = "config.yml"
OUTPUT_DIR = 'public' self.app_name = "microgen"
TEMPLATES_DIR = 'templates' self.app_description = "Simplistic static site generator"
CONTENT_DIR = 'content' self.app_src_url = f"https://git.exocortex.ru/Exocortex/{self.app_name}"
STATIC_DIR = 'static' self.content_dir = 'content'
HEADER_IMAGE = 'static/header.jpg' self.templates_dir = 'templates'
THEME = "default" 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)
# Get the script base_dir and check for templates dir
if Path(f'themes/{self.theme}/templates').exists():
self.base_dir = "."
elif getattr(sys, 'frozen', False):
# Running fron Pyinstaller-packed binary
self.base_dir = Path(sys._MEIPASS)
else:
# Running as a plain Python app
self.base_dir = Path(__file__).resolve().parent
# Prepare template rendering engine
if Path(f"themes/{self.theme}/templates").exists():
# 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()

View File

@@ -1,47 +0,0 @@
import os, subprocess
import shutil
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_old():
logger.debug(f"Initializing new site")
# Recreate output directory if necessary
output_dir = Path(Config.OUTPUT_DIR)
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.mkdir()
# Create output directory subtree
for dir in [ Path(Config.CONTENT_DIR), Path(Config.STATIC_DIR), Path(Config.TEMPLATE_DIR),
Path(Config.OUTPUT_IMG_DIR), Path(Config.OUTPUT_CSS_DIR), Path(Config.OUTPUT_JS_DIR)]:
dir.mkdir(exist_ok=True)
# Create "About.md" content item
create_content("about")
def init_site():
site = Site()
site.init_site()
def build_site():
site = Site()
site.build()

29
helpers.py Normal file
View File

@@ -0,0 +1,29 @@
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()

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from argparser import argparser from argparser import argparser
from classes import * from helpers import *
from functions import *
def main(): def main():
args = argparser.parse_args() args = argparser.parse_args()
@@ -11,6 +10,7 @@ def main():
build_site() build_site()
case "init": case "init":
init_site() init_site()
return
case "new" | "create" | "edit": case "new" | "create" | "edit":
edit_content(args.filename) edit_content(args.filename)

View File

@@ -1,8 +1,19 @@
import sys
from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from config import Config 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 # Prepare template rendering engine
env = Environment(loader=FileSystemLoader(Config.TEMPLATES_DIR)) jinja_env_dir = f"{base_dir}/themes/{Config.THEME}/templates"
env.globals['header_image'] = Config.HEADER_IMAGE 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") index_template = env.get_template("index.html")
content_item_template = env.get_template("content_item.html") content_item_template = env.get_template("content_item.html")

View File

@@ -1,12 +1,14 @@
import logging, sys import logging, sys
from config import Config from config import config
# Logging config section # Logging config section
LOG_TO = sys.stdout LOG_TO = sys.stdout
LOG_LEVEL = logging.INFO
if config.debug == True:
LOG_LEVEL = logging.DEBUG LOG_LEVEL = logging.DEBUG
logger = logging.getLogger(Config.APP_NAME) logger = logging.getLogger(config.app_name)
logger.setLevel(LOG_LEVEL) logger.setLevel(LOG_LEVEL)
stdout_handler = logging.StreamHandler(LOG_TO) stdout_handler = logging.StreamHandler(LOG_TO)
stdout_handler.setLevel(LOG_LEVEL) stdout_handler.setLevel(LOG_LEVEL)

View File

@@ -1,4 +1,5 @@
argparse argparse
bs4
python-frontmatter python-frontmatter
jinja2 jinja2
markdown markdown

View File

@@ -1,6 +0,0 @@
---
title: "About"
omit_second_title: True
---

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -1,182 +0,0 @@
default = """
<!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">
<title>{{ page_title }}</title>
<style>
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: 250px;
background: rgb(70, 70, 124);
background-image: url("{{ parent_path }}static/header.jpg");
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;
}
/*
@media (min-width: 768px) {
.top_header_text {
font-size: 4rem;
}
}
*/
article {
font-size: 1.1rem;
line-height: 1.6;
}
</style>
{% block head_includes %}
{% endblock %}
</head>
<body>
<div class="top_header">
<div class="col-auto">
<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="row justify-content-end pe-2 py-2" >
<div 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 id="content" class="container-fluid mb-2">
{% block content %}
{% endblock %}
</div>
<div id="footer" class="container-fluid">
<div id="footer-data" class="row my-1">
{% block footer_includes %}
{% endblock %}
</div>
<div class="row small d-flex text-muted justify-content-end">
<div class="col-auto" style="font-size: 0.75rem;">
<p>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>
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);
}
</script>
</body>
"""
index = """
{% extends "default.html" %}
{% block head_includes %}
{% endblock %}
{% block content %}
<div class="row justify-content-between 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 %}
"""
content_item="""
{% 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 }}" class="img-fluid mb-5 rounded w-100 h-auto">{% endif %}
{% if not content_item.omit_second_title %}
<h1>{{ content_item.title }}</h1>
{% endif %}
<article>{{ content_item.html | safe }}</article>
<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 %}
"""

View File

@@ -1,114 +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">
<title>{{ page_title }}</title>
<style>
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: 250px;
background: rgb(70, 70, 124);
background-image: url("{{ parent_path }}static/header.jpg");
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;
}
/*
@media (min-width: 768px) {
.top_header_text {
font-size: 4rem;
}
}
*/
article {
font-size: 1.1rem;
line-height: 1.6;
}
</style>
{% block head_includes %}
{% endblock %}
</head>
<body>
<div class="top_header">
<div class="col-auto">
<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="row justify-content-end pe-2 py-2" >
<div 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 id="content" class="container-fluid mb-2">
{% block content %}
{% endblock %}
</div>
<div id="footer" class="container-fluid">
<div id="footer-data" class="row my-1">
{% block footer_includes %}
{% endblock %}
</div>
<div class="row small d-flex text-muted justify-content-end">
<div class="col-auto" style="font-size: 0.75rem;">
<p>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>
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);
}
</script>
</body>

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1,9 @@
---
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.

View File

@@ -0,0 +1,37 @@
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;
}

View File

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 95 B

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,20 @@
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);
}

View File

@@ -9,11 +9,24 @@
{% block content %} {% block content %}
<div class="container mt-4 mb-5"> <div class="container mt-4 mb-5">
{% if content_item.image_file %}<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" class="img-fluid mb-5 rounded w-100 h-auto">{% endif %} {% 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 %} {% if not content_item.omit_second_title %}
<h1>{{ content_item.title }}</h1> <h1>{{ content_item.title }}</h1>
{% endif %} {% endif %}
<article>{{ content_item.html | safe }}</article> <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"> <div id="categories">
{% for category in content_item.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> <a href="{{ parent_path }}categories/{{ category }}.html" class="mx-1 text-decoration-none small text-muted">{{ category }} </a>

View File

@@ -0,0 +1,64 @@
<!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>

View File

@@ -1,12 +1,7 @@
{% extends "default.html" %} {% extends "default.html" %}
{% block head_includes %}
{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-between g-4 mb-4 py-4"> <div class="row justify-content-center g-4 mb-4 py-4">
{% for content_item in content_items %} {% for content_item in content_items %}
<div class="col-auto align-items-stretch d-flex mx-2" style="width: 100%; width: 24em;"> <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 h-100 px-0 rounded mx-1 my-3" style="width: 100%; width: 22em;">