Skip to content

Commit bc76f00

Browse files
sungbin1015claude
andcommitted
feat(sync): 테이블 has_column_header·has_row_header 반영 — 1행/1열 헤더 정확 렌더링 [skip-notion]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8461349 commit bc76f00

2 files changed

Lines changed: 70 additions & 7 deletions

File tree

scripts/notion_to_md.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,28 +409,35 @@ def render_children():
409409
if b_type == "table":
410410
if not children:
411411
return ""
412+
tbl = block.get("table", {})
413+
col_header = tbl.get("has_column_header", False) # 1행 헤더
414+
row_header = tbl.get("has_row_header", False) # 1열 헤더
412415
has_color = any(
413416
_get_cell_bg(cell)
414417
for row in children
415418
for cell in row.get("table_row", {}).get("cells", [])
416419
)
417-
if has_color:
420+
if has_color or row_header:
418421
lines = ["<table>"]
422+
in_tbody = False
419423
for i, row in enumerate(children):
420424
cells = row.get("table_row", {}).get("cells", [])
421-
tag = "th" if i == 0 else "td"
422-
if i == 0:
425+
is_col_header_row = col_header and i == 0
426+
if is_col_header_row:
423427
lines.append("<thead><tr>")
424428
else:
425-
if i == 1:
429+
if not in_tbody:
426430
lines.append("<tbody>")
431+
in_tbody = True
427432
lines.append("<tr>")
428-
for cell in cells:
433+
for j, cell in enumerate(cells):
434+
is_th = is_col_header_row or (row_header and j == 0)
435+
tag = "th" if is_th else "td"
429436
bg = _get_cell_bg(cell)
430437
text = _rich_text_to_html(cell)
431438
attr = f' data-notion-bg="{bg}"' if bg else ""
432439
lines.append(f"<{tag}{attr}>{text}</{tag}>")
433-
lines.append("</tr></thead>" if i == 0 else "</tr>")
440+
lines.append("</tr></thead>" if is_col_header_row else "</tr>")
434441
lines.append("</tbody></table>")
435442
return "\n".join(lines) + "\n\n"
436443
lines = []
@@ -441,7 +448,7 @@ def render_children():
441448
for cell in cells
442449
)
443450
lines.append(f"| {row_text} |")
444-
if i == 0:
451+
if i == 0 and col_header:
445452
sep = " | ".join("---" for _ in cells)
446453
lines.append(f"| {sep} |")
447454
return "\n".join(lines) + "\n\n"

scripts/tests/test_notion_to_md.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,59 @@ def test_extract_text_unknown_notion_link_kept(self):
482482
"href": "https://www.notion.so/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]
483483
result = n.extract_text_from_rich_text(rt)
484484
assert result == "[외부 페이지](https://www.notion.so/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)"
485+
486+
487+
# ── TC-12: 테이블 has_column_header / has_row_header 반영 ─────────────────────
488+
489+
def _make_table_block(has_column_header, has_row_header, rows):
490+
"""rows: [["셀1", "셀2"], ...] 형태의 문자열 리스트."""
491+
def _rt(text):
492+
return [{"plain_text": text, "href": None,
493+
"annotations": {"bold": False, "italic": False, "strikethrough": False,
494+
"underline": False, "code": False, "color": "default"}}]
495+
children = [
496+
{"type": "table_row", "table_row": {"cells": [_rt(c) for c in row]}}
497+
for row in rows
498+
]
499+
return (
500+
{"type": "table", "table": {"has_column_header": has_column_header,
501+
"has_row_header": has_row_header}},
502+
children,
503+
)
504+
505+
506+
class TestTableHeaders:
507+
508+
def _render(self, has_col, has_row, rows):
509+
block, children = _make_table_block(has_col, has_row, rows)
510+
block["_children"] = children
511+
return n.block_to_markdown(block, lambda: "", lambda: 1)
512+
513+
def test_column_header_only_uses_plain_markdown(self):
514+
"""has_column_header=True만 있을 때 → plain markdown (| --- | 구분자)."""
515+
out = self._render(True, False, [["헤더A", "헤더B"], ["값1", "값2"]])
516+
assert "<table>" not in out
517+
assert "| --- |" in out
518+
assert "| 헤더A | 헤더B |" in out
519+
520+
def test_row_header_only_first_col_is_th(self):
521+
"""has_row_header=True → 1열만 <th>, 나머지 <td>."""
522+
out = self._render(False, True, [["레이블1", "값1"], ["레이블2", "값2"]])
523+
assert "<th>" in out
524+
# 두 번째 열은 td
525+
assert "<td>" in out
526+
# thead 없음 (열 헤더가 없으므로)
527+
assert "<thead>" not in out
528+
529+
def test_both_headers(self):
530+
"""has_column_header=True, has_row_header=True → 1행+1열 모두 <th>."""
531+
out = self._render(True, True, [["구분", "A", "B"], ["X", "1", "2"]])
532+
assert "<thead>" in out
533+
assert "<th>" in out
534+
assert "<td>" in out
535+
536+
def test_no_header_plain_markdown(self):
537+
"""헤더 없고 색 없으면 plain markdown."""
538+
out = self._render(False, False, [["a", "b"], ["c", "d"]])
539+
assert "<table>" not in out
540+
assert "| a | b |" in out

0 commit comments

Comments
 (0)