Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions n2y/plugins/internallinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def to_pandoc(self):
)
# Fallback to default behavior for TextRichText conversion
return super().to_pandoc()
if getattr(target_block, "rich_text", None) is None:
# Links can target blocks with no text to anchor to (e.g. images);
# there is no header id to point at, so leave the link unchanged.
self.client.logger.warning(
f"Internal link target block {target_id} has no text; "
"leaving link unresolved"
)
return super().to_pandoc()
header_id = header_id_from_text(target_block.rich_text.to_plain_text())
self.href = f"#{header_id}"
return super().to_pandoc()
Expand Down
5 changes: 5 additions & 0 deletions n2y/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ def header_id_from_text(header_text, existing_ids=None):

See https://pandoc.org/MANUAL.html#extension-auto_identifiers
"""
# Markdown parsers (pandoc, kramdown) strip surrounding whitespace from
# heading text before generating ids; Notion rich text can carry trailing
# spaces, which would otherwise become trailing hyphens that don't match
# the ids the downstream renderer produces.
header_text = header_text.strip()
have_struck_letter = False

new_header_text = ""
Expand Down
50 changes: 49 additions & 1 deletion tests/test_plugin_internallinks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest.mock import Mock, patch

from n2y.blocks import ChildPageBlock, HeadingOneBlock, ParagraphBlock
from n2y.blocks import ChildPageBlock, DividerBlock, HeadingOneBlock, ParagraphBlock
from n2y.notion import Client
from n2y.notion_mocks import (
mock_block,
Expand Down Expand Up @@ -110,3 +110,51 @@ def test_internal_link_to_pandoc():
markdown = pandoc_ast_to_markdown(page.to_pandoc(), Mock())
assert f"# {header_text}" in markdown
assert f"[{link_text}](#{header_id_from_text(header_text)})" in markdown


@patch("n2y.notion.Client.wrap_notion_user")
def mock_page_with_link_to_divider(wrap_notion_user, link_text: str = "see below"):
client = Client("", plugins=["n2y.plugins.internallinks"])
wrap_notion_user.return_value = User(client, mock_user())

page = Page(client, notion_data=mock_page())
page_block = ChildPageBlock(
client=client,
notion_data=mock_block("child_page", {"title": "Mock Page"}),
page=page,
get_children=False,
)
page._block = page_block

divider_block = DividerBlock(
client=client,
notion_data=mock_block("divider", {}),
page=page,
get_children=False,
)

href = (
f"/{page.notion_id.replace('-', '')}"
f"#{divider_block.notion_id.replace('-', '')}"
)
paragraph_with_link_block = ParagraphBlock(
client=client,
notion_data=mock_block(
"paragraph",
{"rich_text": [mock_rich_text(text=link_text, href=href)]},
),
page=page,
get_children=False,
)

page.block.children = [divider_block, paragraph_with_link_block]
return page, href


def test_internal_link_to_textless_block_left_unresolved():
# Links can target blocks with no rich_text (images, dividers); the plugin
# must not crash and should leave the link unchanged.
link_text = "see below"
page, href = mock_page_with_link_to_divider(link_text=link_text)
markdown = pandoc_ast_to_markdown(page.to_pandoc(), Mock())
assert f"[{link_text}]({href})" in markdown
Loading