diff --git a/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py b/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py index 1bf3e38e9..f52a7caf7 100644 --- a/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py +++ b/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py @@ -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 렌더링 시 a ba b가 동일하게 + 표시되는 것과 일치한다. + """ + 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: + """
앞의 공백을 제거한다. + + Forward converter가 list item 구성 시 ' '.join(li_itself)로 +
앞에 공백을 추가하므로, 비교 시 이를 제거한다. + """ + return re.sub(r' +()', r'\1', text) + + +def _apply_minimal_normalizations(text: str) -> str: + """항상 적용하는 최소 정규화 (strict/lenient 모드 공통). + + forward converter의 체계적 출력 특성에 의한 차이만 처리한다: + - 인라인 이중 공백 → 단일 공백 (_normalize_consecutive_spaces_in_text) + -
앞 공백 제거 (_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', @@ -194,11 +247,11 @@ def verify_roundtrip( ) -> VerifyResult: """두 MDX 문자열의 일치를 검증한다. - 기본 동작(엄격 모드): 정규화 없이 문자 그대로 비교한다. - 모든 공백, 줄바꿈, 문자가 동일해야 PASS. + 기본 동작(엄격 모드): forward converter의 체계적 차이(이중 공백,
앞 공백)를 + 정규화한 후 문자 그대로 비교한다. 그 외 공백, 줄바꿈, 문자가 동일해야 PASS. - lenient=True(관대 모드): trailing whitespace, 날짜 형식 등 XHTML↔MDX 변환기 - 한계에 의한 차이를 정규화한 후 전체 행에서 exact match를 검증한다. + lenient=True(관대 모드): trailing whitespace, 날짜 형식, 테이블 패딩 등 + XHTML↔MDX 변환기 한계에 의한 추가 차이를 정규화한 후 exact match를 검증한다. Args: expected_mdx: 개선 MDX (의도한 결과) @@ -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) diff --git a/confluence-mdx/tests/reverse-sync/pages.yaml b/confluence-mdx/tests/reverse-sync/pages.yaml index ae6566efd..63d53796d 100644 --- a/confluence-mdx/tests/reverse-sync/pages.yaml +++ b/confluence-mdx/tests/reverse-sync/pages.yaml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py b/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py index fa199d368..30a327b7d 100644 --- a/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py +++ b/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py @@ -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(): @@ -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): + """
앞의 공백이 제거된다.""" + assert _normalize_br_space("text
") == "text
" + + def test_multiple_spaces_before_br_removed(self): + """
앞의 여러 공백도 제거된다.""" + assert _normalize_br_space("text
") == "text
" + + def test_br_with_space_inside_tag(self): + """
형식도 처리한다.""" + assert _normalize_br_space("text
") == "text
" + + def test_no_space_before_br_unchanged(self): + """
앞에 공백 없으면 변경하지 않는다.""" + assert _normalize_br_space("text
") == "text
" + + def test_text_after_br_unchanged(self): + """
뒤의 텍스트는 영향받지 않는다.""" + assert _normalize_br_space("a
b") == "a
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(): + """
앞 공백 차이는 strict 모드에서도 정규화된다.""" + result = verify_roundtrip( + expected_mdx="item
next\n", + actual_mdx="item
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