feat(notion-writer): add column_list blocks via Pandoc fenced div

Pandoc-style ::: columns / ::: column / ::: blocks emit a Notion
column_list with column children. Each column's body recurses through
_parse_lines so any block type (lists, code, nested columns) works
inside. Single-column wrappers degrade to plain paragraphs because
Notion requires at least two columns.

Single-depth-counter design: depth=1 on enter, increments on `::: columns`
or `::: column`, decrements on `:::`. depth=0 closes the wrapper.

+3 tests, 29 passing total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 11:26:02 +09:00
parent 0f8d7b2df1
commit 0421a65327
2 changed files with 155 additions and 0 deletions

View File

@@ -280,6 +280,69 @@ def test_toggle_unclosed_falls_through():
_assert("body line" in joined, "body preserved as paragraph")
def test_columns_two():
md = """::: columns
::: column
left content
:::
::: column
right content
:::
:::"""
blocks = markdown_to_notion_blocks(md)
_assert(len(blocks) == 1, "one block emitted")
_assert(blocks[0]["type"] == "column_list", "block type is column_list")
cols = blocks[0]["column_list"]["children"]
_assert(len(cols) == 2, "two columns")
_assert(cols[0]["type"] == "column", "child 0 is column")
_assert(cols[1]["type"] == "column", "child 1 is column")
left_para = cols[0]["column"]["children"][0]["paragraph"]["rich_text"][0]["text"]["content"]
right_para = cols[1]["column"]["children"][0]["paragraph"]["rich_text"][0]["text"]["content"]
_assert(left_para == "left content", "left column content preserved")
_assert(right_para == "right content", "right column content preserved")
def test_columns_with_blocks():
"""Columns can hold any block type via per-column _parse_lines recursion."""
md = """::: columns
::: column
- bullet a
- bullet b
:::
::: column
```python
print(1)
```
:::
:::"""
blocks = markdown_to_notion_blocks(md)
_assert(blocks[0]["type"] == "column_list", "outer is column_list")
cols = blocks[0]["column_list"]["children"]
_assert(len(cols) == 2, "two columns")
left_types = [c["type"] for c in cols[0]["column"]["children"]]
right_types = [c["type"] for c in cols[1]["column"]["children"]]
_assert(left_types.count("bulleted_list_item") == 2, "left column has 2 bullets")
_assert("code" in right_types, "right column has code block")
def test_columns_single_degrades():
"""Single-column ::: columns block degrades to paragraphs (Notion requires >= 2)."""
md = """::: columns
::: column
only one column here
:::
:::"""
blocks = markdown_to_notion_blocks(md)
types = [b["type"] for b in blocks]
_assert("column_list" not in types, "no column_list emitted for single column")
paragraph_texts = [
b["paragraph"]["rich_text"][0]["text"]["content"]
for b in blocks if b["type"] == "paragraph"
]
joined = " ".join(paragraph_texts)
_assert("only one column here" in joined, "content preserved as paragraph")
def test_rich_text_anchor_link_becomes_bold():
"""TOC-style fragment links must not be sent as Notion link annotations."""
spans = parse_rich_text("see [Section A](#section-a) below")
@@ -339,6 +402,9 @@ def run_all():
test_toggle_nested_blocks,
test_toggle_nested_toggle,
test_toggle_unclosed_falls_through,
test_columns_two,
test_columns_with_blocks,
test_columns_single_degrades,
test_rich_text_anchor_link_becomes_bold,
test_rich_text_relative_link_becomes_plain,
test_rich_text_absolute_link_preserved,