Skip to content

Commit 43dfb4b

Browse files
jk-kim0claude
andcommitted
confluence-mdx: inline fixup의 bold 경계 변경을 개선합니다
- FC close_sp: 구두점 뒤 불필요한 공백 억제 (CommonMark right-flanking delimiter) - patch_builder: inline_boundary를 has_any_change 게이팅에 추가 - xhtml_patcher: 공백 축소 비교로 이중 공백 XHTML 매칭 지원 - xhtml_patcher: <ac:>/<ri:> 포함 <p>에서 <strong> 경계만 직접 수정 - 테스트 fixture 및 sidecar 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d37a7b commit 43dfb4b

10 files changed

Lines changed: 348 additions & 283 deletions

File tree

confluence-mdx/bin/converter/core.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ def convert_recursively(self, node):
226226
# 다음 텍스트가 이미 공백으로 시작하면 close_sp로 인한 이중 공백 방지
227227
if close_sp and isinstance(node.next_sibling, NavigableString) and str(node.next_sibling)[:1] in (' ', '\t', '\n'):
228228
close_sp = ""
229+
# 다음 텍스트가 구두점으로 시작하면 close_sp 불필요
230+
# (CommonMark: **punct**punct 는 valid right-flanking delimiter)
231+
if close_sp and isinstance(node.next_sibling, NavigableString):
232+
_ns = str(node.next_sibling).lstrip()
233+
if _ns and _is_unicode_punctuation(_ns[0]):
234+
close_sp = ""
229235
self.markdown_lines.append(f"{open_sp}**")
230236
self.markdown_lines.append(inner)
231237
self.markdown_lines.append(f"**{close_sp}")
@@ -242,6 +248,11 @@ def convert_recursively(self, node):
242248
# 다음 텍스트가 이미 공백으로 시작하면 close_sp로 인한 이중 공백 방지
243249
if close_sp and isinstance(node.next_sibling, NavigableString) and str(node.next_sibling)[:1] in (' ', '\t', '\n'):
244250
close_sp = ""
251+
# 다음 텍스트가 구두점으로 시작하면 close_sp 불필요
252+
if close_sp and isinstance(node.next_sibling, NavigableString):
253+
_ns = str(node.next_sibling).lstrip()
254+
if _ns and _is_unicode_punctuation(_ns[0]):
255+
close_sp = ""
245256
self.markdown_lines.append(f"{open_sp}*")
246257
self.markdown_lines.append(inner)
247258
self.markdown_lines.append(f"*{close_sp}")

confluence-mdx/bin/reverse_sync/patch_builder.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,12 @@ def _mark_used(block_id: str, m: BlockMapping):
817817
_old_start and _new_start
818818
and int(_old_start.group(1)) != int(_new_start.group(1))
819819
)
820-
has_any_change = has_content_change or has_ol_start_change
820+
# 인라인 마커 경계 변경 감지 (bold/italic 경계 이동)를
821+
# replace_fragment 판단 전에 수행하여 clean list도 재생성하도록 함
822+
inline_fixups = _build_inline_fixups(
823+
change.old_block.content, change.new_block.content)
824+
has_inline_boundary = bool(inline_fixups)
825+
has_any_change = has_content_change or has_ol_start_change or has_inline_boundary
821826
should_replace_clean_list = (
822827
mapping is not None
823828
and not _contains_preserved_anchor_markup(mapping.xhtml_text)
@@ -845,10 +850,7 @@ def _mark_used(block_id: str, m: BlockMapping):
845850
# preserved anchor list: text-level 패치로 ac:/ri: XHTML 구조 보존
846851
# (_apply_mdx_diff_to_xhtml 경로)
847852
# 같은 부모의 다중 변경은 순차 집계한다 (이전 결과에 누적 적용)
848-
# 인라인 마커 경계 변경 감지 (bold/italic 경계 이동)
849-
inline_fixups = _build_inline_fixups(
850-
change.old_block.content, change.new_block.content)
851-
has_inline_boundary = bool(inline_fixups)
853+
# inline_fixups, has_inline_boundary는 상단에서 이미 계산됨
852854
if mapping is not None and (has_any_change or has_inline_boundary):
853855
bid = mapping.block_id
854856
if bid not in _text_change_patches:

confluence-mdx/bin/reverse_sync/xhtml_patcher.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,53 @@ def _find_child_in_element(parent: Tag, xpath_part: str):
291291
return None
292292

293293

294+
def _collapse_ws(s: str) -> str:
295+
"""연속 공백을 단일 공백으로 축소한다."""
296+
return re.sub(r'\s+', ' ', s).strip()
297+
298+
299+
def _apply_strong_boundary_fixup(p_tag: Tag, new_inner_xhtml: str):
300+
"""<ac:>/<ri:> 보존 시 <strong> 요소만 직접 수정하여 bold 경계를 교정한다.
301+
302+
innerHTML 전체 교체는 <ac:link> 등 preserved anchor를 파괴하므로,
303+
new_inner_xhtml에서 목표 <strong> 구조를 추출하고 기존 <p>의 <strong>
304+
텍스트만 갱신한 뒤, 경계 이동으로 빠져나온 문자를 인접 text node에 반영한다.
305+
"""
306+
new_soup = BeautifulSoup(new_inner_xhtml, 'html.parser')
307+
old_strongs = p_tag.find_all('strong')
308+
new_strongs = new_soup.find_all('strong')
309+
310+
if len(old_strongs) != len(new_strongs):
311+
return
312+
313+
for old_s, new_s in zip(old_strongs, new_strongs):
314+
old_text = old_s.get_text()
315+
new_text = new_s.get_text()
316+
if old_text == new_text:
317+
continue
318+
319+
old_s.clear()
320+
old_s.string = new_text
321+
322+
# bold 끝에서 빠져나온 문자를 다음 text node에 반영
323+
if old_text.startswith(new_text) and len(old_text) > len(new_text):
324+
removed_end = old_text[len(new_text):]
325+
nxt = old_s.next_sibling
326+
if isinstance(nxt, NavigableString):
327+
nxt.replace_with(NavigableString(removed_end + str(nxt)))
328+
else:
329+
old_s.insert_after(NavigableString(removed_end))
330+
331+
# bold 끝으로 흡수된 문자를 다음 text node에서 제거
332+
elif new_text.startswith(old_text) and len(new_text) > len(old_text):
333+
added_end = new_text[len(old_text):]
334+
nxt = old_s.next_sibling
335+
if isinstance(nxt, NavigableString):
336+
ns = str(nxt)
337+
if ns.startswith(added_end):
338+
nxt.replace_with(NavigableString(ns[len(added_end):]))
339+
340+
294341
def _apply_inline_fixups(element: Tag, fixups: list):
295342
"""인라인 마커 경계 변경을 DOM에 적용한다.
296343
@@ -307,18 +354,20 @@ def _apply_inline_fixups(element: Tag, fixups: list):
307354
current_match = 0
308355
if not old_plain:
309356
continue
357+
new_plain_collapsed = _collapse_ws(new_plain)
310358
for p_tag in element.find_all('p'):
311359
p_text = p_tag.get_text().strip()
312360
# _apply_text_changes 이후 <p> 텍스트는 new_plain으로 업데이트되므로
313361
# new_plain 기준으로만 매칭한다. old_plain도 허용하면 미변경 앞 항목을
314362
# 잘못 수정할 수 있다 (old_plain != new_plain인 경우).
315-
if p_text != new_plain:
363+
if _collapse_ws(p_text) != new_plain_collapsed:
316364
continue
317365
if current_match != match_index:
318366
current_match += 1
319367
continue
320368
p_html = str(p_tag)
321369
if '<ac:' in p_html or '<ri:' in p_html:
370+
_apply_strong_boundary_fixup(p_tag, new_inner)
322371
break
323372
_replace_inner_html(p_tag, new_inner)
324373
break

confluence-mdx/tests/reverse-sync/1454342158/improved.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ LDAP 서버로부터 사용자 정보 동기화를 실행하려는 경우 **Use
181181
* **Use an Attribute for Privilege Revoke**: 동기화 시 특정 Attribute에 따라 Privilege를 회수할지 여부를 선택합니다.
182182
* 특정 LDAP Attribute의 변경에 의해 자동으로 DAC Privilege를 회수하고자 하는 경우 이 옵션을 활성화하세요.
183183
* LDAP Attribute 입력 필드에 활성화 변경을 감지하려는 Attribute 이름을 입력합니다.
184-
* **Enable Attribute Synchronization :** LDAP 사용자 속성과 QueryPie 사용자 속성을 매핑하여 동기화할지 여부를 선택합니다.
184+
* **Enable Attribute Synchronization :** LDAP 사용자 속성과 QueryPie 사용자 속성을 매핑하여 동기화할지 여부를 선택합니다.
185185
* LDAP에서 관리 중인 사용자 속성을 QueryPie 내 Attribute와 자동으로 연동하고자 하는 경우, 해당 옵션을 활성화하시기 바랍니다.
186186
* 옵션 활성화 시, 하단에 LDAP Attribute Mapping UI가 표시되며 매핑 작업을 통해 연동할 LDAP Attribute와 QueryPie Attribute를 지정할 수 있습니다.
187187
* 단, 해당 기능은 Profile Editor(Admin&gt; General &gt; User Management &gt; Profile Editor)에서 Source Priority가 Inherit from profile source로 설정된 Attribute에 한해 적용됩니다.
@@ -361,7 +361,7 @@ Okta 상세설정 (1)
361361
* ACS Index : 0 ~ 2,147,483,647 사이의 값을 입력합니다. 기본값은 0 입니다.
362362

363363
<Callout type="info">
364-
**Assertion Consumer Service (ACS)** : SP(Service provider)에 위치한 특정 Endpoint(URL)로, IdP로부터 SAML Assertion을 수신하여 검증하고 사용자의 로그인을 처리하는 역할을 합니다.
364+
**Assertion Consumer Service (ACS)**: SP(Service provider)에 위치한 특정 Endpoint(URL)로, IdP로부터 SAML Assertion을 수신하여 검증하고 사용자의 로그인을 처리하는 역할을 합니다.
365365

366366
<br/>**Assertion Consumer Service Index의 필요성 및 역할**
367367

@@ -548,3 +548,4 @@ Custom Identity Provider는 인증 API 서버를 사용하는 특수한 경우
548548
* 0.0 ~ 1.0 사이의 값을 입력합니다. (기본값은 0.1)
549549
* 예) 기존 유저가 100명이고, Allowed User Deletion Rate Threshold 0.1 인 경우, 다시 동기화 하였을 때, 삭제된 유저가 10명 이상이면 동기화가 실패합니다.
550550
* 11.3.0 이전 버전에서 동기화 설정된 상태에서, 11.3.0으로 제품을 업그레이드하면 이 값이 1.0 으로 기본 설정됩니다.
551+

confluence-mdx/tests/reverse-sync/544378513/improved.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,21 @@ Administrator &gt; General &gt; Workflow Management &gt; Approval Rules &gt; Add
5252
* **Approval Steps** : 승인 단계 및 승인자 지정 방식을 설정합니다.
5353
* `Add Step` 버튼을 클릭하여 승인 단계를 추가할 수 있습니다. 최대 4단계까지 가능합니다. (단, Request Type 중 Web App JIT Request는 4단계 승인을 지원하지 않습니다.)
5454
* 승인자 지정 방식:
55-
* **Allow Assignee selection (Admin-Only)** : 요청자가 직접 승인자를 지정할 수 있으나, Owner 및 해당 요청을 승인할 권한을 가진 관리자만 선택 가능합니다.
56-
* **Allow Assignee selection (All Users)** : 요청자가 직접 승인자를 지정할 수 있으며, 모든 사용자를 선택 가능합니다.
55+
* **Allow Assignee selection (Admin-Only)**: 요청자가 직접 승인자를 지정할 수 있으나, Owner 및 해당 요청을 승인할 권한을 가진 관리자만 선택 가능합니다.
56+
* **Allow Assignee selection (All Users)** : 요청자가 직접 승인자를 지정할 수 있으며, 모든 사용자를 선택 가능합니다.
5757
* 이 옵션을 선택할 경우 요청자 본인을 승인자로 지정하는 것을 허용합니다.
5858
* **Assign Connection Owner**: 요청 시점에 선택한 커넥션의 Connection Owner가 승인자로 지정됩니다. (Request Type으로 SQL / Export Request를 지정한 경우에만 활성화)
5959
* **Assign Server Group Owners**: 요청 시점에 선택한 서버 그룹의 Server Group Owner가 승인자로 지정됩니다. (Request Type으로 Server Access Request / Server Privilege Request를 지정한 경우에만 활성화)
6060
* **Select Assignees** : 현재 선택하는 사용자 또는 그룹만이 승인자로 지정되며, 요청 작성 시 승인자 변경이 불가능합니다.
61-
* **Allow Assignee selection (Attribute-Based)** : 요청자 프로필의 특정 사용자 Attribute 값을 기준으로 승인자를 자동으로 지정합니다.
61+
* **Allow Assignee selection (Attribute-Based)**: 요청자 프로필의 특정 사용자 Attribute 값을 기준으로 승인자를 자동으로 지정합니다.
6262
* **Execution Steps**: SQL 실행자 지정 방식을 설정합니다. (Request Type으로 SQL / Export Request를 지정한 경우에만 활성화)
63-
* **Allow Assignee selection (Admin-Only)** : 요청자가 직접 실행자를 지정할 수 있으나, Owner 및 해당 요청을 승인할 권한을 가진 관리자만 선택 가능합니다.
64-
* **Allow Assignee selection (All Users)** : 요청자가 직접 실행자를 지정할 수 있으며, 모든 사용자를 선택 가능합니다.
63+
* **Allow Assignee selection (Admin-Only)**: 요청자가 직접 실행자를 지정할 수 있으나, Owner 및 해당 요청을 승인할 권한을 가진 관리자만 선택 가능합니다.
64+
* **Allow Assignee selection (All Users)** : 요청자가 직접 실행자를 지정할 수 있으며, 모든 사용자를 선택 가능합니다.
6565
* 이 옵션을 선택할 경우 요청자 본인을 실행자로 지정하는 것을 허용합니다.
6666
* **Assign Connection Owner**: 요청 시점에 선택한 DB 커넥션의 Connection Owner가 실행자로 지정됩니다. 요청 작성 시 실행자 변경이 불가능합니다.
6767
* [DB Connections](../../databases/connection-management/db-connections)에서 DB 커넥션 별로 Connection Owner를 지정하는 방법을 확인할 수 있습니다.
6868
* **Select Assignees** : 현재 선택하는 사용자 또는 그룹만이 실행자로 지정되며, 요청 작성 시 실행자 변경이 불가능합니다.
69-
* **Review :** Administrator &gt; General &gt; Workflow Configurations에서**Activate Review Step to collaborate with others**옵션이 활성화된 경우에만 표시됩니다. 결재 요청에 대한 참조자를 지정하는 방식을 설정합니다.
69+
* **Review :** Administrator &gt; General &gt; Workflow Configurations에서****Activate Review Step to collaborate with others****옵션이 활성화된 경우에만 표시됩니다. 결재 요청에 대한 참조자를 지정하는 방식을 설정합니다.
7070
* **참조자 지정 방식:**
7171
* Select Assignee(s) / Group(s) : 관리자가 특정 사용자 또는 그룹을 고정된 참조자로 미리 지정합니다. 이 규칙으로 결재를 상신하는 사용자는 참조자를 변경할 수 없습니다.
7272
* Allow Assignee selection (All Users) : 결재 상신자가 직접 모든 활성 사용자 중에서 참조자를 선택하도록 허용합니다.
@@ -87,7 +87,7 @@ Administrator &gt; General &gt; Workflow Management &gt; Approval Rules &gt; Add
8787
2. **Allow Assignee selection (Admin-Only)** :
8888
* 활성화 시: 워크플로우 상신 페이지에서 요청자 본인을 승인자로 지정할 수 있습니다.
8989
* 비활성화 시: 워크플로우 상신 페이지에서 승인자 선택 목록에서 요청자 본인이 제외됩니다.
90-
3. **Allow Assignee selection (All Users)** :
90+
3. **Allow Assignee selection (All Users)** :
9191
* 활성화 시: 워크플로우 상신 페이지에서 요청자 본인을 승인자로 지정할 수 있습니다.
9292
* 비활성화 시: 워크플로우 상신 페이지에서 승인자 선택 목록에서 요청자 본인이 제외됩니다.
9393
4. **Select Assignees** :

confluence-mdx/tests/reverse-sync/544379393/improved.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ QueryPie에서 기록하는 로그를 Syslog 형식으로 외부로 전송하는
2121
2. Syslog 타일을 클릭하여 상세 페이지로 이동합니다.
2222

2323
<Callout type="important">
24-
**Syslog (legacy) 는 무엇인가요?** <br/>기존 Syslog를 사용하셨던 경우 **Syslog (Legacy)** 타일이 추가로 표시됩니다.
24+
**Syslog (legacy) 는 무엇인가요?** <br/>기존 Syslog를 사용하셨던 경우 **Syslog (Legacy)** 타일이 추가로 표시됩니다.
2525
이곳에서 기존 포맷을 유지한 상태로 Syslog를 그대로 전송받을 수 있습니다.
2626
Legacy Format은 Syslog 프로토콜 상 Timestamp 필드가 Time Zone의 영향을 받으므로 별도로 Time Zone 설정 항목이 존재합니다.
2727
기본값은 UTC입니다.
@@ -37,13 +37,13 @@ Administrator &gt; General &gt; System &gt; Integrations &gt; Syslog &gt; Config
3737
</figure>
3838

3939
1. Destination 정보를 생성하기 위해 다음의 정보들을 입력합니다.
40-
1. **Destination Name** : syslog를 수신하는 주체를 식별할 수 있도록 적당한 이름을 입력합니다.
41-
2. **Protocol** : syslog에서 선택 가능한 프로토콜은 TCP(기본값)와 UDP입니다. UDP는 패킷의 길이 제약사항이 있고 보안적으로 취약하므로 TCP 사용을 권장합니다.
42-
3. **Destination Address (Hostname)** : syslog를 수신하는 syslog server의 IP address 또는 hostname을 입력합니다.
43-
4. **Port** : syslog server에서 listen하는 port를 입력합니다. (기본값 514)
44-
5. **Test Connection 버튼** : TCP는 syslog 서버와 통신상태를 점검할 수 있습니다.
40+
1. **Destination Name** : syslog를 수신하는 주체를 식별할 수 있도록 적당한 이름을 입력합니다.
41+
2. **Protocol** : syslog에서 선택 가능한 프로토콜은 TCP(기본값)와 UDP입니다. UDP는 패킷의 길이 제약사항이 있고 보안적으로 취약하므로 TCP 사용을 권장합니다.
42+
3. **Destination Address (Hostname)**: syslog를 수신하는 syslog server의 IP address 또는 hostname을 입력합니다.
43+
4. **Port** : syslog server에서 listen하는 port를 입력합니다. (기본값 514)
44+
5. **Test Connection 버튼** : TCP는 syslog 서버와 통신상태를 점검할 수 있습니다.
4545
* UDP는 프로토콜 특성상 통신 상태 점검이 불가능하여, Test Connection 버튼이 비활성화됩니다.
46-
6. **Select Event Items** : 이벤트 항목을 선택적으로 전송할 수 있습니다. 아래에 있는 "Select all event items, including those that may be added later.” 체크박스를 선택하면 전송 가능한 모든 이벤트를 전송합니다.
46+
6. **Select Event Items** : 이벤트 항목을 선택적으로 전송할 수 있습니다. 아래에 있는 "Select all event items, including those that may be added later.” 체크박스를 선택하면 전송 가능한 모든 이벤트를 전송합니다.
4747
7. Disable syslog header : syslog header 정보를 빼고 전송합니다(기본값 Yes). 일부 SIEM에서 json 파싱이 어려운 경우 syslog header를 빼기 위해 제공되는 옵션입니다.
4848
8. Description : 설정 정보에 대한 100자 이내의 간략한 정보를 입력합니다.
4949
2. `OK` 버튼을 누르고 설정을 저장합니다.

0 commit comments

Comments
 (0)