Skip to content
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

Update render inline code #227

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
50 changes: 24 additions & 26 deletions mistletoe/latex_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@
LaTeX renderer for mistletoe.
"""

import string
import re
from itertools import chain
from urllib.parse import quote
import mistletoe.latex_token as latex_token
from mistletoe.base_renderer import BaseRenderer

# (customizable) delimiters for inline code
verb_delimiters = string.punctuation + string.digits
for delimiter in '*': # remove invalid delimiters
verb_delimiters.replace(delimiter, '')
for delimiter in reversed('|!"\'=+'): # start with most common delimiters
verb_delimiters = delimiter + verb_delimiters.replace(delimiter, '')


class LaTeXRenderer(BaseRenderer):
def __init__(self, *extras, **kwargs):
Expand All @@ -26,7 +19,6 @@ def __init__(self, *extras, **kwargs):
"""
tokens = self._tokens_from_module(latex_token)
self.packages = {}
self.verb_delimiters = verb_delimiters
super().__init__(*chain(tokens, extras), **kwargs)

def render_strong(self, token):
Expand All @@ -36,18 +28,15 @@ def render_emphasis(self, token):
return '\\textit{{{}}}'.format(self.render_inner(token))

def render_inline_code(self, token):
content = self.render_raw_text(token.children[0], escape=False)

# search for delimiter not present in content
for delimiter in self.verb_delimiters:
if delimiter not in content:
break
# fontenc to get better results for `_{}\` in `\texttt{}`
self.packages['fontenc'] = ['T1']

if delimiter in content: # no delimiter found
raise RuntimeError('Unable to find delimiter for verb macro')
content = self.render_raw_text(token.children[0], escape=True)
# make \texttt behave like \verb w.r.t. whitespace in inline code
content = re.sub(r'\s{2,}', lambda m: '\\ '*len(m.group(0)), content)

template = '\\verb{delimiter}{content}{delimiter}'
return template.format(delimiter=delimiter, content=content)
template = '\\texttt{{{content}}}'
return template.format(content=content)

def render_strikethrough(self, token):
self.packages['ulem'] = ['normalem']
Expand Down Expand Up @@ -78,11 +67,20 @@ def render_escape_sequence(self, token):
return self.render_inner(token)

def render_raw_text(self, token, escape=True):
return (token.content.replace('$', '\\$').replace('#', '\\#')
.replace('{', '\\{').replace('}', '\\}')
.replace('&', '\\&').replace('_', '\\_')
.replace('%', '\\%')
) if escape else token.content
"""Escape all latex special characters $#&%_{}^~\\ within `token.content`.
"""
if not escape:
return token.content

if not hasattr(self, 'raw_escape_chars'):
self.raw_escape_chars = re.compile('([$#&%_{}])')

content = token.content.replace('\\', '\\textbackslash')
content = self.raw_escape_chars.sub(r'\\\1', content)
# The \text* commands gobble up whitespace behind them -> {} to prevent that.
return content.replace('~', '\\textasciitilde{}') \
.replace('^', '\\textasciicircum{}') \
.replace('\\textbackslash', '\\textbackslash{}')

def render_heading(self, token):
inner = self.render_inner(token)
Expand Down Expand Up @@ -165,8 +163,8 @@ def render_line_break(token):
return '\n' if token.soft else '\\newline\n'

def render_packages(self):
pattern = '\\usepackage{options}{{{package}}}\n'
return ''.join(pattern.format(options=options or '', package=package)
pattern = '\\usepackage[{options}]{{{package}}}\n'
return ''.join(pattern.format(options=', '.join(options) or '', package=package)
for package, options in self.packages.items())

def render_document(self, token):
Expand Down
63 changes: 41 additions & 22 deletions test/test_latex_renderer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from unittest import TestCase, mock
from parameterized import parameterized
import mistletoe.latex_renderer
from mistletoe.latex_renderer import LaTeXRenderer
from mistletoe import markdown

Expand All @@ -13,36 +12,34 @@ def setUp(self):
self.addCleanup(self.renderer.__exit__, None, None, None)

def _test_token(self, token_name, expected_output, children=True,
without_attrs=None, **kwargs):
without_attrs=None, render_func_kwargs={}, **kwargs):
render_func = self.renderer.render_map[token_name]
children = mock.MagicMock(spec=list) if children else None
mock_token = mock.Mock(children=children, **kwargs)
without_attrs = without_attrs or []
for attr in without_attrs:
delattr(mock_token, attr)
self.assertEqual(render_func(mock_token), expected_output)
self.assertEqual(render_func(mock_token, **render_func_kwargs), expected_output)

def test_strong(self):
self._test_token('Strong', '\\textbf{inner}')

def test_emphasis(self):
self._test_token('Emphasis', '\\textit{inner}')

def test_inline_code(self):
@parameterized.expand([
('inner', '\\texttt{inner}'),
('a + b', '\\texttt{a + b}'),
('a | b', '\\texttt{a | b}'),
('|ab!|', '\\texttt{|ab!|}'),
('two spaces', '\\texttt{two\\ \\ spaces}'),
('two\t whitespaces', '\\texttt{two\\ \\ whitespaces}'),
])
def test_inline_code(self, content, expected):
func_path = 'mistletoe.latex_renderer.LaTeXRenderer.render_raw_text'

for content, expected in {'inner': '\\verb|inner|',
'a + b': '\\verb|a + b|',
'a | b': '\\verb!a | b!',
'|ab!|': '\\verb"|ab!|"',
}.items():
with mock.patch(func_path, return_value=content):
self._test_token('InlineCode', expected, content=content)

content = mistletoe.latex_renderer.verb_delimiters
with self.assertRaises(RuntimeError):
with mock.patch(func_path, return_value=content):
self._test_token('InlineCode', None, content=content)
with mock.patch(func_path, return_value=content):
self._test_token('InlineCode', expected, content=content)

def test_strikethrough(self):
self._test_token('Strikethrough', '\\sout{inner}')
Expand Down Expand Up @@ -72,10 +69,25 @@ def test_math(self):
self._test_token('Math', expected,
children=False, content='$ 1 + 2 = 3 $')

def test_raw_text(self):
expected = '\\$\\&\\#\\{\\}'
self._test_token('RawText', expected,
children=False, content='$&#{}')
@parameterized.expand([
('$', '\\$'),
('&', '\\&'),
('#', '\\#'),
('%', '\\%'),
('_', '\\_'),
('{', '\\{'),
('}', '\\}'),
('~', '\\textasciitilde{}'),
('^', '\\textasciicircum{}'),
('\\', '\\textbackslash{}'),
])
def test_raw_text(self, target, expected):
self._test_token('RawText', expected, children=False, content=target)

def test_raw_text_no_escape(self):
expected = '$&#%_{}~^\\'
self._test_token('RawText', expected, children=False, content=expected,
render_func_kwargs={'escape': False})

def test_heading(self):
expected = '\n\\section{inner}\n'
Expand Down Expand Up @@ -132,6 +144,13 @@ def test_document(self):
'\\end{document}\n')
self._test_token('Document', expected, footnotes={})

@parameterized.expand([
({'ulem': ['normalem']}, '\\usepackage[normalem]{ulem}\n'),
({'hyperref': []}, '\\usepackage[]{hyperref}\n'),
])
def test_packages(self, packages, expected):
self.renderer.packages = packages
self.assertEqual(self.renderer.render_packages(), expected)

class TestHtmlEntity(TestCase):
def test_html_entity(self):
Expand All @@ -151,7 +170,7 @@ def test_footnote_image(self):
from mistletoe import Document
raw = ['![alt][foo]\n', '\n', '[foo]: bar "title"\n']
expected = ('\\documentclass{article}\n'
'\\usepackage{graphicx}\n'
'\\usepackage[]{graphicx}\n'
'\\begin{document}\n'
'\n'
'\n\\includegraphics{bar}\n'
Expand All @@ -163,7 +182,7 @@ def test_footnote_link(self):
from mistletoe import Document
raw = ['[name][key]\n', '\n', '[key]: target\n']
expected = ('\\documentclass{article}\n'
'\\usepackage{hyperref}\n'
'\\usepackage[]{hyperref}\n'
'\\begin{document}\n'
'\n'
'\\href{target}{name}'
Expand Down
Loading