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

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

以下は、コアのバグを修正したり、新しいプラグインを作成したりするのに役立つ開発メモです。

基本

Discourse はエンジンの上に少数のヘルパーしか持たないため、学ぶべきことの大部分は Markdown It の理解にあります。

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

以下を読むことを強く推奨します:

エンジンの拡張機能を開発する際、私は通常、既存のルールを確認するために2番目のエディタを開きます。エンジンは長いルールのリストで構成されており、各ルールは追跡しやすい専用のファイルにあります。

インラインルールに取り組んでいる場合、それに近い動作をする既存のインラインルールを考え、それをベースに作業を行います。

場合によっては、レンダラーを変更するだけで所望の機能を得られることがあります。これは通常ははるかに簡単です。

拡張機能の構造はどうすればよいのか?

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

モジュールが /discourse-markdown\/|markdown-it\//(つまり discourse-markdown ディレクトリまたは markdown-it ディレクトリ内にあることを意味する)に一致する場合、初期化の候補となります。

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

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]
- オプション 1

- オプション 2
[/poll]

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

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

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

引用 と投票は、bbcode ブロックルールの良い例です。

インライン BBCode ルール

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

例えば:

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

これにより、

test [u]test[/u]

は以下に変換されます:

test <span class="bbcode-u">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;
  },
});

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

  • tagInfo、これは bbcode を介して指定されたキー/値の辞書です。

    [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 block[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>

beforeafter ルールを使用すると、ブロックレンダリングを完全に制御できます。これにより、タグを二重にネストしたり、他の処理を行ったりできます。

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 でどうぞ。

「いいね!」 36

これをテーマコンポーネントで使用できますか、それともプラグイン専用ですか?