1 | ///@ts-check
|
2 | // Node libs
|
3 | ;
|
4 | var fs = require("fs");
|
5 | var path = require("path");
|
6 | var mime = require("mime");
|
7 | var colors = require("ansi-colors");
|
8 | const imagemin = require("imagemin");
|
9 |
|
10 | const getDefaultPlugins = require("./image-min").getDefaultPlugins;
|
11 | var fancyLog = require("../log/logger");
|
12 |
|
13 | // Cache regex's
|
14 | var rImages = /([\s\S]*?)(url\(([^)]+)\))(?!\s*[;,]?\s*\/\*\s*base64:skip\s*\*\/)|([\s\S]+)/gim;
|
15 | var rExternal = /^\W*([a-zA-z]+:)?\/\//;
|
16 | // var rSchemeless = /^\/\//;
|
17 | var rData = /^\W*data:/;
|
18 | var rQuotes = /['"]/g;
|
19 | var rParams = /([?#].*)$/g;
|
20 |
|
21 | function 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 |
|
32 | const TITLE = colors.gray("inline:");
|
33 | function 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 |
|
43 | function isLocalFile(url) {
|
44 | return !rExternal.test(url) && !rData.test(url);
|
45 | }
|
46 | // Grunt export wrapper
|
47 | module.exports = (function () {
|
48 | ;
|
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 | })();
|