feat(notion-writer): add inline page mentions via @[Title](id-or-url)

Inline rich-text gets a new 'mention' pattern matching @[Title](target).
Target can be a raw 32-hex page ID, a dashed UUID, or a Notion URL —
all resolved via the existing extract_notion_id helper. Invalid targets
degrade to plain text '@Title' with no warning (avoids false-positive
spam from unrelated @[x](y) patterns).

Pattern placed BEFORE 'link' in INLINE_PATTERNS so the @ prefix takes
priority over plain link parsing.

Also widens extract_notion_id's URL regex from [^-]+- to [^/]+?- so
multi-hyphen Notion URLs (e.g. notion.so/My-Cool-Page-{id}) resolve
correctly — previous regex only matched single-segment titles.

+3 tests, 32 passing total.

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

View File

@@ -34,9 +34,9 @@ def extract_notion_id(url_or_id: str) -> Optional[str]:
# Notion URL patterns
patterns = [
r'notion\.so/(?:[^/]+/)?([a-f0-9]{32})', # notion.so/workspace/page-id
r'notion\.so/(?:[^/]+/)?[^-]+-([a-f0-9]{32})', # notion.so/workspace/Page-Title-id
r'notion\.so/(?:[^/]+/)?[^/]+?-([a-f0-9]{32})', # notion.so/workspace/Page-Title-id
r'notion\.site/(?:[^/]+/)?([a-f0-9]{32})', # public notion.site
r'notion\.site/(?:[^/]+/)?[^-]+-([a-f0-9]{32})',
r'notion\.site/(?:[^/]+/)?[^/]+?-([a-f0-9]{32})',
r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})', # UUID format
]
@@ -277,6 +277,7 @@ def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
INLINE_PATTERNS = [
('code', re.compile(r'`([^`\n]+)`')),
('mention', re.compile(r'@\[([^\]]+)\]\(([^)\s]+)\)')),
('link', re.compile(r'\[([^\]]+)\]\(([^)\s]+)\)')),
('bold_star', re.compile(r'\*\*([^*\n]+)\*\*')),
('bold_under', re.compile(r'__([^_\n]+)__')),
@@ -347,6 +348,21 @@ def parse_rich_text(text: str) -> List[Dict[str, Any]]:
if kind == 'code':
spans.append(_rich_span(m.group(1), code=True))
elif kind == 'mention':
mention_title, mention_target = m.group(1), m.group(2)
page_id = extract_notion_id(mention_target)
if page_id:
spans.append({
"type": "mention",
"mention": {
"type": "page",
"page": {"id": format_id_with_dashes(page_id)},
},
"plain_text": mention_title,
})
else:
# Invalid ID — degrade to plain text "@Title"
spans.append(_rich_span(f"@{mention_title}"))
elif kind == 'link':
link_text, link_url = m.group(1), m.group(2)
if _is_absolute_url(link_url):