@@ -116,6 +116,18 @@ def as_markdown(self, caption: Optional[str] = None, width: Optional[str] = None
116116 return f'[{ caption } ]({ self .output_dir } /{ self .filename } )'
117117
118118
119+ def _is_unicode_punctuation (ch : str ) -> bool :
120+ """CommonMark spec의 Unicode punctuation 판정.
121+
122+ Unicode general category가 P(punctuation) 또는 S(symbol)이면 True.
123+ ASCII punctuation도 포함된다.
124+ """
125+ if not ch :
126+ return False
127+ cat = unicodedata .category (ch [0 ])
128+ return cat .startswith ('P' ) or cat .startswith ('S' )
129+
130+
119131class SingleLineParser :
120132 def __init__ (self , node , collector : LostInfoCollector | None = None ):
121133 self .node = node
@@ -202,13 +214,25 @@ def convert_recursively(self, node):
202214 for child in node .children :
203215 self .convert_recursively (child )
204216 else :
205- self .markdown_lines .append (" **" )
206- self .markdown_lines .append (self .markdown_of_children (node ).strip ())
207- self .markdown_lines .append ("** " )
217+ inner = self .markdown_of_children (node ).strip ()
218+ open_sp = " " if inner and _is_unicode_punctuation (inner [0 ]) else ""
219+ close_sp = " " if inner and _is_unicode_punctuation (inner [- 1 ]) else ""
220+ # 연속 emphasis delimiter 충돌 방지
221+ if not close_sp and isinstance (node .next_sibling , Tag ) and node .next_sibling .name in ('strong' , 'em' ):
222+ close_sp = " "
223+ self .markdown_lines .append (f"{ open_sp } **" )
224+ self .markdown_lines .append (inner )
225+ self .markdown_lines .append (f"**{ close_sp } " )
208226 elif node .name in ['em' ]:
209- self .markdown_lines .append (" *" )
210- self .markdown_lines .append (self .markdown_of_children (node ).strip ())
211- self .markdown_lines .append ("* " )
227+ inner = self .markdown_of_children (node ).strip ()
228+ open_sp = " " if inner and _is_unicode_punctuation (inner [0 ]) else ""
229+ close_sp = " " if inner and _is_unicode_punctuation (inner [- 1 ]) else ""
230+ # 연속 emphasis delimiter 충돌 방지
231+ if not close_sp and isinstance (node .next_sibling , Tag ) and node .next_sibling .name in ('strong' , 'em' ):
232+ close_sp = " "
233+ self .markdown_lines .append (f"{ open_sp } *" )
234+ self .markdown_lines .append (inner )
235+ self .markdown_lines .append (f"*{ close_sp } " )
212236 elif node .name in ['code' ]:
213237 self .markdown_lines .append ("`" )
214238 self .markdown_lines .append (self .markdown_of_children (node ).strip ())
@@ -617,6 +641,43 @@ def is_standalone_dash(self):
617641
618642 return True
619643
644+ @staticmethod
645+ def _is_trailing_empty_p (node ):
646+ """Trailing empty <p>/<div> 앞의 separator를 건너뛰어 1:1 매핑을 보장한다.
647+
648+ Markdown에서 블록 사이 빈 줄(separator)은 필수이므로, separator를
649+ 그대로 두면 N개의 trailing empty <p> → N+1개의 blank line이 된다:
650+
651+ XHTML empty <p> 수 | separator 포함 시 blank line 수
652+ 0 | 0
653+ 1 | 2 ← "1"이 불가능
654+ 2 | 3
655+ N | N+1
656+
657+ 1 blank line을 만들 수 있는 XHTML 상태가 존재하지 않으므로,
658+ 사용자가 trailing blank을 2→1로 편집하면 roundtrip에서 재현할 수 없다.
659+
660+ Trailing empty <p> 앞의 separator를 건너뛰면 N → N으로 1:1 매핑되어
661+ 모든 trailing blank 수를 XHTML로 정확히 표현할 수 있다.
662+
663+ Top-level [document] 컨텍스트에서만 적용하여, expand 매크로 등
664+ 중첩 컨테이너 내부에는 영향을 주지 않는다.
665+ """
666+ if node .name not in ('p' , 'div' ):
667+ return False
668+ if node .get_text (strip = True ):
669+ return False
670+ if node .parent .name != '[document]' :
671+ return False
672+ for sibling in node .next_siblings :
673+ if isinstance (sibling , NavigableString ):
674+ if sibling .strip ():
675+ return False
676+ else :
677+ if sibling .get_text (strip = True ):
678+ return False
679+ return True
680+
620681 def append_empty_line_unless_first_child (self , node ):
621682 # Convert generator to list to check length
622683 children_list = list (node .parent .children )
@@ -708,7 +769,8 @@ def convert_recursively(self, node):
708769 self .append_empty_line_unless_first_child (node )
709770 self .markdown_lines .extend (TableToHtmlTable (node , collector = self .collector ).as_markdown )
710771 elif node .name in ['p' , 'div' ]:
711- self .append_empty_line_unless_first_child (node )
772+ if not self ._is_trailing_empty_p (node ):
773+ self .append_empty_line_unless_first_child (node )
712774 child_markdown = []
713775 for child in node .children :
714776 if isinstance (child , NavigableString ):
0 commit comments