1# -*- coding: utf-8 -*-
2# Copyright: Ankitects Pty Ltd and contributors
3# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
4
5import re, os, shutil, html
6from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
7from anki.hooks import addHook
8from anki.lang import _
9
10pngCommands = [
11    ["latex", "-interaction=nonstopmode", "tmp.tex"],
12    ["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"]
13]
14
15svgCommands = [
16    ["latex", "-interaction=nonstopmode", "tmp.tex"],
17    ["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"]
18]
19
20build = True # if off, use existing media but don't create new
21regexps = {
22    "standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
23    "expression": re.compile(r"\[\$\](.+?)\[/\$\]", re.DOTALL | re.IGNORECASE),
24    "math": re.compile(r"\[\$\$\](.+?)\[/\$\$\]", re.DOTALL | re.IGNORECASE),
25    }
26
27# add standard tex install location to osx
28if isMac:
29    os.environ['PATH'] += ":/usr/texbin:/Library/TeX/texbin"
30
31def stripLatex(text):
32    for match in regexps['standard'].finditer(text):
33        text = text.replace(match.group(), "")
34    for match in regexps['expression'].finditer(text):
35        text = text.replace(match.group(), "")
36    for match in regexps['math'].finditer(text):
37        text = text.replace(match.group(), "")
38    return text
39
40def mungeQA(html, type, fields, model, data, col):
41    "Convert TEXT with embedded latex tags to image links."
42    for match in regexps['standard'].finditer(html):
43        html = html.replace(match.group(), _imgLink(col, match.group(1), model))
44    for match in regexps['expression'].finditer(html):
45        html = html.replace(match.group(), _imgLink(
46            col, "$" + match.group(1) + "$", model))
47    for match in regexps['math'].finditer(html):
48        html = html.replace(match.group(), _imgLink(
49            col,
50            "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model))
51    return html
52
53def _imgLink(col, latex, model):
54    "Return an img link for LATEX, creating if necesssary."
55    txt = _latexFromHtml(col, latex)
56
57    if model.get("latexsvg", False):
58        ext = "svg"
59    else:
60        ext = "png"
61
62    # is there an existing file?
63    fname = "latex-%s.%s" % (checksum(txt.encode("utf8")), ext)
64    link = '<img class=latex src="%s">' % fname
65    if os.path.exists(fname):
66        return link
67
68    # building disabled?
69    if not build:
70        return "[latex]%s[/latex]" % latex
71
72    err = _buildImg(col, txt, fname, model)
73    if err:
74        return err
75    else:
76        return link
77
78def _latexFromHtml(col, latex):
79    "Convert entities and fix newlines."
80    latex = re.sub("<br( /)?>|<div>", "\n", latex)
81    latex = stripHTML(latex)
82    return latex
83
84def _buildImg(col, latex, fname, model):
85    # add header/footer
86    latex = (model["latexPre"] + "\n" +
87             latex + "\n" +
88             model["latexPost"])
89    # it's only really secure if run in a jail, but these are the most common
90    tmplatex = latex.replace("\\includegraphics", "")
91    for bad in ("\\write18", "\\readline", "\\input", "\\include",
92                "\\catcode", "\\openout", "\\write", "\\loop",
93                "\\def", "\\shipout"):
94        # don't mind if the sequence is only part of a command
95        bad_re = "\\" + bad + "[^a-zA-Z]"
96        if re.search(bad_re, tmplatex):
97            return _("""\
98For security reasons, '%s' is not allowed on cards. You can still use \
99it by placing the command in a different package, and importing that \
100package in the LaTeX header instead.""") % bad
101
102    # commands to use?
103    if model.get("latexsvg", False):
104        latexCmds = svgCommands
105        ext = "svg"
106    else:
107        latexCmds = pngCommands
108        ext = "png"
109
110    # write into a temp file
111    log = open(namedtmp("latex_log.txt"), "w")
112    texpath = namedtmp("tmp.tex")
113    texfile = open(texpath, "w", encoding="utf8")
114    texfile.write(latex)
115    texfile.close()
116    mdir = col.media.dir()
117    oldcwd = os.getcwd()
118    png = namedtmp("tmp.%s" % ext)
119    try:
120        # generate png
121        os.chdir(tmpdir())
122        for latexCmd in latexCmds:
123            if call(latexCmd, stdout=log, stderr=log):
124                return _errMsg(latexCmd[0], texpath)
125        # add to media
126        shutil.copyfile(png, os.path.join(mdir, fname))
127        return
128    finally:
129        os.chdir(oldcwd)
130        log.close()
131
132def _errMsg(type, texpath):
133    msg = (_("Error executing %s.") % type) + "<br>"
134    msg += (_("Generated file: %s") % texpath) + "<br>"
135    try:
136        with open(namedtmp("latex_log.txt", rm=False)) as f:
137            log = f.read()
138        if not log:
139            raise Exception()
140        msg += "<small><pre>" + html.escape(log) + "</pre></small>"
141    except:
142        msg += _("Have you installed latex and dvipng/dvisvgm?")
143    return msg
144
145# setup q/a filter
146addHook("mungeQA", mungeQA)
147