|
|
|
@@ -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"<code>\1</code>", s)
|
|
|
|
|
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
|
|
|
|
|
s = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", s)
|
|
|
|
|
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', 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("<pre><code>")
|
|
|
|
|
in_code = True
|
|
|
|
|
else:
|
|
|
|
|
out.append("</code></pre>")
|
|
|
|
|
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("<h%d>%s</h%d>" % (level, inline(_escape(m.group(2))), level))
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if re.match(r"^-{3,}$", stripped):
|
|
|
|
|
close_list()
|
|
|
|
|
out.append("<hr>")
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
m = re.match(r"^([-*+])\s+(.*)$", stripped)
|
|
|
|
|
if m:
|
|
|
|
|
if not in_list or list_tag != "ul":
|
|
|
|
|
close_list()
|
|
|
|
|
out.append("<ul>")
|
|
|
|
|
in_list, list_tag = True, "ul"
|
|
|
|
|
out.append("<li>%s</li>" % 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("<ol>")
|
|
|
|
|
in_list, list_tag = True, "ol"
|
|
|
|
|
out.append("<li>%s</li>" % 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("<blockquote><p>%s</p></blockquote>" % 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("<p>%s</p>" % 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 "</body>" in page:
|
|
|
|
|
# Template ohne {{footer}}-Platzhalter: Baustein automatisch anhaengen
|
|
|
|
|
idx = page.rindex("</body>")
|
|
|
|
|
page = page[:idx] + "<footer>\n" + footer_html + "\n</footer>\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 = """<!DOCTYPE html>
|
|
|
|
|
<html lang="de">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<title>Mini-CMS</title>
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: -apple-system, "Segoe UI", sans-serif; margin: 0; display: flex; height: 100vh; }
|
|
|
|
|
#side { width: 220px; background: #f2f2f2; padding: 1rem; box-sizing: border-box; }
|
|
|
|
|
#side h1 { font-size: 1rem; }
|
|
|
|
|
#pages a { display: block; padding: .35rem .5rem; border-radius: 4px; cursor: pointer; text-decoration: none; color: #000; }
|
|
|
|
|
#pages a.active, #pages a:hover { background: #ddd; }
|
|
|
|
|
#main { flex: 1; display: flex; flex-direction: column; padding: 1rem; box-sizing: border-box; gap: .5rem; }
|
|
|
|
|
#toolbar { display: flex; gap: .5rem; align-items: center; }
|
|
|
|
|
button { padding: .45rem .9rem; border: 1px solid #999; border-radius: 4px; background: #fff; cursor: pointer; }
|
|
|
|
|
button.primary { background: #0066cc; color: #fff; border-color: #0066cc; }
|
|
|
|
|
input[type=text] { padding: .45rem; border: 1px solid #999; border-radius: 4px; }
|
|
|
|
|
#editor { flex: 1; width: 100%; font-family: monospace; font-size: .9rem; padding: .5rem; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; resize: none; }
|
|
|
|
|
#preview { flex: 1; overflow: auto; border: 1px solid #ccc; border-radius: 4px; padding: 1rem; display: none; }
|
|
|
|
|
#msg { min-height: 1.2rem; font-size: .85rem; color: #060; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="side">
|
|
|
|
|
<h1>Seiten</h1>
|
|
|
|
|
<div id="pages"></div>
|
|
|
|
|
<hr>
|
|
|
|
|
<p style="font-size:.8rem">Neue Seite:</p>
|
|
|
|
|
<input type="text" id="newslug" placeholder="z.B. ueber-uns" style="width:100%;box-sizing:border-box;padding:.35rem">
|
|
|
|
|
<button id="btn-new" style="width:100%;margin-top:.4rem">Neu</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="main">
|
|
|
|
|
<div id="toolbar">
|
|
|
|
|
<b id="current">-</b>
|
|
|
|
|
<button id="btn-save" class="primary">Speichern</button>
|
|
|
|
|
<button id="btn-preview">Vorschau</button>
|
|
|
|
|
<button id="btn-export" class="primary">Nach output/ exportieren</button>
|
|
|
|
|
<button id="btn-delete" style="margin-left:auto">Seite löschen</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="msg"></div>
|
|
|
|
|
<textarea id="editor" spellcheck="false">---
|
|
|
|
|
titel: Neue Seite
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
Hier den Inhalt schreiben. **fett**, *kursiv*, [Link](seite.html), # Ueberschrift, - Liste.</textarea>
|
|
|
|
|
<div id="preview"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
let pages = [], current = null;
|
|
|
|
|
|
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
|
const msg = t => { $("msg").textContent = t; setTimeout(() => $("msg").textContent = "", 4000); };
|
|
|
|
|
async function api(path, opts) {
|
|
|
|
|
const r = await fetch(path, opts);
|
|
|
|
|
return r.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadPages() {
|
|
|
|
|
const d = await api("/api/pages");
|
|
|
|
|
pages = d.pages;
|
|
|
|
|
$("pages").innerHTML = "";
|
|
|
|
|
for (const p of pages) {
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.textContent = p + (p === d.start ? " (Start)" : "");
|
|
|
|
|
a.onclick = () => openPage(p);
|
|
|
|
|
if (p === current) a.className = "active";
|
|
|
|
|
$("pages").appendChild(a);
|
|
|
|
|
}
|
|
|
|
|
if (d.footer) {
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.textContent = "footer (Baustein)";
|
|
|
|
|
a.onclick = () => openPage("footer");
|
|
|
|
|
if (current === "footer") a.className = "active";
|
|
|
|
|
$("pages").appendChild(a);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openPage(slug) {
|
|
|
|
|
const d = await api("/api/page/" + encodeURIComponent(slug));
|
|
|
|
|
current = slug;
|
|
|
|
|
$("current").textContent = slug;
|
|
|
|
|
$("editor").value = d.raw;
|
|
|
|
|
$("editor").disabled = false;
|
|
|
|
|
loadPages();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function save() {
|
|
|
|
|
if (!current) return;
|
|
|
|
|
const d = await api("/api/page", {
|
|
|
|
|
method: "POST", headers: {"Content-Type": "application/json"},
|
|
|
|
|
body: JSON.stringify({slug: current, raw: $("editor").value})
|
|
|
|
|
});
|
|
|
|
|
msg(d.ok ? "Gespeichert." : ("Fehler: " + d.error));
|
|
|
|
|
if (d.ok) loadPages();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$("btn-save").onclick = save;
|
|
|
|
|
$("btn-delete").onclick = async () => {
|
|
|
|
|
if (!confirm("Seite '" + current + "' loeschen?")) return;
|
|
|
|
|
const d = await api("/api/delete", {
|
|
|
|
|
method: "POST", headers: {"Content-Type": "application/json"},
|
|
|
|
|
body: JSON.stringify({slug: current})
|
|
|
|
|
});
|
|
|
|
|
msg(d.ok ? "Geloescht." : ("Fehler: " + d.error));
|
|
|
|
|
current = null; $("current").textContent = "-"; $("editor").disabled = true;
|
|
|
|
|
loadPages();
|
|
|
|
|
};
|
|
|
|
|
$("btn-new").onclick = async () => {
|
|
|
|
|
const slug = $("newslug").value.trim().toLowerCase();
|
|
|
|
|
if (!slug) return msg("Bitte Namen eingeben.");
|
|
|
|
|
const d = await api("/api/page", {
|
|
|
|
|
method: "POST", headers: {"Content-Type": "application/json"},
|
|
|
|
|
body: JSON.stringify({slug, raw: "---\\ntitel: " + slug + "\\n---\\n\\nNeue Seite.\\n"})
|
|
|
|
|
});
|
|
|
|
|
if (d.ok) { $("newslug").value = ""; openPage(slug); }
|
|
|
|
|
else msg("Fehler: " + d.error);
|
|
|
|
|
};
|
|
|
|
|
$("btn-preview").onclick = async () => {
|
|
|
|
|
const ed = $("editor"), pv = $("preview");
|
|
|
|
|
if (pv.style.display === "block") {
|
|
|
|
|
pv.style.display = "none"; ed.style.display = "block"; return;
|
|
|
|
|
}
|
|
|
|
|
const d = await api("/api/preview", {
|
|
|
|
|
method: "POST", headers: {"Content-Type": "application/json"},
|
|
|
|
|
body: JSON.stringify({raw: ed.value})
|
|
|
|
|
});
|
|
|
|
|
pv.innerHTML = d.html; pv.style.display = "block"; ed.style.display = "none";
|
|
|
|
|
};
|
|
|
|
|
$("btn-export").onclick = async () => {
|
|
|
|
|
const d = await api("/api/export");
|
|
|
|
|
msg(d.exported ? "Exportiert: " + d.exported.join(", ") : ("Fehler: " + d.error));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadPages();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|