#!/usr/bin/env python3
"""Mini-CMS: MD-Dateien in md/ per Browseroberflaeche bearbeiten
und als statische Seite (HTML + CSS) nach output/ exportieren.
Nur Python-Standardbibliothek, kein Internet noetig.
Aufruf:
python3 cms.py -> Server + UI starten (http://localhost:8500)
python3 cms.py export -> nur exportieren, kein Server
python3 cms.py port 9000 -> Server auf anderem Port
"""
import json
import os
import re
import shutil
import sys
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
ROOT = os.path.dirname(os.path.abspath(__file__))
MD_DIR = os.path.join(ROOT, "md")
TPL_DIR = os.path.join(ROOT, "templates")
CSS_DIR = os.path.join(ROOT, "css")
OUT_DIR = os.path.join(ROOT, "output")
PORT = 8500
for d in (MD_DIR, TPL_DIR, CSS_DIR, OUT_DIR):
os.makedirs(d, exist_ok=True)
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$", re.IGNORECASE)
START_PAGE = "startseite"
HEADER_PAGE = "header" # Baustein: wird ueber den Inhalt jeder Seite gesetzt
FOOTER_PAGE = "footer" # Baustein: wird unter den Inhalt jeder Seite gesetzt
# ---------------------------------------------------------------- markdown
def md_to_html(text):
"""Minimaler Markdown->HTML-Konverter:
Ueberschriften, **fett**, *kursiv*, `code`, [Links](url),
Listen, Code-Bloecke, > Zitate, --- Trennlinien, Absaetze.
"""
lines = text.replace("\r\n", "\n").split("\n")
out = []
i = 0
in_code = False
in_list = False
list_tag = None
def inline(s):
s = re.sub(r"`([^`]+)`", r"\1", s)
s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s)
s = re.sub(r"\*([^*]+)\*", r"\1", s)
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', s)
return s
def close_list():
nonlocal in_list, list_tag
if in_list:
out.append("%s>" % list_tag)
in_list = False
list_tag = None
while i < len(lines):
line = lines[i]
if line.strip().startswith("```"):
close_list()
if not in_code:
out.append("
")
in_code = True
else:
out.append("")
in_code = False
i += 1
continue
if in_code:
out.append(_escape(line))
i += 1
continue
stripped = line.strip()
if not stripped:
close_list()
i += 1
continue
m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
if m:
close_list()
level = len(m.group(1))
out.append("" % inline(_escape(" ".join(quote)))) i += 1 continue close_list() para = [stripped] while (i + 1 < len(lines) and lines[i + 1].strip() and not re.match(r"^(#{1,6}\s|[-*+]\s|\d+\.\s|>|```|-{3,}$)", lines[i + 1].strip())): i += 1 para.append(lines[i].strip()) out.append("%s
%s
" % inline(_escape(" ".join(para)))) i += 1 close_list() return "\n".join(out) def _escape(s): return (s.replace("&", "&").replace("<", "<").replace(">", ">")) # ------------------------------------------------------------------- seiten def parse_md(raw): """Trennt Front-Matter (titel: ...) vom Inhalt.""" meta = {} body = raw if raw.startswith("---"): parts = raw.split("---", 2) if len(parts) >= 3: for line in parts[1].strip().splitlines(): if ":" in line: k, v = line.split(":", 1) meta[k.strip().lower()] = v.strip() body = parts[2].lstrip("\n") return meta, body def md_path(slug): return os.path.join(MD_DIR, slug + ".md") def list_pages(): pages = [] for f in sorted(os.listdir(MD_DIR)): if f.endswith(".md") and f[:-3] not in (HEADER_PAGE, FOOTER_PAGE): pages.append(f[:-3]) return pages def load_page(slug): with open(md_path(slug), encoding="utf-8") as fh: return fh.read() def save_page(slug, raw): with open(md_path(slug), "w", encoding="utf-8") as fh: fh.write(raw) def delete_page(slug): p = md_path(slug) if os.path.exists(p): os.remove(p) # ------------------------------------------------------------------ export def block_html(slug): """Rendert einen Baustein (header/footer) als HTML, leer wenn nicht vorhanden.""" if os.path.exists(md_path(slug)): _, body = parse_md(load_page(slug)) return md_to_html(body) return "" def render_page(raw, template, header_html="", footer_html=""): meta, body = parse_md(raw) html = md_to_html(body) page = (template .replace("{{titel}}", _escape(meta.get("titel", ""))) .replace("{{inhalt}}", html)) if "{{header}}" in template: page = page.replace("{{header}}", header_html) elif header_html and "" in page: # Template ohne {{header}}-Platzhalter: Baustein automatisch setzen idx = page.index("") + len("") page = page[:idx] + "\nNeue Seite: