refactor(notion-writer): make markdown_to_notion_blocks reentrant
Split the entry function into a public reentrant entry that accepts either string or List[str], and a private _parse_lines engine that container detectors will recurse into. No behavior change for existing callers; all 16 parser tests still pass. Prep for Phase 3c: callout/toggle/columns/page-mention block coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import re
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any, Union
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from notion_client import Client
|
from notion_client import Client
|
||||||
@@ -72,32 +72,36 @@ def _split_table_row(line: str) -> List[str]:
|
|||||||
return [cell.strip() for cell in stripped.split('|')]
|
return [cell.strip() for cell in stripped.split('|')]
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_notion_blocks(markdown_text: str) -> List[Dict[str, Any]]:
|
def markdown_to_notion_blocks(content: Union[str, List[str]]) -> List[Dict[str, Any]]:
|
||||||
"""Convert markdown text to Notion block objects."""
|
"""Convert markdown text (or pre-split lines) to Notion block objects.
|
||||||
blocks = []
|
|
||||||
lines = markdown_text.split('\n')
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
|
Reentrant: container detectors recursively call _parse_lines on inner content.
|
||||||
|
"""
|
||||||
|
if isinstance(content, str):
|
||||||
|
lines = content.split('\n')
|
||||||
|
else:
|
||||||
|
lines = list(content)
|
||||||
|
return _parse_lines(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""Walk lines and emit Notion blocks. Called recursively by container detectors."""
|
||||||
|
blocks = []
|
||||||
|
i = 0
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
line = lines[i]
|
line = lines[i]
|
||||||
|
|
||||||
# Skip empty lines
|
|
||||||
if not line.strip():
|
if not line.strip():
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Table (header row + separator + body rows)
|
|
||||||
if _is_table_row(line) and i + 1 < len(lines) and TABLE_SEPARATOR_RE.match(lines[i + 1]):
|
if _is_table_row(line) and i + 1 < len(lines) and TABLE_SEPARATOR_RE.match(lines[i + 1]):
|
||||||
header_cells = _split_table_row(line)
|
header_cells = _split_table_row(line)
|
||||||
i += 2 # skip separator
|
i += 2
|
||||||
body_rows: List[List[str]] = []
|
body_rows: List[List[str]] = []
|
||||||
while i < len(lines) and _is_table_row(lines[i]):
|
while i < len(lines) and _is_table_row(lines[i]):
|
||||||
body_rows.append(_split_table_row(lines[i]))
|
body_rows.append(_split_table_row(lines[i]))
|
||||||
i += 1
|
i += 1
|
||||||
blocks.append(create_table_block(header_cells, body_rows))
|
blocks.append(create_table_block(header_cells, body_rows))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Headers
|
|
||||||
if line.startswith('######'):
|
if line.startswith('######'):
|
||||||
blocks.append(create_heading_block(line[6:].strip(), 3))
|
blocks.append(create_heading_block(line[6:].strip(), 3))
|
||||||
elif line.startswith('#####'):
|
elif line.startswith('#####'):
|
||||||
@@ -110,8 +114,6 @@ def markdown_to_notion_blocks(markdown_text: str) -> List[Dict[str, Any]]:
|
|||||||
blocks.append(create_heading_block(line[2:].strip(), 2))
|
blocks.append(create_heading_block(line[2:].strip(), 2))
|
||||||
elif line.startswith('#'):
|
elif line.startswith('#'):
|
||||||
blocks.append(create_heading_block(line[1:].strip(), 1))
|
blocks.append(create_heading_block(line[1:].strip(), 1))
|
||||||
|
|
||||||
# Code blocks
|
|
||||||
elif line.startswith('```'):
|
elif line.startswith('```'):
|
||||||
language = line[3:].strip() or 'plain text'
|
language = line[3:].strip() or 'plain text'
|
||||||
code_lines = []
|
code_lines = []
|
||||||
@@ -120,40 +122,26 @@ def markdown_to_notion_blocks(markdown_text: str) -> List[Dict[str, Any]]:
|
|||||||
code_lines.append(lines[i])
|
code_lines.append(lines[i])
|
||||||
i += 1
|
i += 1
|
||||||
blocks.append(create_code_block('\n'.join(code_lines), language))
|
blocks.append(create_code_block('\n'.join(code_lines), language))
|
||||||
|
|
||||||
# Checkbox / Todo (must come before generic bullet match)
|
|
||||||
elif line.strip().startswith('- [ ]'):
|
elif line.strip().startswith('- [ ]'):
|
||||||
text = line.strip()[5:].strip()
|
text = line.strip()[5:].strip()
|
||||||
blocks.append(create_todo_block(text, False))
|
blocks.append(create_todo_block(text, False))
|
||||||
elif line.strip().startswith('- [x]') or line.strip().startswith('- [X]'):
|
elif line.strip().startswith('- [x]') or line.strip().startswith('- [X]'):
|
||||||
text = line.strip()[5:].strip()
|
text = line.strip()[5:].strip()
|
||||||
blocks.append(create_todo_block(text, True))
|
blocks.append(create_todo_block(text, True))
|
||||||
|
|
||||||
# Bullet list
|
|
||||||
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
||||||
text = line.strip()[2:]
|
text = line.strip()[2:]
|
||||||
blocks.append(create_bulleted_list_block(text))
|
blocks.append(create_bulleted_list_block(text))
|
||||||
|
|
||||||
# Numbered list
|
|
||||||
elif re.match(r'^\d+\.\s', line.strip()):
|
elif re.match(r'^\d+\.\s', line.strip()):
|
||||||
text = re.sub(r'^\d+\.\s', '', line.strip())
|
text = re.sub(r'^\d+\.\s', '', line.strip())
|
||||||
blocks.append(create_numbered_list_block(text))
|
blocks.append(create_numbered_list_block(text))
|
||||||
|
|
||||||
# Blockquote
|
|
||||||
elif line.startswith('>'):
|
elif line.startswith('>'):
|
||||||
text = line[1:].strip()
|
text = line[1:].strip()
|
||||||
blocks.append(create_quote_block(text))
|
blocks.append(create_quote_block(text))
|
||||||
|
|
||||||
# Horizontal rule
|
|
||||||
elif line.strip() in ['---', '***', '___']:
|
elif line.strip() in ['---', '***', '___']:
|
||||||
blocks.append(create_divider_block())
|
blocks.append(create_divider_block())
|
||||||
|
|
||||||
# Regular paragraph
|
|
||||||
else:
|
else:
|
||||||
blocks.append(create_paragraph_block(line))
|
blocks.append(create_paragraph_block(line))
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user