Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions confluence-mdx/bin/reverse_sync/list_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ def split_list_items(content: str) -> List[str]:
return items


def _get_ordered_list_start(content: str) -> Optional[int]:
"""MDX 리스트 콘텐츠에서 첫 번째 순서 번호를 반환한다."""
for line in content.split('\n'):
m = re.match(r'^\s*(\d+)\.\s+', line)
if m:
return int(m.group(1))
return None


def _regenerate_list_from_parent(
change: BlockChange,
parent: Optional[BlockMapping],
Expand Down Expand Up @@ -125,11 +134,16 @@ def _regenerate_list_from_parent(
change.new_block.content, change.new_block.type)
xhtml_text = transfer_text_changes(
old_plain, new_plain, parent.xhtml_plain_text)
return [{
fallback_patch: Dict[str, object] = {
'xhtml_xpath': parent.xhtml_xpath,
'old_plain_text': parent.xhtml_plain_text,
'new_plain_text': xhtml_text,
}]
}
old_start = _get_ordered_list_start(change.old_block.content)
new_start = _get_ordered_list_start(change.new_block.content)
if old_start is not None and new_start is not None and old_start != new_start:
fallback_patch['ol_start'] = new_start
return [fallback_patch]

new_inner = mdx_block_to_inner_xhtml(
change.new_block.content, change.new_block.type)
Expand All @@ -139,11 +153,16 @@ def _regenerate_list_from_parent(
if block_lost:
new_inner = apply_lost_info(new_inner, block_lost)

return [{
patch: Dict[str, object] = {
'xhtml_xpath': parent.xhtml_xpath,
'old_plain_text': parent.xhtml_plain_text,
'new_inner_xhtml': new_inner,
}]
}
old_start = _get_ordered_list_start(change.old_block.content)
new_start = _get_ordered_list_start(change.new_block.content)
if old_start is not None and new_start is not None and old_start != new_start:
patch['ol_start'] = new_start
return [patch]


def build_list_item_patches(
Expand Down
14 changes: 14 additions & 0 deletions confluence-mdx/bin/reverse_sync/xhtml_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ def patch_xhtml(xhtml: str, patches: List[Dict[str, str]]) -> str:
if current_plain_with_emoticons.strip() != old_text.strip():
continue
_replace_inner_html(element, patch['new_inner_xhtml'])
if 'ol_start' in patch and isinstance(element, Tag) and element.name == 'ol':
new_start = patch['ol_start']
if new_start == 1:
if 'start' in element.attrs:
del element['start']
else:
element['start'] = str(new_start)
else:
old_text = patch['old_plain_text']
new_text = patch['new_plain_text']
Expand All @@ -83,6 +90,13 @@ def patch_xhtml(xhtml: str, patches: List[Dict[str, str]]) -> str:
if current_plain_with_emoticons.strip() != old_text.strip():
continue
_apply_text_changes(element, old_text, new_text)
if 'ol_start' in patch and isinstance(element, Tag) and element.name == 'ol':
new_start = patch['ol_start']
if new_start == 1:
if 'start' in element.attrs:
del element['start']
else:
element['start'] = str(new_start)

result = str(soup)
result = _restore_cdata(result)
Expand Down
23 changes: 23 additions & 0 deletions confluence-mdx/tests/test_reverse_sync_list_patcher_ol_start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""_get_ordered_list_start 단위 테스트."""
from reverse_sync.list_patcher import _get_ordered_list_start


class TestGetOrderedListStart:
def test_starts_at_one(self):
assert _get_ordered_list_start('1. first\n2. second\n') == 1

def test_starts_at_three(self):
assert _get_ordered_list_start('3. first\n4. second\n') == 3

def test_starts_at_zero(self):
assert _get_ordered_list_start('0. zeroth\n1. first\n') == 0

def test_unordered_list_returns_none(self):
assert _get_ordered_list_start('* item\n* item2\n') is None

def test_empty_returns_none(self):
assert _get_ordered_list_start('') is None

def test_indented_ordered_list(self):
"""들여쓰기된 순서 목록도 인식한다."""
assert _get_ordered_list_start(' 5. first\n 6. second\n') == 5
49 changes: 49 additions & 0 deletions confluence-mdx/tests/test_reverse_sync_xhtml_patcher.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from bs4 import BeautifulSoup
from reverse_sync.xhtml_patcher import patch_xhtml


Expand Down Expand Up @@ -440,3 +441,51 @@ def test_list_patch_with_emoticon_uses_mapping_plain_text():
result = patch_xhtml(xhtml, patches)
assert '검색할 수 있습니다.' in result, f'리스트 텍스트 변경이 적용되지 않음: {result}'
assert '검색이 가능합니다.' not in result, f'기존 문구가 남아 있음: {result}'


class TestOlStartPatch:
"""<ol start="N"> 속성 변경 패치 테스트."""

def test_set_start_attribute_via_inner_xhtml(self):
"""new_inner_xhtml 경로에서 ol_start로 start 속성을 설정한다."""
xhtml = '<ol><li><p>first</p></li><li><p>second</p></li></ol>'
patches = [{
'xhtml_xpath': 'ol[1]',
'old_plain_text': 'firstsecond',
'new_inner_xhtml': '<li><p>updated first</p></li><li><p>second</p></li>',
'ol_start': 3,
}]
result = patch_xhtml(xhtml, patches)
soup = BeautifulSoup(result, 'html.parser')
ol = soup.find('ol')
assert ol['start'] == '3'
assert 'updated first' in result

def test_remove_start_attribute_when_ol_start_is_one(self):
"""ol_start=1이면 기존 start 속성을 제거한다."""
xhtml = '<ol start="3"><li><p>first</p></li></ol>'
patches = [{
'xhtml_xpath': 'ol[1]',
'old_plain_text': 'first',
'new_inner_xhtml': '<li><p>first item</p></li>',
'ol_start': 1,
}]
result = patch_xhtml(xhtml, patches)
soup = BeautifulSoup(result, 'html.parser')
ol = soup.find('ol')
assert ol.get('start') is None

def test_set_start_attribute_via_text_transfer(self):
"""텍스트 전이 경로에서도 ol_start가 start 속성으로 적용된다."""
xhtml = '<ol><li>first item</li></ol>'
patches = [{
'xhtml_xpath': 'ol[1]',
'old_plain_text': 'first item',
'new_plain_text': 'updated item',
'ol_start': 5,
}]
result = patch_xhtml(xhtml, patches)
soup = BeautifulSoup(result, 'html.parser')
ol = soup.find('ol')
assert ol['start'] == '5'
assert 'updated' in result
Loading