single-file-core 1.6.18 → 1.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/helper.js CHANGED
@@ -86,6 +86,17 @@ const FONT_WEIGHTS = {
86
86
  bolder: "700",
87
87
  lighter: "100"
88
88
  };
89
+ const FONT_STRETCHES = {
90
+ "ultra-condensed": "50%",
91
+ "extra-condensed": "62.5%",
92
+ "condensed": "75%",
93
+ "semi-condensed": "87.5%",
94
+ "normal": "100%",
95
+ "semi-expanded": "112.5%",
96
+ "expanded": "125%",
97
+ "extra-expanded": "150%",
98
+ "ultra-expanded": "200%"
99
+ };
89
100
  const COMMENT_HEADER_LEGACY = "Archive processed by SingleFile";
90
101
  const ELEMENT_NODE_TYPE = 1;
91
102
  const TEXT_NODE_TYPE = 3;
@@ -139,6 +150,8 @@ export {
139
150
  removeQuotes,
140
151
  flatten,
141
152
  getFontWeight,
153
+ getFontStretch,
154
+ getStylesheetsInCascadeOrder,
142
155
  normalizeFontFamily,
143
156
  getShadowRoot,
144
157
  appendInfobar,
@@ -926,8 +939,9 @@ function getUsedFont(computedStyle, usedFonts, usedFontsCharacters, drawnCharact
926
939
  if (fontFamilyName) {
927
940
  const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
928
941
  const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
929
- const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
930
- usedFonts.set(JSON.stringify(value), [fontFamilyName, fontWeight, fontStyle, fontVariant]);
942
+ const fontStretch = getFontStretch(computedStyle.getPropertyValue("font-stretch") || "normal");
943
+ const value = [fontFamilyName, fontWeight, fontStyle, fontVariant, fontStretch];
944
+ usedFonts.set(JSON.stringify(value), value);
931
945
  if (usedFontsCharacters && drawnCharacters) {
932
946
  addUsedFontCharacters(usedFontsCharacters, fontFamilyName, fontStyle, drawnCharacters);
933
947
  }
@@ -1246,6 +1260,57 @@ function getFontWeight(weight) {
1246
1260
  return FONT_WEIGHTS[weight.toLowerCase().trim()] || weight;
1247
1261
  }
1248
1262
 
1263
+ function getFontStretch(stretch) {
1264
+ return FONT_STRETCHES[stretch] || stretch;
1265
+ }
1266
+
1267
+ function getStylesheetsInCascadeOrder(stylesheets) {
1268
+ const entries = [];
1269
+ const entriesByRules = new Map();
1270
+ stylesheets.forEach((stylesheetInfo, key) => {
1271
+ const entry = { stylesheetInfo, key, index: entries.length };
1272
+ entries.push(entry);
1273
+ if (stylesheetInfo.stylesheet && stylesheetInfo.stylesheet.children) {
1274
+ entriesByRules.set(stylesheetInfo.stylesheet.children, entry);
1275
+ }
1276
+ });
1277
+ const importedEntries = new Set();
1278
+ entries.forEach(entry => getImportedStylesheetEntries(entry, entriesByRules).forEach(importedEntry => importedEntries.add(importedEntry)));
1279
+ const orderedEntries = [];
1280
+ const visitedEntries = new Set();
1281
+ entries
1282
+ .filter(entry => !importedEntries.has(entry))
1283
+ .forEach(entry => addStylesheetEntry(entry, entriesByRules, orderedEntries, visitedEntries));
1284
+ entries
1285
+ .filter(entry => !visitedEntries.has(entry))
1286
+ .forEach(entry => addStylesheetEntry(entry, entriesByRules, orderedEntries, visitedEntries));
1287
+ return orderedEntries;
1288
+ }
1289
+
1290
+ function addStylesheetEntry(entry, entriesByRules, orderedEntries, visitedEntries) {
1291
+ if (!visitedEntries.has(entry)) {
1292
+ visitedEntries.add(entry);
1293
+ getImportedStylesheetEntries(entry, entriesByRules).forEach(importedEntry => addStylesheetEntry(importedEntry, entriesByRules, orderedEntries, visitedEntries));
1294
+ orderedEntries.push(entry);
1295
+ }
1296
+ }
1297
+
1298
+ function getImportedStylesheetEntries(entry, entriesByRules) {
1299
+ const importedEntries = [];
1300
+ const stylesheet = entry.stylesheetInfo.stylesheet;
1301
+ if (stylesheet && stylesheet.children) {
1302
+ stylesheet.children.forEach(ruleData => {
1303
+ if (ruleData.type == "Atrule" && ruleData.name == "import" && ruleData.prelude && ruleData.prelude.children && ruleData.prelude.children.head) {
1304
+ const importedEntry = entriesByRules.get(ruleData.prelude.children.head.data.importedChildren);
1305
+ if (importedEntry) {
1306
+ importedEntries.push(importedEntry);
1307
+ }
1308
+ }
1309
+ });
1310
+ }
1311
+ return importedEntries;
1312
+ }
1313
+
1249
1314
  function getContentSize(content) {
1250
1315
  return new Blob([content]).size;
1251
1316
  }
package/core/index.js CHANGED
@@ -1343,10 +1343,11 @@ class Processor {
1343
1343
  if (element.media) {
1344
1344
  mediaText = element.media.toLowerCase();
1345
1345
  }
1346
- const scoped = Boolean(element.closest("[" + SHADOWROOT_ATTRIBUTE_NAME + "]"));
1346
+ const scope = element.closest("[" + SHADOWROOT_ATTRIBUTE_NAME + "]");
1347
1347
  const stylesheetInfo = {
1348
1348
  mediaText,
1349
- scoped
1349
+ scoped: Boolean(scope),
1350
+ scope
1350
1351
  };
1351
1352
  await this.processorHelper.resolveStylesheets(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement, this.resources);
1352
1353
  }));
@@ -24,6 +24,8 @@
24
24
  import {
25
25
  normalizeFontFamily,
26
26
  getFontWeight,
27
+ getFontStretch,
28
+ getStylesheetsInCascadeOrder,
27
29
  getDataURI
28
30
  } from "./../helper.js";
29
31
  import { serialize as serializeSrcset } from "./../../vendor/html-srcset-parser.js";
@@ -51,17 +53,6 @@ const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
51
53
  const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
52
54
  const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
53
55
  const MEDIA_ALL = "all";
54
- const FONT_STRETCHES = {
55
- "ultra-condensed": "50%",
56
- "extra-condensed": "62.5%",
57
- "condensed": "75%",
58
- "semi-condensed": "87.5%",
59
- "normal": "100%",
60
- "semi-expanded": "112.5%",
61
- "expanded": "125%",
62
- "extra-expanded": "150%",
63
- "ultra-expanded": "200%"
64
- };
65
56
  const Blob = globalThis.Blob;
66
57
  const Image = globalThis.Image;
67
58
  const OffscreenCanvas = globalThis.OffscreenCanvas;
@@ -423,9 +414,12 @@ class ProcessorHelperCommon {
423
414
  async removeAlternativeFonts(doc, stylesheets, fonts, fontTests) {
424
415
  const fontsDetails = this.createFontsDetailsInfo();
425
416
  const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
426
- let sheetIndex = 0;
427
- stylesheets.forEach(stylesheetInfo => {
417
+ const ruleScopes = new Map();
418
+ getStylesheetsInCascadeOrder(stylesheets).forEach(({ stylesheetInfo, index: sheetIndex }) => {
428
419
  if (stylesheetInfo.stylesheet) {
420
+ if (stylesheetInfo.scope) {
421
+ cssTree.walk(stylesheetInfo.stylesheet, { visit: "Atrule", enter: node => ruleScopes.set(node, stylesheetInfo.scope) });
422
+ }
429
423
  const cssRules = stylesheetInfo.stylesheet.children;
430
424
  if (cssRules) {
431
425
  stats.rules.processed += cssRules.size;
@@ -439,10 +433,9 @@ class ProcessorHelperCommon {
439
433
  }
440
434
  }
441
435
  }
442
- sheetIndex++;
443
436
  });
444
437
  processFontDetails(fontsDetails, fonts);
445
- this.markRepeatedFontFaceRules(fontsDetails);
438
+ this.markRepeatedFontFaceRules(fontsDetails, ruleScopes, new Map());
446
439
  await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
447
440
  if (stylesheetInfo.stylesheet) {
448
441
  const cssRules = stylesheetInfo.stylesheet.children;
@@ -460,19 +453,23 @@ class ProcessorHelperCommon {
460
453
  return stats;
461
454
  }
462
455
 
463
- markRepeatedFontFaceRules(fontsDetails) {
456
+ markRepeatedFontFaceRules(fontsDetails, ruleScopes, scopeIds) {
464
457
  const lastRules = new Map();
465
458
  fontsDetails.fonts.forEach((fontInfo, ruleData) => {
466
- const ruleKey = this.getFontKey(ruleData) + " " + this.getPropertyValue(ruleData, "src");
459
+ const scope = ruleScopes.get(ruleData);
460
+ if (scope && !scopeIds.has(scope)) {
461
+ scopeIds.set(scope, scopeIds.size + 1);
462
+ }
463
+ const ruleKey = (scope ? scopeIds.get(scope) : 0) + " " + this.getFontKey(ruleData) + " " + this.getPropertyValue(ruleData, "src");
467
464
  const previousRuleData = lastRules.get(ruleKey);
468
465
  if (previousRuleData) {
469
466
  fontsDetails.repeatedFonts.add(previousRuleData);
470
467
  }
471
468
  lastRules.set(ruleKey, ruleData);
472
469
  });
473
- fontsDetails.medias.forEach(mediaFontsDetails => this.markRepeatedFontFaceRules(mediaFontsDetails));
474
- fontsDetails.supports.forEach(supportsFontsDetails => this.markRepeatedFontFaceRules(supportsFontsDetails));
475
- fontsDetails.layers.forEach(layerFontsDetails => this.markRepeatedFontFaceRules(layerFontsDetails));
470
+ fontsDetails.medias.forEach(mediaFontsDetails => this.markRepeatedFontFaceRules(mediaFontsDetails, ruleScopes, scopeIds));
471
+ fontsDetails.supports.forEach(supportsFontsDetails => this.markRepeatedFontFaceRules(supportsFontsDetails, ruleScopes, scopeIds));
472
+ fontsDetails.layers.forEach(layerFontsDetails => this.markRepeatedFontFaceRules(layerFontsDetails, ruleScopes, scopeIds));
476
473
  }
477
474
 
478
475
  async processFontFaceRules(cssRules, sheetIndex, fontsDetails, fonts, fontTests, stats) {
@@ -739,10 +736,6 @@ function getURL(urlFunction) {
739
736
  return urlMatch && urlMatch[1];
740
737
  }
741
738
 
742
- function getFontStretch(stretch) {
743
- return FONT_STRETCHES[stretch] || stretch;
744
- }
745
-
746
739
  async function resizeImage(doc, dataURI, { imageReductionFactor, imageQuality }, element) {
747
740
  if (dataURI) {
748
741
  const contentType = dataURI.substring(5, dataURI.indexOf(";"));
@@ -780,10 +773,13 @@ async function resizeImage(doc, dataURI, { imageReductionFactor, imageQuality },
780
773
  }, contentType, quality));
781
774
  }
782
775
  if (blob.type == contentType) {
783
- dataURI = await toDataURI(blob, contentType);
784
- if (element && isUnsizedSVGImage(element)) {
785
- element.setAttribute("width", image.naturalWidth);
786
- element.setAttribute("height", image.naturalHeight);
776
+ const resizedDataURI = await toDataURI(blob, contentType);
777
+ if (resizedDataURI.length < dataURI.length) {
778
+ dataURI = resizedDataURI;
779
+ if (element && isUnsizedSVGImage(element)) {
780
+ element.setAttribute("width", image.naturalWidth);
781
+ element.setAttribute("height", image.naturalHeight);
782
+ }
787
783
  }
788
784
  }
789
785
  } catch {
@@ -582,9 +582,15 @@ function getProcessorHelperClass(utilInstance) {
582
582
  groupDuplicateFonts(stylesheets, fonts, options) {
583
583
  if (options.usedFonts && options.usedFonts.length) {
584
584
  const fontFaces = [];
585
+ const scopeIds = new Map();
585
586
  stylesheets.forEach(stylesheetInfo => {
586
587
  if (stylesheetInfo.stylesheet && stylesheetInfo.stylesheet.children) {
587
- getFontFaces(stylesheetInfo.stylesheet.children, "", fontFaces);
588
+ const scope = stylesheetInfo.scope || null;
589
+ if (!scopeIds.has(scope)) {
590
+ scopeIds.set(scope, scopeIds.size);
591
+ }
592
+ const mediaScope = stylesheetInfo.mediaText ? "|media " + stylesheetInfo.mediaText : "";
593
+ getFontFaces(stylesheetInfo.stylesheet.children, scopeIds.get(scope) + mediaScope, fontFaces);
588
594
  }
589
595
  });
590
596
  const families = new Map();
@@ -216,7 +216,7 @@ function getProcessorHelperClass(utilInstance) {
216
216
 
217
217
  async resolveImportURLs(stylesheetInfo, baseURI, options, workStylesheet, resources, stylesheets, importedStyleSheets = new Set(), stylesheetReferrer) {
218
218
  const stylesheet = stylesheetInfo.stylesheet;
219
- const scoped = stylesheetInfo.scoped;
219
+ const { scoped, scope } = stylesheetInfo;
220
220
  this.resolveStylesheetURLs(stylesheet, baseURI, workStylesheet, stylesheetReferrer, options.stylesheetReferrers);
221
221
  const imports = getImportFunctions(stylesheet);
222
222
  await Promise.all(imports.map(async node => {
@@ -247,6 +247,7 @@ function getProcessorHelperClass(utilInstance) {
247
247
  }
248
248
  const stylesheetInfo = {
249
249
  scoped,
250
+ scope,
250
251
  mediaText,
251
252
  layerName,
252
253
  supportsCondition
@@ -27,6 +27,8 @@ import {
27
27
  normalizeFontFamily,
28
28
  flatten,
29
29
  getFontWeight,
30
+ getFontStretch,
31
+ getStylesheetsInCascadeOrder,
30
32
  removeQuotes
31
33
  } from "./../core/helper.js";
32
34
 
@@ -34,20 +36,39 @@ const helper = {
34
36
  normalizeFontFamily,
35
37
  flatten,
36
38
  getFontWeight,
39
+ getFontStretch,
40
+ getStylesheetsInCascadeOrder,
37
41
  removeQuotes
38
42
  };
39
43
 
40
44
  const REGEXP_COMMA = /\s*,\s*/;
41
45
  const REGEXP_SPACES = /\s+/g;
42
- const REGEXP_FONT_STYLE_ANGLE = /-?\d*\.?\d+/g;
46
+ const REGEXP_ANGLE = /^([-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(deg|grad|rad|turn)?$/;
47
+ const ANGLE_UNITS = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
48
+ const NORMAL_FONT_STYLE = "normal";
49
+ const ITALIC_FONT_STYLE = "italic";
50
+ const OBLIQUE_FONT_STYLE = "oblique";
43
51
  const DEFAULT_OBLIQUE_ANGLE = 14;
52
+ const NORMAL_FONT_STRETCH = 100;
53
+ const LOWER_WEIGHT_SEARCH_THRESHOLD = 400;
54
+ const UPPER_WEIGHT_SEARCH_THRESHOLD = 500;
55
+ const GECKO_REVERSE_STRETCH_DISTANCE = 1000;
56
+ const GECKO_REVERSE_STYLE_DISTANCE = 100;
57
+ const GECKO_NEGATE_STYLE_DISTANCE = 200;
58
+ const GECKO_BAD_STYLE_DISTANCE = 700;
59
+ const GECKO_REVERSE_WEIGHT_DISTANCE = 600;
60
+ const GECKO_LIGHTER_WEIGHT_DISTANCE = 100;
61
+ const FONT_MATCHING_ALGORITHMS = [
62
+ { getStretchDistance: getWebKitStretchDistance, getStyleDistance: getWebKitStyleDistance, getWeightDistance: getWebKitWeightDistance },
63
+ { getStretchDistance: getWebKitStretchDistance, getStyleDistance: getWebKitObliqueStyleDistance, getWeightDistance: getWebKitWeightDistance },
64
+ { getStretchDistance: getGeckoStretchDistance, getStyleDistance: getGeckoStyleDistance, getWeightDistance: getGeckoWeightDistance }
65
+ ];
44
66
  const REGEXP_DASH = /-/;
45
67
  const REGEXP_QUESTION_MARK = /\?/g;
46
68
  const REGEXP_STARTS_U_PLUS = /^U\+/i;
47
69
  const REGEXP_CUSTOM_PROPERTY = /var\(\s*(--[^\s,)]+)\s*(?:,[^)]*)?\)/g;
48
70
  const REGEXP_CUSTOM_PROPERTY_FAMILY = /^var\(\s*(--[^\s,)]+)\s*(?:,(.*))?\)$/;
49
71
  const REGEXP_CUSTOM_PROPERTY_NAME = /^--/;
50
- const VALID_FONT_STYLES = [/^normal$/, /^italic$/, /^oblique$/, /^oblique\s+/];
51
72
  const NON_GLYPH_CHAR_CODES = [9, 10, 12, 13];
52
73
  const MAX_NON_GLYPH_CHAR_CODE = 13;
53
74
  // a family name kept when the "font" shorthand cannot be read: it resolves to nothing, so it
@@ -74,7 +95,7 @@ function process(doc, stylesheets, styles, options) {
74
95
  });
75
96
  styles.forEach(declarations => getCustomProperties(declarations.children, customProperties));
76
97
  options = Object.assign({}, options, { customProperties });
77
- stylesheets.forEach(stylesheetInfo => {
98
+ helper.getStylesheetsInCascadeOrder(stylesheets).forEach(({ stylesheetInfo }) => {
78
99
  if (stylesheetInfo.stylesheet) {
79
100
  const cssRules = stylesheetInfo.stylesheet.children;
80
101
  if (cssRules) {
@@ -103,7 +124,7 @@ function process(doc, stylesheets, styles, options) {
103
124
  // DOMParser, and that document is never rendered, so it reports nothing and every face it
104
125
  // declares would be dropped
105
126
  const usedFontsUnknown = !options.usedFonts || !options.usedFonts.length;
106
- let unusedFonts, filteredUsedFonts;
127
+ let unusedFonts, filteredUsedFonts, selectedFonts;
107
128
  if (usedFontsUnknown) {
108
129
  unusedFonts = [];
109
130
  } else {
@@ -125,14 +146,15 @@ function process(doc, stylesheets, styles, options) {
125
146
  keepDeclaredFontIfRendered(fontInfo.fontFamily, fontsInfo, filteredUsedFonts, options));
126
147
  }
127
148
  unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
149
+ selectedFonts = getSelectedFonts(fontsInfo.declared, filteredUsedFonts);
128
150
  }
129
- const docChars = Array.from(new Set(docContent)).map(char => char.charCodeAt(0)).sort((value1, value2) => value1 - value2);
151
+ const docChars = Array.from(new Set(docContent)).map(char => char.codePointAt(0)).sort((value1, value2) => value1 - value2);
130
152
  const usedFontsCharacters = getUsedFontsCharacters(options);
131
153
  stylesheets.forEach(stylesheetInfo => {
132
154
  if (stylesheetInfo.stylesheet) {
133
155
  const cssRules = stylesheetInfo.stylesheet.children;
134
156
  if (cssRules) {
135
- filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
157
+ filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, selectedFonts, docChars, usedFontsCharacters);
136
158
  stats.rules.discarded -= cssRules.size;
137
159
  }
138
160
  }
@@ -167,9 +189,10 @@ function getFontsInfo(cssRules, fontsInfo, options) {
167
189
  const fontWeight = getDeclarationValue(ruleData.block.children, "font-weight") || "400";
168
190
  const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
169
191
  const fontVariant = getDeclarationValue(ruleData.block.children, "font-variant") || "normal";
192
+ const fontStretch = getDeclarationValue(ruleData.block.children, "font-stretch") || "normal";
170
193
  const unicodeRange = getDeclarationValue(ruleData.block.children, "unicode-range");
171
194
  fontWeight.split(",").forEach(weightValue =>
172
- fontsInfo.declared.push({ fontFamily, fontWeight: helper.getFontWeight(helper.removeQuotes(weightValue)), fontStyle, fontVariant, unicodeRange, ruleData }));
195
+ fontsInfo.declared.push({ fontFamily, fontWeight: helper.getFontWeight(helper.removeQuotes(weightValue)), fontStyle, fontVariant, fontStretch, unicodeRange, ruleData }));
173
196
  }
174
197
  }
175
198
  }
@@ -296,14 +319,14 @@ function splitValues(value) {
296
319
  return values;
297
320
  }
298
321
 
299
- function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters) {
322
+ function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, selectedFonts, docChars, usedFontsCharacters) {
300
323
  const removedRules = [];
301
324
  for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
302
325
  const ruleData = cssRule.data;
303
326
  if (ruleData.type == "Atrule" && ruleData.name == "import" && ruleData.prelude && ruleData.prelude.children && ruleData.prelude.children.head.data.importedChildren) {
304
- filterUnusedFonts(ruleData.prelude.children.head.data.importedChildren, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
327
+ filterUnusedFonts(ruleData.prelude.children.head.data.importedChildren, declaredFonts, unusedFonts, selectedFonts, docChars, usedFontsCharacters);
305
328
  } else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports" || ruleData.name == "layer" || ruleData.name == "container") && ruleData.block && ruleData.block.children) {
306
- filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docChars, usedFontsCharacters);
329
+ filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, selectedFonts, docChars, usedFontsCharacters);
307
330
  } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
308
331
  const fontFamily = helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
309
332
  if (fontFamily) {
@@ -312,8 +335,8 @@ function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFon
312
335
  if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) ||
313
336
  !testUnicodeRange(docChars, unicodeRange) ||
314
337
  !testReachableUnicodeRange(docChars, unicodeRange, laterUnicodeRanges) ||
315
- !testDrawnUnicodeRange(ruleData, fontFamily, unicodeRange, usedFontsCharacters) ||
316
- !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
338
+ !testDrawnUnicodeRange(ruleData, fontFamily, unicodeRange, usedFontsCharacters, selectedFonts) ||
339
+ !testUsedFont(ruleData, fontFamily, selectedFonts)) {
317
340
  removedRules.push(cssRule);
318
341
  }
319
342
  }
@@ -346,7 +369,7 @@ function getUsedFontsCharacters(options) {
346
369
  return usedFontsCharacters;
347
370
  }
348
371
 
349
- function testDrawnUnicodeRange(ruleData, familyName, unicodeRange, usedFontsCharacters) {
372
+ function testDrawnUnicodeRange(ruleData, familyName, unicodeRange, usedFontsCharacters, selectedFonts) {
350
373
  if (!unicodeRange || !usedFontsCharacters || !usedFontsCharacters.size) {
351
374
  return true;
352
375
  }
@@ -354,11 +377,11 @@ function testDrawnUnicodeRange(ruleData, familyName, unicodeRange, usedFontsChar
354
377
  if (!buckets || !buckets.length) {
355
378
  return true;
356
379
  }
357
- const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
358
- if (!VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
380
+ const selection = selectedFonts && selectedFonts.get(familyName);
381
+ if (!selection || !selection.candidates.has(ruleData) || buckets.find(bucket => !selection.rulesByStyle.has(bucket.fontStyle))) {
359
382
  return true;
360
383
  }
361
- const matchedBuckets = buckets.filter(bucket => testFontStyle(bucket.fontStyle, fontStyle));
384
+ const matchedBuckets = buckets.filter(bucket => selection.rulesByStyle.get(bucket.fontStyle).has(ruleData));
362
385
  if (!matchedBuckets.length || matchedBuckets.find(bucket => bucket.unknown)) {
363
386
  return true;
364
387
  }
@@ -396,93 +419,242 @@ function testDrawnGlyph(drawnRange, range) {
396
419
  return false;
397
420
  }
398
421
 
399
- function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
400
- let test;
401
- const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
402
- if (optionalUsedFonts && optionalUsedFonts.length) {
403
- const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
404
- if (VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
405
- const fontWeight = helper.getFontWeight(getDeclarationValue(ruleData.block.children, "font-weight") || "400");
406
- const declaredFontsWeights = declaredFonts
407
- .filter(fontInfo => fontInfo.fontFamily == familyName && testFontStyle(fontInfo.fontStyle, fontStyle))
408
- .map(fontInfo => fontInfo.fontWeight.split(" "))
409
- .sort((weight1, weight2) => Number.parseInt(weight1[0], 10) - Number.parseInt(weight2[0], 10));
410
- let usedFontWeights = optionalUsedFonts
411
- .map(fontInfo => getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights))
412
- .filter(fontWeight => fontWeight);
413
- test = testFontweight(fontWeight, usedFontWeights);
414
- if (!test) {
415
- usedFontWeights = optionalUsedFonts
416
- .map(fontInfo => {
417
- fontInfo = Array.from(fontInfo);
418
- fontInfo[2] = "normal";
419
- return getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights);
420
- })
421
- .filter(fontWeight => fontWeight);
422
- test = testFontweight(fontWeight, usedFontWeights);
422
+ function testUsedFont(ruleData, familyName, selectedFonts) {
423
+ const selection = selectedFonts && selectedFonts.get(familyName);
424
+ return !selection || !selection.candidates.has(ruleData) || selection.rules.has(ruleData);
425
+ }
426
+
427
+ function getSelectedFonts(declaredFonts, filteredUsedFonts) {
428
+ const selectedFonts = new Map();
429
+ filteredUsedFonts.forEach((usedFonts, familyName) => {
430
+ const fonts = declaredFonts
431
+ .filter(fontInfo => fontInfo.fontFamily == familyName)
432
+ .map(fontInfo => ({
433
+ ruleData: fontInfo.ruleData,
434
+ weight: parseFontWeight(fontInfo.fontWeight),
435
+ style: parseFontStyle(fontInfo.fontStyle),
436
+ stretch: parseFontStretch(fontInfo.fontStretch)
437
+ }))
438
+ .filter(fontInfo => fontInfo.weight && fontInfo.style && fontInfo.stretch);
439
+ const selection = { candidates: new Set(fonts.map(fontInfo => fontInfo.ruleData)), rules: new Set(), rulesByStyle: new Map() };
440
+ usedFonts.forEach(([, fontWeight, fontStyle, , fontStretch]) => {
441
+ let styleRules = selection.rulesByStyle.get(fontStyle);
442
+ if (!styleRules) {
443
+ styleRules = new Set();
444
+ selection.rulesByStyle.set(fontStyle, styleRules);
423
445
  }
446
+ FONT_MATCHING_ALGORITHMS.forEach(algorithm => selectFonts(fonts, fontWeight, fontStyle, fontStretch, algorithm).forEach(fontInfo => {
447
+ styleRules.add(fontInfo.ruleData);
448
+ selection.rules.add(fontInfo.ruleData);
449
+ }));
450
+ });
451
+ selectedFonts.set(familyName, selection);
452
+ });
453
+ return selectedFonts;
454
+ }
455
+
456
+ function selectFonts(fonts, fontWeight, fontStyle, fontStretch, algorithm) {
457
+ const stretchBounds = getBounds(fonts.map(fontInfo => fontInfo.stretch));
458
+ const styleBounds = getBounds(fonts.map(fontInfo => fontInfo.style.range));
459
+ const weightBounds = getBounds(fonts.map(fontInfo => fontInfo.weight));
460
+ const stretch = fontStretch && parseFontStretch(fontStretch);
461
+ if (fonts.length && stretch && stretch[0] == stretch[1]) {
462
+ fonts = filterFonts(fonts, fontInfo => fontInfo.stretch, fontInfo => algorithm.getStretchDistance(fontInfo.stretch, stretch[0], stretchBounds));
463
+ }
464
+ const style = fontStyle && parseFontStyle(fontStyle);
465
+ if (fonts.length && style && style.range[0] == style.range[1]) {
466
+ fonts = filterFonts(fonts, fontInfo => fontInfo.style.range, fontInfo => algorithm.getStyleDistance(fontInfo.style, style, styleBounds));
467
+ }
468
+ const weight = fontWeight && parseFontWeight(String(fontWeight));
469
+ if (fonts.length && weight && weight[0] == weight[1]) {
470
+ fonts = filterFonts(fonts, fontInfo => fontInfo.weight, fontInfo => algorithm.getWeightDistance(fontInfo.weight, weight[0], weightBounds));
471
+ }
472
+ return fonts;
473
+ }
474
+
475
+ function filterFonts(fonts, getRange, getDistance) {
476
+ const results = fonts.map(getDistance);
477
+ const distance = Math.min(...results.map(result => result.distance));
478
+ const values = results
479
+ .filter(result => result.distance == distance && result.value !== undefined)
480
+ .map(result => result.value);
481
+ return fonts.filter((fontInfo, index) => results[index].distance == distance || values.some(value => testRange(getRange(fontInfo), value)));
482
+ }
483
+
484
+ function getBounds(ranges) {
485
+ return [Math.min(...ranges.map(([min]) => min)), Math.max(...ranges.map(([, max]) => max))];
486
+ }
487
+
488
+ function getWebKitStretchDistance([min, max], stretch, [boundsMin, boundsMax]) {
489
+ if (testRange([min, max], stretch)) {
490
+ return { distance: 0, value: stretch };
491
+ } else if (stretch > NORMAL_FONT_STRETCH) {
492
+ return min > stretch ? { distance: min - stretch, value: min } : { distance: Math.max(stretch, boundsMax) - max, value: max };
493
+ } else {
494
+ return max < stretch ? { distance: stretch - max, value: max } : { distance: min - Math.min(stretch, boundsMin), value: min };
495
+ }
496
+ }
497
+
498
+ function getWebKitStyleDistance({ range: [min, max] }, { range: [slope] }, [boundsMin, boundsMax]) {
499
+ if (testRange([min, max], slope)) {
500
+ return { distance: 0, value: slope };
501
+ } else if (slope >= DEFAULT_OBLIQUE_ANGLE) {
502
+ return min > slope ? { distance: min - slope, value: min } : { distance: Math.max(slope, boundsMax) - max, value: max };
503
+ } else if (slope >= 0) {
504
+ if (max >= 0 && max < slope) {
505
+ return { distance: slope - max, value: max };
424
506
  } else {
425
- test = true;
507
+ return min > slope ? { distance: min, value: min } : { distance: Math.max(slope, boundsMax) - max, value: max };
508
+ }
509
+ } else if (slope > -DEFAULT_OBLIQUE_ANGLE) {
510
+ if (min > slope && min <= 0) {
511
+ return { distance: min - slope, value: min };
512
+ } else {
513
+ return max < slope ? { distance: -max, value: max } : { distance: min - Math.min(slope, boundsMin), value: min };
426
514
  }
427
515
  } else {
428
- test = true;
516
+ return max < slope ? { distance: slope - max, value: max } : { distance: min - Math.min(slope, boundsMin), value: min };
429
517
  }
430
- return test;
431
518
  }
432
519
 
433
- function testFontStyle(fontStyle, otherFontStyle) {
434
- const angles = getFontStyleAngles(fontStyle);
435
- const otherAngles = getFontStyleAngles(otherFontStyle);
436
- if (angles && otherAngles) {
437
- return angles[0] <= otherAngles[1] && otherAngles[0] <= angles[1];
520
+ function getWebKitObliqueStyleDistance(fontStyle, style, bounds) {
521
+ if (fontStyle.italic && !style.italic) {
522
+ return { distance: Infinity };
438
523
  } else {
439
- return normalizeFontStyle(fontStyle) == normalizeFontStyle(otherFontStyle);
524
+ return getWebKitStyleDistance(fontStyle, style, bounds);
440
525
  }
441
526
  }
442
527
 
443
- function getFontStyleAngles(fontStyle) {
444
- fontStyle = normalizeFontStyle(fontStyle);
445
- if (fontStyle == "normal") {
446
- return [0, 0];
447
- } else if (fontStyle == "italic") {
448
- return [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE];
449
- } else if (fontStyle == "oblique" || fontStyle.startsWith("oblique ")) {
450
- const angles = (fontStyle.match(REGEXP_FONT_STYLE_ANGLE) || [])
451
- .map(angle => Number.parseFloat(angle))
452
- .filter(angle => !Number.isNaN(angle));
453
- return angles.length ? [Math.min(...angles), Math.max(...angles)] : [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE];
528
+ function getWebKitWeightDistance([min, max], weight, [boundsMin, boundsMax]) {
529
+ if (testRange([min, max], weight)) {
530
+ return { distance: 0, value: weight };
531
+ } else if (weight >= LOWER_WEIGHT_SEARCH_THRESHOLD && weight <= UPPER_WEIGHT_SEARCH_THRESHOLD) {
532
+ if (min > weight && min <= UPPER_WEIGHT_SEARCH_THRESHOLD) {
533
+ return { distance: min - weight, value: min };
534
+ } else {
535
+ return max < weight ? { distance: UPPER_WEIGHT_SEARCH_THRESHOLD - max, value: max } : { distance: min - Math.min(weight, boundsMin), value: min };
536
+ }
537
+ } else if (weight < LOWER_WEIGHT_SEARCH_THRESHOLD) {
538
+ return max < weight ? { distance: weight - max, value: max } : { distance: min - Math.min(weight, boundsMin), value: min };
539
+ } else {
540
+ return min > weight ? { distance: min - weight, value: min } : { distance: Math.max(weight, boundsMax) - max, value: max };
454
541
  }
455
542
  }
456
543
 
457
- function normalizeFontStyle(fontStyle) {
458
- return String(fontStyle || "normal").trim().toLowerCase().replace(REGEXP_SPACES, " ");
544
+ function getGeckoStretchDistance([min, max], stretch) {
545
+ if (stretch < min) {
546
+ return { distance: min - stretch + (stretch > NORMAL_FONT_STRETCH ? 0 : GECKO_REVERSE_STRETCH_DISTANCE) };
547
+ } else if (stretch > max) {
548
+ return { distance: stretch - max + (stretch <= NORMAL_FONT_STRETCH ? 0 : GECKO_REVERSE_STRETCH_DISTANCE) };
549
+ } else {
550
+ return { distance: 0 };
551
+ }
459
552
  }
460
553
 
461
- function testFontweight(fontWeight, usedFontWeights) {
462
- let test;
463
- for (const fontWeightValue of fontWeight.split(",")) {
464
- let { min: fontWeightMin, max: fontWeightMax } = parseFontWeight(fontWeightValue);
465
- if (!fontWeightMax) {
466
- fontWeightMax = fontWeightMin;
554
+ function getGeckoStyleDistance({ italic, range: [min, max] }, style) {
555
+ const [angle] = style.range;
556
+ if (italic ? style.italic : !style.italic && (angle == min || angle == max)) {
557
+ return { distance: 0 };
558
+ } else if (!style.italic && angle == 0) {
559
+ if (italic) {
560
+ return { distance: GECKO_BAD_STYLE_DISTANCE };
561
+ } else if (min >= 0) {
562
+ return { distance: min };
563
+ } else {
564
+ return { distance: max >= 0 ? 0 : GECKO_NEGATE_STYLE_DISTANCE - max };
467
565
  }
468
- test = test || usedFontWeights.find(usedFontWeight => {
469
- let { min: usedFontWeightMin, max: usedFontWeightMax } = parseFontWeight(usedFontWeight);
470
- if (!usedFontWeightMax) {
471
- usedFontWeightMax = usedFontWeightMin;
472
- }
473
- return usedFontWeightMin >= fontWeightMin && usedFontWeightMax <= fontWeightMax;
474
- });
566
+ } else if (style.italic) {
567
+ if (min >= DEFAULT_OBLIQUE_ANGLE) {
568
+ return { distance: min - DEFAULT_OBLIQUE_ANGLE + 1 };
569
+ } else if (max >= DEFAULT_OBLIQUE_ANGLE) {
570
+ return { distance: 1 };
571
+ } else {
572
+ return { distance: (max > 0 ? GECKO_REVERSE_STYLE_DISTANCE : GECKO_REVERSE_STYLE_DISTANCE + GECKO_NEGATE_STYLE_DISTANCE) + DEFAULT_OBLIQUE_ANGLE - max };
573
+ }
574
+ } else if (italic) {
575
+ return { distance: GECKO_BAD_STYLE_DISTANCE };
576
+ } else if (angle >= DEFAULT_OBLIQUE_ANGLE || angle <= -DEFAULT_OBLIQUE_ANGLE) {
577
+ const sign = Math.sign(angle);
578
+ const [signedMin, signedMax] = sign > 0 ? [min, max] : [-max, -min];
579
+ const signedAngle = sign * angle;
580
+ if (signedMin >= signedAngle) {
581
+ return { distance: signedMin - signedAngle };
582
+ } else if (signedMax >= signedAngle) {
583
+ return { distance: 0 };
584
+ } else {
585
+ return { distance: (signedMax > 0 ? GECKO_REVERSE_STYLE_DISTANCE : GECKO_REVERSE_STYLE_DISTANCE + GECKO_NEGATE_STYLE_DISTANCE) + signedAngle - signedMax };
586
+ }
587
+ } else {
588
+ const sign = angle > 0 ? 1 : -1;
589
+ const [signedMin, signedMax] = sign > 0 ? [min, max] : [-max, -min];
590
+ const signedAngle = sign * angle;
591
+ if (signedMin > signedAngle) {
592
+ return { distance: GECKO_REVERSE_STYLE_DISTANCE + signedMin - signedAngle };
593
+ } else if (signedMax >= signedAngle) {
594
+ return { distance: 0 };
595
+ } else {
596
+ return { distance: (signedMax > 0 ? 0 : GECKO_REVERSE_STYLE_DISTANCE + GECKO_NEGATE_STYLE_DISTANCE) + signedAngle - signedMax };
597
+ }
598
+ }
599
+ }
600
+
601
+ function getGeckoWeightDistance([min, max], weight) {
602
+ if (testRange([min, max], weight)) {
603
+ return { distance: 0 };
604
+ } else if (weight < LOWER_WEIGHT_SEARCH_THRESHOLD) {
605
+ return { distance: max < weight ? weight - max : min - weight + GECKO_REVERSE_WEIGHT_DISTANCE };
606
+ } else if (weight > UPPER_WEIGHT_SEARCH_THRESHOLD) {
607
+ return { distance: min > weight ? min - weight : weight - max + GECKO_REVERSE_WEIGHT_DISTANCE };
608
+ } else if (min > weight) {
609
+ return { distance: min - weight + (min <= UPPER_WEIGHT_SEARCH_THRESHOLD ? 0 : GECKO_REVERSE_WEIGHT_DISTANCE) };
610
+ } else {
611
+ return { distance: weight - max + GECKO_LIGHTER_WEIGHT_DISTANCE };
475
612
  }
476
- return test;
613
+ }
614
+
615
+ function testRange([min, max], value) {
616
+ return min <= value && value <= max;
477
617
  }
478
618
 
479
619
  function parseFontWeight(fontWeight) {
480
- const fontWeightValues = fontWeight.split(" ");
481
- const min = Number.parseInt(helper.getFontWeight(fontWeightValues[0]), 10);
482
- const max = fontWeightValues[1] && Number.parseInt(helper.getFontWeight(fontWeightValues[1]), 10);
483
- return {
484
- min, max
485
- };
620
+ return parseRange(fontWeight, value => Number(helper.getFontWeight(value)));
621
+ }
622
+
623
+ function parseFontStretch(fontStretch) {
624
+ return parseRange(fontStretch, value => {
625
+ value = helper.getFontStretch(value.toLowerCase());
626
+ return value.endsWith("%") ? Number(value.slice(0, -1)) : NaN;
627
+ });
628
+ }
629
+
630
+ function parseFontStyle(fontStyle) {
631
+ const values = String(fontStyle).trim().toLowerCase().split(REGEXP_SPACES);
632
+ if (values.length == 1 && values[0] == NORMAL_FONT_STYLE) {
633
+ return { italic: false, range: [0, 0] };
634
+ } else if (values.length == 1 && values[0] == ITALIC_FONT_STYLE) {
635
+ return { italic: true, range: [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE] };
636
+ } else if (values[0] == OBLIQUE_FONT_STYLE) {
637
+ const range = values.length == 1 ? [DEFAULT_OBLIQUE_ANGLE, DEFAULT_OBLIQUE_ANGLE] : parseRange(values.slice(1).join(" "), parseAngle);
638
+ if (range) {
639
+ return { italic: false, range };
640
+ }
641
+ }
642
+ }
643
+
644
+ function parseAngle(angle) {
645
+ const match = angle.match(REGEXP_ANGLE);
646
+ if (match && (match[2] || Number(match[1]) == 0)) {
647
+ return Number(match[1]) * ANGLE_UNITS[match[2] || "deg"];
648
+ } else {
649
+ return NaN;
650
+ }
651
+ }
652
+
653
+ function parseRange(value, parseValue) {
654
+ const values = String(value).trim().split(REGEXP_SPACES).map(parseValue);
655
+ if (values.length && values.length <= 2 && values.every(Number.isFinite)) {
656
+ return [Math.min(...values), Math.max(...values)];
657
+ }
486
658
  }
487
659
 
488
660
  function getDeclarationValue(declarations, propertyName) {
@@ -595,51 +767,6 @@ function parseFamilyNames(fontFamilyNameTokenData, fontFamilyNames) {
595
767
  }
596
768
  }
597
769
 
598
- function getUsedFontWeight(fontInfo, fontStyle, fontWeights) {
599
- let foundWeight;
600
- fontWeights = fontWeights.map(weights => weights.map(value => String(Number.parseInt(value, 10))));
601
- if (testFontStyle(fontInfo[2], fontStyle)) {
602
- let fontWeight = Number(fontInfo[1]);
603
- if (fontWeights.length > 1) {
604
- if (fontWeight >= 400 && fontWeight <= 500) {
605
- foundWeight = fontWeights.find(weights => weights[0] >= fontWeight && weights[0] <= 500);
606
- if (!foundWeight) {
607
- foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
608
- }
609
- if (!foundWeight) {
610
- foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
611
- }
612
- }
613
- if (fontWeight < 400) {
614
- foundWeight = fontWeights.slice().reverse().find(weights => weights[weights.length - 1] <= fontWeight);
615
- if (!foundWeight) {
616
- foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
617
- }
618
- }
619
- if (fontWeight > 500) {
620
- foundWeight = fontWeights.find(weights => weights[0] >= fontWeight);
621
- if (!foundWeight) {
622
- foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
623
- }
624
- }
625
- if (!foundWeight) {
626
- foundWeight = fontWeights.find(weights => weights[0] <= fontWeight && weights[weights.length - 1] >= fontWeight);
627
- }
628
- } else {
629
- foundWeight = fontWeights[0];
630
- }
631
- }
632
- return foundWeight ? foundWeight.join(" ") : undefined;
633
- }
634
-
635
- function findDescendingFontWeight(fontWeight, fontWeights) {
636
- return fontWeights.slice().reverse().find(weights => weights[weights.length - 1] < fontWeight);
637
- }
638
-
639
- function findAscendingFontWeight(fontWeight, fontWeights) {
640
- return fontWeights.find(weights => weights[0] > fontWeight);
641
- }
642
-
643
770
  function getRulesTextContent(doc, cssRules, workStylesheet, content) {
644
771
  cssRules.forEach(ruleData => {
645
772
  if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
@@ -710,13 +837,14 @@ function getLaterUnicodeRanges(declaredFonts, ruleData) {
710
837
  if (index == -1) {
711
838
  return [];
712
839
  }
713
- const { fontFamily, fontWeight, fontStyle } = declaredFonts[index];
840
+ const { fontFamily, fontWeight, fontStyle, fontStretch } = declaredFonts[index];
714
841
  return declaredFonts
715
842
  .slice(index + 1)
716
843
  .filter(fontInfo => fontInfo.ruleData != ruleData &&
717
844
  fontInfo.fontFamily == fontFamily &&
718
845
  fontInfo.fontWeight == fontWeight &&
719
- fontInfo.fontStyle == fontStyle)
846
+ fontInfo.fontStyle == fontStyle &&
847
+ fontInfo.fontStretch == fontStretch)
720
848
  .map(fontInfo => fontInfo.unicodeRange);
721
849
  }
722
850
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.18",
3
+ "version": "1.6.20",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -121,9 +121,22 @@ const FONT_DESCRIPTOR_KEYS = new Set([
121
121
  "stretch"
122
122
  ]);
123
123
 
124
+ const ANGLE_UNITS = new Set([
125
+ "deg",
126
+ "grad",
127
+ "rad",
128
+ "turn"
129
+ ]);
130
+
124
131
  const OPERATOR_TYPE = "Operator";
125
132
  const IDENTIFIER_TYPE = "Identifier";
133
+ const NUMBER_TYPE = "Number";
134
+ const DIMENSION_TYPE = "Dimension";
135
+ const FUNCTION_TYPE = "Function";
126
136
  const NORMAL_KEYWORD = "normal";
137
+ const OBLIQUE_KEYWORD = "oblique";
138
+ const MIN_FONT_WEIGHT = 1;
139
+ const MAX_FONT_WEIGHT = 1000;
127
140
  const LINE_HEIGHT_SEPARATOR = "/";
128
141
  const FAMILY_SEPARATOR = ",";
129
142
 
@@ -142,7 +155,24 @@ function parse(value) {
142
155
  if (GLOBAL_KEYWORDS.has(stringValueLower)) {
143
156
  return { global: stringValue };
144
157
  }
145
- const tokens = value.children;
158
+ const fonts = [];
159
+ let parseError;
160
+ [true, false].forEach(descriptorsFirst => {
161
+ try {
162
+ fonts.push(parseFont(value.children, descriptorsFirst));
163
+ } catch (error) {
164
+ parseError = error;
165
+ }
166
+ });
167
+ if (!fonts.length) {
168
+ throw parseError;
169
+ }
170
+ const [font] = fonts;
171
+ font.family = Array.from(new Set(fonts.flatMap(parsedFont => parsedFont.family)));
172
+ return font;
173
+ }
174
+
175
+ function parseFont(tokens, descriptorsFirst) {
146
176
  const font = {
147
177
  lineHeight: NORMAL_KEYWORD,
148
178
  stretch: NORMAL_KEYWORD,
@@ -154,7 +184,8 @@ function parse(value) {
154
184
  for (let tokenNode = tokens.head; tokenNode; tokenNode = tokenNode.next) {
155
185
  const tokenRaw = tokenNode.data.name || tokenNode.data.value || cssTree.generate(tokenNode.data);
156
186
  const token = tokenRaw.toLowerCase();
157
- if (token === NORMAL_KEYWORD) {
187
+ const keyword = tokenNode.data.type == IDENTIFIER_TYPE || tokenNode.data.type == NUMBER_TYPE;
188
+ if (keyword && token === NORMAL_KEYWORD) {
158
189
  FONT_DESCRIPTOR_KEYS.forEach((prop) => {
159
190
  if (!seen[prop]) {
160
191
  font[prop] = tokenRaw;
@@ -162,28 +193,38 @@ function parse(value) {
162
193
  });
163
194
  continue;
164
195
  }
165
- if (FONT_WEIGHT_KEYWORDS.has(token)) {
196
+ if (keyword && FONT_WEIGHT_KEYWORDS.has(token)) {
166
197
  if (!seen.weight) {
167
198
  font.weight = tokenRaw;
168
199
  seen.weight = true;
169
200
  }
170
201
  continue;
171
202
  }
172
- if (FONT_STYLE_KEYWORDS.has(token)) {
203
+ if (descriptorsFirst && !seen.weight && ((tokenNode.data.type == NUMBER_TYPE && Number(token) >= MIN_FONT_WEIGHT && Number(token) <= MAX_FONT_WEIGHT) || tokenNode.data.type == FUNCTION_TYPE)) {
204
+ font.weight = tokenRaw;
205
+ seen.weight = true;
206
+ continue;
207
+ }
208
+ if (keyword && FONT_STYLE_KEYWORDS.has(token)) {
173
209
  if (!seen.style) {
174
210
  font.style = tokenRaw;
175
211
  seen.style = true;
212
+ const nextToken = tokenNode.next && tokenNode.next.data;
213
+ if (token == OBLIQUE_KEYWORD && nextToken && ((nextToken.type == DIMENSION_TYPE && ANGLE_UNITS.has(nextToken.unit.toLowerCase())) || (descriptorsFirst && nextToken.type == FUNCTION_TYPE))) {
214
+ font.style += " " + cssTree.generate(nextToken);
215
+ tokenNode = tokenNode.next;
216
+ }
176
217
  }
177
218
  continue;
178
219
  }
179
- if (FONT_VARIANT_KEYWORDS.has(token)) {
220
+ if (keyword && FONT_VARIANT_KEYWORDS.has(token)) {
180
221
  if (!seen.variant) {
181
222
  font.variant = tokenRaw;
182
223
  seen.variant = true;
183
224
  }
184
225
  continue;
185
226
  }
186
- if (FONT_STRETCH_KEYWORDS.has(token)) {
227
+ if (keyword && FONT_STRETCH_KEYWORDS.has(token)) {
187
228
  if (!seen.stretch) {
188
229
  font.stretch = tokenRaw;
189
230
  seen.stretch = true;