UNPKG

11.5 kBJavaScriptView Raw
1///@ts-check
2// Node libs
3"use strict";
4var fs = require("fs");
5var path = require("path");
6var mime = require("mime");
7var colors = require("ansi-colors");
8const imagemin = require("imagemin");
9
10const getDefaultPlugins = require("./image-min").getDefaultPlugins;
11var fancyLog = require("../log/logger");
12
13// Cache regex's
14var rImages = /([\s\S]*?)(url\(([^)]+)\))(?!\s*[;,]?\s*\/\*\s*base64:skip\s*\*\/)|([\s\S]+)/gim;
15var rExternal = /^\W*([a-zA-z]+:)?\/\//;
16// var rSchemeless = /^\/\//;
17var rData = /^\W*data:/;
18var rQuotes = /['"]/g;
19var rParams = /([?#].*)$/g;
20
21function whilst(condition, action) {
22 const loop = actionResult => {
23 if (condition(actionResult)) {
24 return Promise.resolve(action()).then(loop);
25 }else{
26 return Promise.resolve();
27 }
28 };
29 return loop();
30}
31
32const TITLE = colors.gray("inline:");
33function log(img, file) {
34 fancyLog.info(
35 TITLE,
36 // path.relative(path.join(file.cwd, file.base), img)
37 colors.underline(path.relative(file.base, img)),
38 colors.gray("→"),
39 colors.gray("(" + colors.underline(path.relative(file.base, file.path)) + ")"),
40 );
41}
42
43function isLocalFile(url) {
44 return !rExternal.test(url) && !rData.test(url);
45}
46// Grunt export wrapper
47module.exports = (function () {
48 "use strict";
49
50 var exports = {};
51
52 /**
53 * Takes a CSS file as input, goes through it line by line, and base64
54 * encodes any images it finds.
55 *
56 * @param file Relative or absolute path to a source stylesheet file.
57 * @param opts Options object
58 * @param done Function to call once encoding has finished.
59 */
60 exports.stylesheet = function (file, opts, done) {
61 opts = opts || {};
62
63 // Cache of already converted images
64 var cache = {};
65
66 // Shift args if no options object is specified
67 if (typeof opts === "function") {
68 done = opts;
69 opts = {};
70 }
71
72 // var deleteAfterEncoding = opts.deleteAfterEncoding;
73 var src = file.contents.toString();
74 var result = "";
75 var match, img, line, tasks, group;
76
77 whilst(
78 function () {
79 group = rImages.exec(src);
80 return group != null;
81 },
82 function () {
83 // console.log( group[1],"\n", group[2],"\n",group[3],"\n",group[4])
84 // if there is another url to be processed, then:
85 // group[1] will hold everything up to the url declaration
86 // group[2] will hold the complete url declaration (useful if no encoding will take place)
87 // group[3] will hold the contents of the url declaration
88 // group[4] will be undefined
89 // if there is no other url to be processed, then group[1-3] will be undefined
90 // group[4] will hold the entire string
91
92 // console.log(group[2]);
93
94 if (group[4] == null) {
95 result += group[1];
96
97 var rawUrl = group[3].trim();
98 img = rawUrl.replace(rQuotes, "").replace(rParams, ""); // remove query string/hash parmams in the filename, like foo.png?bar or foo.png#bar
99
100 var test = true;
101 if (opts.extensions) {
102 //test for extensions if it provided
103 var imgExt = img.split(".").pop();
104 if (typeof opts.extensions === "function") {
105 test = opts.extensions(imgExt, rawUrl);
106 } else {
107 test = opts.extensions.some(function (ext) {
108 return ext instanceof RegExp ? ext.test(rawUrl) : ext === imgExt;
109 });
110 }
111 }
112
113 if (test && opts.exclude) {
114 //test for extensions to exclude if it provided
115 if (typeof opts.exclude === "function") {
116 test = !opts.exclude(rawUrl);
117 } else {
118 test = !opts.exclude.some(function (pattern) {
119 return pattern instanceof RegExp ? pattern.test(rawUrl) : rawUrl.indexOf(pattern) > -1;
120 });
121 }
122 }
123
124 if (!test) {
125 if (opts.debug) {
126 fancyLog(TITLE, img, " skipped by extension or exclude filters");
127 }
128 return result += group[2];
129 // return complete();
130 // resolve(result);
131 }
132 if (!isLocalFile(rawUrl)) {
133 if (opts.debug) {
134 fancyLog(TITLE, img, " skipped not local file");
135 }
136 return result += group[2];
137 }
138 if(!group[1].trim().endsWith(':')){
139 if (opts.debug) {
140 fancyLog(TITLE, img, " not inline image");
141 }
142 return result += group[2];
143 }
144 // see if this img was already processed before...
145 if (cache[img]) {
146 // grunt.log.error("The image " + img + " has already been encoded elsewhere in your stylesheet. I'm going to do it again, but it's going to make your stylesheet a lot larger than it needs to be.");
147 return result += cache[img];
148 // resolve(result);
149 // return;
150 } else {
151
152 var loc = opts.baseDir ? path.join(opts.baseDir, img) : path.join(path.dirname(file.path), img);
153
154 // If that didn't work, try finding the image relative to
155 // the current file instead.
156 if (!fs.existsSync(loc)) {
157 (opts.debug) && fancyLog.info(loc, ' file doesn\'t exist');
158 loc = path.join(file.cwd, img);
159 if (!fs.existsSync(loc)) {
160 (opts.debug) && fancyLog.info(loc, ' file doesn\'t exist');
161 loc = path.join(opts.src, opts.assets, img)
162 if (!fs.existsSync(loc)) {
163 (opts.debug) && fancyLog.info(loc, ' file doesn\'t exist');
164 loc = path.join(opts.src, img);
165 if (!fs.existsSync(loc)) {
166 fancyLog.warn(TITLE, img, colors.red("file doesn't exist"));
167 return result;
168 }
169 }
170 // return complete();
171 }
172 }
173
174 // }
175
176 // Test for scheme less URLs => "//example.com/image.png"
177 // if (!is_local_file && rSchemeless.test(loc)) {
178 // loc = 'http:' + loc;
179 // }
180
181 log(loc, file);
182 return new Promise(function (resolve, reject) {
183 exports.image(loc, opts, function (err, resp, cacheable) {
184 if (err == null) {
185 var url = "url(" + resp + ")";
186 result += url;
187
188 if (cacheable !== false) {
189 cache[img] = url;
190 }
191
192 // if (deleteAfterEncoding && is_local_file) {
193 // if (opts.debug) {
194 // console.info("Deleting file: " + loc);
195 // }
196 // fs.unlinkSync(loc);
197 // }
198 } else {
199 result += group[2];
200 }
201
202 // complete();
203 resolve(result);
204 });
205 })
206 }
207 } else {
208 result += group[4];
209 return result;
210 }
211
212 },
213 ).then(() => done(null, result));
214 };
215
216 /**
217 * Takes an image (absolute path or remote) and base64 encodes it.
218 *
219 * @param img Absolute, resolved path to an image
220 * @param opts Options object
221 * @return A data URI string (mime type, base64 img, etc.) that a browser can interpret as an image
222 */
223 exports.image = function (img, opts, done) {
224 // Shift args
225 if (typeof opts === "function") {
226 done = opts;
227 opts = {};
228 }
229
230 var complete = function (err, encoded, cacheable) {
231 // Return the original source if an error occurred
232 if (err) {
233 // grunt.log.error(err);
234 done(err, img, false);
235
236 // Otherwise cache the processed image and return it
237 } else {
238 done(null, encoded, cacheable);
239 }
240 };
241
242 // Already base64 encoded?
243 if (rData.test(img)) {
244 complete(null, img, false);
245 } else {
246 // Does the image actually exist?
247 if (!fs.existsSync(img) || !fs.lstatSync(img).isFile()) {
248 // grunt.fail.warn("File " + img + " does not exist");
249 if (opts.debug) {
250 fancyLog.warn("File " + img + " does not exist");
251 }
252 complete(true, img, false);
253 return;
254 }
255
256 // grunt.log.writeln("Encoding file: " + img);
257 if (opts.debug) {
258 fancyLog.info("Encoding file: " + img);
259 }
260
261 exports.getDataURI(img).then(encoded => complete(null, encoded, true));
262 }
263 };
264
265 /**
266 * Base64 encodes an image and builds the data URI string
267 *
268 * @param img The source image path
269 * @return {Promise<string>} Data URI string
270 */
271 exports.getDataURI = function (img) {
272 const mimeType = mime.getType(img);
273 // let ret = "data:";
274 // ret += mimeType;
275 if ("image/svg+xml" === mimeType) {
276 // ret += ";charset=UTF-8,";
277 return imagemin([img], {
278 plugins: getDefaultPlugins(),
279 })
280 .then(f => f[0].data.toString())
281 .then(encodeURI)
282 .then(data => `"data:image/svg+xml;charset=UTF-8,${data}"`);
283 // ret += encodeURI(img.toString());
284 } else {
285 // ret += ";base64,";
286 // ret += img.toString("base64");
287 return imagemin([img], {
288 plugins: getDefaultPlugins(),
289 })
290 .then(f => f[0].data.toString("base64"))
291 .then(data => `"data:${mimeType};base64,${data}"`);
292 }
293 };
294
295 return exports;
296})();