utils.js 19 KB

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