# builtin
from os import path
import subprocess
import logging
import re
# packages
import yaml
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
# setup logger for this module
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Markdown to HTML renderer with support for LaTeX equations
# Inline math: $x$
# Block math: $$x$$ or \begin{equation}x\end{equation}
# ---------------------------------------------------------------------------
class MathBlockGrammar(mistune.BlockGrammar):
block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL)
latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", re.DOTALL)
class MathBlockLexer(mistune.BlockLexer):
default_rules = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_rules
def __init__(self, rules=None, **kwargs):
if rules is None:
rules = MathBlockGrammar()
super().__init__(rules, **kwargs)
def parse_block_math(self, m):
"""Parse a $$math$$ block"""
self.tokens.append({
'type': 'block_math',
'text': m.group(1)
})
def parse_latex_environment(self, m):
self.tokens.append({
'type': 'latex_environment',
'name': m.group(1),
'text': m.group(2)
})
class MathInlineGrammar(mistune.InlineGrammar):
math = re.compile(r"^\$(.+?)\$", re.DOTALL)
block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL)
text = re.compile(r'^[\s\S]+?(?=[\\' + header + '
' + body + ""
# def image(self, src, title, text):
# if src.startswith('javascript:'):
# src = ''
# text = mistune.escape(text, quote=True)
# if title:
# title = mistune.escape(title, quote=True)
# html = '
' % html
# return '%s>' % html
# Pass math through unaltered - mathjax does the rendering in the browser
def block_math(self, text):
return fr'$$ {text} $$'
def latex_environment(self, name, text):
return fr'\begin{{{name}}} {text} \end{{{name}}}'
def inline_math(self, text):
return fr'$$$ {text} $$$'
markdown = MarkdownWithMath(HighlightRenderer(escape=False)) # hard_wrap=True to insert
on newline
def md_to_html(text, q=None):
return markdown(text)
# ---------------------------------------------------------------------------
# load data from yaml file
# ---------------------------------------------------------------------------
def load_yaml(filename, default=None):
try:
f = open(path.expanduser(filename), 'r', encoding='utf-8')
except FileNotFoundError:
logger.error(f'Can\'t open "{script}": not found.')
return default
except PermissionError:
logger.error(f'Can\'t open "{script}": no permission.')
return default
except IOError:
logger.error(f'Can\'t open file "{filename}".')
return default
else:
with f:
try:
return yaml.load(f)
except yaml.YAMLError as e:
mark = e.problem_mark
logger.error(f'In YAML file "{filename}" near line {mark.line}, column {mark.column+1}.')
return default
# ---------------------------------------------------------------------------
# Runs a script and returns its stdout parsed as yaml, or None on error.
# ---------------------------------------------------------------------------
def run_script(script, stdin='', timeout=5):
script = path.expanduser(script)
try:
p = subprocess.run([script],
input=stdin,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
timeout=timeout,
)
except FileNotFoundError:
logger.error(f'Could not execute script "{script}": not found.')
except PermissionError:
logger.error(f'Could not execute script "{script}": wrong permissions.')
except OSError:
logger.error(f'Could not execute script "{script}": unknown reason.')
except subprocess.TimeoutExpired:
logger.error(f'Timeout exceeded ({timeout}s) while running "{script}".')
else:
if p.returncode != 0:
logger.error(f'Script "{script}" returned error code {p.returncode}.')
else:
try:
output = yaml.load(p.stdout)
except:
logger.error(f'Error parsing yaml output of "{script}"')
else:
return output