aboutsummaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
authorStefanVukovic99 <stefanvukovic44@gmail.com>2024-06-20 19:27:02 +0200
committerGitHub <noreply@github.com>2024-06-20 17:27:02 +0000
commitd2c930a94d6e445053bcb5e5bb629851165425fc (patch)
tree94ff7034e7d3ab36ed663f353aeb5486bd294d1c /ext/js
parent1a866b3997310a04fc146b91eb47a59a3f049589 (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.js10
-rw-r--r--ext/js/data/anki-note-data-creator.js83
-rw-r--r--ext/js/data/options-util.js9
-rw-r--r--ext/js/dictionary/dictionary-importer.js34
-rw-r--r--ext/js/display/display-anki.js21
-rw-r--r--ext/js/display/display.js38
-rw-r--r--ext/js/pages/settings/dictionary-controller.js6
-rw-r--r--ext/js/pages/settings/dictionary-import-controller.js10
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]});