1var fs = require('fs'); 2var path = require('path'); 3 4var isAllowedResource = require('./is-allowed-resource'); 5var matchDataUri = require('./match-data-uri'); 6var rebaseLocalMap = require('./rebase-local-map'); 7var rebaseRemoteMap = require('./rebase-remote-map'); 8 9var Token = require('../tokenizer/token'); 10var hasProtocol = require('../utils/has-protocol'); 11var isDataUriResource = require('../utils/is-data-uri-resource'); 12var isRemoteResource = require('../utils/is-remote-resource'); 13 14var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/; 15 16function applySourceMaps(tokens, context, callback) { 17 var applyContext = { 18 callback: callback, 19 fetch: context.options.fetch, 20 index: 0, 21 inline: context.options.inline, 22 inlineRequest: context.options.inlineRequest, 23 inlineTimeout: context.options.inlineTimeout, 24 inputSourceMapTracker: context.inputSourceMapTracker, 25 localOnly: context.localOnly, 26 processedTokens: [], 27 rebaseTo: context.options.rebaseTo, 28 sourceTokens: tokens, 29 warnings: context.warnings 30 }; 31 32 return context.options.sourceMap && tokens.length > 0 ? 33 doApplySourceMaps(applyContext) : 34 callback(tokens); 35} 36 37function doApplySourceMaps(applyContext) { 38 var singleSourceTokens = []; 39 var lastSource = findTokenSource(applyContext.sourceTokens[0]); 40 var source; 41 var token; 42 var l; 43 44 for (l = applyContext.sourceTokens.length; applyContext.index < l; applyContext.index++) { 45 token = applyContext.sourceTokens[applyContext.index]; 46 source = findTokenSource(token); 47 48 if (source != lastSource) { 49 singleSourceTokens = []; 50 lastSource = source; 51 } 52 53 singleSourceTokens.push(token); 54 applyContext.processedTokens.push(token); 55 56 if (token[0] == Token.COMMENT && MAP_MARKER_PATTERN.test(token[1])) { 57 return fetchAndApplySourceMap(token[1], source, singleSourceTokens, applyContext); 58 } 59 } 60 61 return applyContext.callback(applyContext.processedTokens); 62} 63 64function findTokenSource(token) { 65 var scope; 66 var metadata; 67 68 if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) { 69 metadata = token[2][0]; 70 } else { 71 scope = token[1][0]; 72 metadata = scope[2][0]; 73 } 74 75 return metadata[2]; 76} 77 78function fetchAndApplySourceMap(sourceMapComment, source, singleSourceTokens, applyContext) { 79 return extractInputSourceMapFrom(sourceMapComment, applyContext, function (inputSourceMap) { 80 if (inputSourceMap) { 81 applyContext.inputSourceMapTracker.track(source, inputSourceMap); 82 applySourceMapRecursively(singleSourceTokens, applyContext.inputSourceMapTracker); 83 } 84 85 applyContext.index++; 86 return doApplySourceMaps(applyContext); 87 }); 88} 89 90function extractInputSourceMapFrom(sourceMapComment, applyContext, whenSourceMapReady) { 91 var uri = MAP_MARKER_PATTERN.exec(sourceMapComment)[1]; 92 var absoluteUri; 93 var sourceMap; 94 var rebasedMap; 95 96 if (isDataUriResource(uri)) { 97 sourceMap = extractInputSourceMapFromDataUri(uri); 98 return whenSourceMapReady(sourceMap); 99 } else if (isRemoteResource(uri)) { 100 return loadInputSourceMapFromRemoteUri(uri, applyContext, function (sourceMap) { 101 var parsedMap; 102 103 if (sourceMap) { 104 parsedMap = JSON.parse(sourceMap); 105 rebasedMap = rebaseRemoteMap(parsedMap, uri); 106 whenSourceMapReady(rebasedMap); 107 } else { 108 whenSourceMapReady(null); 109 } 110 }); 111 } else { 112 // at this point `uri` is already rebased, see lib/reader/rebase.js#rebaseSourceMapComment 113 // it is rebased to be consistent with rebasing other URIs 114 // however here we need to resolve it back to read it from disk 115 absoluteUri = path.resolve(applyContext.rebaseTo, uri); 116 sourceMap = loadInputSourceMapFromLocalUri(absoluteUri, applyContext); 117 118 if (sourceMap) { 119 rebasedMap = rebaseLocalMap(sourceMap, absoluteUri, applyContext.rebaseTo); 120 return whenSourceMapReady(rebasedMap); 121 } else { 122 return whenSourceMapReady(null); 123 } 124 } 125} 126 127function extractInputSourceMapFromDataUri(uri) { 128 var dataUriMatch = matchDataUri(uri); 129 var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii'; 130 var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8'; 131 var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4]; 132 133 var buffer = new Buffer(data, encoding); 134 buffer.charset = charset; 135 136 return JSON.parse(buffer.toString()); 137} 138 139function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) { 140 var isAllowed = isAllowedResource(uri, true, applyContext.inline); 141 var isRuntimeResource = !hasProtocol(uri); 142 143 if (applyContext.localOnly) { 144 applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.'); 145 return whenLoaded(null); 146 } else if (isRuntimeResource) { 147 applyContext.warnings.push('Cannot fetch "' + uri + '" as no protocol given.'); 148 return whenLoaded(null); 149 } else if (!isAllowed) { 150 applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); 151 return whenLoaded(null); 152 } 153 154 applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function (error, body) { 155 if (error) { 156 applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error); 157 return whenLoaded(null); 158 } 159 160 whenLoaded(body); 161 }); 162} 163 164function loadInputSourceMapFromLocalUri(uri, applyContext) { 165 var isAllowed = isAllowedResource(uri, false, applyContext.inline); 166 var sourceMap; 167 168 if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) { 169 applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.'); 170 return null; 171 } else if (!isAllowed) { 172 applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.'); 173 return null; 174 } 175 176 sourceMap = fs.readFileSync(uri, 'utf-8'); 177 return JSON.parse(sourceMap); 178} 179 180function applySourceMapRecursively(tokens, inputSourceMapTracker) { 181 var token; 182 var i, l; 183 184 for (i = 0, l = tokens.length; i < l; i++) { 185 token = tokens[i]; 186 187 switch (token[0]) { 188 case Token.AT_RULE: 189 applySourceMapTo(token, inputSourceMapTracker); 190 break; 191 case Token.AT_RULE_BLOCK: 192 applySourceMapRecursively(token[1], inputSourceMapTracker); 193 applySourceMapRecursively(token[2], inputSourceMapTracker); 194 break; 195 case Token.AT_RULE_BLOCK_SCOPE: 196 applySourceMapTo(token, inputSourceMapTracker); 197 break; 198 case Token.NESTED_BLOCK: 199 applySourceMapRecursively(token[1], inputSourceMapTracker); 200 applySourceMapRecursively(token[2], inputSourceMapTracker); 201 break; 202 case Token.NESTED_BLOCK_SCOPE: 203 applySourceMapTo(token, inputSourceMapTracker); 204 break; 205 case Token.COMMENT: 206 applySourceMapTo(token, inputSourceMapTracker); 207 break; 208 case Token.PROPERTY: 209 applySourceMapRecursively(token, inputSourceMapTracker); 210 break; 211 case Token.PROPERTY_BLOCK: 212 applySourceMapRecursively(token[1], inputSourceMapTracker); 213 break; 214 case Token.PROPERTY_NAME: 215 applySourceMapTo(token, inputSourceMapTracker); 216 break; 217 case Token.PROPERTY_VALUE: 218 applySourceMapTo(token, inputSourceMapTracker); 219 break; 220 case Token.RULE: 221 applySourceMapRecursively(token[1], inputSourceMapTracker); 222 applySourceMapRecursively(token[2], inputSourceMapTracker); 223 break; 224 case Token.RULE_SCOPE: 225 applySourceMapTo(token, inputSourceMapTracker); 226 } 227 } 228 229 return tokens; 230} 231 232function applySourceMapTo(token, inputSourceMapTracker) { 233 var value = token[1]; 234 var metadata = token[2]; 235 var newMetadata = []; 236 var i, l; 237 238 for (i = 0, l = metadata.length; i < l; i++) { 239 newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length)); 240 } 241 242 token[2] = newMetadata; 243} 244 245module.exports = applySourceMaps; 246