aboutsummaryrefslogtreecommitdiff
path: root/ext/js/settings/anki-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/settings/anki-controller.js')
-rw-r--r--ext/js/settings/anki-controller.js729
1 files changed, 729 insertions, 0 deletions
diff --git a/ext/js/settings/anki-controller.js b/ext/js/settings/anki-controller.js
new file mode 100644
index 00000000..db3e3c14
--- /dev/null
+++ b/ext/js/settings/anki-controller.js
@@ -0,0 +1,729 @@
+/*
+ * Copyright (C) 2019-2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * AnkiConnect
+ * AnkiNoteBuilder
+ * ObjectPropertyAccessor
+ * SelectorObserver
+ */
+
+class AnkiController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ this._ankiConnect = new AnkiConnect();
+ this._ankiNoteBuilder = new AnkiNoteBuilder(false);
+ this._selectorObserver = new SelectorObserver({
+ selector: '.anki-card',
+ ignoreSelector: null,
+ onAdded: this._createCardController.bind(this),
+ onRemoved: this._removeCardController.bind(this),
+ isStale: this._isCardControllerStale.bind(this)
+ });
+ this._stringComparer = new Intl.Collator(); // Locale does not matter
+ this._getAnkiDataPromise = null;
+ this._ankiErrorContainer = null;
+ this._ankiErrorMessageNode = null;
+ this._ankiErrorMessageNodeDefaultContent = '';
+ this._ankiErrorMessageDetailsNode = null;
+ this._ankiErrorMessageDetailsContainer = null;
+ this._ankiErrorMessageDetailsToggle = null;
+ this._ankiErrorInvalidResponseInfo = null;
+ this._ankiCardPrimary = null;
+ this._ankiCardPrimaryType = null;
+ this._validateFieldsToken = null;
+ }
+
+ get settingsController() {
+ return this._settingsController;
+ }
+
+ async prepare() {
+ this._ankiErrorContainer = document.querySelector('#anki-error');
+ this._ankiErrorMessageNode = document.querySelector('#anki-error-message');
+ this._ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent;
+ this._ankiErrorMessageDetailsNode = document.querySelector('#anki-error-message-details');
+ this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details-container');
+ this._ankiErrorMessageDetailsToggle = document.querySelector('#anki-error-message-details-toggle');
+ this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');
+ this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');
+ this._ankiCardPrimary = document.querySelector('#anki-card-primary');
+ this._ankiCardPrimaryType = document.querySelector('#anki-card-primary-type');
+
+ this._setupFieldMenus();
+
+ this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false);
+ if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); }
+ if (this._ankiCardPrimaryType !== null) { this._ankiCardPrimaryType.addEventListener('change', this._onAnkiCardPrimaryTypeChange.bind(this), false); }
+
+ const options = await this._settingsController.getOptions();
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+ this._onOptionsChanged({options});
+ }
+
+ getFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'conjugation',
+ 'dictionary',
+ 'document-title',
+ 'expression',
+ 'frequencies',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'glossary-no-dictionary',
+ 'pitch-accents',
+ 'pitch-accent-graphs',
+ 'pitch-accent-positions',
+ 'reading',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'sentence',
+ 'stroke-count',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+ }
+
+ getFieldMarkersHtml(markers) {
+ const fragment = document.createDocumentFragment();
+ for (const marker of markers) {
+ const markerNode = this._settingsController.instantiateTemplate('anki-card-field-marker');
+ markerNode.querySelector('.marker-link').textContent = marker;
+ fragment.appendChild(markerNode);
+ }
+ return fragment;
+ }
+
+ async getAnkiData() {
+ let promise = this._getAnkiDataPromise;
+ if (promise === null) {
+ promise = this._getAnkiData();
+ this._getAnkiDataPromise = promise;
+ promise.finally(() => { this._getAnkiDataPromise = null; });
+ }
+ return promise;
+ }
+
+ async getModelFieldNames(model) {
+ return await this._ankiConnect.getModelFieldNames(model);
+ }
+
+ getRequiredPermissions(fieldValue) {
+ return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
+ }
+
+ containsAnyMarker(field) {
+ return this._ankiNoteBuilder.containsAnyMarker(field);
+ }
+
+ // Private
+
+ async _onOptionsChanged({options: {anki}}) {
+ this._ankiConnect.server = anki.server;
+ this._ankiConnect.enabled = anki.enable;
+
+ this._selectorObserver.disconnect();
+ this._selectorObserver.observe(document.documentElement, true);
+ }
+
+ _onAnkiErrorMessageDetailsToggleClick() {
+ const node = this._ankiErrorMessageDetailsContainer;
+ node.hidden = !node.hidden;
+ }
+
+ _onAnkiEnableChanged({detail: {value}}) {
+ if (this._ankiConnect.server === null) { return; }
+ this._ankiConnect.enabled = value;
+
+ for (const cardController of this._selectorObserver.datas()) {
+ cardController.updateAnkiState();
+ }
+ }
+
+ _onAnkiCardPrimaryTypeChange(e) {
+ if (this._ankiCardPrimary === null) { return; }
+ const node = e.currentTarget;
+ let ankiCardMenu;
+ if (node.selectedIndex >= 0) {
+ const option = node.options[node.selectedIndex];
+ ankiCardMenu = option.dataset.ankiCardMenu;
+ }
+
+ this._ankiCardPrimary.dataset.ankiCardType = node.value;
+ if (typeof ankiCardMenu !== 'undefined') {
+ this._ankiCardPrimary.dataset.ankiCardMenu = ankiCardMenu;
+ } else {
+ delete this._ankiCardPrimary.dataset.ankiCardMenu;
+ }
+ }
+
+ _createCardController(node) {
+ const cardController = new AnkiCardController(this._settingsController, this, node);
+ cardController.prepare();
+ return cardController;
+ }
+
+ _removeCardController(node, cardController) {
+ cardController.cleanup();
+ }
+
+ _isCardControllerStale(node, cardController) {
+ return cardController.isStale();
+ }
+
+ _setupFieldMenus() {
+ const fieldMenuTargets = [
+ [['terms'], '#anki-card-terms-field-menu-template'],
+ [['kanji'], '#anki-card-kanji-field-menu-template'],
+ [['terms', 'kanji'], '#anki-card-all-field-menu-template']
+ ];
+ for (const [types, selector] of fieldMenuTargets) {
+ const element = document.querySelector(selector);
+ if (element === null) { continue; }
+
+ let markers = [];
+ for (const type of types) {
+ markers.push(...this.getFieldMarkers(type));
+ }
+ markers = [...new Set(markers)];
+
+ const container = element.content.querySelector('.popup-menu-body');
+ if (container === null) { return; }
+
+ const fragment = document.createDocumentFragment();
+ for (const marker of markers) {
+ const option = document.createElement('button');
+ option.textContent = marker;
+ option.className = 'popup-menu-item';
+ option.dataset.menuAction = 'setFieldMarker';
+ option.dataset.marker = marker;
+ fragment.appendChild(option);
+ }
+ container.appendChild(fragment);
+ }
+ }
+
+ async _getAnkiData() {
+ this._setAnkiStatusChanging();
+ const [
+ [deckNames, error1],
+ [modelNames, error2]
+ ] = await Promise.all([
+ this._getDeckNames(),
+ this._getModelNames()
+ ]);
+
+ if (error1 !== null) {
+ this._showAnkiError(error1);
+ } else if (error2 !== null) {
+ this._showAnkiError(error2);
+ } else {
+ this._hideAnkiError();
+ }
+
+ return {deckNames, modelNames};
+ }
+
+ async _getDeckNames() {
+ try {
+ const result = await this._ankiConnect.getDeckNames();
+ this._sortStringArray(result);
+ return [result, null];
+ } catch (e) {
+ return [[], e];
+ }
+ }
+
+ async _getModelNames() {
+ try {
+ const result = await this._ankiConnect.getModelNames();
+ this._sortStringArray(result);
+ return [result, null];
+ } catch (e) {
+ return [[], e];
+ }
+ }
+
+ _setAnkiStatusChanging() {
+ this._ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent;
+ this._ankiErrorMessageNode.classList.remove('danger-text');
+ }
+
+ _hideAnkiError() {
+ if (this._ankiErrorContainer !== null) {
+ this._ankiErrorContainer.hidden = true;
+ }
+ this._ankiErrorMessageDetailsContainer.hidden = true;
+ this._ankiErrorMessageDetailsToggle.hidden = true;
+ this._ankiErrorInvalidResponseInfo.hidden = true;
+ this._ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled');
+ this._ankiErrorMessageNode.classList.remove('danger-text');
+ this._ankiErrorMessageDetailsNode.textContent = '';
+ }
+
+ _showAnkiError(error) {
+ let errorString = typeof error === 'object' && error !== null ? error.message : null;
+ if (!errorString) { errorString = `${error}`; }
+ if (!/[.!?]$/.test(errorString)) { errorString += '.'; }
+ this._ankiErrorMessageNode.textContent = errorString;
+ this._ankiErrorMessageNode.classList.add('danger-text');
+
+ const data = error.data;
+ let details = '';
+ if (typeof data !== 'undefined') {
+ details += `${JSON.stringify(data, null, 4)}\n\n`;
+ }
+ details += `${error.stack}`.trimRight();
+ this._ankiErrorMessageDetailsNode.textContent = details;
+
+ if (this._ankiErrorContainer !== null) {
+ this._ankiErrorContainer.hidden = false;
+ }
+ this._ankiErrorMessageDetailsContainer.hidden = true;
+ this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0);
+ this._ankiErrorMessageDetailsToggle.hidden = false;
+ }
+
+ _sortStringArray(array) {
+ const stringComparer = this._stringComparer;
+ array.sort((a, b) => stringComparer.compare(a, b));
+ }
+}
+
+class AnkiCardController {
+ constructor(settingsController, ankiController, node) {
+ this._settingsController = settingsController;
+ this._ankiController = ankiController;
+ this._node = node;
+ this._cardType = node.dataset.ankiCardType;
+ this._cardMenu = node.dataset.ankiCardMenu;
+ this._eventListeners = new EventListenerCollection();
+ this._fieldEventListeners = new EventListenerCollection();
+ this._deck = null;
+ this._model = null;
+ this._fields = null;
+ this._modelChangingTo = null;
+ this._ankiCardDeckSelect = null;
+ this._ankiCardModelSelect = null;
+ this._ankiCardFieldsContainer = null;
+ this._cleaned = false;
+ this._fieldEntries = [];
+ }
+
+ async prepare() {
+ const options = await this._settingsController.getOptions();
+ const ankiOptions = options.anki;
+ if (this._cleaned) { return; }
+
+ const cardOptions = this._getCardOptions(ankiOptions, this._cardType);
+ if (cardOptions === null) { return; }
+ const {deck, model, fields} = cardOptions;
+ this._deck = deck;
+ this._model = model;
+ this._fields = fields;
+
+ this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck');
+ this._ankiCardModelSelect = this._node.querySelector('.anki-card-model');
+ this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields');
+
+ this._setupSelects([], []);
+ this._setupFields();
+
+ this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false);
+ this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false);
+ this._eventListeners.on(this._settingsController, 'permissionsChanged', this._onPermissionsChanged.bind(this));
+
+ await this.updateAnkiState();
+ }
+
+ cleanup() {
+ this._cleaned = true;
+ this._fieldEntries = [];
+ this._eventListeners.removeAllEventListeners();
+ }
+
+ async updateAnkiState() {
+ if (this._fields === null) { return; }
+ const {deckNames, modelNames} = await this._ankiController.getAnkiData();
+ if (this._cleaned) { return; }
+ this._setupSelects(deckNames, modelNames);
+ }
+
+ isStale() {
+ return (this._cardType !== this._node.dataset.ankiCardType);
+ }
+
+ // Private
+
+ _onCardDeckChange(e) {
+ this._setDeck(e.currentTarget.value);
+ }
+
+ _onCardModelChange(e) {
+ this._setModel(e.currentTarget.value);
+ }
+
+ _onFieldChange(index, e) {
+ const node = e.currentTarget;
+ this._validateFieldPermissions(node, index, true);
+ this._validateField(node, index);
+ }
+
+ _onFieldInput(index, e) {
+ const node = e.currentTarget;
+ this._validateField(node, index);
+ }
+
+ _onFieldSettingChanged(index, e) {
+ const node = e.currentTarget;
+ this._validateFieldPermissions(node, index, false);
+ }
+
+ _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {
+ switch (action) {
+ case 'setFieldMarker':
+ this._setFieldMarker(button, item.dataset.marker);
+ break;
+ }
+ }
+
+ _onFieldMarkerLinkClick(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ this._setFieldMarker(link, link.textContent);
+ }
+
+ _validateField(node, index) {
+ let valid = (node.dataset.hasPermissions !== 'false');
+ if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) {
+ valid = false;
+ }
+ node.dataset.invalid = `${!valid}`;
+ }
+
+ _setFieldMarker(element, marker) {
+ const input = element.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value');
+ input.value = `{${marker}}`;
+ input.dispatchEvent(new Event('change'));
+ }
+
+ _getCardOptions(ankiOptions, cardType) {
+ switch (cardType) {
+ case 'terms': return ankiOptions.terms;
+ case 'kanji': return ankiOptions.kanji;
+ default: return null;
+ }
+ }
+
+ _setupSelects(deckNames, modelNames) {
+ const deck = this._deck;
+ const model = this._model;
+ if (!deckNames.includes(deck)) { deckNames = [...deckNames, deck]; }
+ if (!modelNames.includes(model)) { modelNames = [...modelNames, model]; }
+
+ this._setSelectOptions(this._ankiCardDeckSelect, deckNames);
+ this._ankiCardDeckSelect.value = deck;
+
+ this._setSelectOptions(this._ankiCardModelSelect, modelNames);
+ this._ankiCardModelSelect.value = model;
+ }
+
+ _setSelectOptions(select, optionValues) {
+ const fragment = document.createDocumentFragment();
+ for (const optionValue of optionValues) {
+ const option = document.createElement('option');
+ option.value = optionValue;
+ option.textContent = optionValue;
+ fragment.appendChild(option);
+ }
+ select.textContent = '';
+ select.appendChild(fragment);
+ }
+
+ _setupFields() {
+ this._fieldEventListeners.removeAllEventListeners();
+
+ const markers = this._ankiController.getFieldMarkers(this._cardType);
+ const totalFragment = document.createDocumentFragment();
+ this._fieldEntries = [];
+ let index = 0;
+ for (const [fieldName, fieldValue] of Object.entries(this._fields)) {
+ const content = this._settingsController.instantiateTemplateFragment('anki-card-field');
+
+ const fieldNameContainerNode = content.querySelector('.anki-card-field-name-container');
+ fieldNameContainerNode.dataset.index = `${index}`;
+ const fieldNameNode = content.querySelector('.anki-card-field-name');
+ fieldNameNode.textContent = fieldName;
+
+ const valueContainer = content.querySelector('.anki-card-field-value-container');
+ valueContainer.dataset.index = `${index}`;
+
+ const inputField = content.querySelector('.anki-card-field-value');
+ inputField.value = fieldValue;
+ inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]);
+ this._validateFieldPermissions(inputField, index, false);
+
+ this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false);
+ this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.bind(this, index), false);
+ this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false);
+ this._validateField(inputField, index);
+
+ const markerList = content.querySelector('.anki-card-field-marker-list');
+ if (markerList !== null) {
+ const markersFragment = this._ankiController.getFieldMarkersHtml(markers);
+ for (const element of markersFragment.querySelectorAll('.marker-link')) {
+ this._fieldEventListeners.addEventListener(element, 'click', this._onFieldMarkerLinkClick.bind(this), false);
+ }
+ markerList.appendChild(markersFragment);
+ }
+
+ const menuButton = content.querySelector('.anki-card-field-value-menu-button');
+ if (menuButton !== null) {
+ if (typeof this._cardMenu !== 'undefined') {
+ menuButton.dataset.menu = this._cardMenu;
+ } else {
+ delete menuButton.dataset.menu;
+ }
+ this._fieldEventListeners.addEventListener(menuButton, 'menuClose', this._onFieldMenuClose.bind(this), false);
+ }
+
+ totalFragment.appendChild(content);
+ this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode});
+
+ ++index;
+ }
+
+ const ELEMENT_NODE = Node.ELEMENT_NODE;
+ const container = this._ankiCardFieldsContainer;
+ for (const node of [...container.childNodes]) {
+ if (node.nodeType === ELEMENT_NODE && node.dataset.persistent === 'true') { continue; }
+ container.removeChild(node);
+ }
+ container.appendChild(totalFragment);
+
+ this._validateFields();
+ }
+
+ async _validateFields() {
+ const token = {};
+ this._validateFieldsToken = token;
+
+ let fieldNames;
+ try {
+ fieldNames = await this._ankiController.getModelFieldNames(this._model);
+ } catch (e) {
+ return;
+ }
+
+ if (token !== this._validateFieldsToken) { return; }
+
+ const fieldNamesSet = new Set(fieldNames);
+ let index = 0;
+ for (const {fieldName, fieldNameContainerNode} of this._fieldEntries) {
+ fieldNameContainerNode.dataset.invalid = `${!fieldNamesSet.has(fieldName)}`;
+ fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`;
+ ++index;
+ }
+ }
+
+ async _setDeck(value) {
+ if (this._deck === value) { return; }
+ this._deck = value;
+
+ await this._settingsController.modifyProfileSettings([{
+ action: 'set',
+ path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']),
+ value
+ }]);
+ }
+
+ async _setModel(value) {
+ if (this._modelChangingTo !== null) {
+ // Revert
+ this._ankiCardModelSelect.value = this._modelChangingTo;
+ return;
+ }
+ if (this._model === value) { return; }
+
+ let fieldNames;
+ let options;
+ try {
+ this._modelChangingTo = value;
+ fieldNames = await this._ankiController.getModelFieldNames(value);
+ options = await this._ankiController.settingsController.getOptions();
+ } catch (e) {
+ // Revert
+ this._ankiCardModelSelect.value = this._model;
+ return;
+ } finally {
+ this._modelChangingTo = null;
+ }
+
+ const cardType = this._cardType;
+ const cardOptions = this._getCardOptions(options.anki, cardType);
+ const oldFields = cardOptions !== null ? cardOptions.fields : null;
+
+ const fields = {};
+ for (let i = 0, ii = fieldNames.length; i < ii; ++i) {
+ const fieldName = fieldNames[i];
+ fields[fieldName] = this._getDefaultFieldValue(fieldName, i, cardType, oldFields);
+ }
+
+ const targets = [
+ {
+ action: 'set',
+ path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']),
+ value
+ },
+ {
+ action: 'set',
+ path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']),
+ value: fields
+ }
+ ];
+
+ this._model = value;
+ this._fields = fields;
+
+ await this._settingsController.modifyProfileSettings(targets);
+
+ this._setupFields();
+ }
+
+ async _requestPermissions(permissions) {
+ try {
+ await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true);
+ } catch (e) {
+ yomichan.logError(e);
+ }
+ }
+
+ async _validateFieldPermissions(node, index, request) {
+ const fieldValue = node.value;
+ const permissions = this._ankiController.getRequiredPermissions(fieldValue);
+ if (permissions.length > 0) {
+ node.dataset.requiredPermission = permissions.join(' ');
+ const hasPermissions = await (
+ request ?
+ this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) :
+ this._settingsController.permissionsUtil.hasPermissions({permissions})
+ );
+ node.dataset.hasPermissions = `${hasPermissions}`;
+ } else {
+ delete node.dataset.requiredPermission;
+ delete node.dataset.hasPermissions;
+ }
+
+ this._validateField(node, index);
+ }
+
+ _onPermissionsChanged({permissions: {permissions}}) {
+ const permissionsSet = new Set(permissions);
+ for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) {
+ const {inputField} = this._fieldEntries[i];
+ let {requiredPermission} = inputField.dataset;
+ if (typeof requiredPermission !== 'string') { continue; }
+ requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' '));
+
+ let hasPermissions = true;
+ for (const permission of requiredPermission) {
+ if (!permissionsSet.has(permission)) {
+ hasPermissions = false;
+ break;
+ }
+ }
+
+ inputField.dataset.hasPermissions = `${hasPermissions}`;
+ this._validateField(inputField, i);
+ }
+ }
+
+ _getDefaultFieldValue(fieldName, index, cardType, oldFields) {
+ if (
+ typeof oldFields === 'object' &&
+ oldFields !== null &&
+ Object.prototype.hasOwnProperty.call(oldFields, fieldName)
+ ) {
+ return oldFields[fieldName];
+ }
+
+ if (index === 0) {
+ return (cardType === 'kanji' ? '{character}' : '{expression}');
+ }
+
+ const markers = this._ankiController.getFieldMarkers(cardType);
+ const markerAliases = new Map([
+ ['glossary', ['definition', 'meaning']],
+ ['audio', ['sound']],
+ ['dictionary', ['dict']]
+ ]);
+
+ const hyphenPattern = /-/g;
+ for (const marker of markers) {
+ const names = [marker];
+ const aliases = markerAliases.get(marker);
+ if (typeof aliases !== 'undefined') {
+ names.push(...aliases);
+ }
+
+ let pattern = '^(?:';
+ for (let i = 0, ii = names.length; i < ii; ++i) {
+ const name = names[i];
+ if (i > 0) { pattern += '|'; }
+ pattern += name.replace(hyphenPattern, '[-_ ]*');
+ }
+ pattern += ')$';
+ pattern = new RegExp(pattern, 'i');
+
+ if (pattern.test(fieldName)) {
+ return `{${marker}}`;
+ }
+ }
+
+ return '';
+ }
+}