From fac4f10062f04866fb626b79063b1d459160cc5b Mon Sep 17 00:00:00 2001 From: Florian Date: Sat, 3 Oct 2026 10:47:29 +0200 Subject: [PATCH] cms begonnen zu bauen --- .DS_Store | Bin 6148 -> 12292 bytes ascii/ascii-logo.md | 28 +++ cms.py | 482 +++++++++++++++++++++++++++++++++++++++++ css/style.css | 1 + md/footer.md | 6 + md/startseite.md | 5 + output/css/style.css | 1 + output/index.html | 20 ++ start.command | 4 + start.sh | 4 + templates/default.html | 20 ++ 11 files changed, 571 insertions(+) create mode 100644 ascii/ascii-logo.md create mode 100755 cms.py create mode 100644 css/style.css create mode 100644 md/footer.md create mode 100644 md/startseite.md create mode 100644 output/css/style.css create mode 100644 output/index.html create mode 100755 start.command create mode 100755 start.sh create mode 100644 templates/default.html diff --git a/.DS_Store b/.DS_Store index dc8b8565f60c1108e96f7e8e6aa716aba776c692..f912345d14e1a99293a46850160b35b11351f22d 100644 GIT binary patch literal 12292 zcmeGiTWl0n^qy%cGj@P!El-*3*79hu&?mJ8!R@xZq!naa`k+{McZPQC>`dL6-2zQb zjqw4RXhe<1590H)QKA83Q1pugAJH@^rNv6U9;?(n$y4)6wWA`$d=2;-@HKF!X}}F0d``T-)70S0>ubQ*KpqXy z??VtPg&`MCFz9?bu%TOMI!?FHPQgB&0~}h=g&`MCFzB!%kHdSPQZOKJ-E;U&eIVF5y1i>2l$Pz0bc`oG(cm2*4bCL(yi?5TM)^dea~dh zzQSF_;w-tlr?*e+@6qC>5z+Rm1JWD|55QXx-JVp_c<}9xn|dl?SOZc?JgUc2del~1 zbe&E{+8isMGsb zY1>BVGS*HtX(6l#)i5!I$=UM*ODb2^-Ty#n!Wx}))lf8Jun$k+hOO<<)TB@XRYIo`O{i0)3l=WAXR$2j&d#jTlZt6iq%?byOVaC_9U zw~eSq=9R+oW}fe}hN4Mz+bAX~G92wZ@BFYxrz{&3%{G&cHXbgQ$RlY+TEo-`{uV4l!dGPMt|GL8bt_H`pL2SSjs==P@)jBlWU`Dj;m5y| z>>(z3io8e;laI-9@;Uj2oF-?ZD{d<);f zS@;dk!|!kbF2QB^i!0$4aZ9+R+%m43tKsUndTt}PiQCTgaxrd@8)9N`Zj>Usy<%aw z+=qCsyNxh+<0kXFTVT`XEf0z9x4gUkIGZ}y-DPv<%?~bJQQgqAuH}juGONuPs@DM= zo6qtT$L1Zo$hBAIiDIA&okHC^okL19sq^wvbawf@VnCXKZo|-J%MmR?mtp9ARe^xS zp{FvmrZyl-1?Y!@!@If)QE-vwHKHgLqDQi^^)jNwMZ&GP4D~Pm-?sGc$r*BvoF^B^ zMJR$|kRSk+unHQ`vNytJwC;B3K&_!xwM@zI5z<3Vwp0;g=js?#Q-exR7hf=}6Mh zO=A%0)cQG0rz&}pj}ARywzatlR{qFaZyRQg8KEsr zJiF^2%XrgGOWZCP8>?@?Y;9eA{Xb-G52Bn{rp?^GfP+~w`2pDv$fa^v_5<>b3&@^N z2)dgeko|z1f9rYc2V{>W=LBRY?D&nZfh-Nsv>Ce(c5aLR>kU+6V;ydwnpR)u2FfE* zo-L?%)X#SK*$#hmbpx{<+0N!~t^BRk|8r~Qv14!c;?Sv8TixtM){Lt;jQlh5WHLPb z-mTjWI{!Oy-`NUO(mQ|+)Bpd^ndCRV27C?No*IC{wn$qTWy{=Jw0nzOk+5i9m diff --git a/ascii/ascii-logo.md b/ascii/ascii-logo.md new file mode 100644 index 0000000..7598ce1 --- /dev/null +++ b/ascii/ascii-logo.md @@ -0,0 +1,28 @@ + + + &&&& &&&&& + +xX&&XX+++x + &&$x+xX++x+:; + &;;:$&;:;X&& + ::::::::::; + &&$$$$$$$$$$$;::::x::+:; + &&XXX;::::::::::::::::::xxx + &&$x+x;::::::::::::::::::X& + &&X+++x;::::::::::::::::::X& + &&Xxxxx;::::::::::::::::::X& + &&&&&&+;::++;::;++;::;++:x&& + &$::&&X:;&&&&;:X&&::X& + &&xX&&$:;&&&&Xx&&&:;X& + &&&&&&&&& &&&& &&&& + + +██╗ ██╗███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗ ██╗███╗ ███╗ +██║ ██║██╔════╝██╔════╝ ██╔═══██╗██╔════╝ ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██║ ██║████╗ ████║ +███████║█████╗ ██║ ███╗ ██║ ██║███████╗ ██████╔╝██║ ██║██║██║ ██║ ██║ ██║ ██║██╔████╔██║ +██╔══██║██╔══╝ ██║ ██║ ██║ ██║╚════██║ ██╔══██╗██║ ██║██║██║ ██║ ██║ ╚██╗ ██╔╝██║╚██╔╝██║ +██║ ██║██║ ╚██████╔╝███████╗╚██████╔╝███████║ ██████╔╝╚██████╔╝██║███████╗██████╔╝ ╚████╔╝ ██║ ╚═╝ ██║ +╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═══╝ ╚═╝ ╚═╝ + +DEBIAN 13 VM auf w4d-gpu unterm Schreibtisch + + diff --git a/cms.py b/cms.py new file mode 100755 index 0000000..61612d3 --- /dev/null +++ b/cms.py @@ -0,0 +1,482 @@ +#!/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" +FOOTER_PAGE = "footer" # Baustein: wird in jede Seite eingefuegt, nicht eigenstaendig exportiert + + +# ---------------------------------------------------------------- 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("" % 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("%s" % (level, inline(_escape(m.group(2))), level)) + i += 1 + continue + + if re.match(r"^-{3,}$", stripped): + close_list() + out.append("
") + i += 1 + continue + + m = re.match(r"^([-*+])\s+(.*)$", stripped) + if m: + if not in_list or list_tag != "ul": + close_list() + out.append("
    ") + in_list, list_tag = True, "ul" + out.append("
  • %s
  • " % inline(_escape(m.group(2)))) + i += 1 + continue + + m = re.match(r"^\d+\.\s+(.*)$", stripped) + if m: + if not in_list or list_tag != "ol": + close_list() + out.append("
      ") + in_list, list_tag = True, "ol" + out.append("
    1. %s
    2. " % inline(_escape(m.group(1)))) + i += 1 + continue + + if stripped.startswith(">"): + close_list() + quote = [stripped.lstrip("> ")] + while i + 1 < len(lines) and lines[i + 1].strip().startswith(">"): + i += 1 + quote.append(lines[i].strip().lstrip("> ")) + out.append("

      %s

      " % 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

      " % 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] != 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 render_page(raw, template, footer_html=""): + meta, body = parse_md(raw) + html = md_to_html(body) + page = (template + .replace("{{titel}}", _escape(meta.get("titel", ""))) + .replace("{{inhalt}}", html)) + if "{{footer}}" in template: + page = page.replace("{{footer}}", footer_html) + elif footer_html and "" in page: + # Template ohne {{footer}}-Platzhalter: Baustein automatisch anhaengen + idx = page.rindex("") + page = page[:idx] + "
      \n" + footer_html + "\n
      \n" + page[idx:] + return page, meta.get("titel", "") + + +def load_template(): + files = [f for f in os.listdir(TPL_DIR) if f.endswith(".html")] + if not files: + raise SystemExit("Kein Template in templates/ gefunden (z.B. default.html).") + with open(os.path.join(TPL_DIR, files[0]), encoding="utf-8") as fh: + return fh.read() + + +def export_site(): + """Baut output/: pro MD-Datei eine HTML-Seite, CSS wird kopiert.""" + template = load_template() + # Output leeren und neu aufbauen + if os.path.isdir(OUT_DIR): + shutil.rmtree(OUT_DIR) + os.makedirs(os.path.join(OUT_DIR, "css"), exist_ok=True) + + for f in os.listdir(CSS_DIR): + if f.endswith(".css"): + shutil.copy2(os.path.join(CSS_DIR, f), os.path.join(OUT_DIR, "css", f)) + + footer_html = "" + if os.path.exists(md_path(FOOTER_PAGE)): + _, fbody = parse_md(load_page(FOOTER_PAGE)) + footer_html = md_to_html(fbody) + + exported = [] + for slug in list_pages(): + page, _ = render_page(load_page(slug), template, footer_html) + # startseite.md wird zur index.html + out_name = "index.html" if slug == START_PAGE else slug + ".html" + with open(os.path.join(OUT_DIR, out_name), "w", encoding="utf-8") as fh: + fh.write(page) + exported.append(out_name) + return exported + + +# ------------------------------------------------------------------- server + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def _send(self, code, body, ctype="application/json; charset=utf-8"): + data = body.encode("utf-8") if isinstance(body, str) else body + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def _json(self, obj, code=200): + self._send(code, json.dumps(obj)) + + def _body(self): + n = int(self.headers.get("Content-Length", 0)) + return json.loads(self.rfile.read(n).decode("utf-8")) if n else {} + + def do_GET(self): + path = urlparse(self.path).path + + if path in ("/", "/admin"): + self._send(200, ADMIN_HTML, "text/html; charset=utf-8") + elif path == "/api/pages": + self._json({"pages": list_pages(), "start": START_PAGE, + "footer": os.path.exists(md_path(FOOTER_PAGE))}) + elif path.startswith("/api/page/"): + slug = unquote(path[len("/api/page/"):]) + if slug == FOOTER_PAGE or slug in list_pages(): + self._json({"slug": slug, "raw": load_page(slug)}) + else: + self._json({"error": "Seite nicht gefunden"}, 404) + elif path == "/api/export": + self._json({"exported": export_site()}) + else: + self._json({"error": "unbekannter Pfad"}, 404) + + def do_POST(self): + path = urlparse(self.path).path + try: + data = self._body() + except (ValueError, json.JSONDecodeError): + return self._json({"error": "ungueltige Anfrage"}, 400) + + if path == "/api/page": + slug = (data.get("slug") or "").strip() + raw = data.get("raw") or "" + if not SLUG_RE.match(slug): + return self._json({"error": "Ungueltiger Name (a-z, 0-9, -)."}, 400) + save_page(slug, raw) + self._json({"ok": True, "slug": slug}) + elif path == "/api/delete": + slug = data.get("slug", "") + if slug != FOOTER_PAGE and slug not in list_pages(): + return self._json({"error": "Seite nicht gefunden"}, 404) + if slug in (START_PAGE, FOOTER_PAGE): + return self._json({"error": "Startseite und Footer-Baustein koennen nicht geloescht werden."}, 400) + delete_page(slug) + self._json({"ok": True}) + elif path == "/api/preview": + meta, body = parse_md(data.get("raw") or "") + self._json({"html": md_to_html(body)}) + else: + self._json({"error": "unbekannter Pfad"}, 404) + + +ADMIN_HTML = """ + + + +Mini-CMS + + + +
      +

      Seiten

      +
      +
      +

      Neue Seite:

      + + +
      +
      +
      + - + + + + +
      +
      + +
      +
      + + +""" + + +def main(): + global PORT + if len(sys.argv) > 1 and sys.argv[1] == "export": + files = export_site() + print("Exportiert nach output/:") + for f in files: + print(" -", f) + return + + if len(sys.argv) > 2 and sys.argv[1] == "port": + PORT = int(sys.argv[2]) + + server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler) + url = "http://localhost:%d" % PORT + print("Mini-CMS laeuft unter", url) + print("Beenden mit Strg+C.") + threading.Timer(1.0, lambda: webbrowser.open(url)).start() + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nServer gestoppt.") + + +if __name__ == "__main__": + main() diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..e3ec04d --- /dev/null +++ b/css/style.css @@ -0,0 +1 @@ +/* Stylesheet der Website - hier das Layout gestalten. */ diff --git a/md/footer.md b/md/footer.md new file mode 100644 index 0000000..cd5fb52 --- /dev/null +++ b/md/footer.md @@ -0,0 +1,6 @@ +--- +titel: Footer +--- + +Dieser Footer-Baustein wird automatisch an das Ende **aller** Seiten gesetzt. +Zum Beispiel mit Links: [Startseite](index.html) diff --git a/md/startseite.md b/md/startseite.md new file mode 100644 index 0000000..578ade4 --- /dev/null +++ b/md/startseite.md @@ -0,0 +1,5 @@ +--- +titel: startseite +--- + +Neue Seite. hier kommen alle inhalte hin. diff --git a/output/css/style.css b/output/css/style.css new file mode 100644 index 0000000..e3ec04d --- /dev/null +++ b/output/css/style.css @@ -0,0 +1 @@ +/* Stylesheet der Website - hier das Layout gestalten. */ diff --git a/output/index.html b/output/index.html new file mode 100644 index 0000000..8c182bd --- /dev/null +++ b/output/index.html @@ -0,0 +1,20 @@ + + + + + + startseite + + + +
      +

      startseite

      +
      +
      +

      Neue Seite. hier kommen alle inhalte hin.

      +
      +
      +

      Dieser Footer-Baustein wird automatisch an das Ende aller Seiten gesetzt. Zum Beispiel mit Links: Startseite

      +
      + + diff --git a/start.command b/start.command new file mode 100755 index 0000000..fa11e95 --- /dev/null +++ b/start.command @@ -0,0 +1,4 @@ +#!/bin/sh +# Mini-CMS per Doppelklick starten (macOS) +cd "$(dirname "$0")" || exit 1 +python3 cms.py "$@" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..28a04a6 --- /dev/null +++ b/start.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Mini-CMS starten (macOS und Linux) +cd "$(dirname "$0")" || exit 1 +exec python3 cms.py "$@" diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..f916d9b --- /dev/null +++ b/templates/default.html @@ -0,0 +1,20 @@ + + + + + + {{titel}} + + + +
      +

      {{titel}}

      +
      +
      + {{inhalt}} +
      + + +