'use strict'; const https = require('https'); const http = require('http'); const path = require('path'); const url = require('url'); const fs = require('fs'); const PluginError = require('plugin-error'); const through = require('through2'); const cheerio = require('cheerio'); const { log } = require('./util/util'); const PLUGIN_NAME = 'gulp-inline-images'; const MIME_TYPE_REGEX = /.+\/([^\s]*)/; const INLINE_ATTR = 'inline'; const NOT_INLINE_ATTR = `!${INLINE_ATTR}`; function inlineImg(options = {}) { const selector = options.selector || 'img[src]'; const attribute = options.attribute || 'src'; const getHTTP = options.getHTTP || false; return through.obj(function(file, encoding, callback) { if (file.isStream()) { this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!')); return callback(); } if (file.isBuffer()) { const contents = file.contents.toString(encoding); // Load it into cheerio's virtual DOM for easy manipulation const $ = cheerio.load(contents); const inlineFlag = $(`img[${INLINE_ATTR}]`); // If images with an inline attr are found that is the selection we want const imgTags = inlineFlag.length ? inlineFlag : $(selector); let count = 0; imgTags.each(function() { const $img = $(this); const src = $img.attr(attribute); // Save the file format from the extension const extFormat = path.extname(src).substr(1); // If inlineFlag tags were found we want to remove the inline tag if (inlineFlag.length) { $img.removeAttr(INLINE_ATTR); } // Find !inline attribute const notInlineFlag = $img.attr(NOT_INLINE_ATTR); if (typeof notInlineFlag !== typeof undefined && notInlineFlag !== false) { // Remove the tag and don't process this file return $img.removeAttr(NOT_INLINE_ATTR); } // Count async ops count++; getSrcBase64(options.basedir || file.base, getHTTP, src, (err, result, resFormat, skipFormatting) => { if (err) { log.warn(`Failed to load http image. Check the format of ${src}.`); log.error(err); } else { // Need a format in and a result for this to work if (!skipFormatting) { if (result && (extFormat || resFormat)) { $img.attr('src', `data:image/${extFormat};base64,${result}`); } else { $img.attr('src', ``); $img.attr('alt', `Image not found, Please check Url`); log.warn(`Failed to read image. Check the format of ${src}.`); } } if (!--count) { file.contents = Buffer.from($.html()); callback(null, file); } } }); }); // If no files are processing we don't need to wait as none were ever started if (!imgTags.length) { file.contents = Buffer.from($.html()); callback(null, file); } } }); } function getHTTPBase64(url, callback) { // Get applicable library const lib = url.startsWith('https') ? https : http; // Initiate a git request to our URL const req = lib.get(url, res => { // Check for redirect if (res.statusCode >= 301 && res.statusCode < 400 && res.headers.location) { // Redirect return getHTTPBase64(res.headers.location, callback); } // Check for HTTP errors if (res.statusCode < 200 || res.statusCode >= 400) { return callback(new Error('Failed to load page, status code: ' + res.statusCode)); } // Get file format let format; if (res.headers['content-type']) { const matches = res.headers['content-type'].match(MIME_TYPE_REGEX); if (matches) { format = matches[1]; } } // Create an empty buffer to store the body in let body = Buffer.from([]); // Append each chunk to the body res.on('data', chunk => (body = Buffer.concat([body, chunk]))); // Done callback res.on('end', () => callback(null, body.toString('base64'), format)); }); // Listen for network errors req.on('error', err => callback(err)); } function getSrcBase64(base, getHTTP, src, callback) { // TODO: @deprecated — since v11.0.0 url.parse should be replaced with url.URL() ctor if (!url.parse(src).hostname) { // Get local file const filePath = path.join(base, src); if (fs.existsSync(filePath)) { fs.readFile(filePath, 'base64', callback); } else { callback(null); } } else { // Get remote file if (getHTTP) { return getHTTPBase64(src, callback); } else { callback(null, src, null, true); } } } module.exports = { inlineImg, getHTTPBase64, getSrcBase64 };