Markdown拡張機能の開発者ガイド

Discourse は Markdown-it という Markdown エンジンを使用しています。

コアのバグ修正や新しいプラグインの作成に役立つ開発者向けメモを以下に示します。

基本

Discourse はエンジン上にいくつかのヘルパーしか含んでいないため、学ぶべきことの大部分は Markdown It を理解することです。

docs ディレクトリ に現在のドキュメントが含まれています。

以下を読むことを強くお勧めします。

  • エンジンの動作を大まかに理解するための architecture document

  • 基本的な開発ガイドラインのための Development

  • 非常に詳細なリファレンスのための API documentation

  • そして最後に、非常によく文書化されていて明確な source code です。

エンジン用の拡張機能を開発するとき、通常は 2 つ目のエディタを開いて既存のルールを確認します。エンジンは多数のルールのリストで構成されており、各ルールは追跡が比較的容易な専用のファイルにあります。

インラインルールに取り組む場合は、既存のどのインラインルールがそれに似ているかを考え、それを基に作業を行います。

望ましい機能を実現するためには、レンダラーを変更するだけで済む場合があり、通常はそれがはるかに簡単であることを覚えておいてください。

拡張機能の構造化方法

Markdown エンジンが初期化されると、すべてのモジュールを検索します。

モジュールが /discourse-markdown\\/|markdown-it\\// という名前である場合(つまり、discourse-markdown または markdown-it ディレクトリ内にある場合)、初期化の候補となります。

モジュールが setup という名前のメソッドをエクスポートしている場合、初期化中にエンジンによって呼び出されます。

セットアップ プロトコル

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

export function setup(helper) {
  // ... ここにコードを記述します
}

setup メソッドは、初期化に使用できるヘルパーオブジェクトへのアクセスを提供します。これには次のメソッドと変数があります。

  • bool markdownIt : このプロパティは、新しいエンジンが使用されている場合に true に設定されます。適切な後方互換性のために確認する必要があります。

  • registerOptions(cb(opts,siteSettings,state)) : 提供された関数は、Markdown エンジンが初期化される前に呼び出され、エンジンを有効または無効にするかどうかを決定するために使用できます。

  • allowList([spec, ...]): このメソッドは、HTML をサニタイザーで許可するために使用されます。

  • registerPlugin(func(md)): このメソッドは、Markdown It プラグイン を登録するために使用されます。

すべてをまとめる

function amazingMarkdownItInline(state, silent) {
   // 標準の markdown it インライン拡張機能をここに記述します。
   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 固有の拡張機能

BBCode

Discourse には、カスタム BBCode タグに使用できる 2 つのルーラーが含まれています。インラインとブロックレベルのルーラーです。

インライン BBCode ルールは、[b]太字[/b] のようなインライン段落内に存在するものです。

ブロックレベルのルールは、次のような複数行のテキストに適用されます。

[poll]
- option 1

- options 2
[/poll]

md.inline.bbcode.ruler は、順序で適用されるインラインルールのリストを保持します。

md.block.bbcode.ruler は、ブロックレベルルールのリストを保持します。

インラインルールの多くの例は次の場所にあります: bbcode-inline.js

Quotes と polls は、bbcode ブロックルールの良い例です。

インライン BBCode ルール

インライン BBCode ルールは、タグの処理方法に関する情報を含むオブジェクトです。

例えば:

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

これにより、次のような変換が行われます。

test [u]test[/u]

次のように変換されます。

test <span>test</span>

インラインルールは、テキストをラップすることも、テキストを置き換えることもできます。ラップする場合、追加の柔軟性を得るために関数を渡すこともできます。

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 {
      // bbcode タグを削除するだけ
      endToken.content = "";
      startToken.content = "";

      // エッジケース、自動リンクされた場合にこれが onebox として検出されないようにする
      // これにより、削除されないことが保証されます
      startToken.type = "html_inline";
    }

    return false;
  },
});

ラッパー関数は、以下へのアクセスを提供します。

  • bbcode 経由で指定されたキー/値の辞書である tagInfo。

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

  • インラインの開始トークン

  • インラインを終了するトークン

  • bbcode インラインのコンテンツ

この情報を使用して、あらゆる種類のラッピングニーズに対応できます。

場合によっては、BBCode ブロック全体を置き換えることが望ましい場合があります。その場合は replace を使用できます。

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;
  },
});

この場合、[code]コードブロック[code] 全体を単一の code_inline トークンに置き換えています。

ブロック BBCode ルール

ブロック bbcode ルールを使用すると、ブロック全体を置き換えることができます。ブロック API は、単純なケースでは同じです。

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

次のように変換されます。

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

関数ラッパーは、ラップするトークンがないため、わずかに異なる API を持ちます。

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]

次のように変換されます。

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

before および after ルールを使用すると、ブロックレンダリングを完全に制御でき、タグを二重にネストするなどの操作を実行できます。

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]

次のように変換されます。

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

テキストの置き換えの処理

Discourse には、正規表現をテキストに適用するための特別なコアルールが付属しています。

md.core.textPostProcess.ruler

使用方法:

md.core.textPostProcess.ruler.push("onlyfastcars", {
  matcher: /(car)|(bus)/, //正規表現フラグはサポートされていません
  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

次のように変換されます。

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

このドキュメントはバージョン管理されています - 変更の提案は github で行えます。

「いいね!」 35