From d2c930a94d6e445053bcb5e5bb629851165425fc Mon Sep 17 00:00:00 2001
From: StefanVukovic99 <stefanvukovic44@gmail.com>
Date: Thu, 20 Jun 2024 19:27:02 +0200
Subject: 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
---
 ext/js/data/anki-note-builder.js      | 10 +++--
 ext/js/data/anki-note-data-creator.js | 83 ++++++++++++++++++++++++++++++++---
 ext/js/data/options-util.js           |  9 ++++
 3 files changed, 93 insertions(+), 9 deletions(-)

(limited to 'ext/js/data')

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,
@@ -542,6 +558,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[]}
@@ -767,6 +816,28 @@ function getTermGlossaryArray(dictionaryEntry, type) {
     return void 0;
 }
 
+/**
+ * @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
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) {
@@ -1339,6 +1340,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>}
-- 
cgit v1.2.3