sass-embedded 0.2.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +1 -1
- data/.rubocop.yml +11 -0
- data/README.md +1 -3
- data/Rakefile +9 -4
- data/ext/extconf.rb +15 -2
- data/lib/sass.rb +49 -27
- data/lib/sass/embedded.rb +186 -194
- data/lib/sass/error.rb +4 -9
- data/lib/sass/info.rb +30 -0
- data/lib/sass/observer.rb +38 -0
- data/lib/sass/render.rb +242 -0
- data/lib/sass/transport.rb +55 -80
- data/lib/sass/util.rb +24 -3
- data/lib/sass/version.rb +1 -1
- data/sass-embedded.gemspec +3 -1
- data/test/concurrency_test.rb +34 -0
- data/test/functions_test.rb +153 -157
- data/test/{custom_importer_test.rb → importer_test.rb} +24 -31
- data/test/include_paths_test.rb +81 -0
- data/test/indented_syntax_test.rb +61 -0
- data/test/input_test.rb +56 -0
- data/test/output_test.rb +136 -0
- data/test/render_error_test.rb +44 -0
- data/test/render_test.rb +19 -0
- data/test/source_maps_test.rb +178 -0
- data/test/test_helper.rb +1 -0
- metadata +53 -13
- data/test/compiler_test.rb +0 -276
- data/test/error_test.rb +0 -31
- data/test/output_style_test.rb +0 -93
- data/test/sass_test.rb +0 -22
data/lib/sass/error.rb
CHANGED
@@ -1,17 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Sass
|
4
|
-
class
|
4
|
+
class SassError < StandardError; end
|
5
5
|
|
6
|
-
class ProtocolError <
|
6
|
+
class ProtocolError < SassError; end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
class InvalidStyleError < BaseError; end
|
11
|
-
|
12
|
-
class UnsupportedValue < BaseError; end
|
13
|
-
|
14
|
-
class CompilationError < BaseError
|
8
|
+
# The error returned by {Embedded#render}.
|
9
|
+
class RenderError < SassError
|
15
10
|
attr_accessor :formatted, :file, :line, :column, :status
|
16
11
|
|
17
12
|
def initialize(message, formatted, file, line, column, status)
|
data/lib/sass/info.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
# The {Observer} for {Embedded#info}.
|
5
|
+
class Info < Observer
|
6
|
+
def initialize(transport, id)
|
7
|
+
super(transport, id)
|
8
|
+
@transport.send EmbeddedProtocol::InboundMessage::VersionRequest.new(id: @id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(error, message)
|
12
|
+
raise error unless error.nil?
|
13
|
+
|
14
|
+
case message
|
15
|
+
when EmbeddedProtocol::ProtocolError
|
16
|
+
raise ProtocolError, message.message
|
17
|
+
when EmbeddedProtocol::OutboundMessage::VersionResponse
|
18
|
+
return unless message.id == @id
|
19
|
+
|
20
|
+
Thread.new do
|
21
|
+
super(nil, message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
rescue StandardError => e
|
25
|
+
Thread.new do
|
26
|
+
super(e, nil)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
# The abstract {Observer} for tracking state and observing messages
|
5
|
+
# from {Transport}.
|
6
|
+
class Observer
|
7
|
+
def initialize(transport, id)
|
8
|
+
raise NotImplementedError if instance_of? Observer
|
9
|
+
|
10
|
+
@transport = transport
|
11
|
+
@id = id
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@condition_variable = ConditionVariable.new
|
14
|
+
@error = nil
|
15
|
+
@message = nil
|
16
|
+
@transport.add_observer self
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch
|
20
|
+
@mutex.synchronize do
|
21
|
+
@condition_variable.wait(@mutex) if @error.nil? && @message.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
raise @error unless @error.nil?
|
25
|
+
|
26
|
+
@message
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(error, message)
|
30
|
+
@transport.delete_observer self
|
31
|
+
@mutex.synchronize do
|
32
|
+
@error = error
|
33
|
+
@message = message
|
34
|
+
@condition_variable.broadcast
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/sass/render.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
# The {Observer} for {Embedded#render}.
|
5
|
+
class Render < Observer
|
6
|
+
def initialize(transport, id,
|
7
|
+
data:,
|
8
|
+
file:,
|
9
|
+
indented_syntax:,
|
10
|
+
include_paths:,
|
11
|
+
output_style:,
|
12
|
+
source_map:,
|
13
|
+
out_file:,
|
14
|
+
functions:,
|
15
|
+
importer:)
|
16
|
+
raise ArgumentError, 'either data or file must be set' if file.nil? && data.nil?
|
17
|
+
|
18
|
+
super(transport, id)
|
19
|
+
|
20
|
+
@data = data
|
21
|
+
@file = file
|
22
|
+
@indented_syntax = indented_syntax
|
23
|
+
@include_paths = include_paths
|
24
|
+
@output_style = output_style
|
25
|
+
@source_map = source_map
|
26
|
+
@out_file = out_file
|
27
|
+
@global_functions = functions.keys
|
28
|
+
@functions = functions.transform_keys do |key|
|
29
|
+
key.to_s.split('(')[0].chomp
|
30
|
+
end
|
31
|
+
@importer = importer
|
32
|
+
@import_responses = {}
|
33
|
+
|
34
|
+
@transport.send compile_request
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(error, message)
|
38
|
+
raise error unless error.nil?
|
39
|
+
|
40
|
+
case message
|
41
|
+
when EmbeddedProtocol::ProtocolError
|
42
|
+
raise ProtocolError, message.message
|
43
|
+
when EmbeddedProtocol::OutboundMessage::CompileResponse
|
44
|
+
return unless message.id == @id
|
45
|
+
|
46
|
+
Thread.new do
|
47
|
+
super(nil, message)
|
48
|
+
end
|
49
|
+
when EmbeddedProtocol::OutboundMessage::LogEvent
|
50
|
+
# not implemented yet
|
51
|
+
when EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
|
52
|
+
return unless message['compilation_id'] == @id
|
53
|
+
|
54
|
+
Thread.new do
|
55
|
+
@transport.send canonicalize_response message
|
56
|
+
end
|
57
|
+
when EmbeddedProtocol::OutboundMessage::ImportRequest
|
58
|
+
return unless message['compilation_id'] == @id
|
59
|
+
|
60
|
+
Thread.new do
|
61
|
+
@transport.send import_response message
|
62
|
+
end
|
63
|
+
when EmbeddedProtocol::OutboundMessage::FileImportRequest
|
64
|
+
raise NotImplementedError, 'FileImportRequest is not implemented'
|
65
|
+
when EmbeddedProtocol::OutboundMessage::FunctionCallRequest
|
66
|
+
return unless message['compilation_id'] == @id
|
67
|
+
|
68
|
+
Thread.new do
|
69
|
+
@transport.send function_call_response message
|
70
|
+
end
|
71
|
+
end
|
72
|
+
rescue StandardError => e
|
73
|
+
Thread.new do
|
74
|
+
super(e, nil)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def compile_request
|
81
|
+
EmbeddedProtocol::InboundMessage::CompileRequest.new(
|
82
|
+
id: @id,
|
83
|
+
string: string,
|
84
|
+
path: path,
|
85
|
+
style: style,
|
86
|
+
source_map: source_map,
|
87
|
+
importers: importers,
|
88
|
+
global_functions: global_functions,
|
89
|
+
alert_color: $stderr.tty?,
|
90
|
+
alert_ascii: Platform::OS == 'windows'
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def canonicalize_response(canonicalize_request)
|
95
|
+
url = Util.file_uri(File.absolute_path(canonicalize_request.url, (@file.nil? ? 'stdin' : @file)))
|
96
|
+
|
97
|
+
begin
|
98
|
+
result = @importer[canonicalize_request.importer_id].call canonicalize_request.url, @file
|
99
|
+
raise result if result.is_a? StandardError
|
100
|
+
rescue StandardError => e
|
101
|
+
return EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
102
|
+
id: canonicalize_request.id,
|
103
|
+
error: e.message
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
if result&.key? :contents
|
108
|
+
@import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
|
109
|
+
id: canonicalize_request.id,
|
110
|
+
success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
|
111
|
+
contents: result[:contents],
|
112
|
+
syntax: EmbeddedProtocol::Syntax::SCSS,
|
113
|
+
source_map_url: nil
|
114
|
+
)
|
115
|
+
)
|
116
|
+
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
117
|
+
id: canonicalize_request.id,
|
118
|
+
url: url
|
119
|
+
)
|
120
|
+
elsif result&.key? :file
|
121
|
+
canonicalized_url = Util.file_uri(result[:file])
|
122
|
+
|
123
|
+
# TODO: FileImportRequest is not supported yet.
|
124
|
+
# Workaround by reading contents and return it when server asks
|
125
|
+
@import_responses[canonicalized_url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
|
126
|
+
id: canonicalize_request.id,
|
127
|
+
success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
|
128
|
+
contents: File.read(result[:file]),
|
129
|
+
syntax: EmbeddedProtocol::Syntax::SCSS,
|
130
|
+
source_map_url: nil
|
131
|
+
)
|
132
|
+
)
|
133
|
+
|
134
|
+
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
135
|
+
id: canonicalize_request.id,
|
136
|
+
url: canonicalized_url
|
137
|
+
)
|
138
|
+
else
|
139
|
+
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
140
|
+
id: canonicalize_request.id
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def import_response(import_request)
|
146
|
+
url = import_request.url
|
147
|
+
|
148
|
+
if @import_responses.key? url
|
149
|
+
@import_responses[url].id = import_request.id
|
150
|
+
else
|
151
|
+
@import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
|
152
|
+
id: import_request.id,
|
153
|
+
error: "Failed to import: #{url}"
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
@import_responses[url]
|
158
|
+
end
|
159
|
+
|
160
|
+
def function_call_response(function_call_request)
|
161
|
+
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
|
162
|
+
id: function_call_request.id,
|
163
|
+
success: @functions[function_call_request.name].call(*function_call_request.arguments)
|
164
|
+
)
|
165
|
+
rescue StandardError => e
|
166
|
+
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
|
167
|
+
id: function_call_request.id,
|
168
|
+
error: e.message
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def syntax
|
173
|
+
if @indented_syntax == true
|
174
|
+
EmbeddedProtocol::Syntax::INDENTED
|
175
|
+
else
|
176
|
+
EmbeddedProtocol::Syntax::SCSS
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def url
|
181
|
+
return if @file.nil?
|
182
|
+
|
183
|
+
Util.file_uri @file
|
184
|
+
end
|
185
|
+
|
186
|
+
def string
|
187
|
+
return if @data.nil?
|
188
|
+
|
189
|
+
EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
|
190
|
+
source: @data,
|
191
|
+
url: url,
|
192
|
+
syntax: syntax
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
def path
|
197
|
+
@file if @data.nil?
|
198
|
+
end
|
199
|
+
|
200
|
+
def style
|
201
|
+
case @output_style&.to_sym
|
202
|
+
when :expanded
|
203
|
+
EmbeddedProtocol::OutputStyle::EXPANDED
|
204
|
+
when :compressed
|
205
|
+
EmbeddedProtocol::OutputStyle::COMPRESSED
|
206
|
+
else
|
207
|
+
raise ArgumentError, 'output_style must be one of :expanded, :compressed'
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def source_map
|
212
|
+
@source_map.is_a?(String) || (@source_map == true && !@out_file.nil?)
|
213
|
+
end
|
214
|
+
|
215
|
+
attr_reader :global_functions
|
216
|
+
|
217
|
+
# Order
|
218
|
+
# 1. Loading a file relative to the file in which the @use or @import appeared.
|
219
|
+
# 2. Each custom importer.
|
220
|
+
# 3. Loading a file relative to the current working directory.
|
221
|
+
# 4. Each load path in includePaths
|
222
|
+
# 5. Each load path specified in the SASS_PATH environment variable, which should
|
223
|
+
# be semicolon-separated on Windows and colon-separated elsewhere.
|
224
|
+
def importers
|
225
|
+
custom_importers = @importer.map.with_index do |_, id|
|
226
|
+
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
227
|
+
importer_id: id
|
228
|
+
)
|
229
|
+
end
|
230
|
+
|
231
|
+
include_path_importers = @include_paths
|
232
|
+
.concat(Sass.include_paths)
|
233
|
+
.map do |include_path|
|
234
|
+
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
235
|
+
path: File.absolute_path(include_path)
|
236
|
+
)
|
237
|
+
end
|
238
|
+
|
239
|
+
custom_importers.concat include_path_importers
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
data/lib/sass/transport.rb
CHANGED
@@ -5,20 +5,60 @@ require 'observer'
|
|
5
5
|
require_relative '../../ext/embedded_sass_pb'
|
6
6
|
|
7
7
|
module Sass
|
8
|
+
# The {Observable} {Transport} for low level communication with
|
9
|
+
# dart-sass-embedded using protocol buffers via stdio. Received messages
|
10
|
+
# can be observed by an {Observer}.
|
8
11
|
class Transport
|
9
12
|
include Observable
|
10
13
|
|
11
14
|
DART_SASS_EMBEDDED = File.absolute_path(
|
12
|
-
"../../ext/sass_embedded/dart-sass-embedded#{
|
15
|
+
"../../ext/sass_embedded/dart-sass-embedded#{Platform::OS == 'windows' ? '.bat' : ''}", __dir__
|
13
16
|
)
|
14
17
|
|
15
18
|
PROTOCOL_ERROR_ID = 4_294_967_295
|
16
19
|
|
20
|
+
ONEOF_MESSAGE = EmbeddedProtocol::InboundMessage
|
21
|
+
.descriptor
|
22
|
+
.lookup_oneof('message')
|
23
|
+
.collect do |field_descriptor|
|
24
|
+
[field_descriptor.subtype, field_descriptor.name]
|
25
|
+
end.to_h
|
26
|
+
|
17
27
|
def initialize
|
28
|
+
@observerable_mutex = Mutex.new
|
29
|
+
@stdin_mutex = Mutex.new
|
18
30
|
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
|
19
|
-
@
|
20
|
-
|
31
|
+
pipe @stderr, $stderr
|
32
|
+
receive
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_observer(*args)
|
36
|
+
@observerable_mutex.synchronize do
|
37
|
+
super(*args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def send(message)
|
42
|
+
write EmbeddedProtocol::InboundMessage.new(
|
43
|
+
ONEOF_MESSAGE[message.class.descriptor] => message
|
44
|
+
).to_proto
|
45
|
+
end
|
46
|
+
|
47
|
+
def close
|
48
|
+
delete_observers
|
49
|
+
@stdin.close unless @stdin.closed?
|
50
|
+
@stdout.close unless @stdout.closed?
|
51
|
+
@stderr.close unless @stderr.closed?
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def closed?
|
56
|
+
@stdin.closed?
|
57
|
+
end
|
21
58
|
|
59
|
+
private
|
60
|
+
|
61
|
+
def receive
|
22
62
|
Thread.new do
|
23
63
|
loop do
|
24
64
|
bits = length = 0
|
@@ -28,10 +68,11 @@ module Sass
|
|
28
68
|
bits += 7
|
29
69
|
break if byte <= 0x7f
|
30
70
|
end
|
31
|
-
changed
|
32
71
|
payload = @stdout.read length
|
33
|
-
|
34
|
-
|
72
|
+
message = EmbeddedProtocol::OutboundMessage.decode payload
|
73
|
+
@observerable_mutex.synchronize do
|
74
|
+
changed
|
75
|
+
notify_observers nil, message[message.message.to_s]
|
35
76
|
end
|
36
77
|
rescue Interrupt
|
37
78
|
break
|
@@ -41,14 +82,16 @@ module Sass
|
|
41
82
|
break
|
42
83
|
end
|
43
84
|
end
|
85
|
+
end
|
44
86
|
|
87
|
+
def pipe(readable, writeable)
|
45
88
|
Thread.new do
|
46
89
|
loop do
|
47
|
-
|
90
|
+
writeable.write readable.read
|
48
91
|
rescue Interrupt
|
49
92
|
break
|
50
93
|
rescue IOError => e
|
51
|
-
@
|
94
|
+
@observerable_mutex.synchronize do
|
52
95
|
notify_observers e, nil
|
53
96
|
end
|
54
97
|
close
|
@@ -57,82 +100,14 @@ module Sass
|
|
57
100
|
end
|
58
101
|
end
|
59
102
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
req_name = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
|
65
|
-
|
66
|
-
message = Sass::EmbeddedProtocol::InboundMessage.new(req_name.to_sym => req)
|
67
|
-
|
68
|
-
error = nil
|
69
|
-
res = nil
|
70
|
-
|
71
|
-
@observerable_semaphore.synchronize do
|
72
|
-
MessageObserver.new self, id do |e, r|
|
73
|
-
mutex.synchronize do
|
74
|
-
error = e
|
75
|
-
res = r
|
76
|
-
|
77
|
-
resource.signal
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
mutex.synchronize do
|
83
|
-
write message.to_proto
|
84
|
-
|
85
|
-
resource.wait(mutex)
|
86
|
-
end
|
87
|
-
|
88
|
-
raise error if error
|
89
|
-
|
90
|
-
res
|
91
|
-
end
|
92
|
-
|
93
|
-
def close
|
94
|
-
delete_observers
|
95
|
-
@stdin.close unless @stdin.closed?
|
96
|
-
@stdout.close unless @stdout.closed?
|
97
|
-
@stderr.close unless @stderr.closed?
|
98
|
-
nil
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def write(proto)
|
104
|
-
@stdin_semaphore.synchronize do
|
105
|
-
length = proto.length
|
103
|
+
def write(payload)
|
104
|
+
@stdin_mutex.synchronize do
|
105
|
+
length = payload.length
|
106
106
|
while length.positive?
|
107
107
|
@stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
|
108
108
|
length >>= 7
|
109
109
|
end
|
110
|
-
@stdin.write
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
class MessageObserver
|
115
|
-
def initialize(obs, id, &block)
|
116
|
-
@obs = obs
|
117
|
-
@id = id
|
118
|
-
@block = block
|
119
|
-
@obs.add_observer self
|
120
|
-
end
|
121
|
-
|
122
|
-
def update(error, message)
|
123
|
-
if error
|
124
|
-
@obs.delete_observer self
|
125
|
-
@block.call error, nil
|
126
|
-
elsif message.error&.id == Sass::Transport::PROTOCOL_ERROR_ID
|
127
|
-
@obs.delete_observer self
|
128
|
-
@block.call Sass::ProtocolError.new(message.error.message), nil
|
129
|
-
else
|
130
|
-
res = message[message.message.to_s]
|
131
|
-
if (res['compilation_id'] || res['id']) == @id
|
132
|
-
@obs.delete_observer self
|
133
|
-
@block.call error, res
|
134
|
-
end
|
135
|
-
end
|
110
|
+
@stdin.write payload
|
136
111
|
end
|
137
112
|
end
|
138
113
|
end
|