Skip to content

Commit

Permalink
Implement HTMLRenderer methods (#106)
Browse files Browse the repository at this point in the history
Also add minimal page render
  • Loading branch information
chrisjsewell committed Mar 4, 2020
1 parent 97ac07d commit fb83d74
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 20 deletions.
38 changes: 34 additions & 4 deletions docs/using/use_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ print(render_tokens(root, HTMLRenderer))
<li>a list</li>
</ol>
<blockquote>
<p>a quote</p>
<p>a <em>quote</em></p>
</blockquote>
```

Expand All @@ -173,13 +173,43 @@ print(render_tokens(other, HTMLRenderer))
````
````html
<p><span class="role" name="role:name">content</span></p>
<pre><code class="language-{directive_name}">:option: a
<p><span class="myst-role"><code>{role:name}content</code></span></p>
<div class="myst-directive">
<pre><code>{directive_name} arg
:option: a
content
</code></pre>
</code></pre></span>
</div>
````
`````

You can also create a minmal page preview, including CSS:

```python
parse_text(
in_string,
"html",
add_mathjax=True,
as_standalone=True,
add_css=dedent(
"""\
div.myst-front-matter {
border: 1px solid gray;
}
div.myst-directive {
background: lightgreen;
}
hr.myst-block-break {
border-top:1px dotted black;
}
span.myst-role {
background: lightgreen;
}
"""
),
)
```

## Docutils Renderer

The `myst_parser.docutils_renderer.DocutilsRenderer` converts a token directly to the `docutils.document` representation of the document, converting roles and directives to a `docutils.nodes` if a converter can be found for the given name.
Expand Down
2 changes: 1 addition & 1 deletion myst_parser/block_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(self, lines):
if end_line is None:
end_line = len(lines)
self.range = (0, end_line)
self.content = "\n".join(lines[1 : end_line - 1])
self.content = "".join(lines[1 : end_line - 1])
self.children = []

@classmethod
Expand Down
78 changes: 68 additions & 10 deletions myst_parser/html_renderer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import html
from itertools import chain
import re
from textwrap import dedent

from mistletoe import block_token, span_token
from mistletoe import html_renderer
Expand All @@ -10,10 +11,19 @@


class HTMLRenderer(html_renderer.HTMLRenderer):
def __init__(self, add_mathjax=False):
"""This HTML render uses the same block/span tokens as the docutils renderer.
"""This HTML render uses the same block/span tokens as the docutils renderer.
It is used to test compliance with the commonmark spec.
It is used to test compliance with the commonmark spec,
and can be used for basic previews,
but does not run roles/directives, resolve cross-references etc...
"""

def __init__(self, add_mathjax=False, as_standalone=False, add_css=None):
"""Intitalise HTML renderer
:param add_mathjax: add the mathjax CDN
:param as_standalone: return the HTML body within a minmal HTML page
:param add_css: if as_standalone=True, CSS to add to the header
"""
self._suppress_ptag_stack = [False]

Expand Down Expand Up @@ -42,31 +52,57 @@ def __init__(self, add_mathjax=False):
'"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js'
'?config=TeX-MML-AM_CHTML"></script>\n'
)
self.as_standalone = as_standalone
self.add_css = add_css

def render_document(self, token):
"""
Optionally Append CDN link for MathJax to the end of <body>.
"""
return super().render_document(token) + self.mathjax_src
body = super().render_document(token) + self.mathjax_src
if not self.as_standalone:
return body
return minimal_html_page(body, css=self.add_css or "")

def render_code_fence(self, token):
if token.language and token.language.startswith("{"):
return self.render_directive(token)
return self.render_block_code(token)

def render_directive(self, token):
return (
'<div class="myst-directive">\n'
"<pre><code>{name} {args}\n{content}</code></pre></span>\n"
"</div>"
).format(
name=self.escape_html(token.language),
args=self.escape_html(token.arguments),
content=self.escape_html(token.children[0].content),
)

def render_front_matter(self, token):
raise NotImplementedError
return (
'<div class="myst-front-matter">'
'<pre><code class="language-yaml">{}</code></pre>'
"</div>"
).format(self.escape_html(token.content))

def render_line_comment(self, token):
return "<p>{}</p>".format(token.raw)
return "<!-- {} -->".format(self.escape_html(token.content))

def render_block_break(self, token):
return "<p>{}</p>".format(token.raw)
return '<!-- myst-block-data {} -->\n<hr class="myst-block-break" />'.format(
self.escape_html(token.content)
)

def render_target(self, token):
raise NotImplementedError
return (
'<a class="myst-target" href="#{0}" title="Permalink to here">({0})=</a>'
).format(self.escape_html(token.target))

def render_role(self, token):
return '<span class="role" name="{}">{}</span>'.format(
token.name, self.render_raw_text(token.children[0])
return ('<span class="myst-role"><code>{{{0}}}{1}</code></span>').format(
self.escape_html(token.name), self.render_raw_text(token.children[0])
)

def render_math(self, token):
Expand All @@ -76,3 +112,25 @@ def render_math(self, token):
if token.content.startswith("$$"):
return self.render_raw_text(token)
return "${}$".format(self.render_raw_text(token))


def minimal_html_page(
body: str, css: str = "", title: str = "Standalone HTML", lang: str = "en"
):
return dedent(
"""\
<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="utf-8">
<title>{title}</title>
<style>
{css}
</style>
</head>
<body>
{body}
</body>
</html>
"""
).format(title=title, lang=lang, css=css, body=body)
6 changes: 5 additions & 1 deletion tests/test_commonmark/test_commonmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@

@pytest.mark.parametrize("entry", tests)
def test_commonmark(entry):
if entry["example"] == 14:
# This is just a test that +++ are not parsed as thematic breaks
pytest.skip("Expects '+++' to be unconverted (not block break).")
if entry["example"] in [65, 67]:
# This is supported by numerous Markdown flavours, but not strictly CommonMark
# Front matter is supported by numerous Markdown flavours,
# but not strictly CommonMark,
# see: https://talk.commonmark.org/t/metadata-in-documents/721/86
pytest.skip(
"Thematic breaks on the first line conflict with front matter syntax"
Expand Down
97 changes: 93 additions & 4 deletions tests/test_renderers/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from myst_parser import text_to_tokens, render_tokens
from myst_parser import text_to_tokens, render_tokens, parse_text
from mistletoe.block_token import tokenize

from myst_parser.html_renderer import HTMLRenderer
Expand All @@ -27,13 +27,102 @@ def test_math(renderer):

def test_role(renderer):
output = renderer.render(tokenize(["{name}`content`"])[0])
assert output == dedent('<p><span class="role" name="name">content</span></p>')
assert output == (
'<p><span class="myst-role"><code>{name}content</code></span></p>'
)


def test_directive(renderer):
output = renderer.render(tokenize(["```{name} arg\n", "foo\n", "```\n"])[0])
assert output == dedent(
"""\
<pre><code class="language-{name}">foo
</code></pre>"""
<div class="myst-directive">
<pre><code>{name} arg
foo
</code></pre></span>
</div>"""
)


def test_block_break(renderer):
output = renderer.render(text_to_tokens("+++ abc"))
assert output.splitlines() == [
"<!-- myst-block-data abc -->",
'<hr class="myst-block-break" />',
]


def test_line_comment(renderer):
output = renderer.render(tokenize([r"% abc"])[0])
assert output == "<!-- abc -->"


def test_target():
output = parse_text("(a)=", "html")
assert output == (
'<p><a class="myst-target" href="#a" title="Permalink to here">(a)=</a></p>\n'
)


def test_front_matter(renderer):
output = renderer.render(text_to_tokens("---\na: 1\nb: 2\nc: 3\n---"))
assert output.splitlines() == [
'<div class="myst-front-matter"><pre><code class="language-yaml">a: 1',
"b: 2",
"c: 3",
"</code></pre></div>",
]


def test_minimal_html_page(file_regression):
in_string = dedent(
"""\
---
a: 1
---
(title-target)=
# title
Abc $a=1$ {role}`content` then more text
+++ my break
```{directive} args
:option: 1
content
```
```python
def func(a):
print("{}".format(a))
```
% a comment
[link to target](#title-target)
"""
)

out_string = parse_text(
in_string,
"html",
add_mathjax=True,
as_standalone=True,
add_css=dedent(
"""\
div.myst-front-matter {
border: 1px solid gray;
}
div.myst-directive {
background: lightgreen;
}
hr.myst-block-break {
border-top:1px dotted black;
}
span.myst-role {
background: lightgreen;
}
"""
),
)
file_regression.check(out_string, extension=".html")
44 changes: 44 additions & 0 deletions tests/test_renderers/test_html/test_minimal_html_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Standalone HTML</title>
<style>
div.myst-front-matter {
border: 1px solid gray;
}
div.myst-directive {
background: lightgreen;
}
hr.myst-block-break {
border-top:1px dotted black;
}
span.myst-role {
background: lightgreen;
}

</style>
</head>
<body>
<div class="myst-front-matter"><pre><code class="language-yaml">a: 1
</code></pre></div>
<p><a class="myst-target" href="#title-target" title="Permalink to here">(title-target)=</a></p>
<h1>title</h1>
<p>Abc $$a=1$$ <span class="myst-role"><code>{role}content</code></span> then more text</p>
<!-- myst-block-data my break -->
<hr class="myst-block-break" />
<div class="myst-directive">
<pre><code>{directive} args
:option: 1
content
</code></pre></span>
</div>
<pre><code class="language-python">def func(a):
print(&quot;{}&quot;.format(a))
</code></pre>
<!-- a comment -->
<p><a href="#title-target">link to target</a></p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-MML-AM_CHTML"></script>

</body>
</html>

0 comments on commit fb83d74

Please sign in to comment.