Skip to content

Commit 7121320

Browse files
committed
Add BaseRenderer to render page to text, and BaseHTMLRenderer as example
1 parent 56b7a90 commit 7121320

File tree

3 files changed

+244
-1
lines changed

3 files changed

+244
-1
lines changed

notion/renderer.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import markdown2
2+
import requests
3+
4+
from .block import *
5+
6+
7+
class BaseRenderer(object):
8+
9+
def __init__(self, start_block):
10+
self.start_block = start_block
11+
12+
def render(self):
13+
return self.render_block(self.start_block)
14+
15+
def calculate_child_indent(self, block):
16+
if block.type == "page":
17+
return 0
18+
else:
19+
return 1
20+
21+
def render_block(self, block, level=0, preblock=None, postblock=None):
22+
assert isinstance(block, Block)
23+
type_renderer = getattr(self, "handle_" + block._type, None)
24+
if not callable(type_renderer):
25+
if hasattr(self, "handle_default"):
26+
type_renderer = self.handle_default
27+
else:
28+
raise Exception("No handler for block type '{}'.".format(block._type))
29+
pretext = type_renderer(block, level=level, preblock=preblock, postblock=postblock)
30+
if isinstance(pretext, tuple):
31+
pretext, posttext = pretext
32+
else:
33+
posttext = ""
34+
return pretext + self.render_children(block, level=level+self.calculate_child_indent(block)) + posttext
35+
36+
def render_children(self, block, level):
37+
kids = block.children
38+
if not kids:
39+
return ""
40+
text = ""
41+
for i in range(len(kids)):
42+
text += self.render_block(kids[i], level=level)
43+
return text
44+
45+
46+
bookmark_template = """
47+
<div>
48+
<div style="display: flex;">
49+
<a target="_blank" rel="noopener noreferrer" href="{link}" style="display: block; color: inherit; text-decoration: none; flex-grow: 1; min-width: 0px;">
50+
<div role="button" style="user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; width: 100%; display: flex; flex-wrap: wrap-reverse; align-items: stretch; text-align: left; overflow: hidden; border: 1px solid rgba(55, 53, 47, 0.16); border-radius: 3px; position: relative; color: inherit; fill: inherit;">
51+
<div style="flex: 4 1 180px; min-height: 60px; padding: 12px 14px 14px; overflow: hidden; text-align: left;">
52+
<div style="font-size: 14px; line-height: 20px; color: rgb(55, 53, 47); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px;">{title}</div>
53+
<div style="font-size: 12px; line-height: 16px; color: rgba(55, 53, 47, 0.6); height: 32px; overflow: hidden;">{description}</div>
54+
<div style="display: flex; margin-top: 6px;">
55+
<img src="{icon}" style="width: 16px; height: 16px; min-width: 16px; margin-right: 6px;">
56+
<div style="font-size: 12px; line-height: 16px; color: rgb(55, 53, 47); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{link}/div>
57+
</div>
58+
</div>
59+
<div style="flex: 1 1 180px; min-height: 80px; display: block; position: relative;">
60+
<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px;">
61+
<div style="width: 100%; height: 100%;"><img src="{cover}" style="display: block; object-fit: cover; border-radius: 1px; width: 100%; height: 100%;"></div>
62+
</div>
63+
</div>
64+
</div>
65+
</a>
66+
</div>
67+
</div>
68+
"""
69+
70+
callout_template = """
71+
<div style="padding: 16px 16px 16px 12px; display: flex; width: 100%; border-radius: 3px; border-width: 1px; border-style: solid; border-color: transparent; background: rgba(235, 236, 237, 0.3);">
72+
<div>
73+
<div role="button" style="user-select: none; cursor: pointer; display: flex; align-items: center; justify-content: center; height: 24px; width: 24px; border-radius: 3px; flex-shrink: 0;">
74+
<div style="display: flex; align-items: center; justify-content: center; height: 24px; width: 24px;">
75+
<div style="height: 16.8px; width: 16.8px; font-size: 16.8px; line-height: 1.1; margin-left: 0px; color: black;">{icon}</div>
76+
</div>
77+
</div>
78+
</div>
79+
<div style="max-width: 100%; width: 100%; white-space: pre-wrap; word-break: break-word; caret-color: rgb(55, 53, 47); margin-left: 8px;">{title}</div>
80+
</div>
81+
"""
82+
83+
class BaseHTMLRenderer(BaseRenderer):
84+
85+
def create_opening_tag(self, tagname, attributes={}):
86+
attrs = "".join(' {}="{}"'.format(key, val) for key, val in attributes.items())
87+
return "<{tagname}{attrs}>".format(tagname=tagname, attrs=attrs)
88+
89+
def wrap_in_tag(self, block, tagname, fieldname="title", attributes={}):
90+
opentag = self.create_opening_tag(tagname, attributes)
91+
innerhtml = markdown2.markdown(getattr(block, fieldname))
92+
return "{opentag}{innerhtml}</{tagname}>".format(opentag=opentag, tagname=tagname, innerhtml=innerhtml)
93+
94+
def left_margin_for_level(self, level):
95+
return {"display": "margin-left: {}px;".format(level * 20)}
96+
97+
def handle_default(self, block, level=0, preblock=None, postblock=None):
98+
return self.wrap_in_tag(block, "p", attributes=self.left_margin_for_level(level))
99+
100+
def handle_divider(self, block, level=0, preblock=None, postblock=None):
101+
return "<hr/>"
102+
103+
def handle_column_list(self, block, level=0, preblock=None, postblock=None):
104+
return '<div style="display: flex; padding-top: 12px; padding-bottom: 12px;">', '</div>'
105+
106+
def handle_column(self, block, level=0, preblock=None, postblock=None):
107+
buffer = (len(block.parent.children) - 1) * 46
108+
width = block.get("format.column_ratio")
109+
return '<div style="flex-grow: 0; flex-shrink: 0; width: calc((100% - {}px) * {});">'.format(buffer, width), '</div>'
110+
111+
def handle_to_do(self, block, level=0, preblock=None, postblock=None):
112+
return '<input type="checkbox" id="{id}" name="{id}"{checked}><label for="{id}">{title}</label><br/>'.format(
113+
id="chk_" + block.id,
114+
checked=" checked" if block.checked else "",
115+
title=block.title,
116+
)
117+
118+
def handle_code(self, block, level=0, preblock=None, postblock=None):
119+
return self.wrap_in_tag(block, "code", attributes=self.left_margin_for_level(level))
120+
121+
def handle_factory(self, block, level=0, preblock=None, postblock=None):
122+
return ""
123+
124+
def handle_header(self, block, level=0, preblock=None, postblock=None):
125+
return self.wrap_in_tag(block, "h2", attributes=self.left_margin_for_level(level))
126+
127+
def handle_sub_header(self, block, level=0, preblock=None, postblock=None):
128+
return self.wrap_in_tag(block, "h3", attributes=self.left_margin_for_level(level))
129+
130+
def handle_sub_sub_header(self, block, level=0, preblock=None, postblock=None):
131+
return self.wrap_in_tag(block, "h4", attributes=self.left_margin_for_level(level))
132+
133+
def handle_page(self, block, level=0, preblock=None, postblock=None):
134+
return self.wrap_in_tag(block, "h1", attributes=self.left_margin_for_level(level))
135+
136+
def handle_bulleted_list(self, block, level=0, preblock=None, postblock=None):
137+
text = ""
138+
if preblock is None or preblock.type != "bulleted_list":
139+
text = self.create_opening_tag("ul", attributes=self.left_margin_for_level(level))
140+
text += self.wrap_in_tag(block, "li")
141+
if postblock is None or postblock.type != "bulleted_list":
142+
text += "</ul>"
143+
return text
144+
145+
def handle_numbered_list(self, block, level=0, preblock=None, postblock=None):
146+
text = ""
147+
if preblock is None or preblock.type != "numbered_list":
148+
text = self.create_opening_tag("ol", attributes=self.left_margin_for_level(level))
149+
text += self.wrap_in_tag(block, "li")
150+
if postblock is None or postblock.type != "numbered_list":
151+
text += "</ol>"
152+
return text
153+
154+
def handle_toggle(self, block, level=0, preblock=None, postblock=None):
155+
innerhtml = markdown2.markdown(block.title)
156+
opentag = self.create_opening_tag("details", attributes=self.left_margin_for_level(level))
157+
return '{opentag}<summary>{innerhtml}</summary>'.format(opentag=opentag, innerhtml=innerhtml), '</details>'
158+
159+
def handle_quote(self, block, level=0, preblock=None, postblock=None):
160+
return self.wrap_in_tag(block, "blockquote", attributes=self.left_margin_for_level(level))
161+
162+
def handle_text(self, block, level=0, preblock=None, postblock=None):
163+
return self.handle_default(block=block, level=level, preblock=preblock, postblock=postblock)
164+
165+
def handle_equation(self, block, level=0, preblock=None, postblock=None):
166+
text = self.create_opening_tag("p", attributes=self.left_margin_for_level(level))
167+
return text + '<img src="https://chart.googleapis.com/chart?cht=tx&chl={}"/></p>'.format(block.latex)
168+
169+
def handle_embed(self, block, level=0, preblock=None, postblock=None):
170+
iframetag = self.create_opening_tag("iframe", attributes={
171+
"src": block.display_source or block.source,
172+
"frameborder": 0,
173+
"sandbox": "allow-scripts allow-popups allow-forms allow-same-origin",
174+
"allowfullscreen": "",
175+
"style": "width: {width}px; height: {height}px; border-radius: 1px;".format(width=block.width, height=block.height),
176+
})
177+
return '<div style="text-align: center;">' + iframetag + "</iframe></div>"
178+
179+
def handle_video(self, block, level=0, preblock=None, postblock=None):
180+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
181+
182+
def handle_file(self, block, level=0, preblock=None, postblock=None):
183+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
184+
185+
def handle_audio(self, block, level=0, preblock=None, postblock=None):
186+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
187+
188+
def handle_pdf(self, block, level=0, preblock=None, postblock=None):
189+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
190+
191+
def handle_image(self, block, level=0, preblock=None, postblock=None):
192+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
193+
194+
def handle_bookmark(self, block, level=0, preblock=None, postblock=None):
195+
return bookmark_template.format(link=block.link, title=block.title, description=block.description, icon=block.bookmark_icon, cover=block.bookmark_cover)
196+
197+
def handle_link_to_collection(self, block, level=0, preblock=None, postblock=None):
198+
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})
199+
200+
def handle_breadcrumb(self, block, level=0, preblock=None, postblock=None):
201+
return self.wrap_in_tag(block, "p", attributes=self.left_margin_for_level(level))
202+
203+
def handle_collection_view(self, block, level=0, preblock=None, postblock=None):
204+
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})
205+
206+
def handle_collection_view_page(self, block, level=0, preblock=None, postblock=None):
207+
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})
208+
209+
def handle_framer(self, block, level=0, preblock=None, postblock=None):
210+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
211+
212+
def handle_tweet(self, block, level=0, preblock=None, postblock=None):
213+
return requests.get("https://publish.twitter.com/oembed?url=" + block.source).json()["html"]
214+
215+
def handle_gist(self, block, level=0, preblock=None, postblock=None):
216+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
217+
218+
def handle_drive(self, block, level=0, preblock=None, postblock=None):
219+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
220+
221+
def handle_figma(self, block, level=0, preblock=None, postblock=None):
222+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
223+
224+
def handle_loom(self, block, level=0, preblock=None, postblock=None):
225+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
226+
227+
def handle_typeform(self, block, level=0, preblock=None, postblock=None):
228+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
229+
230+
def handle_codepen(self, block, level=0, preblock=None, postblock=None):
231+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
232+
233+
def handle_maps(self, block, level=0, preblock=None, postblock=None):
234+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
235+
236+
def handle_invision(self, block, level=0, preblock=None, postblock=None):
237+
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)
238+
239+
def handle_callout(self, block, level=0, preblock=None, postblock=None):
240+
return callout_template.format(icon=block.icon, title=markdown2.markdown(block.title))
241+

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ bs4
44
tzlocal
55
python-slugify
66
dictdiffer
7-
cached-property
7+
cached-property
8+
markdown2

requirements_build.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
twine

0 commit comments

Comments
 (0)