From 8e304b83c685dde17a00d402877a21303b7c11f2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 3 Jan 2021 12:12:55 -0500 Subject: Translator regex replacements (#1199) * Add support for regex replacements during the translation process * Allow assignment of textReplacements * Rename * Set up test data * Write expected data * Set up options * Prevent infinite loop if regex matches empty string * Implement setting controller * Add support for testing pattern replacements --- .../translation-text-replacements-controller.js | 241 +++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 ext/bg/js/settings2/translation-text-replacements-controller.js (limited to 'ext/bg/js/settings2/translation-text-replacements-controller.js') diff --git a/ext/bg/js/settings2/translation-text-replacements-controller.js b/ext/bg/js/settings2/translation-text-replacements-controller.js new file mode 100644 index 00000000..41ee8e3f --- /dev/null +++ b/ext/bg/js/settings2/translation-text-replacements-controller.js @@ -0,0 +1,241 @@ +/* + * 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 . + */ + +class TranslationTextReplacementsController { + constructor(settingsController) { + this._settingsController = settingsController; + this._entryContainer = null; + this._entries = []; + } + + async prepare() { + this._entryContainer = document.querySelector('#translation-text-replacement-list'); + const addButton = document.querySelector('#translation-text-replacement-add'); + + addButton.addEventListener('click', this._onAdd.bind(this), false); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + await this._updateOptions(); + } + + + async addGroup() { + const options = await this._settingsController.getOptions(); + const {groups} = options.translation.textReplacements; + const newEntry = this._createNewEntry(); + const target = ( + (groups.length === 0) ? + { + action: 'splice', + path: 'translation.textReplacements.groups', + start: 0, + deleteCount: 0, + items: [[newEntry]] + } : + { + action: 'splice', + path: 'translation.textReplacements.groups[0]', + start: groups[0].length, + deleteCount: 0, + items: [newEntry] + } + ); + + await this._settingsController.modifyProfileSettings([target]); + await this._updateOptions(); + } + + async deleteGroup(index) { + const options = await this._settingsController.getOptions(); + const {groups} = options.translation.textReplacements; + if (groups.length === 0) { return false; } + + const group0 = groups[0]; + if (index < 0 || index >= group0.length) { return false; } + + const target = ( + (group0.length > 1) ? + { + action: 'splice', + path: 'translation.textReplacements.groups[0]', + start: index, + deleteCount: 1, + items: [] + } : + { + action: 'splice', + path: 'translation.textReplacements.groups', + start: 0, + deleteCount: group0.length, + items: [] + } + ); + + await this._settingsController.modifyProfileSettings([target]); + await this._updateOptions(); + return true; + } + + // Private + + _onOptionsChanged({options}) { + for (const entry of this._entries) { + entry.cleanup(); + } + this._entries = []; + + const {groups} = options.translation.textReplacements; + if (groups.length > 0) { + const group0 = groups[0]; + for (let i = 0, ii = group0.length; i < ii; ++i) { + const data = group0[i]; + const node = this._settingsController.instantiateTemplate('translation-text-replacement-entry'); + this._entryContainer.appendChild(node); + const entry = new TranslationTextReplacementsEntry(this, node, i, data); + this._entries.push(entry); + entry.prepare(); + } + } + } + + _onAdd() { + this.addGroup(); + } + + async _updateOptions() { + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + + _createNewEntry() { + return {pattern: '', ignoreCase: false, replacement: ''}; + } +} + +class TranslationTextReplacementsEntry { + constructor(parent, node, index) { + this._parent = parent; + this._node = node; + this._index = index; + this._eventListeners = new EventListenerCollection(); + this._patternInput = null; + this._replacementInput = null; + this._ignoreCaseToggle = null; + this._testInput = null; + this._testOutput = null; + } + + prepare() { + const patternInput = this._node.querySelector('.translation-text-replacement-pattern'); + const replacementInput = this._node.querySelector('.translation-text-replacement-replacement'); + const ignoreCaseToggle = this._node.querySelector('.translation-text-replacement-pattern-ignore-case'); + const menuButton = this._node.querySelector('.translation-text-replacement-button'); + const testInput = this._node.querySelector('.translation-text-replacement-test-input'); + const testOutput = this._node.querySelector('.translation-text-replacement-test-output'); + + this._patternInput = patternInput; + this._replacementInput = replacementInput; + this._ignoreCaseToggle = ignoreCaseToggle; + this._testInput = testInput; + this._testOutput = testOutput; + + const pathBase = `translation.textReplacements.groups[0][${this._index}]`; + patternInput.dataset.setting = `${pathBase}.pattern`; + replacementInput.dataset.setting = `${pathBase}.replacement`; + ignoreCaseToggle.dataset.setting = `${pathBase}.ignoreCase`; + + this._eventListeners.addEventListener(menuButton, 'menuOpened', this._onMenuOpened.bind(this), false); + this._eventListeners.addEventListener(menuButton, 'menuClosed', this._onMenuClosed.bind(this), false); + this._eventListeners.addEventListener(patternInput, 'settingChanged', this._onPatternChanged.bind(this), false); + this._eventListeners.addEventListener(ignoreCaseToggle, 'settingChanged', this._updateTestInput.bind(this), false); + this._eventListeners.addEventListener(replacementInput, 'settingChanged', this._updateTestInput.bind(this), false); + this._eventListeners.addEventListener(testInput, 'input', this._updateTestInput.bind(this), false); + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + if (this._node.parentNode !== null) { + this._node.parentNode.removeChild(this._node); + } + } + + // Private + + _onMenuOpened({detail: {menu}}) { + const testVisible = this._isTestVisible(); + menu.querySelector('[data-menu-action=showTest]').hidden = testVisible; + menu.querySelector('[data-menu-action=hideTest]').hidden = !testVisible; + } + + _onMenuClosed({detail: {action}}) { + switch (action) { + case 'remove': + this._parent.deleteGroup(this._index); + break; + case 'showTest': + this._setTestVisible(true); + break; + case 'hideTest': + this._setTestVisible(false); + break; + } + } + + _onPatternChanged({detail: {value}}) { + this._validatePattern(value); + this._updateTestInput(); + } + + _validatePattern(value) { + let okay = false; + try { + new RegExp(value, 'g'); + okay = true; + } catch (e) { + // NOP + } + + this._patternInput.dataset.invalid = `${!okay}`; + } + + _isTestVisible() { + return this._node.dataset.testVisible === 'true'; + } + + _setTestVisible(visible) { + this._node.dataset.testVisible = `${visible}`; + this._updateTestInput(); + } + + _updateTestInput() { + if (!this._isTestVisible()) { return; } + + const ignoreCase = this._ignoreCaseToggle.checked; + const pattern = this._patternInput.value; + let regex; + try { + regex = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); + } catch (e) { + return; + } + + const replacement = this._replacementInput.value; + const input = this._testInput.value; + const output = input.replace(regex, replacement); + this._testOutput.value = output; + } +} -- cgit v1.2.3