Entwickler-Leitfaden zu Markdown-Erweiterungen

Discourse verwendet einen Markdown-Engine namens Markdown-it.

Hier sind einige Entwicklungshinweise, die dir helfen, Fehler im Kern zu beheben oder neue Plugins zu erstellen.

Die Grundlagen

Discourse enthält nur wenige Helfer auf Basis der Engine. Der Großteil des Lernens besteht daher darin, Markdown It zu verstehen.

Das Dokumentationsverzeichnis enthält die aktuelle Dokumentation.

Ich empfehle dringend, Folgendes zu lesen:

Während ich Erweiterungen für die Engine entwickle, öffne ich normalerweise einen zweiten Editor, um mir vorhandene Regeln anzusehen. Die Engine besteht aus einer langen Liste von Regeln, und jede Regel befindet sich in einer eigenen Datei, die sich gut nachvollziehen lässt.

Wenn ich an einer Inline-Regel arbeite, überlege ich, welche vorhandene Inline-Regel mehr oder weniger ähnlich funktioniert, und baue meine Arbeit darauf auf.

Denke daran, dass man manchmal auskommt, indem man nur einen Renderer ändert, um die gewünschte Funktionalität zu erhalten. Das ist in der Regel viel einfacher.

Wie strukturiert man eine Erweiterung?

Wenn die Markdown-Engine initialisiert wird, durchsucht sie alle Module.

Wenn ein Modul /discourse-markdown\/|markdown-it\// heißt (was bedeutet, dass es sich in einem discourse-markdown- oder markdown-it-Verzeichnis befindet), ist es ein Kandidat für die Initialisierung.

Wenn das Modul eine Methode namens setup exportiert, wird diese von der Engine während der Initialisierung aufgerufen.

Das Setup-Protokoll

/my-plugins/assets/javascripts/discourse-markdown/awesome-extension.js

export function setup(helper) {
  // ... dein Code kommt hier hin
}

Eine setup-Methode erhält Zugriff auf ein Helper-Objekt, das für die Initialisierung verwendet werden kann. Dieses enthält die folgenden Methoden und Variablen:

  • bool markdownIt : Diese Eigenschaft wird auf true gesetzt, wenn die neue Engine verwendet wird. Für eine korrekte Abwärtskompatibilität solltest du dies prüfen.

  • registerOptions(cb(opts, siteSettings, state)) : Die übergebene Funktion wird vor der Initialisierung der Markdown-Engine aufgerufen. Du kannst sie verwenden, um zu bestimmen, ob die Engine aktiviert oder deaktiviert werden soll.

  • allowList([spec, ...]): Diese Methode wird verwendet, um HTML mit unserem Sanitizer zu erlauben (Allowlist).

  • registerPlugin(func(md)): Diese Methode wird verwendet, um ein Markdown It Plugin zu registrieren.

Alles zusammenfügen

function amazingMarkdownItInline(state, silent) {
   // Standard Markdown It Inline-Erweiterung kommt hier hin.
   return false;
}

export function setup(helper) {
   if(!helper.markdownIt) { return; }

   helper.registerOptions((opts,siteSettings)=>{
      opts.features.['my_extension'] = !!siteSettings.my_extension_enabled;
   });

   helper.allowList(['span.amazing', 'div.amazing']);

   helper.registerPlugin(md=>{
      md.inline.push('amazing', amazingMarkdownItInline);
   });
}

Discourse-spezifische Erweiterungen

BBCode

Discourse enthält zwei Regler (Rulers), die du für eigene BBCode-Tags verwenden kannst. Einen für Inline-Ebene und einen für Block-Ebene.

Inline-BBCode-Regeln sind solche, die in einem Inline-Absatz stehen wie [b]fett[/b]

Block-Regeln gelten für mehrere Zeilen Text wie:

[poll]
- Option 1

- Option 2
[/poll]

md.inline.bbcode.ruler enthält eine Liste von Inline-Regeln, die in der Reihenfolge angewendet werden.

md.block.bbcode.ruler enthält eine Liste von Block-Regeln

Es gibt viele Beispiele für Inline-Regeln unter: bbcode-inline.js

Zitate und Umfragen sind gute Beispiele für BBCode-Block-Regeln.

Inline-BBCode-Regeln

Inline-BBCode-Regeln sind ein Objekt, das Informationen darüber enthält, wie ein Tag verarbeitet werden soll.

Zum Beispiel:

md.inline.bbcode.ruler.push("underline", {
  tag: "u",
  wrap: "span.bbcode-u",
});

Verursacht, dass

test [u]test[/u]

umgewandelt wird in:

test <span class="bbcode-u">test</span>

Inline-Regeln können Text entweder einhüllen (wrap) oder ersetzen (replace). Beim Einhüllen kannst du auch eine Funktion übergeben, um zusätzliche Flexibilität zu erhalten.

md.inline.bbcode.ruler.push("url", {
  tag: "url",
  wrap: function (startToken, endToken, tagInfo, content) {
    const url = (tagInfo.attrs["_default"] || content).trim();

    if (simpleUrlRegex.test(url)) {
      startToken.type = "link_open";
      startToken.tag = "a";
      startToken.attrs = [
        ["href", url],
        ["data-bbcode", "true"],
      ];
      startToken.content = "";
      startToken.nesting = 1;

      endToken.type = "link_close";
      endToken.tag = "a";
      endToken.content = "";
      endToken.nesting = -1;
    } else {
      // entferne einfach das bbcode-Tag
      endToken.content = "";
      startToken.content = "";

      // Randfall, wir wollen nicht, dass dies als Onebox erkannt wird, wenn automatisch verlinkt
      // dies stellt sicher, dass es nicht entfernt wird
      startToken.type = "html_inline";
    }

    return false;
  },
});

Die Einhüllungs-Funktion bietet Zugriff auf:

  • Die tagInfo, die ein Wörterbuch von Schlüssel/Wert-Paaren ist, die über BBCode angegeben wurden.

    [test=testing]{_default: "testing"}
    [test a=1]{a: "1"}

  • Das Token, das das Inline-Element beginnt

  • Das Token, das das Inline-Element beendet

  • Den Inhalt des BBCode-Inline-Elements

Mit diesen Informationen kannst du alle Arten von Einhüllungsbedürfnissen verarbeiten.

Gelegentlich möchtest du möglicherweise den gesamten BBCode-Block ersetzen. Dafür kannst du replace verwenden

md.inline.bbcode.ruler.push("code", {
  tag: "code",
  replace: function (state, tagInfo, content) {
    let token;
    token = state.push("code_inline", "code", 0);
    token.content = content;
    return true;
  },
});

In diesem Fall ersetzen wir einen gesamten [code]code block[code] durch ein einzelnes code_inline-Token.

Block-BBCode-Regeln

Block-BBCode-Regeln erlauben es dir, einen gesamten Block zu ersetzen. Die Block-APIs sind für einfache Fälle die gleichen:

md.block.bbcode.ruler.push("happy", {
  tag: "happy",
  wrap: "div.happy",
});
[happy]
hello
[/happy]

wird zu

<div class="happy">hello</div>

Die Funktions-Hülle hat eine leicht andere API, da es keine Einhüllungs-Tokens gibt.

md.block.bbcode.ruler.push("money", {
  tag: "money",
  wrap: function (token, tagInfo) {
    token.attrs = [["data-money", tagInfo.attrs["_default"]]];
    return true;
  },
});
[money=100]
**test**
[/money]

Wird zu

<div data-money="100">
  <b>test</b>
</div>

Mit den Regeln before und after kannst du die volle Kontrolle über die Block-Renderung erhalten. Damit kannst du zum Beispiel Tags doppelt verschachteln und so weiter.

md.block.bbcode.ruler.push("ddiv", {
  tag: "ddiv",
  before: function (state, tagInfo) {
    state.push("div_open", "div", 1);
    state.push("div_open", "div", 1);
  },
  after: function (state) {
    state.push("div_close", "div", -1);
    state.push("div_close", "div", -1);
  },
});
[ddiv]
test
[/ddiv]

wird zu

<div>
  <div>test</div>
</div>

Verarbeitung von Textersetzungen

Discourse bringt eine besondere Kernregel mit, um reguläre Ausdrücke auf Text anzuwenden.

md.core.textPostProcess.ruler

Verwendung:

md.core.textPostProcess.ruler.push("onlyfastcars", {
  matcher: /(car)|(bus)/, //regex Flags werden NICHT unterstützt
  onMatch: function (buffer, matches, state) {
    let token = new state.Token("text", "", 0);
    token.content = "fast " + matches[0];
    buffer.push(token);
  },
});
I like cars and buses

Wird zu

<p>I like fast cars and fast buses</p>

Dieses Dokument wird versioniert – Vorschläge für Änderungen auf github.

35 „Gefällt mir“

Kann/dies in einer Theme-Komponente verwendet werden? Oder ist es nur für Plugins verfügbar?