single-file-core 1.6.19 → 1.6.21

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.
@@ -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.19",
3
+ "version": "1.6.21",
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;