참조 처리
GitLab Flavored Markdown에는 다양한 GitLab 도메인 개체에 대한 참조를 처리하는 기능이 포함되어 있습니다. 이는 Banzai
파이프라인의 두 가지 추상화인 ReferenceFilter
와 ReferenceParser
에 의해 구현됩니다. 본 페이지에서는 이러한 것들이 무엇이며 어떻게 사용되며 새로운 필터/파서 쌍을 구현하는지에 대해 설명합니다.
각 ReferenceFilter
는 해당하는 ReferenceParser
를 가져야 합니다.
필터 간에 참조 파서를 공유하는 것이 가능합니다. 즉, 두 필터가 동일한 유형의 객체를 찾고 링크하는 경우(data-reference-type
속성으로 지정됨) 해당 유형의 도메인 개체에 대해 참조 파서가 하나만 필요합니다.
Banzai 파이프라인
Banzai
파이프라인은 파이프라인에서 필터링된 후 result
해시를 반환합니다.
result
해시는 각 필터에게 수정을 위해 전달됩니다. 이것은 필터가 콘텐츠에서 추출된 정보를 저장하는 곳입니다.
이것은 다음을 포함합니다:
- 마지막 필터의 출력을 기반으로 한 DocumentFragment 또는 문자열 HTML 마크업이 있는
:output
키 - 각 파이프라인에서 업데이트된 처리할 준비가 된 DocumentFragment
노드
디렉터리이 있는: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
의 하위 클래스들은 일반적으로 #call
을 재정의하지 않습니다. 대신, AbstractReferenceFilter
의 최소한의 구현은 다음을 정의해야 합니다:
-
.reference_type
: 도메인 개체의 유형.이것은 보통 키워드로, 생성된 링크에
data-reference-type
속성을 설정하고, 해당ReferenceParser
와의 상호 작용의 중요한 부분입니다. -
.object_class
: 필터가 참조하는 객체의 클래스에 대한 참조.이것은 다음을 사용합니다:
- 참조를 찾는 데 사용되는 정규식을 찾습니다. 클래스에는
.link_reference_pattern
과.reference_pattern
두 가지를 정의해야 합니다. 둘 다ReferenceFilter.object_sym
의 값으로 명명된 캡처 그룹을 포함해야 합니다. -
.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
)와 식별자가 주어졌을 때 객체를 찾습니다. 예를 들어, Merge Request에 대한 참조 필터의 경우,project.merge_requests.where(iid: iid)
가 될 수 있습니다.
새로운 참조 접두사와 필터 추가
새로운 개체에 대한 참조 필터를 추가할 때는 다음과 같은 패턴을 따르는 접두어 형식을 사용하십시오: ^<object_type>#
. 그 이유는 다음과 같습니다:
- 다양한 단일 문자 접두사는 사용자에게 추적하기 어렵습니다. 특히 사용 빈도가 낮은 객체 유형의 경우 기능의 가치를 감소시킬 수 있습니다.
- 적절한 단일 문자 접두사가 제한적입니다.
- 일관된 패턴을 따르면 사용자들이 새로운 기능의 존재를 추론할 수 있습니다.
apple
이라는 새로운 개체에 대한 참조 접두어를 추가하려면 다음과 같이 형식을 지정하십시오:
- ID로 식별:
^apple#123
- 이름으로 식별:
^apple#"Granny Smith"
성능
객체 찾기 최적화
이 기본 구현은 각 참조마다 #find_object
를 호출해야 하므로 매번 DB 쿼리를 발급할 수 있기 때문에 효율적이지 않습니다. 따라서 대부분의 참조 필터 구현은 AbstractReferenceFilter
에 포함된 최적화를 사용합니다:
AbstractReferenceFilter
는 느리게 초기화된#records_per_parent
값을 제공합니다. 이 값은 상위 객체에서 도메인 객체의 컬렉션으로 매핑됩니다.
이 메커니즘을 사용하려면 참조 필터가 반드시 #parent_records(parent, set_of_identifiers)
메서드를 구현해야 합니다. 이 메서드는 도메인 객체의 열거형을 반환해야 합니다.
이를 통해 이러한 클래스들은 다음과 같이 #find_object
를 정의할 수 있습니다(예: IssuableReferenceFilter
):
def find_object(parent, iid)
records_per_parent[parent][iid]
end
이로써 쿼리 수는 프로젝트 수에 선형적입니다. 우리는 참조 필터에서 records_per_parent
를 호출할 때 #parent_records
메서드를 구현해야 하는 경우에만 이를 구현할 필요가 있습니다.
필터링 노드 최적화
각 ReferenceFilter
는 문서의 모든 <a>
와 text()
노드를 반복합니다.
모든 노드가 처리되는 것은 아니며, 문서는 원하는 노드만 처리됩니다. 다음을 건너뜁니다:
- 이미 이전 필터에 의해 처리된 링크 태그(그들이
gfm
클래스를 가지고 있다면). - 무시할 조상 노드가 있는 노드(
ignore_ancestor_query
). - 빈 줄.
- 빈
href
속성을 갖는 링크 태그.
각 ReferenceFilter
마다 이러한 노드를 필터링하는 것은 비효율적입니다. 따라서 한 번만 처리하고 파이프라인의 결과 해시에 result[:reference_filter_nodes]
로 저장합니다.
파이프라인 result
는 수정을 위해 각 필터에 전달되므로, ReferenceFilter
가 텍스트 또는 링크 태그를 대체할 때마다 다음 필터에서 사용할 수 있도록 필터링된 디렉터리(reference_filter_nodes
)이 업데이트됩니다.
참조 구문 분석기
성능 최적화를 위한 몇 가지 경우에는 Markdown을 HTML로 렌더링한 후 결과를 캐시하고 나중에 사용자에게 캐시된 값을 제공합니다. 예를 들어, 이는 노트, 이슈 설명 및 Merge Request 설명에 대해 발생합니다. 이로 인한 결과는 렌더링된 문서가 나중에 어떤 사용자가 볼 수 없어야 하는 리소스를 참조할 수 있다는 것입니다.
예를 들어, 이슈를 만들고, 귀하가 액세스 권한을 가지고 있는 기밀 이슈 #1234
를 참조할 수 있습니다. 이는 캐시된 HTML에서 이것을 기밀 이슈로 링크된 상태로 렌더링되며, 해당 ID, 프로젝트 ID 및 기타 기밀 데이터가 포함된 데이터 속성을 가지고 있습니다. 이후에 해당 이슈에 액세스 권한이 있는 다른 사용자는 이슈 #1234
를 읽을 수 없을 수도 있으므로, 이러한 민감한 데이터를 비공개 처리해야 합니다. 이것이 ReferenceParser
클래스의 역할입니다.
참조 구문 분석기는 해당 객체에 링크된 참조 필터에서 이 관계를 알리는 링크와 함께 연결되어 있습니다. 이는 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
과 동일하게 해야 합니다. - 그리고 다음 중 하나 이상을 구현함으로써:
- 가장 정확한 제어를 위해
nodes_visible_to_user(user, nodes)
를 구현합니다. -
nodes_visible_to_user
가 재정의되지 않은 경우 필요한can_read_reference?
를 구현합니다. - ID별로 작동하는 액티브 레코드 관계인
references_relation
을 구현합니다. - 직접적으로 노드를 필터링하기 위해
nodes_user_can_reference(user, nodes)
를 구현합니다.
- 가장 정확한 제어를 위해
이와 같은 참조 유형에 대한 클래스를 구현하지 않은 경우에는 Markdown 처리 중에 애플리케이션이 예외를 발생시킵니다.