Как можно подключить плагин Anki JavaScript?

Anki позволяет использовать карты JavaScript. Например, карта может содержать что-то вроде:

<script>
//JavaScript code here
</script>

и код JavaScript будет выполнен, когда отображается карта.

Чтобы обеспечить большую гибкость, позволяя таким скриптам взаимодействовать с Back-end Anki (например, чтобы изменить значения полей примечаний, добавлять теги, влиять на планирование и т.д.), я хотел бы напишите плагин для Anki (версия 2), который будет реализовывать некоторые внутренние функции и активировать для него карту JavaScript script.

Например, скажем, у меня есть (Python) функция в моем подключаемом модуле, которая взаимодействует с объектами Anki:

def myFunc():
# use plug-in ability to interact with Anki objects to do stuff

Я хочу, чтобы разрешить JavaScript для JavaScript использовать эту функцию, например, чтобы иметь что-то подобное на карте:

<script>
myFunc(); // This should invoke the plug-in myFunc().
</script>

Я знаю, как добавлять крючки, чтобы различные события Anki вызывали мои подключаемые функции, но я хочу, чтобы JavaScript изнутри карты сделал это. Может ли это вообще быть сделано, и если да, то как? Спасибо!

Ответ 1

Прочитав сообщение , связанное с @Louis, и обсудил проблему с некоторыми коллегами, и перепутал, пытаясь разобраться, наконец, удалось найти решение:

Идею можно суммировать в этих двух ключевых точках (и двух под-ключевых точках):

  • Плагин может создать один или несколько объектов, которые будут "отображаться" для скриптов JavaScript на картах, так что скрипты карт могут обращаться к этим объектам - их полям и методам - ​​как если бы они были частью сценариев.

    • чтобы сделать это, объекты должны быть экземплярами определенного класса (или его подкласса), и каждый метод и свойство, которое должно быть подвергнуто карточным скриптам, должно быть объявлено как таковое с правильным декоратором PyQt.

и

  • PyQt предоставляет функциональность для "введения" таких объектов в веб-просмотр.

    • Подключаемый модуль должен обеспечить, чтобы эта инъекция выполнялась каждый раз, когда веб-просмотр Anki reviewer (повторно) инициализирован.

Следующий код показывает, как это сделать. Он предоставляет карточные скрипты с возможностью проверки текущего состояния ( "вопрос" или "ответ" ) и способ доступа (чтения и, что более важно, записи) полей примечаний.

from aqt import mw              # Anki main window object
from aqt import mw QObject      # Our exposed object will be an instance of a subclass of QObject.
from aqt import mw pyqtSlot     # a decorator for exposed methods
from aqt import mw pyqtProperty # a decorator for exposed properties

from anki.hooks import wrap     # We will need this to hook to specific Anki functions in order to make sure the injection happens in time.

# a class whose instance(s) we can expose to card scripts
class CardScriptObject(QObject):
    # some "private" fields - card scripts cannot access these directly 
    _state = None
    _card = None
    _note = None

    # Using pyqtProperty we create a property accessible from the card script.
    # We have to provide the type of the property (in this case str).
    # The second argument is a getter method.
    # This property is read-only. To make it writeable we would add a setter method as a third argument.
    state = pyqtProperty(str, lambda self: self._state)

    # The following methods are exposed to the card script owing to the pyqtSlot decorator.
    # Without it they would be "private".
    @pyqtSlot(str, result = str) # We have to provide the argument type(s) (excluding self),
                                 # as well as the type of the return value - with the named result argument, if a value is to be returned.
    def getField(self, name):
        return self._note[name]

    # Another method, without a return value:
    @pyqtSlot(str, str)
    def setField(self, name, value):
        self._note[name] = value
        self._note.flush()

    # An example of a method that can be invoked with two different signatures -
    # pyqtSlot has to be used for each possible signature:
    # (This method replaces the above two.
    # All three have been included here for the sake of the example.)
    @pyqtSlot(str, result = str)
    @pyqtSlot(str, str)
    def field(self, name, value = None): # sets a field if value given, gets a field otherwise
        if value is None: return self._note[name]
        self._note[name] = value
        self._note.flush()

cardScriptObject = CardScriptObject() # the object to expose to card scripts
flag = None # This flag is used in the injection process, which follows.

# This is a hook to Anki reviewer _initWeb method.
# It lets the plug-in know the reviewer webview is being initialised.
# (It would be too early to perform the injection here, as this method is called before the webview is initialised.
# And it would be too late to do it after _initWeb, as the first card would have already been shown.
# Hence this mechanism.)
def _initWeb():
    global flag
    flag = True

# This is a hook to Anki reviewer _showQuestion method.
# It populates our cardScriptObject "private" fields with the relevant values,
# and more importantly, it exposes ("injects") the object to the webview JavaScript scope -
# but only if this is the first card since the last initialisation, otherwise the object is already exposed.
def _showQuestion():
    global cardScriptObject, flag
    if flag:
        flag = False
        # The following line does the injection.
        # In this example our cardScriptObject will be accessible from card scripts
        # using the name pluginObject.
        mw.web.page().mainFrame().addToJavaScriptWindowObject("pluginObject", cardScriptObject)
    cardScriptObject._state = "question"
    cardScriptObject._card = mw.reviewer.card
    cardScriptObject._note = mw.reviewer.card.note()

# The following hook to Anki reviewer _showAnswer is not necessary for the injection,
# but in this example it serves to update the state.
def _showAnswer():
    global cardScriptObject
    cardScriptObject._state = "answer"

# adding our hooks
# In order to already have our object injected when the first card is shown (so that its scripts can "enjoy" this plug-in),
# and in order for the card scripts to have access to up-to-date information,
# our hooks must be executed _before_ the relevant Anki methods.
mw.reviewer._initWeb = wrap(mw.reviewer._initWeb, _initWeb, "before")
mw.reviewer._showQuestion = wrap(mw.reviewer._showQuestion, _showQuestion, "before")
mw.reviewer._showAnswer = wrap(mw.reviewer._showAnswer, _showAnswer, "before")

Вот оно! С таким подключаемым модулем JavaScript script изнутри карты может использовать pluginObject.state, чтобы проверить, выполняется ли он как часть вопроса или как часть ответа (также может быть достигнуто путем обертывания части вопроса в шаблон ответа с script, который устанавливает переменную, но это опережает), pluginObject.field(name), чтобы получить значение поля из примечания (также можно было бы получить, введя поле непосредственно в код JavaScript с помощью Anki pre-processor) и pluginObject.field(имя, значение), чтобы установить значение поля в заметке (до сих пор, насколько я знаю, выполнить это невозможно). Конечно, в наш CardScriptObject можно было запрограммировать многие другие функциональные возможности, чтобы позволить скриптам карт делать гораздо больше (читать/изменять конфигурацию, реализовывать другой механизм вопросов/ответов, взаимодействовать с планировщиком и т.д.).

Если кто-нибудь может предложить улучшения, мне было бы интересно услышать. В частности, меня интересует:

  • существует ли более простой способ разоблачения методов и свойств, чтобы обеспечить большую гибкость подписи; и
  • существует ли менее громоздкий способ выполнения инъекции.