diff --git a/confluence-mdx/bin/reverse_sync/xhtml_patcher.py b/confluence-mdx/bin/reverse_sync/xhtml_patcher.py index 2760128ce..e7663d740 100644 --- a/confluence-mdx/bin/reverse_sync/xhtml_patcher.py +++ b/confluence-mdx/bin/reverse_sync/xhtml_patcher.py @@ -617,17 +617,23 @@ def _apply_text_changes(element: Tag, old_text: str, new_text: str): exclude_insert_at_start=exclude_at_start, ) - # 직전 노드 범위와 현재 노드 범위 사이의 gap이 diff로 삭제된 경우, + # 직전 노드 범위와 현재 노드 범위 사이의 gap이 diff로 삭제/축소된 경우, # leading whitespace를 제거한다. - # 예: IDENTIFIER 조사 → IDENTIFIER조사 (공백 교정) + # 예1: IDENTIFIER 조사 → IDENTIFIER조사 (공백 삭제) + # 예2:

Admin →

Admin (공백 축소: gap 2→1이면 leading 제거) if leading and i > 0: prev_end = node_ranges[i - 1][1] if prev_end < node_start: + gap_old = old_stripped[prev_end:node_start] gap_new = _map_text_range( old_stripped, new_stripped, opcodes, prev_end, node_start ) if not gap_new: leading = '' + elif (gap_new.isspace() and gap_old.isspace() + and len(gap_new) < len(gap_old) + and len(gap_new) <= len(leading)): + leading = '' if trailing_in_range: # diff가 trailing whitespace를 처리했으므로 별도 보존 불필요 diff --git a/confluence-mdx/tests/test_reverse_sync_xhtml_patcher.py b/confluence-mdx/tests/test_reverse_sync_xhtml_patcher.py index 697a6d58e..7cf4d09b1 100644 --- a/confluence-mdx/tests/test_reverse_sync_xhtml_patcher.py +++ b/confluence-mdx/tests/test_reverse_sync_xhtml_patcher.py @@ -596,3 +596,66 @@ def test_only_one_of_duplicate_shortcodes_is_replaced(self): assert len(soup.find_all('ac:emoticon')) == 1 assert 'A 확인' in result + + +class TestGapWhitespaceReduction: + """텍스트 노드 사이 gap 공백이 축소될 때 leading whitespace 처리 테스트.""" + + def test_li_p_leading_space_removed_when_gap_reduced(self): + """

trailing space + 내부

leading space → 2공백 gap 축소 시 leading 제거. + + XHTML 구조:

...정의합니다.

+ old_plain_text에서 gap이 2공백(" "), new에서 1공백(" ")으로 축소될 때 + 내부

의 leading space가 제거되어야 한다. + (FC가 leading space를 보존하면 "* ·Admin" → "*··Admin" 이중 공백이 됨) + """ + xhtml = ( + '

    ' + '
  1. Allowed Zones : 정의합니다.

    ' + '' + '
  2. ' + '
' + ) + patches = [{ + 'xhtml_xpath': 'ol[1]', + # trailing " " + leading " " = gap 2 + 'old_plain_text': 'Allowed Zones : 정의합니다. Admin 매핑합니다.', + # gap 2 → 1 + 'new_plain_text': 'Allowed Zones : 정의합니다. Admin 매핑합니다.', + }] + result = patch_xhtml(xhtml, patches) + assert '

Admin' in result, ( + f"leading space not removed when gap reduced: {result}" + ) + + def test_gap_fully_deleted(self): + """gap이 완전히 삭제되면 기존 동작대로 leading을 제거한다.""" + xhtml = '

IDENTIFIER 조사

' + patches = [{ + 'xhtml_xpath': 'p[1]', + 'old_plain_text': 'IDENTIFIER 조사', + 'new_plain_text': 'IDENTIFIER조사', + }] + result = patch_xhtml(xhtml, patches) + assert 'IDENTIFIER조사' in result + + def test_gap_not_reduced_preserves_leading(self): + """gap이 축소되지 않으면 leading whitespace를 보존한다.""" + xhtml = ( + '
    ' + '
  1. 텍스트.

    ' + '' + '
  2. ' + '
' + ) + patches = [{ + 'xhtml_xpath': 'ol[1]', + 'old_plain_text': '텍스트. 내용입니다.', + # gap 크기 동일 (2→2), 텍스트만 변경 + 'new_plain_text': '텍스트. 내용변경.', + }] + result = patch_xhtml(xhtml, patches) + # gap이 축소되지 않았으므로 leading space 보존 + assert '

내용변경.' in result, ( + f"leading space should be preserved when gap not reduced: {result}" + )