UNPKG

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