layout
This commit is contained in:
1 parent
7ed84a1368
commit
45d3b0ec0d
11 files changed
+1106
-12
No files matched your search
@@ -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("&", "&").replace("<", "<").replace(">", ">"))
|
||||
|
||||
|
||||
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öschen</button>
|
||||
</div>
|
||||
<div id="msg"></div>
|
||||
<div id="imgdrop" style="display:none">
|
||||
<div class="imgdrop-title">Bild aus images/ einfü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>
|
||||
|
||||
Reference in new issue
Block a user