Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for footnotes #48

Merged
merged 22 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c7f9a9c
Add plugin for supporting footnotes
bimbashrestha Aug 10, 2022
3e38994
Use n2y.plugins.footnotes as key instead of footntoes
bimbashrestha Aug 10, 2022
c32fe32
Raise UseNextClass when not footnote in paragraph
bimbashrestha Aug 10, 2022
6ecf742
Raise UseNextClass when not footnote in richtext
bimbashrestha Aug 10, 2022
dd14a26
Pre-fetch page blocks to ensure constructor called before to_pandoc
bimbashrestha Aug 10, 2022
b24863b
Add warning when footnote id is overloaded
bimbashrestha Aug 10, 2022
55b9eca
Add warning when footnote is missing
bimbashrestha Aug 10, 2022
4887705
Merge branch 'main' into footnotes-no-second-pass
bimbashrestha Aug 11, 2022
49c5482
Add block reference to plugin
bimbashrestha Aug 11, 2022
920a55d
Add plugin_data dict to Page class
bimbashrestha Aug 11, 2022
67d9bdf
Use page plugin_data instead of client plugin_data
bimbashrestha Aug 11, 2022
5ddecab
Revert pre-fetching of blocks inside pages
bimbashrestha Aug 11, 2022
55fac4c
Always return None because UseNextClass handles non-footnotes
bimbashrestha Aug 11, 2022
009b8b5
Clarify footnote stripping logic
bimbashrestha Aug 11, 2022
dc74d6b
Add warning for empty footnote
bimbashrestha Aug 11, 2022
1b4cf99
Prserve prefix and suffix of footnote containing token
bimbashrestha Aug 11, 2022
1b63360
Add footnotes test to end-to-end tests
bimbashrestha Aug 11, 2022
502574e
Add notion url to warning for missing footnote
bimbashrestha Aug 11, 2022
38b7832
Add notion url to warning for empty footnote
bimbashrestha Aug 11, 2022
c67e610
Warning re-word
bimbashrestha Aug 11, 2022
77430ee
Add documentation about n2y.plugins.footnotes
bimbashrestha Aug 11, 2022
5a804ec
Fix _footnote_empty() and rename
bimbashrestha Aug 11, 2022
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ This plugin assumes that the `mmdc` mermaid commandline tool is available, and w

If there are errors with the mermaid syntax, it is treated as a normal codeblock and the warning is logged.

### Footnotes

Adds support for Pandoc-style footnotes. Any `text` rich texts that contain footnote references in the format `[^NUMBER]` (eg: `...some claim [^2].`) will be linked to the corresponding footnote paragraph block starting with `[NUMBER]:` (eg: `[2]: This is a footnote.`).

## Architecture

N2y's architecture is divided into four main steps:
Expand Down Expand Up @@ -180,6 +184,7 @@ Here are some features we're planning to add in the future:
- Add support for dumping the notion urls using `--url-property`.
- Add support for all types of rollups (including arrays of other values)
- Add a property to rich text arrays, rich text, and mention instances back to the block they're contained in IF they happen to be contained in a block (some rich text arrays, etc. are from property values). This is useful when developing plugins.
- Add `n2y.plugins.footnotes` plugin

### v0.4.2

Expand Down
2 changes: 2 additions & 0 deletions n2y/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def __init__(self, client, notion_data):
self._block = None
bimbashrestha marked this conversation as resolved.
Show resolved Hide resolved
self._children = None

self.plugin_data = {}

@property
def title(self):
for property_value in self.properties.values():
Expand Down
107 changes: 107 additions & 0 deletions n2y/plugins/footnotes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import re
import logging

from pandoc.types import Note, Str, Para

from n2y.rich_text import TextRichText
from n2y.blocks import ParagraphBlock
from n2y.errors import UseNextClass


plugin_data_key = "n2y.plugins.footnotes"

logger = logging.getLogger(__name__)


class ParagraphWithFootnoteBlock(ParagraphBlock):
def __init__(self, client, notion_data, page, get_children=True):
super().__init__(client, notion_data, page, get_children)
if self._is_footnote():
self._attach_footnote_data()
else:
raise UseNextClass()

def to_pandoc(self):
bimbashrestha marked this conversation as resolved.
Show resolved Hide resolved
return None

def _attach_footnote_data(self):
if plugin_data_key not in self.page.plugin_data:
self.page.plugin_data[plugin_data_key] = {}
if self._footnote() not in self.page.plugin_data[plugin_data_key]:
self.page.plugin_data[plugin_data_key][self._footnote()] = self._footnote_ast()
if self._footnote_empty():
msg = 'Empty footnote "[%s]" (%s)'
logger.warning(msg, self._footnote(), self.notion_url)
else:
msg = 'Multiple footnotes for "[%s]", skipping latest (%s)'
logger.warning(msg, self._footnote(), self.notion_url)

def _is_footnote(self):
return self._footnote() is not None

def _footnote(self):
first_str = self.rich_text.to_plain_text().split(" ")[0]
footnotes = re.findall(r"\[(\d+)\]:", first_str)
if len(footnotes) != 1:
return None
return footnotes[0]

def _footnote_ast(self):
ast = super().to_pandoc()
if isinstance(ast, list):
first_paragraph_footnote_stripped = Para(ast[0][0][2:])
remaining_paragraphs = ast[1:]
return [first_paragraph_footnote_stripped] + remaining_paragraphs
else:
paragraph_footnote_stripped = Para(ast[0][2:])
return paragraph_footnote_stripped

def _footnote_empty(self):
return len(self.rich_text.to_plain_text()) == 0


class TextRichTextWithFootnoteRef(TextRichText):
def __init__(self, client, notion_data, block=None):
super().__init__(client, notion_data, block)
if not self._is_footnote():
raise UseNextClass()

def to_pandoc(self):
bimbashrestha marked this conversation as resolved.
Show resolved Hide resolved
pandoc_ast = []
for token in super().to_pandoc():
ref = self._footnote_from_token(token)
if ref is None:
pandoc_ast.append(token)
continue
if ref not in self.block.page.plugin_data[plugin_data_key]:
pandoc_ast.append(token)
msg = 'Missing footnote "[%s]". Rendering as plain text (%s)'
logger.warning(msg, ref, self.block.notion_url)
continue
self._append_footnote_to_ast(pandoc_ast, token, ref)
return pandoc_ast

bimbashrestha marked this conversation as resolved.
Show resolved Hide resolved
def _append_footnote_to_ast(self, pandoc_ast, token, ref):
block = self.block.page.plugin_data[plugin_data_key][ref]
footnote = Note(block) if isinstance(block, list) else Note([block])
prefix, suffix = token[0].split(f"[^{ref}]")
pandoc_ast.append(Str(prefix))
pandoc_ast.append(footnote)
pandoc_ast.append(Str(suffix))

def _is_footnote(self):
return any(self._footnote_from_token(t) is not None for t in super().to_pandoc())

def _footnote_from_token(self, token):
if not isinstance(token, Str):
return None
refs = re.findall(r"\[\^(\d+)\]", token[0])
if len(refs) != 1:
return None
return refs[0]


notion_classes = {
"blocks": {"paragraph": ParagraphWithFootnoteBlock},
"rich_texts": {"text": TextRichTextWithFootnoteRef},
}
6 changes: 6 additions & 0 deletions tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ def test_builtin_plugins(tmp_path):
'--plugin', 'n2y.plugins.removecallouts',
'--plugin', 'n2y.plugins.rawcodeblocks',
'--plugin', 'n2y.plugins.mermaid',
'--plugin', 'n2y.plugins.footnotes',
bimbashrestha marked this conversation as resolved.
Show resolved Hide resolved
'--media-root', str(tmp_path),
])
assert status == 0
Expand All @@ -281,6 +282,11 @@ def test_builtin_plugins(tmp_path):
assert 'Raw markdown should show up' in lines
assert 'Raw html should not show up' not in lines

assert "# Header with Footnotes[^1]" in lines
assert "Paragraph with footnote.[^2]" in lines
assert "[^1]: First **footnote**." in lines
assert "[^2]: Second footnote" in lines


def test_missing_object_exception():
invalid_page_id = "11111111111111111111111111111111"
Expand Down