Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)

Add basic webserver API (#2025)

* Add to settings.html

* Add setting

* Base implementation

* Add yomitan api to permissions check

* Disable version checking

* Separate startApiServer and add proper logging

* Remove unused

* Start server if remote version is requested but not started

* Start api server on extension startup

* Pass apiMap to yomitanApi to call for data

* Remove sequence and data

* Pull params from piped post body

* Less risky formatting

* Send json through without stringify

* Simplify user facing api params

* Add kanjiEntries api path

* Log error in yomitan on bad api request

* Remove profileindex requirement

* Add initial `ankiFields` api path support

* Add linkedom dependency

* Require passing document into AnkiTemplateRenderer

* Get TEXT_NODE and ELEMENT_NODE from document

* Use linkedom domless document for backend API side AnkiTemplateRenderer

* Allow multiple markers and rename handlebars to markers

* Support kanji type in ankiFields api path

* Allow document to be undefined in template renderer test

* Set up commonData for every dictionary entry

* Send data for all entries

* Allow setting the max number of entries

* Fix copyright year

* Update legal-npm for linkedom inclusion

* Add general.yomitanApiServer option and fix general.enableYomitanApi assignment

* Pass through url for testing yomitan api

* Fix extension on yomitan-api.d.ts

* Make test actually test the api server

* Change remoteVersion to serverVersion

* Dont parse body if no length

* Add yomitanVersion api path

* Remove unnecessary array

* Fix link

* Remove unused

* Remove isActive usage

authored by

Kuuuube and committed by
GitHub
c9f7ef42 b20fe656

+837 -16
+18
dev/lib/linkedom.js
··· 1 + /* 2 + * Copyright (C) 2025 Yomitan Authors 3 + * 4 + * This program is free software: you can redistribute it and/or modify 5 + * it under the terms of the GNU General Public License as published by 6 + * the Free Software Foundation, either version 3 of the License, or 7 + * (at your option) any later version. 8 + * 9 + * This program is distributed in the hope that it will be useful, 10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 + * GNU General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 + */ 17 + 18 + export * from 'linkedom';
+11 -1
ext/data/schemas/options-schema.json
··· 127 127 "stickySearchHeader", 128 128 "fontFamily", 129 129 "fontSize", 130 - "lineHeight" 130 + "lineHeight", 131 + "enableYomitanApi", 132 + "yomitanApiServer" 131 133 ], 132 134 "properties": { 133 135 "enable": { ··· 336 338 "stickySearchHeader": { 337 339 "type": "boolean", 338 340 "default": false 341 + }, 342 + "enableYomitanApi": { 343 + "type": "boolean", 344 + "default": false 345 + }, 346 + "yomitanApiServer": { 347 + "type": "string", 348 + "default": "http://127.0.0.1:8766" 339 349 } 340 350 } 341 351 },
+44
ext/js/background/backend.js
··· 20 20 import {ClipboardMonitor} from '../comm/clipboard-monitor.js'; 21 21 import {ClipboardReader} from '../comm/clipboard-reader.js'; 22 22 import {Mecab} from '../comm/mecab.js'; 23 + import {YomitanApi} from '../comm/yomitan-api.js'; 23 24 import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; 24 25 import {ExtensionError} from '../core/extension-error.js'; 25 26 import {fetchText} from '../core/fetch-utilities.js'; ··· 179 180 ['isTabSearchPopup', this._onApiIsTabSearchPopup.bind(this)], 180 181 ['triggerDatabaseUpdated', this._onApiTriggerDatabaseUpdated.bind(this)], 181 182 ['testMecab', this._onApiTestMecab.bind(this)], 183 + ['testYomitanApi', this._onApiTestYomitanApi.bind(this)], 182 184 ['isTextLookupWorthy', this._onApiIsTextLookupWorthy.bind(this)], 183 185 ['getTermFrequencies', this._onApiGetTermFrequencies.bind(this)], 184 186 ['findAnkiNotes', this._onApiFindAnkiNotes.bind(this)], ··· 202 204 ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], 203 205 ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)], 204 206 ])); 207 + 208 + /** @type {YomitanApi} */ 209 + this._yomitanApi = new YomitanApi(this._apiMap); 205 210 } 206 211 207 212 /** ··· 994 999 return true; 995 1000 } 996 1001 1002 + /** @type {import('api').ApiHandler<'testYomitanApi'>} */ 1003 + async _onApiTestYomitanApi({url}) { 1004 + if (!this._yomitanApi.isEnabled()) { 1005 + throw new Error('Yomitan Api not enabled'); 1006 + } 1007 + 1008 + let permissionsOkay = false; 1009 + try { 1010 + permissionsOkay = await hasPermissions({permissions: ['nativeMessaging']}); 1011 + } catch (e) { 1012 + // NOP 1013 + } 1014 + if (!permissionsOkay) { 1015 + throw new Error('Insufficient permissions'); 1016 + } 1017 + 1018 + const disconnect = !this._yomitanApi.isConnected(); 1019 + try { 1020 + const version = await this._yomitanApi.getRemoteVersion(url); 1021 + if (version === null) { 1022 + throw new Error('Could not connect to native Yomitan API component'); 1023 + } 1024 + 1025 + const localVersion = this._yomitanApi.getLocalVersion(); 1026 + if (version !== localVersion) { 1027 + throw new Error(`Yomitan API component version not supported: ${version}`); 1028 + } 1029 + } finally { 1030 + // Disconnect if the connection was previously disconnected 1031 + if (disconnect && this._yomitanApi.isEnabled()) { 1032 + this._yomitanApi.disconnect(); 1033 + } 1034 + } 1035 + 1036 + return true; 1037 + } 1038 + 997 1039 /** @type {import('api').ApiHandler<'isTextLookupWorthy'>} */ 998 1040 _onApiIsTextLookupWorthy({text, language}) { 999 1041 return isTextLookupWorthy(text, language); ··· 1404 1446 this._anki.apiKey = apiKey; 1405 1447 1406 1448 this._mecab.setEnabled(options.parsing.enableMecabParser && enabled); 1449 + 1450 + void this._yomitanApi.setEnabled(options.general.enableYomitanApi && enabled); 1407 1451 1408 1452 if (options.clipboard.enableBackgroundMonitor && enabled) { 1409 1453 this._clipboardMonitor.start();
+8
ext/js/comm/api.js
··· 347 347 } 348 348 349 349 /** 350 + * @param {string} url 351 + * @returns {Promise<import('api').ApiReturn<'testYomitanApi'>>} 352 + */ 353 + testYomitanApi(url) { 354 + return this._invoke('testYomitanApi', {url}); 355 + } 356 + 357 + /** 350 358 * @param {import('api').ApiParam<'isTextLookupWorthy', 'text'>} text 351 359 * @param {import('api').ApiParam<'isTextLookupWorthy', 'language'>} language 352 360 * @returns {Promise<import('api').ApiReturn<'isTextLookupWorthy'>>}
+394
ext/js/comm/yomitan-api.js
··· 1 + /* 2 + * Copyright (C) 2025 Yomitan Authors 3 + * 4 + * This program is free software: you can redistribute it and/or modify 5 + * it under the terms of the GNU General Public License as published by 6 + * the Free Software Foundation, either version 3 of the License, or 7 + * (at your option) any later version. 8 + * 9 + * This program is distributed in the hope that it will be useful, 10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 + * GNU General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 + */ 17 + 18 + import {parseHTML} from '../../lib/linkedom.js'; 19 + import {invokeApiMapHandler} from '../core/api-map.js'; 20 + import {EventListenerCollection} from '../core/event-listener-collection.js'; 21 + import {ExtensionError} from '../core/extension-error.js'; 22 + import {parseJson, readResponseJson} from '../core/json.js'; 23 + import {log} from '../core/log.js'; 24 + import {toError} from '../core/to-error.js'; 25 + import {getDynamicTemplates} from '../data/anki-template-util.js'; 26 + import {AnkiTemplateRenderer} from '../templates/anki-template-renderer.js'; 27 + 28 + /** */ 29 + export class YomitanApi { 30 + /** 31 + * @param {import('api').ApiMap} apiMap 32 + */ 33 + constructor(apiMap) { 34 + /** @type {?chrome.runtime.Port} */ 35 + this._port = null; 36 + /** @type {EventListenerCollection} */ 37 + this._eventListeners = new EventListenerCollection(); 38 + /** @type {number} */ 39 + this._timeout = 5000; 40 + /** @type {number} */ 41 + this._version = 1; 42 + /** @type {?number} */ 43 + this._remoteVersion = null; 44 + /** @type {boolean} */ 45 + this._enabled = false; 46 + /** @type {?Promise<void>} */ 47 + this._setupPortPromise = null; 48 + /** @type {import('api').ApiMap} */ 49 + this._apiMap = apiMap; 50 + } 51 + 52 + /** 53 + * @returns {boolean} 54 + */ 55 + isEnabled() { 56 + return this._enabled; 57 + } 58 + 59 + /** 60 + * @param {boolean} enabled 61 + */ 62 + async setEnabled(enabled) { 63 + this._enabled = !!enabled; 64 + if (!this._enabled && this._port !== null) { 65 + this._clearPort(); 66 + } 67 + if (this._enabled) { 68 + await this.startApiServer(); 69 + } 70 + } 71 + 72 + /** */ 73 + disconnect() { 74 + if (this._port !== null) { 75 + this._clearPort(); 76 + } 77 + } 78 + 79 + /** 80 + * @returns {boolean} 81 + */ 82 + isConnected() { 83 + return (this._port !== null); 84 + } 85 + 86 + /** 87 + * @returns {number} 88 + */ 89 + getLocalVersion() { 90 + return this._version; 91 + } 92 + 93 + /** 94 + * @param {string} url 95 + * @returns {Promise<?number>} 96 + */ 97 + async getRemoteVersion(url) { 98 + if (this._port === null) { 99 + await this.startApiServer(); 100 + } 101 + await this._updateRemoteVersion(url); 102 + return this._remoteVersion; 103 + } 104 + 105 + /** 106 + * @returns {Promise<boolean>} 107 + */ 108 + async startApiServer() { 109 + try { 110 + await this._setupPortWrapper(); 111 + return true; 112 + } catch (e) { 113 + log.error(e); 114 + return false; 115 + } 116 + } 117 + 118 + // Private 119 + 120 + /** 121 + * @param {unknown} message 122 + */ 123 + async _onMessage(message) { 124 + if (typeof message !== 'object' || message === null) { return; } 125 + 126 + if (this._port !== null) { 127 + const {action, params, body} = /** @type {import('core').SerializableObject} */ (message); 128 + if (typeof action !== 'string' || typeof params !== 'object' || typeof body !== 'string') { 129 + this._port.postMessage({action, params, body, data: 'null', responseStatusCode: 400}); 130 + return; 131 + } 132 + 133 + const optionsFull = await this._invoke('optionsGetFull', void 0); 134 + 135 + try { 136 + /** @type {?object} */ 137 + const parsedBody = body.length > 0 ? parseJson(body) : null; 138 + 139 + let result = null; 140 + let statusCode = 200; 141 + switch (action) { 142 + case 'yomitanVersion': { 143 + const {version} = chrome.runtime.getManifest(); 144 + result = {version: version}; 145 + break; 146 + } 147 + case 'termEntries': { 148 + /** @type {import('yomitan-api.js').termEntriesInput} */ 149 + // @ts-expect-error - Allow this to error 150 + const {term} = parsedBody; 151 + const invokeParams = { 152 + text: term, 153 + details: {}, 154 + optionsContext: {index: optionsFull.profileCurrent}, 155 + }; 156 + result = await this._invoke( 157 + 'termsFind', 158 + invokeParams, 159 + ); 160 + break; 161 + } 162 + case 'kanjiEntries': { 163 + /** @type {import('yomitan-api.js').kanjiEntriesInput} */ 164 + // @ts-expect-error - Allow this to error 165 + const {character} = parsedBody; 166 + const invokeParams = { 167 + text: character, 168 + details: {}, 169 + optionsContext: {index: optionsFull.profileCurrent}, 170 + }; 171 + result = await this._invoke( 172 + 'kanjiFind', 173 + invokeParams, 174 + ); 175 + break; 176 + } 177 + case 'ankiFields': { 178 + /** @type {import('yomitan-api.js').ankiFieldsInput} */ 179 + // @ts-expect-error - Allow this to error 180 + const {text, type, markers, maxEntries} = parsedBody; 181 + 182 + const ankiTemplate = await this._getAnkiTemplate(optionsFull.profiles[optionsFull.profileCurrent].options); 183 + let dictionaryEntries = await this._getDictionaryEntries(text, type, optionsFull.profileCurrent); 184 + if (maxEntries > 0) { 185 + dictionaryEntries = dictionaryEntries.slice(0, maxEntries); 186 + } 187 + const commonDatas = await this._createCommonDatas(text, dictionaryEntries); 188 + // @ts-expect-error - `parseHTML` can return `null` but this input has been validated to not be `null` 189 + const domlessDocument = parseHTML('').document; 190 + const ankiTemplateRenderer = new AnkiTemplateRenderer(domlessDocument); 191 + await ankiTemplateRenderer.prepare(); 192 + const templateRenderer = ankiTemplateRenderer.templateRenderer; 193 + 194 + /** @type {Array<Record<string, string>>} */ 195 + const ankiFieldsResults = []; 196 + for (const commonData of commonDatas) { 197 + /** @type {Record<string, string>} */ 198 + const ankiFieldsResult = {}; 199 + for (const marker of markers) { 200 + const templateResult = templateRenderer.render(ankiTemplate, {marker: marker, commonData: commonData}, 'ankiNote'); 201 + ankiFieldsResult[marker] = templateResult.result; 202 + } 203 + ankiFieldsResults.push(ankiFieldsResult); 204 + } 205 + result = ankiFieldsResults; 206 + break; 207 + } 208 + default: 209 + statusCode = 400; 210 + } 211 + 212 + this._port.postMessage({action, params, body, data: result, responseStatusCode: statusCode}); 213 + } catch (error) { 214 + log.error(error); 215 + this._port.postMessage({action, params, body, data: JSON.stringify(error), responseStatusCode: 500}); 216 + } 217 + } 218 + } 219 + 220 + /** 221 + * @param {import('settings').ProfileOptions} options 222 + * @returns {Promise<string>} 223 + */ 224 + async _getAnkiTemplate(options) { 225 + let staticTemplates = options.anki.fieldTemplates; 226 + if (typeof staticTemplates !== 'string') { staticTemplates = await this._invoke('getDefaultAnkiFieldTemplates', void 0); } 227 + const dictionaryInfo = await this._invoke('getDictionaryInfo', void 0); 228 + const dynamicTemplates = getDynamicTemplates(options, dictionaryInfo); 229 + return staticTemplates + '\n' + dynamicTemplates; 230 + } 231 + 232 + /** 233 + * @param {string} text 234 + * @param {import('settings.js').AnkiCardFormatType} type 235 + * @param {number} profileIndex 236 + * @returns {Promise<import('dictionary.js').DictionaryEntry[]>} 237 + */ 238 + async _getDictionaryEntries(text, type, profileIndex) { 239 + if (type === 'term') { 240 + const invokeParams = { 241 + text: text, 242 + details: {}, 243 + optionsContext: {index: profileIndex}, 244 + }; 245 + return (await this._invoke('termsFind', invokeParams)).dictionaryEntries; 246 + } else { 247 + const invokeParams = { 248 + text: text, 249 + details: {}, 250 + optionsContext: {index: profileIndex}, 251 + }; 252 + return await this._invoke('kanjiFind', invokeParams); 253 + } 254 + } 255 + 256 + /** 257 + * @param {string} text 258 + * @param {import('dictionary.js').DictionaryEntry[]} dictionaryEntries 259 + * @returns {Promise<import('anki-note-builder.js').CommonData[]>} 260 + */ 261 + async _createCommonDatas(text, dictionaryEntries) { 262 + /** @type {import('anki-note-builder.js').CommonData[]} */ 263 + const commonDatas = []; 264 + for (const dictionaryEntry of dictionaryEntries) { 265 + commonDatas.push({ 266 + dictionaryEntry: dictionaryEntry, 267 + resultOutputMode: 'group', 268 + cardFormat: { 269 + type: 'term', 270 + name: '', 271 + deck: '', 272 + model: '', 273 + fields: {}, 274 + icon: 'big-circle', 275 + }, 276 + glossaryLayoutMode: 'default', 277 + compactTags: false, 278 + context: { 279 + url: '', 280 + documentTitle: '', 281 + query: text, 282 + fullQuery: text, 283 + sentence: { 284 + text: text, 285 + offset: 0, 286 + }, 287 + }, 288 + dictionaryStylesMap: new Map(), 289 + }); 290 + } 291 + return commonDatas; 292 + } 293 + 294 + /** 295 + * @param {string} url 296 + */ 297 + async _updateRemoteVersion(url) { 298 + if (!url) { 299 + throw new Error('Missing Yomitan API URL'); 300 + } 301 + try { 302 + const response = await fetch(url + '/serverVersion', { 303 + method: 'POST', 304 + }); 305 + /** @type {import('yomitan-api.js').remoteVersionResponse} */ 306 + const {version} = await readResponseJson(response); 307 + 308 + this._remoteVersion = version; 309 + } catch (e) { 310 + log.error(e); 311 + throw new Error('Failed to fetch. Try again in a moment. The nativemessaging component can take a few seconds to start.'); 312 + } 313 + } 314 + 315 + /** 316 + * @returns {void} 317 + */ 318 + _onDisconnect() { 319 + if (this._port === null) { return; } 320 + const e = chrome.runtime.lastError; 321 + const error = new Error(e ? e.message : 'Yomitan Api disconnected'); 322 + log.error(error); 323 + this._clearPort(); 324 + } 325 + 326 + /** 327 + * @returns {Promise<void>} 328 + */ 329 + async _setupPortWrapper() { 330 + if (!this._enabled) { 331 + throw new Error('Yomitan Api not enabled'); 332 + } 333 + if (this._setupPortPromise === null) { 334 + this._setupPortPromise = this._setupPort(); 335 + } 336 + try { 337 + await this._setupPortPromise; 338 + } catch (e) { 339 + throw toError(e); 340 + } 341 + } 342 + 343 + /** 344 + * @returns {Promise<void>} 345 + */ 346 + async _setupPort() { 347 + const port = chrome.runtime.connectNative('yomitan_api'); 348 + this._eventListeners.addListener(port.onMessage, this._onMessage.bind(this)); 349 + this._eventListeners.addListener(port.onDisconnect, this._onDisconnect.bind(this)); 350 + this._port = port; 351 + } 352 + 353 + /** 354 + * @returns {void} 355 + */ 356 + _clearPort() { 357 + if (this._port !== null) { 358 + this._port.disconnect(); 359 + this._port = null; 360 + } 361 + this._eventListeners.removeAllEventListeners(); 362 + this._setupPortPromise = null; 363 + } 364 + 365 + /** 366 + * @template {import('api').ApiNames} TAction 367 + * @template {import('api').ApiParams<TAction>} TParams 368 + * @param {TAction} action 369 + * @param {TParams} params 370 + * @returns {Promise<import('api').ApiReturn<TAction>>} 371 + */ 372 + _invoke(action, params) { 373 + return new Promise((resolve, reject) => { 374 + try { 375 + invokeApiMapHandler(this._apiMap, action, params, [{}], (response) => { 376 + if (response !== null && typeof response === 'object') { 377 + const {error} = /** @type {import('core').UnknownObject} */ (response); 378 + if (typeof error !== 'undefined') { 379 + reject(ExtensionError.deserialize(/** @type {import('core').SerializedError} */(error))); 380 + } else { 381 + const {result} = /** @type {import('core').UnknownObject} */ (response); 382 + resolve(/** @type {import('api').ApiReturn<TAction>} */(result)); 383 + } 384 + } else { 385 + const message = response === null ? 'Unexpected null response. You may need to refresh the page.' : `Unexpected response of type ${typeof response}. You may need to refresh the page.`; 386 + reject(new Error(`${message} (${JSON.stringify(action)})`)); 387 + } 388 + }); 389 + } catch (e) { 390 + reject(e); 391 + } 392 + }); 393 + } 394 + }
+13
ext/js/data/options-util.js
··· 576 576 this._updateVersion62, 577 577 this._updateVersion63, 578 578 this._updateVersion64, 579 + this._updateVersion65, 579 580 ]; 580 581 /* eslint-enable @typescript-eslint/unbound-method */ 581 582 if (typeof targetVersion === 'number' && targetVersion < result.length) { ··· 1720 1721 break; 1721 1722 } 1722 1723 } 1724 + } 1725 + } 1726 + 1727 + /** 1728 + * - Added general.enableYomitanApi 1729 + * - Added general.yomitanApiServer 1730 + * @type {import('options-util').UpdateFunction} 1731 + */ 1732 + async _updateVersion65(options) { 1733 + for (const profile of options.profiles) { 1734 + profile.options.general.enableYomitanApi = false; 1735 + profile.options.general.yomitanApiServer = 'http://127.0.0.1:8766'; 1723 1736 } 1724 1737 } 1725 1738
+1 -1
ext/js/data/permissions-util.js
··· 121 121 export function hasRequiredPermissionsForOptions(permissions, options) { 122 122 const permissionsSet = new Set(permissions.permissions); 123 123 124 - if (!permissionsSet.has('nativeMessaging') && options.parsing.enableMecabParser) { 124 + if (!permissionsSet.has('nativeMessaging') && (options.parsing.enableMecabParser || options.general.enableYomitanApi)) { 125 125 return false; 126 126 } 127 127
+4
ext/js/pages/settings/settings-main.js
··· 51 51 import {StatusFooter} from './status-footer.js'; 52 52 import {StorageController} from './storage-controller.js'; 53 53 import {TranslationTextReplacementsController} from './translation-text-replacements-controller.js'; 54 + import {YomitanApiController} from './yomitan-api-controller.js'; 54 55 55 56 /** 56 57 * @param {GenericSettingController} genericSettingController ··· 168 169 169 170 const mecabController = new MecabController(application.api); 170 171 mecabController.prepare(); 172 + 173 + const yomitanApiController = new YomitanApiController(application.api); 174 + yomitanApiController.prepare(); 171 175 172 176 const collapsibleDictionaryController = new CollapsibleDictionaryController(settingsController); 173 177 preparePromises.push(collapsibleDictionaryController.prepare());
+83
ext/js/pages/settings/yomitan-api-controller.js
··· 1 + /* 2 + * Copyright (C) 2025 Yomitan Authors 3 + * 4 + * This program is free software: you can redistribute it and/or modify 5 + * it under the terms of the GNU General Public License as published by 6 + * the Free Software Foundation, either version 3 of the License, or 7 + * (at your option) any later version. 8 + * 9 + * This program is distributed in the hope that it will be useful, 10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 + * GNU General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 + */ 17 + 18 + import {toError} from '../../core/to-error.js'; 19 + import {querySelectorNotNull} from '../../dom/query-selector.js'; 20 + 21 + export class YomitanApiController { 22 + /** 23 + * @param {import('../../comm/api.js').API} api 24 + */ 25 + constructor(api) { 26 + /** @type {import('../../comm/api.js').API} */ 27 + this._api = api; 28 + /** @type {HTMLButtonElement} */ 29 + this._testButton = querySelectorNotNull(document, '#test-yomitan-api-button'); 30 + /** @type {HTMLElement} */ 31 + this._resultsContainer = querySelectorNotNull(document, '#test-yomitan-api-results'); 32 + /** @type {HTMLInputElement} */ 33 + this._urlInput = querySelectorNotNull(document, '#test-yomitan-url-input'); 34 + /** @type {boolean} */ 35 + this._testActive = false; 36 + } 37 + 38 + /** */ 39 + prepare() { 40 + this._testButton.addEventListener('click', this._onTestButtonClick.bind(this), false); 41 + } 42 + 43 + // Private 44 + 45 + /** 46 + * @param {MouseEvent} e 47 + */ 48 + _onTestButtonClick(e) { 49 + e.preventDefault(); 50 + void this._testYomitanApi(); 51 + } 52 + 53 + /** */ 54 + async _testYomitanApi() { 55 + if (this._testActive) { return; } 56 + 57 + try { 58 + this._testActive = true; 59 + const resultsContainer = /** @type {HTMLElement} */ (this._resultsContainer); 60 + /** @type {HTMLButtonElement} */ (this._testButton).disabled = true; 61 + resultsContainer.textContent = ''; 62 + resultsContainer.hidden = true; 63 + await this._api.testYomitanApi(this._urlInput.value); 64 + this._setStatus('Connection was successful', false); 65 + } catch (e) { 66 + this._setStatus(toError(e).message, true); 67 + } finally { 68 + this._testActive = false; 69 + /** @type {HTMLButtonElement} */ (this._testButton).disabled = false; 70 + } 71 + } 72 + 73 + /** 74 + * @param {string} message 75 + * @param {boolean} isError 76 + */ 77 + _setStatus(message, isError) { 78 + const resultsContainer = /** @type {HTMLElement} */ (this._resultsContainer); 79 + resultsContainer.textContent = message; 80 + resultsContainer.hidden = false; 81 + resultsContainer.classList.toggle('danger-text', isError); 82 + } 83 + }
+13 -8
ext/js/templates/anki-template-renderer.js
··· 17 17 */ 18 18 19 19 import {Handlebars} from '../../lib/handlebars.js'; 20 + import {NodeFilter} from '../../lib/linkedom.js'; 20 21 import {createAnkiNoteData} from '../data/anki-note-data-creator.js'; 21 22 import {getPronunciationsOfType, isNonNounVerbOrAdjective} from '../dictionary/dictionary-data-util.js'; 22 23 import {createPronunciationDownstepPosition, createPronunciationGraph, createPronunciationGraphJJ, createPronunciationText} from '../display/pronunciation-generator.js'; ··· 34 35 export class AnkiTemplateRenderer { 35 36 /** 36 37 * Creates a new instance of the class. 38 + * @param {Document} document 37 39 */ 38 - constructor() { 40 + constructor(document) { 39 41 /** @type {CssStyleApplier} */ 40 42 this._structuredContentStyleApplier = new CssStyleApplier('/data/structured-content-style.json'); 41 43 /** @type {CssStyleApplier} */ ··· 54 56 this._cleanupCallbacks = []; 55 57 /** @type {?HTMLElement} */ 56 58 this._temporaryElement = null; 59 + /** @type {Document} */ 60 + this._document = document; 57 61 } 58 62 59 63 /** ··· 562 566 _getTemporaryElement() { 563 567 let element = this._temporaryElement; 564 568 if (element === null) { 565 - element = document.createElement('div'); 569 + element = this._document.createElement('div'); 566 570 this._temporaryElement = element; 567 571 } 568 572 return element; ··· 605 609 * @param {?RegExp} datasetKeyIgnorePattern 606 610 */ 607 611 _normalizeHtml(root, styleApplier, datasetKeyIgnorePattern) { 608 - const {ELEMENT_NODE, TEXT_NODE} = Node; 609 - const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); 612 + const TEXT_NODE = this._document.TEXT_NODE; 613 + const ELEMENT_NODE = this._document.ELEMENT_NODE; 614 + const treeWalker = this._document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); 610 615 /** @type {HTMLElement[]} */ 611 616 const elements = []; 612 617 /** @type {Text[]} */ ··· 644 649 if (parts.length <= 1) { return; } 645 650 const {parentNode} = textNode; 646 651 if (parentNode === null) { return; } 647 - const fragment = document.createDocumentFragment(); 652 + const fragment = this._document.createDocumentFragment(); 648 653 for (let i = 0, ii = parts.length; i < ii; ++i) { 649 - if (i > 0) { fragment.appendChild(document.createElement('br')); } 650 - fragment.appendChild(document.createTextNode(parts[i])); 654 + if (i > 0) { fragment.appendChild(this._document.createElement('br')); } 655 + fragment.appendChild(this._document.createTextNode(parts[i])); 651 656 } 652 657 parentNode.replaceChild(fragment, textNode); 653 658 } ··· 658 663 */ 659 664 _createStructuredContentGenerator(data) { 660 665 const contentManager = new AnkiTemplateRendererContentManager(this._mediaProvider, data); 661 - const instance = new StructuredContentGenerator(contentManager, document); 666 + const instance = new StructuredContentGenerator(contentManager, this._document); 662 667 this._cleanupCallbacks.push(() => contentManager.unloadAll()); 663 668 return instance; 664 669 }
+1 -1
ext/js/templates/template-renderer-frame-main.js
··· 21 21 22 22 /** Entry point. */ 23 23 async function main() { 24 - const ankiTemplateRenderer = new AnkiTemplateRenderer(); 24 + const ankiTemplateRenderer = new AnkiTemplateRenderer(document); 25 25 await ankiTemplateRenderer.prepare(); 26 26 const templateRendererFrameApi = new TemplateRendererFrameApi(ankiTemplateRenderer.templateRenderer); 27 27 templateRendererFrameApi.prepare();
+1 -1
ext/legal-npm.html
··· 60 60 } 61 61 62 62 </style> 63 - <table><thead><tr><th class="string">name</th><th class="string">installed version</th><th class="string">license type</th><th class="string">link</th></tr></thead><tbody><tr><td class="string">@resvg/resvg-wasm</td><td class="string">2.6.2</td><td class="string">MPL-2.0</td><td class="string">git+ssh://git@github.com/yisibl/resvg-js.git</td></tr><tr><td class="string">@zip.js/zip.js</td><td class="string">2.7.54</td><td class="string">BSD-3-Clause</td><td class="string">git+https://github.com/gildas-lormeau/zip.js.git</td></tr><tr><td class="string">dexie</td><td class="string">3.2.5</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dfahlander/Dexie.js.git</td></tr><tr><td class="string">dexie-export-import</td><td class="string">4.1.4</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">hangul-js</td><td class="string">0.2.6</td><td class="string">MIT</td><td class="string">git://github.com/e-/Hangul.js.git</td></tr><tr><td class="string">parse5</td><td class="string">7.2.1</td><td class="string">MIT</td><td class="string">git://github.com/inikulin/parse5.git</td></tr><tr><td class="string">yomitan-handlebars</td><td class="string">1.0.0</td><td class="string">MIT</td><td class="string">n/a</td></tr></tbody></table> 63 + <table><thead><tr><th class="string">name</th><th class="string">installed version</th><th class="string">license type</th><th class="string">link</th></tr></thead><tbody><tr><td class="string">@resvg/resvg-wasm</td><td class="string">2.6.2</td><td class="string">MPL-2.0</td><td class="string">git+ssh://git@github.com/yisibl/resvg-js.git</td></tr><tr><td class="string">@zip.js/zip.js</td><td class="string">2.7.54</td><td class="string">BSD-3-Clause</td><td class="string">git+https://github.com/gildas-lormeau/zip.js.git</td></tr><tr><td class="string">dexie</td><td class="string">4.0.11</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">dexie-export-import</td><td class="string">4.1.4</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">hangul-js</td><td class="string">0.2.6</td><td class="string">MIT</td><td class="string">git://github.com/e-/Hangul.js.git</td></tr><tr><td class="string">kanji-processor</td><td class="string">1.0.2</td><td class="string">n/a</td><td class="string">https://registry.npmjs.org/kanji-processor/-/kanji-processor-1.0.2.tgz</td></tr><tr><td class="string">parse5</td><td class="string">7.2.1</td><td class="string">MIT</td><td class="string">git://github.com/inikulin/parse5.git</td></tr><tr><td class="string">yomitan-handlebars</td><td class="string">1.0.0</td><td class="string">MIT</td><td class="string">n/a</td></tr><tr><td class="string">linkedom</td><td class="string">0.18.10</td><td class="string">ISC</td><td class="string">git+https://github.com/WebReflection/linkedom.git</td></tr></tbody></table> 64 64 </body> 65 65 </html>
+31
ext/settings.html
··· 172 172 <input type="number" min="1" data-setting="general.maxResults"> 173 173 </div> 174 174 </div></div> 175 + <div class="settings-item advanced-only"> 176 + <div class="settings-item-inner"> 177 + <div class="settings-item-left"> 178 + <div class="settings-item-invalid-indicator"></div> 179 + <div class="settings-item-label"> 180 + Enable Yomitan API 181 + </div> 182 + <div class="settings-item-description"> 183 + Enable support for sending local web requests to fetch data from Yomitan. 184 + <a tabindex="0" class="more-toggle more-only" data-parent-distance="4">More&hellip;</a> 185 + </div> 186 + </div> 187 + <div class="settings-item-right"> 188 + <label class="toggle"><input type="checkbox" class="permissions-toggle" data-permissions-setting="general.enableYomitanApi" data-required-permissions="nativeMessaging"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label> 189 + </div> 190 + </div> 191 + <div class="settings-item-children more" hidden> 192 + <p> 193 + To activate the Yomitan API, a native messaging component must be installed. 194 + A setup guide can be found <a href="https://github.com/Kuuuube/yomitan-api/blob/master/README.md" target="_blank" rel="noopener noreferrer">here</a>. 195 + </p> 196 + <div class="margin-above flex-row-nowrap"> 197 + <button type="button" id="test-yomitan-api-button">Test</button> 198 + <input id="test-yomitan-url-input" class="flex-margin-left" type="text" placeholder="http://127.0.0.1:8766" spellcheck="false" autocomplete="off" data-setting="general.yomitanApiServer"> 199 + <div id="test-yomitan-api-results" class="flex-margin-left" hidden></div> 200 + </div> 201 + <p class="margin-above"> 202 + <a tabindex="0" class="more-toggle" data-parent-distance="3">Less&hellip;</a> 203 + </p> 204 + </div> 205 + </div> 175 206 </div> 176 207 177 208 <!-- Dictionaries -->
+164
package-lock.json
··· 15 15 "dexie-export-import": "^4.1.4", 16 16 "hangul-js": "^0.2.6", 17 17 "kanji-processor": "^1.0.2", 18 + "linkedom": "^0.18.10", 18 19 "parse5": "^7.2.1", 19 20 "yomitan-handlebars": "git+https://github.com/yomidevs/yomitan-handlebars.git#12aff5e3550954d7d3a98a5917ff7d579f3cce25" 20 21 }, ··· 4050 4051 "dev": true, 4051 4052 "license": "MIT" 4052 4053 }, 4054 + "node_modules/boolbase": { 4055 + "version": "1.0.0", 4056 + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 4057 + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 4058 + "license": "ISC" 4059 + }, 4053 4060 "node_modules/brace-expansion": { 4054 4061 "version": "1.1.11", 4055 4062 "dev": true, ··· 4509 4516 "node": ">=12 || >=16" 4510 4517 } 4511 4518 }, 4519 + "node_modules/css-select": { 4520 + "version": "5.1.0", 4521 + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 4522 + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 4523 + "license": "BSD-2-Clause", 4524 + "dependencies": { 4525 + "boolbase": "^1.0.0", 4526 + "css-what": "^6.1.0", 4527 + "domhandler": "^5.0.2", 4528 + "domutils": "^3.0.1", 4529 + "nth-check": "^2.0.1" 4530 + }, 4531 + "funding": { 4532 + "url": "https://github.com/sponsors/fb55" 4533 + } 4534 + }, 4512 4535 "node_modules/css-tree": { 4513 4536 "version": "3.1.0", 4514 4537 "dev": true, ··· 4521 4544 "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" 4522 4545 } 4523 4546 }, 4547 + "node_modules/css-what": { 4548 + "version": "6.1.0", 4549 + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 4550 + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 4551 + "license": "BSD-2-Clause", 4552 + "engines": { 4553 + "node": ">= 6" 4554 + }, 4555 + "funding": { 4556 + "url": "https://github.com/sponsors/fb55" 4557 + } 4558 + }, 4524 4559 "node_modules/cssesc": { 4525 4560 "version": "3.0.0", 4526 4561 "dev": true, ··· 4531 4566 "engines": { 4532 4567 "node": ">=4" 4533 4568 } 4569 + }, 4570 + "node_modules/cssom": { 4571 + "version": "0.5.0", 4572 + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 4573 + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", 4574 + "license": "MIT" 4534 4575 }, 4535 4576 "node_modules/cssstyle": { 4536 4577 "version": "4.1.0", ··· 4751 4792 "node": ">=8" 4752 4793 } 4753 4794 }, 4795 + "node_modules/dom-serializer": { 4796 + "version": "2.0.0", 4797 + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 4798 + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 4799 + "license": "MIT", 4800 + "dependencies": { 4801 + "domelementtype": "^2.3.0", 4802 + "domhandler": "^5.0.2", 4803 + "entities": "^4.2.0" 4804 + }, 4805 + "funding": { 4806 + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 4807 + } 4808 + }, 4809 + "node_modules/domelementtype": { 4810 + "version": "2.3.0", 4811 + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 4812 + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 4813 + "funding": [ 4814 + { 4815 + "type": "github", 4816 + "url": "https://github.com/sponsors/fb55" 4817 + } 4818 + ], 4819 + "license": "BSD-2-Clause" 4820 + }, 4821 + "node_modules/domhandler": { 4822 + "version": "5.0.3", 4823 + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 4824 + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 4825 + "license": "BSD-2-Clause", 4826 + "dependencies": { 4827 + "domelementtype": "^2.3.0" 4828 + }, 4829 + "engines": { 4830 + "node": ">= 4" 4831 + }, 4832 + "funding": { 4833 + "url": "https://github.com/fb55/domhandler?sponsor=1" 4834 + } 4835 + }, 4836 + "node_modules/domutils": { 4837 + "version": "3.2.2", 4838 + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", 4839 + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 4840 + "license": "BSD-2-Clause", 4841 + "dependencies": { 4842 + "dom-serializer": "^2.0.0", 4843 + "domelementtype": "^2.3.0", 4844 + "domhandler": "^5.0.3" 4845 + }, 4846 + "funding": { 4847 + "url": "https://github.com/fb55/domutils?sponsor=1" 4848 + } 4849 + }, 4754 4850 "node_modules/dotenv": { 4755 4851 "version": "16.4.7", 4756 4852 "dev": true, ··· 6327 6423 } 6328 6424 } 6329 6425 }, 6426 + "node_modules/htmlparser2": { 6427 + "version": "10.0.0", 6428 + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", 6429 + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", 6430 + "funding": [ 6431 + "https://github.com/fb55/htmlparser2?sponsor=1", 6432 + { 6433 + "type": "github", 6434 + "url": "https://github.com/sponsors/fb55" 6435 + } 6436 + ], 6437 + "license": "MIT", 6438 + "dependencies": { 6439 + "domelementtype": "^2.3.0", 6440 + "domhandler": "^5.0.3", 6441 + "domutils": "^3.2.1", 6442 + "entities": "^6.0.0" 6443 + } 6444 + }, 6445 + "node_modules/htmlparser2/node_modules/entities": { 6446 + "version": "6.0.0", 6447 + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", 6448 + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", 6449 + "license": "BSD-2-Clause", 6450 + "engines": { 6451 + "node": ">=0.12" 6452 + }, 6453 + "funding": { 6454 + "url": "https://github.com/fb55/entities?sponsor=1" 6455 + } 6456 + }, 6330 6457 "node_modules/http-cache-semantics": { 6331 6458 "version": "4.1.1", 6332 6459 "dev": true, ··· 7151 7278 "dev": true, 7152 7279 "license": "MIT" 7153 7280 }, 7281 + "node_modules/linkedom": { 7282 + "version": "0.18.10", 7283 + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.10.tgz", 7284 + "integrity": "sha512-ESCqVAtme2GI3zZnlVRidiydByV6WmPlmKeFzFVQslADiAO2Wi+H6xL/5kr/pUOESjEoVb2Eb3cYFJ/TQhQOWA==", 7285 + "license": "ISC", 7286 + "dependencies": { 7287 + "css-select": "^5.1.0", 7288 + "cssom": "^0.5.0", 7289 + "html-escaper": "^3.0.3", 7290 + "htmlparser2": "^10.0.0", 7291 + "uhyphen": "^0.2.0" 7292 + } 7293 + }, 7294 + "node_modules/linkedom/node_modules/html-escaper": { 7295 + "version": "3.0.3", 7296 + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", 7297 + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", 7298 + "license": "MIT" 7299 + }, 7154 7300 "node_modules/lint-staged": { 7155 7301 "version": "15.3.0", 7156 7302 "dev": true, ··· 7753 7899 }, 7754 7900 "funding": { 7755 7901 "url": "https://github.com/sponsors/sindresorhus" 7902 + } 7903 + }, 7904 + "node_modules/nth-check": { 7905 + "version": "2.1.1", 7906 + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 7907 + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 7908 + "license": "BSD-2-Clause", 7909 + "dependencies": { 7910 + "boolbase": "^1.0.0" 7911 + }, 7912 + "funding": { 7913 + "url": "https://github.com/fb55/nth-check?sponsor=1" 7756 7914 } 7757 7915 }, 7758 7916 "node_modules/nwsapi": { ··· 9962 10120 "engines": { 9963 10121 "node": ">=0.8.0" 9964 10122 } 10123 + }, 10124 + "node_modules/uhyphen": { 10125 + "version": "0.2.0", 10126 + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", 10127 + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", 10128 + "license": "ISC" 9965 10129 }, 9966 10130 "node_modules/unbox-primitive": { 9967 10131 "version": "1.1.0",
+2 -1
package.json
··· 119 119 "hangul-js": "^0.2.6", 120 120 "kanji-processor": "^1.0.2", 121 121 "parse5": "^7.2.1", 122 - "yomitan-handlebars": "git+https://github.com/yomidevs/yomitan-handlebars.git#12aff5e3550954d7d3a98a5917ff7d579f3cce25" 122 + "yomitan-handlebars": "git+https://github.com/yomidevs/yomitan-handlebars.git#12aff5e3550954d7d3a98a5917ff7d579f3cce25", 123 + "linkedom": "^0.18.10" 123 124 }, 124 125 "lint-staged": { 125 126 "*.md": "prettier --write"
+3 -1
test/fixtures/anki-template-renderer-test.js
··· 27 27 */ 28 28 export async function createAnkiTemplateRendererTest() { 29 29 const test = createDomTest(void 0); 30 - const ankiTemplateRenderer = new AnkiTemplateRenderer(); 30 + // @ts-expect-error - Document is not accessible in this test and is not accessed, allow it to be undefined 31 + // eslint-disable-next-line no-undefined 32 + const ankiTemplateRenderer = new AnkiTemplateRenderer(undefined); 31 33 await ankiTemplateRenderer.prepare(); 32 34 /** @type {import('vitest').TestAPI<{window: import('jsdom').DOMWindow, ankiTemplateRenderer: AnkiTemplateRenderer}>} */ 33 35 // eslint-disable-next-line sonarjs/prefer-immediate-return
+3 -1
test/options-util.test.js
··· 312 312 sortFrequencyDictionary: null, 313 313 sortFrequencyDictionaryOrder: 'descending', 314 314 stickySearchHeader: false, 315 + enableYomitanApi: false, 316 + yomitanApiServer: 'http://127.0.0.1:8766', 315 317 }, 316 318 audio: { 317 319 enabled: true, ··· 687 689 }, 688 690 ], 689 691 profileCurrent: 0, 690 - version: 64, 692 + version: 65, 691 693 global: { 692 694 database: { 693 695 prefixWildcardsSupported: false,
+1 -1
test/utilities/anki.js
··· 83 83 * @returns {Promise<import('anki').NoteFields[]>} 84 84 */ 85 85 export async function getTemplateRenderResults(dictionaryEntries, mode, template, expect, styles = '') { 86 - const ankiTemplateRenderer = new AnkiTemplateRenderer(); 86 + const ankiTemplateRenderer = new AnkiTemplateRenderer(document); 87 87 await ankiTemplateRenderer.prepare(); 88 88 const clozePrefix = 'cloze-prefix'; 89 89 const clozeSuffix = 'cloze-suffix';
+6
types/ext/api.d.ts
··· 352 352 params: void; 353 353 return: true; 354 354 }; 355 + testYomitanApi: { 356 + params: { 357 + url: string; 358 + }; 359 + return: true; 360 + }; 355 361 isTextLookupWorthy: { 356 362 params: { 357 363 text: string;
+1
types/ext/settings.d.ts
··· 151 151 sortFrequencyDictionary: string | null; 152 152 sortFrequencyDictionaryOrder: SortFrequencyDictionaryOrder; 153 153 stickySearchHeader: boolean; 154 + enableYomitanApi: boolean; 154 155 }; 155 156 156 157 export type PopupWindowOptions = {
+35
types/ext/yomitan-api.d.ts
··· 1 + /* 2 + * Copyright (C) 2025 Yomitan Authors 3 + * 4 + * This program is free software: you can redistribute it and/or modify 5 + * it under the terms of the GNU General Public License as published by 6 + * the Free Software Foundation, either version 3 of the License, or 7 + * (at your option) any later version. 8 + * 9 + * This program is distributed in the hope that it will be useful, 10 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 + * GNU General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 + */ 17 + 18 + export type termEntriesInput = { 19 + term: string; 20 + }; 21 + 22 + export type kanjiEntriesInput = { 23 + character: string; 24 + }; 25 + 26 + export type ankiFieldsInput = { 27 + text: string; 28 + type: 'term' | 'kanji'; 29 + markers: [string]; 30 + maxEntries: number; 31 + }; 32 + 33 + export type remoteVersionResponse = { 34 + version: number; 35 + };