diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index c337d99164..4c3eeeb2da 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -21,7 +21,7 @@ jobs: run: npm ci - name: Build Legal run: npm run license-report:html - - uses: lycheeverse/lychee-action@v1.10.0 + - uses: lycheeverse/lychee-action@v2.0.0 with: fail: true jobSummary: false diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3c82761f62..aab0563064 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -51,7 +51,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3.26.10 + uses: github/codeql-action/init@v3.26.12 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3.26.10 + uses: github/codeql-action/autobuild@v3.26.12 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -78,6 +78,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.26.10 + uses: github/codeql-action/analyze@v3.26.12 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 28f4f89f4f..22cb6238b8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@cf5b0a9041d3c1d336516f1944c96d96598193cc # v2.22.12 + uses: github/codeql-action/upload-sarif@572cc5268d94f11b89e12e7a166cf93275856072 # v2.22.12 with: sarif_file: results.sarif diff --git a/README.md b/README.md index 931b73127d..056d405186 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Get Yomitan for Edge](https://img.shields.io/badge/dynamic/json?logo=puzzle&label=get%20yomitan%20for%20edge&style=for-the-badge&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fidelnfbbmikgfiejhgmddlbkfgiifnnn)](https://microsoftedge.microsoft.com/addons/detail/yomitan/idelnfbbmikgfiejhgmddlbkfgiifnnn) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/yomidevs/yomitan/badge?style=for-the-badge)](https://securityscorecards.dev/viewer/?uri=github.com/yomidevs/yomitan) -General: [![Discord](https://dcbadge.vercel.app/api/server/YkQrXW6TXF?style=for-the-badge)](https://discord.gg/YkQrXW6TXF) Japanese: [![Discord](https://dcbadge.vercel.app/api/server/UGNPMDE7zC?style=for-the-badge)](https://discord.gg/UGNPMDE7zC) +[![Discord](https://dcbadge.vercel.app/api/server/YkQrXW6TXF?style=for-the-badge)](https://discord.gg/YkQrXW6TXF) # Visit [yomitan.wiki](https://yomitan.wiki) to learn more! diff --git a/ext/css/search.css b/ext/css/search.css index 81e4a74c78..7327b89ac1 100644 --- a/ext/css/search.css +++ b/ext/css/search.css @@ -135,6 +135,48 @@ h1 { --icon-size: 16px 16px; } +.clear-button { + flex: 0 0 auto; + position: relative; + width: 2.5em; + height: var(--search-textbox-height); + min-height: var(--search-textbox-min-height); + max-height: var(--search-textbox-max-height); + background-color: var(--input-background-color); + border: 0; + padding: 0; + margin: 0; + cursor: pointer; + outline: none; + transition: background-color var(--animation-duration) ease-in-out; + border-radius: 0; +} +.clear-button:hover, +.clear-button:focus { + background-color: var(--input-background-color-dark); +} +.clear-button:focus:not(:focus-visible):not(:hover) { + background-color: var(--input-background-color); +} +.clear-button:focus-visible { + background-color: var(--input-background-color-dark); +} +.clear-button:active, +.clear-button:active:focus { + background-color: var(--input-background-color-darker); +} + +.clear-button>.icon { + display: block; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: var(--button-default-icon-color); + --icon-size: 16px 16px; +} + /* Search options */ #search-settings-button>.icon { display: block; diff --git a/ext/js/dictionary/dictionary-importer.js b/ext/js/dictionary/dictionary-importer.js index 13fbb7078c..e8a72ba821 100644 --- a/ext/js/dictionary/dictionary-importer.js +++ b/ext/js/dictionary/dictionary-importer.js @@ -373,10 +373,9 @@ export class DictionaryImporter { * @returns {ExtensionError} */ _formatAjvSchemaError(schema, fileName) { - const e2 = new ExtensionError(`Dictionary has invalid data in '${fileName}'`); - e2.data = schema.errors; - - return e2; + const e = new ExtensionError(`Dictionary has invalid data in '${fileName}' '${JSON.stringify(schema.errors)}'`); + e.data = schema.errors; + return e; } /** diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 2f90189d90..3f6d893a8d 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -784,11 +784,14 @@ export class Display extends EventDispatcher { async _onStateChanged() { if (this._historyChangeIgnore) { return; } + performance.mark('display:onStateChanged:start'); + /** @type {?import('core').TokenObject} */ const token = {}; // Unique identifier token this._setContentToken = token; try { // Clear + performance.mark('display:clear:start'); this._closePopups(); this._closeAllPopupMenus(); this._eventListeners.removeAllEventListeners(); @@ -799,8 +802,11 @@ export class Display extends EventDispatcher { this._dictionaryEntries = []; this._dictionaryEntryNodes = []; this._elementOverflowController.clearElements(); + performance.mark('display:clear:end'); + performance.measure('display:clear', 'display:clear:start', 'display:clear:end'); // Prepare + performance.mark('display:prepare:start'); const urlSearchParams = new URLSearchParams(location.search); let type = urlSearchParams.get('type'); if (type === null && urlSearchParams.get('query') !== null) { type = 'terms'; } @@ -809,7 +815,10 @@ export class Display extends EventDispatcher { this._queryParserVisibleOverride = (fullVisible === null ? null : (fullVisible !== 'false')); this._historyHasChanged = true; + performance.mark('display:prepare:end'); + performance.measure('display:prepare', 'display:prepare:start', 'display:prepare:end'); + performance.mark('display:setContent:start'); // Set content switch (type) { case 'terms': @@ -826,9 +835,13 @@ export class Display extends EventDispatcher { this._clearContent(); break; } + performance.mark('display:setContent:end'); + performance.measure('display:setContent', 'display:setContent:start', 'display:setContent:end'); } catch (e) { this.onError(toError(e)); } + performance.mark('display:onStateChanged:end'); + performance.measure('display:onStateChanged', 'display:onStateChanged:start', 'display:onStateChanged:end'); } /** @@ -1309,6 +1322,7 @@ export class Display extends EventDispatcher { const hasEnabledDictionaries = this._options ? this._options.dictionaries.some(({enabled}) => enabled) : false; // Set query + performance.mark('display:setQuery:start'); let query = urlSearchParams.get('query'); if (query === null) { query = ''; } let queryFull = urlSearchParams.get('full'); @@ -1320,6 +1334,8 @@ export class Display extends EventDispatcher { queryOffset = Number.isFinite(queryOffset) ? Math.max(0, Math.min(queryFull.length - query.length, queryOffset)) : 0; } this._setQuery(query, queryFull, queryOffset); + performance.mark('display:setQuery:end'); + performance.measure('display:setQuery', 'display:setQuery:start', 'display:setQuery:end'); let {state, content} = this._history; let changeHistory = false; @@ -1384,9 +1400,12 @@ export class Display extends EventDispatcher { const container = this._container; container.textContent = ''; + performance.mark('display:contentUpdate:start'); this._triggerContentUpdateStart(); for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) { + performance.mark('display:createEntry:start'); + if (i > 0) { await promiseTimeout(1); if (this._setContentToken !== token) { return; } @@ -1408,6 +1427,9 @@ export class Display extends EventDispatcher { } this._elementOverflowController.addElements(entry); + + performance.mark('display:createEntry:end'); + performance.measure('display:createEntry', 'display:createEntry:start', 'display:createEntry:end'); } if (typeof scrollX === 'number' || typeof scrollY === 'number') { @@ -1419,6 +1441,8 @@ export class Display extends EventDispatcher { } this._triggerContentUpdateComplete(); + performance.mark('display:contentUpdate:end'); + performance.measure('display:contentUpdate', 'display:contentUpdate:start', 'display:contentUpdate:end'); } /** */ diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index 925f9e0533..50175cb9e6 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -38,6 +38,8 @@ export class SearchDisplayController { /** @type {HTMLButtonElement} */ this._searchButton = querySelectorNotNull(document, '#search-button'); /** @type {HTMLButtonElement} */ + this._clearButton = querySelectorNotNull(document, '#clear-button'); + /** @type {HTMLButtonElement} */ this._searchBackButton = querySelectorNotNull(document, '#search-back-button'); /** @type {HTMLTextAreaElement} */ this._queryInput = querySelectorNotNull(document, '#search-textbox'); @@ -104,6 +106,8 @@ export class SearchDisplayController { this._display.setHistorySettings({useBrowserHistory: true}); this._searchButton.addEventListener('click', this._onSearch.bind(this), false); + this._clearButton.addEventListener('click', this._onClear.bind(this), false); + this._searchBackButton.addEventListener('click', this._onSearchBackButtonClick.bind(this), false); this._wanakanaEnableCheckbox.addEventListener('change', this._onWanakanaEnableChange.bind(this)); window.addEventListener('copy', this._onCopy.bind(this)); @@ -268,6 +272,15 @@ export class SearchDisplayController { this._search(true, 'new', true, null); } + /** + * @param {MouseEvent} e + */ + _onClear(e) { + e.preventDefault(); + this._queryInput.value = ''; + this._queryInput.focus(); + } + /** */ _onSearchBackButtonClick() { this._display.history.back(); @@ -617,7 +630,7 @@ export class SearchDisplayController { */ _updateSearchHeight(shrink) { const searchTextbox = this._queryInput; - const searchItems = [this._queryInput, this._searchButton, this._searchBackButton]; + const searchItems = [this._queryInput, this._searchButton, this._searchBackButton, this._clearButton]; if (shrink) { for (const searchButton of searchItems) { diff --git a/ext/js/language/ja/japanese-transforms.js b/ext/js/language/ja/japanese-transforms.js index 69cabe82b1..6288822639 100644 --- a/ext/js/language/ja/japanese-transforms.js +++ b/ext/js/language/ja/japanese-transforms.js @@ -25,7 +25,7 @@ const passiveEnglishDescription = '1. Indicates an action received from an actio '2. Expresses respect for the subject of action performer.\n'; const ikuVerbs = ['いく', '行く', '逝く', '往く']; -const godanUSpecialVerbs = ['こう', 'とう', '請う', '乞う', '問う', '訪う', '宣う', '曰う', '給う', '賜う', '揺蕩う']; +const godanUSpecialVerbs = ['こう', 'とう', '請う', '乞う', '恋う', '問う', '訪う', '宣う', '曰う', '給う', '賜う', '揺蕩う']; const fuVerbTeConjugations = [ ['のたまう', 'のたもう'], ['たまう', 'たもう'], diff --git a/ext/js/language/zh/chinese.js b/ext/js/language/zh/chinese.js index 8c5dd206fe..ca89de1ec9 100644 --- a/ext/js/language/zh/chinese.js +++ b/ext/js/language/zh/chinese.js @@ -71,5 +71,5 @@ export function isCodePointChinese(codePoint) { /** @type {import('language').ReadingNormalizer} */ export function normalizePinyin(str) { - return str.normalize('NFC').toLowerCase().replace(/[\s・:]|\/\//g, ''); + return str.normalize('NFC').toLowerCase().replace(/[\s・:'’-]|\/\//g, ''); } diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index 15bc4b908f..f2e0623550 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -208,6 +208,10 @@ export class DictionaryImportController { _renderRecommendedDictionaryGroup(recommendedDictionaries, dictionariesList, installedDictionaryNames, installedDictionaryDownloadUrls) { const dictionariesListParent = dictionariesList.parentElement; dictionariesList.innerHTML = ''; + // Hide section if no dictionaries are available + if (dictionariesListParent) { + dictionariesListParent.hidden = recommendedDictionaries.length === 0; + } for (const dictionary of recommendedDictionaries) { if (dictionariesList) { if (dictionariesListParent) { diff --git a/ext/js/templates/anki-template-renderer.js b/ext/js/templates/anki-template-renderer.js index d526a1d13e..f359f858e3 100644 --- a/ext/js/templates/anki-template-renderer.js +++ b/ext/js/templates/anki-template-renderer.js @@ -285,7 +285,11 @@ export class AnkiTemplateRenderer { */ _mergeTags(args) { const [object, isGroupMode, isMergeMode] = /** @type {[object: import('anki-templates').TermDictionaryEntry, isGroupMode: boolean, isMergeMode: boolean]} */ (args); + /** @type {import('anki-templates').Tag[][]} */ const tagSources = []; + if (Array.isArray(object.termTags)) { + tagSources.push(object.termTags); + } if (isGroupMode || isMergeMode) { const {definitions} = object; if (Array.isArray(definitions)) { @@ -294,12 +298,13 @@ export class AnkiTemplateRenderer { } } } else { - tagSources.push(object.definitionTags); + if (Array.isArray(object.definitionTags)) { + tagSources.push(object.definitionTags); + } } const tags = new Set(); for (const tagSource of tagSources) { - if (!Array.isArray(tagSource)) { continue; } for (const tag of tagSource) { tags.add(tag.name); } diff --git a/ext/search.html b/ext/search.html index 958b8a2858..7e9be81e20 100644 --- a/ext/search.html +++ b/ext/search.html @@ -55,6 +55,7 @@