From 6716e2a0bf08c107d75f201420b1ef598d950735 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 3 Apr 2026 22:35:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(reverse=5Fsync):=20=5Fapply=5Ftext=5Fch?= =?UTF-8?q?anges=20gap=20=EC=B6=95=EC=86=8C=20=EC=8B=9C=20leading=20whites?= =?UTF-8?q?pace=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 텍스트 노드 사이 gap이 축소될 때(예: 2공백→1공백) leading whitespace가 보존되어 `

Admin` → FC → `* Admin` (이중 공백)이 되는 문제를 수정합니다. gap이 삭제(not gap_new)뿐 아니라 공백만으로 축소된 경우에도 leading을 제거하여 원래 diff 의도를 반영합니다. Co-Authored-By: Claude Opus 4.6 --- confluence-mdx/bin/reverse_sync/xhtml_patcher.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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를 처리했으므로 별도 보존 불필요 From f1131a06a434656bf44a3f9f61eac60917f15ff4 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 3 Apr 2026 22:56:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(reverse=5Fsync):=20gap=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=20=EC=B6=95=EC=86=8C=20=EC=8B=9C=20leading=20whitespa?= =?UTF-8?q?ce=20=EC=A0=9C=EA=B1=B0=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _apply_text_changes의 gap 축소 로직을 검증하는 3건의 테스트를 추가합니다: - gap 2공백→1공백 축소 시

leading space 제거 (버그 재현) - gap 완전 삭제 시 leading 제거 (기존 동작) - gap 미축소 시 leading 보존 (회귀 방지) Co-Authored-By: Claude Opus 4.6 --- .../tests/test_reverse_sync_xhtml_patcher.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) 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 : 정의합니다.

    ' + '
    • Admin 매핑합니다.

    ' + '
  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}" + )