diff --git a/anki/collection.py b/anki/collection.py
index 1991ff895..6b0882e00 100644
--- a/anki/collection.py
+++ b/anki/collection.py
@@ -510,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
afmt = afmt or template['afmt']
for (type, format) in (("q", qfmt), ("a", afmt)):
if type == "q":
- format = format.replace("{{cloze:", "{{cq:%d:" % (
- data[4]+1))
+ format = re.sub("{{(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%cq:%d:" % (
data[4]+1))
else:
- format = format.replace("{{cloze:", "{{ca:%d:" % (
- data[4]+1))
+ format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%ca:%d:" % (
data[4]+1))
fields['FrontSide'] = stripSounds(d['q'])
diff --git a/anki/models.py b/anki/models.py
index 8ad5b158d..599a10c92 100644
--- a/anki/models.py
+++ b/anki/models.py
@@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map),
sflds = splitFields(flds)
map = self.fieldMap(m)
ords = set()
- matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt'])
+ matches = re.findall("{{[^}]*?cloze:(?:.*?:)*(.+?)}}", m['tmpls'][0]['qfmt'])
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
for fname in matches:
if fname not in map:
diff --git a/anki/template/template.py b/anki/template/template.py
index bdddb2422..828c042ca 100644
--- a/anki/template/template.py
+++ b/anki/template/template.py
@@ -158,40 +158,42 @@ class Template(object):
return txt
# field modifiers
- parts = tag_name.split(':',2)
+ parts = tag_name.split(':')
extra = None
if len(parts) == 1 or parts[0] == '':
return '{unknown field %s}' % tag_name
- elif len(parts) == 2:
- (mod, tag) = parts
- elif len(parts) == 3:
- (mod, extra, tag) = parts
+ else:
+ mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts
txt = get_or_attr(context, tag)
-
- # built-in modifiers
- if mod == 'text':
- # strip html
- if txt:
- return stripHTML(txt)
- return ""
- elif mod == 'type':
- # type answer field; convert it to [[type:...]] for the gui code
- # to process
- return "[[%s]]" % tag_name
- elif mod == 'cq' or mod == 'ca':
- # cloze deletion
- if txt and extra:
- return self.clozeText(txt, extra, mod[1])
+
+ #Since 'text:' and other mods can affect html on which Anki relies to
+ #process Clozes and Types, we need to make sure cloze/type are always
+ #treated after all the other mods, regardless of how they're specified
+ #in the template, so that {{cloze:text: == {{text:cloze:
+ mods.reverse()
+ mods.sort(key=lambda s: s.startswith("cq-") or s.startswith("ca-") or s=="type")
+
+ for mod in mods:
+ # built-in modifiers
+ if mod == 'text':
+ # strip html
+ txt = stripHTML(txt) if txt else ""
+ elif mod == 'type':
+ # type answer field; convert it to [[type:...]] for the gui code
+ # to process
+ txt = "[[%s]]" % tag_name
+ elif mod.startswith('cq-') or mod.startswith('ca-'):
+ # cloze deletion
+ mod, extra = mod.split("-")
+ txt = self.clozeText(txt, extra, mod[1]) if txt and extra else ""
else:
- return ""
- else:
- # hook-based field modifier
- txt = runFilter('fmod_' + mod, txt or '', extra, context,
- tag, tag_name);
- if txt is None:
- return '{unknown field %s}' % tag_name
- return txt
+ # hook-based field modifier
+ txt = runFilter('fmod_' + mod, txt or '', extra, context,
+ tag, tag_name);
+ if txt is None:
+ return '{unknown field %s}' % tag_name
+ return txt
def clozeText(self, txt, ord, type):
reg = clozeReg
diff --git a/tests/test_models.py b/tests/test_models.py
index 2d4d3d493..22c5e088d 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -107,6 +107,29 @@ def test_templates():
mm.addTemplate(m, t)
assert not d.models.remTemplate(m, m['tmpls'][0])
+def test_cloze_ordinals():
+ d = getEmptyDeck()
+ d.models.setCurrent(d.models.byName("Cloze"))
+ m = d.models.current(); mm = d.models
+
+ #We replace the default Cloze template
+ t = mm.newTemplate("ChainedCloze")
+ t['qfmt'] = "{{cloze:text:Text}}"
+ t['afmt'] = "{{text:cloze:Text}}" #independent of the order of mods
+ mm.addTemplate(m, t)
+ mm.save(m)
+ d.models.remTemplate(m, m['tmpls'][0])
+
+ f = d.newNote()
+ f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
+ d.addNote(f)
+ assert d.cardCount() == 2
+ (c, c2) = f.cards()
+ # first card should have first ord
+ assert c.ord == 0
+ assert c2.ord == 1
+
+
def test_text():
d = getEmptyDeck()
m = d.models.current()
@@ -163,6 +186,29 @@ def test_cloze():
f.flush()
assert len(f.cards()) == 2
+def test_chained_mods():
+ d = getEmptyDeck()
+ d.models.setCurrent(d.models.byName("Cloze"))
+ m = d.models.current(); mm = d.models
+
+ #We replace the default Cloze template
+ t = mm.newTemplate("ChainedCloze")
+ t['qfmt'] = "{{cloze:text:Text}}"
+ t['afmt'] = "{{text:cloze:Text}}" #independent of the order of mods
+ mm.addTemplate(m, t)
+ mm.save(m)
+ d.models.remTemplate(m, m['tmpls'][0])
+
+ f = d.newNote()
+ q1 = 'phrase'
+ a1 = 'sentence'
+ q2 = 'en chaine'
+ a2 = 'chained'
+ f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
+ assert d.addNote(f) == 1
+ assert "This [sentence] demonstrates [chained] clozes." in f.cards()[0].q()
+ assert "This phrase demonstrates en chaine clozes." in f.cards()[0].a()
+
def test_modelChange():
deck = getEmptyDeck()
basic = deck.models.byName("Basic")