From ae92e0b3781e27d54cbac2570ba2a1b8a6b11999 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 24 Feb 2021 21:54:58 -0500 Subject: [PATCH] AnkiUtil (#1439) * Add AnkiUtil * Update AnkiConnect to use AnkiUtil * Use AnkiUtil in AnkiNoteBuilder * Replace containsAnyMarker with AnkiUtil.stringContainsAnyFieldMarker * Add AnkiUtil.getFieldMarkers * Add fieldsObjectContainsMarker to AnkiUtil * Remove unused global * Remove unused parameter: enabled * Add cloneFieldMarkerPattern --- .eslintrc.json | 1 + ext/action-popup.html | 1 + ext/background.html | 1 + ext/info.html | 1 + ext/js/comm/anki.js | 11 ++- ext/js/data/anki-note-builder.js | 30 ++----- ext/js/data/anki-util.js | 87 +++++++++++++++++++ ext/js/data/permissions-util.js | 21 ++--- ext/js/display/display.js | 11 +-- ext/js/pages/settings/anki-controller.js | 9 +- .../settings/anki-templates-controller.js | 2 +- ext/permissions.html | 1 + ext/popup.html | 1 + ext/search.html | 1 + ext/settings-old.html | 1 + ext/settings.html | 1 + ext/sw.js | 1 + ext/welcome.html | 1 + 18 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 ext/js/data/anki-util.js 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 @@ +