mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
Merge branch 'master' of github.com:dae/anki
This commit is contained in:
commit
f7ae4c3ef4
5 changed files with 84 additions and 28 deletions
10
anki/tags.py
10
anki/tags.py
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
16
tests/test_template.py
Normal 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}} \]')
|
Loading…
Reference in a new issue