single-file-core 1.2.24 → 1.2.26

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/index.js CHANGED
@@ -1324,7 +1324,7 @@ class Processor {
1324
1324
  }
1325
1325
 
1326
1326
  replaceStylesheets() {
1327
- this.processorHelper.replaceStylesheets(this.doc, this.stylesheets, this.resources, this.options);
1327
+ this.processorHelper.replaceStylesheets(this.doc, this.stylesheets, this.options, this.resources);
1328
1328
  }
1329
1329
 
1330
1330
  replaceStyleAttributes() {
@@ -21,6 +21,11 @@
21
21
  * Source.
22
22
  */
23
23
 
24
+ import {
25
+ normalizeFontFamily,
26
+ getFontWeight
27
+ } from "./../helper.js";
28
+
24
29
  const DATA_URI_PREFIX = "data:";
25
30
  const ABOUT_BLANK_URI = "about:blank";
26
31
  const REGEXP_URL_HASH = /(#.+?)$/;
@@ -31,6 +36,29 @@ const EMPTY_URL = /^https?:\/\/+\s*$/;
31
36
  const NOT_EMPTY_URL = /^(https?:\/\/|file:\/\/|blob:).+/;
32
37
  const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
33
38
  const UTF8_CHARSET = "utf-8";
39
+ const REGEXP_URL_FUNCTION = /(url|local|-sf-url-original)\(.*?\)\s*(,|$)/g;
40
+ const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
41
+ const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
42
+ const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
43
+ const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
44
+ const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
45
+ const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
46
+ const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
47
+ const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
48
+ const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
49
+ const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
50
+ const MEDIA_ALL = "all";
51
+ const FONT_STRETCHES = {
52
+ "ultra-condensed": "50%",
53
+ "extra-condensed": "62.5%",
54
+ "condensed": "75%",
55
+ "semi-condensed": "87.5%",
56
+ "normal": "100%",
57
+ "semi-expanded": "112.5%",
58
+ "expanded": "125%",
59
+ "extra-expanded": "150%",
60
+ "ultra-expanded": "200%"
61
+ };
34
62
 
35
63
  let util, cssTree;
36
64
 
@@ -56,6 +84,164 @@ function getProcessorHelperCommonClass(utilInstance, cssTreeInstance) {
56
84
  }
57
85
 
58
86
  class ProcessorHelperCommon {
87
+ async processPageResources(doc, baseURI, options, resources, styles, batchRequest) {
88
+ const processAttributeArgs = [
89
+ ["link[href][rel*=\"icon\"]", "href", true],
90
+ ["object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"], object[data*=\".svg\"]", "data"],
91
+ ["img[src], input[src][type=image]", "src", false, true],
92
+ ["embed[src*=\".svg\"]", "src"],
93
+ ["video[poster]", "poster"],
94
+ ["*[background]", "background"],
95
+ ["image", "xlink:href"],
96
+ ["image", "href"]
97
+ ];
98
+ if (options.blockImages) {
99
+ doc.querySelectorAll("svg").forEach(element => element.remove());
100
+ }
101
+ let resourcePromises = processAttributeArgs.map(([selector, attributeName, removeElementIfMissing, processDuplicates]) =>
102
+ this.processAttribute(doc.querySelectorAll(selector), attributeName, baseURI, options, "image", resources, removeElementIfMissing, batchRequest, styles, processDuplicates)
103
+ );
104
+ resourcePromises = resourcePromises.concat([
105
+ this.processXLinks(doc.querySelectorAll("use"), doc, baseURI, options, batchRequest),
106
+ this.processSrcset(doc.querySelectorAll("img[srcset], source[srcset]"), baseURI, options, resources, batchRequest)
107
+ ]);
108
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("object[data*=\".pdf\"]"), "data", baseURI, options, null, resources, batchRequest, styles));
109
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("embed[src*=\".pdf\"]"), "src", baseURI, options, null, resources, batchRequest, styles));
110
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("audio[src], audio > source[src]"), "src", baseURI, options, "audio", resources, batchRequest, styles));
111
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("video[src], video > source[src]"), "src", baseURI, options, "video", resources, batchRequest, styles));
112
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("audio track[src], video track[src]"), "src", baseURI, options, null, resources, batchRequest, styles));
113
+ resourcePromises.push(this.processAttribute(doc.querySelectorAll("model[src]"), "src", baseURI, options, null, resources, batchRequest, styles));
114
+ await Promise.all(resourcePromises);
115
+ if (options.saveFavicon) {
116
+ this.processShortcutIcons(doc);
117
+ }
118
+ }
119
+
120
+ async processXLinks(resourceElements, doc, baseURI, options, batchRequest) {
121
+ let attributeName = "xlink:href";
122
+ await Promise.all(Array.from(resourceElements).map(async resourceElement => {
123
+ let originalResourceURL = resourceElement.getAttribute(attributeName);
124
+ if (originalResourceURL == null) {
125
+ attributeName = "href";
126
+ originalResourceURL = resourceElement.getAttribute(attributeName);
127
+ }
128
+ if (options.saveOriginalURLs && !isDataURL(originalResourceURL)) {
129
+ resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
130
+ }
131
+ let resourceURL = normalizeURL(originalResourceURL);
132
+ if (!options.blockImages) {
133
+ if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
134
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
135
+ try {
136
+ resourceURL = util.resolveURL(resourceURL, baseURI);
137
+ } catch (error) {
138
+ // ignored
139
+ }
140
+ if (testValidURL(resourceURL)) {
141
+ const hashMatch = originalResourceURL.match(REGEXP_URL_HASH);
142
+ if (originalResourceURL.startsWith(baseURI + "#")) {
143
+ resourceElement.setAttribute(attributeName, hashMatch[0]);
144
+ } else {
145
+ const response = await batchRequest.addURL(resourceURL, { expectedType: "image" });
146
+ const svgDoc = util.parseSVGContent(response.content);
147
+ if (hashMatch && hashMatch[0]) {
148
+ let symbolElement;
149
+ try {
150
+ symbolElement = svgDoc.querySelector(hashMatch[0]);
151
+ } catch (error) {
152
+ // ignored
153
+ }
154
+ if (symbolElement) {
155
+ resourceElement.setAttribute(attributeName, hashMatch[0]);
156
+ resourceElement.parentElement.insertBefore(symbolElement, resourceElement.parentElement.firstChild);
157
+ }
158
+ } else {
159
+ resourceElement.setAttribute(attributeName, PREFIX_DATA_URI_IMAGE_SVG + "," + response.content);
160
+ }
161
+ }
162
+ }
163
+ } else if (resourceURL == options.url) {
164
+ resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
165
+ }
166
+ } else {
167
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
168
+ }
169
+ }));
170
+ }
171
+
172
+ async processStylesheet(cssRules, baseURI, options, resources, batchRequest) {
173
+ const promises = [];
174
+ const removedRules = [];
175
+ const processorHelper = this;
176
+ for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
177
+ const ruleData = cssRule.data;
178
+ if (ruleData.type == "Atrule" && ruleData.name == "charset") {
179
+ removedRules.push(cssRule);
180
+ } else if (ruleData.block && ruleData.block.children) {
181
+ if (ruleData.type == "Rule") {
182
+ promises.push(processorHelper.processStyle(ruleData, options, resources, batchRequest));
183
+ } else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
184
+ promises.push(processorHelper.processStylesheet(ruleData.block.children, baseURI, options, resources, batchRequest));
185
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
186
+ promises.push(processFontFaceRule(ruleData));
187
+ }
188
+ }
189
+ }
190
+ removedRules.forEach(cssRule => cssRules.remove(cssRule));
191
+ await Promise.all(promises);
192
+
193
+ async function processFontFaceRule(ruleData) {
194
+ const urls = getUrlFunctions(ruleData);
195
+ await Promise.all(urls.map(async urlNode => {
196
+ const originalResourceURL = urlNode.value;
197
+ if (!options.blockFonts) {
198
+ const resourceURL = normalizeURL(originalResourceURL);
199
+ if (!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) {
200
+ await processorHelper.processFont(resourceURL, urlNode, originalResourceURL, baseURI, options, resources, batchRequest);
201
+ }
202
+ } else {
203
+ urlNode.value = util.EMPTY_RESOURCE;
204
+ }
205
+ }));
206
+ }
207
+ }
208
+
209
+ async processSrcset(resourceElements, baseURI, options, resources, batchRequest) {
210
+ await Promise.all(Array.from(resourceElements).map(async resourceElement => {
211
+ const originSrcset = resourceElement.getAttribute("srcset");
212
+ const srcset = util.parseSrcset(originSrcset);
213
+ if (options.saveOriginalURLs && !isDataURL(originSrcset)) {
214
+ resourceElement.setAttribute("data-sf-original-srcset", originSrcset);
215
+ }
216
+ if (!options.blockImages) {
217
+ const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
218
+ let resourceURL = normalizeURL(srcsetValue.url);
219
+ if (!testIgnoredPath(resourceURL)) {
220
+ if (testValidPath(resourceURL)) {
221
+ try {
222
+ resourceURL = util.resolveURL(resourceURL, baseURI);
223
+ } catch (error) {
224
+ // ignored
225
+ }
226
+ if (testValidURL(resourceURL)) {
227
+ await this.processImageSrcset(resourceURL, srcsetValue, resources, batchRequest);
228
+ } else {
229
+ return "";
230
+ }
231
+ } else {
232
+ return "";
233
+ }
234
+ } else {
235
+ return resourceURL + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
236
+ }
237
+ }));
238
+ resourceElement.setAttribute("srcset", srcsetValues.join(", "));
239
+ } else {
240
+ resourceElement.setAttribute("srcset", "");
241
+ }
242
+ }));
243
+ }
244
+
59
245
  setBackgroundImage(element, url, style) {
60
246
  element.style.setProperty("background-blend-mode", "normal", "important");
61
247
  element.style.setProperty("background-clip", "content-box", "important");
@@ -201,56 +387,189 @@ class ProcessorHelperCommon {
201
387
  });
202
388
  }
203
389
 
204
- async processXLinks(resourceElements, doc, baseURI, options, batchRequest) {
205
- let attributeName = "xlink:href";
206
- await Promise.all(Array.from(resourceElements).map(async resourceElement => {
207
- let originalResourceURL = resourceElement.getAttribute(attributeName);
208
- if (originalResourceURL == null) {
209
- attributeName = "href";
210
- originalResourceURL = resourceElement.getAttribute(attributeName);
390
+ async removeAlternativeFonts(doc, stylesheets, fonts, fontTests) {
391
+ const fontsDetails = {
392
+ fonts: new Map(),
393
+ medias: new Map(),
394
+ supports: new Map()
395
+ };
396
+ const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
397
+ let sheetIndex = 0;
398
+ stylesheets.forEach(stylesheetInfo => {
399
+ const cssRules = stylesheetInfo.stylesheet.children;
400
+ if (cssRules) {
401
+ stats.rules.processed += cssRules.size;
402
+ stats.rules.discarded += cssRules.size;
403
+ if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
404
+ const mediaFontsDetails = this.createFontsDetailsInfo();
405
+ fontsDetails.medias.set("media-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaFontsDetails);
406
+ this.getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails);
407
+ } else {
408
+ this.getFontsDetails(doc, cssRules, sheetIndex, fontsDetails);
409
+ }
211
410
  }
212
- if (options.saveOriginalURLs && !isDataURL(originalResourceURL)) {
213
- resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
411
+ sheetIndex++;
412
+ });
413
+ processFontDetails(fontsDetails);
414
+ await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
415
+ const cssRules = stylesheetInfo.stylesheet.children;
416
+ const media = stylesheetInfo.mediaText;
417
+ if (cssRules) {
418
+ if (media && media != MEDIA_ALL) {
419
+ await this.processFontFaceRules(cssRules, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + media), fonts, fontTests, stats);
420
+ } else {
421
+ await this.processFontFaceRules(cssRules, sheetIndex, fontsDetails, fonts, fontTests, stats);
422
+ }
423
+ stats.rules.discarded -= cssRules.size;
214
424
  }
215
- let resourceURL = normalizeURL(originalResourceURL);
216
- if (!options.blockImages) {
217
- if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
218
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
219
- try {
220
- resourceURL = util.resolveURL(resourceURL, baseURI);
221
- } catch (error) {
222
- // ignored
425
+ }));
426
+ return stats;
427
+ }
428
+
429
+ async processFontFaceRules(cssRules, sheetIndex, fontsDetails, fonts, fontTests, stats) {
430
+ const removedRules = [];
431
+ let mediaIndex = 0, supportsIndex = 0;
432
+ for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
433
+ const ruleData = cssRule.data;
434
+ if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
435
+ const mediaText = cssTree.generate(ruleData.prelude);
436
+ await this.processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText), fonts, fontTests, stats);
437
+ mediaIndex++;
438
+ } else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
439
+ const supportsText = cssTree.generate(ruleData.prelude);
440
+ await this.processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.supports.get("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText), fonts, fontTests, stats);
441
+ supportsIndex++;
442
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
443
+ const key = this.getFontKey(ruleData);
444
+ const fontInfo = fontsDetails.fonts.get(key);
445
+ if (fontInfo) {
446
+ const processed = await this.processFontFaceRule(ruleData, fontInfo, fonts, fontTests, stats);
447
+ if (processed) {
448
+ fontsDetails.fonts.delete(key);
223
449
  }
224
- if (testValidURL(resourceURL)) {
225
- const hashMatch = originalResourceURL.match(REGEXP_URL_HASH);
226
- if (originalResourceURL.startsWith(baseURI + "#")) {
227
- resourceElement.setAttribute(attributeName, hashMatch[0]);
228
- } else {
229
- const response = await batchRequest.addURL(resourceURL, { expectedType: "image" });
230
- const svgDoc = util.parseSVGContent(response.content);
231
- if (hashMatch && hashMatch[0]) {
232
- let symbolElement;
233
- try {
234
- symbolElement = svgDoc.querySelector(hashMatch[0]);
235
- } catch (error) {
236
- // ignored
237
- }
238
- if (symbolElement) {
239
- resourceElement.setAttribute(attributeName, hashMatch[0]);
240
- resourceElement.parentElement.insertBefore(symbolElement, resourceElement.parentElement.firstChild);
241
- }
242
- } else {
243
- resourceElement.setAttribute(attributeName, PREFIX_DATA_URI_IMAGE_SVG + "," + response.content);
244
- }
245
- }
450
+ } else {
451
+ removedRules.push(cssRule);
452
+ }
453
+ }
454
+ }
455
+ removedRules.forEach(cssRule => cssRules.remove(cssRule));
456
+ }
457
+
458
+ getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
459
+ let mediaIndex = 0, supportsIndex = 0;
460
+ cssRules.forEach(ruleData => {
461
+ if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
462
+ const mediaText = cssTree.generate(ruleData.prelude);
463
+ const fontsDetails = this.createFontsDetailsInfo();
464
+ mediaFontsDetails.medias.set("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, fontsDetails);
465
+ mediaIndex++;
466
+ this.getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
467
+ } else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
468
+ const supportsText = cssTree.generate(ruleData.prelude);
469
+ const fontsDetails = this.createFontsDetailsInfo();
470
+ mediaFontsDetails.supports.set("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText, fontsDetails);
471
+ supportsIndex++;
472
+ this.getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
473
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
474
+ const fontKey = this.getFontKey(ruleData);
475
+ let fontInfo = mediaFontsDetails.fonts.get(fontKey);
476
+ if (!fontInfo) {
477
+ fontInfo = [];
478
+ mediaFontsDetails.fonts.set(fontKey, fontInfo);
479
+ }
480
+ const src = this.getPropertyValue(ruleData, "src");
481
+ if (src) {
482
+ const fontSources = src.match(REGEXP_URL_FUNCTION);
483
+ if (fontSources) {
484
+ fontSources.forEach(source => fontInfo.unshift(source));
246
485
  }
247
- } else if (resourceURL == options.url) {
248
- resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
249
486
  }
487
+ }
488
+ });
489
+ }
490
+
491
+ createFontsDetailsInfo() {
492
+ return {
493
+ fonts: new Map(),
494
+ medias: new Map(),
495
+ supports: new Map()
496
+ };
497
+ }
498
+
499
+ getFontKey(ruleData) {
500
+ return JSON.stringify([
501
+ normalizeFontFamily(this.getPropertyValue(ruleData, "font-family")),
502
+ getFontWeight(this.getPropertyValue(ruleData, "font-weight") || "400"),
503
+ this.getPropertyValue(ruleData, "font-style") || "normal",
504
+ this.getPropertyValue(ruleData, "unicode-range"),
505
+ getFontStretch(this.getPropertyValue(ruleData, "font-stretch")),
506
+ this.getPropertyValue(ruleData, "font-variant") || "normal",
507
+ this.getPropertyValue(ruleData, "font-feature-settings"),
508
+ this.getPropertyValue(ruleData, "font-variation-settings")
509
+ ]);
510
+ }
511
+
512
+ getPropertyValue(ruleData, propertyName) {
513
+ let property;
514
+ if (ruleData.block.children) {
515
+ property = ruleData.block.children.filter(node => {
516
+ try {
517
+ return node.property == propertyName && !cssTree.generate(node.value).match(/\\9$/);
518
+ } catch (error) {
519
+ return node.property == propertyName;
520
+ }
521
+ }).tail;
522
+ }
523
+ if (property) {
524
+ try {
525
+ return cssTree.generate(property.data.value);
526
+ } catch (error) {
527
+ // ignored
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ function processFontDetails(fontsDetails, fontResources) {
534
+ fontsDetails.fonts.forEach((fontInfo, fontKey) => {
535
+ fontsDetails.fonts.set(fontKey, fontInfo.map(fontSource => {
536
+ const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
537
+ let fontFormat;
538
+ const fontUrl = getURL(fontSource);
539
+ if (fontFormatMatch && fontFormatMatch[1]) {
540
+ fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
541
+ }
542
+ if (!fontFormat) {
543
+ const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
544
+ if (fontFormatMatch && fontFormatMatch[1]) {
545
+ fontFormat = fontFormatMatch[1];
546
+ } else {
547
+ const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
548
+ if (fontFormatMatch && fontFormatMatch[1]) {
549
+ fontFormat = fontFormatMatch[1];
550
+ }
551
+ }
552
+ }
553
+ if (!fontFormat && fontUrl) {
554
+ const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
555
+ if (fontFormatMatch && fontFormatMatch[1]) {
556
+ fontFormat = fontFormatMatch[1];
557
+ }
558
+ }
559
+ if (fontResources) {
560
+ const fontResource = Array.from(fontResources.values()).find(info => info.name == fontUrl);
561
+ return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat, contentType: fontResource && fontResource.contentType };
250
562
  } else {
251
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
563
+ return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
252
564
  }
253
565
  }));
566
+ });
567
+ if (fontResources) {
568
+ fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails, fontResources));
569
+ fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails, fontResources));
570
+ } else {
571
+ fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails));
572
+ fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails));
254
573
  }
255
574
  }
256
575
 
@@ -319,4 +638,16 @@ function testValidPath(resourceURL) {
319
638
 
320
639
  function testValidURL(resourceURL) {
321
640
  return testValidPath(resourceURL) && (resourceURL.match(HTTP_URI_PREFIX) || resourceURL.match(FILE_URI_PREFIX) || resourceURL.startsWith(BLOB_URI_PREFIX)) && resourceURL.match(NOT_EMPTY_URL);
641
+ }
642
+
643
+ function getURL(urlFunction) {
644
+ urlFunction = urlFunction.replace(/url\(-sf-url-original\\\(\\"(.*?)\\"\\\)\\ /g, "");
645
+ const urlMatch = urlFunction.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
646
+ urlFunction.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
647
+ urlFunction.match(REGEXP_URL_NO_QUOTES_FN);
648
+ return urlMatch && urlMatch[1];
649
+ }
650
+
651
+ function getFontStretch(stretch) {
652
+ return FONT_STRETCHES[stretch] || stretch;
322
653
  }