UNPKG

29.3 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.parseSrcset = parseSrcset;
7exports.parseSrc = parseSrc;
8exports.normalizeUrl = normalizeUrl;
9exports.requestify = requestify;
10exports.isUrlRequestable = isUrlRequestable;
11exports.stringifyRequest = stringifyRequest;
12exports.typeSrc = typeSrc;
13exports.typeSrcset = typeSrcset;
14exports.normalizeOptions = normalizeOptions;
15exports.pluginRunner = pluginRunner;
16exports.getFilter = getFilter;
17exports.getImportCode = getImportCode;
18exports.getModuleCode = getModuleCode;
19exports.getExportCode = getExportCode;
20exports.c0ControlCodesExclude = c0ControlCodesExclude;
21
22var _path = _interopRequireDefault(require("path"));
23
24var _HtmlSourceError = _interopRequireDefault(require("./HtmlSourceError"));
25
26function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
27
28function isASCIIWhitespace(character) {
29 return (// Horizontal tab
30 character === '\u0009' || // New line
31 character === '\u000A' || // Form feed
32 character === '\u000C' || // Carriage return
33 character === '\u000D' || // Space
34 character === '\u0020'
35 );
36} // (Don't use \s, to avoid matching non-breaking space)
37// eslint-disable-next-line no-control-regex
38
39
40const regexLeadingSpaces = /^[ \t\n\r\u000c]+/; // eslint-disable-next-line no-control-regex
41
42const regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/; // eslint-disable-next-line no-control-regex
43
44const regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/;
45const regexTrailingCommas = /[,]+$/;
46const regexNonNegativeInteger = /^\d+$/; // ( Positive or negative or unsigned integers or decimals, without or without exponents.
47// Must include at least one digit.
48// According to spec tests any decimal point must be followed by a digit.
49// No leading plus sign is allowed.)
50// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
51
52const regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
53
54function parseSrcset(input) {
55 // 1. Let input be the value passed to this algorithm.
56 const inputLength = input.length;
57 let url;
58 let descriptors;
59 let currentDescriptor;
60 let state;
61 let c; // 2. Let position be a pointer into input, initially pointing at the start
62 // of the string.
63
64 let position = 0;
65 let startUrlPosition; // eslint-disable-next-line consistent-return
66
67 function collectCharacters(regEx) {
68 let chars;
69 const match = regEx.exec(input.substring(position));
70
71 if (match) {
72 [chars] = match;
73 position += chars.length;
74 return chars;
75 }
76 } // 3. Let candidates be an initially empty source set.
77
78
79 const candidates = []; // 4. Splitting loop: Collect a sequence of characters that are space
80 // characters or U+002C COMMA characters. If any U+002C COMMA characters
81 // were collected, that is a parse error.
82 // eslint-disable-next-line no-constant-condition
83
84 while (true) {
85 collectCharacters(regexLeadingCommasOrSpaces); // 5. If position is past the end of input, return candidates and abort these steps.
86
87 if (position >= inputLength) {
88 if (candidates.length === 0) {
89 throw new Error('Must contain one or more image candidate strings');
90 } // (we're done, this is the sole return path)
91
92
93 return candidates;
94 } // 6. Collect a sequence of characters that are not space characters,
95 // and let that be url.
96
97
98 startUrlPosition = position;
99 url = collectCharacters(regexLeadingNotSpaces); // 7. Let descriptors be a new empty list.
100
101 descriptors = []; // 8. If url ends with a U+002C COMMA character (,), follow these substeps:
102 // (1). Remove all trailing U+002C COMMA characters from url. If this removed
103 // more than one character, that is a parse error.
104
105 if (url.slice(-1) === ',') {
106 url = url.replace(regexTrailingCommas, ''); // (Jump ahead to step 9 to skip tokenization and just push the candidate).
107
108 parseDescriptors();
109 } // Otherwise, follow these substeps:
110 else {
111 tokenize();
112 } // 16. Return to the step labeled splitting loop.
113
114 }
115 /**
116 * Tokenizes descriptor properties prior to parsing
117 * Returns undefined.
118 */
119
120
121 function tokenize() {
122 // 8.1. Descriptor tokenizer: Skip whitespace
123 collectCharacters(regexLeadingSpaces); // 8.2. Let current descriptor be the empty string.
124
125 currentDescriptor = ''; // 8.3. Let state be in descriptor.
126
127 state = 'in descriptor'; // eslint-disable-next-line no-constant-condition
128
129 while (true) {
130 // 8.4. Let c be the character at position.
131 c = input.charAt(position); // Do the following depending on the value of state.
132 // For the purpose of this step, "EOF" is a special character representing
133 // that position is past the end of input.
134 // In descriptor
135
136 if (state === 'in descriptor') {
137 // Do the following, depending on the value of c:
138 // Space character
139 // If current descriptor is not empty, append current descriptor to
140 // descriptors and let current descriptor be the empty string.
141 // Set state to after descriptor.
142 if (isASCIIWhitespace(c)) {
143 if (currentDescriptor) {
144 descriptors.push(currentDescriptor);
145 currentDescriptor = '';
146 state = 'after descriptor';
147 }
148 } // U+002C COMMA (,)
149 // Advance position to the next character in input. If current descriptor
150 // is not empty, append current descriptor to descriptors. Jump to the step
151 // labeled descriptor parser.
152 else if (c === ',') {
153 position += 1;
154
155 if (currentDescriptor) {
156 descriptors.push(currentDescriptor);
157 }
158
159 parseDescriptors();
160 return;
161 } // U+0028 LEFT PARENTHESIS (()
162 // Append c to current descriptor. Set state to in parens.
163 else if (c === '\u0028') {
164 currentDescriptor += c;
165 state = 'in parens';
166 } // EOF
167 // If current descriptor is not empty, append current descriptor to
168 // descriptors. Jump to the step labeled descriptor parser.
169 else if (c === '') {
170 if (currentDescriptor) {
171 descriptors.push(currentDescriptor);
172 }
173
174 parseDescriptors();
175 return; // Anything else
176 // Append c to current descriptor.
177 } else {
178 currentDescriptor += c;
179 }
180 } // In parens
181 else if (state === 'in parens') {
182 // U+0029 RIGHT PARENTHESIS ())
183 // Append c to current descriptor. Set state to in descriptor.
184 if (c === ')') {
185 currentDescriptor += c;
186 state = 'in descriptor';
187 } // EOF
188 // Append current descriptor to descriptors. Jump to the step labeled
189 // descriptor parser.
190 else if (c === '') {
191 descriptors.push(currentDescriptor);
192 parseDescriptors();
193 return;
194 } // Anything else
195 // Append c to current descriptor.
196 else {
197 currentDescriptor += c;
198 }
199 } // After descriptor
200 else if (state === 'after descriptor') {
201 // Do the following, depending on the value of c:
202 if (isASCIIWhitespace(c)) {// Space character: Stay in this state.
203 } // EOF: Jump to the step labeled descriptor parser.
204 else if (c === '') {
205 parseDescriptors();
206 return;
207 } // Anything else
208 // Set state to in descriptor. Set position to the previous character in input.
209 else {
210 state = 'in descriptor';
211 position -= 1;
212 }
213 } // Advance position to the next character in input.
214
215
216 position += 1;
217 }
218 }
219 /**
220 * Adds descriptor properties to a candidate, pushes to the candidates array
221 * @return undefined
222 */
223 // Declared outside of the while loop so that it's only created once.
224
225
226 function parseDescriptors() {
227 // 9. Descriptor parser: Let error be no.
228 let pError = false; // 10. Let width be absent.
229 // 11. Let density be absent.
230 // 12. Let future-compat-h be absent. (We're implementing it now as h)
231
232 let w;
233 let d;
234 let h;
235 let i;
236 const candidate = {};
237 let desc;
238 let lastChar;
239 let value;
240 let intVal;
241 let floatVal; // 13. For each descriptor in descriptors, run the appropriate set of steps
242 // from the following list:
243
244 for (i = 0; i < descriptors.length; i++) {
245 desc = descriptors[i];
246 lastChar = desc[desc.length - 1];
247 value = desc.substring(0, desc.length - 1);
248 intVal = parseInt(value, 10);
249 floatVal = parseFloat(value); // If the descriptor consists of a valid non-negative integer followed by
250 // a U+0077 LATIN SMALL LETTER W character
251
252 if (regexNonNegativeInteger.test(value) && lastChar === 'w') {
253 // If width and density are not both absent, then let error be yes.
254 if (w || d) {
255 pError = true;
256 } // Apply the rules for parsing non-negative integers to the descriptor.
257 // If the result is zero, let error be yes.
258 // Otherwise, let width be the result.
259
260
261 if (intVal === 0) {
262 pError = true;
263 } else {
264 w = intVal;
265 }
266 } // If the descriptor consists of a valid floating-point number followed by
267 // a U+0078 LATIN SMALL LETTER X character
268 else if (regexFloatingPoint.test(value) && lastChar === 'x') {
269 // If width, density and future-compat-h are not all absent, then let error
270 // be yes.
271 if (w || d || h) {
272 pError = true;
273 } // Apply the rules for parsing floating-point number values to the descriptor.
274 // If the result is less than zero, let error be yes. Otherwise, let density
275 // be the result.
276
277
278 if (floatVal < 0) {
279 pError = true;
280 } else {
281 d = floatVal;
282 }
283 } // If the descriptor consists of a valid non-negative integer followed by
284 // a U+0068 LATIN SMALL LETTER H character
285 else if (regexNonNegativeInteger.test(value) && lastChar === 'h') {
286 // If height and density are not both absent, then let error be yes.
287 if (h || d) {
288 pError = true;
289 } // Apply the rules for parsing non-negative integers to the descriptor.
290 // If the result is zero, let error be yes. Otherwise, let future-compat-h
291 // be the result.
292
293
294 if (intVal === 0) {
295 pError = true;
296 } else {
297 h = intVal;
298 } // Anything else, Let error be yes.
299
300 } else {
301 pError = true;
302 }
303 } // 15. If error is still no, then append a new image source to candidates whose
304 // URL is url, associated with a width width if not absent and a pixel
305 // density density if not absent. Otherwise, there is a parse error.
306
307
308 if (!pError) {
309 candidate.source = {
310 value: url,
311 startIndex: startUrlPosition
312 };
313
314 if (w) {
315 candidate.width = {
316 value: w
317 };
318 }
319
320 if (d) {
321 candidate.density = {
322 value: d
323 };
324 }
325
326 if (h) {
327 candidate.height = {
328 value: h
329 };
330 }
331
332 candidates.push(candidate);
333 } else {
334 throw new Error(`Invalid srcset descriptor found in '${input}' at '${desc}'`);
335 }
336 }
337}
338
339function parseSrc(input) {
340 if (!input) {
341 throw new Error('Must be non-empty');
342 }
343
344 let startIndex = 0;
345 let value = input;
346
347 while (isASCIIWhitespace(value.substring(0, 1))) {
348 startIndex += 1;
349 value = value.substring(1, value.length);
350 }
351
352 while (isASCIIWhitespace(value.substring(value.length - 1, value.length))) {
353 value = value.substring(0, value.length - 1);
354 }
355
356 if (!value) {
357 throw new Error('Must be non-empty');
358 }
359
360 return {
361 value,
362 startIndex
363 };
364}
365
366const moduleRequestRegex = /^[^?]*~/;
367const matchNativeWin32Path = /^[A-Z]:[/\\]|^\\\\/i;
368
369function normalizeUrl(url) {
370 return matchNativeWin32Path.test(url) ? decodeURI(url).replace(/[\t\n\r]/g, '') : decodeURI(url).replace(/[\t\n\r]/g, '').replace(/\\/g, '/');
371}
372
373function requestify(url) {
374 if (matchNativeWin32Path.test(url) || url[0] === '/') {
375 return url;
376 }
377
378 if (/^file:/i.test(url)) {
379 return url;
380 }
381
382 if (/^\.\.?\//.test(url)) {
383 return url;
384 } // A `~` makes the url an module
385
386
387 if (moduleRequestRegex.test(url)) {
388 return url.replace(moduleRequestRegex, '');
389 } // every other url is threaded like a relative url
390
391
392 return `./${url}`;
393}
394
395function isUrlRequestable(url) {
396 // Protocol-relative URLs
397 if (/^\/\//.test(url)) {
398 return false;
399 } // `file:` protocol
400
401
402 if (/^file:/i.test(url)) {
403 return true;
404 } // Absolute URLs
405
406
407 if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !matchNativeWin32Path.test(url)) {
408 return false;
409 } // It's some kind of url for a template
410
411
412 if (/^[{}[\]#*;,'§$%&(=?`´^°<>]/.test(url)) {
413 return false;
414 }
415
416 return true;
417}
418
419const matchRelativePath = /^\.\.?[/\\]/;
420
421function isAbsolutePath(str) {
422 return matchNativeWin32Path.test(str) && _path.default.win32.isAbsolute(str);
423}
424
425function isRelativePath(str) {
426 return matchRelativePath.test(str);
427}
428
429function stringifyRequest(context, request) {
430 const splitted = request.split('!');
431 return JSON.stringify(splitted.map(part => {
432 // First, separate singlePath from query, because the query might contain paths again
433 const splittedPart = part.match(/^(.*?)(\?.*)/);
434 const query = splittedPart ? splittedPart[2] : '';
435 let singlePath = splittedPart ? splittedPart[1] : part;
436
437 if (isAbsolutePath(singlePath) && context) {
438 singlePath = _path.default.relative(context, singlePath);
439
440 if (isAbsolutePath(singlePath)) {
441 // If singlePath still matches an absolute path, singlePath was on a different drive than context.
442 // In this case, we leave the path platform-specific without replacing any separators.
443 // @see https://github.com/webpack/loader-utils/pull/14
444 return singlePath + query;
445 }
446
447 if (isRelativePath(singlePath) === false) {
448 // Ensure that the relative path starts at least with ./ otherwise it would be a request into the modules directory (like node_modules).
449 singlePath = `./${singlePath}`;
450 }
451 }
452
453 return singlePath.replace(/\\/g, '/') + query;
454 }).join('!'));
455}
456
457function isProductionMode(loaderContext) {
458 return loaderContext.mode === 'production' || !loaderContext.mode;
459}
460
461const defaultMinimizerOptions = {
462 caseSensitive: true,
463 // `collapseBooleanAttributes` is not always safe, since this can break CSS attribute selectors and not safe for XHTML
464 collapseWhitespace: true,
465 conservativeCollapse: true,
466 keepClosingSlash: true,
467 // We need ability to use cssnano, or setup own function without extra dependencies
468 minifyCSS: true,
469 minifyJS: true,
470 // `minifyURLs` is unsafe, because we can't guarantee what the base URL is
471 // `removeAttributeQuotes` is not safe in some rare cases, also HTML spec recommends against doing this
472 removeComments: true,
473 // `removeEmptyAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
474 // `removeRedundantAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
475 removeScriptTypeAttributes: true,
476 removeStyleLinkTypeAttributes: true // `useShortDoctype` is not safe for XHTML
477
478};
479
480function getMinimizeOption(rawOptions, loaderContext) {
481 if (typeof rawOptions.minimize === 'undefined') {
482 return isProductionMode(loaderContext) ? defaultMinimizerOptions : false;
483 }
484
485 if (typeof rawOptions.minimize === 'boolean') {
486 return rawOptions.minimize === true ? defaultMinimizerOptions : false;
487 }
488
489 return rawOptions.minimize;
490}
491
492function getAttributeValue(attributes, name) {
493 const [result] = attributes.filter(i => i.name.toLowerCase() === name);
494 return typeof result === 'undefined' ? result : result.value;
495}
496
497function scriptSrcFilter(tag, attribute, attributes) {
498 let type = getAttributeValue(attributes, 'type');
499
500 if (!type) {
501 return true;
502 }
503
504 type = type.trim();
505
506 if (!type) {
507 return false;
508 }
509
510 if (type !== 'module' && type !== 'text/javascript' && type !== 'application/javascript') {
511 return false;
512 }
513
514 return true;
515}
516
517function linkHrefFilter(tag, attribute, attributes) {
518 let rel = getAttributeValue(attributes, 'rel');
519
520 if (!rel) {
521 return false;
522 }
523
524 rel = rel.trim();
525
526 if (!rel) {
527 return false;
528 }
529
530 rel = rel.toLowerCase();
531 const usedRels = rel.split(' ').filter(value => value);
532 const allowedRels = ['stylesheet', 'icon', 'mask-icon', 'apple-touch-icon', 'apple-touch-icon-precomposed', 'apple-touch-startup-image', 'manifest', 'prefetch', 'preload'];
533 return allowedRels.filter(value => usedRels.includes(value)).length > 0;
534}
535
536const META = new Map([['name', new Set([// msapplication-TileImage
537'msapplication-tileimage', 'msapplication-square70x70logo', 'msapplication-square150x150logo', 'msapplication-wide310x150logo', 'msapplication-square310x310logo', 'msapplication-config', 'twitter:image'])], ['property', new Set(['og:image', 'og:image:url', 'og:image:secure_url', 'og:audio', 'og:audio:secure_url', 'og:video', 'og:video:secure_url', 'vk:image'])], ['itemprop', new Set(['image', 'logo', 'screenshot', 'thumbnailurl', 'contenturl', 'downloadurl', 'duringmedia', 'embedurl', 'installurl', 'layoutimage'])], ['name', new Set(['msapplication-task'])]]);
538
539function linkItempropFilter(tag, attribute, attributes) {
540 let name = getAttributeValue(attributes, 'itemprop');
541
542 if (name) {
543 name = name.trim();
544
545 if (!name) {
546 return false;
547 }
548
549 name = name.toLowerCase();
550 return META.get('itemprop').has(name);
551 }
552
553 return false;
554}
555
556function linkUnionFilter(tag, attribute, attributes) {
557 return linkHrefFilter(tag, attribute, attributes) || linkItempropFilter(tag, attribute, attributes);
558}
559
560function metaContentFilter(tag, attribute, attributes) {
561 for (const item of META) {
562 const [key, allowedNames] = item;
563 let name = getAttributeValue(attributes, key);
564
565 if (name) {
566 name = name.trim();
567
568 if (!name) {
569 // eslint-disable-next-line no-continue
570 continue;
571 }
572
573 name = name.toLowerCase();
574 return allowedNames.has(name);
575 }
576 }
577
578 return false;
579}
580
581function typeSrc({
582 name,
583 attribute,
584 node,
585 target,
586 html,
587 options
588}) {
589 const {
590 tagName,
591 sourceCodeLocation
592 } = node;
593 const {
594 value
595 } = attribute;
596 const result = [];
597 let source;
598
599 try {
600 source = parseSrc(value);
601 } catch (error) {
602 options.errors.push(new _HtmlSourceError.default(`Bad value for attribute "${attribute.name}" on element "${tagName}": ${error.message}`, sourceCodeLocation.attrs[name].startOffset, sourceCodeLocation.attrs[name].endOffset, html));
603 return result;
604 }
605
606 source = c0ControlCodesExclude(source);
607
608 if (!isUrlRequestable(source.value)) {
609 return result;
610 }
611
612 const startOffset = sourceCodeLocation.attrs[name].startOffset + target.indexOf(source.value, name.length);
613 result.push({
614 value: source.value,
615 startIndex: startOffset,
616 endIndex: startOffset + source.value.length
617 });
618 return result;
619}
620
621function typeSrcset({
622 name,
623 attribute,
624 node,
625 target,
626 html,
627 options
628}) {
629 const {
630 tagName,
631 sourceCodeLocation
632 } = node;
633 const {
634 value
635 } = attribute;
636 const result = [];
637 let sourceSet;
638
639 try {
640 sourceSet = parseSrcset(value);
641 } catch (error) {
642 options.errors.push(new _HtmlSourceError.default(`Bad value for attribute "${attribute.name}" on element "${tagName}": ${error.message}`, sourceCodeLocation.attrs[name].startOffset, sourceCodeLocation.attrs[name].endOffset, html));
643 return result;
644 }
645
646 sourceSet = sourceSet.map(item => {
647 return {
648 source: c0ControlCodesExclude(item.source)
649 };
650 });
651 let searchFrom = name.length;
652 sourceSet.forEach(sourceItem => {
653 const {
654 source
655 } = sourceItem;
656
657 if (!isUrlRequestable(source.value)) {
658 return false;
659 }
660
661 const startOffset = sourceCodeLocation.attrs[name].startOffset + target.indexOf(source.value, searchFrom);
662 searchFrom = target.indexOf(source.value, searchFrom) + 1;
663 result.push({
664 value: source.value,
665 startIndex: startOffset,
666 endIndex: startOffset + source.value.length
667 });
668 return false;
669 });
670 return result;
671}
672
673function typeMsapplicationTask({
674 name,
675 attribute,
676 node,
677 target,
678 html,
679 options
680}) {
681 const {
682 tagName,
683 sourceCodeLocation
684 } = node;
685 const [content] = typeSrc({
686 name,
687 attribute,
688 node,
689 target,
690 html,
691 options
692 });
693 const result = [];
694
695 if (!content) {
696 return result;
697 }
698
699 let startIndex = 0;
700 let endIndex = 0;
701 let foundIconUri;
702 let source;
703 content.value.split(';').forEach(i => {
704 if (foundIconUri) {
705 return;
706 }
707
708 if (!i.includes('icon-uri')) {
709 // +1 because of ";"
710 startIndex += i.length + 1;
711 return;
712 }
713
714 foundIconUri = true;
715 const [, aValue] = i.split('=');
716
717 try {
718 source = parseSrc(aValue);
719 } catch (error) {
720 options.errors.push(new _HtmlSourceError.default(`Bad value for attribute "icon-uri" on element "${tagName}": ${error.message}`, sourceCodeLocation.attrs[name].startOffset, sourceCodeLocation.attrs[name].endOffset, html));
721 return;
722 } // +1 because of "="
723
724
725 startIndex += i.indexOf('=') + source.startIndex + 1;
726 endIndex = startIndex + source.value.length;
727 });
728
729 if (!source) {
730 return result;
731 }
732
733 result.push({ ...content,
734 startIndex: content.startIndex + startIndex,
735 endIndex: content.startIndex + endIndex,
736 name: 'icon-uri',
737 value: source.value
738 });
739 return result;
740}
741
742function metaContentType({
743 name,
744 attribute,
745 node,
746 target,
747 html,
748 options
749}) {
750 const isMsapplicationTask = node.attrs.filter(i => i.name.toLowerCase() === 'name' && i.value.toLowerCase() === 'msapplication-task');
751 return isMsapplicationTask.length === 0 ? typeSrc({
752 name,
753 attribute,
754 node,
755 target,
756 html,
757 options
758 }) : typeMsapplicationTask({
759 name,
760 attribute,
761 node,
762 target,
763 html,
764 options
765 });
766}
767
768const defaultAttributes = [{
769 tag: 'audio',
770 attribute: 'src',
771 type: 'src'
772}, {
773 tag: 'embed',
774 attribute: 'src',
775 type: 'src'
776}, {
777 tag: 'img',
778 attribute: 'src',
779 type: 'src'
780}, {
781 tag: 'img',
782 attribute: 'srcset',
783 type: 'srcset'
784}, {
785 tag: 'input',
786 attribute: 'src',
787 type: 'src'
788}, {
789 tag: 'link',
790 attribute: 'href',
791 type: 'src',
792 filter: linkUnionFilter
793}, {
794 tag: 'link',
795 attribute: 'imagesrcset',
796 type: 'srcset',
797 filter: linkHrefFilter
798}, {
799 tag: 'meta',
800 attribute: 'content',
801 type: metaContentType,
802 filter: metaContentFilter
803}, {
804 tag: 'object',
805 attribute: 'data',
806 type: 'src'
807}, {
808 tag: 'script',
809 attribute: 'src',
810 type: 'src',
811 filter: scriptSrcFilter
812}, // Using href with <script> is described here: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
813{
814 tag: 'script',
815 attribute: 'href',
816 type: 'src',
817 filter: scriptSrcFilter
818}, {
819 tag: 'script',
820 attribute: 'xlink:href',
821 type: 'src',
822 filter: scriptSrcFilter
823}, {
824 tag: 'source',
825 attribute: 'src',
826 type: 'src'
827}, {
828 tag: 'source',
829 attribute: 'srcset',
830 type: 'srcset'
831}, {
832 tag: 'track',
833 attribute: 'src',
834 type: 'src'
835}, {
836 tag: 'video',
837 attribute: 'poster',
838 type: 'src'
839}, {
840 tag: 'video',
841 attribute: 'src',
842 type: 'src'
843}, // SVG
844{
845 tag: 'image',
846 attribute: 'xlink:href',
847 type: 'src'
848}, {
849 tag: 'image',
850 attribute: 'href',
851 type: 'src'
852}, {
853 tag: 'use',
854 attribute: 'xlink:href',
855 type: 'src'
856}, {
857 tag: 'use',
858 attribute: 'href',
859 type: 'src'
860}];
861
862function rewriteSourcesList(sourcesList, attribute, source) {
863 for (const key of sourcesList.keys()) {
864 const item = sourcesList.get(key);
865
866 if (!item.has(attribute)) {
867 // eslint-disable-next-line no-continue
868 continue;
869 }
870
871 item.set(attribute, { ...item.get(attribute),
872 ...source
873 });
874 sourcesList.set(key, item);
875 }
876}
877
878function createSourcesList(sources, accumulator = new Map()) {
879 for (const source of sources) {
880 if (source === '...') {
881 // eslint-disable-next-line no-continue
882 continue;
883 }
884
885 let {
886 tag = '*',
887 attribute = '*'
888 } = source;
889 tag = tag.toLowerCase();
890 attribute = attribute.toLowerCase();
891
892 if (tag === '*') {
893 rewriteSourcesList(accumulator, attribute, source);
894 }
895
896 if (!accumulator.has(tag)) {
897 accumulator.set(tag, new Map());
898 }
899
900 accumulator.get(tag).set(attribute, source);
901 }
902
903 return accumulator;
904}
905
906function smartMergeSources(array, factory) {
907 if (typeof array === 'undefined') {
908 return factory();
909 }
910
911 const result = array.some(i => i === '...') ? createSourcesList(array, factory()) : createSourcesList(array);
912 return result;
913}
914
915function getSourcesOption(rawOptions) {
916 if (typeof rawOptions.sources === 'undefined') {
917 return {
918 list: createSourcesList(defaultAttributes)
919 };
920 }
921
922 if (typeof rawOptions.sources === 'boolean') {
923 return rawOptions.sources === true ? {
924 list: createSourcesList(defaultAttributes)
925 } : false;
926 }
927
928 const sources = smartMergeSources(rawOptions.sources.list, () => createSourcesList(defaultAttributes));
929 return {
930 list: sources,
931 urlFilter: rawOptions.sources.urlFilter,
932 root: rawOptions.sources.root
933 };
934}
935
936function normalizeOptions(rawOptions, loaderContext) {
937 return {
938 preprocessor: rawOptions.preprocessor,
939 sources: getSourcesOption(rawOptions),
940 minimize: getMinimizeOption(rawOptions, loaderContext),
941 esModule: typeof rawOptions.esModule === 'undefined' ? true : rawOptions.esModule
942 };
943}
944
945function pluginRunner(plugins) {
946 return {
947 process: content => {
948 const result = {};
949
950 for (const plugin of plugins) {
951 // eslint-disable-next-line no-param-reassign
952 content = plugin(content, result);
953 }
954
955 result.html = content;
956 return result;
957 }
958 };
959}
960
961function getFilter(filter, defaultFilter = null) {
962 return (attribute, value, resourcePath) => {
963 if (defaultFilter && !defaultFilter(value)) {
964 return false;
965 }
966
967 if (typeof filter === 'function') {
968 return filter(attribute, value, resourcePath);
969 }
970
971 return true;
972 };
973}
974
975const GET_SOURCE_FROM_IMPORT_NAME = '___HTML_LOADER_GET_SOURCE_FROM_IMPORT___';
976
977function getImportCode(html, loaderContext, imports, options) {
978 if (imports.length === 0) {
979 return '';
980 }
981
982 const stringifiedHelperRequest = `"${_path.default.relative(loaderContext.context, require.resolve('./runtime/getUrl.js')).replace(/\\/g, '/')}"`;
983 let code = options.esModule ? `import ${GET_SOURCE_FROM_IMPORT_NAME} from ${stringifiedHelperRequest};\n` : `var ${GET_SOURCE_FROM_IMPORT_NAME} = require(${stringifiedHelperRequest});\n`;
984
985 for (const item of imports) {
986 const {
987 importName,
988 source
989 } = item;
990 code += options.esModule ? `var ${importName} = new URL(${source}, import.meta.url);\n` : `var ${importName} = require(${source});\n`;
991 }
992
993 return `// Imports\n${code}`;
994}
995
996function getModuleCode(html, replacements) {
997 let code = JSON.stringify(html) // Invalid in JavaScript but valid HTML
998 .replace(/[\u2028\u2029]/g, str => str === '\u2029' ? '\\u2029' : '\\u2028');
999 let replacersCode = '';
1000
1001 for (const item of replacements) {
1002 const {
1003 importName,
1004 replacementName,
1005 unquoted,
1006 hash
1007 } = item;
1008 const getUrlOptions = [].concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []).concat(unquoted ? 'maybeNeedQuotes: true' : []);
1009 const preparedOptions = getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : '';
1010 replacersCode += `var ${replacementName} = ${GET_SOURCE_FROM_IMPORT_NAME}(${importName}${preparedOptions});\n`;
1011 code = code.replace(new RegExp(replacementName, 'g'), () => `" + ${replacementName} + "`);
1012 }
1013
1014 return `// Module\n${replacersCode}var code = ${code};\n`;
1015}
1016
1017function getExportCode(html, options) {
1018 if (options.esModule) {
1019 return `// Exports\nexport default code;`;
1020 }
1021
1022 return `// Exports\nmodule.exports = code;`;
1023}
1024
1025function isASCIIC0group(character) {
1026 // C0 and &nbsp;
1027 // eslint-disable-next-line no-control-regex
1028 return /^[\u0001-\u0019\u00a0]/.test(character);
1029}
1030
1031function c0ControlCodesExclude(source) {
1032 let {
1033 value,
1034 startIndex
1035 } = source;
1036
1037 if (!value) {
1038 throw new Error('Must be non-empty');
1039 }
1040
1041 while (isASCIIC0group(value.substring(0, 1))) {
1042 startIndex += 1;
1043 value = value.substring(1, value.length);
1044 }
1045
1046 while (isASCIIC0group(value.substring(value.length - 1, value.length))) {
1047 value = value.substring(0, value.length - 1);
1048 }
1049
1050 if (!value) {
1051 throw new Error('Must be non-empty');
1052 }
1053
1054 return {
1055 value,
1056 startIndex
1057 };
1058}
\No newline at end of file