md-editor – Schlanker Markdown-Editor mit Live-Preview
Wie und warum ein schlanker Markdown-Editor mit Live-Preview entstanden ist.
Zusammenfassung
Der md-editor bietet eine schlanke und hochgradig anpassbare Lösung für die Markdown-Bearbeitung ohne komplexe Build-Prozesse. Durch die bewusste Entscheidung für eine Textarea-Basis ermöglicht das Werkzeug maximale Kontrolle über das Rendering und die Integration in eigene Systeme. Besondere Funktionen wie die Live-Vorschau und die korrekte Darstellung von Frontmatter unterstützen einen effizienten Schreibworkflow.
Diese Zusammenfassung wurde mit KI-Unterstützung erstellt.
Warum ein eigener Markdown-Editor?
Markdown-Editoren gibt es viele, und darunter sind sehr gute Projekte. Es gab keinen zwingenden Grund, noch einen zu bauen – und erst recht keinen Anspruch, es besser zu können.
Was mich am Markt nicht überzeugt hat, war nicht die Qualität dieser Werkzeuge, sondern der Fit. Eine fertige Lösung bringt ihren eigenen Update-Zyklus mit, ihr eigenes CSS und ihre eigene Vorstellung davon, was ein Markdown-Editor können oder wie er sich verhalten soll. Wenn ich die Vorschau anders rendern will, den Frontmatter-Block gesondert behandeln will oder später eine direkte Verbindung zum SSG-Buildprozess einbauen möchte, beginnt schnell der Punkt, an dem man sich sehr tief in fremden Code einarbeiten muss. Und gerade bei gewachsenen Projekten kostet dieses Verstehen und Anpassen am Ende oft mehr Zeit als eine bewusst schlanke Neuentwicklung für den eigenen Anwendungsfall.
Das war der eigentliche Auslöser: den Grundstein für eine Komponente zu legen, die ich vollständig kenne, vollständig kontrolliere und in jede Richtung weiterentwickeln kann. Nicht als Gegenentwurf zu bestehenden Projekten, sondern als bewusste Entscheidung für mehr Kontrolle im eigenen System.
Die Anforderungen als Klartext
Was der Editor können muss, war schnell aufgeschrieben:
- Toolbar mit den üblichen Markdown-Aktionen
- Live-Vorschau, schaltbar, nicht immer sichtbar
- Originalsprache und Übersetzung gegenübergestellt darstellbar
- Frontmatter in der Vorschau korrekt dargestellt – nicht als Markdown gerendert
- Vollbild-Modus für fokussiertes Schreiben
- Einbindung per
<script>und<link>– fertig, ohne Build-Schritt
Die Entscheidung: <textarea> statt contenteditable
Das erste architektonische Grundprinzip: Der Editor basiert auf einer normalen <textarea>, nicht auf einem contenteditable-Element.
Das ist eine bewusste Einschränkung. contenteditable erlaubt reichere Interaktionen – zum Beispiel klickbare Links, visuelle Formatierung direkt im Text oder sehr freie Cursor-Operationen. Gleichzeitig bringt dieser Ansatz in der Praxis aber ein deutlich komplexeres Verhalten bei Selektion, Eingaben, Paste und DOM-Manipulation mit sich als eine klassische <textarea>. Wer damit einmal intensiver gearbeitet hat, kennt dieses Thema.
Eine <textarea> verhält sich dagegen deutlich vorhersagbarer. Selektionen, Cursor-Positionen und Textmanipulation laufen über selectionStart, selectionEnd und value. Das ist testbar, nachvollziehbar und über die großen Browser hinweg wesentlich konsistenter.
Das ist der Kompromiss: kein Inline-Highlighting und keine Rich-Text-Illusion. Dafür lässt sich derselbe Editor-Code browserübergreifend in der Regel deutlich konsistenter betreiben.
IIFE-Wrapper und warum nicht ES-Module
An diesem Punkt ging es nicht mehr um Markdown, sondern um die Art der Einbindung im Browser. Die Frage war: Soll der Editor als modernes ES-Modul aufgebaut werden oder als klassische Datei, die sich direkt per <script> in eine Seite hängen lässt?
Ich habe mich hier bewusst für die zweite Variante entschieden. Der Editor sollte ohne Build-Schritt, ohne Bundler und ohne Modul-Setup sofort nutzbar sein. Genau dafür ist ein IIFE-Wrapper geeignet – also eine Funktion, die direkt bei ihrer Definition ausgeführt wird und dabei den eigenen Code in einen klaren Scope packt.
(function(global) {
"use strict";
// ... MdEditor class ...
global.MdEditor = MdEditor;
})(window);
Warum kein export default MdEditor? Weil export und import zur Modulsyntax gehören und im Browser nur im Modulkontext funktionieren. Wer eine Datei einfach per <script src="md-editor.js"></script> einbindet, lädt zunächst ein klassisches Script – kein ES-Modul. Das Ziel war hier aber ganz bewusst genau diese einfache Einbindung: ohne type="module", ohne Import-Pfade und ohne Bundler-Kontext. Das IIFE macht MdEditor deshalb global auf window verfügbar, ähnlich wie es viele klassische Browser-Bibliotheken tun.
Das schwierigste Stück: _toggleLinePrefix
Die meisten Toolbar-Aktionen sind vergleichsweise einfach: Text markieren, Wrapping-Zeichen einfügen, Selektion aktualisieren. Das ist schnell gebaut.
Der Toggle-Präfix ist dagegen nicht trivial. Was soll passieren, wenn jemand auf H2 klickt und die Zeile bereits mit ### beginnt? Oder wenn man auf Bullet List klickt und die Zeile aktuell ## Titel ist?
Das naive Verhalten wäre: einfach ## vorne dranhängen. Dann steht dort ## ### Titel. Technisch leicht, praktisch aber unbrauchbar.
Die saubere Lösung:
- Zeile ermitteln (Zeilenstart via
lastIndexOf("\n")rückwärts) - Prüfen, ob der gewünschte Präfix bereits vorhanden ist – wenn ja, entfernen
- Wenn nicht: bestehenden Präfix entfernen, dann den neuen setzen
Der dritte Schritt ist der entscheidende Teil. Ein replace(/^(#{1,6} |- |\d+\. )/, "") am Zeilenanfang entfernt in diesem Editor vorhandene Heading- und Listen-Präfixe. Danach wird nur noch der neue Präfix gesetzt. Ein Klick auf H2 bei einer H3-Zeile macht daraus also H2 – und nicht ## ### Zeile.
Gleichzeitig muss der Cursor-Offset sauber nachgeführt werden. Wenn ein Präfix kürzer oder länger wird, verschiebt sich die Cursor-Position entsprechend. Diesen cursorDelta korrekt zu berechnen und auf selectionStart sowie selectionEnd anzuwenden, war am Ende der Teil, der die meiste Sorgfalt gebraucht hat.
Frontmatter in der Vorschau
Ein Teil der Markdown-Dateien, für die der Editor gedacht ist, beginnt mit einem YAML-Frontmatter-Block:
---
title: "Mein Beitrag"
date: 2026-04-06
---
# Der eigentliche Text
Dieser Block enthält Metadaten wie Titel oder Datum und gehört in vielen statischen Webseiten- oder Blog-Setups ganz normal zur Datei dazu. Für die Vorschau ist das aber ein Sonderfall: Der Block soll sichtbar bleiben, aber nicht wie normaler Markdown-Inhalt gerendert werden.
In marked.js gibt es dafür keine eingebaute Frontmatter-Unterstützung. Ohne Vorverarbeitung würde der Block deshalb nicht als Metadatenbereich behandelt, sondern als normaler Markdown-Inhalt interpretiert. Für eine Vorschau, die sich an der tatsächlichen Dateistruktur orientieren soll, passt das nicht.
Die Lösung ist deshalb vorgeschaltet: Ein Regex FRONTMATTER_RE prüft den Anfang des Inhalts. Wenn dort ein Frontmatter-Block erkannt wird, wird er aus dem Text extrahiert und separat als gestalteter <div class="mde-fm-block"> dargestellt. Der restliche Inhalt läuft danach durch marked.parse(). In der Vorschau ergibt das ein klareres Bild: oben die Metadaten, darunter der eigentliche Markdown-Inhalt.
Vollbild und der globale Instanz-Tracker
Der Vollbild-Modus selbst ist technisch nicht kompliziert: Der Editor legt sich per position: fixed über das gesamte Viewport.
Interessanter wird es, sobald mehrere MdEditor-Instanzen auf derselben Seite laufen. Wenn Instanz A in den Vollbild-Modus wechselt, sollen Instanzen B und C natürlich nicht ebenfalls im Vollbild bleiben. Das wäre visuell und logisch unsauber. Die Lösung ist ein modulweites Set _instances, das alle aktiven Instanzen verwaltet. Wenn eine Instanz _toggleFullscreen() aufruft, iteriert sie über _instances und beendet den Vollbild-Modus bei allen anderen.
Dasselbe Set ist auch die Grundlage für den globalen Escape-Handler: Ein einzelner Event-Listener auf document.keydown – nicht einer pro Instanz – iteriert über alle Instanzen und ruft _exitFullscreen() auf, sobald Escape erkannt wird.
Die destroy()-Methode räumt am Ende auf: Instanz aus _instances entfernen, DOM-Elemente leeren, Event-Listener entfernen.
Das CSS: Custom Properties bis zum Ende durchgezogen
Das Theming-Konzept war von Anfang an klar: keine hartkodierten Hex-Werte im laufenden Design, keine Sass-Variablen als Voraussetzung, sondern CSS Custom Properties. Alle Farb-Tokens sind auf .mde definiert:
.mde {
--mde-bg: #1a1a2e;
--mde-accent: #7c6af7;
/* … */
}
Drei Theme-Modi:
- Dark (Standard):
.mde– immer dunkel, egal was das OS sagt - Light (explizit):
.mde.mde-light– immer hell - Auto:
.mde.mde-auto+@media (prefers-color-scheme: light)– folgt dem OS
Das bedeutet: Wer den Editor in eine Dark-Mode-App einbaut, muss nichts weiter tun. Wer ihn in ein helles Interface einbindet, ergänzt .mde-light an der Container-Klasse. Und wer Farben anpassen will, überschreibt die Custom Properties im eigenen CSS – ohne den Editor selbst anzufassen.