リファレンス処理
GitLab Flavored MarkdownにはGitLabドメインオブジェクトへの参照を処理する機能があります。これはBanzai
パイプラインの2つの抽象化によって実装されています:ReferenceFilter
とReferenceParser
。このページでは、これらが何なのか、どのように使われるのか、そして新しいフィルタとパーサのペアをどのように実装するのかを説明します。
各ReferenceFilter
には、対応するReferenceParser
が必要です。
2つのフィルターが(data-reference-type
属性で指定された)同じタイプのオブジェクトを見つけてリンクする場合、そのタイプのドメインオブジェクトの参照パーサーは1つで済みます。
バンザイ パイプライン
Banzai
パイプラインは、パイプラインによってフィルタリングされた後のresult
Hash を返します。
result
Hashは各フィルタに渡され、修正されます。ここには、コンテンツから抽出された情報が格納されます。コンテナがあります:
- パイプラインの最後のフィルターの出力に基づく、DocumentFragment または文字列 HTML マークアップの
:output
キー。 - パイプラインの各フィルタによって更新された、処理の準備ができた DocumentFragment
nodes
のリストを持つ:reference_filter_nodes
キー。
参照フィルタ
参照を処理する最初の方法は、参照フィルターです。これは、マークアップ文書からショートコードやURI参照を識別し、それらが表すリソースへの構造化リンクに変換するツールです。
例えば、Banzai::Filter::IssueReferenceFilter
というクラスは、gitlab-org/gitlab#123
やhttps://gitlab.com/gitlab-org/gitlab/-/issues/200048
といったイシューへの参照を処理する役割を担っています。
すべての参照フィルタはHTML::Pipeline::Filter
のインスタンスであり、Banzai::Filter::ReferenceFilter
を(間接的に)継承しています。
HTML::Pipeline::Filter
#call
ReferenceFilter
は、適切な#call
メソッドの定義を容易にするメソッドを提供します。しかし、ほとんどの参照フィルタは、これらのクラスのいずれかを直接継承しているわけではなく、AbstractReferenceFilter
を継承しており、 はより高レベルのインタフェースを提供しています。
AbstractReferenceFilter
AbstractReferenceFilter
のサブクラスは一般的に#call
をオーバーライドしません:
-
.reference_type
:ドメイン・オブジェクトの型。これは通常キーワードで、生成されたリンクに
data-reference-type
属性を設定するために使われ、対応するReferenceParser
(下記参照) とのインタラクションの重要な部分です。 -
.object_class
: フィルタが参照するオブジェクトのクラスへの参照です。これは
- 参照の検索に使用される正規表現を検索します。
ReferenceFilter.object_sym
このクラスにはReferable
が含まれ、2 つの正規表現が定義されている必要があります:.link_reference_pattern
と.reference_pattern
です。 -
.object_name
を計算します。 -
.object_sym
(参照パターンのグループ名)を計算します。
- 参照の検索に使用される正規表現を検索します。
-
.parse_symbol(string)
: テキスト値をオブジェクト識別子 ( デフォルトでは#to_i
) にパースします。 -
#record_identifier(record)
:.parse_symbol
の逆、つまりドメインオブジェクトを識別子に変換します ( デフォルトでは#id
)。 -
#url_for_object(object, parent_object)
ドメインオブジェクトのURLを生成します。 -
#find_object(parent_object, id)
: 親 (通常はProject
) と識別子が与えられたら、オブジェクトを見つけます。例えば、マージリクエストの参照フィルタでは、project.merge_requests.where(iid: iid)
となります。
新しい参照プレフィックスとフィルタを追加
新しいオブジェクトの参照フィルタには、^<object_type>#
, because のパターンに従ったプレフィックス形式を使用します:
- 多様な一文字接頭辞はユーザーが追跡しにくい。特に、使用頻度の低いオブジェクトタイプでは、これは機能の価値を低下させる可能性があります。
- 適切な一文字接頭辞は限られています。
- 一貫したパターンに従うことで、ユーザーは新機能の存在を推測することができます。
名前と ID の両方を持つ新しいオブジェクトapple
の参照接頭辞を追加するには、参照を次のようにフォーマットします:
-
^apple#123
としてください。 -
^apple#"Granny Smith"
名前で識別する場合
パフォーマンス
オブジェクトの最適化
このデフォルトの実装はあまり効率的ではありません。なぜなら、参照ごとに#find_object
を呼び出す必要があり、毎回 DB クエリを発行する必要があるからです。このため、ほとんどの参照フィルタの実装では、AbstractReferenceFilter
に含まれる最適化を使用しています:
AbstractReferenceFilter
に含まれる最適化を使用します。#records_per_parent
は、親オブジェクトからドメインオブジェクトのコレクションへのマッピングです。
このメカニズムを使用するには、参照フィルターはメソッドを実装する必要があります:#parent_records(parent, set_of_identifiers)
メソッドを実装しなければなりません。
これにより、そのようなクラスは(IssuableReferenceFilter
がそうであるように)#find_object
を定義することができます:
def find_object(parent, iid)
records_per_parent[parent][iid]
end
これにより、クエリの数はプロジェクトの数に比例します。私たちがparent_records
メソッドを実装する必要があるのは、参照フィルタでrecords_per_parent
を呼び出すときだけです。
フィルタリングノードの最適化
各ReferenceFilter
は、ドキュメント内のすべての<a>
とtext()
ノードを繰り返し処理します。
すべてのノードが処理されるわけではなく、ドキュメントは処理したいノードに対してのみフィルタリングされます。スキップしています:
- リンクタグは既に以前のフィルターで処理されています (
gfm
クラスがある場合)。 - 無視したい祖先ノードを持つノード(
ignore_ancestor_query
)。 - 空行。
- 空の
href
属性を持つリンクタグ。
このようなノードをReferenceFilter
ごとにフィルタリングするのを避けるため、1 回だけフィルタリングを行い、その結果をパイプラインの結果ハッシュにresult[:reference_filter_nodes]
として格納します。
パイプラインresult
は各フィルタに渡され、ReferenceFilter
がテキストまたはリンクタグを置き換えるたびに、フィルタリングされたリスト (reference_filter_nodes
) が更新されます。
リファレンスパーサー
多くの場合、パフォーマンスの最適化として、Markdownを一度HTMLにレンダリングし、その結果をキャッシュし、キャッシュされた値からユーザーに表示します。例えば、これはノート、イシューの説明、マージリクエストの説明のために行われます。この結果、レンダリングされたドキュメントが、一部の後続の読者が見ることができないリソースを参照する可能性があります。
例えば、あなたがイシューを作成し、#1234
あなたがアクセス #1234
できる機密のイシューを参照するとします。#1234
この場合、キャッシュされたHTMLには、その機密イシューへのリンクとして、ID、プロジェクトのID、その他の機密データを含むデータ属性とともに表示されます。後であなたのイシューにアクセスする読者は、issueを読む権限を持たない可能性がある #1234
ため、これらの機密データを再編集する必要があります。これがReferenceParser
。
参照パーサは、data-reference-type
属性(参照フィルタによって設定されます)のこの関係を宣伝するリンクによって、それが扱うオブジェクトにリンクされます。これはReferenceRedactor
によって、どのノードをユーザーに見せるべきかを計算するために使われます:
def nodes_visible_to_user(nodes)
per_type = Hash.new { |h, k| h[k] = [] }
visible = Set.new
nodes.each do |node|
per_type[node.attr('data-reference-type')] << node
end
per_type.each do |type, nodes|
parser = Banzai::ReferenceParser[type].new(context)
visible.merge(parser.nodes_visible_to_user(user, nodes))
end
visible
end
ここで重要なのはBanzai::ReferenceParser[type]
で、ドメインオブジェクトのタイプごとに正しい参照パーサーを検索するために使用されます。このため、各参照パーサーは
-
Banzai::ReferenceParser
名前空間に配置されていること。 -
.nodes_visible_to_user(user, nodes)
メソッドを実装してください。
実際には、すべてのリファレンスパーサーはBaseParser
を継承し、定義することで実装されます:
-
.reference_type
ReferenceFilter.reference_type
を定義することで実装されています。 - そして、1つ以上の実装によって
-
#nodes_visible_to_user(user, nodes)
最高級の粒度制御のために -
#can_read_reference?
nodes_visible_to_user
をオーバーライドしない場合に必要。 -
#references_relation
IDによるオブジェクトのアクティブレコード関係。 -
#nodes_user_can_reference(user, nodes)
を使用してノードを直接フィルタリングできます。
-
各参照タイプに対してこのクラスを実装しないと、アプリケーションはMarkdown処理中に例外を発生させます。