1 | var fs = require('fs');
|
---|
2 | var path = require('path');
|
---|
3 |
|
---|
4 | var applySourceMaps = require('./apply-source-maps');
|
---|
5 | var extractImportUrlAndMedia = require('./extract-import-url-and-media');
|
---|
6 | var isAllowedResource = require('./is-allowed-resource');
|
---|
7 | var loadOriginalSources = require('./load-original-sources');
|
---|
8 | var normalizePath = require('./normalize-path');
|
---|
9 | var rebase = require('./rebase');
|
---|
10 | var rebaseLocalMap = require('./rebase-local-map');
|
---|
11 | var rebaseRemoteMap = require('./rebase-remote-map');
|
---|
12 | var restoreImport = require('./restore-import');
|
---|
13 |
|
---|
14 | var tokenize = require('../tokenizer/tokenize');
|
---|
15 | var Token = require('../tokenizer/token');
|
---|
16 | var Marker = require('../tokenizer/marker');
|
---|
17 | var hasProtocol = require('../utils/has-protocol');
|
---|
18 | var isImport = require('../utils/is-import');
|
---|
19 | var isRemoteResource = require('../utils/is-remote-resource');
|
---|
20 |
|
---|
21 | var UNKNOWN_URI = 'uri:unknown';
|
---|
22 | var FILE_RESOURCE_PROTOCOL = 'file://';
|
---|
23 |
|
---|
24 | function readSources(input, context, callback) {
|
---|
25 | return doReadSources(input, context, function(tokens) {
|
---|
26 | return applySourceMaps(tokens, context, function() {
|
---|
27 | return loadOriginalSources(context, function() { return callback(tokens); });
|
---|
28 | });
|
---|
29 | });
|
---|
30 | }
|
---|
31 |
|
---|
32 | function doReadSources(input, context, callback) {
|
---|
33 | if (typeof input == 'string') {
|
---|
34 | return fromString(input, context, callback);
|
---|
35 | } if (Buffer.isBuffer(input)) {
|
---|
36 | return fromString(input.toString(), context, callback);
|
---|
37 | } if (Array.isArray(input)) {
|
---|
38 | return fromArray(input, context, callback);
|
---|
39 | } if (typeof input == 'object') {
|
---|
40 | return fromHash(input, context, callback);
|
---|
41 | }
|
---|
42 | }
|
---|
43 |
|
---|
44 | function fromString(input, context, callback) {
|
---|
45 | context.source = undefined;
|
---|
46 | context.sourcesContent[undefined] = input;
|
---|
47 | context.stats.originalSize += input.length;
|
---|
48 |
|
---|
49 | return fromStyles(input, context, { inline: context.options.inline }, callback);
|
---|
50 | }
|
---|
51 |
|
---|
52 | function fromArray(input, context, callback) {
|
---|
53 | var inputAsImports = input.reduce(function(accumulator, uriOrHash) {
|
---|
54 | if (typeof uriOrHash === 'string') {
|
---|
55 | return addStringSource(uriOrHash, accumulator);
|
---|
56 | }
|
---|
57 | return addHashSource(uriOrHash, context, accumulator);
|
---|
58 | }, []);
|
---|
59 |
|
---|
60 | return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
|
---|
61 | }
|
---|
62 |
|
---|
63 | function fromHash(input, context, callback) {
|
---|
64 | var inputAsImports = addHashSource(input, context, []);
|
---|
65 | return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
|
---|
66 | }
|
---|
67 |
|
---|
68 | function addStringSource(input, imports) {
|
---|
69 | imports.push(restoreAsImport(normalizeUri(input)));
|
---|
70 | return imports;
|
---|
71 | }
|
---|
72 |
|
---|
73 | function addHashSource(input, context, imports) {
|
---|
74 | var uri;
|
---|
75 | var normalizedUri;
|
---|
76 | var source;
|
---|
77 |
|
---|
78 | for (uri in input) {
|
---|
79 | source = input[uri];
|
---|
80 | normalizedUri = normalizeUri(uri);
|
---|
81 |
|
---|
82 | imports.push(restoreAsImport(normalizedUri));
|
---|
83 |
|
---|
84 | context.sourcesContent[normalizedUri] = source.styles;
|
---|
85 |
|
---|
86 | if (source.sourceMap) {
|
---|
87 | trackSourceMap(source.sourceMap, normalizedUri, context);
|
---|
88 | }
|
---|
89 | }
|
---|
90 |
|
---|
91 | return imports;
|
---|
92 | }
|
---|
93 |
|
---|
94 | function normalizeUri(uri) {
|
---|
95 | var currentPath = path.resolve('');
|
---|
96 | var absoluteUri;
|
---|
97 | var relativeToCurrentPath;
|
---|
98 | var normalizedUri;
|
---|
99 |
|
---|
100 | if (isRemoteResource(uri)) {
|
---|
101 | return uri;
|
---|
102 | }
|
---|
103 |
|
---|
104 | absoluteUri = path.isAbsolute(uri)
|
---|
105 | ? uri
|
---|
106 | : path.resolve(uri);
|
---|
107 | relativeToCurrentPath = path.relative(currentPath, absoluteUri);
|
---|
108 | normalizedUri = normalizePath(relativeToCurrentPath);
|
---|
109 |
|
---|
110 | return normalizedUri;
|
---|
111 | }
|
---|
112 |
|
---|
113 | function trackSourceMap(sourceMap, uri, context) {
|
---|
114 | var parsedMap = typeof sourceMap == 'string'
|
---|
115 | ? JSON.parse(sourceMap)
|
---|
116 | : sourceMap;
|
---|
117 | var rebasedMap = isRemoteResource(uri)
|
---|
118 | ? rebaseRemoteMap(parsedMap, uri)
|
---|
119 | : rebaseLocalMap(parsedMap, uri || UNKNOWN_URI, context.options.rebaseTo);
|
---|
120 |
|
---|
121 | context.inputSourceMapTracker.track(uri, rebasedMap);
|
---|
122 | }
|
---|
123 |
|
---|
124 | function restoreAsImport(uri) {
|
---|
125 | return restoreImport('url(' + uri + ')', '') + Marker.SEMICOLON;
|
---|
126 | }
|
---|
127 |
|
---|
128 | function fromStyles(styles, context, parentInlinerContext, callback) {
|
---|
129 | var tokens;
|
---|
130 | var rebaseConfig = {};
|
---|
131 |
|
---|
132 | if (!context.source) {
|
---|
133 | rebaseConfig.fromBase = path.resolve('');
|
---|
134 | rebaseConfig.toBase = context.options.rebaseTo;
|
---|
135 | } else if (isRemoteResource(context.source)) {
|
---|
136 | rebaseConfig.fromBase = context.source;
|
---|
137 | rebaseConfig.toBase = context.source;
|
---|
138 | } else if (path.isAbsolute(context.source)) {
|
---|
139 | rebaseConfig.fromBase = path.dirname(context.source);
|
---|
140 | rebaseConfig.toBase = context.options.rebaseTo;
|
---|
141 | } else {
|
---|
142 | rebaseConfig.fromBase = path.dirname(path.resolve(context.source));
|
---|
143 | rebaseConfig.toBase = context.options.rebaseTo;
|
---|
144 | }
|
---|
145 |
|
---|
146 | tokens = tokenize(styles, context);
|
---|
147 | tokens = rebase(tokens, context.options.rebase, context.validator, rebaseConfig);
|
---|
148 |
|
---|
149 | return allowsAnyImports(parentInlinerContext.inline)
|
---|
150 | ? inline(tokens, context, parentInlinerContext, callback)
|
---|
151 | : callback(tokens);
|
---|
152 | }
|
---|
153 |
|
---|
154 | function allowsAnyImports(inline) {
|
---|
155 | return !(inline.length == 1 && inline[0] == 'none');
|
---|
156 | }
|
---|
157 |
|
---|
158 | function inline(tokens, externalContext, parentInlinerContext, callback) {
|
---|
159 | var inlinerContext = {
|
---|
160 | afterContent: false,
|
---|
161 | callback: callback,
|
---|
162 | errors: externalContext.errors,
|
---|
163 | externalContext: externalContext,
|
---|
164 | fetch: externalContext.options.fetch,
|
---|
165 | inlinedStylesheets: parentInlinerContext.inlinedStylesheets || externalContext.inlinedStylesheets,
|
---|
166 | inline: parentInlinerContext.inline,
|
---|
167 | inlineRequest: externalContext.options.inlineRequest,
|
---|
168 | inlineTimeout: externalContext.options.inlineTimeout,
|
---|
169 | isRemote: parentInlinerContext.isRemote || false,
|
---|
170 | localOnly: externalContext.localOnly,
|
---|
171 | outputTokens: [],
|
---|
172 | rebaseTo: externalContext.options.rebaseTo,
|
---|
173 | sourceTokens: tokens,
|
---|
174 | warnings: externalContext.warnings
|
---|
175 | };
|
---|
176 |
|
---|
177 | return doInlineImports(inlinerContext);
|
---|
178 | }
|
---|
179 |
|
---|
180 | function doInlineImports(inlinerContext) {
|
---|
181 | var token;
|
---|
182 | var i, l;
|
---|
183 |
|
---|
184 | for (i = 0, l = inlinerContext.sourceTokens.length; i < l; i++) {
|
---|
185 | token = inlinerContext.sourceTokens[i];
|
---|
186 |
|
---|
187 | if (token[0] == Token.AT_RULE && isImport(token[1])) {
|
---|
188 | inlinerContext.sourceTokens.splice(0, i);
|
---|
189 | return inlineStylesheet(token, inlinerContext);
|
---|
190 | } if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) {
|
---|
191 | inlinerContext.outputTokens.push(token);
|
---|
192 | } else {
|
---|
193 | inlinerContext.outputTokens.push(token);
|
---|
194 | inlinerContext.afterContent = true;
|
---|
195 | }
|
---|
196 | }
|
---|
197 |
|
---|
198 | inlinerContext.sourceTokens = [];
|
---|
199 | return inlinerContext.callback(inlinerContext.outputTokens);
|
---|
200 | }
|
---|
201 |
|
---|
202 | function inlineStylesheet(token, inlinerContext) {
|
---|
203 | var uriAndMediaQuery = extractImportUrlAndMedia(token[1]);
|
---|
204 | var uri = uriAndMediaQuery[0];
|
---|
205 | var mediaQuery = uriAndMediaQuery[1];
|
---|
206 | var metadata = token[2];
|
---|
207 |
|
---|
208 | return isRemoteResource(uri)
|
---|
209 | ? inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext)
|
---|
210 | : inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext);
|
---|
211 | }
|
---|
212 |
|
---|
213 | function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
|
---|
214 | var isAllowed = isAllowedResource(uri, true, inlinerContext.inline);
|
---|
215 | var originalUri = uri;
|
---|
216 | var isLoaded = uri in inlinerContext.externalContext.sourcesContent;
|
---|
217 | var isRuntimeResource = !hasProtocol(uri);
|
---|
218 |
|
---|
219 | if (inlinerContext.inlinedStylesheets.indexOf(uri) > -1) {
|
---|
220 | inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.');
|
---|
221 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
222 | return doInlineImports(inlinerContext);
|
---|
223 | } if (inlinerContext.localOnly && inlinerContext.afterContent) {
|
---|
224 | inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.');
|
---|
225 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
226 | return doInlineImports(inlinerContext);
|
---|
227 | } if (isRuntimeResource) {
|
---|
228 | inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no protocol given.');
|
---|
229 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
|
---|
230 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
231 | return doInlineImports(inlinerContext);
|
---|
232 | } if (inlinerContext.localOnly && !isLoaded) {
|
---|
233 | inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.');
|
---|
234 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
|
---|
235 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
236 | return doInlineImports(inlinerContext);
|
---|
237 | } if (!isAllowed && inlinerContext.afterContent) {
|
---|
238 | inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource is not allowed and after other content.');
|
---|
239 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
240 | return doInlineImports(inlinerContext);
|
---|
241 | } if (!isAllowed) {
|
---|
242 | inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as resource is not allowed.');
|
---|
243 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
|
---|
244 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
245 | return doInlineImports(inlinerContext);
|
---|
246 | }
|
---|
247 |
|
---|
248 | inlinerContext.inlinedStylesheets.push(uri);
|
---|
249 |
|
---|
250 | function whenLoaded(error, importedStyles) {
|
---|
251 | if (error) {
|
---|
252 | inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error);
|
---|
253 |
|
---|
254 | return process.nextTick(function() {
|
---|
255 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
|
---|
256 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
257 | doInlineImports(inlinerContext);
|
---|
258 | });
|
---|
259 | }
|
---|
260 |
|
---|
261 | inlinerContext.inline = inlinerContext.externalContext.options.inline;
|
---|
262 | inlinerContext.isRemote = true;
|
---|
263 |
|
---|
264 | inlinerContext.externalContext.source = originalUri;
|
---|
265 | inlinerContext.externalContext.sourcesContent[uri] = importedStyles;
|
---|
266 | inlinerContext.externalContext.stats.originalSize += importedStyles.length;
|
---|
267 |
|
---|
268 | return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function(importedTokens) {
|
---|
269 | importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
|
---|
270 |
|
---|
271 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
|
---|
272 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
273 |
|
---|
274 | return doInlineImports(inlinerContext);
|
---|
275 | });
|
---|
276 | }
|
---|
277 |
|
---|
278 | return isLoaded
|
---|
279 | ? whenLoaded(null, inlinerContext.externalContext.sourcesContent[uri])
|
---|
280 | : inlinerContext.fetch(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded);
|
---|
281 | }
|
---|
282 |
|
---|
283 | function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) {
|
---|
284 | var protocolLessUri = uri.replace(FILE_RESOURCE_PROTOCOL, '');
|
---|
285 | var currentPath = path.resolve('');
|
---|
286 | var absoluteUri = path.isAbsolute(protocolLessUri)
|
---|
287 | ? path.resolve(currentPath, protocolLessUri[0] == '/' ? protocolLessUri.substring(1) : protocolLessUri)
|
---|
288 | : path.resolve(inlinerContext.rebaseTo, protocolLessUri);
|
---|
289 | var relativeToCurrentPath = path.relative(currentPath, absoluteUri);
|
---|
290 | var importedStyles;
|
---|
291 | var isAllowed = isAllowedResource(protocolLessUri, false, inlinerContext.inline);
|
---|
292 | var normalizedPath = normalizePath(relativeToCurrentPath);
|
---|
293 | var isLoaded = normalizedPath in inlinerContext.externalContext.sourcesContent;
|
---|
294 |
|
---|
295 | if (inlinerContext.inlinedStylesheets.indexOf(absoluteUri) > -1) {
|
---|
296 | inlinerContext.warnings.push('Ignoring local @import of "' + protocolLessUri + '" as it has already been imported.');
|
---|
297 | } else if (isAllowed && !isLoaded && (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile())) {
|
---|
298 | inlinerContext.errors.push('Ignoring local @import of "' + protocolLessUri + '" as resource is missing.');
|
---|
299 | } else if (!isAllowed && inlinerContext.afterContent) {
|
---|
300 | inlinerContext.warnings.push('Ignoring local @import of "' + protocolLessUri + '" as resource is not allowed and after other content.');
|
---|
301 | } else if (inlinerContext.afterContent) {
|
---|
302 | inlinerContext.warnings.push('Ignoring local @import of "' + protocolLessUri + '" as after other content.');
|
---|
303 | } else if (!isAllowed) {
|
---|
304 | inlinerContext.warnings.push('Skipping local @import of "' + protocolLessUri + '" as resource is not allowed.');
|
---|
305 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
|
---|
306 | } else {
|
---|
307 | importedStyles = isLoaded
|
---|
308 | ? inlinerContext.externalContext.sourcesContent[normalizedPath]
|
---|
309 | : fs.readFileSync(absoluteUri, 'utf-8');
|
---|
310 |
|
---|
311 | if (importedStyles.charCodeAt(0) === 65279) {
|
---|
312 | importedStyles = importedStyles.substring(1);
|
---|
313 | }
|
---|
314 |
|
---|
315 | inlinerContext.inlinedStylesheets.push(absoluteUri);
|
---|
316 | inlinerContext.inline = inlinerContext.externalContext.options.inline;
|
---|
317 |
|
---|
318 | inlinerContext.externalContext.source = normalizedPath;
|
---|
319 | inlinerContext.externalContext.sourcesContent[normalizedPath] = importedStyles;
|
---|
320 | inlinerContext.externalContext.stats.originalSize += importedStyles.length;
|
---|
321 |
|
---|
322 | return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function(importedTokens) {
|
---|
323 | importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
|
---|
324 |
|
---|
325 | inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
|
---|
326 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
327 |
|
---|
328 | return doInlineImports(inlinerContext);
|
---|
329 | });
|
---|
330 | }
|
---|
331 |
|
---|
332 | inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
|
---|
333 |
|
---|
334 | return doInlineImports(inlinerContext);
|
---|
335 | }
|
---|
336 |
|
---|
337 | function wrapInMedia(tokens, mediaQuery, metadata) {
|
---|
338 | if (mediaQuery) {
|
---|
339 | return [[Token.NESTED_BLOCK, [[Token.NESTED_BLOCK_SCOPE, '@media ' + mediaQuery, metadata]], tokens]];
|
---|
340 | }
|
---|
341 | return tokens;
|
---|
342 | }
|
---|
343 |
|
---|
344 | module.exports = readSources;
|
---|