* 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
This commit is contained in:
toasted-nutbread 2021-02-24 21:54:58 -05:00 committed by GitHub
parent 0a76de1b44
commit ae92e0b378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 123 additions and 59 deletions

View File

@ -178,6 +178,7 @@
"ext/js/comm/clipboard-monitor.js", "ext/js/comm/clipboard-monitor.js",
"ext/js/comm/clipboard-reader.js", "ext/js/comm/clipboard-reader.js",
"ext/js/comm/mecab.js", "ext/js/comm/mecab.js",
"ext/js/data/anki-util.js",
"ext/js/data/database.js", "ext/js/data/database.js",
"ext/js/data/json-schema.js", "ext/js/data/json-schema.js",
"ext/js/data/options-util.js", "ext/js/data/options-util.js",

View File

@ -88,6 +88,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/permissions-util.js"></script> <script src="/js/data/permissions-util.js"></script>
<script src="/js/input/hotkey-help-controller.js"></script> <script src="/js/input/hotkey-help-controller.js"></script>
<script src="/js/input/hotkey-util.js"></script> <script src="/js/input/hotkey-util.js"></script>

View File

@ -31,6 +31,7 @@
<script src="/js/comm/clipboard-monitor.js"></script> <script src="/js/comm/clipboard-monitor.js"></script>
<script src="/js/comm/clipboard-reader.js"></script> <script src="/js/comm/clipboard-reader.js"></script>
<script src="/js/comm/mecab.js"></script> <script src="/js/comm/mecab.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/database.js"></script> <script src="/js/data/database.js"></script>
<script src="/js/data/json-schema.js"></script> <script src="/js/data/json-schema.js"></script>
<script src="/js/data/options-util.js"></script> <script src="/js/data/options-util.js"></script>

View File

@ -64,6 +64,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/permissions-util.js"></script> <script src="/js/data/permissions-util.js"></script>
<script src="/js/dom/document-focus-controller.js"></script> <script src="/js/dom/document-focus-controller.js"></script>
<script src="/js/dom/html-template-collection.js"></script> <script src="/js/dom/html-template-collection.js"></script>

View File

@ -15,6 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* global
* AnkiUtil
*/
class AnkiConnect { class AnkiConnect {
constructor() { constructor() {
this._enabled = false; this._enabled = false;
@ -113,7 +117,7 @@ class AnkiConnect {
query = `"deck:${this._escapeQuery(note.deckName)}" `; query = `"deck:${this._escapeQuery(note.deckName)}" `;
break; break;
case 'deck-root': case 'deck-root':
query = `"deck:${this._escapeQuery(this.getRootDeckName(note.deckName))}" `; query = `"deck:${this._escapeQuery(AnkiUtil.getRootDeckName(note.deckName))}" `;
break; break;
} }
query += this._fieldsToQuery(note.fields); query += this._fieldsToQuery(note.fields);
@ -138,11 +142,6 @@ class AnkiConnect {
return await this.findCards(`nid:${noteId}`); return await this.findCards(`nid:${noteId}`);
} }
getRootDeckName(deckName) {
const index = deckName.indexOf('::');
return index >= 0 ? deckName.substring(0, index) : deckName;
}
// Private // Private
async _checkVersion() { async _checkVersion() {

View File

@ -16,13 +16,14 @@
*/ */
/* global /* global
* AnkiUtil
* TemplateRendererProxy * TemplateRendererProxy
*/ */
class AnkiNoteBuilder { class AnkiNoteBuilder {
constructor(enabled) { constructor() {
this._markerPattern = /\{([\w-]+)\}/g; this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true);
this._templateRenderer = enabled ? new TemplateRendererProxy() : null; this._templateRenderer = new TemplateRendererProxy();
} }
async createNote({ async createNote({
@ -46,7 +47,7 @@ class AnkiNoteBuilder {
let duplicateScopeCheckChildren = false; let duplicateScopeCheckChildren = false;
if (duplicateScope === 'deck-root') { if (duplicateScope === 'deck-root') {
duplicateScope = 'deck'; duplicateScope = 'deck';
duplicateScopeDeckName = this.getRootDeckName(deckName); duplicateScopeDeckName = AnkiUtil.getRootDeckName(deckName);
duplicateScopeCheckChildren = true; 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 // Private
async _formatField(field, data, templates, errors=null) { async _formatField(field, data, templates, errors=null) {

87
ext/js/data/anki-util.js Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
/**
* 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;

View File

@ -15,13 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* global
* AnkiUtil
*/
class PermissionsUtil { class PermissionsUtil {
constructor() { constructor() {
this._ankiFieldMarkersRequiringClipboardPermission = new Set([ this._ankiFieldMarkersRequiringClipboardPermission = new Set([
'clipboard-image', 'clipboard-image',
'clipboard-text' 'clipboard-text'
]); ]);
this._ankiMarkerPattern = /\{([\w-]+)\}/g;
} }
hasPermissions(permissions) { hasPermissions(permissions) {
@ -69,7 +72,7 @@ class PermissionsUtil {
} }
getRequiredPermissionsForAnkiFieldValue(fieldValue) { getRequiredPermissionsForAnkiFieldValue(fieldValue) {
const markers = this._getAnkiFieldMarkers(fieldValue); const markers = AnkiUtil.getFieldMarkers(fieldValue);
const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission; const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission;
for (const marker of markers) { for (const marker of markers) {
if (markerPermissions.has(marker)) { if (markerPermissions.has(marker)) {
@ -99,7 +102,7 @@ class PermissionsUtil {
]; ];
for (const fields of fieldsList) { for (const fields of fieldsList) {
for (const fieldValue of Object.values(fields)) { for (const fieldValue of Object.values(fields)) {
const markers = this._getAnkiFieldMarkers(fieldValue); const markers = AnkiUtil.getFieldMarkers(fieldValue);
for (const marker of markers) { for (const marker of markers) {
if (fieldMarkersRequiringClipboardPermission.has(marker)) { if (fieldMarkersRequiringClipboardPermission.has(marker)) {
return false; return false;
@ -111,16 +114,4 @@ class PermissionsUtil {
return true; 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;
}
} }

View File

@ -17,6 +17,7 @@
/* global /* global
* AnkiNoteBuilder * AnkiNoteBuilder
* AnkiUtil
* DisplayAudio * DisplayAudio
* DisplayGenerator * DisplayGenerator
* DisplayHistory * DisplayHistory
@ -85,7 +86,7 @@ class Display extends EventDispatcher {
}); });
this._ankiFieldTemplates = null; this._ankiFieldTemplates = null;
this._ankiFieldTemplatesDefault = null; this._ankiFieldTemplatesDefault = null;
this._ankiNoteBuilder = new AnkiNoteBuilder(true); this._ankiNoteBuilder = new AnkiNoteBuilder();
this._updateAdderButtonsPromise = Promise.resolve(); this._updateAdderButtonsPromise = Promise.resolve();
this._contentScrollElement = document.querySelector('#content-scroll'); this._contentScrollElement = document.querySelector('#content-scroll');
this._contentScrollBodyElement = document.querySelector('#content-body'); this._contentScrollBodyElement = document.querySelector('#content-body');
@ -1493,7 +1494,7 @@ class Display extends EventDispatcher {
const definitionDetails = this._getDefinitionDetailsForNote(definition); const definitionDetails = this._getDefinitionDetailsForNote(definition);
let audioDetails = null; 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); const primaryCardAudio = this._displayAudio.getPrimaryCardAudio(definitionDetails.expression, definitionDetails.reading);
let preferredAudioIndex = null; let preferredAudioIndex = null;
let sources2 = sources; let sources2 = sources;
@ -1504,11 +1505,11 @@ class Display extends EventDispatcher {
audioDetails = {sources: sources2, preferredAudioIndex, customSourceUrl, customSourceType}; 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 = { const clipboardDetails = {
image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text') text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
}; };
return await yomichan.api.injectAnkiNoteMedia( return await yomichan.api.injectAnkiNoteMedia(

View File

@ -17,7 +17,7 @@
/* global /* global
* AnkiConnect * AnkiConnect
* AnkiNoteBuilder * AnkiUtil
* ObjectPropertyAccessor * ObjectPropertyAccessor
* SelectorObserver * SelectorObserver
*/ */
@ -26,7 +26,6 @@ class AnkiController {
constructor(settingsController) { constructor(settingsController) {
this._settingsController = settingsController; this._settingsController = settingsController;
this._ankiConnect = new AnkiConnect(); this._ankiConnect = new AnkiConnect();
this._ankiNoteBuilder = new AnkiNoteBuilder(false);
this._selectorObserver = new SelectorObserver({ this._selectorObserver = new SelectorObserver({
selector: '.anki-card', selector: '.anki-card',
ignoreSelector: null, ignoreSelector: null,
@ -156,10 +155,6 @@ class AnkiController {
return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue); return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
} }
containsAnyMarker(field) {
return this._ankiNoteBuilder.containsAnyMarker(field);
}
// Private // Private
async _onOptionsChanged({options: {anki}}) { async _onOptionsChanged({options: {anki}}) {
@ -439,7 +434,7 @@ class AnkiCardController {
_validateField(node, index) { _validateField(node, index) {
let valid = (node.dataset.hasPermissions !== 'false'); 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; valid = false;
} }
node.dataset.invalid = `${!valid}`; node.dataset.invalid = `${!valid}`;

View File

@ -32,7 +32,7 @@ class AnkiTemplatesController {
this._renderFieldInput = null; this._renderFieldInput = null;
this._renderResult = null; this._renderResult = null;
this._fieldTemplateResetModal = null; this._fieldTemplateResetModal = null;
this._ankiNoteBuilder = new AnkiNoteBuilder(true); this._ankiNoteBuilder = new AnkiNoteBuilder();
} }
async prepare() { async prepare() {

View File

@ -167,6 +167,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/permissions-util.js"></script> <script src="/js/data/permissions-util.js"></script>
<script src="/js/dom/document-focus-controller.js"></script> <script src="/js/dom/document-focus-controller.js"></script>
<script src="/js/dom/html-template-collection.js"></script> <script src="/js/dom/html-template-collection.js"></script>

View File

@ -97,6 +97,7 @@
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/comm/frame-endpoint.js"></script> <script src="/js/comm/frame-endpoint.js"></script>
<script src="/js/data/anki-note-builder.js"></script> <script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/display/display.js"></script> <script src="/js/display/display.js"></script>
<script src="/js/display/display-audio.js"></script> <script src="/js/display/display-audio.js"></script>
<script src="/js/display/display-generator.js"></script> <script src="/js/display/display-generator.js"></script>

View File

@ -83,6 +83,7 @@
<script src="/js/comm/clipboard-monitor.js"></script> <script src="/js/comm/clipboard-monitor.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-note-builder.js"></script> <script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/display/display.js"></script> <script src="/js/display/display.js"></script>
<script src="/js/display/display-audio.js"></script> <script src="/js/display/display-audio.js"></script>
<script src="/js/display/display-generator.js"></script> <script src="/js/display/display-generator.js"></script>

View File

@ -1288,6 +1288,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-note-builder.js"></script> <script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/database.js"></script> <script src="/js/data/database.js"></script>
<script src="/js/data/json-schema.js"></script> <script src="/js/data/json-schema.js"></script>
<script src="/js/data/options-util.js"></script> <script src="/js/data/options-util.js"></script>

View File

@ -3198,6 +3198,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-note-builder.js"></script> <script src="/js/data/anki-note-builder.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/database.js"></script> <script src="/js/data/database.js"></script>
<script src="/js/data/json-schema.js"></script> <script src="/js/data/json-schema.js"></script>
<script src="/js/data/options-util.js"></script> <script src="/js/data/options-util.js"></script>

View File

@ -28,6 +28,7 @@ self.importScripts(
'/js/comm/clipboard-monitor.js', '/js/comm/clipboard-monitor.js',
'/js/comm/clipboard-reader.js', '/js/comm/clipboard-reader.js',
'/js/comm/mecab.js', '/js/comm/mecab.js',
'/js/data/anki-util.js',
'/js/data/database.js', '/js/data/database.js',
'/js/data/json-schema.js', '/js/data/json-schema.js',
'/js/data/options-util.js', '/js/data/options-util.js',

View File

@ -320,6 +320,7 @@
<script src="/js/comm/api.js"></script> <script src="/js/comm/api.js"></script>
<script src="/js/comm/cross-frame-api.js"></script> <script src="/js/comm/cross-frame-api.js"></script>
<script src="/js/data/anki-util.js"></script>
<script src="/js/data/database.js"></script> <script src="/js/data/database.js"></script>
<script src="/js/data/json-schema.js"></script> <script src="/js/data/json-schema.js"></script>
<script src="/js/data/permissions-util.js"></script> <script src="/js/data/permissions-util.js"></script>