UNPKG

19.6 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.normalizeOptions = normalizeOptions;
12exports.pluginRunner = pluginRunner;
13exports.getFilter = getFilter;
14exports.getImportCode = getImportCode;
15exports.getModuleCode = getModuleCode;
16exports.getExportCode = getExportCode;
17
18var _loaderUtils = require("loader-utils");
19
20function isASCIIWhitespace(character) {
21 return (// Horizontal tab
22 character === '\u0009' || // New line
23 character === '\u000A' || // Form feed
24 character === '\u000C' || // Carriage return
25 character === '\u000D' || // Space
26 character === '\u0020'
27 );
28} // (Don't use \s, to avoid matching non-breaking space)
29// eslint-disable-next-line no-control-regex
30
31
32const regexLeadingSpaces = /^[ \t\n\r\u000c]+/; // eslint-disable-next-line no-control-regex
33
34const regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/; // eslint-disable-next-line no-control-regex
35
36const regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/;
37const regexTrailingCommas = /[,]+$/;
38const regexNonNegativeInteger = /^\d+$/; // ( Positive or negative or unsigned integers or decimals, without or without exponents.
39// Must include at least one digit.
40// According to spec tests any decimal point must be followed by a digit.
41// No leading plus sign is allowed.)
42// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
43
44const regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
45
46function parseSrcset(input) {
47 // 1. Let input be the value passed to this algorithm.
48 const inputLength = input.length;
49 let url;
50 let descriptors;
51 let currentDescriptor;
52 let state;
53 let c; // 2. Let position be a pointer into input, initially pointing at the start
54 // of the string.
55
56 let position = 0;
57 let startUrlPosition; // eslint-disable-next-line consistent-return
58
59 function collectCharacters(regEx) {
60 let chars;
61 const match = regEx.exec(input.substring(position));
62
63 if (match) {
64 [chars] = match;
65 position += chars.length;
66 return chars;
67 }
68 } // 3. Let candidates be an initially empty source set.
69
70
71 const candidates = []; // 4. Splitting loop: Collect a sequence of characters that are space
72 // characters or U+002C COMMA characters. If any U+002C COMMA characters
73 // were collected, that is a parse error.
74 // eslint-disable-next-line no-constant-condition
75
76 while (true) {
77 collectCharacters(regexLeadingCommasOrSpaces); // 5. If position is past the end of input, return candidates and abort these steps.
78
79 if (position >= inputLength) {
80 if (candidates.length === 0) {
81 throw new Error('Must contain one or more image candidate strings');
82 } // (we're done, this is the sole return path)
83
84
85 return candidates;
86 } // 6. Collect a sequence of characters that are not space characters,
87 // and let that be url.
88
89
90 startUrlPosition = position;
91 url = collectCharacters(regexLeadingNotSpaces); // 7. Let descriptors be a new empty list.
92
93 descriptors = []; // 8. If url ends with a U+002C COMMA character (,), follow these substeps:
94 // (1). Remove all trailing U+002C COMMA characters from url. If this removed
95 // more than one character, that is a parse error.
96
97 if (url.slice(-1) === ',') {
98 url = url.replace(regexTrailingCommas, ''); // (Jump ahead to step 9 to skip tokenization and just push the candidate).
99
100 parseDescriptors();
101 } // Otherwise, follow these substeps:
102 else {
103 tokenize();
104 } // 16. Return to the step labeled splitting loop.
105
106 }
107 /**
108 * Tokenizes descriptor properties prior to parsing
109 * Returns undefined.
110 */
111
112
113 function tokenize() {
114 // 8.1. Descriptor tokenizer: Skip whitespace
115 collectCharacters(regexLeadingSpaces); // 8.2. Let current descriptor be the empty string.
116
117 currentDescriptor = ''; // 8.3. Let state be in descriptor.
118
119 state = 'in descriptor'; // eslint-disable-next-line no-constant-condition
120
121 while (true) {
122 // 8.4. Let c be the character at position.
123 c = input.charAt(position); // Do the following depending on the value of state.
124 // For the purpose of this step, "EOF" is a special character representing
125 // that position is past the end of input.
126 // In descriptor
127
128 if (state === 'in descriptor') {
129 // Do the following, depending on the value of c:
130 // Space character
131 // If current descriptor is not empty, append current descriptor to
132 // descriptors and let current descriptor be the empty string.
133 // Set state to after descriptor.
134 if (isASCIIWhitespace(c)) {
135 if (currentDescriptor) {
136 descriptors.push(currentDescriptor);
137 currentDescriptor = '';
138 state = 'after descriptor';
139 }
140 } // U+002C COMMA (,)
141 // Advance position to the next character in input. If current descriptor
142 // is not empty, append current descriptor to descriptors. Jump to the step
143 // labeled descriptor parser.
144 else if (c === ',') {
145 position += 1;
146
147 if (currentDescriptor) {
148 descriptors.push(currentDescriptor);
149 }
150
151 parseDescriptors();
152 return;
153 } // U+0028 LEFT PARENTHESIS (()
154 // Append c to current descriptor. Set state to in parens.
155 else if (c === '\u0028') {
156 currentDescriptor += c;
157 state = 'in parens';
158 } // EOF
159 // If current descriptor is not empty, append current descriptor to
160 // descriptors. Jump to the step labeled descriptor parser.
161 else if (c === '') {
162 if (currentDescriptor) {
163 descriptors.push(currentDescriptor);
164 }
165
166 parseDescriptors();
167 return; // Anything else
168 // Append c to current descriptor.
169 } else {
170 currentDescriptor += c;
171 }
172 } // In parens
173 else if (state === 'in parens') {
174 // U+0029 RIGHT PARENTHESIS ())
175 // Append c to current descriptor. Set state to in descriptor.
176 if (c === ')') {
177 currentDescriptor += c;
178 state = 'in descriptor';
179 } // EOF
180 // Append current descriptor to descriptors. Jump to the step labeled
181 // descriptor parser.
182 else if (c === '') {
183 descriptors.push(currentDescriptor);
184 parseDescriptors();
185 return;
186 } // Anything else
187 // Append c to current descriptor.
188 else {
189 currentDescriptor += c;
190 }
191 } // After descriptor
192 else if (state === 'after descriptor') {
193 // Do the following, depending on the value of c:
194 if (isASCIIWhitespace(c)) {// Space character: Stay in this state.
195 } // EOF: Jump to the step labeled descriptor parser.
196 else if (c === '') {
197 parseDescriptors();
198 return;
199 } // Anything else
200 // Set state to in descriptor. Set position to the previous character in input.
201 else {
202 state = 'in descriptor';
203 position -= 1;
204 }
205 } // Advance position to the next character in input.
206
207
208 position += 1;
209 }
210 }
211 /**
212 * Adds descriptor properties to a candidate, pushes to the candidates array
213 * @return undefined
214 */
215 // Declared outside of the while loop so that it's only created once.
216
217
218 function parseDescriptors() {
219 // 9. Descriptor parser: Let error be no.
220 let pError = false; // 10. Let width be absent.
221 // 11. Let density be absent.
222 // 12. Let future-compat-h be absent. (We're implementing it now as h)
223
224 let w;
225 let d;
226 let h;
227 let i;
228 const candidate = {};
229 let desc;
230 let lastChar;
231 let value;
232 let intVal;
233 let floatVal; // 13. For each descriptor in descriptors, run the appropriate set of steps
234 // from the following list:
235
236 for (i = 0; i < descriptors.length; i++) {
237 desc = descriptors[i];
238 lastChar = desc[desc.length - 1];
239 value = desc.substring(0, desc.length - 1);
240 intVal = parseInt(value, 10);
241 floatVal = parseFloat(value); // If the descriptor consists of a valid non-negative integer followed by
242 // a U+0077 LATIN SMALL LETTER W character
243
244 if (regexNonNegativeInteger.test(value) && lastChar === 'w') {
245 // If width and density are not both absent, then let error be yes.
246 if (w || d) {
247 pError = true;
248 } // Apply the rules for parsing non-negative integers to the descriptor.
249 // If the result is zero, let error be yes.
250 // Otherwise, let width be the result.
251
252
253 if (intVal === 0) {
254 pError = true;
255 } else {
256 w = intVal;
257 }
258 } // If the descriptor consists of a valid floating-point number followed by
259 // a U+0078 LATIN SMALL LETTER X character
260 else if (regexFloatingPoint.test(value) && lastChar === 'x') {
261 // If width, density and future-compat-h are not all absent, then let error
262 // be yes.
263 if (w || d || h) {
264 pError = true;
265 } // Apply the rules for parsing floating-point number values to the descriptor.
266 // If the result is less than zero, let error be yes. Otherwise, let density
267 // be the result.
268
269
270 if (floatVal < 0) {
271 pError = true;
272 } else {
273 d = floatVal;
274 }
275 } // If the descriptor consists of a valid non-negative integer followed by
276 // a U+0068 LATIN SMALL LETTER H character
277 else if (regexNonNegativeInteger.test(value) && lastChar === 'h') {
278 // If height and density are not both absent, then let error be yes.
279 if (h || d) {
280 pError = true;
281 } // Apply the rules for parsing non-negative integers to the descriptor.
282 // If the result is zero, let error be yes. Otherwise, let future-compat-h
283 // be the result.
284
285
286 if (intVal === 0) {
287 pError = true;
288 } else {
289 h = intVal;
290 } // Anything else, Let error be yes.
291
292 } else {
293 pError = true;
294 }
295 } // 15. If error is still no, then append a new image source to candidates whose
296 // URL is url, associated with a width width if not absent and a pixel
297 // density density if not absent. Otherwise, there is a parse error.
298
299
300 if (!pError) {
301 candidate.source = {
302 value: url,
303 startIndex: startUrlPosition
304 };
305
306 if (w) {
307 candidate.width = {
308 value: w
309 };
310 }
311
312 if (d) {
313 candidate.density = {
314 value: d
315 };
316 }
317
318 if (h) {
319 candidate.height = {
320 value: h
321 };
322 }
323
324 candidates.push(candidate);
325 } else {
326 throw new Error(`Invalid srcset descriptor found in '${input}' at '${desc}'`);
327 }
328 }
329}
330
331function parseSrc(input) {
332 if (!input) {
333 throw new Error('Must be non-empty');
334 }
335
336 let startIndex = 0;
337 let value = input;
338
339 while (isASCIIWhitespace(value.substring(0, 1))) {
340 startIndex += 1;
341 value = value.substring(1, value.length);
342 }
343
344 while (isASCIIWhitespace(value.substring(value.length - 1, value.length))) {
345 value = value.substring(0, value.length - 1);
346 }
347
348 if (!value) {
349 throw new Error('Must be non-empty');
350 }
351
352 return {
353 value,
354 startIndex
355 };
356}
357
358function normalizeUrl(url) {
359 return decodeURIComponent(url).replace(/[\t\n\r]/g, '');
360}
361
362function requestify(url, root) {
363 return (0, _loaderUtils.urlToRequest)(url, root);
364}
365
366function isUrlRequestable(url, root) {
367 return (0, _loaderUtils.isUrlRequest)(url, root);
368}
369
370function isProductionMode(loaderContext) {
371 return loaderContext.mode === 'production' || !loaderContext.mode;
372}
373
374const defaultMinimizerOptions = {
375 caseSensitive: true,
376 // `collapseBooleanAttributes` is not always safe, since this can break CSS attribute selectors and not safe for XHTML
377 collapseWhitespace: true,
378 conservativeCollapse: true,
379 keepClosingSlash: true,
380 // We need ability to use cssnano, or setup own function without extra dependencies
381 minifyCSS: true,
382 minifyJS: true,
383 // `minifyURLs` is unsafe, because we can't guarantee what the base URL is
384 // `removeAttributeQuotes` is not safe in some rare cases, also HTML spec recommends against doing this
385 removeComments: true,
386 // `removeEmptyAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
387 // `removeRedundantAttributes` is not safe, can affect certain style or script behavior, look at https://github.com/webpack-contrib/html-loader/issues/323
388 removeScriptTypeAttributes: true,
389 removeStyleLinkTypeAttributes: true // `useShortDoctype` is not safe for XHTML
390
391};
392
393function getMinimizeOption(rawOptions, loaderContext) {
394 if (typeof rawOptions.minimize === 'undefined') {
395 return isProductionMode(loaderContext) ? defaultMinimizerOptions : false;
396 }
397
398 if (typeof rawOptions.minimize === 'boolean') {
399 return rawOptions.minimize === true ? defaultMinimizerOptions : false;
400 }
401
402 return rawOptions.minimize;
403}
404
405function getAttributeValue(attributes, name) {
406 const lowercasedAttributes = Object.keys(attributes).reduce((keys, k) => {
407 // eslint-disable-next-line no-param-reassign
408 keys[k.toLowerCase()] = k;
409 return keys;
410 }, {});
411 return attributes[lowercasedAttributes[name.toLowerCase()]];
412}
413
414function scriptFilter(tag, attribute, attributes) {
415 if (attributes.type) {
416 const type = getAttributeValue(attributes, 'type').trim().toLowerCase();
417
418 if (type !== 'module' && type !== 'text/javascript' && type !== 'application/javascript') {
419 return false;
420 }
421 }
422
423 return true;
424}
425
426const defaultAttributes = [{
427 tag: 'audio',
428 attribute: 'src',
429 type: 'src'
430}, {
431 tag: 'embed',
432 attribute: 'src',
433 type: 'src'
434}, {
435 tag: 'img',
436 attribute: 'src',
437 type: 'src'
438}, {
439 tag: 'img',
440 attribute: 'srcset',
441 type: 'srcset'
442}, {
443 tag: 'input',
444 attribute: 'src',
445 type: 'src'
446}, {
447 tag: 'link',
448 attribute: 'href',
449 type: 'src',
450 filter: (tag, attribute, attributes) => {
451 if (!/stylesheet/i.test(getAttributeValue(attributes, 'rel'))) {
452 return false;
453 }
454
455 if (attributes.type && getAttributeValue(attributes, 'type').trim().toLowerCase() !== 'text/css') {
456 return false;
457 }
458
459 return true;
460 }
461}, {
462 tag: 'object',
463 attribute: 'data',
464 type: 'src'
465}, {
466 tag: 'script',
467 attribute: 'src',
468 type: 'src',
469 filter: scriptFilter
470}, // Using href with <script> is described here: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
471{
472 tag: 'script',
473 attribute: 'href',
474 type: 'src',
475 filter: scriptFilter
476}, {
477 tag: 'script',
478 attribute: 'xlink:href',
479 type: 'src',
480 filter: scriptFilter
481}, {
482 tag: 'source',
483 attribute: 'src',
484 type: 'src'
485}, {
486 tag: 'source',
487 attribute: 'srcset',
488 type: 'srcset'
489}, {
490 tag: 'track',
491 attribute: 'src',
492 type: 'src'
493}, {
494 tag: 'video',
495 attribute: 'poster',
496 type: 'src'
497}, {
498 tag: 'video',
499 attribute: 'src',
500 type: 'src'
501}, // SVG
502{
503 tag: 'image',
504 attribute: 'xlink:href',
505 type: 'src'
506}, {
507 tag: 'image',
508 attribute: 'href',
509 type: 'src'
510}, {
511 tag: 'use',
512 attribute: 'xlink:href',
513 type: 'src'
514}, {
515 tag: 'use',
516 attribute: 'href',
517 type: 'src'
518}];
519
520function smartMergeSources(array, factory) {
521 if (typeof array === 'undefined') {
522 return factory();
523 }
524
525 const newArray = [];
526
527 for (let i = 0; i < array.length; i++) {
528 const item = array[i];
529
530 if (item === '...') {
531 const items = factory();
532
533 if (typeof items !== 'undefined') {
534 // eslint-disable-next-line no-shadow
535 for (const item of items) {
536 newArray.push(item);
537 }
538 }
539 } else if (typeof newArray !== 'undefined') {
540 newArray.push(item);
541 }
542 }
543
544 return newArray;
545}
546
547function getAttributesOption(rawOptions) {
548 if (typeof rawOptions.attributes === 'undefined') {
549 return {
550 list: defaultAttributes
551 };
552 }
553
554 if (typeof rawOptions.attributes === 'boolean') {
555 return rawOptions.attributes === true ? {
556 list: defaultAttributes
557 } : false;
558 }
559
560 const sources = smartMergeSources(rawOptions.attributes.list, () => defaultAttributes);
561 return {
562 list: sources,
563 urlFilter: rawOptions.attributes.urlFilter,
564 root: rawOptions.attributes.root
565 };
566}
567
568function normalizeOptions(rawOptions, loaderContext) {
569 return {
570 preprocessor: rawOptions.preprocessor,
571 attributes: getAttributesOption(rawOptions),
572 minimize: getMinimizeOption(rawOptions, loaderContext),
573 esModule: typeof rawOptions.esModule === 'undefined' ? false : rawOptions.esModule
574 };
575}
576
577function pluginRunner(plugins) {
578 return {
579 process: content => {
580 const result = {};
581
582 for (const plugin of plugins) {
583 // eslint-disable-next-line no-param-reassign
584 content = plugin(content, result);
585 }
586
587 result.html = content;
588 return result;
589 }
590 };
591}
592
593function getFilter(filter, defaultFilter = null) {
594 return (attribute, value, resourcePath) => {
595 if (defaultFilter && !defaultFilter(value)) {
596 return false;
597 }
598
599 if (typeof filter === 'function') {
600 return filter(attribute, value, resourcePath);
601 }
602
603 return true;
604 };
605}
606
607const GET_SOURCE_FROM_IMPORT_NAME = '___HTML_LOADER_GET_SOURCE_FROM_IMPORT___';
608
609function getImportCode(html, loaderContext, imports, options) {
610 if (imports.length === 0) {
611 return '';
612 }
613
614 const stringifiedHelperRequest = (0, _loaderUtils.stringifyRequest)(loaderContext, require.resolve('./runtime/getUrl.js'));
615 let code = options.esModule ? `import ${GET_SOURCE_FROM_IMPORT_NAME} from ${stringifiedHelperRequest};\n` : `var ${GET_SOURCE_FROM_IMPORT_NAME} = require(${stringifiedHelperRequest});\n`;
616
617 for (const item of imports) {
618 const {
619 importName,
620 source
621 } = item;
622 code += options.esModule ? `import ${importName} from ${source};\n` : `var ${importName} = require(${source});\n`;
623 }
624
625 return `// Imports\n${code}`;
626}
627
628function getModuleCode(html, replacements) {
629 let code = JSON.stringify(html) // Invalid in JavaScript but valid HTML
630 .replace(/[\u2028\u2029]/g, str => str === '\u2029' ? '\\u2029' : '\\u2028');
631 let replacersCode = '';
632
633 for (const item of replacements) {
634 const {
635 importName,
636 replacementName,
637 unquoted,
638 hash
639 } = item;
640 const getUrlOptions = [].concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []).concat(unquoted ? 'maybeNeedQuotes: true' : []);
641 const preparedOptions = getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : '';
642 replacersCode += `var ${replacementName} = ${GET_SOURCE_FROM_IMPORT_NAME}(${importName}${preparedOptions});\n`;
643 code = code.replace(new RegExp(replacementName, 'g'), () => `" + ${replacementName} + "`);
644 }
645
646 return `// Module\n${replacersCode}var code = ${code};\n`;
647}
648
649function getExportCode(html, options) {
650 if (options.esModule) {
651 return `// Exports\nexport default code;`;
652 }
653
654 return `// Exports\nmodule.exports = code;`;
655}
\No newline at end of file