diff --git a/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py b/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py
index 9a733078b..7e90af9dd 100644
--- a/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py
+++ b/confluence-mdx/bin/reverse_sync/roundtrip_verifier.py
@@ -106,26 +106,50 @@ def _normalize_empty_bold(text: str) -> str:
def _normalize_empty_list_items(text: str) -> str:
- """내용 없는 번호 리스트 항목(예: `` 12.``)을 빈 줄로 치환한다.
+ """내용 없는 번호 리스트 항목(예: `` 12.``)을 줄째 제거한다.
Forward converter가 XHTML의 텍스트 없는 ``
`` (이미지만 포함)를
번호만 있는 항목(``12.``)으로 변환한다. 이 항목은 시각적으로 무의미하므로
improved.mdx에서 제거하더라도 XHTML 패치로 ```` 구조를 삭제할 수 없다.
- 양쪽을 빈 줄로 정규화하여 이 차이를 무시한다.
+ 줄 전체(newline 포함)를 제거하여 이 차이를 무시한다.
"""
- return re.sub(r'^([ \t]+)\d+\.\s*$', '', text, flags=re.MULTILINE)
+ return re.sub(r'^[ \t]+\d+\.\s*\n', '', text, flags=re.MULTILINE)
+
+
+def _normalize_consecutive_blank_lines(text: str) -> str:
+ """연속 빈 줄(3개 이상의 개행)을 단일 빈 줄(2개 개행)로 정규화한다.
+
+ Forward converter가 블록 요소 사이에 추가 빈 줄을 삽입하거나,
+ _normalize_empty_list_items가 줄을 제거한 뒤 남은 연속 빈 줄을
+ 정규화한다. MDX에서 빈 줄 수는 시각적으로 동일하다.
+ """
+ return re.sub(r'\n{3,}', '\n\n', text)
+
+
+def _normalize_blank_line_after_br(text: str) -> str:
+ """
로 끝나는 줄 뒤의 빈 줄을 제거한다.
+
+
자체가 줄바꿈을 생성하므로, 뒤따르는 빈 줄은 시각적으로
+ 무의미하다. improved.mdx에서 빈 리스트 번호(12.)를 제거한 뒤
+ 남은 빈 줄과 FC가 빈 줄 없이 출력하는 차이를 정규화한다.
+ """
+ return re.sub(r'(
\n)\n+', r'\1', text)
def _apply_minimal_normalizations(text: str) -> str:
"""항상 적용하는 최소 정규화 (strict/lenient 모드 공통).
forward converter의 체계적 출력 특성에 의한 차이만 처리한다:
- - 인라인 이중 공백 → 단일 공백 (_normalize_consecutive_spaces_in_text)
- -
앞 공백 제거 (_normalize_br_space)
- - 링크 텍스트 앞뒤 공백 제거 (_normalize_link_text_spacing)
- - 빈 bold 마커(****) 정규화 (_normalize_empty_bold)
- - 내용 없는 번호 리스트 항목 제거 (_normalize_empty_list_items)
- - 문장 경계 줄바꿈 결합 (_normalize_sentence_breaks)
+ - 인라인 이중 공백 → 단일 공백
+ -
앞 공백 제거
+ - 링크 텍스트 앞뒤 공백 제거
+ - 빈 bold 마커(****) 정규화
+ - 내용 없는 번호 리스트 항목 제거
+ - 연속 빈 줄 정규화
+ - 테이블 셀 패딩 정규화
+ - 문장 경계 줄바꿈 결합
+ - 첫 번째 h1 heading 제거
+ - trailing 빈 줄 정규화
lenient 모드에서는 이 정규화 이후 _apply_normalizations가 추가로 적용된다.
"""
@@ -134,6 +158,8 @@ def _apply_minimal_normalizations(text: str) -> str:
text = _normalize_link_text_spacing(text)
text = _normalize_empty_bold(text)
text = _normalize_empty_list_items(text)
+ text = _normalize_consecutive_blank_lines(text)
+ text = _normalize_blank_line_after_br(text)
text = _normalize_table_cell_padding(text)
text = _normalize_sentence_breaks(text)
text = _strip_first_heading(text)
diff --git a/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py b/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py
index 1fb1d72bd..01d55ca5e 100644
--- a/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py
+++ b/confluence-mdx/tests/test_reverse_sync_roundtrip_verifier.py
@@ -7,6 +7,9 @@
_normalize_link_text_spacing,
_normalize_sentence_breaks,
_normalize_table_cell_padding,
+ _normalize_empty_list_items,
+ _normalize_consecutive_blank_lines,
+ _normalize_blank_line_after_br,
)
@@ -190,6 +193,78 @@ def test_multiple_links_all_normalized(self):
assert result == "* [**A**](a) and [**B**](b)"
+# --- _normalize_empty_list_items 단위 테스트 ---
+
+
+class TestNormalizeEmptyListItems:
+ def test_removes_number_only_line(self):
+ """번호만 있는 리스트 항목(예: ' 12.')을 줄째 제거한다.
+
+ 재현: integrating-with-email.mdx — FC가 이미지만 포함된 를
+ ' 12.'로 변환하지만, improved.mdx에서는 제거됨.
+ """
+ text = " 11. item\n 12.\n 13. next\n"
+ assert _normalize_empty_list_items(text) == " 11. item\n 13. next\n"
+
+ def test_preserves_item_with_content(self):
+ """내용이 있는 리스트 항목은 제거하지 않는다."""
+ text = " 1. first item\n 2. second item\n"
+ assert _normalize_empty_list_items(text) == text
+
+ def test_removes_with_tab_indent(self):
+ """탭 들여쓰기인 경우에도 제거한다."""
+ text = "\t5.\n\t6. content\n"
+ assert _normalize_empty_list_items(text) == "\t6. content\n"
+
+
+# --- _normalize_consecutive_blank_lines 단위 테스트 ---
+
+
+class TestNormalizeConsecutiveBlankLines:
+ def test_triple_newline_collapsed(self):
+ """3개 이상 연속 개행을 2개로 정규화한다.
+
+ 재현: identity-providers.mdx, user-management.mdx — FC가 블록 사이에
+ 추가 빈 줄을 삽입하거나, empty list item 제거 후 연속 빈 줄이 남음.
+ """
+ text = "para1\n\n\npara2\n"
+ assert _normalize_consecutive_blank_lines(text) == "para1\n\npara2\n"
+
+ def test_double_newline_unchanged(self):
+ """2개 개행(단일 빈 줄)은 변경하지 않는다."""
+ text = "para1\n\npara2\n"
+ assert _normalize_consecutive_blank_lines(text) == text
+
+ def test_quadruple_newline_collapsed(self):
+ """4개 이상 연속 개행도 2개로 정규화한다."""
+ text = "para1\n\n\n\npara2\n"
+ assert _normalize_consecutive_blank_lines(text) == "para1\n\npara2\n"
+
+
+# --- _normalize_blank_line_after_br 단위 테스트 ---
+
+
+class TestNormalizeBlankLineAfterBr:
+ def test_removes_blank_after_br(self):
+ """
뒤의 빈 줄을 제거한다.
+
+ 재현: integrating-with-email.mdx — improved.mdx에서 빈 리스트 번호
+ 제거 후
바로 뒤에 빈 줄이 남음. FC는 빈 줄 없이 출력.
+ """
+ text = "text
\n\nnext\n"
+ assert _normalize_blank_line_after_br(text) == "text
\nnext\n"
+
+ def test_preserves_br_without_blank(self):
+ """
뒤에 빈 줄이 없으면 변경하지 않는다."""
+ text = "text
\nnext\n"
+ assert _normalize_blank_line_after_br(text) == text
+
+ def test_handles_self_closing_variants(self):
+ """
형태도 처리한다."""
+ text = "text
\n\nnext\n"
+ assert _normalize_blank_line_after_br(text) == "text
\nnext\n"
+
+
# --- _normalize_table_cell_padding 단위 테스트 ---
@@ -238,7 +313,7 @@ def test_minimal_norm_double_space_passes():
def test_minimal_norm_br_space_passes():
- """
앞 공백 차이는 strict 모드에서도 정규화된다."""
+ """
앞 공백 차이는 최소 정규화로 통과한다."""
result = verify_roundtrip(
expected_mdx="item
next\n",
actual_mdx="item
next\n",
@@ -274,6 +349,44 @@ def test_minimal_norm_link_spacing_multiple_items():
assert result.passed is True
+def test_minimal_norm_empty_list_item_passes():
+ """빈 리스트 번호 차이는 최소 정규화로 통과한다.
+
+ 재현: integrating-with-email.mdx — improved.mdx에서 '12.' 제거,
+ FC verify.mdx에서 '12.' 출력. 정규화로 양쪽 모두 해당 줄 제거.
+ """
+ result = verify_roundtrip(
+ expected_mdx=" 11. item\n 13. next\n",
+ actual_mdx=" 11. item\n 12.\n 13. next\n",
+ )
+ assert result.passed is True
+
+
+def test_minimal_norm_consecutive_blank_lines_passes():
+ """연속 빈 줄 차이는 최소 정규화로 통과한다.
+
+ 재현: identity-providers.mdx — FC가 블록 사이에 빈 줄을 추가 삽입.
+ """
+ result = verify_roundtrip(
+ expected_mdx="para1\n\npara2\n",
+ actual_mdx="para1\n\n\npara2\n",
+ )
+ assert result.passed is True
+
+
+def test_minimal_norm_blank_line_after_br_passes():
+ """
뒤 빈 줄 차이는 최소 정규화로 통과한다.
+
+ 재현: integrating-with-email.mdx — empty list item 제거 후
+
뒤에 빈 줄이 남는 차이.
+ """
+ result = verify_roundtrip(
+ expected_mdx="text
\n\nnext\n",
+ actual_mdx="text
\nnext\n",
+ )
+ assert result.passed is True
+
+
def test_strict_mode_still_fails_on_trailing_ws():
"""strict 모드: trailing whitespace 차이는 여전히 실패한다 (최소 정규화 대상 아님)."""
result = verify_roundtrip(