Skip to content

Commit

Permalink
feat: resolve resource files in an attribute containing the JSON value
Browse files Browse the repository at this point in the history
  • Loading branch information
webdiscus committed Mar 7, 2024
1 parent 80264f5 commit 4185ada
Show file tree
Hide file tree
Showing 20 changed files with 309 additions and 45 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Change log

## 3.6.0 (2024-03-08)

- feat: resolve resource files in an attribute containing the JSON value using the `require()` function,\
source template:
```js
<a href="#" data-image='{ "alt":"image", "imgSrc": require("./pic1.png"), "bgImgSrc": require("./pic2.png") }'> ... </a>
```
generated HTML contains resolved output assets filenames:
```js
<a href="#" data-image='{ "alt":"image", "imgSrc": "img/pic1.da3e3cc9.png", "bgImgSrc": "img/pic2.e3cc9da3.png" }'> ... </a>
```

## 3.5.5 (2024-03-03)

- fix: initialize the singleton of the Config only once
Expand Down Expand Up @@ -31,7 +43,7 @@
## 3.5.3 (2024-02-28)

- fix: correct parsing the data passed via query in JSON notation, e.g.: `index.ejs?{"title":"Homepage","lang":"en"}`
- fix: by paring of the generated html ignore files already resolved via a preprocessor, e.g. pug
- fix: by parsing of the generated html ignore files already resolved via a preprocessor, e.g. pug
- fix(pug): resolve resource required in pug code and content, also outer tag attributes
- fix(pug): resolve images generated via `responsive-loader` when used query parameters with `,` and `&` separators
- test: add tests from pug-plugin
Expand Down
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ See [boilerplate](https://github.com/webdiscus/webpack-html-scss-boilerplate)
- [How to inline CSS in HTML](#recipe-inline-css)
- [How to inline JS in HTML](#recipe-inline-js)
- [How to inline SVG, PNG images in HTML](#recipe-inline-image)
- [How to resolve source assets in an attribute containing JSON value](#recipe-resolve-attr-json)
- [How to load CSS file dynamically](#recipe-dynamic-load-css)
- [How to process a PHP template](#recipe-preprocessor-php)
- [How to pass data into multiple templates](#recipe-pass-data-to-templates)
Expand Down Expand Up @@ -4378,7 +4379,7 @@ _./partials/people.ejs_
`include` is supported
- [twig](#loader-option-preprocessor-options-nunjucks) - generates a precompiled template with runtime (~110KB)\
`include` is supported
- pug (the support will be added later) - generates a small pure template function
- [pug](#loader-option-preprocessor-options-pug) - generates a small pure template function

#### Template engines that do NOT support the `template function` on client-side

Expand Down Expand Up @@ -5096,6 +5097,56 @@ The plugin automatically inlines images smaller then `maxSize`.
#### [↑ back to contents](#contents)
<a id="recipe-resolve-attr-json" name="recipe-resolve-attr-json"></a>
## How to resolve source assets in an attribute containing JSON value
For example, source images should be defined in the custom `data-image` attribute of the `a` tag:
```html
<a data-image='{ "imgSrc": "./pic1.png", "bgImgSrc": "./pic2.png" }' href="#" >
...
</a>
```
To resolve such files, just use the `require()` function:
```html
<a data-image='{ "imgSrc": require("./pic1.png"), "bgImgSrc": require("./pic2.png") }' href="#" >
...
</a>
```
Add to `sources` loader option the `data-image` attribute for the `a` tag:
```js
new HtmlBundlerPlugin({
entry: {
index: './src/index.html',
},
loaderOptions: {
sources: [
{
tag: 'a', // <= specify the 'a' tag
attributes: ['data-image'], // <= specify custom attribute for the 'a' tag
},
],
},
}),
```
The custom attribute will contains in the generated HTML the resolved output assets filenames:
```html
<a data-image='{ "imgSrc": "img/pic1.da3e3cc9.png", "bgImgSrc": "img/pic2.e3cc9da3.png" }' href="#" >
...
</a>
```
---
#### [↑ back to contents](#contents)
<a id="recipe-dynamic-load-css" name="recipe-dynamic-load-css"></a>
## How to load CSS file dynamically
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "html-bundler-webpack-plugin",
"version": "3.5.5",
"version": "3.6.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",
Expand Down
87 changes: 70 additions & 17 deletions src/Common/HtmlParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// - comparing numbers is faster than comparing strings
// - transforming a string to a byte array with TextEncoder.encode() is 100x faster than charCodeAt() via `for` loop

const { Obj } = require('nunjucks/src/object');
const textEncoder = new TextEncoder();
const spaceCodes = textEncoder.encode(' \n\r\t\f');
const tagEndCodes = textEncoder.encode(' \n\r\t\f/>');
Expand Down Expand Up @@ -311,6 +312,8 @@ class HtmlParser {
* @return {{attr: string, attrs?: Array, value: string, parsedValue: Array<string>, startPos: number, endPos: number, offset: number, inEscapedDoubleQuotes: boolean}|boolean}
*/
static parseAttr(content, attr, type = 'asset', offset = 0) {
// TODO: allow zero or more spaces around `=`
// https://www.w3.org/TR/2011/WD-html5-20110113/syntax.html#attributes-0
const open = `${attr}=`;
let startPos = 0;
let pos;
Expand All @@ -335,13 +338,16 @@ class HtmlParser {
// module.exports = fn("<img src=\"" + require('image.png') + "\">")

// find open quote pos
startPos = content.indexOf('"', startPos);
let inEscapedDoubleQuotes = content.charCodeAt(startPos - 1) === escapeCode;
let quote = inEscapedDoubleQuotes ? `\\"` : '"';
let openQuotePos = indexOfQuote(content, startPos);
let quoteChar = content.charAt(openQuotePos);
let inEscapedDoubleQuotes = content.charCodeAt(openQuotePos - 1) === escapeCode;
let quote = inEscapedDoubleQuotes ? `\\` + quoteChar : quoteChar;

startPos = openQuotePos + 1;

// find close quote pos
startPos++;
let endPos = content.indexOf(quote, startPos);

if (endPos < 0) endPos = content.length - 1;

let value = content.slice(startPos, endPos);
Expand All @@ -361,16 +367,65 @@ class HtmlParser {
};
}

return {
let result = {
type,
attr,
value,
parsedValue: [value.split('?', 1)[0]],
parsedValue: '',
startPos,
endPos,
offset,
inEscapedDoubleQuotes,
};

// parse for required values which are not yet resolved by a preprocessor like pug
if (value.indexOf('\\u0027') < 0 && value.indexOf('require(') > 0) {
const { values, attrs } = this.parseRequiredValues(value, startPos, offset);

return { ...result, parsedValue: values, attrs };
}

result.parsedValue = [value.split('?', 1)[0]];

return result;
}

/**
* Parse require() in the attribute value.
*
* For example:
* <a href="#" data-picture='{ "alt":"picture", "imgSrc": require("./picture.png") }'></a>
*
* @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 parseRequiredValues(content, valueOffset, offset) {
let pos;
let values = [];
let attrs = [];

while ((pos = content.indexOf('require(', pos)) > -1) {
let valueStartPos = pos + 8;
let valueEndPos = content.indexOf(')', valueStartPos);
let quote = content.charAt(valueStartPos);
let value = content.slice(valueStartPos + 1, valueEndPos - 1); // skip quotes around value

values.push(value);

attrs.push({
value,
quote, // quotes used in require()
startPos: valueOffset + pos, // skip `require(`
endPos: valueOffset + valueEndPos + 1, // skip `)`
offset,
});

pos = valueEndPos + 1;
}

return { values, attrs };
}

/**
Expand All @@ -380,14 +435,14 @@ class HtmlParser {
* "img1.png"
* "img1.png, img2.png 100w, img3.png 1.5x"
*
* @param {string} srcsetValue
* @param {string} content The srcset value.
* @param {Number} valueOffset The offset of value in the tag.
* @param {Number} offset The absolute tag offset in the content.
* @param {boolean} inEscapedDoubleQuotes Bypass the property to all `attrs` objects.
* @return {{values: Array<string>, attrs: [{attr: string, value, startPos: Number, endPos: Number, offset: number, inEscapedDoubleQuotes: boolean}]}}
*/
static parseSrcsetValue(srcsetValue, valueOffset, offset, inEscapedDoubleQuotes) {
const lastPos = srcsetValue.length;
static parseSrcsetValue(content, valueOffset, offset, inEscapedDoubleQuotes) {
const lastPos = content.length;
let startPos = 0;
let endPos = 0;
let currentPos;
Expand All @@ -398,25 +453,23 @@ class HtmlParser {
// supports the query for 'responsive-loader' in following notations:
// - image.png?{sizes: [100,200,300], format: 'jpg'} // JSON5
// - require('image.png?sizes[]=100,sizes[]=200,sizes[]=300,format=jpg') // `,` as parameter separator in require(), used in pug
if (srcsetValue.indexOf('?{') > 0 || srcsetValue.indexOf('require(') > -1) {
if (content.indexOf('?{') > 0 || content.indexOf('require(') > -1) {
return {
values: [srcsetValue],
attrs: [
{ type, attr: 'srcset', value: srcsetValue, startPos: valueOffset, endPos: valueOffset + lastPos, offset },
],
values: [content],
attrs: [{ type, attr: 'srcset', value: content, startPos: valueOffset, endPos: valueOffset + lastPos, offset }],
};
}

do {
currentPos = srcsetValue.indexOf(',', startPos);
currentPos = content.indexOf(',', startPos);
if (currentPos < 0) {
currentPos = lastPos;
}

startPos = indexOfNonSpace(srcsetValue, startPos);
startPos = indexOfNonSpace(content, startPos);

// parse value like "img.png"
let value = srcsetValue.slice(startPos, currentPos);
let value = content.slice(startPos, currentPos);
let pos = value.indexOf(' ');

// parse value like "img.png 100w"
Expand Down
8 changes: 6 additions & 2 deletions src/Loader/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Template {
let parsedTags = [];
for (let opts of sources) {
opts.resourcePath = issuer;

parsedTags.push(...HtmlParser.parseTag(content, opts));
}
parsedTags = parsedTags.sort(comparePos);
Expand All @@ -34,7 +35,7 @@ class Template {
let pos = 0;

for (let { tag, source, parsedAttrs } of parsedTags) {
for (let { type, attr, startPos, endPos, value, offset, inEscapedDoubleQuotes } of parsedAttrs) {
for (let { type, attr, startPos, endPos, value, quote, offset, inEscapedDoubleQuotes } of parsedAttrs) {
const result = this.resolveFile({
isBasedir,
type,
Expand All @@ -61,7 +62,10 @@ class Template {
// note: if the hook returns `undefined`, then the hookResult contains the value of the first argument
const resolvedValue = hookResult && hookResult !== source ? hookResult : requireExpression;

output += content.slice(pos, startPos + offset) + resolvedValue;
// enclose the value in quotes
if (!quote) quote = '';

output += content.slice(pos, startPos + offset) + quote + resolvedValue + quote;
pos = endPos + offset;
}
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions test/cases/resolve-attr-json-require/expected/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script src="js/main.bundle.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>

<a id="test-elm" href="#" data-image='{ "alt":"picture", "imgSrc": "img/kiwi.da3e3cc9.png", "bgImage": { "imgSrc": "img/lemon.7b66be8e.png" } }'>
<img src="img/image.697ef306.png" >
</a>
</body>
</html>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions test/cases/resolve-attr-json-require/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>

<a id="test-elm" href="#" data-image='{ "alt":"picture", "imgSrc": require("./kiwi.png"), "bgImage": { "imgSrc": require("./lemon.png") } }'>
<img src="./image.png" >
</a>
</body>
</html>
Binary file added test/cases/resolve-attr-json-require/src/kiwi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions test/cases/resolve-attr-json-require/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const elm = document.getElementById('test-elm');

const value = elm.getAttribute('data-bigpicture');
const data = JSON.parse(value);

console.log('>> main', { value }, data);
3 changes: 3 additions & 0 deletions test/cases/resolve-attr-json-require/src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h1 {
color: red;
}
57 changes: 57 additions & 0 deletions test/cases/resolve-attr-json-require/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const path = require('path');
const HtmlBundlerPlugin = require('@test/html-bundler-webpack-plugin');

module.exports = {
mode: 'production',

output: {
path: path.join(__dirname, 'dist/'),
},

plugins: [
new HtmlBundlerPlugin({
entry: {
index: './src/index.html',
},

js: {
// output filename of extracted JS
filename: 'js/[name].bundle.js',
},

css: {
// output filename of extracted CSS
filename: 'css/[name].bundle.css',
},

loaderOptions: {
sources: [
{
tag: 'a',
attributes: ['data-image'],
filter: (obj) => {
//console.log('### Filter: ', obj);
},
},
],
},
}),
],

module: {
rules: [
{
test: /\.css$/,
use: ['css-loader'],
},

{
test: /\.(png|jpe?g|ico|svg)$/,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:8][ext]',
},
},
],
},
};
Loading

0 comments on commit 4185ada

Please sign in to comment.