diff --git a/notion/renderer.py b/notion/renderer.py new file mode 100644 index 0000000..46c16f0 --- /dev/null +++ b/notion/renderer.py @@ -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 + + +bookmark_template = """ +
+
+ +
+
+
{title}
+
{description}
+
+ +
{link}/div> +
+
+
+
+
+
+
+
+
+
+
+""" + +callout_template = """ +
+
+
+
+
{icon}
+
+
+
+
{title}
+
+""" + +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) + + 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}".format(opentag=opentag, tagname=tagname, innerhtml=innerhtml) + + def left_margin_for_level(self, level): + return {"display": "margin-left: {}px;".format(level * 20)} + + 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 "
" + + def handle_column_list(self, block, level=0, preblock=None, postblock=None): + return '
', '
' + + 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 '
'.format(buffer, width), '
' + + def handle_to_do(self, block, level=0, preblock=None, postblock=None): + return '
'.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 += "" + 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 += "" + 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}{innerhtml}'.format(opentag=opentag, innerhtml=innerhtml), '' + + 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 + '

'.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", + "allowfullscreen": "", + "style": "width: {width}px; height: {height}px; border-radius: 1px;".format(width=block.width, height=block.height), + }) + return '
' + iframetag + "
" + + 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)) + diff --git a/requirements.txt b/requirements.txt index 2ff84eb..c25cb42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ bs4 tzlocal python-slugify dictdiffer -cached-property \ No newline at end of file +cached-property +markdown2 \ No newline at end of file diff --git a/requirements_build.txt b/requirements_build.txt new file mode 100644 index 0000000..c9133d6 --- /dev/null +++ b/requirements_build.txt @@ -0,0 +1 @@ +twine \ No newline at end of file