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 |
|
23 | function 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 |
|
31 | function 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 |
|
43 | function 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 |
|
51 | function 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 |
|
64 | function fromHash(input, context, callback) {
|
65 | var inputAsImports = addHashSource(input, context, []);
|
66 | return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
|
67 | }
|
68 |
|
69 | function addStringSource(input, imports) {
|
70 | imports.push(restoreAsImport(normalizeUri(input)));
|
71 | return imports;
|
72 | }
|
73 |
|
74 | function 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 |
|
95 | function 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 |
|
114 | function 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 |
|
125 | function restoreAsImport(uri) {
|
126 | return restoreImport('url(' + uri + ')', '') + Marker.SEMICOLON;
|
127 | }
|
128 |
|
129 | function 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 |
|
155 | function allowsAnyImports(inline) {
|
156 | return !(inline.length == 1 && inline[0] == 'none');
|
157 | }
|
158 |
|
159 | function 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 |
|
181 | function 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 |
|
203 | function 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 |
|
214 | function 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 |
|
284 | function 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 |
|
333 | function 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 |
|
341 | module.exports = readSources;
|