diff options
author | StefanVukovic99 <stefanvukovic44@gmail.com> | 2024-06-20 19:27:02 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-20 17:27:02 +0000 |
commit | d2c930a94d6e445053bcb5e5bb629851165425fc (patch) | |
tree | 94ff7034e7d3ab36ed663f353aeb5486bd294d1c /ext/js | |
parent | 1a866b3997310a04fc146b91eb47a59a3f049589 (diff) |
support css file in dictionaries (#1080)
* get styles in db
* get styles in settings
* use styles
* fix test
* scope
* fix comma separated
* escape dict name in css selector
* g regex
* get styles in anki
* fix tests
* more specificity
* whitespace
* test importing
* test handlebars
* add styles to glossary-first
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/data/anki-note-builder.js | 10 | ||||
-rw-r--r-- | ext/js/data/anki-note-data-creator.js | 83 | ||||
-rw-r--r-- | ext/js/data/options-util.js | 9 | ||||
-rw-r--r-- | ext/js/dictionary/dictionary-importer.js | 34 | ||||
-rw-r--r-- | ext/js/display/display-anki.js | 21 | ||||
-rw-r--r-- | ext/js/display/display.js | 38 | ||||
-rw-r--r-- | ext/js/pages/settings/dictionary-controller.js | 6 | ||||
-rw-r--r-- | ext/js/pages/settings/dictionary-import-controller.js | 10 |
8 files changed, 188 insertions, 23 deletions
diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index f7d4a12a..17bc1a5c 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -60,6 +60,7 @@ export class AnkiNoteBuilder { glossaryLayoutMode = 'default', compactTags = false, mediaOptions = null, + dictionaryStylesMap, }) { let duplicateScopeDeckName = null; let duplicateScopeCheckChildren = false; @@ -80,7 +81,7 @@ export class AnkiNoteBuilder { } } - const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media); + const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap); const formattedFieldValuePromises = []; for (const [, fieldValue] of fields) { const formattedFieldValuePromise = this._formatField(fieldValue, commonData, template); @@ -135,8 +136,9 @@ export class AnkiNoteBuilder { glossaryLayoutMode = 'default', compactTags = false, marker, + dictionaryStylesMap, }) { - const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, void 0); + const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, void 0, dictionaryStylesMap); return await this._templateRenderer.getModifiedData({marker, commonData}, 'ankiNote'); } @@ -181,9 +183,10 @@ export class AnkiNoteBuilder { * @param {import('settings').GlossaryLayoutMode} glossaryLayoutMode * @param {boolean} compactTags * @param {import('anki-templates').Media|undefined} media + * @param {Map<string, string>} dictionaryStylesMap * @returns {import('anki-note-builder').CommonData} */ - _createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media) { + _createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap) { return { dictionaryEntry, mode, @@ -192,6 +195,7 @@ export class AnkiNoteBuilder { glossaryLayoutMode, compactTags, media, + dictionaryStylesMap, }; } diff --git a/ext/js/data/anki-note-data-creator.js b/ext/js/data/anki-note-data-creator.js index 11618524..0bfd76cb 100644 --- a/ext/js/data/anki-note-data-creator.js +++ b/ext/js/data/anki-note-data-creator.js @@ -33,8 +33,9 @@ export function createAnkiNoteData(marker, { compactTags, context, media, + dictionaryStylesMap, }) { - const definition = createCachedValue(getDefinition.bind(null, dictionaryEntry, context, resultOutputMode)); + const definition = createCachedValue(getDefinition.bind(null, dictionaryEntry, context, resultOutputMode, dictionaryStylesMap)); const uniqueExpressions = createCachedValue(getUniqueExpressions.bind(null, dictionaryEntry)); const uniqueReadings = createCachedValue(getUniqueReadings.bind(null, dictionaryEntry)); const context2 = createCachedValue(getPublicContext.bind(null, context)); @@ -306,12 +307,13 @@ function getPitchCount(cachedPitches) { * @param {import('dictionary').DictionaryEntry} dictionaryEntry * @param {import('anki-templates-internal').Context} context * @param {import('settings').ResultOutputMode} resultOutputMode + * @param {Map<string, string>} dictionaryStylesMap * @returns {import('anki-templates').DictionaryEntry} */ -function getDefinition(dictionaryEntry, context, resultOutputMode) { +function getDefinition(dictionaryEntry, context, resultOutputMode, dictionaryStylesMap) { switch (dictionaryEntry.type) { case 'term': - return getTermDefinition(dictionaryEntry, context, resultOutputMode); + return getTermDefinition(dictionaryEntry, context, resultOutputMode, dictionaryStylesMap); case 'kanji': return getKanjiDefinition(dictionaryEntry, context); default: @@ -409,9 +411,10 @@ function getKanjiFrequencies(dictionaryEntry) { * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry * @param {import('anki-templates-internal').Context} context * @param {import('settings').ResultOutputMode} resultOutputMode + * @param {Map<string, string>} dictionaryStylesMap * @returns {import('anki-templates').TermDictionaryEntry} */ -function getTermDefinition(dictionaryEntry, context, resultOutputMode) { +function getTermDefinition(dictionaryEntry, context, resultOutputMode, dictionaryStylesMap) { /** @type {import('anki-templates').TermDictionaryEntryType} */ let type = 'term'; switch (resultOutputMode) { @@ -427,7 +430,7 @@ function getTermDefinition(dictionaryEntry, context, resultOutputMode) { const primarySource = getPrimarySource(dictionaryEntry); const dictionaryNames = createCachedValue(getTermDictionaryNames.bind(null, dictionaryEntry)); - const commonInfo = createCachedValue(getTermDictionaryEntryCommonInfo.bind(null, dictionaryEntry, type)); + const commonInfo = createCachedValue(getTermDictionaryEntryCommonInfo.bind(null, dictionaryEntry, type, dictionaryStylesMap)); const termTags = createCachedValue(getTermTags.bind(null, dictionaryEntry, type)); const expressions = createCachedValue(getTermExpressions.bind(null, dictionaryEntry)); const frequencies = createCachedValue(getTermFrequencies.bind(null, dictionaryEntry)); @@ -436,6 +439,7 @@ function getTermDefinition(dictionaryEntry, context, resultOutputMode) { const pitches = createCachedValue(getTermPitches.bind(null, dictionaryEntry)); const phoneticTranscriptions = createCachedValue(getTermPhoneticTranscriptions.bind(null, dictionaryEntry)); const glossary = createCachedValue(getTermGlossaryArray.bind(null, dictionaryEntry, type)); + const styleInfo = createCachedValue(getTermStyles.bind(null, dictionaryEntry, type, dictionaryStylesMap)); const cloze = createCachedValue(getCloze.bind(null, dictionaryEntry, context)); const furiganaSegments = createCachedValue(getTermFuriganaSegments.bind(null, dictionaryEntry, type)); const sequence = createCachedValue(getTermDictionaryEntrySequence.bind(null, dictionaryEntry)); @@ -466,6 +470,8 @@ function getTermDefinition(dictionaryEntry, context, resultOutputMode) { }, get expressions() { return getCachedValue(expressions); }, get glossary() { return getCachedValue(glossary); }, + get glossaryScopedStyles() { return getCachedValue(styleInfo)?.glossaryScopedStyles; }, + get dictScopedStyles() { return getCachedValue(styleInfo)?.dictScopedStyles; }, get definitionTags() { return type === 'term' ? getCachedValue(commonInfo).definitionTags : void 0; }, get termTags() { return getCachedValue(termTags); }, get definitions() { return getCachedValue(commonInfo).definitions; }, @@ -496,9 +502,10 @@ function getTermDictionaryNames(dictionaryEntry) { /** * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry * @param {import('anki-templates').TermDictionaryEntryType} type + * @param {Map<string, string>} dictionaryStylesMap * @returns {import('anki-templates').TermDictionaryEntryCommonInfo} */ -function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { +function getTermDictionaryEntryCommonInfo(dictionaryEntry, type, dictionaryStylesMap) { const merged = (type === 'termMerged'); const hasDefinitions = (type !== 'term'); @@ -518,6 +525,13 @@ function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { /** @type {import('anki-templates').Tag[]} */ const definitionTags = []; for (const {tags, headwordIndices, entries, dictionary, sequences} of dictionaryEntry.definitions) { + const dictionaryStyles = dictionaryStylesMap.get(dictionary); + let glossaryScopedStyles = ''; + let dictScopedStyles = ''; + if (dictionaryStyles) { + glossaryScopedStyles = addGlossaryScopeToCss(dictionaryStyles); + dictScopedStyles = addGlossaryScopeToCss(addDictionaryScopeToCss(dictionaryStyles, dictionary)); + } const definitionTags2 = []; for (const tag of tags) { definitionTags.push(convertTag(tag)); @@ -528,6 +542,8 @@ function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { definitions.push({ sequence: sequences[0], dictionary, + glossaryScopedStyles, + dictScopedStyles, glossary: entries, definitionTags: definitionTags2, only, @@ -543,6 +559,39 @@ function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { } /** + * @param {string} css + * @returns {string} + */ +function addGlossaryScopeToCss(css) { + return addScopeToCss(css, '.yomitan-glossary'); +} + +/** + * @param {string} css + * @param {string} dictionaryTitle + * @returns {string} + */ +function addDictionaryScopeToCss(css, dictionaryTitle) { + const escapedTitle = dictionaryTitle + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"'); + + return addScopeToCss(css, `[data-dictionary="${escapedTitle}"]`); +} + +/** + * @param {string} css + * @param {string} scopeSelector + * @returns {string} + */ +function addScopeToCss(css, scopeSelector) { + const regex = /([^\r\n,{}]+)(\s*[,{])/g; + const replacement = `${scopeSelector} $1$2`; + return css.replace(regex, replacement); +} + + +/** * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry * @returns {import('anki-templates').TermFrequency[]} */ @@ -770,6 +819,28 @@ function getTermGlossaryArray(dictionaryEntry, type) { /** * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry * @param {import('anki-templates').TermDictionaryEntryType} type + * @param {Map<string, string>} dictionaryStylesMap + * @returns {{glossaryScopedStyles: string, dictScopedStyles: string}|undefined} + */ +function getTermStyles(dictionaryEntry, type, dictionaryStylesMap) { + if (type !== 'term') { + return void 0; + } + let glossaryScopedStyles = ''; + let dictScopedStyles = ''; + for (const {dictionary} of dictionaryEntry.definitions) { + const dictionaryStyles = dictionaryStylesMap.get(dictionary); + if (dictionaryStyles) { + glossaryScopedStyles += addGlossaryScopeToCss(dictionaryStyles); + dictScopedStyles += addGlossaryScopeToCss(addDictionaryScopeToCss(dictionaryStyles, dictionary)); + } + } + return {glossaryScopedStyles, dictScopedStyles}; +} + +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates').TermDictionaryEntryType} type * @returns {import('anki-templates').Tag[]|undefined} */ function getTermTags(dictionaryEntry, type) { diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 64b2e3bc..8af299d8 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -548,6 +548,7 @@ export class OptionsUtil { this._updateVersion38, this._updateVersion39, this._updateVersion40, + this._updateVersion41, ]; /* eslint-enable @typescript-eslint/unbound-method */ if (typeof targetVersion === 'number' && targetVersion < result.length) { @@ -1340,6 +1341,14 @@ export class OptionsUtil { } /** + * - Updated `glossary` handlebars to support dictionary css. + * @type {import('options-util').UpdateFunction} + */ + async _updateVersion41(options) { + await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v41.handlebars'); + } + + /** * @param {string} url * @returns {Promise<chrome.tabs.Tab>} */ diff --git a/ext/js/dictionary/dictionary-importer.js b/ext/js/dictionary/dictionary-importer.js index 62453a13..d558636e 100644 --- a/ext/js/dictionary/dictionary-importer.js +++ b/ext/js/dictionary/dictionary-importer.js @@ -189,7 +189,25 @@ export class DictionaryImporter { tagMeta: {total: tagList.length}, media: {total: media.length}, }; - const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported, counts}); + + const stylesFileName = 'styles.css'; + const stylesFile = fileMap.get(stylesFileName); + let styles = ''; + if (typeof stylesFile !== 'undefined') { + styles = await this._getData(stylesFile, new TextWriter()); + const cssErrors = this._validateCss(styles); + if (cssErrors.length > 0) { + return { + errors: cssErrors, + result: null, + }; + } + } + + /** @type {import('dictionary-importer').SummaryDetails} */ + const summaryDetails = {prefixWildcardsSupported, counts, styles}; + + const summary = this._createSummary(dictionaryTitle, version, index, summaryDetails); await dictionaryDatabase.bulkAdd('dictionaries', [summary], 0, 1); // Add data @@ -267,13 +285,12 @@ export class DictionaryImporter { * @param {string} dictionaryTitle * @param {number} version * @param {import('dictionary-data').Index} index - * @param {{prefixWildcardsSupported: boolean, counts: import('dictionary-importer').SummaryCounts}} details + * @param {import('dictionary-importer').SummaryDetails} details * @returns {import('dictionary-importer').Summary} */ _createSummary(dictionaryTitle, version, index, details) { const indexSequenced = index.sequenced; - const {prefixWildcardsSupported, counts} = details; - + const {prefixWildcardsSupported, counts, styles} = details; /** @type {import('dictionary-importer').Summary} */ const summary = { title: dictionaryTitle, @@ -283,6 +300,7 @@ export class DictionaryImporter { importDate: Date.now(), prefixWildcardsSupported, counts, + styles, }; const {author, url, description, attribution, frequencyMode, sourceLanguage, targetLanguage} = index; @@ -332,6 +350,14 @@ export class DictionaryImporter { } /** + * @param {string} css + * @returns {Error[]} + */ + _validateCss(css) { + return css ? [] : [new Error('No styles found')]; + } + + /** * @param {import('dictionary-data').TermGlossaryText|import('dictionary-data').TermGlossaryImage|import('dictionary-data').TermGlossaryStructuredContent} data * @param {import('dictionary-database').DatabaseTermEntry} entry * @param {import('dictionary-importer').ImportRequirement[]} requirements diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index fa82a7b6..68a6654c 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -91,6 +91,8 @@ export class DisplayAnki { this._noteTags = []; /** @type {Map<import('display-anki').CreateMode, import('settings').AnkiNoteOptions>} */ this._modeOptions = new Map(); + /** @type {import('settings').DictionariesOptions} */ + this._dictionaries = []; /** @type {Map<import('dictionary').DictionaryEntryType, import('display-anki').CreateMode[]>} */ this._dictionaryEntryTypeModeMap = new Map([ ['kanji', ['kanji']], @@ -147,6 +149,7 @@ export class DisplayAnki { glossaryLayoutMode: this._glossaryLayoutMode, compactTags: this._compactTags, marker: 'test', + dictionaryStylesMap: this._getDictionaryStylesMap(), }); } catch (e) { ankiNoteDataException = e; @@ -191,6 +194,7 @@ export class DisplayAnki { _onOptionsUpdated({options}) { const { general: {resultOutputMode, glossaryLayoutMode, compactTags}, + dictionaries, anki: { tags, duplicateScope, @@ -227,6 +231,7 @@ export class DisplayAnki { this._modeOptions.set('kanji', kanji); this._modeOptions.set('term-kanji', terms); this._modeOptions.set('term-kana', terms); + this._dictionaries = dictionaries; void this._updateAnkiFieldTemplates(options); } @@ -808,6 +813,7 @@ export class DisplayAnki { const details = this._ankiNoteBuilder.getDictionaryEntryDetailsForNote(dictionaryEntry); const audioDetails = this._getAnkiNoteMediaAudioDetails(details); const optionsContext = this._display.getOptionsContext(); + const dictionaryStylesMap = this._getDictionaryStylesMap(); const {note, errors, requirements: outputRequirements} = await this._ankiNoteBuilder.createNote({ dictionaryEntry, @@ -836,11 +842,26 @@ export class DisplayAnki { }, }, requirements, + dictionaryStylesMap, }); return {note, errors, requirements: outputRequirements}; } /** + * @returns {Map<string, string>} + */ + _getDictionaryStylesMap() { + const styleMap = new Map(); + for (const dictionary of this._dictionaries) { + const {name, styles} = dictionary; + if (typeof styles === 'string') { + styleMap.set(name, styles); + } + } + return styleMap; + } + + /** * @param {boolean} isTerms * @returns {import('display-anki').CreateMode[]} */ diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 6b3838e5..ebd11e0a 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -1152,7 +1152,7 @@ export class Display extends EventDispatcher { */ _setTheme(options) { const {general} = options; - const {popupTheme} = general; + const {popupTheme, popupOuterTheme} = general; /** @type {string} */ let pageType = this._pageType; try { @@ -1169,10 +1169,42 @@ export class Display extends EventDispatcher { log.error(e); } this._themeController.theme = popupTheme; - this._themeController.outerTheme = general.popupOuterTheme; + this._themeController.outerTheme = popupOuterTheme; this._themeController.siteOverride = pageType === 'search' || pageType === 'popupPreview'; this._themeController.updateTheme(); - this.setCustomCss(general.customPopupCss); + const customCss = this._getCustomCss(options); + this.setCustomCss(customCss); + } + + /** + * @param {import('settings').ProfileOptions} options + * @returns {string} + */ + _getCustomCss(options) { + const {general: {customPopupCss}, dictionaries} = options; + let customCss = customPopupCss; + for (const {name, enabled, styles = ''} of dictionaries) { + if (enabled) { + customCss += '\n' + this._addScopeToCss(styles, name); + } + } + this.setCustomCss(customCss); + return customCss; + } + + /** + * @param {string} css + * @param {string} dictionaryTitle + * @returns {string} + */ + _addScopeToCss(css, dictionaryTitle) { + const escapedTitle = dictionaryTitle + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"'); + + const regex = /([^\r\n,{}]+)(\s*[,{])/g; + const replacement = `[data-dictionary="${escapedTitle}"] $1$2`; + return css.replace(regex, replacement); } /** diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index e7a9444f..5c0e49d4 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -519,9 +519,10 @@ export class DictionaryController { /** * @param {string} name * @param {boolean} enabled + * @param {string} styles * @returns {import('settings').DictionaryOptions} */ - static createDefaultDictionarySettings(name, enabled) { + static createDefaultDictionarySettings(name, enabled, styles) { return { name, priority: 0, @@ -530,6 +531,7 @@ export class DictionaryController { definitionsCollapsible: 'not-collapsible', partsOfSpeechFilter: true, useDeinflections: true, + styles: styles ?? '', }; } @@ -572,7 +574,7 @@ export class DictionaryController { } for (const name of missingDictionaries) { - const value = DictionaryController.createDefaultDictionarySettings(name, newDictionariesEnabled); + const value = DictionaryController.createDefaultDictionarySettings(name, newDictionariesEnabled, ''); dictionaryOptionsArray.push(value); modified = true; } diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index c721a9dd..d7bb5d30 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -402,7 +402,7 @@ export class DictionaryImportController { return errors; } - const errors2 = await this._addDictionarySettings(result.sequenced, result.title); + const errors2 = await this._addDictionarySettings(result); await this._settingsController.application.api.triggerDatabaseUpdated('dictionary', 'import'); @@ -415,11 +415,11 @@ export class DictionaryImportController { } /** - * @param {boolean} sequenced - * @param {string} title + * @param {import('dictionary-importer').Summary} summary * @returns {Promise<Error[]>} */ - async _addDictionarySettings(sequenced, title) { + async _addDictionarySettings(summary) { + const {title, sequenced, styles} = summary; let optionsFull; // Workaround Firefox bug sometimes causing getOptionsFull to fail for (let i = 0, success = false; (i < 10) && (success === false); i++) { @@ -439,7 +439,7 @@ export class DictionaryImportController { for (let i = 0; i < profileCount; ++i) { const {options} = optionsFull.profiles[i]; const enabled = profileIndex === i; - const value = DictionaryController.createDefaultDictionarySettings(title, enabled); + const value = DictionaryController.createDefaultDictionarySettings(title, enabled, styles); const path1 = `profiles[${i}].options.dictionaries`; targets.push({action: 'push', path: path1, items: [value]}); |