|
| 1 | +const { interpolateName } = require('loader-utils'); |
| 2 | + |
1 | 3 | /**
|
2 |
| - * @author denisx <github.com@denisx.com> |
| 4 | + * Заменяем css-классы на 64-битный префикс по номеру позиции в файле + хеш от пути файла |
3 | 5 | */
|
4 | 6 |
|
5 |
| -const loaderUtils = require('loader-utils'); |
| 7 | +// Парсим кодирующую строку, или берем дефолтные значения |
| 8 | +const getRule = (externalRule) => { |
| 9 | + let iRule = { |
| 10 | + type: 'hash', |
| 11 | + rule: 'base64', |
| 12 | + hashLen: 8, |
| 13 | + val: '', |
| 14 | + }; |
| 15 | + |
| 16 | + iRule.val = `[${iRule.type}:${iRule.rule}:${iRule.hashLen}]`; |
| 17 | + |
| 18 | + const matchHashRule = |
| 19 | + externalRule |
| 20 | + .replace(/_/g, '') |
| 21 | + .match(/^(?:\[local])*\[([a-z\d]+):([a-z\d]+):(\d+)]$/) || []; |
| 22 | + |
| 23 | + if (matchHashRule.length >= 4) { |
| 24 | + const [, type, rule, hashLen] = matchHashRule; |
| 25 | + |
| 26 | + iRule = { |
| 27 | + type, |
| 28 | + rule, |
| 29 | + hashLen, |
| 30 | + val: `[${type}:${rule}:${hashLen}]`, |
| 31 | + }; |
| 32 | + } |
| 33 | + |
| 34 | + return iRule; |
| 35 | +}; |
6 | 36 |
|
7 |
| -export default class OneLetterCss { |
| 37 | +export default class OneLetterCssClasses { |
8 | 38 | constructor() {
|
9 |
| - // Save char symbol start positions |
| 39 | + // Сохраняем начальные точки из таблицы |
10 | 40 | this.a = 'a'.charCodeAt(0);
|
11 | 41 | this.A = 'A'.charCodeAt(0);
|
12 |
| - // file hashes cache |
| 42 | + this.zero = '0'.charCodeAt(0); |
13 | 43 | this.files = {};
|
14 |
| - /** encoding [a-zA-Z] */ |
15 |
| - this.symbols = 52; |
16 |
| - /** a half of encoding */ |
17 |
| - this.half = 26; |
18 |
| - /** prevent loop-hell */ |
19 |
| - this.maxLoop = 10; |
| 44 | + // [a-zA-Z\d_-] |
| 45 | + this.encoderSize = 64; |
| 46 | + this.symbolsArea = { |
| 47 | + // a-z |
| 48 | + az: 26, |
| 49 | + // A-Z |
| 50 | + AZ: 52, |
| 51 | + // _ |
| 52 | + under: 53, |
| 53 | + // 0-9 | \d |
| 54 | + digit: 63, |
| 55 | + // - |
| 56 | + // dash: 64 |
| 57 | + }; |
| 58 | + // prevent loop hell |
| 59 | + this.maxLoop = 5; |
| 60 | + this.rootPathLen = process.cwd().length; |
20 | 61 | }
|
21 | 62 |
|
22 |
| - /** encoding by rule count at file, 0 - a, 1 - b, 51 - Z, 52 - ba, etc */ |
23 |
| - getName(lastUsed) { |
24 |
| - const { a, A, symbols, maxLoop, half } = this; |
25 |
| - let name = ''; |
26 |
| - let loop = 0; |
27 |
| - let main = lastUsed; |
28 |
| - let tail = 0; |
29 |
| - |
30 |
| - while ( |
31 |
| - ((main > 0 && tail >= 0) || |
32 |
| - // first step anyway needed |
33 |
| - loop === 0) && |
34 |
| - loop < maxLoop |
35 |
| - ) { |
36 |
| - const newMain = Math.floor(main / symbols); |
37 |
| - |
38 |
| - tail = main % symbols; |
39 |
| - name = String.fromCharCode((tail >= half ? A - half : a) + tail) + name; |
40 |
| - main = newMain; |
41 |
| - loop += 1; |
| 63 | + getSingleSymbol(n) { |
| 64 | + const { |
| 65 | + a, |
| 66 | + A, |
| 67 | + zero, |
| 68 | + encoderSize, |
| 69 | + symbolsArea: { az, AZ, under, digit }, |
| 70 | + } = this; |
| 71 | + |
| 72 | + if (!n) { |
| 73 | + console.error(`!n, n=${n}`); |
| 74 | + |
| 75 | + return ''; |
| 76 | + } |
| 77 | + |
| 78 | + if (n > encoderSize) { |
| 79 | + console.error(`n > ${encoderSize}, n=${n}`); |
| 80 | + |
| 81 | + return ''; |
| 82 | + } |
| 83 | + |
| 84 | + // work with 1 <= n <= 64 |
| 85 | + if (n <= az) { |
| 86 | + return String.fromCharCode(n - 1 + a); |
| 87 | + } |
| 88 | + |
| 89 | + if (n <= AZ) { |
| 90 | + return String.fromCharCode(n - 1 - az + A); |
| 91 | + } |
| 92 | + |
| 93 | + if (n <= under) { |
| 94 | + return '_'; |
| 95 | + } |
| 96 | + |
| 97 | + if (n <= digit) { |
| 98 | + return String.fromCharCode(n - 1 - under + zero); |
42 | 99 | }
|
43 | 100 |
|
44 |
| - return name; |
| 101 | + return '-'; |
45 | 102 | }
|
46 | 103 |
|
47 |
| - getLocalIdent(context, localIdentName, localName) { |
| 104 | + /** Кодируем класс по позиции в списке, 0 - а, 1 - b, итп */ |
| 105 | + getNamePrefix(num) { |
| 106 | + const { maxLoop, encoderSize } = this; |
| 107 | + |
| 108 | + if (!num) { |
| 109 | + return ''; |
| 110 | + } |
| 111 | + |
| 112 | + let loopCount = 0; |
| 113 | + let n = num; |
| 114 | + let res = ''; |
| 115 | + |
| 116 | + // Немного усовеншенственный енкодер. В найденых простейших пропускаются комбинации |
| 117 | + // Ходим в цикле, делим на кодирующий размер (64) |
| 118 | + // Например, с 1 по 64 - 1 проход, с 65 по 4096 (64*64) - 2 прохода цикла, итд |
| 119 | + while (n && loopCount < maxLoop) { |
| 120 | + // Остаток от деления, будем его кодировать от 1 до 64. |
| 121 | + let tail = n % encoderSize; |
| 122 | + const origTail = tail; |
| 123 | + |
| 124 | + // Проверка граничных значений n === encoderSize. 64 % 64 = 0, а кодировать будем 64 |
| 125 | + if (tail === 0) { |
| 126 | + tail = encoderSize; |
| 127 | + } |
| 128 | + |
| 129 | + // Берем результат кодирования, добавляем в строку |
| 130 | + res = this.getSingleSymbol(tail) + res; |
| 131 | + |
| 132 | + // Проверяем, нужно ли уходить на новый цикл |
| 133 | + if (Math.floor((n - 1) / encoderSize)) { |
| 134 | + // Находим кол-во разрядов для след. цикла кодирования. |
| 135 | + n = (n - origTail) / encoderSize; |
| 136 | + |
| 137 | + // На граничном значении (64) уйдем на новый круг, -1 чтобы этого избежать (это мы уже закодировали в текущем проходе) |
| 138 | + if (origTail === 0) { |
| 139 | + n -= 1; |
| 140 | + } |
| 141 | + } else { |
| 142 | + n = 0; |
| 143 | + } |
| 144 | + |
| 145 | + loopCount += 1; |
| 146 | + } |
| 147 | + |
| 148 | + return res; |
| 149 | + } |
| 150 | + |
| 151 | + /** |
| 152 | + * Переопределяем ф-ию хеширования класса. |
| 153 | + * Т.к. обработка на этапе сборки, то файлы разные, отсюда меньше выхлопа |
| 154 | + */ |
| 155 | + getLocalIdentWithFileHash(context, localIdentName, localName) { |
48 | 156 | const { resourcePath } = context;
|
49 |
| - const { files } = this; |
| 157 | + const { files, rootPathLen } = this; |
| 158 | + |
| 159 | + // Чтобы убрать разницу стендов - оставляем только значимый кусок пути файла |
| 160 | + const resPath = resourcePath.substr(rootPathLen); |
50 | 161 |
|
51 |
| - // check file data at cache by absolute path |
52 |
| - let fileShort = files[resourcePath]; |
| 162 | + // Файл уже в списке, берем его новое имя |
| 163 | + let fileShort = files[resPath]; |
53 | 164 |
|
54 |
| - // no file data, lets generate and save |
| 165 | + // Файла нет в списке, генерируем новое имя, и сохраняем |
55 | 166 | if (!fileShort) {
|
56 |
| - // if we know file position, we must use base52 encoding with '_' |
57 |
| - // between rule position and file position |
58 |
| - // to avoid collapse hash combination. a_ab vs aa_b |
59 |
| - const fileShortName = loaderUtils.interpolateName( |
60 |
| - context, |
61 |
| - '[hash:base64:8]', |
62 |
| - { |
63 |
| - content: resourcePath, |
64 |
| - } |
65 |
| - ); |
| 167 | + // парсим переданное правило |
| 168 | + const localIdentRule = getRule(localIdentName); |
| 169 | + |
| 170 | + const fileShortName = interpolateName(context, localIdentRule.val, { |
| 171 | + content: resPath, |
| 172 | + }); |
66 | 173 |
|
67 |
| - fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} }; |
68 |
| - files[resourcePath] = fileShort; |
| 174 | + fileShort = { name: fileShortName, lastUsed: 0, ruleNames: {} }; |
| 175 | + files[resPath] = fileShort; |
69 | 176 | }
|
70 | 177 |
|
71 |
| - // Get generative rule name from this file |
| 178 | + // Берем сгенерированное имя правило, если такое уже было в текущем файле |
72 | 179 | let newRuleName = fileShort.ruleNames[localName];
|
73 | 180 |
|
74 |
| - // If no rule - renerate new, and save |
| 181 | + // Если его не было - генерируем новое, и сохраняем |
75 | 182 | if (!newRuleName) {
|
76 |
| - // Count +1 |
| 183 | + // Увеличиваем счетчик правила для текущего файла |
77 | 184 | fileShort.lastUsed += 1;
|
78 | 185 |
|
79 |
| - // Generate new rule name |
80 |
| - newRuleName = this.getName(fileShort.lastUsed) + fileShort.name; |
| 186 | + // Генерируем новое имя правила |
| 187 | + newRuleName = this.getNamePrefix(fileShort.lastUsed) + fileShort.name; |
81 | 188 |
|
82 |
| - // Saved |
| 189 | + // сохраняем |
83 | 190 | fileShort.ruleNames[localName] = newRuleName;
|
84 | 191 | }
|
85 | 192 |
|
86 |
| - // If has "local" at webpack settings |
| 193 | + // Проверяем, есть ли в веб-паке настройки, что нам нужны оригинальные имена классов |
87 | 194 | const hasLocal = /\[local]/.test(localIdentName);
|
88 | 195 |
|
89 |
| - // If has - add prefix |
90 |
| - return hasLocal ? `${localName}__${newRuleName}` : newRuleName; |
| 196 | + // Если develop-настройка есть - добавляем префикс |
| 197 | + const res = hasLocal ? `${localName}__${newRuleName}` : newRuleName; |
| 198 | + |
| 199 | + // Добавляем префикс '_' для классов, начинающихся с '-', цифры '\d' |
| 200 | + // или '_' (для исключения коллизий, т.к. символ участвует в кодировании) |
| 201 | + return /^[\d_-]/.test(res) ? `_${res}` : res; |
| 202 | + } |
| 203 | + |
| 204 | + getStat() { |
| 205 | + return this.files; |
91 | 206 | }
|
92 | 207 | }
|
0 commit comments