đŸ’Ÿ Archived View for data.konfusator.de â€ș 2023-05-31_pelican_reader.gmi captured on 2024-02-05 at 09:34:27. Gemini links have been rewritten to link to archived content

View Raw

More Information

âŹ…ïž Previous capture (2023-09-08)

-=-=-=-=-=-=-

Ein Pelican-Reader fĂŒr Gemini

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("&", "&amp;")
        gmi_line = gmi_line.replace("<", "&lt;")
        # keep > for blockqoute or link
        if not (gmi_line.startswith(">") or gmi_line.startswith("=>")):
            gmi_line = gmi_line.replace(">", "&gt;")
        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)

════════════════════════

2023-05-31T00:53+02:00

🏡