diff --git a/.eslintrc.json b/.eslintrc.json
index 0fe6743c..297f7500 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -178,6 +178,7 @@
"ext/js/comm/clipboard-monitor.js",
"ext/js/comm/clipboard-reader.js",
"ext/js/comm/mecab.js",
+ "ext/js/data/anki-util.js",
"ext/js/data/database.js",
"ext/js/data/json-schema.js",
"ext/js/data/options-util.js",
diff --git a/ext/action-popup.html b/ext/action-popup.html
index 03c94846..d5e8ee22 100644
--- a/ext/action-popup.html
+++ b/ext/action-popup.html
@@ -88,6 +88,7 @@
+
diff --git a/ext/background.html b/ext/background.html
index c04ed7c1..4ce7ddf0 100644
--- a/ext/background.html
+++ b/ext/background.html
@@ -31,6 +31,7 @@
+
diff --git a/ext/info.html b/ext/info.html
index 78210baf..d3bd3400 100644
--- a/ext/info.html
+++ b/ext/info.html
@@ -64,6 +64,7 @@
+
diff --git a/ext/js/comm/anki.js b/ext/js/comm/anki.js
index 251e0e0c..da234eff 100644
--- a/ext/js/comm/anki.js
+++ b/ext/js/comm/anki.js
@@ -15,6 +15,10 @@
* along with this program. If not, see .
*/
+/* global
+ * AnkiUtil
+ */
+
class AnkiConnect {
constructor() {
this._enabled = false;
@@ -113,7 +117,7 @@ class AnkiConnect {
query = `"deck:${this._escapeQuery(note.deckName)}" `;
break;
case 'deck-root':
- query = `"deck:${this._escapeQuery(this.getRootDeckName(note.deckName))}" `;
+ query = `"deck:${this._escapeQuery(AnkiUtil.getRootDeckName(note.deckName))}" `;
break;
}
query += this._fieldsToQuery(note.fields);
@@ -138,11 +142,6 @@ class AnkiConnect {
return await this.findCards(`nid:${noteId}`);
}
- getRootDeckName(deckName) {
- const index = deckName.indexOf('::');
- return index >= 0 ? deckName.substring(0, index) : deckName;
- }
-
// Private
async _checkVersion() {
diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js
index e1399f66..f12846b1 100644
--- a/ext/js/data/anki-note-builder.js
+++ b/ext/js/data/anki-note-builder.js
@@ -16,13 +16,14 @@
*/
/* global
+ * AnkiUtil
* TemplateRendererProxy
*/
class AnkiNoteBuilder {
- constructor(enabled) {
- this._markerPattern = /\{([\w-]+)\}/g;
- this._templateRenderer = enabled ? new TemplateRendererProxy() : null;
+ constructor() {
+ this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true);
+ this._templateRenderer = new TemplateRendererProxy();
}
async createNote({
@@ -46,7 +47,7 @@ class AnkiNoteBuilder {
let duplicateScopeCheckChildren = false;
if (duplicateScope === 'deck-root') {
duplicateScope = 'deck';
- duplicateScopeDeckName = this.getRootDeckName(deckName);
+ duplicateScopeDeckName = AnkiUtil.getRootDeckName(deckName);
duplicateScopeCheckChildren = true;
}
@@ -89,27 +90,6 @@ class AnkiNoteBuilder {
};
}
- containsMarker(fields, marker) {
- marker = `{${marker}}`;
- for (const [, fieldValue] of fields) {
- if (fieldValue.includes(marker)) {
- return true;
- }
- }
- return false;
- }
-
- containsAnyMarker(field) {
- const result = this._markerPattern.test(field);
- this._markerPattern.lastIndex = 0;
- return result;
- }
-
- getRootDeckName(deckName) {
- const index = deckName.indexOf('::');
- return index >= 0 ? deckName.substring(0, index) : deckName;
- }
-
// Private
async _formatField(field, data, templates, errors=null) {
diff --git a/ext/js/data/anki-util.js b/ext/js/data/anki-util.js
new file mode 100644
index 00000000..fc081ddc
--- /dev/null
+++ b/ext/js/data/anki-util.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 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 .
+ */
+
+/**
+ * This class has some general utility functions for working with Anki data.
+ */
+class AnkiUtil {
+ /**
+ * Gets the root deck name of a full deck name. If the deck is a root deck,
+ * the same name is returned. Nested decks are separated using '::'.
+ * @param deckName A string of the deck name.
+ * @returns A string corresponding to the name of the root deck.
+ */
+ static getRootDeckName(deckName) {
+ const index = deckName.indexOf('::');
+ return index >= 0 ? deckName.substring(0, index) : deckName;
+ }
+
+ /**
+ * Checks whether or not any marker is contained in a string.
+ * @param string A string to check.
+ * @return `true` if the text contains an Anki field marker, `false` otherwise.
+ */
+ static stringContainsAnyFieldMarker(string) {
+ const result = this._markerPattern.test(string);
+ this._markerPattern.lastIndex = 0;
+ return result;
+ }
+
+ /**
+ * Gets a list of all markers that are contained in a string.
+ * @param string A string to check.
+ * @return An array of marker strings.
+ */
+ static getFieldMarkers(string) {
+ const pattern = this._markerPattern;
+ const markers = [];
+ while (true) {
+ const match = pattern.exec(string);
+ if (match === null) { break; }
+ markers.push(match[1]);
+ }
+ return markers;
+ }
+
+ /**
+ * Checks whether an object of key-value pairs has a value which contains a specific marker.
+ * @param fieldsObject An object with key-value pairs, where the value corresponds to the field value.
+ * @param marker The marker string to check for, excluding brackets.
+ * @returns `true` if any of the fields contains the marker, `false` otherwise.
+ */
+ static fieldsObjectContainsMarker(fieldsObject, marker) {
+ marker = `{${marker}}`;
+ for (const [, fieldValue] of fieldsObject) {
+ if (fieldValue.includes(marker)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a regular expression which can be used to find markers in a string.
+ * @param global Whether or not the regular expression should have the global flag.
+ * @returns A new `RegExp` instance.
+ */
+ static cloneFieldMarkerPattern(global) {
+ return new RegExp(this._markerPattern.source, global ? 'g' : '');
+ }
+}
+
+// eslint-disable-next-line no-underscore-dangle
+AnkiUtil._markerPattern = /\{([\w-]+)\}/g;
diff --git a/ext/js/data/permissions-util.js b/ext/js/data/permissions-util.js
index bd3a18ce..3a5b5de5 100644
--- a/ext/js/data/permissions-util.js
+++ b/ext/js/data/permissions-util.js
@@ -15,13 +15,16 @@
* along with this program. If not, see .
*/
+/* global
+ * AnkiUtil
+ */
+
class PermissionsUtil {
constructor() {
this._ankiFieldMarkersRequiringClipboardPermission = new Set([
'clipboard-image',
'clipboard-text'
]);
- this._ankiMarkerPattern = /\{([\w-]+)\}/g;
}
hasPermissions(permissions) {
@@ -69,7 +72,7 @@ class PermissionsUtil {
}
getRequiredPermissionsForAnkiFieldValue(fieldValue) {
- const markers = this._getAnkiFieldMarkers(fieldValue);
+ const markers = AnkiUtil.getFieldMarkers(fieldValue);
const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission;
for (const marker of markers) {
if (markerPermissions.has(marker)) {
@@ -99,7 +102,7 @@ class PermissionsUtil {
];
for (const fields of fieldsList) {
for (const fieldValue of Object.values(fields)) {
- const markers = this._getAnkiFieldMarkers(fieldValue);
+ const markers = AnkiUtil.getFieldMarkers(fieldValue);
for (const marker of markers) {
if (fieldMarkersRequiringClipboardPermission.has(marker)) {
return false;
@@ -111,16 +114,4 @@ class PermissionsUtil {
return true;
}
-
- // Private
-
- _getAnkiFieldMarkers(fieldValue) {
- const pattern = this._ankiMarkerPattern;
- const markers = [];
- let match;
- while ((match = pattern.exec(fieldValue)) !== null) {
- markers.push(match[1]);
- }
- return markers;
- }
}
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index 517b391d..0e029748 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -17,6 +17,7 @@
/* global
* AnkiNoteBuilder
+ * AnkiUtil
* DisplayAudio
* DisplayGenerator
* DisplayHistory
@@ -85,7 +86,7 @@ class Display extends EventDispatcher {
});
this._ankiFieldTemplates = null;
this._ankiFieldTemplatesDefault = null;
- this._ankiNoteBuilder = new AnkiNoteBuilder(true);
+ this._ankiNoteBuilder = new AnkiNoteBuilder();
this._updateAdderButtonsPromise = Promise.resolve();
this._contentScrollElement = document.querySelector('#content-scroll');
this._contentScrollBodyElement = document.querySelector('#content-body');
@@ -1493,7 +1494,7 @@ class Display extends EventDispatcher {
const definitionDetails = this._getDefinitionDetailsForNote(definition);
let audioDetails = null;
- if (definitionDetails.type !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio')) {
+ if (definitionDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio')) {
const primaryCardAudio = this._displayAudio.getPrimaryCardAudio(definitionDetails.expression, definitionDetails.reading);
let preferredAudioIndex = null;
let sources2 = sources;
@@ -1504,11 +1505,11 @@ class Display extends EventDispatcher {
audioDetails = {sources: sources2, preferredAudioIndex, customSourceUrl, customSourceType};
}
- const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} : null);
+ const screenshotDetails = (AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') ? {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} : null);
const clipboardDetails = {
- image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'),
- text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text')
+ image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
+ text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
};
return await yomichan.api.injectAnkiNoteMedia(
diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js
index 26cab68f..509e263c 100644
--- a/ext/js/pages/settings/anki-controller.js
+++ b/ext/js/pages/settings/anki-controller.js
@@ -17,7 +17,7 @@
/* global
* AnkiConnect
- * AnkiNoteBuilder
+ * AnkiUtil
* ObjectPropertyAccessor
* SelectorObserver
*/
@@ -26,7 +26,6 @@ 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,
@@ -156,10 +155,6 @@ class AnkiController {
return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
}
- containsAnyMarker(field) {
- return this._ankiNoteBuilder.containsAnyMarker(field);
- }
-
// Private
async _onOptionsChanged({options: {anki}}) {
@@ -439,7 +434,7 @@ class AnkiCardController {
_validateField(node, index) {
let valid = (node.dataset.hasPermissions !== 'false');
- if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) {
+ if (valid && index === 0 && !AnkiUtil.stringContainsAnyFieldMarker(node.value)) {
valid = false;
}
node.dataset.invalid = `${!valid}`;
diff --git a/ext/js/pages/settings/anki-templates-controller.js b/ext/js/pages/settings/anki-templates-controller.js
index 8e3a1a70..710946be 100644
--- a/ext/js/pages/settings/anki-templates-controller.js
+++ b/ext/js/pages/settings/anki-templates-controller.js
@@ -32,7 +32,7 @@ class AnkiTemplatesController {
this._renderFieldInput = null;
this._renderResult = null;
this._fieldTemplateResetModal = null;
- this._ankiNoteBuilder = new AnkiNoteBuilder(true);
+ this._ankiNoteBuilder = new AnkiNoteBuilder();
}
async prepare() {
diff --git a/ext/permissions.html b/ext/permissions.html
index ded314a3..5bd6987f 100644
--- a/ext/permissions.html
+++ b/ext/permissions.html
@@ -167,6 +167,7 @@
+
diff --git a/ext/popup.html b/ext/popup.html
index 88648809..60058b77 100644
--- a/ext/popup.html
+++ b/ext/popup.html
@@ -97,6 +97,7 @@
+
diff --git a/ext/search.html b/ext/search.html
index d1c28fc8..b5aa56f0 100644
--- a/ext/search.html
+++ b/ext/search.html
@@ -83,6 +83,7 @@
+
diff --git a/ext/settings-old.html b/ext/settings-old.html
index 6d5bb6f7..be791bb2 100644
--- a/ext/settings-old.html
+++ b/ext/settings-old.html
@@ -1288,6 +1288,7 @@
+
diff --git a/ext/settings.html b/ext/settings.html
index 2dcbc083..54ee7d6d 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -3198,6 +3198,7 @@
+
diff --git a/ext/sw.js b/ext/sw.js
index 802f3ba3..89fb4d0f 100644
--- a/ext/sw.js
+++ b/ext/sw.js
@@ -28,6 +28,7 @@ self.importScripts(
'/js/comm/clipboard-monitor.js',
'/js/comm/clipboard-reader.js',
'/js/comm/mecab.js',
+ '/js/data/anki-util.js',
'/js/data/database.js',
'/js/data/json-schema.js',
'/js/data/options-util.js',
diff --git a/ext/welcome.html b/ext/welcome.html
index 22f6a681..5876ad4c 100644
--- a/ext/welcome.html
+++ b/ext/welcome.html
@@ -320,6 +320,7 @@
+