feat(notion-writer): add GitHub-alert callout blocks
Adds support for > [!NOTE] / > [!TIP] / > [!IMPORTANT] / > [!WARNING] / > [!CAUTION] callouts. Each alert type maps to a Notion callout block with an emoji icon and matching colored background. Unknown alert types (e.g. > [!BOGUS]) fall through to the existing quote handler with the marker preserved. Also restores the "must come before generic bullet match" comment on the todo branch (load-bearing ordering note). +6 tests, 22 passing total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,7 @@ def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
|
||||
code_lines.append(lines[i])
|
||||
i += 1
|
||||
blocks.append(create_code_block('\n'.join(code_lines), language))
|
||||
# Checkbox / Todo (must come before generic bullet match)
|
||||
elif line.strip().startswith('- [ ]'):
|
||||
text = line.strip()[5:].strip()
|
||||
blocks.append(create_todo_block(text, False))
|
||||
@@ -134,9 +135,21 @@ def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
|
||||
elif re.match(r'^\d+\.\s', line.strip()):
|
||||
text = re.sub(r'^\d+\.\s', '', line.strip())
|
||||
blocks.append(create_numbered_list_block(text))
|
||||
# Blockquote — GitHub-alert callout takes priority over generic quote
|
||||
elif line.startswith('>'):
|
||||
text = line[1:].strip()
|
||||
blocks.append(create_quote_block(text))
|
||||
first_body = line[1:].strip()
|
||||
alert_match = ALERT_RE.match(first_body)
|
||||
if alert_match:
|
||||
alert_type = alert_match.group(1)
|
||||
body_lines: List[str] = []
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].lstrip().startswith('>'):
|
||||
body_lines.append(lines[i].lstrip()[1:].lstrip())
|
||||
i += 1
|
||||
body_text = '\n'.join(body_lines)
|
||||
blocks.append(create_callout_block(body_text, alert_type))
|
||||
continue
|
||||
blocks.append(create_quote_block(first_body))
|
||||
elif line.strip() in ['---', '***', '___']:
|
||||
blocks.append(create_divider_block())
|
||||
else:
|
||||
@@ -155,6 +168,15 @@ INLINE_PATTERNS = [
|
||||
('italic_under', re.compile(r'(?<![a-zA-Z0-9_])_([^_\n]+)_(?![a-zA-Z0-9_])')),
|
||||
]
|
||||
|
||||
ALERT_TYPES = {
|
||||
'NOTE': ('ℹ️', 'blue_background'),
|
||||
'TIP': ('💡', 'green_background'),
|
||||
'IMPORTANT': ('☝️', 'purple_background'),
|
||||
'WARNING': ('⚠️', 'yellow_background'),
|
||||
'CAUTION': ('🚨', 'red_background'),
|
||||
}
|
||||
ALERT_RE = re.compile(r'^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$')
|
||||
|
||||
|
||||
_ABSOLUTE_URL_RE = re.compile(r'^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)')
|
||||
|
||||
@@ -292,6 +314,19 @@ def create_quote_block(text: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def create_callout_block(text: str, alert_type: str) -> Dict[str, Any]:
|
||||
"""Create a callout block with icon + color from a GitHub alert type."""
|
||||
icon, color = ALERT_TYPES[alert_type]
|
||||
return {
|
||||
"type": "callout",
|
||||
"callout": {
|
||||
"rich_text": parse_rich_text(text),
|
||||
"icon": {"type": "emoji", "emoji": icon},
|
||||
"color": color,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_code_block(code: str, language: str) -> Dict[str, Any]:
|
||||
return {
|
||||
"object": "block",
|
||||
|
||||
@@ -165,6 +165,52 @@ def test_blocks_table():
|
||||
"link inline formatting preserved inside table cell")
|
||||
|
||||
|
||||
def test_callout_note():
|
||||
md = "> [!NOTE]\n> This is a note.\n> Spans multiple lines."
|
||||
blocks = markdown_to_notion_blocks(md)
|
||||
_assert(len(blocks) == 1, "exactly one block emitted")
|
||||
_assert(blocks[0]["type"] == "callout", "block type is callout")
|
||||
callout = blocks[0]["callout"]
|
||||
_assert(callout["icon"] == {"type": "emoji", "emoji": "ℹ️"}, "NOTE icon is ℹ️")
|
||||
_assert(callout["color"] == "blue_background", "NOTE color is blue_background")
|
||||
body_text = "".join(s["text"]["content"] for s in callout["rich_text"])
|
||||
_assert("This is a note." in body_text, "body line 1 preserved")
|
||||
_assert("Spans multiple lines." in body_text, "body line 2 preserved")
|
||||
|
||||
|
||||
def test_callout_tip():
|
||||
blocks = markdown_to_notion_blocks("> [!TIP]\n> Use this trick.")
|
||||
_assert(blocks[0]["type"] == "callout", "TIP block is callout")
|
||||
_assert(blocks[0]["callout"]["icon"]["emoji"] == "💡", "TIP icon is 💡")
|
||||
_assert(blocks[0]["callout"]["color"] == "green_background", "TIP color is green")
|
||||
|
||||
|
||||
def test_callout_important():
|
||||
blocks = markdown_to_notion_blocks("> [!IMPORTANT]\n> Read this.")
|
||||
_assert(blocks[0]["callout"]["icon"]["emoji"] == "☝️", "IMPORTANT icon is ☝️")
|
||||
_assert(blocks[0]["callout"]["color"] == "purple_background", "IMPORTANT color is purple")
|
||||
|
||||
|
||||
def test_callout_warning():
|
||||
blocks = markdown_to_notion_blocks("> [!WARNING]\n> Be careful.")
|
||||
_assert(blocks[0]["callout"]["icon"]["emoji"] == "⚠️", "WARNING icon is ⚠️")
|
||||
_assert(blocks[0]["callout"]["color"] == "yellow_background", "WARNING color is yellow")
|
||||
|
||||
|
||||
def test_callout_caution():
|
||||
blocks = markdown_to_notion_blocks("> [!CAUTION]\n> Do not proceed.")
|
||||
_assert(blocks[0]["callout"]["icon"]["emoji"] == "🚨", "CAUTION icon is 🚨")
|
||||
_assert(blocks[0]["callout"]["color"] == "red_background", "CAUTION color is red")
|
||||
|
||||
|
||||
def test_callout_unknown_falls_through():
|
||||
"""An unrecognized alert type renders as a plain quote with the marker preserved."""
|
||||
blocks = markdown_to_notion_blocks("> [!BOGUS]\n> some content")
|
||||
_assert(blocks[0]["type"] == "quote", "unknown alert renders as quote, not callout")
|
||||
quote_text = blocks[0]["quote"]["rich_text"][0]["text"]["content"]
|
||||
_assert("[!BOGUS]" in quote_text, "[!BOGUS] marker preserved in quote text")
|
||||
|
||||
|
||||
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")
|
||||
@@ -214,6 +260,12 @@ def run_all():
|
||||
test_blocks_todo,
|
||||
test_blocks_quote_code_divider,
|
||||
test_blocks_table,
|
||||
test_callout_note,
|
||||
test_callout_tip,
|
||||
test_callout_important,
|
||||
test_callout_warning,
|
||||
test_callout_caution,
|
||||
test_callout_unknown_falls_through,
|
||||
test_rich_text_anchor_link_becomes_bold,
|
||||
test_rich_text_relative_link_becomes_plain,
|
||||
test_rich_text_absolute_link_preserved,
|
||||
|
||||
Reference in New Issue
Block a user