sass-embedded 0.3.0 → 0.6.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,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- class SassError < StandardError; end
4
+ class Error < StandardError; end
5
5
 
6
- class ProtocolError < SassError; end
6
+ class ProtocolError < Error; end
7
7
 
8
- # The error returned by {Sass.render}
9
- class RenderError < SassError
10
- attr_accessor :formatted, :file, :line, :column, :status
8
+ # The {Error} raised by {Embedded#render}.
9
+ class RenderError < Error
10
+ include Struct
11
+
12
+ attr_reader :formatted, :file, :line, :column, :status
11
13
 
12
14
  def initialize(message, formatted, file, line, column, status)
15
+ super(message)
13
16
  @formatted = formatted
14
17
  @file = file
15
18
  @line = line
16
19
  @column = column
17
20
  @status = status
18
- super(message)
19
21
  end
20
22
 
21
23
  def backtrace
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 Version < 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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Observer} for receiving messages from {Transport}.
5
+ class Observer
6
+ def initialize(transport, id)
7
+ raise NotImplementedError if instance_of? Observer
8
+
9
+ @transport = transport
10
+ @id = id
11
+ @mutex = Mutex.new
12
+ @condition_variable = ConditionVariable.new
13
+ @error = nil
14
+ @message = nil
15
+ @transport.add_observer self
16
+ end
17
+
18
+ def fetch
19
+ @mutex.synchronize do
20
+ @condition_variable.wait(@mutex) if @error.nil? && @message.nil?
21
+ end
22
+
23
+ raise @error unless @error.nil?
24
+
25
+ @message
26
+ end
27
+
28
+ def update(error, message)
29
+ @transport.delete_observer self
30
+ @mutex.synchronize do
31
+ @error = error
32
+ @message = message
33
+ @condition_variable.broadcast
34
+ end
35
+ end
36
+ end
37
+ 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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Result} of {Embedded#render}.
5
+ class Result
6
+ include Struct
7
+
8
+ attr_reader :css, :map, :stats
9
+
10
+ def initialize(css, map, stats)
11
+ @css = css
12
+ @map = map
13
+ @stats = stats
14
+ end
15
+
16
+ # The {Stats} of {Embedded#render}.
17
+ class Stats
18
+ include Struct
19
+
20
+ attr_reader :entry, :start, :end, :duration
21
+
22
+ def initialize(entry, start, finish, duration)
23
+ @entry = entry
24
+ @start = start
25
+ @end = finish
26
+ @duration = duration
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Struct} module.
5
+ module Struct
6
+ def [](key)
7
+ instance_variable_get("@#{key}".to_sym)
8
+ end
9
+
10
+ def to_h
11
+ instance_variables.map do |variable|
12
+ [variable[1..].to_sym, instance_variable_get(variable)]
13
+ end.to_h
14
+ end
15
+
16
+ def to_s
17
+ to_h.to_s
18
+ end
19
+ end
20
+ end
@@ -5,9 +5,9 @@ require 'observer'
5
5
  require_relative '../../ext/embedded_sass_pb'
6
6
 
7
7
  module Sass
8
- # The interface for communicating with dart-sass-embedded.
9
- # It handles message serialization and deserialization as well as
10
- # tracking concurrent request and response
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}.
11
11
  class Transport
12
12
  include Observable
13
13
 
@@ -17,45 +17,31 @@ module Sass
17
17
 
18
18
  PROTOCOL_ERROR_ID = 4_294_967_295
19
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
+
20
27
  def initialize
21
- @stdin_semaphore = Mutex.new
22
- @observerable_semaphore = Mutex.new
28
+ @observerable_mutex = Mutex.new
29
+ @stdin_mutex = Mutex.new
23
30
  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
24
31
  pipe @stderr, $stderr
25
32
  receive
26
33
  end
27
34
 
28
- def send(req, res_id)
29
- mutex = Mutex.new
30
- resource = ConditionVariable.new
31
-
32
- req_kind = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
33
-
34
- message = EmbeddedProtocol::InboundMessage.new(req_kind => req)
35
-
36
- error = nil
37
- res = nil
38
-
39
- @observerable_semaphore.synchronize do
40
- MessageObserver.new self, res_id do |e, r|
41
- mutex.synchronize do
42
- error = e
43
- res = r
44
-
45
- resource.signal
46
- end
47
- end
48
- end
49
-
50
- mutex.synchronize do
51
- write message.to_proto
52
-
53
- resource.wait(mutex)
35
+ def add_observer(*args)
36
+ @observerable_mutex.synchronize do
37
+ super(*args)
54
38
  end
39
+ end
55
40
 
56
- raise error if error
57
-
58
- res
41
+ def send(message)
42
+ write EmbeddedProtocol::InboundMessage.new(
43
+ ONEOF_MESSAGE[message.class.descriptor] => message
44
+ ).to_proto
59
45
  end
60
46
 
61
47
  def close
@@ -66,6 +52,10 @@ module Sass
66
52
  nil
67
53
  end
68
54
 
55
+ def closed?
56
+ @stdin.closed?
57
+ end
58
+
69
59
  private
70
60
 
71
61
  def receive
@@ -78,10 +68,11 @@ module Sass
78
68
  bits += 7
79
69
  break if byte <= 0x7f
80
70
  end
81
- changed
82
71
  payload = @stdout.read length
83
- @observerable_semaphore.synchronize do
84
- notify_observers nil, 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]
85
76
  end
86
77
  rescue Interrupt
87
78
  break
@@ -100,7 +91,7 @@ module Sass
100
91
  rescue Interrupt
101
92
  break
102
93
  rescue IOError => e
103
- @observerable_semaphore.synchronize do
94
+ @observerable_mutex.synchronize do
104
95
  notify_observers e, nil
105
96
  end
106
97
  close
@@ -110,7 +101,7 @@ module Sass
110
101
  end
111
102
 
112
103
  def write(payload)
113
- @stdin_semaphore.synchronize do
104
+ @stdin_mutex.synchronize do
114
105
  length = payload.length
115
106
  while length.positive?
116
107
  @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
@@ -119,32 +110,5 @@ module Sass
119
110
  @stdin.write payload
120
111
  end
121
112
  end
122
-
123
- # The observer used to listen on messages from stdout, check if id
124
- # matches the given request id, and yield back to the given block.
125
- class MessageObserver
126
- def initialize(obs, id, &block)
127
- @obs = obs
128
- @id = id
129
- @block = block
130
- @obs.add_observer self
131
- end
132
-
133
- def update(error, message)
134
- if error
135
- @obs.delete_observer self
136
- @block.call error, nil
137
- elsif message.error&.id == Transport::PROTOCOL_ERROR_ID
138
- @obs.delete_observer self
139
- @block.call ProtocolError.new(message.error.message), nil
140
- else
141
- res = message[message.message.to_s]
142
- if (res['compilation_id'] || res['id']) == @id
143
- @obs.delete_observer self
144
- @block.call error, res
145
- end
146
- end
147
- end
148
- end
149
113
  end
150
114
  end