Skip to content

Add BaseRenderer to render page to text, and BaseHTMLRenderer as example #91

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
247 changes: 247 additions & 0 deletions notion/renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import markdown2
import requests

from .block import *


class BaseRenderer(object):

def __init__(self, start_block):
self.start_block = start_block

def render(self):
return self.render_block(self.start_block)

def calculate_child_indent(self, block):
if block.type == "page":
return 0
else:
return 1

def render_block(self, block, level=0, preblock=None, postblock=None):
assert isinstance(block, Block)
type_renderer = getattr(self, "handle_" + block._type, None)
if not callable(type_renderer):
if hasattr(self, "handle_default"):
type_renderer = self.handle_default
else:
raise Exception("No handler for block type '{}'.".format(block._type))
pretext = type_renderer(block, level=level, preblock=preblock, postblock=postblock)
if isinstance(pretext, tuple):
pretext, posttext = pretext
else:
posttext = ""
return pretext + self.render_children(block, level=level+self.calculate_child_indent(block)) + posttext

def render_children(self, block, level):
kids = block.children
if not kids:
return ""
text = ""
for i in range(len(kids)):
preblock = None
postblock = None
if i > 0:
preblock = kids[i-1]
if i < len(kids)-2:
postblock = kids[i + 1]
text += self.render_block(kids[i], level=level, preblock=preblock, postblock=postblock)
return text
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible simplification to "".join([self.render_block(child) for child in (block.children or [])])
(unless kids is always at least an array, then you can omit the or [])



bookmark_template = """
<div>
<div style="display: flex;">
<a target="_blank" rel="noopener noreferrer" href="{link}" style="display: block; color: inherit; text-decoration: none; flex-grow: 1; min-width: 0px;">
<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;">
<div style="flex: 4 1 180px; min-height: 60px; padding: 12px 14px 14px; overflow: hidden; text-align: left;">
<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>
<div style="font-size: 12px; line-height: 16px; color: rgba(55, 53, 47, 0.6); height: 32px; overflow: hidden;">{description}</div>
<div style="display: flex; margin-top: 6px;">
<img src="{icon}" style="width: 16px; height: 16px; min-width: 16px; margin-right: 6px;">
<div style="font-size: 12px; line-height: 16px; color: rgb(55, 53, 47); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{link}/div>
</div>
</div>
<div style="flex: 1 1 180px; min-height: 80px; display: block; position: relative;">
<div style="position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px;">
<div style="width: 100%; height: 100%;"><img src="{cover}" style="display: block; object-fit: cover; border-radius: 1px; width: 100%; height: 100%;"></div>
</div>
</div>
</div>
</a>
</div>
</div>
"""

callout_template = """
<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);">
<div>
<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;">
<div style="display: flex; align-items: center; justify-content: center; height: 24px; width: 24px;">
<div style="height: 16.8px; width: 16.8px; font-size: 16.8px; line-height: 1.1; margin-left: 0px; color: black;">{icon}</div>
</div>
</div>
</div>
<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>
</div>
"""

class BaseHTMLRenderer(BaseRenderer):

def create_opening_tag(self, tagname, attributes={}):
attrs = "".join(' {}="{}"'.format(key, val) for key, val in attributes.items())
return "<{tagname}{attrs}>".format(tagname=tagname, attrs=attrs)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use f"" strings? Python 3.6 feature though and it looks like this library targets 3.5+.


def wrap_in_tag(self, block, tagname, fieldname="title", attributes={}):
opentag = self.create_opening_tag(tagname, attributes)
innerhtml = markdown2.markdown(getattr(block, fieldname))
return "{opentag}{innerhtml}</{tagname}>".format(opentag=opentag, tagname=tagname, innerhtml=innerhtml)

def left_margin_for_level(self, level):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, inline styles would really complicate things for people wanting to post-process the output HTML or stick it into a HBS template or Angular/React/Vue app.

It'd be nicer if we could either add classes and provide a tiny CSS stylesheet (to make it easier to integrate into larger websites) or leave styling completely up to the dev calling this endpoint. This might be okay for now though, as a dev can always just Array.from(document.querySelectorAll('*')).forEach((el)=>el.setAttribute('style', '')) after importing.

return {"display": "margin-left: {}px;".format(level * 20)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this mean to be {"style"... and not {"display"...? for inline styles?


def handle_default(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "p", attributes=self.left_margin_for_level(level))

def handle_divider(self, block, level=0, preblock=None, postblock=None):
return "<hr/>"

def handle_column_list(self, block, level=0, preblock=None, postblock=None):
return '<div style="display: flex; padding-top: 12px; padding-bottom: 12px;">', '</div>'

def handle_column(self, block, level=0, preblock=None, postblock=None):
buffer = (len(block.parent.children) - 1) * 46
width = block.get("format.column_ratio")
return '<div style="flex-grow: 0; flex-shrink: 0; width: calc((100% - {}px) * {});">'.format(buffer, width), '</div>'

def handle_to_do(self, block, level=0, preblock=None, postblock=None):
return '<input type="checkbox" id="{id}" name="{id}"{checked}><label for="{id}">{title}</label><br/>'.format(
id="chk_" + block.id,
checked=" checked" if block.checked else "",
title=block.title,
)

def handle_code(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "code", attributes=self.left_margin_for_level(level))

def handle_factory(self, block, level=0, preblock=None, postblock=None):
return ""

def handle_header(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "h2", attributes=self.left_margin_for_level(level))

def handle_sub_header(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "h3", attributes=self.left_margin_for_level(level))

def handle_sub_sub_header(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "h4", attributes=self.left_margin_for_level(level))

def handle_page(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "h1", attributes=self.left_margin_for_level(level))

def handle_bulleted_list(self, block, level=0, preblock=None, postblock=None):
text = ""
if preblock is None or preblock.type != "bulleted_list":
text = self.create_opening_tag("ul", attributes=self.left_margin_for_level(level))
text += self.wrap_in_tag(block, "li")
if postblock is None or postblock.type != "bulleted_list":
text += "</ul>"
return text

def handle_numbered_list(self, block, level=0, preblock=None, postblock=None):
text = ""
if preblock is None or preblock.type != "numbered_list":
text = self.create_opening_tag("ol", attributes=self.left_margin_for_level(level))
text += self.wrap_in_tag(block, "li")
if postblock is None or postblock.type != "numbered_list":
text += "</ol>"
return text

def handle_toggle(self, block, level=0, preblock=None, postblock=None):
innerhtml = markdown2.markdown(block.title)
opentag = self.create_opening_tag("details", attributes=self.left_margin_for_level(level))
return '{opentag}<summary>{innerhtml}</summary>'.format(opentag=opentag, innerhtml=innerhtml), '</details>'

def handle_quote(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "blockquote", attributes=self.left_margin_for_level(level))

def handle_text(self, block, level=0, preblock=None, postblock=None):
return self.handle_default(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_equation(self, block, level=0, preblock=None, postblock=None):
text = self.create_opening_tag("p", attributes=self.left_margin_for_level(level))
return text + '<img src="https://chart.googleapis.com/chart?cht=tx&chl={}"/></p>'.format(block.latex)

def handle_embed(self, block, level=0, preblock=None, postblock=None):
iframetag = self.create_opening_tag("iframe", attributes={
"src": block.display_source or block.source,
"frameborder": 0,
"sandbox": "allow-scripts allow-popups allow-forms allow-same-origin",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these defaults for embed sandbox sourced from somewhere/based on a best practice/needed for some of the default embeds that notion supports?

"allowfullscreen": "",
"style": "width: {width}px; height: {height}px; border-radius: 1px;".format(width=block.width, height=block.height),
})
return '<div style="text-align: center;">' + iframetag + "</iframe></div>"

def handle_video(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_file(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_audio(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_pdf(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_image(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_bookmark(self, block, level=0, preblock=None, postblock=None):
return bookmark_template.format(link=block.link, title=block.title, description=block.description, icon=block.bookmark_icon, cover=block.bookmark_cover)

def handle_link_to_collection(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})

def handle_breadcrumb(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "p", attributes=self.left_margin_for_level(level))

def handle_collection_view(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})

def handle_collection_view_page(self, block, level=0, preblock=None, postblock=None):
return self.wrap_in_tag(block, "p", attributes={"href": "https://www.notion.so/" + block.id.replace("-", "")})

def handle_framer(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_tweet(self, block, level=0, preblock=None, postblock=None):
return requests.get("https://publish.twitter.com/oembed?url=" + block.source).json()["html"]

def handle_gist(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_drive(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_figma(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_loom(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_typeform(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_codepen(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_maps(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_invision(self, block, level=0, preblock=None, postblock=None):
return self.handle_embed(block=block, level=level, preblock=preblock, postblock=postblock)

def handle_callout(self, block, level=0, preblock=None, postblock=None):
return callout_template.format(icon=block.icon, title=markdown2.markdown(block.title))

3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ bs4
tzlocal
python-slugify
dictdiffer
cached-property
cached-property
markdown2
1 change: 1 addition & 0 deletions requirements_build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
twine