feat(notion-writer): parse standalone images to image blocks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 08:15:39 +09:00
parent 80c9efa282
commit 2ee5809f57
2 changed files with 46 additions and 0 deletions

View File

@@ -56,6 +56,7 @@ def format_id_with_dashes(raw_id: str) -> str:
TABLE_SEPARATOR_RE = re.compile(r'^\s*\|?(\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$')
IMAGE_RE = re.compile(r'^\s*!\[([^\]]*)\]\(([^)\s]+)\)\s*$')
def _is_table_row(line: str) -> bool:
@@ -210,6 +211,13 @@ def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
blocks.append(create_column_list_block(column_blocks))
continue
image_match = IMAGE_RE.match(line)
if image_match:
blocks.append(create_image_block(
image_match.group(1), image_match.group(2)))
i += 1
continue
if _is_table_row(line) and i + 1 < len(lines) and TABLE_SEPARATOR_RE.match(lines[i + 1]):
header_cells = _split_table_row(line)
i += 2
@@ -507,6 +515,19 @@ def create_divider_block() -> Dict[str, Any]:
}
def create_image_block(alt: str, target: str) -> Dict[str, Any]:
"""Image block in external shape. Local targets are converted to
file_upload shape later by ntn_files.materialize_local_media."""
block: Dict[str, Any] = {
"object": "block",
"type": "image",
"image": {"type": "external", "external": {"url": target}},
}
if alt:
block["image"]["caption"] = parse_rich_text(alt)
return block
def create_table_block(header_cells: List[str], body_rows: List[List[str]]) -> Dict[str, Any]:
"""Build a Notion `table` block with header + body rows.

View File

@@ -413,6 +413,28 @@ def test_no_literal_markers_leak():
_assert("bold" in joined and "link" in joined, "visible words preserved")
def test_image_remote_external():
blocks = markdown_to_notion_blocks("![a chart](https://ex.com/c.png)")
assert len(blocks) == 1
b = blocks[0]
assert b["type"] == "image"
assert b["image"]["type"] == "external"
assert b["image"]["external"]["url"] == "https://ex.com/c.png"
assert b["image"]["caption"][0]["text"]["content"] == "a chart"
def test_image_local_external_shape_preupload():
blocks = markdown_to_notion_blocks("![local](./pics/x.png)")
assert len(blocks) == 1
assert blocks[0]["image"]["external"]["url"] == "./pics/x.png"
def test_image_only_when_standalone():
# An inline bang-bracket inside prose is NOT an image block.
blocks = markdown_to_notion_blocks("see ![x](y.png) inline")
assert blocks[0]["type"] == "paragraph"
def run_all():
tests = [
test_rich_text_plain,
@@ -447,6 +469,9 @@ def run_all():
test_rich_text_relative_link_becomes_plain,
test_rich_text_absolute_link_preserved,
test_no_literal_markers_leak,
test_image_remote_external,
test_image_local_external_shape_preupload,
test_image_only_when_standalone,
]
for t in tests:
print(f"\n{t.__name__}")