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
100 changes: 95 additions & 5 deletions confluence-mdx/bin/reverse_sync/patch_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
RoundtripSidecar,
SidecarBlock,
find_mapping_by_sidecar,
find_sidecar_block_by_identity,
sha256_text,
SidecarEntry,
)
from reverse_sync.lost_info_patcher import apply_lost_info, distribute_lost_info_to_mappings
from reverse_sync.mdx_to_xhtml_inline import mdx_block_to_xhtml_element, mdx_block_to_inner_xhtml
from reverse_sync.reconstructors import (
sidecar_block_requires_reconstruction,
reconstruct_fragment_with_sidecar,
)
from reverse_sync.list_patcher import (
build_list_item_patches,
)
Expand Down Expand Up @@ -90,10 +96,13 @@ def _emit_replacement_fragment(block: MdxBlock) -> str:
def _build_replace_fragment_patch(
mapping: BlockMapping,
new_block: MdxBlock,
sidecar_block: Optional[SidecarBlock] = None,
mapping_lost_info: Optional[dict] = None,
) -> Dict[str, str]:
"""whole-fragment replacement patch를 생성한다."""
new_element = _emit_replacement_fragment(new_block)
if sidecar_block_requires_reconstruction(sidecar_block):
new_element = reconstruct_fragment_with_sidecar(new_element, sidecar_block)
block_lost = (mapping_lost_info or {}).get(mapping.block_id, {})
if block_lost:
new_element = apply_lost_info(new_element, block_lost)
Expand All @@ -104,6 +113,57 @@ def _build_replace_fragment_patch(
}


def _find_roundtrip_sidecar_block(
change: BlockChange,
mapping: Optional[BlockMapping],
roundtrip_sidecar: Optional[RoundtripSidecar],
xpath_to_sidecar_block: Dict[str, SidecarBlock],
) -> Optional[SidecarBlock]:
"""xpath → identity hash 순으로 roundtrip sidecar block을 탐색한다.

1. xpath로 빠른 조회
2. mdx_content_hash + mdx_line_range로 검증 → 일치하면 확정 반환
3. 검증 실패 시 find_sidecar_block_by_identity로 더 정확한 블록 탐색
4. identity도 없으면 xpath 결과를 fallback으로 반환
"""
if roundtrip_sidecar is None:
return None

identity_block = change.old_block or change.new_block

# xpath 조회
xpath_match: Optional[SidecarBlock] = None
if mapping is not None:
xpath_match = xpath_to_sidecar_block.get(mapping.xhtml_xpath)

# hash + line range 검증 → 확정 일치
if xpath_match is not None and identity_block is not None:
expected_hash = sha256_text(identity_block.content) if identity_block.content else ""
expected_range = (identity_block.line_start, identity_block.line_end)
if (
xpath_match.mdx_content_hash == expected_hash
and tuple(xpath_match.mdx_line_range) == expected_range
):
return xpath_match

# identity fallback: mapping.yaml이 어긋난 경우 hash 기반으로 재탐색
# xpath 태그 타입(p, ul, ol, table 등)이 일치하는 경우에만 반환하여 cross-type 오매칭 방지
if identity_block is not None and identity_block.content:
identity_match = find_sidecar_block_by_identity(
roundtrip_sidecar.blocks,
sha256_text(identity_block.content),
(identity_block.line_start, identity_block.line_end),
)
if identity_match is not None:
mapping_tag = mapping.xhtml_xpath.split('[')[0] if mapping else ''
identity_tag = identity_match.xhtml_xpath.split('[')[0] if identity_match.xhtml_xpath else ''
if mapping_tag == identity_tag:
return identity_match

# xpath 결과를 마지막 fallback으로 반환 (hash 불일치라도 없는 것보다 나음)
return xpath_match


def _flush_containing_changes(
containing_changes: dict,
used_ids: 'set | None' = None,
Expand Down Expand Up @@ -252,7 +312,7 @@ def _mark_used(block_id: str, m: BlockMapping):
_build_replace_fragment_patch(
mapping,
add_change.new_block,
mapping_lost_info,
mapping_lost_info=mapping_lost_info,
)
)
_paired_indices.add(idx)
Expand Down Expand Up @@ -307,6 +367,22 @@ def _mark_used(block_id: str, m: BlockMapping):
continue

if strategy == 'list':
list_sidecar = _find_roundtrip_sidecar_block(
change, mapping, roundtrip_sidecar, xpath_to_sidecar_block,
)
if (mapping is not None
and not _contains_preserved_anchor_markup(mapping.xhtml_text)
and sidecar_block_requires_reconstruction(list_sidecar)):
_mark_used(mapping.block_id, mapping)
patches.append(
_build_replace_fragment_patch(
mapping,
change.new_block,
sidecar_block=list_sidecar,
mapping_lost_info=mapping_lost_info,
)
)
continue
patches.extend(
build_list_item_patches(
change, mappings, used_ids,
Expand All @@ -321,7 +397,7 @@ def _mark_used(block_id: str, m: BlockMapping):
_build_replace_fragment_patch(
mapping,
change.new_block,
mapping_lost_info,
mapping_lost_info=mapping_lost_info,
)
)
else:
Expand Down Expand Up @@ -350,13 +426,15 @@ def _mark_used(block_id: str, m: BlockMapping):
and collapse_ws(new_plain) == collapse_ws(mapping.xhtml_plain_text)):
continue

sidecar_block = xpath_to_sidecar_block.get(mapping.xhtml_xpath)
sidecar_block = _find_roundtrip_sidecar_block(
change, mapping, roundtrip_sidecar, xpath_to_sidecar_block,
)
if _can_replace_table_fragment(change, mapping, roundtrip_sidecar):
patches.append(
_build_replace_fragment_patch(
mapping,
change.new_block,
mapping_lost_info,
mapping_lost_info=mapping_lost_info,
)
)
continue
Expand All @@ -366,7 +444,19 @@ def _mark_used(block_id: str, m: BlockMapping):
_build_replace_fragment_patch(
mapping,
change.new_block,
mapping_lost_info,
sidecar_block=sidecar_block,
mapping_lost_info=mapping_lost_info,
)
)
continue

if sidecar_block_requires_reconstruction(sidecar_block):
patches.append(
_build_replace_fragment_patch(
mapping,
change.new_block,
sidecar_block=sidecar_block,
mapping_lost_info=mapping_lost_info,
)
)
continue
Expand Down
Loading
Loading