mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00

Previously cloze deletions were handled by copying the contents of one field into another and applying transforms to it. This had a number of problems: - after you add a card, you can't undo the cloze deletion - if you spot a mistake, you have to edit it twice (or more if you have more than one cloze for a sentence) - making multiple clozes requires copying & pasting the sentence multiple times - this also lead to much bigger decks if the sentences being cloze-deleted are large - related clozes can't be spaced apart as siblings To address these issues, we introduce the idea of cloze tags in the card template and fields. If the template has the text: {{cloze:1:field}} And a field has the following contents: {{c1::hello}} Then the template will automatically replace that part of the text with either occluded text, or a highlighted answer. All other clozes in the field are displayed normally. At the same time, we add support for text: into the template library, instead of manually creating text: fields in the dict for every field. Finally, add a forecast routine to get the due counts for the next week, which is used in the GUI.
193 lines
6.1 KiB
Python
193 lines
6.1 KiB
Python
import re
|
|
import cgi
|
|
import collections
|
|
from anki.utils import stripHTML
|
|
|
|
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}"
|
|
|
|
|
|
modifiers = {}
|
|
def modifier(symbol):
|
|
"""Decorator for associating a function with a Mustache tag modifier.
|
|
|
|
@modifier('P')
|
|
def render_tongue(self, tag_name=None, context=None):
|
|
return ":P %s" % tag_name
|
|
|
|
{{P yo }} => :P yo
|
|
"""
|
|
def set_modifier(func):
|
|
modifiers[symbol] = func
|
|
return func
|
|
return set_modifier
|
|
|
|
|
|
def get_or_attr(obj, name, default=None):
|
|
try:
|
|
return obj[name]
|
|
except KeyError:
|
|
return default
|
|
except:
|
|
try:
|
|
return getattr(obj, name)
|
|
except AttributeError:
|
|
return default
|
|
|
|
|
|
class Template(object):
|
|
# The regular expression used to find a #section
|
|
section_re = None
|
|
|
|
# The regular expression used to find a tag.
|
|
tag_re = None
|
|
|
|
# Opening tag delimiter
|
|
otag = '{{'
|
|
|
|
# Closing tag delimiter
|
|
ctag = '}}'
|
|
|
|
def __init__(self, template, context=None):
|
|
self.template = template
|
|
self.context = context or {}
|
|
self.compile_regexps()
|
|
|
|
def render(self, template=None, context=None, encoding=None):
|
|
"""Turns a Mustache template into something wonderful."""
|
|
template = template or self.template
|
|
context = context or self.context
|
|
|
|
template = self.render_sections(template, context)
|
|
result = self.render_tags(template, context)
|
|
if encoding is not None:
|
|
result = result.encode(encoding)
|
|
return result
|
|
|
|
def compile_regexps(self):
|
|
"""Compiles our section and tag regular expressions."""
|
|
tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) }
|
|
|
|
section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s"
|
|
self.section_re = re.compile(section % tags, re.M|re.S)
|
|
|
|
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
|
|
self.tag_re = re.compile(tag % tags)
|
|
|
|
def render_sections(self, template, context):
|
|
"""Expands sections."""
|
|
while 1:
|
|
match = self.section_re.search(template)
|
|
if match is None:
|
|
break
|
|
|
|
section, section_name, inner = match.group(0, 1, 2)
|
|
section_name = section_name.strip()
|
|
|
|
# check for cloze
|
|
m = re.match("c[qa]:(\d+):(.+)", section_name)
|
|
if m:
|
|
# get full field text
|
|
txt = get_or_attr(context, m.group(2), None)
|
|
m = re.search(clozeReg%m.group(1), txt)
|
|
if m:
|
|
it = m.group(1)
|
|
else:
|
|
it = None
|
|
else:
|
|
it = get_or_attr(context, section_name, None)
|
|
|
|
replacer = ''
|
|
# if it and isinstance(it, collections.Callable):
|
|
# replacer = it(inner)
|
|
if it and not hasattr(it, '__iter__'):
|
|
if section[2] != '^':
|
|
replacer = inner
|
|
elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
|
|
if section[2] != '^':
|
|
replacer = self.render(inner, it)
|
|
elif it:
|
|
insides = []
|
|
for item in it:
|
|
insides.append(self.render(inner, item))
|
|
replacer = ''.join(insides)
|
|
elif not it and section[2] == '^':
|
|
replacer = inner
|
|
|
|
template = template.replace(section, replacer)
|
|
|
|
return template
|
|
|
|
def render_tags(self, template, context):
|
|
"""Renders all the tags in a template for a context."""
|
|
while 1:
|
|
match = self.tag_re.search(template)
|
|
if match is None:
|
|
break
|
|
|
|
tag, tag_type, tag_name = match.group(0, 1, 2)
|
|
tag_name = tag_name.strip()
|
|
try:
|
|
func = modifiers[tag_type]
|
|
replacement = func(self, tag_name, context)
|
|
template = template.replace(tag, replacement)
|
|
except:
|
|
return u"{{invalid template}}"
|
|
|
|
return template
|
|
|
|
@modifier('{')
|
|
def render_tag(self, tag_name, context):
|
|
"""Given a tag name and context, finds, escapes, and renders the tag."""
|
|
raw = get_or_attr(context, tag_name, '')
|
|
if not raw and raw is not 0:
|
|
return ''
|
|
return re.sub("^<span.+?>(.*)</span>", "\\1", raw)
|
|
|
|
@modifier('!')
|
|
def render_comment(self, tag_name=None, context=None):
|
|
"""Rendering a comment always returns nothing."""
|
|
return ''
|
|
|
|
@modifier(None)
|
|
def render_unescaped(self, tag_name=None, context=None):
|
|
"""Render a tag without escaping it."""
|
|
if tag_name.startswith("text:"):
|
|
tag = tag_name[5:]
|
|
txt = get_or_attr(context, tag)
|
|
if txt:
|
|
return stripHTML(txt)
|
|
return ""
|
|
elif tag_name.startswith("cq:") or tag_name.startswith("ca:"):
|
|
m = re.match("c(.):(\d+):(.+)", tag_name)
|
|
(type, ord, tag) = (m.group(1), m.group(2), m.group(3))
|
|
txt = get_or_attr(context, tag)
|
|
if txt:
|
|
return self.clozeText(txt, ord, type)
|
|
return ""
|
|
return get_or_attr(context, tag_name, '{unknown field %s}' % tag_name)
|
|
|
|
# fixme: need a way to conditionally add these, so that including extra
|
|
# fields in the question fmt doesn't make empty clozes
|
|
def clozeText(self, txt, ord, type):
|
|
reg = clozeReg
|
|
m = re.search(reg%ord, txt)
|
|
if not m:
|
|
# cloze doesn't exist; return empty
|
|
return ""
|
|
# replace chosen cloze with type
|
|
if type == "q":
|
|
if m.group(2):
|
|
txt = re.sub(reg%ord, "<b>...(\\3)</b>", txt)
|
|
else:
|
|
txt = re.sub(reg%ord, "<b>...</b>", txt)
|
|
else:
|
|
txt = re.sub(reg%ord, "<b>\\1</b>", txt)
|
|
# and display other clozes normally
|
|
return re.sub(reg%".*?", "\\1", txt)
|
|
|
|
@modifier('=')
|
|
def render_delimiter(self, tag_name=None, context=None):
|
|
"""Changes the Mustache delimiter."""
|
|
self.otag, self.ctag = tag_name.split(' ')
|
|
self.compile_regexps()
|
|
return ''
|