diff --git a/confluence-mdx/bin/reverse_sync/patch_builder.py b/confluence-mdx/bin/reverse_sync/patch_builder.py index 252e42cf4..637910b3a 100644 --- a/confluence-mdx/bin/reverse_sync/patch_builder.py +++ b/confluence-mdx/bin/reverse_sync/patch_builder.py @@ -190,21 +190,19 @@ def _normalize_list_for_content_compare(content: str) -> str: 내용 변경 판정에서는 무시한다. 대신 항목 경계와 항목 내부의 실제 공백 수 차이는 그대로 보존해 no-op reflow와 가시 공백 변경을 구분한다. - 마커 뒤 공백 수도 보존한다. normalize_mdx_to_plain이 마커를 제거하므로 - 마커 뒤 공백 수를 별도로 접두어에 기록하여 ``* text``와 ``* text``를 구분한다. + 마커 뒤 공백 변경은 이 함수가 아닌 ``_has_marker_ws_change``로 별도 감지한다. """ marker_re = re.compile(r'^(\s*(?:\d+\.|[-*+]))(\s+)') lines = content.strip().split('\n') item_chunks: List[str] = [] current_chunk: List[str] = [] - current_marker_ws: str = '' def _flush_current() -> None: if not current_chunk: return plain = normalize_mdx_to_plain('\n'.join(current_chunk), 'list') if plain: - item_chunks.append(current_marker_ws + plain.replace('\n', ' ')) + item_chunks.append(plain.replace('\n', ' ').strip()) for line in lines: if not line.strip(): @@ -213,23 +211,39 @@ def _flush_current() -> None: if m: _flush_current() current_chunk = [line] - current_marker_ws = m.group(2) continue if re.match(r'^\s*(?:\d+\.(?:\s+|$)|[-*+]\s+)', line): _flush_current() current_chunk = [line] - current_marker_ws = '' continue if current_chunk: current_chunk.append(line) else: current_chunk = [line] - current_marker_ws = '' _flush_current() return '\n'.join(item_chunks) +def _has_marker_ws_change(old_content: str, new_content: str) -> bool: + """리스트 마커 뒤 공백 수가 변경되었는지 감지한다. + + ``_normalize_list_for_content_compare``는 텍스트 내용만 비교하므로 + 마커 뒤 공백 변경(``* text`` → ``* text``)은 여기서 별도로 감지한다. + """ + marker_re = re.compile(r'^(\s*(?:\d+\.|[-*+]))(\s+)') + + def _extract_marker_ws(content: str) -> List[str]: + result: List[str] = [] + for line in content.strip().split('\n'): + m = marker_re.match(line) + if m: + result.append(m.group(2)) + return result + + return _extract_marker_ws(old_content) != _extract_marker_ws(new_content) + + def _build_inline_fixups( old_content: str, new_content: str, @@ -1113,10 +1127,12 @@ def _mark_used(block_id: str, m: BlockMapping): _old_plain_raw = _normalize_list_for_content_compare(change.old_block.content) _new_plain_raw = _normalize_list_for_content_compare(change.new_block.content) has_content_change = _old_plain_raw != _new_plain_raw - # _apply_mdx_diff_to_xhtml에 전달할 기본값은 collapse_ws 적용: - # XHTML plain text에는 줄바꿈이 없으므로 clean list 정렬에는 공백 축약본이 맞다. - _old_plain = collapse_ws(_old_plain_raw) - _new_plain = collapse_ws(_new_plain_raw) + has_marker_ws_change = _has_marker_ws_change( + change.old_block.content, change.new_block.content) + # _apply_mdx_diff_to_xhtml 전달용: 항목간 \n을 공백으로 변환하고 + # 양쪽 공백을 제거하여 XHTML plain text와 정렬한다. + _old_plain = _old_plain_raw.replace('\n', ' ').strip() + _new_plain = _new_plain_raw.replace('\n', ' ').strip() # ol start 변경 감지: 숫자 목록의 시작 번호가 달라진 경우 _old_start = re.match(r'^\s*(\d+)\.', change.old_block.content) _new_start = re.match(r'^\s*(\d+)\.', change.new_block.content) @@ -1132,7 +1148,8 @@ def _mark_used(block_id: str, m: BlockMapping): block_type=change.old_block.type, ) has_inline_boundary = bool(inline_fixups) - has_any_change = has_content_change or has_ol_start_change or has_inline_boundary + has_any_change = (has_content_change or has_ol_start_change + or has_inline_boundary or has_marker_ws_change) should_replace_clean_list = ( mapping is not None and not _contains_preserved_anchor_markup(mapping.xhtml_text) diff --git a/confluence-mdx/tests/test_reverse_sync_patch_builder.py b/confluence-mdx/tests/test_reverse_sync_patch_builder.py index ef32f8ac6..cc2571c64 100644 --- a/confluence-mdx/tests/test_reverse_sync_patch_builder.py +++ b/confluence-mdx/tests/test_reverse_sync_patch_builder.py @@ -21,6 +21,7 @@ _extract_inline_markers, _find_roundtrip_sidecar_block, _has_inline_boundary_change, + _has_marker_ws_change, _normalize_list_for_content_compare, _resolve_mapping_for_change, build_patches, @@ -2972,13 +2973,13 @@ def test_image_anchor_list_keeps_collapsed_text_diff(self): class TestNormalizeListMarkerWhitespace: - """_normalize_list_for_content_compare: 마커 뒤 공백 차이를 보존하여 변경 감지.""" + """_normalize_list_for_content_compare: 텍스트 내용 변경만 감지하고 마커 공백은 무시.""" - def test_marker_ws_difference_detected(self): - """마커 뒤 공백 수가 다르면 정규화 결과가 다르다.""" + def test_marker_ws_difference_ignored(self): + """마커 뒤 공백 수가 달라도 정규화 결과는 같다.""" old = _normalize_list_for_content_compare("* 항목") new = _normalize_list_for_content_compare("* 항목") - assert old != new + assert old == new def test_same_content_same_result(self): """마커 공백이 같으면 정규화 결과도 같다.""" @@ -2992,20 +2993,41 @@ def test_text_only_change_detected(self): new = _normalize_list_for_content_compare("* 새것") assert old != new - def test_numbered_list_marker_ws(self): - """번호 리스트 마커 뒤 공백 차이.""" + def test_numbered_list_marker_ws_ignored(self): + """번호 리스트 마커 뒤 공백 차이는 무시한다.""" old = _normalize_list_for_content_compare("7. 생성이") new = _normalize_list_for_content_compare("7. 생성이") - assert old != new + assert old == new - def test_nested_list_marker_ws(self): - """중첩 리스트에서 하위 항목 마커 공백 차이.""" + def test_nested_list_marker_ws_ignored(self): + """중첩 리스트에서 하위 항목 마커 공백 차이는 무시한다.""" old = _normalize_list_for_content_compare("1. 상위\n * 하위") new = _normalize_list_for_content_compare("1. 상위\n * 하위") - assert old != new + assert old == new def test_text_and_marker_ws_change(self): - """텍스트와 마커 공백이 동시에 변경.""" + """텍스트와 마커 공백이 동시에 변경되면 텍스트 차이를 감지한다.""" old = _normalize_list_for_content_compare("* 원래 텍스트") new = _normalize_list_for_content_compare("* 새 텍스트") assert old != new + + +class TestHasMarkerWsChange: + """_has_marker_ws_change: 마커 뒤 공백 변경을 별도로 감지한다.""" + + def test_ws_difference_detected(self): + assert _has_marker_ws_change("* 항목", "* 항목") is True + + def test_same_ws_no_change(self): + assert _has_marker_ws_change("* 항목", "* 항목") is False + + def test_numbered_list_ws(self): + assert _has_marker_ws_change("7. 생성이", "7. 생성이") is True + + def test_nested_list_ws(self): + assert _has_marker_ws_change( + "1. 상위\n * 하위", "1. 상위\n * 하위") is True + + def test_text_change_same_ws(self): + """텍스트만 변경되고 마커 공백은 같으면 False.""" + assert _has_marker_ws_change("* 원래", "* 새것") is False