Merge branch 'master' of github.com:dae/anki

This commit is contained in:
Damien Elmes 2019-12-23 08:32:19 +10:00
commit f7ae4c3ef4
5 changed files with 84 additions and 28 deletions

View file

@ -12,7 +12,7 @@ This module manages the tag cache and tags for notes.
import json import json
import re import re
from typing import Any, Callable, Dict, List, Tuple from typing import Callable, Dict, List, Tuple
from anki.hooks import runHook from anki.hooks import runHook
from anki.utils import ids2str, intTime from anki.utils import ids2str, intTime
@ -66,13 +66,13 @@ class TagManager:
self.register(set(self.split( self.register(set(self.split(
" ".join(self.col.db.list("select distinct tags from notes"+lim))))) " ".join(self.col.db.list("select distinct tags from notes"+lim)))))
def allItems(self) -> List[Tuple[Any, Any]]: def allItems(self) -> List[Tuple[str, int]]:
return list(self.tags.items()) return list(self.tags.items())
def save(self) -> None: def save(self) -> None:
self.changed = True self.changed = True
def byDeck(self, did, children=False) -> List: def byDeck(self, did, children=False) -> List[str]:
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children: if not children:
query = basequery + " AND c.did=?" query = basequery + " AND c.did=?"
@ -127,7 +127,7 @@ class TagManager:
# String-based utilities # String-based utilities
########################################################################## ##########################################################################
def split(self, tags) -> List: def split(self, tags) -> List[str]:
"Parse a string and return a list of tags." "Parse a string and return a list of tags."
return [t for t in tags.replace('\u3000', ' ').split(" ") if t] return [t for t in tags.replace('\u3000', ' ').split(" ") if t]
@ -165,7 +165,7 @@ class TagManager:
# List-based utilities # List-based utilities
########################################################################## ##########################################################################
def canonify(self, tagList) -> List: def canonify(self, tagList) -> List[str]:
"Strip duplicates, adjust case to match existing tags, and sort." "Strip duplicates, adjust case to match existing tags, and sort."
strippedTags = [] strippedTags = []
for t in tagList: for t in tagList:

View file

@ -4,6 +4,7 @@ from typing import Any, Callable, Dict, Pattern
from anki.hooks import runFilter from anki.hooks import runFilter
from anki.utils import stripHTML, stripHTMLMedia from anki.utils import stripHTML, stripHTMLMedia
# Matches a {{c123::clozed-out text::hint}} Cloze deletion, case-insensitively.
clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}" clozeReg = r"(?si)\{\{(c)%s::(.*?)(::(.*?))?\}\}"
modifiers: Dict[str, Callable] = {} modifiers: Dict[str, Callable] = {}
@ -34,6 +35,7 @@ def get_or_attr(obj, name, default=None) -> Any:
return default return default
class Template: class Template:
# The regular expression used to find a #section # The regular expression used to find a #section
section_re: Pattern = None section_re: Pattern = None
@ -197,6 +199,7 @@ class Template:
def clozeText(self, txt, ord, type) -> str: def clozeText(self, txt, ord, type) -> str:
reg = clozeReg reg = clozeReg
if not re.search(reg%ord, txt): if not re.search(reg%ord, txt):
# No Cloze deletion was found in txt.
return "" return ""
txt = self._removeFormattingFromMathjax(txt, ord) txt = self._removeFormattingFromMathjax(txt, ord)
def repl(m): def repl(m):
@ -216,27 +219,56 @@ class Template:
# and display other clozes normally # and display other clozes normally
return re.sub(reg%r"\d+", "\\2", txt) return re.sub(reg%r"\d+", "\\2", txt)
# look for clozes wrapped in mathjax, and change {{cx to {{Cx
def _removeFormattingFromMathjax(self, txt, ord) -> str: def _removeFormattingFromMathjax(self, txt, ord) -> str:
opening = ["\\(", "\\["] """Marks all clozes within MathJax to prevent formatting them.
closing = ["\\)", "\\]"]
# flags in middle of expression deprecated Active Cloze deletions within MathJax should not be wrapped inside
a Cloze <span>, as that would interfere with MathJax.
This method finds all Cloze deletions number `ord` in `txt` which are
inside MathJax inline or display formulas, and replaces their opening
'{{c123' with a '{{C123'. The clozeText method interprets the upper-case
C as "don't wrap this Cloze in a <span>".
"""
creg = clozeReg.replace("(?si)", "") creg = clozeReg.replace("(?si)", "")
regex = r"(?si)(\\[([])(.*?)"+(creg%ord)+r"(.*?)(\\[\])])"
def repl(m): # Scan the string left to right.
enclosed = True # After a MathJax opening - \( or \[ - flip in_mathjax to True.
for s in closing: # After a MathJax closing - \) or \] - flip in_mathjax to False.
if s in m.group(1): # When a Cloze pattern number `ord` is found and we are in MathJax,
enclosed = False # replace its '{{c' with '{{C'.
for s in opening: #
if s in m.group(7): # TODO: Report mismatching opens/closes - e.g. '\(\]'
enclosed = False # TODO: Report errors in this method better than printing to stdout.
if not enclosed: # flags in middle of expression deprecated
return m.group(0) in_mathjax = False
# remove formatting def replace(match):
return m.group(0).replace("{{c", "{{C") nonlocal in_mathjax
txt = re.sub(regex, repl, txt) if match.group('mathjax_open'):
return txt if in_mathjax:
print("MathJax opening found while already in MathJax")
in_mathjax = True
elif match.group('mathjax_close'):
if not in_mathjax:
print("MathJax close found while not in MathJax")
in_mathjax = False
elif match.group('cloze'):
if in_mathjax:
return match.group(0).replace(
'{{c{}::'.format(ord),
'{{C{}::'.format(ord))
else:
print("Unexpected: no expected capture group is present")
return match.group(0)
# The following regex matches one of:
# - MathJax opening
# - MathJax close
# - Cloze deletion number `ord`
return re.sub(
r"(?si)"
r"(?P<mathjax_open>\\[([])|"
r"(?P<mathjax_close>\\[\])])|"
r"(?P<cloze>" + (creg%ord) + ")", replace, txt)
@modifier('=') @modifier('=')
def render_delimiter(self, tag_name=None, context=None) -> str: def render_delimiter(self, tag_name=None, context=None) -> str:

View file

@ -3,6 +3,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import base64 import base64
import html import html
import itertools
import json import json
import mimetypes import mimetypes
import re import re
@ -553,10 +554,10 @@ to a cloze type first, via Edit>Change Note Type."""))
###################################################################### ######################################################################
def onAddMedia(self): def onAddMedia(self):
key = (_("Media") + extension_filter = ' '.join(
" (*.jpg *.png *.gif *.tiff *.svg *.tif *.jpeg "+ '*.' + extension
"*.mp3 *.ogg *.wav *.avi *.ogv *.mpg *.mpeg *.mov *.mp4 " + for extension in sorted(itertools.chain(pics, audio)))
"*.mkv *.ogx *.ogv *.oga *.flv *.swf *.flac *.webp *.m4a)") key = (_("Media") + " (" + extension_filter + ")")
def accept(file): def accept(file):
self.addMedia(file, canDelete=True) self.addMedia(file, canDelete=True)
file = getFile(self.widget, _("Add Media"), accept, key, key="media") file = getFile(self.widget, _("Add Media"), accept, key, key="media")

View file

@ -199,6 +199,13 @@ def test_cloze_mathjax():
assert "class=cloze" in f.cards()[3].q() assert "class=cloze" in f.cards()[3].q()
assert "class=cloze" in f.cards()[4].q() assert "class=cloze" in f.cards()[4].q()
f = d.newNote()
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
assert d.addNote(f)
assert len(f.cards()) == 1
assert f.cards()[0].q().endswith('\(a\) <span class=cloze>[...]</span> \[ [...] \]')
def test_chained_mods(): def test_chained_mods():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))

16
tests/test_template.py Normal file
View file

@ -0,0 +1,16 @@
from anki.template import Template
def test_remove_formatting_from_mathjax():
t = Template('')
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)'
txt = (r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) '
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}')
# Cloze 2 is not in MathJax, so it should not get protected against
# formatting.
assert t._removeFormattingFromMathjax(txt, 2) == txt
txt = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
assert t._removeFormattingFromMathjax(txt, 1) == (
r'\(a\) {{c1::b}} \[ {{C1::c}} \]')