sass-embedded 0.1.3 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c42f58b6f4f3200d3033fd59e65e7cd33132731ab4520bafb2a375cf55577ec
4
- data.tar.gz: 2da13b64dab78a0b3fccd9a90cfcc6bd5affcd09dfe0b978aed53b69b2afa51e
3
+ metadata.gz: edeb1d7a1a3ba0d1485671e579478b4b1bc5d372350a25869cd4b0305bd2f356
4
+ data.tar.gz: 9eda21b2bdb2d7f278d03c9e5091eeec13956d9e0216ebb4a415988f8ac90b62
5
5
  SHA512:
6
- metadata.gz: d484872bd432f0092e67f699b18af1e242c982ff0e2019f9d23340593744ca58a015f5d63c76ae83d0a29e05ed362a2015f8f55bdbe472da67488e99c1cdaa8f
7
- data.tar.gz: bd452474519dabcd9d793205d8f0dcc64482a5313d48d9eda4464f0a5d6ed8b9d6a34ea1c57e4a8f178beafb008b080481fd6059ab2f3ad53494e2acd272527b
6
+ metadata.gz: 2cc608b5597a0c820633f1dc386de5e9125b00ed99004c5237e0223d5a181d4782da4b2025983f86fad47409ef738e1a5954e810f35e9f443dd319035b2aa4dc
7
+ data.tar.gz: a45ad64a3670c8d6f2136a287773eed2f77abaeb6911173931af8570fa1edeeed53a747ebc747288dc5b1941c08eb9ddc5c410ede3c536c380dd3297e0bf8092
data/lib/sass.rb CHANGED
@@ -4,8 +4,8 @@ module Sass
4
4
  # The global include_paths for Sass files. This is meant for plugins and
5
5
  # libraries to register the paths to their Sass stylesheets to that they may
6
6
  # be `@imported`. This include path is used by every instance of
7
- # {Sass::Embedded::Compiler}. They are lower-precedence than any include
8
- # paths passed in via the `:include_paths` option.
7
+ # {Sass::Embedded}. They are lower-precedence than any include paths passed
8
+ # in via the `:include_paths` option.
9
9
  #
10
10
  # If the `SASS_PATH` environment variable is set,
11
11
  # the initial value of `include_paths` will be initialized based on that.
@@ -24,13 +24,13 @@ module Sass
24
24
  end
25
25
 
26
26
  def self.render(options)
27
- unless defined? @compiler
28
- @compiler = Sass::Embedded::Compiler.new
27
+ unless defined? @embedded
28
+ @embedded = Sass::Embedded.new
29
29
  at_exit do
30
- @compiler.close
30
+ @embedded.close
31
31
  end
32
32
  end
33
- @compiler.render options
33
+ @embedded.render options
34
34
  end
35
35
  end
36
36
 
@@ -38,5 +38,5 @@ require_relative 'sass/version'
38
38
  require_relative 'sass/error'
39
39
  require_relative 'sass/platform'
40
40
  require_relative 'sass/util'
41
- require_relative 'sass/embedded/transport'
42
- require_relative 'sass/embedded/compiler'
41
+ require_relative 'sass/transport'
42
+ require_relative 'sass/embedded'
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ def initialize
6
+ @transport = Transport.new
7
+ @id_semaphore = Mutex.new
8
+ @id = 0
9
+ end
10
+
11
+ def render(options)
12
+ start = Sass::Util.now
13
+
14
+ raise Sass::NotRenderedError, 'Either :data or :file must be set.' if options[:file].nil? && options[:data].nil?
15
+
16
+ string = if options[:data]
17
+ Sass::EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
18
+ source: options[:data],
19
+ url: options[:file] ? Sass::Util.file_uri(options[:file]) : 'stdin',
20
+ syntax: options[:indented_syntax] == true ? Sass::EmbeddedProtocol::Syntax::INDENTED : Sass::EmbeddedProtocol::Syntax::SCSS
21
+ )
22
+ end
23
+
24
+ path = options[:data] ? nil : options[:file]
25
+
26
+ style = case options[:output_style]&.to_sym
27
+ when :expanded, nil
28
+ Sass::EmbeddedProtocol::OutputStyle::EXPANDED
29
+ when :compressed
30
+ Sass::EmbeddedProtocol::OutputStyle::COMPRESSED
31
+ when :nested, :compact
32
+ raise Sass::UnsupportedValue, "#{options[:output_style]} is not a supported :output_style"
33
+ else
34
+ raise Sass::InvalidStyleError, "#{options[:output_style]} is not a valid :output_style"
35
+ end
36
+
37
+ source_map = options[:source_map].is_a?(String) || (options[:source_map] == true && !options[:out_file].nil?)
38
+
39
+ # 1. Loading a file relative to the file in which the @use or @import appeared.
40
+ # 2. Each custom importer.
41
+ # 3. Loading a file relative to the current working directory.
42
+ # 4. Each load path in includePaths
43
+ # 5. Each load path specified in the SASS_PATH environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.
44
+ importers = (if options[:importer]
45
+ [
46
+ Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(importer_id: 0)
47
+ ]
48
+ else
49
+ []
50
+ end).concat(
51
+ (options[:include_paths] || []).concat(Sass.include_paths)
52
+ .map do |include_path|
53
+ Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
54
+ path: File.absolute_path(include_path)
55
+ )
56
+ end
57
+ )
58
+
59
+ signatures = []
60
+ functions = {}
61
+ options[:functions]&.each do |signature, function|
62
+ signatures.push signature
63
+ functions[signature.to_s.split('(')[0].chomp] = function
64
+ end
65
+
66
+ compilation_id = next_id
67
+
68
+ compile_request = Sass::EmbeddedProtocol::InboundMessage::CompileRequest.new(
69
+ id: compilation_id,
70
+ string: string,
71
+ path: path,
72
+ style: style,
73
+ source_map: source_map,
74
+ importers: importers,
75
+ global_functions: options[:functions] ? signatures : [],
76
+ alert_color: true,
77
+ alert_ascii: true
78
+ )
79
+
80
+ response = @transport.send compile_request, compilation_id
81
+
82
+ file = options[:file] || 'stdin'
83
+ canonicalizations = {}
84
+ imports = {}
85
+
86
+ loop do
87
+ case response
88
+ when Sass::EmbeddedProtocol::OutboundMessage::CompileResponse
89
+ break
90
+ when Sass::EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
91
+ url = Sass::Util.file_uri(File.absolute_path(response.url, File.dirname(file)))
92
+
93
+ if canonicalizations.key? url
94
+ canonicalizations[url].id = response.id
95
+ else
96
+ resolved = nil
97
+ options[:importer].each do |importer|
98
+ begin
99
+ resolved = importer.call response.url, file
100
+ rescue StandardError => e
101
+ resolved = e
102
+ end
103
+ break if resolved
104
+ end
105
+ if resolved.nil?
106
+ canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
107
+ id: response.id,
108
+ url: url
109
+ )
110
+ elsif resolved.is_a? StandardError
111
+ canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
112
+ id: response.id,
113
+ error: resolved.message
114
+ )
115
+ elsif resolved.key? :contents
116
+ canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
117
+ id: response.id,
118
+ url: url
119
+ )
120
+ imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
121
+ id: response.id,
122
+ success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
123
+ contents: resolved[:contents],
124
+ syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
125
+ source_map_url: nil
126
+ )
127
+ )
128
+ elsif resolved.key? :file
129
+ canonicalized_url = Sass::Util.file_uri(resolved[:file])
130
+ canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
131
+ id: response.id,
132
+ url: canonicalized_url
133
+ )
134
+ imports[canonicalized_url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
135
+ id: response.id,
136
+ success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
137
+ contents: File.read(resolved[:file]),
138
+ syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
139
+ source_map_url: nil
140
+ )
141
+ )
142
+ else
143
+ canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
144
+ id: response.id,
145
+ error: "Unexpected value returned from importer: #{resolved}"
146
+ )
147
+ end
148
+ end
149
+
150
+ response = @transport.send canonicalizations[url], compilation_id
151
+ when Sass::EmbeddedProtocol::OutboundMessage::ImportRequest
152
+ url = response.url
153
+
154
+ if imports.key? url
155
+ imports[url].id = response.id
156
+ else
157
+ imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
158
+ id: response.id,
159
+ error: "Failed to import: #{url}"
160
+ )
161
+ end
162
+
163
+ response = @transport.send imports[url], compilation_id
164
+ when Sass::EmbeddedProtocol::OutboundMessage::FunctionCallRequest
165
+ begin
166
+ message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
167
+ id: response.id,
168
+ success: functions[response.name].call(*response.arguments)
169
+ )
170
+ rescue StandardError => e
171
+ message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
172
+ id: response.id,
173
+ error: e.message
174
+ )
175
+ end
176
+
177
+ response = @transport.send message, compilation_id
178
+ when Sass::EmbeddedProtocol::ProtocolError
179
+ raise Sass::ProtocolError, response.message
180
+ else
181
+ raise Sass::ProtocolError, "Unexpected packet received: #{response}"
182
+ end
183
+ end
184
+
185
+ if response.failure
186
+ raise Sass::CompilationError.new(
187
+ response.failure.message,
188
+ response.failure.formatted,
189
+ response.failure.span ? response.failure.span.url : nil,
190
+ response.failure.span ? response.failure.span.start.line + 1 : nil,
191
+ response.failure.span ? response.failure.span.start.column + 1 : nil,
192
+ 1
193
+ )
194
+ end
195
+
196
+ finish = Sass::Util.now
197
+
198
+ {
199
+ css: response.success.css,
200
+ map: response.success.source_map,
201
+ stats: {
202
+ entry: options[:file] || 'data',
203
+ start: start,
204
+ end: finish,
205
+ duration: finish - start
206
+ }
207
+ }
208
+ end
209
+
210
+ def close
211
+ @transport.close
212
+ end
213
+
214
+ private
215
+
216
+ def info
217
+ version_response = @transport.send Sass::EmbeddedProtocol::InboundMessage::VersionRequest.new(
218
+ id: next_id
219
+ )
220
+ {
221
+ compiler_version: version_response.compiler_version,
222
+ protocol_version: version_response.protocol_version,
223
+ implementation_name: version_response.implementation_name,
224
+ implementation_version: version_response.implementation_version
225
+ }
226
+ end
227
+
228
+ def next_id
229
+ @id_semaphore.synchronize do
230
+ @id += 1
231
+ @id = 0 if @id == Transport::PROTOCOL_ERROR_ID
232
+ @id
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'observer'
5
+ require_relative '../../ext/sass_embedded/embedded_sass_pb'
6
+
7
+ module Sass
8
+ class Transport
9
+ include Observable
10
+
11
+ DART_SASS_EMBEDDED = File.absolute_path(
12
+ "../../ext/sass_embedded/sass_embedded/dart-sass-embedded#{Sass::Platform::OS == 'windows' ? '.bat' : ''}", __dir__
13
+ )
14
+
15
+ PROTOCOL_ERROR_ID = 4_294_967_295
16
+
17
+ def initialize
18
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
19
+ @stdin_semaphore = Mutex.new
20
+ @observerable_semaphore = Mutex.new
21
+
22
+ Thread.new do
23
+ loop do
24
+ bits = length = 0
25
+ loop do
26
+ byte = @stdout.readbyte
27
+ length += (byte & 0x7f) << bits
28
+ bits += 7
29
+ break if byte <= 0x7f
30
+ end
31
+ changed
32
+ payload = @stdout.read length
33
+ @observerable_semaphore.synchronize do
34
+ notify_observers nil, Sass::EmbeddedProtocol::OutboundMessage.decode(payload)
35
+ end
36
+ rescue Interrupt
37
+ break
38
+ rescue IOError => e
39
+ notify_observers e, nil
40
+ close
41
+ break
42
+ end
43
+ end
44
+
45
+ Thread.new do
46
+ loop do
47
+ warn @stderr.read
48
+ rescue Interrupt
49
+ break
50
+ rescue IOError => e
51
+ @observerable_semaphore.synchronize do
52
+ notify_observers e, nil
53
+ end
54
+ close
55
+ break
56
+ end
57
+ end
58
+ end
59
+
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
+ end
99
+
100
+ private
101
+
102
+ def write(proto)
103
+ @stdin_semaphore.synchronize do
104
+ length = proto.length
105
+ while length.positive?
106
+ @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
107
+ length >>= 7
108
+ end
109
+ @stdin.write proto
110
+ end
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
136
+ end
137
+ end
138
+ end
data/lib/sass/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- VERSION = '0.1.3'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -3,19 +3,19 @@
3
3
  require_relative 'test_helper'
4
4
 
5
5
  module Sass
6
- class CompilerTest < MiniTest::Test
6
+ class EmbeddedTest < MiniTest::Test
7
7
  include TempFileTest
8
8
 
9
9
  def setup
10
- @compiler = Embedded::Compiler.new
10
+ @embedded = Embedded.new
11
11
  end
12
12
 
13
13
  def teardown
14
- @compiler.close
14
+ @embedded.close
15
15
  end
16
16
 
17
17
  def render(data)
18
- @compiler.render({ data: data })[:css]
18
+ @embedded.render({ data: data })[:css]
19
19
  end
20
20
 
21
21
  def test_line_comments
@@ -31,7 +31,7 @@ module Sass
31
31
  baz: bang;
32
32
  }
33
33
  CSS
34
- output = @compiler.render({
34
+ output = @embedded.render({
35
35
  data: template,
36
36
  source_comments: true
37
37
  })
@@ -97,7 +97,7 @@ module Sass
97
97
  baz: 0.33333333;
98
98
  }
99
99
  CSS
100
- output = @compiler.render({
100
+ output = @embedded.render({
101
101
  data: template,
102
102
  precision: 8
103
103
  })
@@ -134,7 +134,7 @@ module Sass
134
134
  padding: 20px;
135
135
  }
136
136
  SCSS
137
- output = @compiler.render({
137
+ output = @embedded.render({
138
138
  data: File.read('style.scss'),
139
139
  source_map: 'style.scss.map'
140
140
  })
@@ -143,7 +143,7 @@ module Sass
143
143
  end
144
144
 
145
145
  def test_no_source_map
146
- output = @compiler.render({
146
+ output = @embedded.render({
147
147
  data: '$size: 30px;'
148
148
  })
149
149
  assert_equal '', output[:map]
@@ -157,7 +157,7 @@ module Sass
157
157
  temp_file('included_2/import.scss', "@use 'import_parent' as *; $size: $s;")
158
158
  temp_file('styles.scss', "@use 'import.scss' as *; .hi { width: $size; }")
159
159
 
160
- assert_equal ".hi {\n width: 30px;\n}", @compiler.render({
160
+ assert_equal ".hi {\n width: 30px;\n}", @embedded.render({
161
161
  data: File.read('styles.scss'),
162
162
  include_paths: %w[
163
163
  included_1 included_2
@@ -216,9 +216,9 @@ module Sass
216
216
  }
217
217
  CSS
218
218
 
219
- assert_equal css, @compiler.render({ data: sass, indented_syntax: true })[:css]
219
+ assert_equal css, @embedded.render({ data: sass, indented_syntax: true })[:css]
220
220
  assert_raises(CompilationError) do
221
- @compiler.render({ data: sass, indented_syntax: false })
221
+ @embedded.render({ data: sass, indented_syntax: false })
222
222
  end
223
223
  end
224
224
 
@@ -230,7 +230,7 @@ module Sass
230
230
  baz: bang; }
231
231
  SCSS
232
232
 
233
- output = @compiler.render({
233
+ output = @embedded.render({
234
234
  data: template,
235
235
  source_map: '.',
236
236
  source_map_embed: true,
@@ -263,7 +263,7 @@ module Sass
263
263
  threads = []
264
264
  10.times do |i|
265
265
  threads << Thread.new(i) do |id|
266
- output = @compiler.render({
266
+ output = @embedded.render({
267
267
  data: "div { width: #{id} }"
268
268
  })[:css]
269
269
  assert_match(/#{id}/, output)
@@ -7,15 +7,15 @@ module Sass
7
7
  include TempFileTest
8
8
 
9
9
  def setup
10
- @compiler = Embedded::Compiler.new
10
+ @embedded = Embedded.new
11
11
  end
12
12
 
13
13
  def teardown
14
- @compiler.close
14
+ @embedded.close
15
15
  end
16
16
 
17
17
  def render(data, importer)
18
- @compiler.render({ data: data, importer: importer })[:css]
18
+ @embedded.render({ data: data, importer: importer })[:css]
19
19
  end
20
20
 
21
21
  def test_custom_importer_works
@@ -116,7 +116,7 @@ module Sass
116
116
  end
117
117
 
118
118
  def test_parent_path_is_accessible
119
- output = @compiler.render({
119
+ output = @embedded.render({
120
120
  data: "@import 'parent.scss';",
121
121
  file: 'import-parent-filename.scss',
122
122
  importer: [
@@ -133,13 +133,13 @@ module Sass
133
133
  CSS
134
134
  end
135
135
 
136
- def test_call_compiler_importer
137
- output = @compiler.render({
136
+ def test_call_embedded_importer
137
+ output = @embedded.render({
138
138
  data: "@import 'parent.scss';",
139
139
  importer: [
140
140
  lambda { |_url, _prev|
141
141
  {
142
- contents: @compiler.render({
142
+ contents: @embedded.render({
143
143
  data: "@import 'parent-parent.scss'",
144
144
  importer: [
145
145
  lambda { |_url, _prev|
data/test/error_test.rb CHANGED
@@ -5,11 +5,11 @@ require_relative 'test_helper'
5
5
  module Sass
6
6
  class ErrorTest < MiniTest::Test
7
7
  def setup
8
- @compiler = Embedded::Compiler.new
8
+ @embedded = Embedded.new
9
9
  end
10
10
 
11
11
  def teardown
12
- @compiler.close
12
+ @embedded.close
13
13
  end
14
14
 
15
15
  def test_first_backtrace_is_sass
@@ -20,7 +20,7 @@ module Sass
20
20
  }
21
21
  SCSS
22
22
 
23
- @compiler.render({
23
+ @embedded.render({
24
24
  data: template
25
25
  })
26
26
  rescue Sass::CompilationError => e
@@ -5,15 +5,15 @@ require_relative 'test_helper'
5
5
  module Sass
6
6
  class FunctionsTest < MiniTest::Test
7
7
  def setup
8
- @compiler = Embedded::Compiler.new
8
+ @embedded = Embedded.new
9
9
  end
10
10
 
11
11
  def teardown
12
- @compiler.close
12
+ @embedded.close
13
13
  end
14
14
 
15
15
  def render(sass)
16
- @compiler.render({
16
+ @embedded.render({
17
17
  data: sass,
18
18
  functions: {
19
19
  'javascript_path($path)': lambda { |path|
@@ -322,7 +322,7 @@ module Sass
322
322
  threads = []
323
323
  10.times do |i|
324
324
  threads << Thread.new(i) do |id|
325
- output = @compiler.render({
325
+ output = @embedded.render({
326
326
  data: 'div { url: test-function() }',
327
327
  functions: {
328
328
  'test_function()': lambda {
@@ -346,7 +346,7 @@ module Sass
346
346
  end
347
347
 
348
348
  def test_pass_custom_functions_as_a_parameter
349
- output = @compiler.render({
349
+ output = @embedded.render({
350
350
  data: 'div { url: test-function(); }',
351
351
  functions: {
352
352
  'test_function()': lambda {
@@ -365,7 +365,7 @@ module Sass
365
365
 
366
366
  def test_pass_incompatible_type_to_custom_functions
367
367
  assert_raises(CompilationError) do
368
- @compiler.render({
368
+ @embedded.render({
369
369
  data: 'div { url: test-function(); }',
370
370
  functions: {
371
371
  'test_function()': lambda {
@@ -5,11 +5,11 @@ require_relative 'test_helper'
5
5
  module Sass
6
6
  class OutputStyleTest < MiniTest::Test
7
7
  def setup
8
- @compiler = Embedded::Compiler.new
8
+ @embedded = Embedded.new
9
9
  end
10
10
 
11
11
  def teardown
12
- @compiler.close
12
+ @embedded.close
13
13
  end
14
14
 
15
15
  def input_scss
@@ -51,40 +51,40 @@ module Sass
51
51
  end
52
52
 
53
53
  def test_expanded_output_is_default
54
- output = @compiler.render({ data: input_scss })[:css]
54
+ output = @embedded.render({ data: input_scss })[:css]
55
55
  assert_equal expected_expanded_output, output
56
56
  end
57
57
 
58
58
  def test_output_style_accepts_strings
59
- output = @compiler.render({ data: input_scss, output_style: :expanded })[:css]
59
+ output = @embedded.render({ data: input_scss, output_style: :expanded })[:css]
60
60
  assert_equal expected_expanded_output, output
61
61
  end
62
62
 
63
63
  def test_invalid_output_style
64
64
  assert_raises(InvalidStyleError) do
65
- @compiler.render({ data: input_scss, output_style: :totally_wrong })[:css]
65
+ @embedded.render({ data: input_scss, output_style: :totally_wrong })[:css]
66
66
  end
67
67
  end
68
68
 
69
69
  def test_unsupported_output_style
70
70
  assert_raises(UnsupportedValue) do
71
- @compiler.render({ data: input_scss, output_style: :nested })[:css]
71
+ @embedded.render({ data: input_scss, output_style: :nested })[:css]
72
72
  end
73
73
 
74
74
  assert_raises(UnsupportedValue) do
75
- @compiler.render({ data: input_scss, output_style: :compact })[:css]
75
+ @embedded.render({ data: input_scss, output_style: :compact })[:css]
76
76
  end
77
77
  end
78
78
 
79
79
  def test_compressed_output
80
- output = @compiler.render({ data: input_scss, output_style: :compressed })[:css]
80
+ output = @embedded.render({ data: input_scss, output_style: :compressed })[:css]
81
81
  assert_equal <<~CSS.chomp, output
82
82
  #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline}
83
83
  CSS
84
84
  end
85
85
 
86
86
  def test_string_output_style_names
87
- output = @compiler.render({ data: input_scss, output_style: 'compressed' })[:css]
87
+ output = @embedded.render({ data: input_scss, output_style: 'compressed' })[:css]
88
88
  assert_equal <<~CSS.chomp, output
89
89
  #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline}
90
90
  CSS
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sass-embedded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - なつき
@@ -126,10 +126,10 @@ files:
126
126
  - ext/sass_embedded/Makefile
127
127
  - ext/sass_embedded/extconf.rb
128
128
  - lib/sass.rb
129
- - lib/sass/embedded/compiler.rb
130
- - lib/sass/embedded/transport.rb
129
+ - lib/sass/embedded.rb
131
130
  - lib/sass/error.rb
132
131
  - lib/sass/platform.rb
132
+ - lib/sass/transport.rb
133
133
  - lib/sass/util.rb
134
134
  - lib/sass/version.rb
135
135
  - sass-embedded.gemspec
@@ -1,238 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sass
4
- module Embedded
5
- class Compiler
6
- def initialize
7
- @transport = Transport.new
8
- @id_semaphore = Mutex.new
9
- @id = 0
10
- end
11
-
12
- def render(options)
13
- start = Sass::Util.now
14
-
15
- raise Sass::NotRenderedError, 'Either :data or :file must be set.' if options[:file].nil? && options[:data].nil?
16
-
17
- string = if options[:data]
18
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
19
- source: options[:data],
20
- url: options[:file] ? Sass::Util.file_uri(options[:file]) : 'stdin',
21
- syntax: options[:indented_syntax] == true ? Sass::EmbeddedProtocol::Syntax::INDENTED : Sass::EmbeddedProtocol::Syntax::SCSS
22
- )
23
- end
24
-
25
- path = options[:data] ? nil : options[:file]
26
-
27
- style = case options[:output_style]&.to_sym
28
- when :expanded, nil
29
- Sass::EmbeddedProtocol::OutputStyle::EXPANDED
30
- when :compressed
31
- Sass::EmbeddedProtocol::OutputStyle::COMPRESSED
32
- when :nested, :compact
33
- raise Sass::UnsupportedValue, "#{options[:output_style]} is not a supported :output_style"
34
- else
35
- raise Sass::InvalidStyleError, "#{options[:output_style]} is not a valid :output_style"
36
- end
37
-
38
- source_map = options[:source_map].is_a?(String) || (options[:source_map] == true && !options[:out_file].nil?)
39
-
40
- # 1. Loading a file relative to the file in which the @use or @import appeared.
41
- # 2. Each custom importer.
42
- # 3. Loading a file relative to the current working directory.
43
- # 4. Each load path in includePaths
44
- # 5. Each load path specified in the SASS_PATH environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.
45
- importers = (if options[:importer]
46
- [
47
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(importer_id: 0)
48
- ]
49
- else
50
- []
51
- end).concat(
52
- (options[:include_paths] || []).concat(Sass.include_paths)
53
- .map do |include_path|
54
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
55
- path: File.absolute_path(include_path)
56
- )
57
- end
58
- )
59
-
60
- signatures = []
61
- functions = {}
62
- options[:functions]&.each do |signature, function|
63
- signatures.push signature
64
- functions[signature.to_s.split('(')[0].chomp] = function
65
- end
66
-
67
- compilation_id = next_id
68
-
69
- compile_request = Sass::EmbeddedProtocol::InboundMessage::CompileRequest.new(
70
- id: compilation_id,
71
- string: string,
72
- path: path,
73
- style: style,
74
- source_map: source_map,
75
- importers: importers,
76
- global_functions: options[:functions] ? signatures : [],
77
- alert_color: true,
78
- alert_ascii: true
79
- )
80
-
81
- response = @transport.send compile_request, compilation_id
82
-
83
- file = options[:file] || 'stdin'
84
- canonicalizations = {}
85
- imports = {}
86
-
87
- loop do
88
- case response
89
- when Sass::EmbeddedProtocol::OutboundMessage::CompileResponse
90
- break
91
- when Sass::EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
92
- url = Sass::Util.file_uri(File.absolute_path(response.url, File.dirname(file)))
93
-
94
- if canonicalizations.key? url
95
- canonicalizations[url].id = response.id
96
- else
97
- resolved = nil
98
- options[:importer].each do |importer|
99
- begin
100
- resolved = importer.call response.url, file
101
- rescue StandardError => e
102
- resolved = e
103
- end
104
- break if resolved
105
- end
106
- if resolved.nil?
107
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
108
- id: response.id,
109
- url: url
110
- )
111
- elsif resolved.is_a? StandardError
112
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
113
- id: response.id,
114
- error: resolved.message
115
- )
116
- elsif resolved.key? :contents
117
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
118
- id: response.id,
119
- url: url
120
- )
121
- imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
122
- id: response.id,
123
- success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
124
- contents: resolved[:contents],
125
- syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
126
- source_map_url: nil
127
- )
128
- )
129
- elsif resolved.key? :file
130
- canonicalized_url = Sass::Util.file_uri(resolved[:file])
131
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
132
- id: response.id,
133
- url: canonicalized_url
134
- )
135
- imports[canonicalized_url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
136
- id: response.id,
137
- success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
138
- contents: File.read(resolved[:file]),
139
- syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
140
- source_map_url: nil
141
- )
142
- )
143
- else
144
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
145
- id: response.id,
146
- error: "Unexpected value returned from importer: #{resolved}"
147
- )
148
- end
149
- end
150
-
151
- response = @transport.send canonicalizations[url], compilation_id
152
- when Sass::EmbeddedProtocol::OutboundMessage::ImportRequest
153
- url = response.url
154
-
155
- if imports.key? url
156
- imports[url].id = response.id
157
- else
158
- imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
159
- id: response.id,
160
- error: "Failed to import: #{url}"
161
- )
162
- end
163
-
164
- response = @transport.send imports[url], compilation_id
165
- when Sass::EmbeddedProtocol::OutboundMessage::FunctionCallRequest
166
- begin
167
- message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
168
- id: response.id,
169
- success: functions[response.name].call(*response.arguments)
170
- )
171
- rescue StandardError => e
172
- message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
173
- id: response.id,
174
- error: e.message
175
- )
176
- end
177
-
178
- response = @transport.send message, compilation_id
179
- when Sass::EmbeddedProtocol::ProtocolError
180
- raise Sass::ProtocolError, response.message
181
- else
182
- raise Sass::ProtocolError, "Unexpected packet received: #{response}"
183
- end
184
- end
185
-
186
- if response.failure
187
- raise Sass::CompilationError.new(
188
- response.failure.message,
189
- response.failure.formatted,
190
- response.failure.span ? response.failure.span.url : nil,
191
- response.failure.span ? response.failure.span.start.line + 1 : nil,
192
- response.failure.span ? response.failure.span.start.column + 1 : nil,
193
- 1
194
- )
195
- end
196
-
197
- finish = Sass::Util.now
198
-
199
- {
200
- css: response.success.css,
201
- map: response.success.source_map,
202
- stats: {
203
- entry: options[:file] || 'data',
204
- start: start,
205
- end: finish,
206
- duration: finish - start
207
- }
208
- }
209
- end
210
-
211
- def close
212
- @transport.close
213
- end
214
-
215
- private
216
-
217
- def info
218
- version_response = @transport.send Sass::EmbeddedProtocol::InboundMessage::VersionRequest.new(
219
- id: next_id
220
- )
221
- {
222
- compiler_version: version_response.compiler_version,
223
- protocol_version: version_response.protocol_version,
224
- implementation_name: version_response.implementation_name,
225
- implementation_version: version_response.implementation_version
226
- }
227
- end
228
-
229
- def next_id
230
- @id_semaphore.synchronize do
231
- @id += 1
232
- @id = 0 if @id == Transport::PROTOCOL_ERROR_ID
233
- @id
234
- end
235
- end
236
- end
237
- end
238
- end
@@ -1,140 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'observer'
5
- require_relative '../../../ext/sass_embedded/embedded_sass_pb'
6
-
7
- module Sass
8
- module Embedded
9
- class Transport
10
- include Observable
11
-
12
- DART_SASS_EMBEDDED = File.absolute_path(
13
- "../../../ext/sass_embedded/sass_embedded/dart-sass-embedded#{Sass::Platform::OS == 'windows' ? '.bat' : ''}", __dir__
14
- )
15
-
16
- PROTOCOL_ERROR_ID = 4_294_967_295
17
-
18
- def initialize
19
- @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
20
- @stdin_semaphore = Mutex.new
21
- @observerable_semaphore = Mutex.new
22
-
23
- Thread.new do
24
- loop do
25
- bits = length = 0
26
- loop do
27
- byte = @stdout.readbyte
28
- length += (byte & 0x7f) << bits
29
- bits += 7
30
- break if byte <= 0x7f
31
- end
32
- changed
33
- payload = @stdout.read length
34
- @observerable_semaphore.synchronize do
35
- notify_observers nil, Sass::EmbeddedProtocol::OutboundMessage.decode(payload)
36
- end
37
- rescue Interrupt
38
- break
39
- rescue IOError => e
40
- notify_observers e, nil
41
- close
42
- break
43
- end
44
- end
45
-
46
- Thread.new do
47
- loop do
48
- warn @stderr.read
49
- rescue Interrupt
50
- break
51
- rescue IOError => e
52
- @observerable_semaphore.synchronize do
53
- notify_observers e, nil
54
- end
55
- close
56
- break
57
- end
58
- end
59
- end
60
-
61
- def send(req, id)
62
- mutex = Mutex.new
63
- resource = ConditionVariable.new
64
-
65
- req_name = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
66
-
67
- message = Sass::EmbeddedProtocol::InboundMessage.new(req_name.to_sym => req)
68
-
69
- error = nil
70
- res = nil
71
-
72
- @observerable_semaphore.synchronize do
73
- MessageObserver.new self, id do |e, r|
74
- mutex.synchronize do
75
- error = e
76
- res = r
77
-
78
- resource.signal
79
- end
80
- end
81
- end
82
-
83
- mutex.synchronize do
84
- write message.to_proto
85
-
86
- resource.wait(mutex)
87
- end
88
-
89
- raise error if error
90
-
91
- res
92
- end
93
-
94
- def close
95
- delete_observers
96
- @stdin.close unless @stdin.closed?
97
- @stdout.close unless @stdout.closed?
98
- @stderr.close unless @stderr.closed?
99
- end
100
-
101
- private
102
-
103
- def write(proto)
104
- @stdin_semaphore.synchronize do
105
- length = proto.length
106
- while length.positive?
107
- @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
108
- length >>= 7
109
- end
110
- @stdin.write proto
111
- end
112
- end
113
- end
114
-
115
- class MessageObserver
116
- def initialize(obs, id, &block)
117
- @obs = obs
118
- @id = id
119
- @block = block
120
- @obs.add_observer self
121
- end
122
-
123
- def update(error, message)
124
- if error
125
- @obs.delete_observer self
126
- @block.call error, nil
127
- elsif message.error&.id == Sass::Embedded::Transport::PROTOCOL_ERROR_ID
128
- @obs.delete_observer self
129
- @block.call Sass::ProtocolError.new(message.error.message), nil
130
- else
131
- res = message[message.message.to_s]
132
- if (res['compilation_id'] ? res['compilation_id'] : res['id']) == @id
133
- @obs.delete_observer self
134
- @block.call error, res
135
- end
136
- end
137
- end
138
- end
139
- end
140
- end