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 = """
+
+
+"""
+
+callout_template = """
+
+"""
+
+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}{tagname}>".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