참조 처리

GitLab Flavored Markdown에는 다양한 GitLab 도메인 개체에 대한 참조를 처리하는 기능이 포함되어 있습니다. 이는 Banzai 파이프라인의 두 가지 추상화인 ReferenceFilterReferenceParser에 의해 구현됩니다. 본 페이지에서는 이러한 것들이 무엇이며 어떻게 사용되며 새로운 필터/파서 쌍을 구현하는지에 대해 설명합니다.

ReferenceFilter는 해당하는 ReferenceParser를 가져야 합니다.

필터 간에 참조 파서를 공유하는 것이 가능합니다. 즉, 두 필터가 동일한 유형의 객체를 찾고 링크하는 경우(data-reference-type 속성으로 지정됨) 해당 유형의 도메인 개체에 대해 참조 파서가 하나만 필요합니다.

Banzai 파이프라인

Banzai 파이프라인은 파이프라인에서 필터링된 후 result 해시를 반환합니다.

result 해시는 각 필터에게 수정을 위해 전달됩니다. 이것은 필터가 콘텐츠에서 추출된 정보를 저장하는 곳입니다.

이것은 다음을 포함합니다:

  • 마지막 필터의 출력을 기반으로 한 DocumentFragment 또는 문자열 HTML 마크업이 있는 :output
  • 각 파이프라인에서 업데이트된 처리할 준비가 된 DocumentFragment 노드 디렉터리이 있는 :reference_filter_nodes

참조 필터

참조가 처리되는 첫 번째 방법은 참조 필터에 의해 처리됩니다. 이러한 도구들은 마크업 문서에서 단축 코드와 URI 참조를 식별하고, 이를 나타내는 리소스에 대한 구조화된 링크로 변환하는 역할을 합니다.

예를 들어, 클래스 Banzai::Filter::IssueReferenceFiltergitlab-org/gitlab#123https://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>#. 그 이유는 다음과 같습니다:

  1. 다양한 단일 문자 접두사는 사용자에게 추적하기 어렵습니다. 특히 사용 빈도가 낮은 객체 유형의 경우 기능의 가치를 감소시킬 수 있습니다.
  2. 적절한 단일 문자 접두사가 제한적입니다.
  3. 일관된 패턴을 따르면 사용자들이 새로운 기능의 존재를 추론할 수 있습니다.

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_typeReferenceFilter.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 처리 중에 애플리케이션이 예외를 발생시킵니다.