UNPKG

@_gio/gulp-html-php-picture

Version:

generate <picture> from <img> with gulp

322 lines (268 loc) 15.3 kB
"use strict"; // 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() }) }