This commit is contained in:
florian committed 2026-10-03 12:52:58 +02:00
1 parent 7ed84a1368
commit 45d3b0ec0d
11 files changed
+1106 -12

No files matched your search

+151 -10
View File
@@ -37,6 +37,16 @@ FOOTER_PAGE = "footer" # Baustein: wird unter den Inhalt jeder Seite gesetzt
# ---------------------------------------------------------------- markdown
def inline(s):
"""Inline-Markdown: bilder, code, fett, kursiv, Links."""
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
s = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", r'<img src="\2" alt="\1">', 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 md_to_html(text, headings=None):
"""Minimaler Markdown->HTML-Konverter:
Ueberschriften, **fett**, *kursiv*, `code`, [Links](url),
@@ -44,6 +54,8 @@ def md_to_html(text, headings=None):
Ueberschriften bekommen id-Anker; in headings (Liste) werden
optional (anker, text) aller Ueberschriften gesammelt.
"""
# vergessenes Trigger-Wort "image" direkt vor einem Bild nicht mit ausgeben
text = re.sub(r"\bimage\s*(?=!\[)", "", text, flags=re.IGNORECASE)
lines = text.replace("\r\n", "\n").split("\n")
out = []
i = 0
@@ -53,13 +65,6 @@ def md_to_html(text, headings=None):
used_ids = {}
sec_stack = [] # offene Sektionen (pro # mehr eine Ebene, eingerueckt)
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:
@@ -134,6 +139,24 @@ def md_to_html(text, headings=None):
i += 1
continue
# Tabellen: | zelle | zelle | mit --- Trennzeile
if stripped.startswith("|"):
close_list()
rows = []
j = i
while j < len(lines) and lines[j].strip().startswith("|"):
rows.append(lines[j].strip())
j += 1
tbl = _parse_table(rows)
if tbl:
out.append(tbl)
i = j
else:
# keine gueltige Tabelle -> als normaler Absatz
out.append("<p>%s</p>" % inline(_escape(stripped)))
i += 1
continue
m = re.match(r"^([-*+])\s+(.*)$", stripped)
if m:
if not in_list or list_tag != "ul":
@@ -167,7 +190,7 @@ def md_to_html(text, headings=None):
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,}$|<)",
and not re.match(r"^(#{1,6}\s|[-*+]\s|\d+\.\s|>|```|-{3,}$|<|\|)",
lines[i + 1].strip())):
i += 1
para.append(lines[i].strip())
@@ -185,6 +208,54 @@ def _escape(s):
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
def _parse_table(rows):
"""Baut aus |-getrennten Zeilen eine HTML-Tabelle.
Zeile 1: Kopfzeile, Zeile 2: Trenner (---, :---, ---:, :---:),
danach: Datenzeilen. Liefert None, wenn es keine gueltige Tabelle ist.
"""
def split_row(r):
if r.startswith("|"):
r = r[1:]
if r.endswith("|"):
r = r[:-1]
return [c.strip() for c in r.split("|")]
def is_sep(c):
return re.match(r"^:?-{2,}:?$", c) is not None
if len(rows) < 2 or not all(is_sep(c) and c for c in split_row(rows[1])):
return None
aligns = []
for c in split_row(rows[1]):
left, right = c.startswith(":"), c.endswith(":")
if left and right:
aligns.append("center")
elif right:
aligns.append("right")
else:
aligns.append("left")
def cell(tag, j, text):
style = ' style="text-align: %s"' % aligns[j] if j < len(aligns) else ""
return "<%s%s>%s</%s>" % (tag, style, inline(text), tag)
out = ["<table>", "<thead><tr>"]
for j, c in enumerate(split_row(rows[0])):
out.append(cell("th", j, _escape(c)))
out.append("</tr></thead>")
if len(rows) > 2:
out.append("<tbody>")
for r in rows[2:]:
out.append("<tr>")
for j, c in enumerate(split_row(r)):
out.append(cell("td", j, _escape(c)))
out.append("</tr>")
out.append("</tbody>")
out.append("</table>")
return "".join(out)
def _slugify(s):
"""Ueberschriftentext -> HTML-id (ankertauglich, klein, ohne Sonderzeichen)."""
s = s.lower()
@@ -388,6 +459,14 @@ class Handler(BaseHTTPRequestHandler):
self._json({"slug": slug, "raw": load_page(slug)})
else:
self._json({"error": "Seite nicht gefunden"}, 404)
elif path == "/api/images":
img_dir = os.path.join(ROOT, "images")
files = []
if os.path.isdir(img_dir):
for f in sorted(os.listdir(img_dir)):
if os.path.isfile(os.path.join(img_dir, f)) and not f.startswith("."):
files.append(f)
self._json({"images": files})
elif path == "/api/export":
self._json({"exported": export_site()})
else:
@@ -433,7 +512,11 @@ ADMIN_HTML = """<!DOCTYPE html>
#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; }
#main { flex: 1; display: flex; flex-direction: column; padding: 1rem; box-sizing: border-box; gap: .5rem; position: relative; }
#imgdrop { position: absolute; top: 3.2rem; left: 1rem; background: #fff; border: 1px solid #999; border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,.25); max-height: 220px; overflow: auto; z-index: 20; min-width: 240px; }
#imgdrop .imgdrop-title { font-size: .8rem; padding: .4rem .6rem; background: #eee; }
#imglist a { display: block; padding: .35rem .6rem; cursor: pointer; color: #000; }
#imglist a:hover { background: #dde8ff; }
#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; }
@@ -461,12 +544,17 @@ ADMIN_HTML = """<!DOCTYPE html>
<button id="btn-delete" style="margin-left:auto">Seite l&ouml;schen</button>
</div>
<div id="msg"></div>
<div id="imgdrop" style="display:none">
<div class="imgdrop-title">Bild aus images/ einf&uuml;gen:</div>
<div id="imglist"></div>
</div>
<textarea id="editor" spellcheck="false">---
titel: Neue Seite
beschreibung: Kurze Seitenbeschreibung fuer Suchmaschinen (SEO).
---
Hier den Inhalt schreiben. **fett**, *kursiv*, [Link](seite.html), # Ueberschrift, - Liste.</textarea>
Hier den Inhalt schreiben. **fett**, *kursiv*, [Link](seite.html), # Ueberschrift, - Liste.
Das Wort "image" tippen, um ein Bild aus dem images-Ordner einzufuegen.</textarea>
<div id="preview"></div>
</div>
<script>
@@ -557,6 +645,59 @@ $("btn-export").onclick = async () => {
msg(d.exported ? "Exportiert: " + d.exported.join(", ") : ("Fehler: " + d.error));
};
// ---- Bild einfuegen: "image" tippen -> Dropdown mit Dateien aus images/ ----
let dropGen = 0;
let lastCaret = 0;
function hideImageDrop() {
dropGen++;
$("imgdrop").style.display = "none";
}
async function showImageDrop(caret) {
const gen = ++dropGen;
lastCaret = caret;
const d = await api("/api/images");
if (gen !== dropGen) return; // inzwischen wieder geschlossen
const list = $("imglist");
list.innerHTML = "";
if (!d.images.length) {
const p = document.createElement("div");
p.style.padding = ".4rem .6rem";
p.textContent = "Keine Dateien in images/.";
list.appendChild(p);
}
for (const f of d.images) {
const a = document.createElement("a");
a.textContent = f;
a.onmousedown = e => { e.preventDefault(); insertImage(f); };
list.appendChild(a);
}
$("imgdrop").style.display = "block";
}
function insertImage(name) {
const ed = $("editor");
let pos = lastCaret;
let before = ed.value.slice(0, pos), after = ed.value.slice(pos);
const md = "![" + name.replace(/\.[^.]+$/, "") + "](images/" + name + ")";
// das getippte Wort "image" durch die Bild-Syntax ersetzen
if (before.slice(-5).toLowerCase() === "image") before = before.slice(0, -5);
ed.value = before + md + after;
ed.focus();
ed.selectionStart = ed.selectionEnd = (before + md).length;
hideImageDrop();
}
$("editor").addEventListener("input", () => {
const ed = $("editor");
const pos = ed.selectionStart;
const tail = ed.value.slice(Math.max(0, pos - 5), pos).toLowerCase();
if (tail === "image") showImageDrop(pos);
else hideImageDrop();
});
$("editor").addEventListener("keydown", e => { if (e.key === "Escape") hideImageDrop(); });
loadPages();
</script>
</body>