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.
data/lib/sass/error.rb CHANGED
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- class BaseError < StandardError; end
4
+ class SassError < StandardError; end
5
5
 
6
- class ProtocolError < BaseError; end
6
+ class ProtocolError < SassError; end
7
7
 
8
- class NotRenderedError < BaseError; end
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
@@ -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
@@ -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#{Sass::Platform::OS == 'windows' ? '.bat' : ''}", __dir__
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
- @stdin_semaphore = Mutex.new
20
- @observerable_semaphore = Mutex.new
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
- @observerable_semaphore.synchronize do
34
- notify_observers nil, Sass::EmbeddedProtocol::OutboundMessage.decode(payload)
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
- warn @stderr.read
90
+ writeable.write readable.read
48
91
  rescue Interrupt
49
92
  break
50
93
  rescue IOError => e
51
- @observerable_semaphore.synchronize do
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 send(req, id)
61
- mutex = Mutex.new
62
- resource = ConditionVariable.new
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 proto
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