Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at upstream/master 223 lines 8.7 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2021-2022 Yomichan Authors 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19import css from 'css'; 20import fs from 'fs'; 21import path from 'path'; 22import {fileURLToPath} from 'url'; 23 24const dirname = path.dirname(fileURLToPath(import.meta.url)); 25 26/** 27 * @returns {{cssFilePath: string, overridesCssFilePath: string, outputPath: string}[]} 28 */ 29export function getTargets() { 30 return [ 31 { 32 cssFilePath: path.join(dirname, '..', 'ext/css/structured-content.css'), 33 overridesCssFilePath: path.join(dirname, 'data/structured-content-overrides.css'), 34 outputPath: path.join(dirname, '..', 'ext/data/structured-content-style.json'), 35 }, 36 { 37 cssFilePath: path.join(dirname, '..', 'ext/css/display-pronunciation.css'), 38 overridesCssFilePath: path.join(dirname, 'data/display-pronunciation-overrides.css'), 39 outputPath: path.join(dirname, '..', 'ext/data/pronunciation-style.json'), 40 }, 41 ]; 42} 43 44/** 45 * @param {import('css-style-applier').RawStyleData} rules 46 * @param {string[]} selectors 47 * @returns {number} 48 */ 49function indexOfRule(rules, selectors) { 50 const jj = selectors.length; 51 for (let i = 0, ii = rules.length; i < ii; ++i) { 52 const ruleSelectors = rules[i].selectors; 53 if (ruleSelectors.length !== jj) { continue; } 54 let okay = true; 55 for (let j = 0; j < jj; ++j) { 56 if (selectors[j] !== ruleSelectors[j]) { 57 okay = false; 58 break; 59 } 60 } 61 if (okay) { return i; } 62 } 63 return -1; 64} 65 66/** 67 * @param {import('css-style-applier').RawStyleDataStyleArray} styles 68 * @param {string} property 69 * @param {Map<string, number>} removedProperties 70 * @returns {number} 71 */ 72function removeProperty(styles, property, removedProperties) { 73 let removeCount = removedProperties.get(property); 74 if (typeof removeCount !== 'undefined') { return removeCount; } 75 removeCount = 0; 76 for (let i = 0, ii = styles.length; i < ii; ++i) { 77 const key = styles[i][0]; 78 if (key !== property) { continue; } 79 styles.splice(i, 1); 80 --i; 81 --ii; 82 ++removeCount; 83 } 84 removedProperties.set(property, removeCount); 85 return removeCount; 86} 87 88/** 89 * Manually formats JSON for easier CSS parseability. 90 * @param {import('css-style-applier').RawStyleData} rules CSS ruleset. 91 * @returns {string} 92 */ 93export function formatRulesJson(rules) { 94 // This is similar to the following code, but formatted a but more succinctly: 95 // return JSON.stringify(rules, null, 4); 96 const indent1 = ' '; 97 const indent2 = indent1.repeat(2); 98 const indent3 = indent1.repeat(3); 99 let result = ''; 100 result += '['; 101 let ruleIndex = 0; 102 for (const {selectors, styles} of rules) { 103 if (ruleIndex > 0) { result += ','; } 104 result += `\n${indent1}{\n${indent2}"selectors": `; 105 result += ( 106 selectors.length === 1 ? 107 `[${JSON.stringify(selectors[0], null, 4)}]` : 108 JSON.stringify(selectors, null, 4).replace(/\n/g, '\n' + indent2) 109 ); 110 result += `,\n${indent2}"styles": [`; 111 let styleIndex = 0; 112 for (const [key, value] of styles) { 113 if (styleIndex > 0) { result += ','; } 114 result += `\n${indent3}[${JSON.stringify(key)}, ${JSON.stringify(value)}]`; 115 ++styleIndex; 116 } 117 if (styleIndex > 0) { result += `\n${indent2}`; } 118 result += `]\n${indent1}}`; 119 ++ruleIndex; 120 } 121 if (ruleIndex > 0) { result += '\n'; } 122 result += ']'; 123 result += '\n'; 124 return result; 125} 126 127/** 128 * Generates a CSS ruleset. 129 * @param {string} cssFilePath 130 * @param {string} overridesCssFilePath 131 * @returns {import('css-style-applier').RawStyleData} 132 * @throws {Error} 133 */ 134export function generateRules(cssFilePath, overridesCssFilePath) { 135 const cssFileContent = fs.readFileSync(cssFilePath, {encoding: 'utf8'}); 136 const overridesCssFileContent = fs.readFileSync(overridesCssFilePath, {encoding: 'utf8'}); 137 const defaultStylesheet = /** @type {css.StyleRules} */ (css.parse(cssFileContent, {}).stylesheet); 138 const overridesStylesheet = /** @type {css.StyleRules} */ (css.parse(overridesCssFileContent, {}).stylesheet); 139 140 const removePropertyPattern = /^remove-property\s+([\w\W]+)$/; 141 const removeRulePattern = /^remove-rule$/; 142 const propertySeparator = /\s+/; 143 144 /** @type {import('css-style-applier').RawStyleData} */ 145 const rules = []; 146 147 for (const rule of defaultStylesheet.rules) { 148 if (rule.type !== 'rule') { continue; } 149 const {selectors, declarations} = /** @type {css.Rule} */ (rule); 150 if (typeof selectors === 'undefined') { continue; } 151 /** @type {import('css-style-applier').RawStyleDataStyleArray} */ 152 const styles = []; 153 if (typeof declarations !== 'undefined') { 154 for (const declaration of declarations) { 155 if (declaration.type !== 'declaration') { 156 console.log(declaration); 157 continue; 158 } 159 const {property, value} = /** @type {css.Declaration} */ (declaration); 160 if (typeof property !== 'string' || typeof value !== 'string') { continue; } 161 styles.push([property, value]); 162 } 163 } 164 if (styles.length > 0) { 165 rules.push({selectors, styles}); 166 } 167 } 168 169 for (const rule of overridesStylesheet.rules) { 170 if (rule.type !== 'rule') { continue; } 171 const {selectors, declarations} = /** @type {css.Rule} */ (rule); 172 if (typeof selectors === 'undefined' || typeof declarations === 'undefined') { continue; } 173 /** @type {Map<string, number>} */ 174 const removedProperties = new Map(); 175 for (const declaration of declarations) { 176 switch (declaration.type) { 177 case 'declaration': 178 { 179 const index = indexOfRule(rules, selectors); 180 let entry; 181 if (index >= 0) { 182 entry = rules[index]; 183 } else { 184 entry = {selectors, styles: []}; 185 rules.push(entry); 186 } 187 const {property, value} = /** @type {css.Declaration} */ (declaration); 188 if (typeof property === 'string' && typeof value === 'string') { 189 removeProperty(entry.styles, property, removedProperties); 190 entry.styles.push([property, value]); 191 } 192 } 193 break; 194 case 'comment': 195 { 196 const index = indexOfRule(rules, selectors); 197 if (index < 0) { throw new Error('Could not find rule with matching selectors'); } 198 const comment = (/** @type {css.Comment} */ (declaration).comment || '').trim(); 199 let m; 200 if ((m = removePropertyPattern.exec(comment)) !== null) { 201 for (const property of m[1].split(propertySeparator)) { 202 const removeCount = removeProperty(rules[index].styles, property, removedProperties); 203 if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); } 204 } 205 } else if (removeRulePattern.test(comment)) { 206 rules.splice(index, 1); 207 } 208 } 209 break; 210 } 211 } 212 } 213 214 // Remove empty 215 for (let i = 0, ii = rules.length; i < ii; ++i) { 216 if (rules[i].styles.length > 0) { continue; } 217 rules.splice(i, 1); 218 --i; 219 --ii; 220 } 221 222 return rules; 223}