# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html """ This file contains code that is no longer used by Anki, but left around for the benefit of add-ons. It may go away in the future, so please copy any routines you need into your own add-on instead of using them directly from this module. If your add-on was previously calling anki.template.render(), you now need to call anki.template.render_template(), passing col in as the first argument. """ from __future__ import annotations import re from typing import Any, Callable from anki.lang import _ from anki.template import ( CLOZE_REGEX_MATCH_GROUP_CONTENT, CLOZE_REGEX_MATCH_GROUP_HINT, CLOZE_REGEX_MATCH_GROUP_TAG, clozeReg, ) from anki.utils import stripHTML # Cloze filter ########################################################################## def _clozeText(txt: str, ord: str, type: str) -> str: """Process the given Cloze deletion within the given template.""" reg = clozeReg currentRegex = clozeReg % ord if not re.search(currentRegex, txt): # No Cloze deletion was found in txt. return "" txt = _removeFormattingFromMathjax(txt, ord) def repl(m): # replace chosen cloze with type if type == "q": if m.group(CLOZE_REGEX_MATCH_GROUP_HINT): buf = "[%s]" % m.group(CLOZE_REGEX_MATCH_GROUP_HINT) else: buf = "[...]" else: buf = m.group(CLOZE_REGEX_MATCH_GROUP_CONTENT) # uppercase = no formatting if m.group(CLOZE_REGEX_MATCH_GROUP_TAG) == "c": buf = "%s" % buf return buf txt = re.sub(currentRegex, repl, txt) # and display other clozes normally return re.sub(reg % r"\d+", "\\2", txt) def _removeFormattingFromMathjax(txt, ord) -> str: """Marks all clozes within MathJax to prevent formatting them. Active Cloze deletions within MathJax should not be wrapped inside a Cloze , 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 ". """ creg = clozeReg.replace("(?si)", "") # Scan the string left to right. # After a MathJax opening - \( or \[ - flip in_mathjax to True. # After a MathJax closing - \) or \] - flip in_mathjax to False. # When a Cloze pattern number `ord` is found and we are in MathJax, # replace its '{{c' with '{{C'. # # TODO: Report mismatching opens/closes - e.g. '\(\]' # TODO: Report errors in this method better than printing to stdout. # flags in middle of expression deprecated in_mathjax = False def replace(match): nonlocal in_mathjax if match.group("mathjax_open"): 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\\[([])|" r"(?P\\[\])])|" r"(?P" + (creg % ord) + ")", replace, txt, ) def _cloze_filter(field_text: str, filter_args: str, q_or_a: str): return _clozeText(field_text, filter_args, q_or_a) def cloze_qfilter(field_text: str, filter_args: str, *args): return _cloze_filter(field_text, filter_args, "q") def cloze_afilter(field_text: str, filter_args: str, *args): return _cloze_filter(field_text, filter_args, "a") # addHook("fmod_cq", cloze_qfilter) # addHook("fmod_ca", cloze_afilter) # Other filters ########################################################################## def hint_filter(txt: str, args, context, tag: str, fullname) -> str: if not txt.strip(): return "" # random id domid = "hint%d" % id(txt) return """ %s """ % ( domid, _("Show %s") % tag, domid, txt, ) FURIGANA_RE = r" ?([^ >]+?)\[(.+?)\]" RUBY_REPL = r"\1\2" def replace_if_not_audio(repl: str) -> Callable[[Any], Any]: def func(match): if match.group(2).startswith("sound:"): # return without modification return match.group(0) else: return re.sub(FURIGANA_RE, repl, match.group(0)) return func def without_nbsp(s: str) -> str: return s.replace(" ", " ") def kanji_filter(txt: str, *args) -> str: return re.sub(FURIGANA_RE, replace_if_not_audio(r"\1"), without_nbsp(txt)) def kana_filter(txt: str, *args) -> str: return re.sub(FURIGANA_RE, replace_if_not_audio(r"\2"), without_nbsp(txt)) def furigana_filter(txt: str, *args) -> str: return re.sub(FURIGANA_RE, replace_if_not_audio(RUBY_REPL), without_nbsp(txt)) def text_filter(txt: str, *args) -> str: return stripHTML(txt) def type_answer_filter(txt: str, filter_args: str, context, tag: str, dummy) -> str: # convert it to [[type:...]] for the gui code to process if filter_args: return f"[[type:{filter_args}:{tag}]]" else: return f"[[type:{tag}]]" # addHook("fmod_text", text_filter) # addHook("fmod_type", type_answer_filter) # addHook("fmod_hint", hint_filter) # addHook("fmod_kanji", kanji_filter) # addHook("fmod_kana", kana_filter) # addHook("fmod_furigana", furigana_filter)