Manchmal ist eine Gemini-Kapsel doch sehr unsichtbar und man wünscht sich den Text auch auf dem Pelican-Blog, ohne dass man viel ändern muss. Mit Pelican ist man es ja schon gewöhnt eine einfache Auszeichnungssprache zu benutzen (markdown oder rst, wobei ich wegen Sphinx lieber rst benutze), so dass der Gedanke gemini als Input zu benutzen nicht sehr fern liegt. Natürlich muss man wegen der Struktur von Gemini-Text mit Einschränkungen leben – Links können nicht inline sein. Die Link-Section unten sieht in HTML ungewohnt aus.
Wie in der Dokumentation beschrieben wird ist das für das Einlesen von Text ein Reader zuständig. Auch ein Beispiel findet dort. Ein Reader wird einem Dateityp zugeordnet (also einem oder mehreren Suffixen). Die Rückgabe eines Readers ist ein Tupel aus einem HTML-String und Metadaten. Da gemini-Text zeilenorientiert ist lässt sich die Umwandlung mit einem regulären Ausdruck pro Zeile und einem Mrker für vorformatierte Blöcke erledigen.
Bei den Metadaten war eine Entscheidung nötig: Sollen sie am Anfang der gmi-Datei stehen oder kommen sie in eine eigene Datei? Ich habe mich für letzteres entschieden, denn damit kann ich einen Beitrag nur für die Gemini-Kapsel schreiben ohne die Gemini-Datei mit überflüssigen Daten zu belasten. Im Verzeichnis des Pelican-Blogs kann ich einfach eine Datei mit den Metadaten und einen Link zu Gemini-Datei anlegen.
Etwas fehleranfällig war beim Erzeugen des HTML waren die reservierten Zeichen < und >, die ersetzt werden mussten, wenn sie nicht gerade Bedeutung im Gemini-Text haben (bei Link- und Blockquote-Zeilen).
Alles in allem hat der Reader Stand heute 98 Zeilen, ist also nicht wirklich komplex. Das sehe ich als Zeichen dass das Versprechen „Gemtext is carefully designed to be very, very easy to parse and render.“ eingehalten wird; und auch Pelican macht da einen guten Job.
Links
Pelican internals - Reader, Writer & Co. Mit Beispiel.
https://docs.getpelican.com/en/latest/internals.html
Als Inspiration genutzt, aber letztlich komplett anders gemacht.
https://github.com/khoulihan/pelican-gemini
Inspiration für das Konvertieren. Viel Nacharbeit nötig.
https://github.com/huntingb/gemtext-html-converter
Code
from pelican import signals
from pelican.readers import BaseReader
import logging
import re
class GeminiReader(BaseReader):
enabled = True
logger = logging.getLogger(__name__)
file_extensions = ["gmi", "gemini"]
def read(self, filename):
metadata = {}
content = ""
# Metadaten sind in Dateiname.pelican_meta
with open(filename + ".pelican_meta", mode="r") as f:
while current := f.readline():
current = current.strip()
split = current.split(": ", 1)
metadata[split[0].lower()] = split[1]
with open(filename, mode="r") as f:
# After the first blank line, there is the title
current = f.readline()
if match := re.match(r"^#\s*(.*)$", current):
# use if no title, otherwise ignore
if not metadata.get("title"):
self.logger.info(f"No title, using {current}")
metadata["title"] = match.groups()[0]
else:
content = current
# The rest is content.
content = content + f.read()
parsed = {}
for key, value in metadata.items():
parsed[key] = self.process_metadata(key, value)
return self._convert2html(content), parsed
def _convert2html(self, content):
result = ""
preformat = False
gemini_footer_delim = self.settings.get("GEMINI_FOOTER_DELIM")
for line in content.splitlines():
if line.startswith("```"):
preformat = not preformat
result += "<pre>\n" if preformat else "</pre>\n"
continue
# Meine Art von gemini-Footer – Alles danach ignorieren
if (
gemini_footer_delim
and line.startswith(gemini_footer_delim)
and not preformat
):
return result
result += self._parse_gmi_line(line, preformat)
return result
def _parse_gmi_line(self, gmi_line, preformat):
tags_dict = {
r"^#\s*([^#].*)": "h1",
r"^##\s*([^#].*)": "h2",
r"^###\s*([^#].*)": "h3",
r"^\* (.*)": "li",
r"^> (.*)": "blockquote",
r"^=>\s+(\S+)(\s+.*)?": "a",
}
# HTML special chars
gmi_line = gmi_line.replace("&", "&")
gmi_line = gmi_line.replace("<", "<")
# keep > for blockqoute or link
if not (gmi_line.startswith(">") or gmi_line.startswith("=>")):
gmi_line = gmi_line.replace(">", ">")
if preformat:
return gmi_line + "\n"
for pattern in tags_dict.keys():
if match := re.match(pattern, gmi_line):
tag = tags_dict[pattern]
groups = match.groups()
if tag == "a":
href = groups[0]
text = groups[1].strip() if len(groups) > 1 and groups[1] else href
# text might be empty after strip - not by spec, but in practice
text = href if not text else text
return f"<p><a href='{href}'>{text}</a></p>\n"
else:
text = groups[0].strip()
return f"<{tag}>{text}</{tag}>\n"
return f"<p>{gmi_line}</p>\n"
def add_reader(readers):
for ext in GeminiReader.file_extensions:
readers.reader_classes[ext] = GeminiReader
def register():
signals.readers_init.connect(add_reader)