mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -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)
|
return [self._renderQA(mods[row[2]], groups[row[3]], row)
|
||||||
for row in self._qaData(where)]
|
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."
|
"Returns hash of id, question, answer."
|
||||||
# data is [cid, fid, mid, gid, ord, tags, flds, data]
|
# data is [cid, fid, mid, gid, ord, tags, flds, data]
|
||||||
# unpack fields and create dict
|
# 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
|
# render q & a
|
||||||
d = dict(id=data[0])
|
d = dict(id=data[0])
|
||||||
for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])):
|
for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])):
|
||||||
# if filters:
|
fields = runFilter("mungeFields", fields, model, gname, data, self)
|
||||||
# fields = runFilter("renderQA.pre", fields, , self)
|
|
||||||
html = anki.template.render(format, fields)
|
html = anki.template.render(format, fields)
|
||||||
# if filters:
|
d[type] = runFilter("mungeQA", html, fields, model, gname, data, self)
|
||||||
# d[type] = runFilter("renderQA.post", html, fields, meta, self)
|
|
||||||
d[type] = html
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _qaData(self, where=""):
|
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 htmlentitydefs import entitydefs
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
|
|
||||||
|
latexCmd = ["latex", "-interaction=nonstopmode"]
|
||||||
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
|
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
|
||||||
|
build = True # if off, use existing media but don't create new
|
||||||
regexps = {
|
regexps = {
|
||||||
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
|
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
|
||||||
"expression": re.compile(r"\[\$\](.+?)\[/\$\]", 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":
|
if sys.platform == "darwin":
|
||||||
os.environ['PATH'] += ":/usr/texbin"
|
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):
|
def stripLatex(text):
|
||||||
for match in regexps['standard'].finditer(text):
|
for match in regexps['standard'].finditer(text):
|
||||||
text = text.replace(match.group(), "")
|
text = text.replace(match.group(), "")
|
||||||
|
@ -46,23 +32,50 @@ def stripLatex(text):
|
||||||
text = text.replace(match.group(), "")
|
text = text.replace(match.group(), "")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def latexImgFile(deck, latexCode):
|
def mungeQA(html, fields, model, gname, data, deck):
|
||||||
key = checksum(latexCode)
|
"Convert TEXT with embedded latex tags to image links."
|
||||||
return "latex-%s.png" % key
|
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):
|
def _imgLink(deck, latex):
|
||||||
"Convert entities, fix newlines, convert to utf8, and wrap pre/postamble."
|
"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):
|
for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex):
|
||||||
if match.group(1) in entitydefs:
|
if match.group(1) in entitydefs:
|
||||||
latex = latex.replace(match.group(), entitydefs[match.group(1)])
|
latex = latex.replace(match.group(), entitydefs[match.group(1)])
|
||||||
latex = re.sub("<br( /)?>", "\n", latex)
|
latex = re.sub("<br( /)?>", "\n", latex)
|
||||||
latex = (deck.getVar("latexPre") + "\n" +
|
|
||||||
latex + "\n" +
|
|
||||||
deck.getVar("latexPost"))
|
|
||||||
latex = latex.encode("utf-8")
|
latex = latex.encode("utf-8")
|
||||||
return latex
|
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+")
|
log = open(os.path.join(tmpdir, "latex_log.txt"), "w+")
|
||||||
texpath = os.path.join(tmpdir, "tmp.tex")
|
texpath = os.path.join(tmpdir, "tmp.tex")
|
||||||
texfile = file(texpath, "w")
|
texfile = file(texpath, "w")
|
||||||
|
@ -71,60 +84,33 @@ def buildImg(deck, latex):
|
||||||
# make sure we have a valid mediaDir
|
# make sure we have a valid mediaDir
|
||||||
mdir = deck.media.dir(create=True)
|
mdir = deck.media.dir(create=True)
|
||||||
oldcwd = os.getcwd()
|
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:
|
try:
|
||||||
|
# generate dvi
|
||||||
os.chdir(tmpdir)
|
os.chdir(tmpdir)
|
||||||
def errmsg(type):
|
if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log):
|
||||||
msg = _("Error executing %s.\n") % type
|
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:
|
try:
|
||||||
log = open(os.path.join(tmpdir, "latex_log.txt")).read()
|
log = open(os.path.join(tmpdir, "latex_log.txt")).read()
|
||||||
|
if not log:
|
||||||
|
raise Exception()
|
||||||
msg += "<small><pre>" + cgi.escape(log) + "</pre></small>"
|
msg += "<small><pre>" + cgi.escape(log) + "</pre></small>"
|
||||||
except:
|
except:
|
||||||
msg += _("Have you installed latex and dvipng?")
|
msg += _("Have you installed latex and dvipng?")
|
||||||
pass
|
pass
|
||||||
return msg
|
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
|
# 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
|
# loop through directory and find unused & missing media
|
||||||
unused = []
|
unused = []
|
||||||
for file in os.listdir(mdir):
|
for file in os.listdir(mdir):
|
||||||
|
if file.startswith("latex-"):
|
||||||
|
continue
|
||||||
path = os.path.join(mdir, file)
|
path = os.path.join(mdir, file)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
# ignore directories
|
# ignore directories
|
||||||
|
|
|
@ -276,11 +276,23 @@ def fieldChecksum(data):
|
||||||
return int(checksum(data.encode("utf-8"))[:8], 16)
|
return int(checksum(data.encode("utf-8"))[:8], 16)
|
||||||
|
|
||||||
def call(argv, wait=True, **kwargs):
|
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:
|
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:
|
except OSError:
|
||||||
# command not found
|
# command not found
|
||||||
return -1
|
return -1
|
||||||
|
# wait for command to finish
|
||||||
if wait:
|
if wait:
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
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