From 548859597ad47d111f61583704e7b3c80fcf68cf Mon Sep 17 00:00:00 2001 From: biodiscus Date: Sun, 26 May 2024 23:17:29 +0200 Subject: [PATCH] feat: add resolving the url() value in the style attribute --- CHANGELOG.md | 7 ++ README.md | 46 +++++++++++++ package.json | 2 +- src/Common/HtmlParser.js | 63 ++++++++++++++++-- src/Loader/Template.js | 4 +- .../expected/img/bild.7b396424.png | Bin 0 -> 2743 bytes .../expected/img/image.d4711676.webp | Bin 0 -> 398 bytes .../expected/index.html | 12 ++++ .../resolve-attr-style-url/src/index.html | 12 ++++ .../resolve-attr-style-url/webpack.config.js | 48 +++++++++++++ test/integration.test.js | 2 + 11 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 test/cases/resolve-attr-style-url/expected/img/bild.7b396424.png create mode 100644 test/cases/resolve-attr-style-url/expected/img/image.d4711676.webp create mode 100644 test/cases/resolve-attr-style-url/expected/index.html create mode 100644 test/cases/resolve-attr-style-url/src/index.html create mode 100644 test/cases/resolve-attr-style-url/webpack.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2455c276..8fa75c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change log +## 3.13.0 (2024-05-26) + +- feat: add resolving the url() value in the style attribute: + ```html +
+ ``` + ## 3.12.0 (2024-05-19) - feat: add support for the `css-loader` option `exportType` as [css-style-sheet](https://github.com/webpack-contrib/css-loader?#exporttype) diff --git a/README.md b/README.md index dc0486f8..0c8d03d5 100644 --- a/README.md +++ b/README.md @@ -527,6 +527,7 @@ See [boilerplate](https://github.com/webdiscus/webpack-html-scss-boilerplate) - [How to inline SVG, PNG images in HTML](#recipe-inline-image) - [How to inline all resources into single HTML file](#recipe-inline-all-assets-to-html) - [How to resolve source assets in an attribute containing JSON value](#recipe-resolve-attr-json) + - [How to resolve source image in the `style` attribute](#recipe-resolve-attr-style-url) - [How to load CSS file dynamically](#recipe-dynamic-load-css) (lazy loading CSS) - [How to import CSS class names in JS](#recipe-css-modules) (CSS modules) - [How to import CSS stylesheet in JS](#recipe-css-style-sheet) ([CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet)) @@ -5351,6 +5352,51 @@ The custom attribute will contains in the generated HTML the resolved output ass ``` +--- + +#### [↑ back to contents](#contents) + + + +## How to resolve source image in the `style` attribute + +For example, there is the source image file defined in the `style` attribute as the background of the `div` tag: + +```html +
+``` + +The source image file can be a file relative to the template or you can use a webpack alias to the image directory. + +> **Note** +> +> This is BAD practice. Use it only in special cases. +> The background image should be defined in CSS. + +By default, the `style` attribute is not parsed and therefore needs to be configured: + +```js +new HtmlBundlerPlugin({ + entry: { + index: './src/index.html', + }, + loaderOptions: { + sources: [ + { + tag: 'div', // <= specify the tag where should be parsed style + attributes: ['style'], // <= specify the style attribute + }, + ], + }, +}), +``` + +The plugin resolves the `url()` value and replaces it in the generated HTML with the output filename: +```html +
+``` + + --- #### [↑ back to contents](#contents) diff --git a/package.json b/package.json index 54bf1cd8..31c21aee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-bundler-webpack-plugin", - "version": "3.12.0", + "version": "3.13.0", "description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.", "keywords": [ "html", diff --git a/src/Common/HtmlParser.js b/src/Common/HtmlParser.js index e1ab0e2e..3e1070e1 100644 --- a/src/Common/HtmlParser.js +++ b/src/Common/HtmlParser.js @@ -308,7 +308,7 @@ class HtmlParser { * @param {string} attr The attribute to parse value. * @param {string} type The type of attribute value. * @param {Number} offset The absolute tag offset in the content. - * @return {{attr: string, attrs?: Array, value: string, parsedValue: Array, startPos: number, endPos: number, offset: number, inEscapedDoubleQuotes: boolean}|boolean} + * @return {{attr: string, attrs?: Array, value: string, parsedValue: Array, startPos: number, endPos: number, offset: number, inEscapedDoubleQuotes: boolean} | {} | boolean} */ static parseAttr(content, attr, type = 'asset', offset = 0) { // TODO: allow zero or more spaces around `=` @@ -356,21 +356,37 @@ class HtmlParser { return { attr, - attrs, value, - parsedValue: values, startPos, endPos, offset, + attrs, + parsedValue: values, inEscapedDoubleQuotes, }; } + if (attr === 'style') { + const { values, attrs } = this.parseStyleUrlValues(value, startPos, offset); + + return values + ? { + attr, + value, + startPos, + endPos, + offset, + attrs, + parsedValue: values, + inEscapedDoubleQuotes, + } + : {}; + } + let result = { type, attr, value, - parsedValue: '', startPos, endPos, offset, @@ -389,6 +405,45 @@ class HtmlParser { return result; } + /** + * Parse url() in the style attribute. + * + * For example: + *
+ * + * @param {string} content The attribute value. + * @param {Number} valueOffset The offset of value in the tag. + * @param {Number} offset The absolute tag offset in the content. + * @return {{values: *[], attrs: *[]} | {}} + */ + static parseStyleUrlValues(content, valueOffset, offset) { + let pos = content.indexOf('url('); + + if (pos < 0) return {}; + + let valueStartPos = pos + 4; + let valueEndPos = content.indexOf(')', valueStartPos); + let quote = content.charAt(valueStartPos); + let skipQuotes = 1; + + if (quote !== '"' && quote !== "'") { + quote = ''; + skipQuotes = 0; + } + + let parsedValue = content.slice(valueStartPos + skipQuotes, valueEndPos - skipQuotes); // skip quotes around value + + let attrs = { + value: parsedValue, + quote, + startPos: valueStartPos, + endPos: valueEndPos, + offset: offset + valueOffset, + }; + + return { values: [parsedValue], attrs: [attrs] }; + } + /** * Parse require() in the attribute value. * diff --git a/src/Loader/Template.js b/src/Loader/Template.js index 1654ffa6..947cc05b 100644 --- a/src/Loader/Template.js +++ b/src/Loader/Template.js @@ -12,7 +12,6 @@ class Template { * @param { HtmlBundlerPlugin.Hooks} hooks The plugin hooks. * @return {string} */ - static resolve(content, issuer, entryId, hooks) { const sources = Option.getSources(); @@ -24,7 +23,6 @@ class Template { let parsedTags = []; for (let opts of sources) { opts.resourcePath = issuer; - parsedTags.push(...HtmlParser.parseTag(content, opts)); } parsedTags = parsedTags.sort(comparePos); @@ -36,6 +34,8 @@ class Template { for (let { tag, source, parsedAttrs } of parsedTags) { for (let { type, attr, startPos, endPos, value, quote, offset, inEscapedDoubleQuotes } of parsedAttrs) { + if (!value) continue; + const result = this.resolveFile({ isBasedir, type, diff --git a/test/cases/resolve-attr-style-url/expected/img/bild.7b396424.png b/test/cases/resolve-attr-style-url/expected/img/bild.7b396424.png new file mode 100644 index 0000000000000000000000000000000000000000..1e0c9bcd9880f81ccebcf1c2b8ad4b217f51508b GIT binary patch literal 2743 zcmV;o3P|;dP)z4uVW_kGj)VafPj$oM+E_c6Kla?klquv0{$PD{e~NTgJH()w+~z(&CL zIJ@^mzxPVP_fM*7f70-O)AUrYd3)0OSjPBt(D`@K`A@N0Os`R3xtvF*OMTJqcg)^< z&*^8ru71+-KE3x&!}s^^duYu0bIQwp)A~Zc_hY(eUbtdT!R%Vc_-DLs$Ks%R(B*c} z`Dwm+TC|E-wOmia^Ig1&a>&qT%lT2VRZFK{R>t^W$oPKI=}5r$Yrlde}8*L~6HZNZOm#;8cGiebK$OuNnf@OOaD!g$Qh z^X`4e;h0>zg>S^1O186T$>m+g?NzmGS;p{Jw{*YXoNvRLp4h!hzv0y8mE!7)Rkm+w zzJ2cPg6ZpqNwAt*w2NrS(3aN2cgw{|v7cwj<(1U6Rl?km)W=uF@omfDT*mI=>4!79 z@I<6tQpELl%g21r;Ap*bF1Yh7xAe>7oUGfh-RO_z>xk>@f`-t)UBuUH#<)kVh)cJ< zdd}5Fr)f^VRsUE3W9im zC{1GlK}kS@1~r0cB%+A|LP%mamJ~I4jIp<5XZf1`> z5AP>scV>Ug&CYv(0sq5*SOaL%kG3lPXsgnXwkrK-tJ06QD*b4y(vP+({b;KK2BfBY zT@@~`H$63~i5%%pugm4li*lL)*IKupKfks0n%+GiM+#@0-%IzzIA?W1WSX3OuY+nD zIjsjO?ZH$YXvNjZc!D~R){_;9`gfiRh zkIbN@eEDKro?rD-e_mwC3C!f`I}g$5hj+}NrD{io_9&A#YDH+dog`*r^<%WzZhveF zE#Wq7iZ?LOXSAPtLv(`r?%~k)5O1>%8lPtQFw;FZpx@E>nuriICLFi zbd1pQ;<OXgBTe@bf9?h!vDr>>cyyWF-1$w zk-wW(pwTN(!(xk82Kg$YUc_cd2?_t<{crWJ0 zzJ!);oH-sLA3t*gcVC}EOVXu_b-_OsE9)*^!r!HfuGzh5!-h?}Ygp6tbLH~oEBbfn zpc~F+gJ81HHn3$y8$7yZ76@mS#fxnaEV@BqmeqY~t+D8n0Ln=#QAWX`H+RA4uFY1e z41z(|TnDOLm$!9Bv8TyXTbyC?8J)#z}AM~nXI4;(ss zv}VyTTcioQ%sp90@J8sPJKgiLN^^^H>z7q|GNgttaAp98Cgao@O~E-@O6I%eRqRIY zW@QrH3Pf5DprwA*G|xaIafYV~4D2p}Cd>L4Zw6qb=INP%Pui2ERMyWoA_pGS6hd^i zK|3ii%dI1pwepidrfRaOU3%!QshX#kN}d)1q*m<-E;8bXS=WYH?u@FQT?bSH3a9It z0hnH>c}A}0Cu9kHMs4W5U*tiV-MtH}Mo-o^12CDAh)8`}3HI75&0=R{g)gemLyXL- zVvH_&uWO6yRBTjypfGyOU}Lnp7mDWy&bVXo4z)Lt2ar%Y<+i{K*te zQiX5`i?Ll{7KEe27^2Cl5Drl>Chr%Z%w>ostU@>>#rPI%7DkL`h9<2-I0VJ`?jRLf zc!kBw7(kO(AsoYE9JWAh7KELQ&?c%7j!`kbuQdz8t4z=fO;jNqgJSGJnT4`;2I%r7 zv}U;&%XOw6^(GQ6ERFj6I7m1}ZwBC$&eS{6W5^gv_j0j31%Q;>25Q(TZZ!t3o)~Vw|xYi56!jGet*= zQNIe|V2Uw*J=!dazp+J!i&4J{;b4k!5zZ{m{+um3RE$Pd2nSh=a23LtRL&ILQ;fz{ z2nSJ&>MDe@i!C}>jK7;Rql!^eg>Y`NMa#w5Vb-iIMr{?s2^1qkG+5G3RE2O<#fYjx zIC3$vM1$|lo7KgLtU@@8*rLJn6lkaz(Nzd%;w`r5q$eoUKSYWVSA}q9vPFXzDA2+` zy=HtB!eNXCFH@ky#fYy$zGy(RK|;l-SA~4hFH)d;ic!A`xuU_7H0nSx{zPi#fCev7 zpygsTszSc#B$}mOZCr(1(R3T+XL2(aG*vNDGUz8zyU-ML4&LAar*iji=G@IzA-BiXsf4^6c}Mi8BQ9MSxWktdp4G4e!n zD@LwpUd6~4&8ZkUqxlpgXEdK;X!gZu0h)a= xT7hO>jFzBT7o#<3#>Hq2nsG5&gl1ce{{b8S;zP1hl63$8002ovPDHLkV1kgoeY*ev literal 0 HcmV?d00001 diff --git a/test/cases/resolve-attr-style-url/expected/img/image.d4711676.webp b/test/cases/resolve-attr-style-url/expected/img/image.d4711676.webp new file mode 100644 index 0000000000000000000000000000000000000000..238589d7ce8bede62b59313bbd80031d6595dd6a GIT binary patch literal 398 zcmV;90df9PNk&G70RRA3MM6+kP&goZ0RR9n7XY0BDxd&@06t9~jYJ}$p_%%X#3%&B zw15k0V7)P!<=2O#W6`@%N0bB`=ZCZYf6Gku^*GKW4$f00K<9(Zmi1rMvUEwSo~#5{ zJXY8MT}qWoJn>wt1wie0=k=4fFYvVq^);8^vmX7d)>SMfM+f3^gekx~8wPNHsa(S@ z^+p1J`&ttxjhg(C?#mzVcrRpe5K>IF^h(Zpkb&U@+|+!KS)RN&G51-)4%;N?!4E-t zqYl#I8Zt2{QfglifB^pdaG=6x%D}PJ!9_07$k~wXan#~mM49Br-a@kN5+VD1`L1(D zx*&dE0-(g5)N6r0CIA2c0EiyIhyVZp literal 0 HcmV?d00001 diff --git a/test/cases/resolve-attr-style-url/expected/index.html b/test/cases/resolve-attr-style-url/expected/index.html new file mode 100644 index 00000000..e7ba21ba --- /dev/null +++ b/test/cases/resolve-attr-style-url/expected/index.html @@ -0,0 +1,12 @@ + + + + Test + + +

Hello World!

+
text
+
+
+ + \ No newline at end of file diff --git a/test/cases/resolve-attr-style-url/src/index.html b/test/cases/resolve-attr-style-url/src/index.html new file mode 100644 index 00000000..3dadf2da --- /dev/null +++ b/test/cases/resolve-attr-style-url/src/index.html @@ -0,0 +1,12 @@ + + + + Test + + +

Hello World!

+
text
+
+
+ + \ No newline at end of file diff --git a/test/cases/resolve-attr-style-url/webpack.config.js b/test/cases/resolve-attr-style-url/webpack.config.js new file mode 100644 index 00000000..b185e2d4 --- /dev/null +++ b/test/cases/resolve-attr-style-url/webpack.config.js @@ -0,0 +1,48 @@ +const path = require('path'); +const HtmlBundlerPlugin = require('@test/html-bundler-webpack-plugin'); + +module.exports = { + mode: 'production', + + output: { + path: path.join(__dirname, 'dist/'), + }, + + resolve: { + alias: { + '@images': path.join(__dirname, '../../fixtures/images'), + }, + }, + + plugins: [ + new HtmlBundlerPlugin({ + entry: { + index: './src/index.html', + }, + loaderOptions: { + sources: [ + { + tag: 'div', + attributes: ['style'], + }, + ], + }, + }), + ], + + module: { + rules: [ + { + test: /\.css$/, + use: ['css-loader'], + }, + { + test: /\.(png|jpe?g|webp|ico|svg)$/, + type: 'asset/resource', + generator: { + filename: 'img/[name].[hash:8][ext]', + }, + }, + ], + }, +}; diff --git a/test/integration.test.js b/test/integration.test.js index 6689dd88..6ffa5414 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -449,6 +449,8 @@ describe('special cases', () => { test('modify js in postprocess', () => compareFiles('postprocess-modify-js')); + test('resolve-attr-style-url', () => compareFiles('resolve-attr-style-url')); + // for debugging // test('resolve hmr file', () => watchCompareFiles('resolve-hmr-file')); });