Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions confluence-mdx/bin/reverse_sync/roundtrip_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,59 @@ def _normalize_trailing_ws(text: str) -> str:
return re.sub(r'[ \t]+$', '', text, flags=re.MULTILINE)


def _normalize_consecutive_spaces_in_text(text: str) -> str:
"""코드 블록 외 텍스트에서 인라인 연속 공백을 단일 공백으로 정규화한다.

Forward converter가 XHTML에서 생성한 MDX는 단일 공백만 사용하므로,
improved.mdx의 이중 공백(예: **bold** :, * `item`)과의 차이를 무시한다.
줄 앞 들여쓰기(leading whitespace)는 보존한다.

fenced code block(```) 내부는 변경하지 않는다.
인라인 code span(` `` `) 내부의 연속 공백도 정규화 대상에 포함된다.
이는 HTML 렌더링 시 <code>a b</code>와 <code>a b</code>가 동일하게
표시되는 것과 일치한다.
"""
lines = text.split('\n')
result = []
in_code_block = False
for line in lines:
stripped = line.strip()
if stripped.startswith('```'):
in_code_block = not in_code_block
result.append(line)
continue
if in_code_block:
result.append(line)
continue
leading = len(line) - len(line.lstrip(' \t'))
rest = re.sub(r' {2,}', ' ', line[leading:])
result.append(line[:leading] + rest)
return '\n'.join(result)


def _normalize_br_space(text: str) -> str:
"""<br/> 앞의 공백을 제거한다.

Forward converter가 list item 구성 시 ' '.join(li_itself)로
<br/> 앞에 공백을 추가하므로, 비교 시 이를 제거한다.
"""
return re.sub(r' +(<br\s*/>)', r'\1', text)


def _apply_minimal_normalizations(text: str) -> str:
"""항상 적용하는 최소 정규화 (strict/lenient 모드 공통).

forward converter의 체계적 출력 특성에 의한 차이만 처리한다:
- 인라인 이중 공백 → 단일 공백 (_normalize_consecutive_spaces_in_text)
- <br/> 앞 공백 제거 (_normalize_br_space)

lenient 모드에서는 이 정규화 이후 _apply_normalizations가 추가로 적용된다.
"""
text = _normalize_consecutive_spaces_in_text(text)
text = _normalize_br_space(text)
return text


_MONTH_KO_TO_EN = {
'01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
'05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
Expand Down Expand Up @@ -194,11 +247,11 @@ def verify_roundtrip(
) -> VerifyResult:
"""두 MDX 문자열의 일치를 검증한다.

기본 동작(엄격 모드): 정규화 없이 문자 그대로 비교한다.
모든 공백, 줄바꿈, 문자가 동일해야 PASS.
기본 동작(엄격 모드): forward converter의 체계적 차이(이중 공백, <br/> 앞 공백)를
정규화한 후 문자 그대로 비교한다. 그 외 공백, 줄바꿈, 문자가 동일해야 PASS.

lenient=True(관대 모드): trailing whitespace, 날짜 형식 등 XHTML↔MDX 변환기
한계에 의한 차이를 정규화한 후 전체 행에서 exact match를 검증한다.
lenient=True(관대 모드): trailing whitespace, 날짜 형식, 테이블 패딩 등
XHTML↔MDX 변환기 한계에 의한 추가 차이를 정규화한 후 exact match를 검증한다.

Args:
expected_mdx: 개선 MDX (의도한 결과)
Expand All @@ -208,6 +261,10 @@ def verify_roundtrip(
Returns:
VerifyResult: passed=True면 통과, 아니면 diff_report 포함
"""
# 항상 최소 정규화 적용 (forward converter 특성에 의한 체계적 차이 처리)
expected_mdx = _apply_minimal_normalizations(expected_mdx)
actual_mdx = _apply_minimal_normalizations(actual_mdx)

if lenient:
expected_mdx = _apply_normalizations(expected_mdx)
actual_mdx = _apply_normalizations(actual_mdx)
Expand Down
14 changes: 7 additions & 7 deletions confluence-mdx/tests/reverse-sync/pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
description: '**예시:** (이중 공백) 형태가 라운드트립 후 **예시:** (단일 공백)으로 변환됨.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: 'Bold 뒤 이중 공백 소실 (예시: → 예시:)'
mdx_path: administrator-manual/general/company-management/general.mdx
Expand Down Expand Up @@ -183,7 +183,7 @@
description: '**Request Header (JSON)** : 형태가 **Request Header (JSON)** : 로 변환됨.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: Bold 뒤 이중 공백 소실 (Request Header (JSON) :)
mdx_path: administrator-manual/general/company-management/channels.mdx
Expand Down Expand Up @@ -380,7 +380,7 @@
description: '**Enable Attribute Synchronization :** (이중 공백) 및 **Dry Run :** 형태가 단일 공백으로 변환됨. 2개 항목 영향.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: 'Bold 뒤 이중 공백 소실 (Enable Attribute Synchronization : , Dry Run : )'
mdx_path: administrator-manual/general/user-management/authentication/integrating-with-ldap.mdx
Expand Down Expand Up @@ -743,7 +743,7 @@
description: '* `+Add` 버튼 형태에서 목록 마커 뒤 이중 공백이 단일 공백으로 변환됨. 또한 **Access Token Timeout (Minutes)** : 형태의 이중 공백도 소실됨.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: 목록 항목 앞 이중 공백 소실 (* `+Add` → * `+Add`)
mdx_path: administrator-manual/general/system/integrations/oauth-client-application.mdx
Expand Down Expand Up @@ -775,7 +775,7 @@
동일 패턴)

'
expected_status: fail
expected_status: pass
failure_type: 13
label: 'Bold 뒤 이중 공백 소실 (Enable Attribute Synchronization : )'
mdx_path: administrator-manual/general/system/integrations/identity-providers.mdx
Expand Down Expand Up @@ -1113,7 +1113,7 @@
description: '팝업이 나타나면 *DELETE* 문구 형태에서 이탤릭 앞뒤 이중 공백이 단일 공백으로 변환됨.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: Italic 앞뒤 공백 소실 ( *DELETE* → *DELETE*)
mdx_path: administrator-manual/kubernetes/connection-management/cloud-providers.mdx
Expand Down Expand Up @@ -1201,7 +1201,7 @@
description: '**(Title)** : 형태가 **(Title)** : 로 변환됨.

'
expected_status: fail
expected_status: pass
failure_type: 13
label: Bold 뒤 이중 공백 소실 ((Title) :)
mdx_path: administrator-manual/kubernetes/k8s-access-control/policies.mdx
Expand Down
98 changes: 97 additions & 1 deletion confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import pytest
from reverse_sync.roundtrip_verifier import verify_roundtrip, VerifyResult
from reverse_sync.roundtrip_verifier import (
verify_roundtrip,
VerifyResult,
_normalize_consecutive_spaces_in_text,
_normalize_br_space,
)


def test_identical_mdx_passes():
Expand Down Expand Up @@ -87,3 +92,94 @@ def test_strict_mode_fails_on_date_format():
actual_mdx="Jan 15, 2024\n",
)
assert result.passed is False


# --- _normalize_consecutive_spaces_in_text 단위 테스트 ---


class TestNormalizeConsecutiveSpaces:
def test_double_space_collapsed(self):
"""이중 공백이 단일 공백으로 정규화된다."""
assert _normalize_consecutive_spaces_in_text("**bold** :") == "**bold** :"

def test_leading_indent_preserved(self):
"""줄 앞 들여쓰기는 보존된다."""
assert _normalize_consecutive_spaces_in_text(" * item") == " * item"

def test_code_block_preserved(self):
"""코드 블록 내부 연속 공백은 보존된다."""
text = "```\ncode with spaces\n```"
assert _normalize_consecutive_spaces_in_text(text) == text

def test_code_block_with_surrounding_text(self):
"""코드 블록 전후 일반 텍스트의 이중 공백은 정규화되고, 블록 내부는 보존된다."""
text = "**bold** before\n```\ncode inside\n```\n**bold** after"
expected = "**bold** before\n```\ncode inside\n```\n**bold** after"
assert _normalize_consecutive_spaces_in_text(text) == expected

def test_inline_code_span_spaces_collapsed(self):
"""인라인 code span 내부 연속 공백도 정규화 대상이다 (HTML 렌더링과 일치)."""
assert _normalize_consecutive_spaces_in_text("`code here`") == "`code here`"

def test_single_space_unchanged(self):
"""단일 공백은 변경되지 않는다."""
assert _normalize_consecutive_spaces_in_text("a b c") == "a b c"

def test_three_spaces_collapsed_to_one(self):
"""3개 이상의 공백도 단일 공백으로 정규화된다."""
assert _normalize_consecutive_spaces_in_text("a b") == "a b"


# --- _normalize_br_space 단위 테스트 ---


class TestNormalizeBrSpace:
def test_space_before_br_removed(self):
"""<br/> 앞의 공백이 제거된다."""
assert _normalize_br_space("text <br/>") == "text<br/>"

def test_multiple_spaces_before_br_removed(self):
"""<br/> 앞의 여러 공백도 제거된다."""
assert _normalize_br_space("text <br/>") == "text<br/>"

def test_br_with_space_inside_tag(self):
"""<br /> 형식도 처리한다."""
assert _normalize_br_space("text <br />") == "text<br />"

def test_no_space_before_br_unchanged(self):
"""<br/> 앞에 공백 없으면 변경하지 않는다."""
assert _normalize_br_space("text<br/>") == "text<br/>"

def test_text_after_br_unchanged(self):
"""<br/> 뒤의 텍스트는 영향받지 않는다."""
assert _normalize_br_space("a <br/>b") == "a<br/>b"


# --- verify_roundtrip에서의 최소 정규화 동작 ---


def test_minimal_norm_double_space_passes():
"""이중 공백 차이는 strict 모드에서도 정규화된다 (forward converter 특성)."""
result = verify_roundtrip(
expected_mdx="**bold** : value\n",
actual_mdx="**bold** : value\n",
)
assert result.passed is True


def test_minimal_norm_br_space_passes():
"""<br/> 앞 공백 차이는 strict 모드에서도 정규화된다."""
result = verify_roundtrip(
expected_mdx="item <br/>next\n",
actual_mdx="item<br/>next\n",
)
assert result.passed is True


def test_strict_mode_still_fails_on_trailing_ws():
"""strict 모드: trailing whitespace 차이는 여전히 실패한다 (최소 정규화 대상 아님)."""
result = verify_roundtrip(
expected_mdx="Paragraph. \n",
actual_mdx="Paragraph.\n",
)
assert result.passed is False
Loading