mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
update latex support
This commit is contained in:
parent
da0fb0c555
commit
8705085200
5 changed files with 134 additions and 79 deletions
|
@ -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=""):
|
||||
|
|
128
anki/latex.py
128
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 = '<img src="%s">' % 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("<br( /)?>", "\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
|
||||
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):
|
||||
return _errMsg("dvipng")
|
||||
# add to media
|
||||
shutil.copy2(os.path.join(tmpdir, "tmp.png"),
|
||||
os.path.join(mdir, fname))
|
||||
return
|
||||
finally:
|
||||
os.chdir(oldcwd)
|
||||
|
||||
def _errMsg(type):
|
||||
msg = (_("Error executing %s.") % type) + "<br>"
|
||||
try:
|
||||
log = open(os.path.join(tmpdir, "latex_log.txt")).read()
|
||||
if not log:
|
||||
raise Exception()
|
||||
msg += "<small><pre>" + cgi.escape(log) + "</pre></small>"
|
||||
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(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"],
|
||||
stdout=log, stderr=log, startupinfo=si):
|
||||
return (False, 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)
|
||||
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 '<img src="%s" alt="%s">' % (img, latex)
|
||||
else:
|
||||
return img
|
||||
|
||||
def formatQA(html, type, cid, mid, fact, tags, cm, deck):
|
||||
return renderLatex(deck, html)
|
||||
|
||||
# setup q/a filter
|
||||
addHook("formatQA", formatQA)
|
||||
addHook("mungeQA", mungeQA)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
o = subprocess.Popen(argv, **kwargs)
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
except:
|
||||
si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW
|
||||
else:
|
||||
si = None
|
||||
# run
|
||||
try:
|
||||
o = subprocess.Popen(argv, startupinfo=si, **kwargs)
|
||||
except OSError:
|
||||
# command not found
|
||||
return -1
|
||||
# wait for command to finish
|
||||
if wait:
|
||||
while 1:
|
||||
try:
|
||||
|
|
58
tests/test_latex.py
Normal file
58
tests/test_latex.py
Normal file
|
@ -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()
|
Loading…
Reference in a new issue