Markdown-Spezifikation
Stand: lebendes Dokument, geschrieben zusammen mit dem Serialisierer.
Markdown ist der Austauschvertrag: Jede Seite wird zu kanonischem Markdown serialisiert und verlustfrei zurückgelesen. Abgelegt wird weiterhin als Blöcke plus CRDT; diese Spezifikation legt den kanonischen Dialekt fest — die eine Ausgabeform, die der Serialisierer schreibt und auf die der Parser jede Eingabe hin normalisiert.
Zwei Garantien, beide von einem Eigenschaftstest gehalten, der bei jedem Build läuft:
parse(serialize(doc)) ≡ docfür jedes kanonische Dokument.serialize(parse(md)) ≡ normalize(md)— kanonisches Markdown ist ein Fixpunkt.
Grunddialekt: CommonMark + GFM (Tabellen, Aufgabenlisten, Durchstreichung,
Autolink-Literale) plus YAML-Frontmatter. Die Obsidian-kompatible
Verweissyntax ([[wikilink]], ![[transclusion]], ^anchors) und die
basalt:-Fences für Datenbanken und Ansichten sind zurückgestellt — die
Nahtstellen sind da, geparst wird noch nichts davon.
Eine Anmerkung zu den Namen weiter unten: Die Fences heißen basalt-embed und
basalt-view. Sie gehören der Plattform, nicht einem einzelnen Produkt. Worauf
es ankommt: Genau so schreibt sie der Serialisierer und genau so liest sie der
Parser — und genau so stehen sie hier, in der Schreibweise, die Ihre Dateien
enthalten werden.
Aufbau eines Dokuments
Ein Dokument besteht aus optionalem YAML-Frontmatter, gefolgt von einer Folge von Körperblöcken, getrennt durch je eine Leerzeile. Der Serialisierer beendet ein Dokument immer mit einem Zeilenumbruch (ein leeres Dokument serialisiert zur leeren Zeichenkette).
Frontmatter
---
title: Meine Seite
key: value
---
titlesteht zuerst; alle übrigen Schlüssel folgen sortiert.- Die übrigen Schlüssel sind die Workspace-Feldwerte der Seite, benannt nach dem Feldschlüssel. Sie werden undurchsichtig durchgereicht (beliebiger YAML-Skalar, -Liste oder -Abbildung).
- Mehrzeilige Zeichenketten stehen immer in doppelten Anführungszeichen (keine
|-Blockskalare — die Fence-Grenze schluckte sonst einen abschließenden Zeilenumbruch). Keine Zeilenfaltung, der Bestimmtheit wegen. - Frontmatter entfällt vollständig, wenn es weder
titlenoch Eigenschaften gibt.
Hinweis: Die Schnittstelle
GET/PUT …?format=markdownschreibt und liest die Feldwerte der Seite als Frontmatter, benannt nach dem Feldschlüssel. Dertitlebleibt draußen: Er gehört zur Strukturebene, die Route gibt ihn nicht aus und liest ihn nicht zurück. Ändern Sie ihn mitPATCH …/pages/{pageId}. Relationen werden beim Lesen ausgegeben, beim Schreiben aber ignoriert; maßgeblich ist die Relationstabelle. Formelfelder werden berechnet und erscheinen in keiner Richtung.
Blockzuordnung
Die kanonische Form je Körperblock. Aller Rich Text folgt den Inline-Regeln weiter unten.
| Block | Kanonisches Markdown |
|---|---|
paragraph |
schlichte Zeile aus Inline-Inhalt |
heading |
#, ##, ### (Ebenen werden auf 1–3 geklemmt) |
list_item (Aufzählung) |
- Punkt |
list_item (nummeriert) |
1. 2. … (fortlaufend ab 1 neu nummeriert) |
todo |
- [ ] Punkt / - [x] Punkt |
quote |
> … Blockzitat |
toggle |
> [!toggle] Zusammenfassung (siehe unten) |
callout |
> [!note] … / > [!note|<icon>] … (siehe unten) |
code |
```-Fence mit optionalem Sprachkürzel |
divider |
*** Trennlinie |
image |
 auf eigener Zeile, plus {width=50% align=center}, wenn ein Layout gewählt wurde |
table |
GFM-Pipe-Tabelle (siehe unten) |
Überschriften
# H1, ## H2, ### H3. Eingehende Ebenen 4–6 werden auf ### geklemmt;
Setext-Überschriften (=== / --- als Unterstreichung) werden zu ATX
normalisiert. Eine leere Überschrift sind die nackten Rauten (#).
Listen (Aufzählung, Nummerierung, Aufgaben)
-
Aufzählungen verwenden immer
-. Nummerierte Listen verwenden immer1.2.…, ab 1 neu durchgezählt, unabhängig vom Startwert der Eingabe und davon, ob dort)oder.stand. -
Listen sind eng (keine Leerzeilen zwischen Geschwisterpunkten).
-
Aufgabenpunkte sind GFM-Kästchen:
- [ ](offen) /- [x](erledigt). Ein eingehendes[X]wird zu[x]. -
Verschachtelt wird über Einrückung: Aufzählungs- und Nummerierungsinhalte rücken zwei Leerzeichen ein (hinter
-bzw. das Kästchen), passend zur Markerbreite; ein nummerierter Punkt rückt drei ein (hinter1.). Die Kinder eines Listenpunkts sind seine verschachtelten Blöcke. -
Aufeinanderfolgende Listenpunkte gleicher Art (Aufzählung gegen Nummerierung, Aufgabe gegen Nicht-Aufgabe) bilden eine Markdown-Liste; ein Artwechsel beginnt eine neue.
-
Ein Kind ist jeder Block, nicht nur eine verschachtelte Liste — ein Bild, ein Codeblock, ein zweiter Absatz. Es gehört zum Punkt, wenn es auf Markerbreite eingerückt ist, und zum Elternteil der Liste, wenn nicht. Das ist CommonMarks eigene Regel:
- Schritt eins Ein Punkt ohne eigenen Text erscheint als bloße Markerzeile mit darunter eingerücktem Kind (
-, dann zwei Leerzeichen) — ein Listenpunkt darf mit höchstens einer Leerzeile beginnen, und die Markerzeile ist bereits diese Zeile.
Zitat
> Ein zitierter Absatz.
>
> - verschachtelter Inhalt
Der eigene richText des Zitats ist seine führende Zeile; weitere Blöcke sind
seine children. Ein Zitat ohne eigenen Text beginnt nie mit einem
Absatz-Kind — der Absatz würde sonst wieder als eigener Text des Zitats
eingesogen.
Klappelement (Toggle) — in Obsidian-Callout-Form
> [!toggle] Zusammenfassung
> [!toggle] Zusammenfassung
>
> Erster Block im Körper.
>
> - Liste im Körper
- Die erste Zeile eines Blockzitats, die
[!toggle]lautet (wahlweise mit Faltungshinweis+/-, der nicht gespeichert wird), ist ein Klappelement — Callout-Semantik nach Obsidian. Da CommonMark\[und[gleich auflöst, ist das eindeutig. - Die Zusammenfassung reitet auf der Markerzeile und wird zum
richTextdes Klappelements; die übrigen Blöcke werden seinechildren. - Ein Klappelement ohne Zusammenfassung ist
> [!toggle]. - Ein Blockzitat, dessen erste Zeile mit
[!toggle]…beginnt, kann deshalb nie ein schlichtes Zitat sein. Seit es den Callout-Block gibt, gilt das für jeden[!…]-Marker: Ein Blockzitat, das mit einem beginnt, ist ein Klappelement oder eine Hervorhebung, nie ein Zitat.
Hervorhebung (Callout) — in Obsidian-Callout-Form
> [!note] Eine schlichte Hervorhebung
> [!note|💡] Mit gewähltem Symbol
>
> Erster Block im Körper.
Vom Aufbau her mit dem Klappelement oben identisch — ein Blockzitat, dessen erste Zeile einen Marker trägt —, weil es dieselbe Form ist. Dem Vorbild des Klappelements zu folgen erhält eine Konvention statt zweier. Die Unterschiede:
- Das Typ-Token heißt
note, nichttoggle.[!toggle]ist reserviert; jedes andere Typ-Token in der ersten Zeile eines Blockzitats liest sich als Hervorhebung. - Ein selbst gewähltes Symbol reitet hinter einem
|auf dem Marker:[!note|🎉]. Obsidians Syntax hat keinen Platz für ein Symbol, und dassnotedas führende Typ-Token bleibt, sorgt dafür, dass der Block in jedem Obsidian-kompatiblen Werkzeug weiterhin als Hervorhebung erscheint. - Das Symbol wird prozentkodiert für
% ] | [ * _ \~ < > &,:` und Leerraum — die Zeichen, die den Marker beenden, den zugrundeliegenden Textknoten zerteilen oder von GFM in einen Autolink verwandelt würden. Emojis gehen unangetastet durch, der häufige Fall bleibt also lesbar. (Backslashes helfen hier nicht: remark dekodiert sie, bevor der Parser den Text sieht — genau wie bei Wikilink-Beschriftungen.) - Die Kopfzeile reitet auf der Markerzeile und wird zum
richTextder Hervorhebung; die übrigen Blöcke werden ihrechildren. Eine leere Kopfzeile ist> [!note]. - Callout-Typen aus anderen Werkzeugen behalten ihre Bedeutung:
[!warning],[!tip],[!info],[!important],[!success],[!danger],[!question]und ein paar mehr werden als Hervorhebung mit dem passenden Symbol gelesen und als[!note|⚠️]und so weiter zurückgeschrieben. Ein unbekannter Typ fällt auf die neutrale, symbollose Form zurück.noteselbst steht bewusst nicht in dieser Tabelle — es ist die kanonische symbollose Schreibweise und muss eine bleiben.
Tabelle — GFM-Pipe-Tabelle
| Region | Verantwortung | Budget |
| :--- | --- | ---: |
| Nord | Ada | 1200 |
| Süd | *Grace* | 900 |
Das Blockmodell umfasst genau das, was eine Pipe-Tabelle ausdrücken kann, und nichts darüber hinaus: eine Kopfzeile (die zugleich die Ausrichtung je Spalte trägt), rechteckige Körperzeilen und Zellen, die nur Inline-Inhalt tragen. Keine verbundenen Zellen, kein Blockinhalt in einer Zelle, keine Spaltenbreiten — nichts davon hat eine Pipe-Tabellen-Form, also ist nichts davon darstellbar.
- Kanonische Form: jede Zelle mit je einem Leerzeichen gepolstert (
| a |); Trennzeile---,:---,---:,:---:; mindestens eine Spalte. - Ausgefranste Eingaben konvergieren: Eine zu kurze Zeile wird mit leeren Zellen aufgefüllt, eine zu lange auf die Breite der Kopfzeile gekürzt — genau das tut GFM beim Lesen, die Normalisierung ist also ein einziger Schritt und idempotent.
- Ein
|in einer Zelle wird überall in der Zelle maskiert, auch innerhalb einer Code-Spanne und innerhalb eines Linkziels, so will es GFM. GFM zählt eine Pipe als maskiert, wenn die Backslash-Folge davor ungerade ist; der Maskierer setzt deshalb bei einer geraden Folge einen Backslash und bei einer ungeraden zwei. - Der eine verlustbehaftete Fall des Dialekts: eine Pipe in einer Code-Spanne, der eine ungerade Anzahl Backslashes vorangeht. Backslashes in einer Code-Spanne sind wörtlich (sie lassen sich selbst nicht maskieren), und GFMs Zeilenteiler verbraucht genau einen — nach einem Rundlauf ist die Backslash-Folge vor einer Pipe also immer gerade. Eine solche Zelle gewinnt einen Backslash hinzu, statt die Zeile in eine zusätzliche Spalte zu zerreißen, und ist ab dem zweiten Schreiben exakt (das Ergebnis hat dann eine gerade Folge).
- Eine Tabelle hat keine
children: Ihre Zellen sind Inhalt, undflattenBlocksgibt für eine ganze Tabelle genau eine Projektionszeile aus.
Code
```lang
Codetext
```
- Das Info-Kürzel ist die Sprache (erstes durch Leerraum begrenztes Token, Backticks entfernt); ohne Sprache entfällt es.
- Die Fence-Länge wächst über die längste enthaltene Backtick-Folge hinaus, damit sich jeder Inhalt sicher einfassen lässt. Eingerückte Codeblöcke werden zu Fences normalisiert.
- Die Syntaxfärbung beim Betrachten ist schemakompatibel und berührt diese Serialisierung nicht.
- Ob lange Zeilen umbrechen, hat hier bewusst keinen Träger. Das ist eine Ansichtsvorliebe des Lesers — eine Geräteeinstellung plus eine Übersteuerung je Block und Sitzung, die der Editor für Sie behält —, keine Eigenschaft des Dokuments. Das Info-Kürzel bleibt deshalb die Sprache und sonst nichts. Erfinden Sie dafür kein Flag: Es beschriebe die Spaltenbreite eines Lesers allen anderen, und es zu schreiben machte aus dem Lesen eine Änderung.
Link-Einbettungen — Lesezeichenkarte und iframe (basalt-embed)
Eine eingefügte URL lässt sich auf vier Arten setzen. Zwei davon sind gewöhnliches Markdown und brauchen keine Erweiterung:
<https://example.com/post> schlichter Link
[Wie die Analytical Engine arbeitete](https://…) benannter Link
Die anderen beiden — eine Lesezeichenkarte und eine iframe-Einbettung —
haben keine Markdown-Form. Sie sind deshalb ein Fence-Block mit einem
basalt-Info-Kürzel, derselben Konvention, die auch die eingebettete
Datenbankansicht verwendet (basalt-view):
```basalt-embed
kind: card
url: https://example.com/post
title: Wie die Analytical Engine arbeitete
description: Ein kurzer Bericht über Mühle und Speicher.
site: example.com
image: https://example.com/og.png
icon: https://example.com/favicon.ico
```
- Grammatik des Körpers: ein
key: valueje Zeile, Reihenfolge egal, Werte einzeilig.urlist Pflicht und musshttp(s)sein; das gilt beim Schreiben wie beim Lesen auch fürimageundicon— daraus werden<a href>,<img src>und<iframe src>, und dieser Block lässt sich über die Markdown-PUT-Oberfläche von Hand schreiben. kindistcardoderiframe. Ein unbekanntes oder fehlendeskindliest sich alscard, die Form, die dem Browser des Lesers am wenigsten abverlangt.- Unbekannte Schlüssel werden übergangen, ein später ergänztes Feld bringt einen älteren Client also nicht dazu, den Block wegzuwerfen.
- Ein Fence ohne brauchbare
urlbleibt ein gewöhnlicher Codeblock. Nichts verschwindet stillschweigend — ein von Hand geschriebenesbasalt-embed, das keine echte Einbettung ist, ist sichtbarer Text. - Auf der Ablageseite ist das gar kein neuer Blocktyp: Es ist ein
code-Block, dessen Sprachebasalt-embedheißt. Er läuft also durch die bestehende Kette Block ⇄ PM ⇄ Markdown, und die Rundlauf-Tests decken ihn bereits ab.
Die Metadaten sind eine Momentaufnahme, kein lebender Spiegel. title,
description, site, image und icon werden beim Einfügen geschrieben und
beim Darstellen nicht erneuert. Genau das macht die .md-Datei in einem anderen
Werkzeug selbsterklärend (der Fence zeigt die URL und worum es ging), und
genau das verhindert, dass das Anzeigen einer Seite ausgehende Anfragen
auslöst: Die Metadaten einer URL werden einmal geholt, beim Einfügen der Karte
— und nur, wenn Linkvorschauen für die Instanz überhaupt eingeschaltet sind.
Der Preis ist Veralterung: Eine umbenannte Seite behält den alten Titel, bis
jemand die Karte neu einfügt. Für ein Lesezeichen ist das der richtige Tausch.
In einem anderen Markdown-Werkzeug erscheint der Block als Codeblock statt als Karte. Das ist die ehrliche Bruchstelle der Fence-Konvention: URL, Titel und Beschreibung stehen sämtlich als lesbarer Klartext da, durchsuchbar und verlustfrei reimportierbar. Verloren geht dem Leser das Bild, nie der Inhalt.
Trennlinie
*** (nie ---, das mit dem Frontmatter-Fence am Dokumentanfang kollidieren
würde). Eingehendes --- / ___ / - - - wird zu ***.
Bild (strukturell)
Ein allein stehendes Bild auf eigener Zeile () ist ein
struktureller image-Block. Ein Bild inmitten anderen Inline-Inhalts ist
kein Block — es fällt auf einen Link zurück (einen Inline-Bildknoten gibt es
derzeit nicht).
- Externe URLs werden unverändert serialisiert:
. - Bilder aus Anhängen verwenden
. altwird als vollständiger Inline-Text maskiert, damit markup-artiger Alternativtext (*a*,[x]) als wörtliche Zeichenkette durch den Rundlauf kommt.
Breite und Ausrichtung. Eine gewählte Breite und/oder Ausrichtung reiten am Bild als Attributblock im Pandoc-Stil, ohne Leerzeichen davor:
{width=50% align=center}
{align=right}
widthist ein ganzzahliger Prozentsatz der Inhaltsspalte, 10–100 — nie Pixel. Dasselbe Dokument wird auf einem 390 Pixel breiten Telefon und auf einem 1600 Pixel breiten Bildschirm gelesen, und ein Anteil an der Spalte ist die einzige Einheit, die auf beiden dasselbe bedeutet.width=100%ist nicht dasselbe wie keine Breite: Es dehnt ein kleines Bild auf die Spaltenbreite, was die Abwesenheit nie tut.alignistleft|center|right. Fehlt es, gilt die Voreinstellung des Lesers (bündig mit dem Text), und das ist nicht derselbe gespeicherte Wert wieleft.- Feste Schreibreihenfolge,
widthvoralign, ein Leerzeichen dazwischen. Ein Bild ohne beide Attribute wird genau so geschrieben wie eh und je — ohne geschweifte Klammern —, jede vor dieser Form erzeugte.mdbleibt also kanonisch. - Unbekannte Schlüssel innerhalb des Blocks werden übergangen (dieselbe
Regel wie beim
basalt-embed-Fence oben). Ein Attribut, das eine spätere Fassung neben einwidthoderalignschreibt, macht aus einem funktionierenden Bild hier also keinen Absatz. Der Block muss allerdings mindestens einen Schlüssel nennen, den diese Fassung kennt:{foo=bar}allein ist kein Layoutblock und bleibt wörtlicher Text — die Alternative wäre, getippte Wörter zu löschen. Und ein unbrauchbarer Wert bei einem Schlüssel, der bekannt ist ({width=300px},{align=justify}— auch wenn er neben einem brauchbaren steht, wie in{width=50% align=justify}), lässt den ganzen Block wörtlich stehen, statt stillschweigend die Hälfte zu verwerfen. - Gelesen wird der Block nur, wenn er der gesamte Text hinter dem Bild ist.
Ein Leerzeichen davor (
 {siehe Hinweis}) ist Prosa, und dieser Absatz fällt zurück wie jeder andere Absatz mit einem Bild darin.
In einem fremden Markdown-Werkzeug erscheint das Bild trotzdem — das
 bleibt unangetastet — und der Klammerblock steht schlicht als
wörtlicher Text daneben. Das ist der umgekehrte Tausch zum basalt-embed-Fence
oben, und zwar mit Absicht: Bei einer Lesezeichenkarte ist das Bild Beiwerk, bei
einem Bild ist das Bild der Inhalt — also das eine, was nicht verloren gehen
darf.
Inline (Rich Text)
Auszeichnungen: fett (**), kursiv (*), durchgestrichen (~~),
Hervorhebung (<mark>…</mark>), Textfarbe (<span data-color="…">…</span>),
Code (Backticks) und Links ([Text](href)). Das ist der gesamte Satz;
unbekannte Auszeichnungen (etwa Unterstreichung) fallen beim Parsen weg. Harte
Zeilenumbrüche werden zu einem Leerzeichen, weiche kollabieren zu einem.
Kanonischer Umgang mit Auszeichnungen:
- Schachtelungsreihenfolge, von außen nach innen:
link,bold,italic,strike,highlight,color,code.codeist ein Blatt und umschließt nie andere Auszeichnungen. - Benachbarte Spannen mit gleichem Auszeichnungssatz werden verschmolzen, leere verworfen. Zwei Auszeichnungen sind nur dann dieselbe, wenn auch ihr WERT übereinstimmt — das Ziel eines Links, der Name einer Farbe. Ein roter Lauf neben einem blauen bleibt also zwei Läufe.
- An jeder Position öffnet die Auszeichnung, die den längsten folgenden Lauf abdeckt, am weitesten außen (bei Gleichstand entscheidet die Reihenfolge oben), damit gemischte Läufe für CommonMark parsbar bleiben.
link,highlightundcolorsind klammernde Auszeichnungen: Sie öffnen außen, und ein Betonungslauf endet nie auf einer Spanne, die eine von ihnen trägt (CommonMarks Flanking-Regeln um[/)und</>).
Hervorhebung
CommonMark hat keine Syntax für Hervorhebungen, die kanonische Form ist deshalb das HTML-Element:
schlicht <mark>hervorgehoben</mark> wieder schlicht
==text== wurde bewusst verworfen: Es bräuchte eine micromark-Erweiterung und
ein Maskierungsschema, das nicht funktionieren kann — remark dekodiert \= zu
=, bevor der Parser den Text sieht. Maskiertes und unmaskiertes == wären
also nicht zu unterscheiden, und jeder Benutzertext mit == bräche den
Rundlauf. < und > werden in jedem Textlauf maskiert; ein wörtlich getipptes
<mark> serialisiert deshalb als \<mark\> und wird als Text zurückgelesen.
Das Element ist damit eindeutig — und jeder Markdown-Renderer stellt es ohnehin
schon dar.
Textfarbe
Für Farbe hat CommonMark ebenso wenig eine Syntax, die kanonische Form ist deshalb — wie bei der Hervorhebung — ein HTML-Element:
schlicht <span data-color="blue">blau</span> wieder schlicht
Der Wert ist ein Palettenname, nie CSS. Die Palette ist der geschlossene
Satz von neun Farben, den sie sich mit der Symbolfärbung teilt (gray, brown,
orange, yellow, green, blue, purple, pink, red). Ein Name
außerhalb davon ist keine Farbe: Die Spanne fällt auf wörtlichen Text zurück wie
jedes andere rohe Inline-HTML — über diesen Vertrag lässt sich also kein
beliebiges CSS in eine Seite schmuggeln. Beim Parsen wird data-color in
doppelten, einfachen oder gar keinen Anführungszeichen angenommen.
Warum ein Name und kein Hex-Wert: Jeder Palettenname löst sich in ein Paar Theme-Token auf (hell und dunkel), farbiger Text bleibt also lesbar, wenn der Leser das Farbschema wechselt — was eine handverlesene Farbe nicht versprechen kann. Außerdem hält es die serialisierte Form kurz und überprüfbar.
Schachtelung ist erlaubt und läuft so durch, wie sie geschrieben steht
(<span data-color="red">a<span data-color="blue">b</span></span>); der Editor
erzeugt sie nie, weil eine ProseMirror-Auszeichnung ihren eigenen Typ
ausschließt.
Maskierung (Bestimmtheit)
- Jeder Textlauf maskiert
\ ` * _ [ ] < > ~ & # ! @mit Backslash und entschärft Autolink-Literale (www.→www\.,http(s)://→http\://), damit schlichter Text beim erneuten Parsen nie zum Link wird. - Aufzählungsmarker am Zeilenanfang (
-,+) und Nummerierungsmarker (1.,1)) werden in Absatz- und Überschriftentext maskiert. - Code-Spannen polstern und bemessen ihre Fences nach CommonMarks Abschneideregeln.
- Link- und Bildziele kodieren
&neu und verwenden die<…>-Form, wenn sie Leerzeichen, runde oder eckige Klammern enthalten.
Rückfall (nicht-kanonische Eingaben)
Konstrukte außerhalb des Blocksatzes werden bestimmt normalisiert (idempotent
unter normalize):
- Überschriftenebenen 4–6 →
###; Setext → ATX. - Rohes HTML (Block und inline) → wörtlicher Text (beim erneuten Serialisieren maskiert).
- Referenzdefinitionen für Links und Bilder sowie Fußnoten → verworfen, sichtbarer Text bleibt.
- Inline-Bilder → Link (oder Alternativtext).
Schnittstelle
GET /api/v1/workspaces/:workspaceId/pages/:pageId?format=markdown→ Körper alstext/markdown(kanonische Serialisierung der aktuellen Körperblöcke, aus dem lebenden Yjs-Dokument gelesen) plus ein starkesETag.PUT …/pages/:pageId?format=markdownmit einemtext/markdown-Körper → wird zu Körperblöcken geparst und über den Dokument-Mutator in einer einzigen Yjs-Transaktion angewandt, nie als direkter Tabellenschreibvorgang.If-Match: <etag>erzwingt optimistische Nebenläufigkeit; eine Abweichung ist412.- Fehlendes
If-Matchheißt unbedingtes Schreiben;If-Match: *passt auf jede vorhandene Repräsentation. - Die Antwort gibt das gespeicherte kanonische Markdown und das neue
ETagzurück.
Das ETag ist ein starker Validator, abgeleitet aus den Bytes des kanonischen
Markdown (SHA-256). Gleicher Inhalt ergibt gleiches ETag; es bleibt also über
eine Verdichtung des Dokuments hinweg stabil und spiegelt genau die
Repräsentation, die die Schnittstelle ausliefert. Das ist die ehrliche Grenze:
Ein Markdown-PUT ist unter If-Match „der letzte Schreiber je Block", keine
zeichengenaue CRDT-Verschmelzung (Assistenten schreiben transaktional, Menschen
bearbeiten live im kollaborativen Editor).
Die Blockidentität (attrs.id) bleibt über ein Ersetzen hinweg erhalten, weil
der Mutator klont. Projektion und Verweise bleiben also stabil, obwohl der
Markdown-Text selbst derzeit keine Kennungen trägt.
Zwischenablage
Die Zwischenablage ist derselbe Vertrag, nur über eine Geste statt über eine Anfrage erreicht. Sie fügt keine Syntax hinzu: Der Browser führt genau den Parser und den Serialisierer aus, die dieses Dokument beschreibt — denselben Code, keine zweite Umsetzung davon. Was eine Kopie erzeugt, ist also ein Dokument, das die Rundlaufgarantie dieser Spezifikation bereits abdeckt. Die Regeln, die einem Ausschnitt eigen sind und deshalb hierhergehören:
Kopieren schreibt kanonisches Markdown nach text/plain, neben die
text/html-Variante, die ProseMirror seit jeher schreibt.
- Eine Auswahl über Blockgrenzen hinweg gibt die Blockform aus —
byteweise das, was
GET …?format=markdownfür diese Blöcke ausgäbe, abzüglich des abschließenden Zeilenumbruchs (eine Zwischenablage-Zeichenkette ist kein Dokument). - Eine Auswahl streng innerhalb eines Textblocks gibt die Inline-Form
aus: nur den Rich Text, ohne Blockmarker. Zwei Wörter aus einer Überschrift
ergeben
zwei Wörter, nicht# zwei Wörter— und das gilt, wie tief dieser Textblock auch sitzt. Zwei Wörter aus einem Listenpunkt, einer Aufgabe, einem Zitat, einer Hervorhebung, der Zusammenfassung eines Klappelements oder einer Tabellenzelle ergeben ebenfallszwei Wörter, nie- zwei Wörter. Leerraum an den Rändern wird abgeschnitten, weil CommonMark Leerraum an Blockrändern ohnehin entfernt und ihn nicht zurücklesen könnte. - Ein teilweise ausgewählter Codeblock gibt seinen Text wörtlich aus, unmaskiert: Er ist keine Prosa, und Maskieren verdürbe genau die Zeichen, die ihn zu Code machen.
- Frontmatter wird nie ausgegeben — eine Auswahl ist ein Körperausschnitt, und Feldwerte gehören zu einer Seite.
- Weil
text/htmlweiterhin geschrieben wird, nimmt ein Einfügen zurück in die App die HTML-Variante. Blockkennungen und Ausschnittkontext überleben eine interne Kopie also genau wie bisher. Der Markdown-Text ist das, was jedes andere Ziel bekommt.
Einfügen von schlichtem Text macht aus Markdown echte Blöcke.
- Nur Text mit einem Markdown-Signal wird als Markdown geparst: eine
ATX-Überschrift, ein Aufzählungs- oder Nummerierungsmarker, ein Zitatmarker,
ein Fence, eine Pipe-Tabellenzeile, eine Trennlinie, ein führender
----Frontmatter-Fence oder inline ein**fett**/~~durchgestrichen~~/`Code`/[Text](url)/[[wikilink]]. Alles andere wird eingefügt wie bisher, ein Absatz je Zeile: CommonMark faltet einzelne Zeilenumbrüche zu weichen Umbrüchen, und eine hart umbrochene E-Mail darf nicht stillschweigend zu einem einzigen Absatz kollabieren. - Eingefügte Blöcke bekommen immer frische Kennungen. Ein Ausschnitt ist neuer Inhalt, keine Wiederholung bestehender Blöcke.
- Eingefügtes Frontmatter wird verworfen. Ein Ausschnitt hat keine Seite, deren Feldwerte er setzen könnte.
- Eine nackte URL ist kein Markdown-Einfügen: Sie bleibt bei der Link-Einfüge-Geste (schlichter Link, Lesezeichenkarte oder Einbettung).
- Einfügen in einen Codeblock setzt wörtlichen Text ein, unverändert.