From 8705085200384622f4ebe3db8131e6f06f17b899 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 21 Mar 2011 10:28:03 +0900 Subject: [PATCH] update latex support --- anki/deck.py | 9 +-- anki/latex.py | 130 ++++++++++++++++++++------------------------ anki/media.py | 2 + anki/utils.py | 14 ++++- tests/test_latex.py | 58 ++++++++++++++++++++ 5 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 tests/test_latex.py diff --git a/anki/deck.py b/anki/deck.py index 3b5c37b79..143dd15c9 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -422,7 +422,7 @@ select id from cards where fid in (select id from facts where mid = ?)""", return [self._renderQA(mods[row[2]], groups[row[3]], row) for row in self._qaData(where)] - def _renderQA(self, model, gname, data, filters=True): + def _renderQA(self, model, gname, data): "Returns hash of id, question, answer." # data is [cid, fid, mid, gid, ord, tags, flds, data] # unpack fields and create dict @@ -446,12 +446,9 @@ select id from cards where fid in (select id from facts where mid = ?)""", # render q & a d = dict(id=data[0]) for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])): - # if filters: - # fields = runFilter("renderQA.pre", fields, , self) + fields = runFilter("mungeFields", fields, model, gname, data, self) html = anki.template.render(format, fields) - # if filters: - # d[type] = runFilter("renderQA.post", html, fields, meta, self) - d[type] = html + d[type] = runFilter("mungeQA", html, fields, model, gname, data, self) return d def _qaData(self, where=""): diff --git a/anki/latex.py b/anki/latex.py index 97a847593..62fda9525 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -8,8 +8,9 @@ from anki.hooks import addHook from htmlentitydefs import entitydefs from anki.lang import _ +latexCmd = ["latex", "-interaction=nonstopmode"] latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"] - +build = True # if off, use existing media but don't create new regexps = { "standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE), "expression": re.compile(r"\[\$\](.+?)\[/\$\]", re.DOTALL | re.IGNORECASE), @@ -22,21 +23,6 @@ tmpdir = tempfile.mkdtemp(prefix="anki") if sys.platform == "darwin": os.environ['PATH'] += ":/usr/texbin" -def renderLatex(deck, text, build=True): - "Convert TEXT with embedded latex tags to image links." - for match in regexps['standard'].finditer(text): - text = text.replace(match.group(), imgLink(deck, match.group(1), - build)) - for match in regexps['expression'].finditer(text): - text = text.replace(match.group(), imgLink( - deck, "$" + match.group(1) + "$", build)) - for match in regexps['math'].finditer(text): - text = text.replace(match.group(), imgLink( - deck, - "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", - build)) - return text - def stripLatex(text): for match in regexps['standard'].finditer(text): text = text.replace(match.group(), "") @@ -46,23 +32,50 @@ def stripLatex(text): text = text.replace(match.group(), "") return text -def latexImgFile(deck, latexCode): - key = checksum(latexCode) - return "latex-%s.png" % key +def mungeQA(html, fields, model, gname, data, deck): + "Convert TEXT with embedded latex tags to image links." + for match in regexps['standard'].finditer(html): + html = html.replace(match.group(), _imgLink(deck, match.group(1))) + for match in regexps['expression'].finditer(html): + html = html.replace(match.group(), _imgLink( + deck, "$" + match.group(1) + "$")) + for match in regexps['math'].finditer(html): + html = html.replace(match.group(), _imgLink( + deck, + "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}")) + return html -def mungeLatex(deck, latex): - "Convert entities, fix newlines, convert to utf8, and wrap pre/postamble." +def _imgLink(deck, latex): + "Return an img link for LATEX, creating if necesssary." + txt = _latexFromHtml(deck, latex) + fname = "latex-%s.png" % checksum(txt) + link = '' % fname + if os.path.exists(fname): + return link + elif not build: + return "[latex]"+latex+"[/latex]" + else: + err = _buildImg(deck, txt, fname) + if err: + return err + else: + return link + +def _latexFromHtml(deck, latex): + "Convert entities, fix newlines, and convert to utf8." for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex): if match.group(1) in entitydefs: latex = latex.replace(match.group(), entitydefs[match.group(1)]) latex = re.sub("", "\n", latex) - latex = (deck.getVar("latexPre") + "\n" + - latex + "\n" + - deck.getVar("latexPost")) latex = latex.encode("utf-8") return latex -def buildImg(deck, latex): +def _buildImg(deck, latex, fname): + # add header/footer + latex = (deck.conf["latexPre"] + "\n" + + latex + "\n" + + deck.conf["latexPost"]) + # write into a temp file log = open(os.path.join(tmpdir, "latex_log.txt"), "w+") texpath = os.path.join(tmpdir, "tmp.tex") texfile = file(texpath, "w") @@ -71,60 +84,33 @@ def buildImg(deck, latex): # make sure we have a valid mediaDir mdir = deck.media.dir(create=True) oldcwd = os.getcwd() - if sys.platform == "win32": - si = subprocess.STARTUPINFO() - try: - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - except: - si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW - else: - si = None try: + # generate dvi os.chdir(tmpdir) - def errmsg(type): - msg = _("Error executing %s.\n") % type - try: - log = open(os.path.join(tmpdir, "latex_log.txt")).read() - msg += "
" + cgi.escape(log) + "
" - except: - msg += _("Have you installed latex and dvipng?") - pass - return msg - if call(["latex", "-interaction=nonstopmode", - "tmp.tex"], stdout=log, stderr=log, startupinfo=si): - return (False, errmsg("latex")) + if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log): + return _errMsg("latex") + # and png if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"], - stdout=log, stderr=log, startupinfo=si): - return (False, errmsg("dvipng")) + stdout=log, stderr=log): + return _errMsg("dvipng") # add to media - target = latexImgFile(deck, latex) shutil.copy2(os.path.join(tmpdir, "tmp.png"), - os.path.join(mdir, target)) - return (True, target) + os.path.join(mdir, fname)) + return finally: os.chdir(oldcwd) -def imageForLatex(deck, latex, build=True): - "Return an image that represents 'latex', building if necessary." - imageFile = latexImgFile(deck, latex) - ok = True - if build and (not imageFile or not os.path.exists(imageFile)): - (ok, imageFile) = buildImg(deck, latex) - if not ok: - return (False, imageFile) - return (True, imageFile) - -def imgLink(deck, latex, build=True): - "Parse LATEX and return a HTML image representing the output." - munged = mungeLatex(deck, latex) - (ok, img) = imageForLatex(deck, munged, build) - if ok: - return '%s' % (img, latex) - else: - return img - -def formatQA(html, type, cid, mid, fact, tags, cm, deck): - return renderLatex(deck, html) +def _errMsg(type): + msg = (_("Error executing %s.") % type) + "
" + try: + log = open(os.path.join(tmpdir, "latex_log.txt")).read() + if not log: + raise Exception() + msg += "
" + cgi.escape(log) + "
" + except: + msg += _("Have you installed latex and dvipng?") + pass + return msg # setup q/a filter -addHook("formatQA", formatQA) +addHook("mungeQA", mungeQA) diff --git a/anki/media.py b/anki/media.py index 2a5813405..a8c313b72 100644 --- a/anki/media.py +++ b/anki/media.py @@ -130,6 +130,8 @@ If the same name exists, compare checksums.""" # loop through directory and find unused & missing media unused = [] for file in os.listdir(mdir): + if file.startswith("latex-"): + continue path = os.path.join(mdir, file) if not os.path.isfile(path): # ignore directories diff --git a/anki/utils.py b/anki/utils.py index 49516160e..895e8ba59 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -276,11 +276,23 @@ def fieldChecksum(data): return int(checksum(data.encode("utf-8"))[:8], 16) def call(argv, wait=True, **kwargs): + "Execute a command. If WAIT, return exit code." + # ensure we don't open a separate window for forking process on windows + if sys.platform == "win32": + si = subprocess.STARTUPINFO() + try: + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + except: + si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW + else: + si = None + # run try: - o = subprocess.Popen(argv, **kwargs) + o = subprocess.Popen(argv, startupinfo=si, **kwargs) except OSError: # command not found return -1 + # wait for command to finish if wait: while 1: try: diff --git a/tests/test_latex.py b/tests/test_latex.py new file mode 100644 index 000000000..8e5af7467 --- /dev/null +++ b/tests/test_latex.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +import os +from tests.shared import assertException, getEmptyDeck +from anki.stdmodels import BasicModel +from anki.utils import stripHTML, intTime +from anki.hooks import addHook + +def test_latex(): + d = getEmptyDeck() + # no media directory to start + assert not d.media.dir() + # change latex cmd to simulate broken build + import anki.latex + anki.latex.latexCmd[0] = "nolatex" + # add a fact with latex + f = d.newFact() + f['Front'] = u"[latex]hello[/latex]" + d.addFact(f) + # adding will have created the media + assert d.media.dir() + # but since latex couldn't run, it will be empty + assert len(os.listdir(d.media.dir())) == 0 + # check the error message + msg = f.cards()[0].q() + assert "executing latex" in msg + assert "installed" in msg + # check if we have latex installed, and abort test if we don't + if not os.path.exists("/usr/bin/latex"): + print "aborting test; latex is not installed" + return + # fix path + anki.latex.latexCmd[0] = "latex" + # check media db should cause latex to be generated + d.media.check() + assert len(os.listdir(d.media.dir())) == 1 + assert ".png" in f.cards()[0].q() + # adding new facts should cause immediate generation + f = d.newFact() + f['Front'] = u"[latex]world[/latex]" + d.addFact(f) + assert len(os.listdir(d.media.dir())) == 2 + # another fact with the same media should reuse + f = d.newFact() + f['Front'] = u" [latex]world[/latex]" + d.addFact(f) + assert len(os.listdir(d.media.dir())) == 2 + oldcard = f.cards()[0] + assert ".png" in oldcard.q() + # if we turn off building, then previous cards should work, but cards with + # missing media will show the latex + anki.latex.build = False + f = d.newFact() + f['Front'] = u"[latex]foo[/latex]" + d.addFact(f) + assert len(os.listdir(d.media.dir())) == 2 + assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]" + assert ".png" in oldcard.q()