Tim van der Lippe | 652ccb7 | 2021-05-27 17:07:12 +0100 | [diff] [blame^] | 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 (!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; |