1st commit

This commit is contained in:
2025-08-06 11:16:18 +00:00
commit b9e3467c01
49 changed files with 2345 additions and 0 deletions

0
src/py/__init__.py Normal file
View File

71
src/py/css_generator.py Normal file
View File

@ -0,0 +1,71 @@
import logging
from pathlib import Path
from shutil import copyfile
def generate_css_variables(colors_dict, output_path):
css_lines = [":root {"]
for key, value in colors_dict.items():
css_lines.append(f" --color-{key.replace('_', '-')}: {value};")
css_lines.append("}")
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(css_lines))
logging.info(f"[✓] CSS variables written to {output_path}")
def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
font_files = list(fonts_dir.glob("*"))
font_faces = {}
preload_links = []
format_map = {".woff2": "woff2", ".woff": "woff", ".ttf": "truetype", ".otf": "opentype"}
for font_file in font_files:
name = font_file.stem
ext = font_file.suffix.lower()
if ext not in format_map:
continue
font_faces.setdefault(name, []).append((font_file.name, format_map[ext]))
dest_font_path = output_path.parent.parent / "fonts" / font_file.name
dest_font_path.parent.mkdir(parents=True, exist_ok=True)
copyfile(font_file, dest_font_path)
preload_links.append(
f'<link rel="preload" href="fonts/{font_file.name}" as="font" type="font/{format_map[ext]}" crossorigin>'
)
css_lines = []
for font_name, sources in font_faces.items():
css_lines.append(f"@font-face {{")
css_lines.append(f" font-family: '{font_name}';")
srcs = [f"url('../fonts/{file}') format('{fmt}')" for file, fmt in sorted(sources)]
css_lines.append(f" src: {', '.join(srcs)};")
css_lines.append(" font-weight: normal;")
css_lines.append(" font-style: normal;")
css_lines.append("}")
if fonts_cfg:
css_lines.append(":root {")
if "primary" in fonts_cfg:
p = fonts_cfg["primary"]
css_lines.append(f" --font-primary: '{p['name']}', {p['fallback']};")
if "secondary" in fonts_cfg:
s = fonts_cfg["secondary"]
css_lines.append(f" --font-secondary: '{s['name']}', {s['fallback']};")
css_lines.append("}")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text("\n\n".join(css_lines), encoding="utf-8")
logging.info(f"[✓] Generated fonts CSS: {output_path}")
return preload_links
def generate_google_fonts_link(fonts):
if not fonts:
return ""
families = []
for font in fonts:
family = font["family"].replace(" ", "+")
weights = font.get("weights", [])
if weights:
families.append(f"{family}:wght@{';'.join(weights)}")
else:
families.append(family)
href = "https://fonts.googleapis.com/css2?" + "&".join(f"family={f}" for f in families) + "&display=swap"
return f'<link href="{href}" rel="stylesheet">'

60
src/py/html_generator.py Normal file
View File

@ -0,0 +1,60 @@
import json
import logging
from pathlib import Path
def render_template(template_path, context):
with open(template_path, encoding="utf-8") as f:
content = f.read()
for key, value in context.items():
placeholder = "{{ " + key + " }}"
content = content.replace(placeholder, str(value) if value is not None else "")
return content
def render_gallery_images(images):
html = ""
for img in images:
tags = " ".join(img.get("tags", []))
tag_html = "".join(f'<span class="tag">#{t}</span>' for t in img.get("tags", []))
html += f"""
<div class="section" data-tags="{tags}">
<div class="tags">{tag_html}</div>
<img class="fade-in-img lazyload" data-src="/img/{img['src']}" alt="{img.get('alt', '')}" loading="lazy">
</div>
"""
return html
def generate_gallery_json_from_images(images, output_path):
try:
img_list = [img["src"] for img in images]
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(img_list, f, indent=2)
logging.info(f"[✓] Generated hero gallery JSON: {output_path}")
except Exception as e:
logging.error(f"[✗] Error generating gallery JSON: {e}")
def generate_robots_txt(canonical_url, allowed_paths):
robots_lines = ["User-agent: *"]
for path in allowed_paths:
robots_lines.append(f"Allow: {path}")
robots_lines.append("Disallow: /")
robots_lines.append("")
robots_lines.append(f"Sitemap: {canonical_url}/sitemap.xml")
content = "\n".join(robots_lines)
output_path = Path(".output/robots.txt")
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)
logging.info(f"[✓] robots.txt generated at {output_path}")
def generate_sitemap_xml(canonical_url, allowed_paths):
urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
urlset_end = '</urlset>\n'
urls = ""
for path in allowed_paths:
loc = canonical_url.rstrip("/") + path
urls += f" <url>\n <loc>{loc}</loc>\n </url>\n"
sitemap_content = urlset_start + urls + urlset_end
output_path = Path(".output/sitemap.xml")
with open(output_path, "w", encoding="utf-8") as f:
f.write(sitemap_content)
logging.info(f"[✓] sitemap.xml generated at {output_path}")

73
src/py/image_processor.py Normal file
View File

@ -0,0 +1,73 @@
import logging
from pathlib import Path
from PIL import Image
from shutil import copyfile
def convert_and_resize_image(input_path, output_path, resize=True, max_width=1140):
try:
img = Image.open(input_path)
if img.mode != "RGB":
img = img.convert("RGB")
if resize:
width, height = img.size
if width > max_width:
new_height = int((max_width / width) * height)
img = img.resize((max_width, new_height), Image.LANCZOS)
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, "WEBP", quality=100)
logging.info(f"[✓] Processed: {input_path}{output_path}")
except Exception as e:
logging.error(f"[✗] Failed to process {input_path}: {e}")
def process_images(images, resize_images, img_dir, build_dir):
for img in images:
src_path = img_dir / img["src"]
webp_path = build_dir / "img" / Path(img["src"]).with_suffix(".webp")
convert_and_resize_image(src_path, webp_path, resize=resize_images)
img["src"] = str(Path(img["src"]).with_suffix(".webp"))
def copy_original_images(images, img_dir, build_dir):
for img in images:
src_path = img_dir / img["src"]
dest_path = build_dir / "img" / img["src"]
try:
dest_path.parent.mkdir(parents=True, exist_ok=True)
copyfile(src_path, dest_path)
logging.info(f"[✓] Copied original: {src_path}{dest_path}")
except Exception as e:
logging.error(f"[✗] Failed to copy {src_path}: {e}")
def generate_favicons_from_logo(theme_vars, theme_dir, output_dir):
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] No favicon path defined, skipping favicon PNGs.")
return
output_dir.mkdir(parents=True, exist_ok=True)
specs = [(32, "favicon-32.png"), (96, "favicon-96.png"), (128, "favicon-128.png"),
(192, "favicon-192.png"), (196, "favicon-196.png"), (152, "favicon-152.png"), (180, "favicon-180.png")]
img = Image.open(logo_path).convert("RGBA")
for size, name in specs:
img.resize((size, size), Image.LANCZOS).save(output_dir / name, format="PNG")
logging.info(f"[✓] Favicons generated in {output_dir}")
def generate_favicon_ico(theme_vars, theme_dir, output_path):
logo_path = get_favicon_path(theme_vars, theme_dir)
if not logo_path:
logging.warning("[~] No favicon path defined, skipping .ico generation.")
return
try:
img = Image.open(logo_path).convert("RGBA")
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, format="ICO", sizes=[(16, 16), (32, 32), (48, 48)])
logging.info(f"[✓] favicon.ico generated at {output_path}")
except Exception as e:
logging.error(f"[✗] Failed to generate favicon.ico: {e}")
def get_favicon_path(theme_vars, theme_dir):
fav_path = theme_vars.get("favicon", {}).get("path")
if not fav_path:
return None
path = Path(fav_path)
if not path.is_absolute():
path = theme_dir / path
return path if path.exists() else None

34
src/py/utils.py Normal file
View File

@ -0,0 +1,34 @@
import yaml
import logging
from pathlib import Path
from shutil import copytree, rmtree, copyfile
def load_yaml(path):
if not path.exists():
logging.warning(f"[!] YAML file not found: {path}")
return {}
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def load_theme_config(theme_name, themes_dir):
theme_dir = themes_dir / theme_name
theme_config_path = theme_dir / "theme.yaml"
if not theme_config_path.exists():
raise FileNotFoundError(f"[✗] Theme config not found: {theme_config_path}")
with open(theme_config_path, "r", encoding="utf-8") as f:
theme_vars = yaml.safe_load(f)
return theme_vars, theme_dir
def ensure_dir(path):
if path.exists():
rmtree(path)
path.mkdir(parents=True)
def copy_assets(js_dir, style_dir, build_dir):
for folder in [js_dir, style_dir]:
if folder.exists():
dest = build_dir / folder.name
copytree(folder, dest)
logging.info(f"[✓] Copied assets from {folder.name}")
else:
logging.warning(f"[~] Skipped missing folder: {folder.name}")