InfoGrab DocsInfoGrab Docs

참조 처리

요약

GitLab Flavored Markdown에는 다양한 GitLab 도메인 객체에 대한 참조를 처리하는 기능이 포함되어 있습니다. 각 ReferenceFilter에는 대응하는 ReferenceParser가 있어야 합니다.

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

ReferenceFilter에는 대응하는 ReferenceParser가 있어야 합니다.

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

Banzai 파이프라인#

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

result 해시는 수정을 위해 각 필터에 전달됩니다. 여기서 필터는 콘텐츠에서 추출한 정보를 저장합니다. 다음 내용이 포함됩니다:

  • 파이프라인의 마지막 필터 출력을 기반으로 한 DocumentFragment 또는 String HTML 마크업이 담긴 :output 키.

  • 파이프라인의 각 필터에 의해 업데이트되며, 처리 준비가 된 DocumentFragment nodes 목록이 담긴 :reference_filter_nodes 키.

참조 필터#

참조가 처리되는 첫 번째 방법은 참조 필터입니다. 이는 마크업 문서에서 단축 코드 및 URI 참조를 식별하고, 이를 해당 리소스에 대한 구조화된 링크로 변환하는 도구입니다.

예를 들어, 클래스 Banzai::Filter::References::IssueReferenceFiltergitlab-org/gitlab#123https://gitlab.com/gitlab-org/gitlab/-/issues/200048과 같은 이슈 참조를 처리하는 역할을 합니다.

모든 참조 필터는 HTML::Pipeline::Filter의 인스턴스이며, Banzai::Filter::References::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)와 식별자가 주어졌을 때 객체를 찾습니다. 예를 들어, 머지 리퀘스트에 대한 참조 필터에서는 project.merge_requests.where(iid: iid)가 될 수 있습니다.

새 참조 접두사 및 필터 추가#

새 객체에 대한 참조 필터는 [object_type:identifier] 패턴을 따르는 형식을 사용하세요. 이유는 다음과 같습니다:

  • 다양한 단일 문자 접두사는 사용자가 추적하기 어렵습니다. 특히 사용 빈도가 낮은 객체 유형의 경우, 기능의 가치가 줄어들 수 있습니다.

  • 적합한 단일 문자 접두사는 한정되어 있으며, 새 참조에 대해서는 더 이상 허용되지 않습니다.

  • 일관된 패턴을 따르면 사용자가 새 기능의 존재를 추론할 수 있습니다.

확장 가능한 참조 필터(Extensible reference filters) 에픽에서 이 형식의 사용에 대해 설명합니다.

이름과 ID를 모두 가진 새 객체 apple에 대한 참조 접두사를 추가하려면, 참조 형식을 다음과 같이 지정하세요:

  • ID로 식별하는 경우: [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

이렇게 하면 쿼리 수가 프로젝트 수에 비례하여 선형적으로 유지됩니다. 참조 필터에서 records_per_parent를 호출할 때만 parent_records 메서드를 구현하면 됩니다.

노드 필터링 최적화#

ReferenceFilter는 문서의 모든 <a>text() 노드를 순회합니다.

모든 노드가 처리되는 것은 아니며, 문서는 처리하려는 노드에 대해서만 필터링됩니다. 다음 항목은 건너뜁니다:

  • 이전 필터에서 이미 처리된 링크 태그(gfm 클래스가 있는 경우).

  • 무시하려는 조상 노드(ignore_ancestor_query)가 있는 노드.

  • 빈 줄.

  • href 속성이 비어 있는 링크 태그.

ReferenceFilter마다 이러한 노드를 필터링하지 않기 위해, 한 번만 수행하고 결과를 파이프라인의 result 해시에 result[:reference_filter_nodes]로 저장합니다.

파이프라인 result는 수정을 위해 각 필터에 전달되므로, ReferenceFilter가 텍스트나 링크 태그를 대체할 때마다 필터링된 목록(reference_filter_nodes)이 다음 필터가 사용할 수 있도록 업데이트됩니다.

참조 파서#

여러 경우에, 성능 최적화로서 마크다운을 HTML로 한 번 렌더링하고, 결과를 캐시한 후 캐시된 값에서 사용자에게 표시합니다. 예를 들어 노트, 이슈 설명, 머지 리퀘스트 설명에서 이러한 일이 발생합니다. 이로 인해 렌더링된 문서가 일부 이후 독자가 볼 수 없어야 하는 리소스를 참조할 수 있습니다.

예를 들어, 이슈를 생성하고 접근 권한이 있는 기밀 이슈 #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에서 상속하며, 다음을 정의하여 구현됩니다:

  • ReferenceFilter.reference_type과 같아야 하는 .reference_type.

  • 그리고 다음 중 하나 이상을 구현하여:

가장 세밀한 제어를 위한 #nodes_visible_to_user(user, nodes).

  • nodes_visible_to_user가 재정의되지 않은 경우 필요한 #can_read_reference?.

  • ID별 객체에 대한 active record relation인 #references_relation.

  • 노드를 직접 필터링하는 #nodes_user_can_reference(user, nodes).

각 참조 유형에 대해 이 클래스를 구현하지 않으면 마크다운 처리 중 애플리케이션에서 예외가 발생합니다.

참조 처리

GitLab v19.1
원문 보기
요약

GitLab Flavored Markdown에는 다양한 GitLab 도메인 객체에 대한 참조를 처리하는 기능이 포함되어 있습니다. 각 ReferenceFilter에는 대응하는 ReferenceParser가 있어야 합니다.

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

ReferenceFilter에는 대응하는 ReferenceParser가 있어야 합니다.

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

Banzai 파이프라인#

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

result 해시는 수정을 위해 각 필터에 전달됩니다. 여기서 필터는 콘텐츠에서 추출한 정보를 저장합니다. 다음 내용이 포함됩니다:

  • 파이프라인의 마지막 필터 출력을 기반으로 한 DocumentFragment 또는 String HTML 마크업이 담긴 :output 키.

  • 파이프라인의 각 필터에 의해 업데이트되며, 처리 준비가 된 DocumentFragment nodes 목록이 담긴 :reference_filter_nodes 키.

참조 필터#

참조가 처리되는 첫 번째 방법은 참조 필터입니다. 이는 마크업 문서에서 단축 코드 및 URI 참조를 식별하고, 이를 해당 리소스에 대한 구조화된 링크로 변환하는 도구입니다.

예를 들어, 클래스 Banzai::Filter::References::IssueReferenceFiltergitlab-org/gitlab#123https://gitlab.com/gitlab-org/gitlab/-/issues/200048과 같은 이슈 참조를 처리하는 역할을 합니다.

모든 참조 필터는 HTML::Pipeline::Filter의 인스턴스이며, Banzai::Filter::References::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)와 식별자가 주어졌을 때 객체를 찾습니다. 예를 들어, 머지 리퀘스트에 대한 참조 필터에서는 project.merge_requests.where(iid: iid)가 될 수 있습니다.

새 참조 접두사 및 필터 추가#

새 객체에 대한 참조 필터는 [object_type:identifier] 패턴을 따르는 형식을 사용하세요. 이유는 다음과 같습니다:

  • 다양한 단일 문자 접두사는 사용자가 추적하기 어렵습니다. 특히 사용 빈도가 낮은 객체 유형의 경우, 기능의 가치가 줄어들 수 있습니다.

  • 적합한 단일 문자 접두사는 한정되어 있으며, 새 참조에 대해서는 더 이상 허용되지 않습니다.

  • 일관된 패턴을 따르면 사용자가 새 기능의 존재를 추론할 수 있습니다.

확장 가능한 참조 필터(Extensible reference filters) 에픽에서 이 형식의 사용에 대해 설명합니다.

이름과 ID를 모두 가진 새 객체 apple에 대한 참조 접두사를 추가하려면, 참조 형식을 다음과 같이 지정하세요:

  • ID로 식별하는 경우: [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

이렇게 하면 쿼리 수가 프로젝트 수에 비례하여 선형적으로 유지됩니다. 참조 필터에서 records_per_parent를 호출할 때만 parent_records 메서드를 구현하면 됩니다.

노드 필터링 최적화#

ReferenceFilter는 문서의 모든 <a>text() 노드를 순회합니다.

모든 노드가 처리되는 것은 아니며, 문서는 처리하려는 노드에 대해서만 필터링됩니다. 다음 항목은 건너뜁니다:

  • 이전 필터에서 이미 처리된 링크 태그(gfm 클래스가 있는 경우).

  • 무시하려는 조상 노드(ignore_ancestor_query)가 있는 노드.

  • 빈 줄.

  • href 속성이 비어 있는 링크 태그.

ReferenceFilter마다 이러한 노드를 필터링하지 않기 위해, 한 번만 수행하고 결과를 파이프라인의 result 해시에 result[:reference_filter_nodes]로 저장합니다.

파이프라인 result는 수정을 위해 각 필터에 전달되므로, ReferenceFilter가 텍스트나 링크 태그를 대체할 때마다 필터링된 목록(reference_filter_nodes)이 다음 필터가 사용할 수 있도록 업데이트됩니다.

참조 파서#

여러 경우에, 성능 최적화로서 마크다운을 HTML로 한 번 렌더링하고, 결과를 캐시한 후 캐시된 값에서 사용자에게 표시합니다. 예를 들어 노트, 이슈 설명, 머지 리퀘스트 설명에서 이러한 일이 발생합니다. 이로 인해 렌더링된 문서가 일부 이후 독자가 볼 수 없어야 하는 리소스를 참조할 수 있습니다.

예를 들어, 이슈를 생성하고 접근 권한이 있는 기밀 이슈 #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에서 상속하며, 다음을 정의하여 구현됩니다:

  • ReferenceFilter.reference_type과 같아야 하는 .reference_type.

  • 그리고 다음 중 하나 이상을 구현하여:

가장 세밀한 제어를 위한 #nodes_visible_to_user(user, nodes).

  • nodes_visible_to_user가 재정의되지 않은 경우 필요한 #can_read_reference?.

  • ID별 객체에 대한 active record relation인 #references_relation.

  • 노드를 직접 필터링하는 #nodes_user_can_reference(user, nodes).

각 참조 유형에 대해 이 클래스를 구현하지 않으면 마크다운 처리 중 애플리케이션에서 예외가 발생합니다.