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")