1st commit
This commit is contained in:
0
src/py/__init__.py
Normal file
0
src/py/__init__.py
Normal file
71
src/py/css_generator.py
Normal file
71
src/py/css_generator.py
Normal 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
60
src/py/html_generator.py
Normal 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
73
src/py/image_processor.py
Normal 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
34
src/py/utils.py
Normal 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}")
|
Reference in New Issue
Block a user