Как плагин может улучшить JavaScript Anki?
Anki позволяет картам использовать JavaScript. Например, карта может содержать что-то вроде:
<script>
//JavaScript code here
</script>
и код JavaScript будет выполнен, когда карта будет показана.
Чтобы обеспечить большую гибкость, позволяя таким сценариям взаимодействовать с серверной частью Anki (например, чтобы изменять значения полей заметки, добавлять теги, влиять на планирование и т. Д.), Я хотел бы написать плагин - для Anki (версия 2), которая реализует некоторые внутренние функции и позволяет JavaScript-скрипту карты вызывать их.
Например, скажем, у меня есть (Python) функция в моем плагине, который взаимодействует с объектами Anki:
def myFunc():
# use plug-in's ability to interact with Anki's objects to do stuff
Я хочу, чтобы JavaScript-карты могли вызывать эту функцию, например, иметь что-то подобное на карте:
<script>
myFunc(); // This should invoke the plug-in's myFunc().
</script>
Я знаю, как добавлять хуки, чтобы различные события Anki вызывали функции моего плагина, но я хочу разрешить JavaScript из карты. Можно ли вообще это сделать, и если да, то как? Спасибо!
1 ответ
Прочитав пост, на который ссылается @Louis, и обсудив проблему с некоторыми коллегами, и приняв участие в экспериментах, я наконец нашел решение:
Идея может быть кратко изложена в следующих двух ключевых моментах (и двух вспомогательных пунктах):
Плагин может создавать один или несколько объектов, которые будут "открыты" для сценариев JavaScript карт, так что сценарии карт могут обращаться к этим объектам - их полям и методам - как если бы они были частью области действия сценариев.
- чтобы сделать это, объекты должны быть экземплярами определенного класса (или его подкласса), и каждый метод и свойство, которые должны быть представлены сценариям карты, должны быть объявлены как таковые с помощью надлежащего декоратора PyQt.
а также
PyQt предоставляет функциональность для "внедрения" таких объектов в веб-просмотр.
- Плагин должен гарантировать, что эта инъекция происходит каждый раз, когда (ре) инициализируется рецензирование веб-сайта Anki.
Следующий код показывает, как этого добиться. Он предоставляет сценариям карты способ проверки текущего состояния ("вопрос" или "ответ") и способ доступа (чтения и, что более важно, записи) к полям заметки.
from aqt import mw # Anki's 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's reviewer's _initWeb method.
# It lets the plug-in know the reviewer's 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's reviewer's _showQuestion method.
# It populates our cardScriptObject's "private" fields with the relevant values,
# and more importantly, it exposes ("injects") the object to the webview's 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's reviewer's _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 изнутри карты может использовать pluginObject.state, чтобы проверить, выполняется ли он как часть вопроса или как часть ответа (его также можно получить, обернув часть вопроса в шаблоне ответа). с помощью сценария, который устанавливает переменную, но это лучше), pluginObject.field (name), чтобы получить значение поля из заметки (также можно получить, введя поле непосредственно в код JavaScript с помощью препроцессора Anki) и pluginObject.field (name, value) для установки значения поля в заметке (насколько я знаю, до сих пор этого не делалось). Конечно, многие другие функциональные возможности могут быть запрограммированы в нашем CardScriptObject, чтобы позволить сценариям карты выполнять намного больше (чтение / изменение конфигурации, реализация другого механизма вопросов / ответов, взаимодействие с планировщиком и т. Д.).
Если кто-нибудь может предложить улучшения, мне было бы интересно услышать. В частности, я заинтересован в:
- есть ли более аккуратный способ раскрытия методов и свойств, чтобы обеспечить большую гибкость подписи; а также
- есть ли менее громоздкий способ выполнить инъекцию.