diff --git a/pylib/anki/template/__init__.py b/pylib/anki/template/__init__.py
index 39a0012a7..beff00e84 100644
--- a/pylib/anki/template/__init__.py
+++ b/pylib/anki/template/__init__.py
@@ -1,12 +1,7 @@
from typing import Any
-from . import furigana, hint
from .template import Template
-furigana.install()
-
-hint.install()
-
def render(template, context=None, **kwargs) -> Any:
context = context and context.copy() or {}
diff --git a/pylib/anki/template/furigana.py b/pylib/anki/template/furigana.py
deleted file mode 100644
index 93e15f724..000000000
--- a/pylib/anki/template/furigana.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright: Ankitects Pty Ltd and contributors
-# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-# Based off Kieran Clancy's initial implementation.
-
-import re
-from typing import Any, Callable
-
-from anki.hooks import addHook
-
-r = r" ?([^ >]+?)\[(.+?)\]"
-ruby = r"\1"
-
-
-def noSound(repl) -> Callable[[Any], Any]:
- def func(match):
- if match.group(2).startswith("sound:"):
- # return without modification
- return match.group(0)
- else:
- return re.sub(r, repl, match.group(0))
-
- return func
-
-
-def _munge(s) -> Any:
- return s.replace(" ", " ")
-
-
-def kanji(txt, *args) -> str:
- return re.sub(r, noSound(r"\1"), _munge(txt))
-
-
-def kana(txt, *args) -> str:
- return re.sub(r, noSound(r"\2"), _munge(txt))
-
-
-def furigana(txt, *args) -> str:
- return re.sub(r, noSound(ruby), _munge(txt))
-
-
-def install() -> None:
- addHook("fmod_kanji", kanji)
- addHook("fmod_kana", kana)
- addHook("fmod_furigana", furigana)
diff --git a/pylib/anki/template/hint.py b/pylib/anki/template/hint.py
deleted file mode 100644
index 52733b68b..000000000
--- a/pylib/anki/template/hint.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright: Ankitects Pty Ltd and contributors
-# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-
-from anki.hooks import addHook
-from anki.lang import _
-
-
-def hint(txt, extra, context, tag, fullname) -> str:
- if not txt.strip():
- return ""
- # random id
- domid = "hint%d" % id(txt)
- return """
-
-%s
%s
-""" % (
- domid,
- _("Show %s") % tag,
- domid,
- txt,
- )
-
-
-def install() -> None:
- addHook("fmod_hint", hint)
diff --git a/pylib/anki/template2.py b/pylib/anki/template2.py
index 16e6333f6..f8a4a5b3c 100644
--- a/pylib/anki/template2.py
+++ b/pylib/anki/template2.py
@@ -9,9 +9,11 @@ connected to pystache. It may be renamed in the future.
from __future__ import annotations
import re
-from typing import Dict, Tuple
+from typing import Any, Callable, Dict, Tuple
import anki
+from anki.hooks import addHook
+from anki.lang import _
from anki.sound import stripSounds
@@ -31,3 +33,61 @@ def renderFromFieldMap(
atext = anki.template.render(format, fields)
return qtext, atext
+
+
+# Filters
+##########################################################################
+
+
+def hint(txt, extra, context, tag, fullname) -> str:
+ if not txt.strip():
+ return ""
+ # random id
+ domid = "hint%d" % id(txt)
+ return """
+
+%s%s
+""" % (
+ domid,
+ _("Show %s") % tag,
+ domid,
+ txt,
+ )
+
+
+FURIGANA_RE = r" ?([^ >]+?)\[(.+?)\]"
+RUBY_REPL = r"\1"
+
+
+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(txt: str, *args) -> str:
+ return re.sub(FURIGANA_RE, replace_if_not_audio(r"\1"), without_nbsp(txt))
+
+
+def kana(txt: str, *args) -> str:
+ return re.sub(FURIGANA_RE, replace_if_not_audio(r"\2"), without_nbsp(txt))
+
+
+def furigana(txt: str, *args) -> str:
+ return re.sub(FURIGANA_RE, replace_if_not_audio(RUBY_REPL), without_nbsp(txt))
+
+
+addHook("fmod_hint", hint)
+addHook("fmod_kanji", kanji)
+addHook("fmod_kana", kana)
+addHook("fmod_furigana", furigana)