Repository: astariul/swole (language: python)

Issue:
Add markdown parser
Add the markdown parser in the `Markdown` widget.


Discussion / hints:

Fix patch:
diff --git a/examples/texture.py b/examples/texture.py
index ef205de..50907e8 100644
--- a/examples/texture.py
+++ b/examples/texture.py
@@ -29,6 +29,31 @@
 Header(level=5, center=True)
 Header(level=6, center=True)
 
+Markdown("""# Title 1
+## Title 2
+
+Markdown.
+Markdown.
+
+This is a list :
+* Element 1
+* Element 2
+
+Normal text
+**Bold text**
+*Italic text*
+_Italic text_
+
+---
+
+Horizontal ruler.
+
+Code `inside` the line.
+
+```python
+Full code
+```""")
+
 
 if __name__ == "__main__":
     Application().serve()
diff --git a/requirements.txt b/requirements.txt
index 07024e7..6fc365f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 dominate==2.5.2
 fastapi==0.61.1
 uvicorn==0.11.8
-aiofiles==0.5.0
\ No newline at end of file
+aiofiles==0.5.0
+markdown-it-py==0.5.6
\ No newline at end of file
diff --git a/swole/widgets/markdown.py b/swole/widgets/markdown.py
index 07d447a..7f48b01 100644
--- a/swole/widgets/markdown.py
+++ b/swole/widgets/markdown.py
@@ -1,8 +1,82 @@
-from dominate.tags import p
+import dominate
+from dominate.tags import div, code, pre
+from markdown_it import MarkdownIt
+from markdown_it.renderer import RendererHTML
+from markdown_it.common.utils import escapeHtml
 
 from swole.widgets.base import Widget
 
 
+class DominateRenderer(RendererHTML):
+    """ Custom renderer for markdown-it parser. It render Markdown using
+    Dominate tags.
+    """
+    def _render(self, tokens, options, env):
+        """ Recursive function parsing each token into the right Dominate tag.
+        """
+        pending_tags = []
+        pending_content = [[]]
+        for t, token in enumerate(tokens):
+            if token.type == "fence":       # Special case
+                pending_content[-1].append(self.fence(tokens, t, options, env))
+            elif token.tag != "":
+                if not token.nesting:       # Directly append to content
+                    c = [token.content] if token.content else []
+                    tag = getattr(dominate.tags, token.tag)
+                    tag = tag(*c) if token.attrs is None else tag(*c, **token.attrs)
+                    pending_content[-1].append(tag)
+                elif len(pending_tags) > 0 and pending_tags[-1] == token.tag:       # Closing tag
+                    t = pending_tags.pop()
+                    c = pending_content.pop()
+                    tag = getattr(dominate.tags, t)
+                    tag = tag(c) if token.attrs is None else tag(c, **token.attrs)
+                    pending_content[-1].append(tag)
+                else:       # Opening tag
+                    if token.tag == "p" and len(pending_tags) > 0 and pending_tags[-1] == "li":
+                        continue
+
+                    pending_tags.append(token.tag)
+                    pending_content.append([])
+            elif token.children is not None:
+                assert len(token.children) > 0
+                pending_content[-1].extend(self._render(token.children, options, env))
+            else:
+                if not token.hidden:
+                    pending_content[-1].append(escapeHtml(token.content))
+
+        assert len(pending_tags) == 0, pending_tags
+        assert len(pending_content) == 1, pending_content
+
+        return pending_content[-1]
+
+    def render(self, tokens, options, env):
+        """ Takes token stream and generates Dominate HTML.
+        Check the default implementation at :
+        https://github.com/executablebooks/markdown-it-py/blob/master/markdown_it/renderer.py
+        """
+        content = self._render(tokens, options, env)
+
+        d = div()
+        d.add(*content)
+        return d
+
+    def fence(self, tokens, idx, options, env):
+        token = tokens[idx]
+        # TODO : Later, add correct highlights for languages
+        # info = unescapeAll(token.info).strip() if token.info else ""
+        # langName = ""
+
+        # if info:
+        #     langName = info.split()[0]
+
+        highlighted = escapeHtml(token.content)
+
+        if token.attrs:
+            return pre(code(highlighted), **token.attr)
+        else:
+            return pre(code(highlighted))
+
+
 class Markdown(Widget):
     """ A general widget to write Markdown. """
     def __init__(self, content="", **kwargs):
@@ -14,8 +88,10 @@ def __init__(self, content="", **kwargs):
         self.content = content
 
     def html(self):
-        # TODO : Full markdown parsing + convertion to dominate tags
-        return p(self.content, id=self.id)
+        md = MarkdownIt(renderer_cls=DominateRenderer).enable('table')
+        html = md.render(self.content)
+        html.attributes["id"] = self.id
+        return html
 
     def get(self):
         return self.content


Test patch:
diff --git a/tests/test_markdown.py b/tests/test_markdown.py
new file mode 100644
index 0000000..eca43fb
--- /dev/null
+++ b/tests/test_markdown.py
@@ -0,0 +1,27 @@
+import pytest
+from markdown_it import MarkdownIt
+from swole.widgets.markdown import DominateRenderer
+
+
+@pytest.mark.parametrize("inp, out", [
+    ("# Title 1", "<div><h1>Title 1</h1></div>"),
+    ("## Title 2", "<div><h2>Title 2</h2></div>"),
+    ("### Title 3", "<div><h3>Title 3</h3></div>"),
+    ("#### Title 4", "<div><h4>Title 4</h4></div>"),
+    ("##### Title 5", "<div><h5>Title 5</h5></div>"),
+    ("###### Title 6", "<div><h6>Title 6</h6></div>"),
+    ("Paragraph 1\n\nParagraph 2", "<div><p>Paragraph 1</p><p>Paragraph 2</p></div>"),
+    ("This *is* right", "<div><p>This <em>is</em> right</p></div>"),
+    ("This **is** right", "<div><p>This <strong>is</strong> right</p></div>"),
+    ("This _is_ right", "<div><p>This <em>is</em> right</p></div>"),
+    ("Paragraph 1\n\n---\n\nParapgrah 2", "<div><p>Paragraph 1</p><hr><p>Parapgrah 2</p></div>"),
+    ("* List 1\n* List 2", "<div><ul><li>List 1</li><li>List 2</li></ul></div>"),
+    ("This `is` code", "<div><p>This <code>is</code> code</p></div>"),
+    ("```python\nis\n```", "<div><pre><code>is\n</code></pre></div>"),
+    ("Line 1\nLine 2", "<div><p>Line 1<br>Line 2</p></div>"),
+    ])
+def test_renderer(inp, out):
+    md = MarkdownIt(renderer_cls=DominateRenderer).enable('table')
+    dominate = md.render(inp)
+    print()
+    assert dominate.render(pretty=False) == out