/
map-generator.coffee
187 lines (150 loc) · 5.03 KB
/
map-generator.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
base64js = require('base64-js')
mozilla = require('source-map')
Result = require('./result')
lazy = require('./lazy')
path = require('path')
fs = require('fs')
# All tools to generate source maps
class MapGenerator
constructor: (@root, @opts) ->
# Is `string` is starting with `start`
startWith: (string, start) ->
string[0..start.length-1] == start
# Should map be generated
isMap: ->
return @opts.map if typeof(@opts.map) == 'boolean'
!!@opts.inlineMap || !!@prevMap()
# Should we inline source map to annotation comment
lazy @, 'isInline', ->
return @opts.inlineMap if @opts.inlineMap?
@isPrevInline()
# Is source map from previous compilation step is inline to annotation comment
lazy @, 'isPrevInline', ->
return false unless @prevAnnotation()
text = @prevAnnotation().text
@startWith(text, '# sourceMappingURL=data:')
# Source map from previous compilation step (like Sass)
lazy @, 'prevMap', ->
return @opts.map if @opts.map and typeof(@opts.map) != 'boolean'
if @isPrevInline()
@encodeInline(@prevAnnotation().text)
else if @opts.from
map = @opts.from + '.map'
if @prevAnnotation()
file = @prevAnnotation().text.replace('# sourceMappingURL=', '')
map = path.join(path.dirname(@opts.from), file)
if fs.existsSync?(map)
fs.readFileSync(map).toString()
else
false
# Lazy load for annotation comment from previous compilation step
lazy @, 'prevAnnotation', ->
last = @root.last
return null unless last
if last.type == 'comment' and @startWith(last.text, '# sourceMappingURL=')
last
else
null
# Encode different type of inline
encodeInline: (text) ->
uri = '# sourceMappingURL=data:application/json,'
base64 = '# sourceMappingURL=data:application/json;base64,'
if @startWith(text, uri)
decodeURIComponent( text[uri.length..-1] )
else if @startWith(text, base64)
text = text[base64.length..-1]
bytes = base64js.toByteArray(text)
(String.fromCharCode(byte) for byte in bytes).join('')
else
throw new Error('Unknown source map encoding')
# Clear source map annotation comment
clearAnnotation: ->
@prevAnnotation()?.removeSelf()
# Apply source map from previous compilation step (like Sass)
applyPrevMap: ->
if @prevMap()
prev = @prevMap()
prev = if typeof(prev) == 'string'
JSON.parse(prev)
else if prev instanceof mozilla.SourceMapConsumer
mozilla.SourceMapGenerator.fromSourceMap(prev).toJSON()
else if typeof(prev) == 'object' and prev.toJSON
prev.toJSON()
else
prev
prev = new mozilla.SourceMapConsumer(prev)
from = @relative(@opts.from)
@map.applySourceMap(prev, from, path.dirname(from))
# Add source map annotation comment if it is needed
addAnnotation: () ->
return if @opts.mapAnnotation == false
return if @prevMap() and not @prevAnnotation()
content = if @isInline()
bytes = (char.charCodeAt(0) for char in @map.toString())
"data:application/json;base64," + base64js.fromByteArray(bytes)
else
@outputFile() + '.map'
@css += "\n/*# sourceMappingURL=#{ content } */"
# Return output CSS file path
outputFile: ->
if @opts.to then path.basename(@opts.to) else 'to.css'
# Return Result object with map
generateMap: ->
@stringify()
@applyPrevMap()
@addAnnotation()
if @isInline()
new Result(@css)
else
new Result(@css, @map.toString())
# Return path relative from output CSS file
relative: (file) ->
from = if @opts.to then path.dirname(@opts.to) else '.'
file = path.relative(from, file)
file = file.replace('\\', '/') if path.sep == '\\'
file
# Return path of node source for map
sourcePath: (node) ->
@relative(node.source.file || 'from.css')
# Return CSS string and source map
stringify: () ->
@css = ''
@map = new mozilla.SourceMapGenerator(file: @outputFile())
line = 1
column = 1
builder = (str, node, type) =>
@css += str
if node?.source?.start and type != 'end'
@map.addMapping
source: @sourcePath(node)
original:
line: node.source.start.line
column: node.source.start.column - 1
generated:
line: line
column: column - 1
lines = str.match(/\n/g)
if lines
line += lines.length
last = str.lastIndexOf("\n")
column = str.length - last
else
column = column + str.length
if node?.source?.end and type != 'start'
@map.addMapping
source: @sourcePath(node)
original:
line: node.source.end.line
column: node.source.end.column
generated:
line: line
column: column
@root.stringify(builder)
# Return Result object with or without map
getResult: ->
@clearAnnotation()
if @isMap()
@generateMap()
else
new Result(@root.toString())
module.exports = MapGenerator