Compare commits
34 Commits
bootstrap-
...
themes
| Author | SHA1 | Date | |
|---|---|---|---|
| 808aab3609 | |||
| 162b45809b | |||
| 664a6adb77 | |||
| b633c21ef2 | |||
| 802f8022d0 | |||
| d58d9464a9 | |||
| dca4aa7a7e | |||
| 8813d91493 | |||
| 9fa58d4700 | |||
| 15879e66b7 | |||
| 3abe3f2fac | |||
| d3b369be46 | |||
| 66d3132e53 | |||
| 4136a5684a | |||
| 39b5b64e77 | |||
| 0a2d621a1d | |||
| 6ad76fc082 | |||
| fd6e8d15c0 | |||
| 00f1c1a9ae | |||
| 0ea2fbeda6 | |||
| 326e5c5edd | |||
| 4ea54eaa91 | |||
| 438a8d6a14 | |||
| 83e6b7f0a5 | |||
| 0583eb07ee | |||
| 7d81226d83 | |||
| cfe8e9a799 | |||
| 87484fe341 | |||
| 3dfc802509 | |||
| 3ddf1a97d0 | |||
| 2072bf966f | |||
| fe74be81c6 | |||
| 793388d4ba | |||
| ed9d1dade2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -170,3 +170,4 @@ cython_debug/
|
||||
|
||||
public
|
||||
content/*
|
||||
config.yml
|
||||
|
||||
10
argparser.py
10
argparser.py
@@ -1,15 +1,13 @@
|
||||
import argparse
|
||||
from config import Config
|
||||
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.")
|
||||
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)
|
||||
|
||||
#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('build', help = "Build the site from existing content.")
|
||||
new_content = subparsers.add_parser('create', aliases = ['new'], help = "Create a new content file.")
|
||||
|
||||
160
classes.py
160
classes.py
@@ -1,67 +1,97 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
import markdown
|
||||
import frontmatter
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
from logger import logger
|
||||
from config import Config
|
||||
from jinja_env import env, content_item_template, index_template
|
||||
from config import config
|
||||
|
||||
class ContentItem:
|
||||
def render_content(self, categories):
|
||||
logger.debug(f"Rendering {self.source_filename} to {self.target_filename}")
|
||||
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 self.image_file.exists():
|
||||
image_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/images/{self.image_file.name}")
|
||||
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"
|
||||
if self.css_file and self.css_file.exists():
|
||||
css_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/css/{self.css_file.name}")
|
||||
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)
|
||||
if self.js_file and self.js_file.exists():
|
||||
js_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/js/{self.js_file.name}")
|
||||
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)
|
||||
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:
|
||||
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):
|
||||
logger.debug(f"Parsing file {self.source_filename}")
|
||||
logger.debug(f"Parsing file {self.source_file}")
|
||||
try:
|
||||
self.source_filename = Path(self.source_filename)
|
||||
self.subdir = self.source_filename.parent
|
||||
self.slug = self.source_filename.stem
|
||||
self.target_filename = Path(f"{Config.OUTPUT_DIR}/{self.source_filename.parent}/{self.source_filename.stem}.html")
|
||||
self.data = frontmatter.load(self.source_filename)
|
||||
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')
|
||||
self.data.content = self.data.content.replace('\n', ' \n') # For markdown newline rendering
|
||||
self.html = markdown.markdown(self.data.content)
|
||||
cover_image_path = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.jpg")
|
||||
self.image_file = cover_image_path if cover_image_path.exists() else ""
|
||||
css_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.css")
|
||||
self.css_file = css_filepath if css_filepath.exists() else ""
|
||||
js_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.js")
|
||||
self.js_file = js_filepath if js_filepath.exists() else ""
|
||||
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(e)
|
||||
logger.error(f"Parser error, {e}")
|
||||
|
||||
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([
|
||||
"---\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",
|
||||
"description: ''\n",
|
||||
"author: ''\n",
|
||||
@@ -72,42 +102,41 @@ class ContentItem:
|
||||
"\n\n\n"
|
||||
])
|
||||
|
||||
def __init__(self, filename):
|
||||
self.source_filename = filename
|
||||
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.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, self.static_dir, self.templates_dir,
|
||||
self.images_dir, self.css_dir, self.js_dir]:
|
||||
for subdir in [self.content_dir]:
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
# Create templates from literals
|
||||
import templates
|
||||
template_names = [t for t in dir(templates) if not t.startswith('_')]
|
||||
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
|
||||
|
||||
# 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"):
|
||||
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)
|
||||
@@ -120,20 +149,19 @@ class Site:
|
||||
self.categories[category].append(content_item.slug)
|
||||
|
||||
def build(self):
|
||||
# Recreate the output dir if needed
|
||||
# Recreate the output dir if exists
|
||||
if self.output_dir.exists():
|
||||
shutil.rmtree(self.output_dir)
|
||||
self.output_dir.mkdir()
|
||||
|
||||
# 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:
|
||||
subdir = self.output_dir / subdir
|
||||
subdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy static files if exist
|
||||
if self.static_dir.exists():
|
||||
shutil.copytree(self.static_dir, self.output_dir / self.static_dir, dirs_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()
|
||||
@@ -146,22 +174,30 @@ class Site:
|
||||
content_item.render_content(categories = self.categories)
|
||||
|
||||
# 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.render_content(categories = self.categories)
|
||||
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(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
|
||||
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")]
|
||||
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(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.")
|
||||
|
||||
81
config.py
81
config.py
@@ -1,14 +1,69 @@
|
||||
import os
|
||||
# Main config section
|
||||
class Config:
|
||||
MAIN_PAGE_TITLE = "microgen library"
|
||||
import os, sys, yaml
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from pathlib import Path
|
||||
|
||||
APP_NAME = "hydrogen"
|
||||
APP_DESCRIPTION = "Simplistic static site generator"
|
||||
APP_SRC_URL = f"https://git.exocortex.ru/Exocortex/{APP_NAME}"
|
||||
OUTPUT_DIR = 'public'
|
||||
TEMPLATES_DIR = 'templates'
|
||||
CONTENT_DIR = 'content'
|
||||
STATIC_DIR = 'static'
|
||||
HEADER_IMAGE = 'static/header.jpg'
|
||||
THEME = "default"
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.config_path = "config.yml"
|
||||
self.app_name = "microgen"
|
||||
self.app_description = "Simplistic static site generator"
|
||||
self.app_src_url = f"https://git.exocortex.ru/Exocortex/{self.app_name}"
|
||||
self.content_dir = 'content'
|
||||
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)
|
||||
|
||||
|
||||
# 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()
|
||||
47
functions.py
47
functions.py
@@ -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
29
helpers.py
Normal 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()
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from argparser import argparser
|
||||
from classes import *
|
||||
from functions import *
|
||||
from helpers import *
|
||||
|
||||
def main():
|
||||
args = argparser.parse_args()
|
||||
@@ -11,6 +10,7 @@ def main():
|
||||
build_site()
|
||||
case "init":
|
||||
init_site()
|
||||
return
|
||||
case "new" | "create" | "edit":
|
||||
edit_content(args.filename)
|
||||
|
||||
|
||||
15
jinja_env.py
15
jinja_env.py
@@ -1,8 +1,19 @@
|
||||
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
|
||||
env = Environment(loader=FileSystemLoader(Config.TEMPLATES_DIR))
|
||||
env.globals['header_image'] = Config.HEADER_IMAGE
|
||||
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")
|
||||
@@ -1,12 +1,14 @@
|
||||
import logging, sys
|
||||
from config import Config
|
||||
from config import config
|
||||
|
||||
# Logging config section
|
||||
|
||||
LOG_TO = sys.stdout
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
LOG_LEVEL = logging.INFO
|
||||
if config.debug == True:
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
|
||||
logger = logging.getLogger(Config.APP_NAME)
|
||||
logger = logging.getLogger(config.app_name)
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
stdout_handler = logging.StreamHandler(LOG_TO)
|
||||
stdout_handler.setLevel(LOG_LEVEL)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
argparse
|
||||
bs4
|
||||
python-frontmatter
|
||||
jinja2
|
||||
markdown
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "About"
|
||||
omit_second_title: True
|
||||
---
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
182
templates.py
182
templates.py
@@ -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 %}
|
||||
"""
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
9
themes/default/content/about.md
Normal file
9
themes/default/content/about.md
Normal 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.
|
||||
37
themes/default/static/css/theme.css
Normal file
37
themes/default/static/css/theme.css
Normal 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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 95 B After Width: | Height: | Size: 95 B |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
20
themes/default/static/js/theme.js
Normal file
20
themes/default/static/js/theme.js
Normal 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);
|
||||
}
|
||||
@@ -9,11 +9,24 @@
|
||||
|
||||
{% 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 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>
|
||||
64
themes/default/templates/default.html
Normal file
64
themes/default/templates/default.html
Normal 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>
|
||||
@@ -1,12 +1,7 @@
|
||||
|
||||
{% extends "default.html" %}
|
||||
|
||||
{% block head_includes %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
<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;">
|
||||
Reference in New Issue
Block a user