source: frontend/node_modules/postcss-minify-font-values/src/lib/minify-family.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 6.3 KB
Line 
1'use strict';
2const { stringify } = require('postcss-value-parser');
3
4/**
5 * @param {string[]} list
6 * @return {string[]}
7 */
8function uniqueFontFamilies(list) {
9 return list.filter((item, i) => {
10 if (item.toLowerCase() === 'monospace') {
11 return true;
12 }
13 return i === list.indexOf(item);
14 });
15}
16
17const globalKeywords = ['inherit', 'initial', 'unset'];
18const genericFontFamilykeywords = new Set([
19 'sans-serif',
20 'serif',
21 'fantasy',
22 'cursive',
23 'monospace',
24 'system-ui',
25]);
26
27/**
28 * @param {string} value
29 * @param {number} length
30 * @return {string[]}
31 */
32function makeArray(value, length) {
33 let array = [];
34 while (length--) {
35 array[length] = value;
36 }
37 return array;
38}
39
40// eslint-disable-next-line no-useless-escape
41const regexSimpleEscapeCharacters = /[ !"#$%&'()*+,.\/;<=>?@\[\\\]^`{|}~]/;
42
43/**
44 * @param {string} string
45 * @param {boolean} escapeForString
46 * @return {string}
47 */
48function escape(string, escapeForString) {
49 let counter = 0;
50 let character;
51 let charCode;
52 let value;
53 let output = '';
54
55 while (counter < string.length) {
56 character = string.charAt(counter++);
57 charCode = character.charCodeAt(0);
58
59 // \r is already tokenized away at this point
60 // `:` can be escaped as `\:`, but that fails in IE < 8
61 if (!escapeForString && /[\t\n\v\f:]/.test(character)) {
62 value = '\\' + charCode.toString(16) + ' ';
63 } else if (
64 !escapeForString &&
65 regexSimpleEscapeCharacters.test(character)
66 ) {
67 value = '\\' + character;
68 } else {
69 value = character;
70 }
71
72 output += value;
73 }
74
75 if (!escapeForString) {
76 if (/^-[-\d]/.test(output)) {
77 output = '\\-' + output.slice(1);
78 }
79
80 const firstChar = string.charAt(0);
81
82 if (/\d/.test(firstChar)) {
83 output = '\\3' + firstChar + ' ' + output.slice(1);
84 }
85 }
86
87 return output;
88}
89
90const regexKeyword = new RegExp(
91 [...genericFontFamilykeywords].concat(globalKeywords).join('|'),
92 'i'
93);
94const regexInvalidIdentifier = /^(-?\d|--)/;
95const regexSpaceAtStart = /^\x20/;
96const regexWhitespace = /[\t\n\f\r\x20]/g;
97const regexIdentifierCharacter = /^[a-zA-Z\d\xa0-\uffff_-]+$/;
98const regexConsecutiveSpaces = /(\\(?:[a-fA-F0-9]{1,6}\x20|\x20))?(\x20{2,})/g;
99const regexTrailingEscape = /\\[a-fA-F0-9]{0,6}\x20$/;
100const regexTrailingSpace = /\x20$/;
101
102/**
103 * @param {string} string
104 * @return {string}
105 */
106function escapeIdentifierSequence(string) {
107 let identifiers = string.split(regexWhitespace);
108 let index = 0;
109 /** @type {string[] | string} */
110 let result = [];
111 let escapeResult;
112
113 while (index < identifiers.length) {
114 let subString = identifiers[index++];
115
116 if (subString === '') {
117 result.push(subString);
118 continue;
119 }
120
121 escapeResult = escape(subString, false);
122
123 if (regexIdentifierCharacter.test(subString)) {
124 // the font family name part consists of allowed characters exclusively
125 if (regexInvalidIdentifier.test(subString)) {
126 // the font family name part starts with two hyphens, a digit, or a
127 // hyphen followed by a digit
128 if (index === 1) {
129 // if this is the first item
130 result.push(escapeResult);
131 } else {
132 // if it’s not the first item, we can simply escape the space
133 // between the two identifiers to merge them into a single
134 // identifier rather than escaping the start characters of the
135 // second identifier
136 result[index - 2] += '\\';
137 result.push(escape(subString, true));
138 }
139 } else {
140 // the font family name part doesn’t start with two hyphens, a digit,
141 // or a hyphen followed by a digit
142 result.push(escapeResult);
143 }
144 } else {
145 // the font family name part contains invalid identifier characters
146 result.push(escapeResult);
147 }
148 }
149
150 result = result.join(' ').replace(regexConsecutiveSpaces, ($0, $1, $2) => {
151 const spaceCount = $2.length;
152 const escapesNeeded = Math.floor(spaceCount / 2);
153 const array = makeArray('\\ ', escapesNeeded);
154
155 if (spaceCount % 2) {
156 array[escapesNeeded - 1] += '\\ ';
157 }
158
159 return ($1 || '') + ' ' + array.join(' ');
160 });
161
162 // Escape trailing spaces unless they’re already part of an escape
163 if (regexTrailingSpace.test(result) && !regexTrailingEscape.test(result)) {
164 result = result.replace(regexTrailingSpace, '\\ ');
165 }
166
167 if (regexSpaceAtStart.test(result)) {
168 result = '\\ ' + result.slice(1);
169 }
170
171 return result;
172}
173/**
174 * @param {import('postcss-value-parser').Node[]} nodes
175 * @param {import('../index').Options} opts
176 * @return {import('postcss-value-parser').WordNode[]}
177 */
178module.exports = function (nodes, opts) {
179 /** @type {import('postcss-value-parser').Node[]} */
180 const family = [];
181 /** @type {import('postcss-value-parser').WordNode | null} */
182 let last = null;
183 let i, max;
184
185 nodes.forEach((node, index, arr) => {
186 if (node.type === 'string' || node.type === 'function') {
187 family.push(node);
188 } else if (node.type === 'word') {
189 if (!last) {
190 last = /** @type {import('postcss-value-parser').WordNode} */ ({
191 type: 'word',
192 value: '',
193 });
194 family.push(last);
195 }
196
197 last.value += node.value;
198 } else if (node.type === 'space') {
199 if (last && index !== arr.length - 1) {
200 last.value += ' ';
201 }
202 } else {
203 last = null;
204 }
205 });
206
207 let normalizedFamilies = family.map((node) => {
208 if (node.type === 'string') {
209 const isKeyword = regexKeyword.test(node.value);
210
211 if (
212 !opts.removeQuotes ||
213 isKeyword ||
214 /[0-9]/.test(node.value.slice(0, 1))
215 ) {
216 return stringify(node);
217 }
218
219 let escaped = escapeIdentifierSequence(node.value);
220
221 if (escaped.length < node.value.length + 2) {
222 return escaped;
223 }
224 }
225
226 return stringify(node);
227 });
228
229 if (opts.removeAfterKeyword) {
230 for (i = 0, max = normalizedFamilies.length; i < max; i += 1) {
231 if (genericFontFamilykeywords.has(normalizedFamilies[i].toLowerCase())) {
232 normalizedFamilies = normalizedFamilies.slice(0, i + 1);
233 break;
234 }
235 }
236 }
237
238 if (opts.removeDuplicates) {
239 normalizedFamilies = uniqueFontFamilies(normalizedFamilies);
240 }
241
242 return [
243 /** @type {import('postcss-value-parser').WordNode} */ ({
244 type: 'word',
245 value: normalizedFamilies.join(),
246 }),
247 ];
248};
Note: See TracBrowser for help on using the repository browser.