참조 처리

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

ReferenceFilter는 해당하는 ReferenceParser가 있어야 합니다.

두 개의 필터가 동일한 유형의 객체를 찾고 연결하는 경우 (‘data-reference-type’ 속성으로 지정된) 같은 유형의 도메인 객체에 대해 하나의 참조 파서만 필요합니다.

Banzai 파이프라인

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

result 해시는 각 필터에게 수정을 위해 전달됩니다. 여기에는:

  • 마지막 필터의 출력에 따라 :output 키가 DocumentFragment 또는 String HTML 마크업이 포함됩니다.
  • 매 필터가 업데이트한 처리를 위해 준비된 DocumentFragment nodes 디렉터리이 :reference_filter_nodes 키로 포함됩니다.

참조 필터

참조가 처리되는 첫 번째 방법은 참조 필터에 의해 처리됩니다. 이 도구들은 마크업 문서에서 짧은 코드와 URI 참조를 식별하고 이를 나타내는 리소스로의 구조화된 링크로 변환하는 데 사용됩니다.

예를 들어, Banzai::Filter::IssueReferenceFilter 클래스는 이슈에 대한 참조(예: gitlab-org/gitlab#123https://gitlab.com/gitlab-org/gitlab/-/issues/200048)를 처리하는 데 책임이 있습니다.

모든 참조 필터는 HTML::Pipeline::Filter의 인스턴스이며, 보통 Banzai::Filter::ReferenceFilter로부터 (대개 간접적으로) 상속됩니다.

HTML::Pipeline::Filter에는 현재 문서를 변경하는 void 메서드인 #call이라는 간단한 인터페이스가 있습니다. ReferenceFilter#call 메서드를 쉽게 정의할 수 있도록 하는 메서드를 제공합니다. 그러나 대부분의 참조 필터는 이러한 클래스 중 어느 것에서 직접 상속받지 않고 AbstractReferenceFilter에서 상속받습니다. 이것은 더 높은 수준의 인터페이스를 제공합니다.

AbstractReferenceFilter의 하위 클래스들은 일반적으로 #call을 오버라이드하지 않으며, 대신 최소한의 AbstractReferenceFilter 구현은 다음을 정의해야 합니다:

  • .reference_type: 도메인 오브젝트의 유형입니다.

    이것은 보통 키워드이며, 생성된 링크에 data-reference-type 속성을 설정하고 해당 ReferenceParser와의 상호 작용의 중요한 부분입니다 (아래 참조).

  • .object_class: 필터가 참조하는 객체의 클래스에 대한 참조입니다.

    이것은 다음에 사용됩니다:

    • 참조를 찾는 데 사용되는 정규 표현식을 찾습니다. 클래스는 Referable을 포함해야 하며, 따라서 .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)일 수 있습니다.

새로운 참조 접두사 및 필터 추가

새로운 객체에 대한 참조 필터를 추가할 때, 기능의 가치를 떨어뜨릴 수 있으므로 기능의 가치를 줄일 수 있습니다:

  1. 다양한 한 글자 접두사는 사용자가 추적하기 어렵습니다. 특히 낮은 사용량의 객체 유형의 경우, 이것은 기능의 가치를 줄일 수 있습니다.
  2. 적합한 한 글자 접두사가 제한적입니다.
  3. 일관된 패턴을 따르면 사용자가 새로운 기능의 존재를 추론할 수 있게 됩니다.

새로운 객체 apple에 대한 참조 접두사를 추가하려면 이름과 ID가 모두 있는 접두사 형식을 다음과 같이 포맷화합니다:

  • ID로 식별하는 경우 ^apple#123.
  • 이름으로 식별하는 경우 ^apple#"Granny Smith".

성능

객체 찾기 최적화

이 기본적인 구현은 모든 참조에 대해 #find_object를 호출해야 하므로 매번 DB 쿼리를 요청해야 할 수 있기 때문에 효율적이지 않습니다. 따라서 대부분의 참조 필터 구현은 AbstractReferenceFilter에 포함된 최적화를 대신 사용합니다:

AbstractReferenceFilter는 부모 객체에서 초기화되는 값 #records_per_parent을 제공합니다. 이 값은 부모 객체에서 도메인 객체로 매핑하는 컬렉션입니다.

이 메커니즘을 사용하려면 참조 필터는 부모 객체를 입력으로 받아서 set_of_identifiers를 돌려주는 #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)이 다음 필터를 위해 업데이트됩니다.

참조 파서

성능 최적화로 인해 경우에 따라 마크다운을 HTML로 렌더링한 후 결과를 캐시하고 캐시된 값을 통해 사용자에게 제시합니다. 예를 들어, 이는 노트, 이슈 설명 및 Merge Request 설명에 대해 발생합니다. 이로 인해 렌더된 문서는 이후 사용자가 볼 수 없는 리소스를 참조할 수 있습니다.

예를 들어, 이슈를 생성하고 귀하의 액세스 권한이 있는 기밀 이슈 #1234을 참조할 수 있습니다. 이는 캐시된 HTML에서 기밀 이슈에 대한 링크로 렌더링되며, 해당 ID, 프로젝트 ID 및 기타 기밀 데이터를 포함하는 데이터 속성을 가지고 있습니다. 귀하의 이슈에 액세스 권한이 있는 이후의 독자는 이슈 #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과 같아야 합니다.
  • 그리고 다음 중 하나 이상을 구현함으로써:
    • 가장 세밀한 제어를 위해 #nodes_visible_to_user(user, nodes)를 구현합니다.
    • 오버라이드된 nodes_visible_to_user가 아닌 경우 필요한 #can_read_reference?.
    • ID별로 오브젝트를 위한 활성 레코드 관계인 #references_relation.
    • 노드를 직접 필터링하는 #nodes_user_can_reference(user, nodes)입니다.

이 참조 유형에 대해 이러한 클래스를 구현하지 않으면 응용 프로그램이 마크다운 처리 중에 예외를 발생시킵니다.