@_gio/gulp-html-php-picture
Version:
generate <picture> from <img> with gulp
322 lines (268 loc) • 15.3 kB
JavaScript
;
// dependencies
const through = require('through2')
const fs = require('fs');
const path = require('path');
const pluginName = 'gulp-html-php-picture'
const PluginError = require("plugin-error")
module.exports = function (customParameters, pluginLogName=pluginName) {
return through.obj(function (file, enc, cb) {
// Errors log
if (file.isNull()) {
cb(null, file)
return
}
if (file.isStream()) {
cb(new PluginError(pluginLogName, 'Streaming not supported'))
return
}
try {
const parameters = {
imgFolder: false, // `${buildFolder}/`
extensions: ['.jpg', '.jpeg'],
ignoreClassname: 'img-ignore',
ignoreAttribute: 'data-ignore',
ignoreAvifAttribute: 'data-avif-ignore',
pictureClassAttribute: 'data-picture-class',
addPngAttribute: 'data-add-png',
add2xRetinaAttribute: 'data-dppx-two',
add3xRetinaAttribute: 'data-dppx-three',
logger: true,
sortBySize: true,
ignoreScripts: true,
ignoreComments: true,
filterUnexistedImages: true,
sourceExtensions: [
{
extension: 'avif',
mimetype: 'image/avif',
},
{
extension: 'webp',
mimetype: 'image/webp',
},
],
...customParameters
};
const EXTENSION_REGEX = /(?<=src=[\'\"][\w\W]*)\.[\w]+(?=[\'\"])/i; // Get images extension from string
const PICTURE_CLASS_REGEX = new RegExp(`<img.*?(${parameters.pictureClassAttribute}=([\\"\\']\\S+[\\"\\'])){1}.*?[^>]*[\\/]?>`, "i"); // Get attr pictureClass from string
const IMG_SRC_REGEX = /src=[\'\"]((?:<\?php(?:(?:(?!php)[^?]>|(?!php)[^?]|...\?[^?])*)\?>)*?[\s]*?[^\"\s]+[\s]*?)[\'\"]\s*?/i; // Get Image src value from string
const IMG_REGEX = /<img\s*?(?:class\=[\'\"]((?:\s*?(?:<\?php(?:(?:(?!php)[^?]>|(?!php)[^?]|...\?[^?])*)\?>)*?[\s]*?[\w\s]*?[^\"\']\s*?)+)[\'\"])?\s*?src\=[\'\"](?:<\?php(?:(?:(?!php)[^?]>|(?!php)[^?]|...\?[^?])*)\?>)*?[\s]*?[^\'\"]+[\s]*?[\'\"]\s*?(?:[a-zA-Z-_]*\=?(?:[\'\"](?:[\s]*?[\w|\s|\-]*?)*?(?:<\?php(?:(?:(?!php)[^?]>|(?!php)[^?]|...\?[^?])*)\?>)*?[\s]*?[\w\s?\W?]*?[\'\"]+?\s*)*?\s*)*?\s*?[\/]?>/gi; // Get <img> from HTML code string
const IMG_CLASS_REGEX = /(?: class)=(?:[\'\"]((?:\s*?(?:<\?php(?:(?:(?!php)[^?]>|(?!php)[^?]|...\?[^?])*)\?>)*?[\s]*?[\w\s]*?[^\"\']\s*?)+)[\'\"])/; // Get class="" from image
const PICTURE_REGEX = /<\s*picture[^>]*>([\w\W]*?)<\s*\/\s*picture\s*>/gi; // Get <picture> from HTML code string
const SCRIPT_REGEX = /<\s*script[^>]*>([\w\W]*?)<\s*\/\s*script\s*>/gi; // Get <script> from HTML code string
const COMMENTS_REGEX = /(?=<!--)([\s\S]*?)-->/gi; // Get comments from HTML code string
// Get HTML code string from File
const htmlString = file.contents.toString();
// Variables
let newHtmlString;
let tempHtmlString;
let imagesConvertedToPictureCount = 0;
let imagesIgnored = 0;
/*** Comments ***/
// Save all comments from source HTML
const comments = htmlString.match(COMMENTS_REGEX);
// Remove all comments from html and replace it with {{}}
if (parameters.ignoreComments) {
tempHtmlString = htmlString.replace(
COMMENTS_REGEX,
`{{ ${pluginLogName}__insert-comment }}`
);
}
/*** Scripts ***/
// Save all <script> from source HTML
const scripts = tempHtmlString.match(SCRIPT_REGEX);
// Remove all <script> from html and replace it with {{}}
if (parameters.ignoreScripts) {
tempHtmlString = tempHtmlString.replace(
SCRIPT_REGEX,
`{{ ${pluginLogName}__insert-script }}`
);
}
/*** <picture> ***/
// Save all <picture> from source HTML
const pictures = tempHtmlString.match(PICTURE_REGEX);
// Remove all <picture> from html and replace it with {{}}
tempHtmlString = tempHtmlString.replace(
PICTURE_REGEX,
`{{ ${pluginLogName}__insert-picture }}`
);
/*** <img> ***/
// Save all <img> from source HTML
const images = tempHtmlString.match(IMG_REGEX);
// Remove all <img> from html and replace it with {{}}
tempHtmlString = tempHtmlString.replace(
IMG_REGEX,
`{{ ${pluginLogName}__insert-image }}`
);
newHtmlString = tempHtmlString
if (images) {
const newImages = images.map((image) => {
// Flags
let hasIgnoreClass = false;
let hasIgnoreAttribute = false;
const ignoreAvif = image.includes(parameters.ignoreAvifAttribute);
const imageRetinaX2 = image.includes(parameters.add2xRetinaAttribute);
const imageRetinaX3 = image.includes(parameters.add3xRetinaAttribute);
const hasRetina = (imageRetinaX2 || imageRetinaX3);
if(image.includes(parameters.addPngAttribute) && !parameters.extensions.includes('.png')) {
parameters.extensions.push('.png');
image = image.replace(parameters.addPngAttribute, '')
}
if(imageRetinaX2) image = image.replace(parameters.add2xRetinaAttribute, '')
if(imageRetinaX3) image = image.replace(parameters.add3xRetinaAttribute, '')
// If image has class="" attribute
if (image.match(IMG_CLASS_REGEX)) {
const matches = image.match(IMG_CLASS_REGEX)
let classesArray = matches[1].split(' ')
// If image has a img-ignore class
if (hasRetina || classesArray.includes(parameters.ignoreClassname)) {
if(classesArray.includes(parameters.ignoreClassname)) hasIgnoreClass = true;
// Remove class img-ignore
classesArray = classesArray.filter(function (item) {
return item !== parameters.ignoreClassname;
})
image = image.replace(matches[0], ` class="${classesArray.join(' ')}"`);
}
}
// If image has data-ignore attribute
if (image.includes(parameters.ignoreAttribute)) {
hasIgnoreAttribute = true;
// Remove attribute data-ignore
image = image.replace(parameters.ignoreAttribute, '');
}
// If image has avif ignore attribute
if(ignoreAvif) {
// Remove avif ignore attribute
image = image.replace(parameters.ignoreAvifAttribute, '');
}
// Remove attribute data-picture-class if image should be ignored
if ((hasIgnoreClass || hasIgnoreAttribute) &&
PICTURE_CLASS_REGEX.test(image)) {
const pictureClassAttr = image.match(PICTURE_CLASS_REGEX)[1];
image = image.replace(pictureClassAttr, '');
}
// Convert <img> to <picture> if img has extenstion and not ignored
if (!hasIgnoreClass && !hasIgnoreAttribute && EXTENSION_REGEX.test(image)) {
const imageExtension = image.match(EXTENSION_REGEX)[0];
// Check if extension is allowed
if (!parameters.extensions.includes(imageExtension)) {
return image;
}
const srcValueWithoutExt = image.match(IMG_SRC_REGEX)[1].replace(imageExtension, '');
let pictureClass = false;
if (PICTURE_CLASS_REGEX.test(image)) {
const pictureClassAttr = image.match(PICTURE_CLASS_REGEX)[1];
pictureClass = image.match(PICTURE_CLASS_REGEX)[2];
image = image.replace(pictureClassAttr, '');
}
function getFileSize(src) {
try {
return fs.statSync(src).size
} catch (err) {
return 0;
}
}
if(hasRetina) {
let retinaX2Html = imageRetinaX2 ? `, ${srcValueWithoutExt}@2x${imageExtension} 2x` : '';
let retinaX3Html = imageRetinaX3 ? `, ${srcValueWithoutExt}@3x${imageExtension} 3x` : '';
let imageSrcsetHtml = `srcset="${srcValueWithoutExt}${imageExtension} 1x${retinaX2Html}${retinaX3Html}" `;
const imageSrcsetInsertPosition = image.indexOf('src');
const partBeforeSrcset = image.slice(0, imageSrcsetInsertPosition);
const partAfterSrcset = image.slice(imageSrcsetInsertPosition);
image = `${partBeforeSrcset}${imageSrcsetHtml}${partAfterSrcset}`;
}
let imagesArray = [
{
name: 'original',
html: image,
size: getFileSize(`${parameters.imgFolder}${srcValueWithoutExt}${imageExtension}`)
},
]
if (parameters.sourceExtensions) {
parameters.sourceExtensions.forEach((imgElement) => {
if(ignoreAvif && imgElement.extension === 'avif') return;
let retinaX2Html = imageRetinaX2 ? `, ${srcValueWithoutExt}@2x.${imgElement.extension} 2x` : '';
let retinaX3Html = imageRetinaX3 ? `, ${srcValueWithoutExt}@3x.${imgElement.extension} 3x` : '';
let retinaX1Pointer = hasRetina ? ' 1x' : '';
let pictureSourceHtml = `<source srcset="${srcValueWithoutExt}.${imgElement.extension}${retinaX1Pointer}${retinaX2Html}${retinaX3Html}" type="${imgElement.mimetype}">`
imagesArray.unshift(
{
name: imgElement.extension,
html: pictureSourceHtml,
size: getFileSize(`${parameters.imgFolder}${srcValueWithoutExt}.${imgElement.extension}`)
}
)
})
}
// Filter Images
if (parameters.imgFolder && parameters.filterUnexistedImages) {
imagesArray = imagesArray.filter(function (img) {
if (img.name !== 'original' && img.size === 0) {
// Remove unexisted img
} else {
return img;
}
})
}
// Sort Images
if (parameters.imgFolder && parameters.sortBySize) {
imagesArray.sort((a, b) => a.size > b.size ? 1 : -1);
}
if (pictureClass) {
pictureClass = ` class=${pictureClass} `
} else {
pictureClass = ''
}
let newTag = `<picture${pictureClass}>`;
imagesArray.forEach((img) => {
newTag += img.html;
})
newTag += `</picture>`;
imagesConvertedToPictureCount++;
return newTag;
}
// Ignored Images Counter
if (hasIgnoreAttribute || hasIgnoreClass) {
imagesIgnored++;
}
return image;
});
// Replace {{}} with new tags
newImages.forEach((newImage) => {
newHtmlString = newHtmlString.replace(`{{ ${pluginLogName}__insert-image }}`, newImage);
});
}
if (pictures) {
// Insert <picture> back to html
pictures.forEach((picture) => {
newHtmlString = newHtmlString.replace(`{{ ${pluginLogName}__insert-picture }}`, picture);
});
}
if (comments && parameters.ignoreComments) {
// Insert comments back to html
comments.forEach((comment) => {
newHtmlString = newHtmlString.replace(`{{ ${pluginLogName}__insert-comment }}`, comment);
});
}
if (scripts && parameters.ignoreScripts) {
// Insert <picture> back to html
scripts.forEach((script) => {
newHtmlString = newHtmlString.replace(`{{ ${pluginLogName}__insert-script }}`, script);
});
}
file.contents = new Buffer.from(newHtmlString || htmlString);
this.push(file);
// Logger
if (parameters.logger) {
console.info(`${pluginLogName}:`, path.basename(file.path));
console.info(`${pluginLogName}:`, `${imagesConvertedToPictureCount} images converted to <picture>`);
console.info(`${pluginLogName}:`, `${imagesIgnored} images ignored`);
}
this.push(file)
} catch (err) {
this.emit('error', new PluginError(pluginLogName, err));
}
cb()
})
}