sass-embedded 0.2.1 → 0.4.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 {Sass.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)
@@ -2,68 +2,36 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'observer'
5
- require_relative '../../ext/sass_embedded/embedded_sass_pb'
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
11
  class Transport
9
12
  include Observable
10
13
 
11
14
  DART_SASS_EMBEDDED = File.absolute_path(
12
- "../../ext/sass_embedded/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
 
17
20
  def initialize
18
- @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
19
21
  @stdin_semaphore = Mutex.new
20
22
  @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
23
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
24
+ pipe @stderr, $stderr
25
+ receive
58
26
  end
59
27
 
60
28
  def send(req, id)
61
29
  mutex = Mutex.new
62
30
  resource = ConditionVariable.new
63
31
 
64
- req_name = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
32
+ req_kind = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
65
33
 
66
- message = Sass::EmbeddedProtocol::InboundMessage.new(req_name.to_sym => req)
34
+ message = EmbeddedProtocol::InboundMessage.new(req_kind => req)
67
35
 
68
36
  error = nil
69
37
  res = nil
@@ -95,42 +63,86 @@ module Sass
95
63
  @stdin.close unless @stdin.closed?
96
64
  @stdout.close unless @stdout.closed?
97
65
  @stderr.close unless @stderr.closed?
66
+ nil
98
67
  end
99
68
 
100
69
  private
101
70
 
102
- def write(proto)
71
+ def receive
72
+ Thread.new do
73
+ loop do
74
+ bits = length = 0
75
+ loop do
76
+ byte = @stdout.readbyte
77
+ length += (byte & 0x7f) << bits
78
+ bits += 7
79
+ break if byte <= 0x7f
80
+ end
81
+ changed
82
+ payload = @stdout.read length
83
+ @observerable_semaphore.synchronize do
84
+ notify_observers nil, EmbeddedProtocol::OutboundMessage.decode(payload)
85
+ end
86
+ rescue Interrupt
87
+ break
88
+ rescue IOError => e
89
+ notify_observers e, nil
90
+ close
91
+ break
92
+ end
93
+ end
94
+ end
95
+
96
+ def pipe(readable, writeable)
97
+ Thread.new do
98
+ loop do
99
+ writeable.write readable.read
100
+ rescue Interrupt
101
+ break
102
+ rescue IOError => e
103
+ @observerable_semaphore.synchronize do
104
+ notify_observers e, nil
105
+ end
106
+ close
107
+ break
108
+ end
109
+ end
110
+ end
111
+
112
+ def write(payload)
103
113
  @stdin_semaphore.synchronize do
104
- length = proto.length
114
+ length = payload.length
105
115
  while length.positive?
106
116
  @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
107
117
  length >>= 7
108
118
  end
109
- @stdin.write proto
119
+ @stdin.write payload
110
120
  end
111
121
  end
112
- end
113
122
 
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
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
121
132
 
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
133
+ def update(error, message)
134
+ if error
132
135
  @obs.delete_observer self
133
- @block.call error, res
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
134
146
  end
135
147
  end
136
148
  end
data/lib/sass/util.rb CHANGED
@@ -1,19 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
+ # Utilities functions
4
5
  module Util
5
6
  module_function
6
7
 
8
+ FILE_PROTOCOL = 'file://'
9
+
7
10
  def file_uri(path)
8
11
  absolute_path = File.absolute_path(path)
9
12
 
10
- unless absolute_path.start_with?('/')
13
+ unless absolute_path.start_with? File::SEPARATOR
14
+ components = absolute_path.split File::SEPARATOR
15
+ components[0] = components[0][0].downcase
16
+ absolute_path = components.join File::SEPARATOR
17
+ end
18
+
19
+ "#{FILE_PROTOCOL}#{absolute_path}"
20
+ end
21
+
22
+ def path(file_uri)
23
+ absolute_path = file_uri[FILE_PROTOCOL.length..]
24
+
25
+ unless absolute_path.start_with? File::SEPARATOR
11
26
  components = absolute_path.split File::SEPARATOR
12
- components[0] = components[0].split(':').first.downcase
27
+ components[0] = "#{components[0].upcase}:"
13
28
  absolute_path = components.join File::SEPARATOR
14
29
  end
15
30
 
16
- "file://#{absolute_path}"
31
+ absolute_path
32
+ end
33
+
34
+ def relative(from, to)
35
+ Pathname.new(File.absolute_path(to)).relative_path_from(Pathname.new(File.absolute_path(from))).to_s
17
36
  end
18
37
 
19
38
  def now
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.2.1'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -15,11 +15,11 @@ Gem::Specification.new do |spec|
15
15
  spec.license = 'MIT'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0")
18
- spec.extensions = ['ext/sass_embedded/extconf.rb']
18
+ spec.extensions = ['ext/extconf.rb']
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
 
22
- spec.required_ruby_version = '>= 2.0.0'
22
+ spec.required_ruby_version = '>= 2.6'
23
23
 
24
24
  spec.require_paths = ['lib']
25
25
 
@@ -33,4 +33,6 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency 'rake'
34
34
  spec.add_development_dependency 'rake-compiler'
35
35
  spec.add_development_dependency 'rubocop'
36
+ spec.add_development_dependency 'rubocop-minitest'
37
+ spec.add_development_dependency 'rubocop-rake'
36
38
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ module Sass
6
+ class ConcurrencyTest < MiniTest::Test
7
+ include TempFileTest
8
+
9
+ def setup
10
+ @embedded = Embedded.new
11
+ end
12
+
13
+ def teardown
14
+ @embedded.close
15
+ end
16
+
17
+ def render(data)
18
+ @embedded.render(data: data)[:css]
19
+ end
20
+
21
+ def test_concurrency
22
+ 10.times do
23
+ threads = []
24
+ 10.times do |i|
25
+ threads << Thread.new(i) do |id|
26
+ output = @embedded.render(data: "div { width: #{id} }")[:css]
27
+ assert_match(/#{id}/, output)
28
+ end
29
+ end
30
+ threads.each(&:join)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -13,155 +13,153 @@ module Sass
13
13
  end
14
14
 
15
15
  def render(sass)
16
- @embedded.render({
17
- data: sass,
18
- functions: {
19
- 'javascript_path($path)': lambda { |path|
20
- path.string.text = "/js/#{path.string.text}"
21
- path
22
- },
23
- 'stylesheet_path($path)': lambda { |path|
24
- path.string.text = "/css/#{path.string.text}"
25
- path
26
- },
27
- 'sass_return_path($path)': lambda { |path|
28
- path
29
- },
30
- 'no_return_path($path)': lambda { |_path|
31
- Sass::EmbeddedProtocol::Value.new(
32
- singleton: Sass::EmbeddedProtocol::SingletonValue::NULL
33
- )
16
+ @embedded.render(data: sass,
17
+ functions: {
18
+ 'javascript_path($path)': lambda { |path|
19
+ path.string.text = "/js/#{path.string.text}"
20
+ path
21
+ },
22
+ 'stylesheet_path($path)': lambda { |path|
23
+ path.string.text = "/css/#{path.string.text}"
24
+ path
25
+ },
26
+ 'sass_return_path($path)': lambda { |path|
27
+ path
34
28
  },
35
- 'optional_arguments($path, $optional: null)': lambda { |path, optional|
36
- Sass::EmbeddedProtocol::Value.new(
37
- string: Sass::EmbeddedProtocol::Value::String.new(
38
- text: "#{path.string.text}/#{optional.singleton == :NULL ? 'bar' : optional.string.text}",
39
- quoted: true
40
- )
29
+ 'no_return_path($path)': lambda { |_path|
30
+ EmbeddedProtocol::Value.new(
31
+ singleton: EmbeddedProtocol::SingletonValue::NULL
32
+ )
33
+ },
34
+ 'optional_arguments($path, $optional: null)': lambda { |path, optional|
35
+ EmbeddedProtocol::Value.new(
36
+ string: EmbeddedProtocol::Value::String.new(
37
+ text: "#{path.string.text}/#{optional.singleton == :NULL ? 'bar' : optional.string.text}",
38
+ quoted: true
41
39
  )
42
- },
43
- 'function_that_raises_errors()': lambda {
44
- raise StandardError,
45
- 'Intentional wrong thing happened somewhere inside the custom function'
46
- },
47
- 'nice_color_argument($color)': lambda { |color|
48
- color
40
+ )
41
+ },
42
+ 'function_that_raises_errors()': lambda {
43
+ raise StandardError,
44
+ 'Intentional wrong thing happened somewhere inside the custom function'
49
45
  },
50
- 'returns_a_color()': lambda {
51
- Sass::EmbeddedProtocol::Value.new(
52
- rgb_color: Sass::EmbeddedProtocol::Value::RgbColor.new(
53
- red: 0,
54
- green: 0,
55
- blue: 0,
56
- alpha: 1
57
- )
46
+ 'nice_color_argument($color)': lambda { |color|
47
+ color
48
+ },
49
+ 'returns_a_color()': lambda {
50
+ EmbeddedProtocol::Value.new(
51
+ rgb_color: EmbeddedProtocol::Value::RgbColor.new(
52
+ red: 0,
53
+ green: 0,
54
+ blue: 0,
55
+ alpha: 1
58
56
  )
59
- },
60
- 'returns_a_number()': lambda {
61
- Sass::EmbeddedProtocol::Value.new(
62
- number: Sass::EmbeddedProtocol::Value::Number.new(
63
- value: -312,
64
- numerators: ['rem']
65
- )
57
+ )
58
+ },
59
+ 'returns_a_number()': lambda {
60
+ EmbeddedProtocol::Value.new(
61
+ number: EmbeddedProtocol::Value::Number.new(
62
+ value: -312,
63
+ numerators: ['rem']
66
64
  )
67
- },
68
- 'returns_a_bool()': lambda {
69
- Sass::EmbeddedProtocol::Value.new(
70
- singleton: Sass::EmbeddedProtocol::SingletonValue::TRUE
71
65
  )
72
66
  },
73
- 'inspect_bool($argument)': lambda { |argument|
74
- unless argument&.singleton == :TRUE || argument.singleton == :FALSE
67
+ 'returns_a_bool()': lambda {
68
+ EmbeddedProtocol::Value.new(
69
+ singleton: EmbeddedProtocol::SingletonValue::TRUE
70
+ )
71
+ },
72
+ 'inspect_bool($argument)': lambda { |argument|
73
+ unless argument&.singleton == :TRUE || argument.singleton == :FALSE
74
+ raise StandardError,
75
+ 'passed value is not a EmbeddedProtocol::SingletonValue::TRUE or EmbeddedProtocol::SingletonValue::FALSE'
76
+ end
77
+
78
+ argument
79
+ },
80
+ 'inspect_number($argument)': lambda { |argument|
81
+ unless argument&.number.is_a? EmbeddedProtocol::Value::Number
75
82
  raise StandardError,
76
- 'passed value is not a Sass::EmbeddedProtocol::SingletonValue::TRUE or Sass::EmbeddedProtocol::SingletonValue::FALSE'
83
+ 'passed value is not a EmbeddedProtocol::Value::Number'
77
84
  end
78
85
 
79
86
  argument
80
87
  },
81
- 'inspect_number($argument)': lambda { |argument|
82
- unless argument&.number.is_a? Sass::EmbeddedProtocol::Value::Number
83
- raise StandardError,
84
- 'passed value is not a Sass::EmbeddedProtocol::Value::Number'
85
- end
86
-
87
- argument
88
- },
89
- 'inspect_map($argument)': lambda { |argument|
90
- unless argument&.map.is_a? Sass::EmbeddedProtocol::Value::Map
91
- raise StandardError,
92
- 'passed value is not a Sass::EmbeddedProtocol::Value::Map'
93
- end
88
+ 'inspect_map($argument)': lambda { |argument|
89
+ unless argument&.map.is_a? EmbeddedProtocol::Value::Map
90
+ raise StandardError,
91
+ 'passed value is not a EmbeddedProtocol::Value::Map'
92
+ end
94
93
 
95
- argument
96
- },
97
- 'inspect_list($argument)': lambda { |argument|
98
- unless argument&.list.is_a? Sass::EmbeddedProtocol::Value::List
99
- raise StandardError,
100
- 'passed value is not a Sass::EmbeddedProtocol::Value::List'
101
- end
94
+ argument
95
+ },
96
+ 'inspect_list($argument)': lambda { |argument|
97
+ unless argument&.list.is_a? EmbeddedProtocol::Value::List
98
+ raise StandardError,
99
+ 'passed value is not a EmbeddedProtocol::Value::List'
100
+ end
102
101
 
103
- argument
104
- },
105
- 'returns_sass_value()': lambda {
106
- Sass::EmbeddedProtocol::Value.new(
107
- rgb_color: Sass::EmbeddedProtocol::Value::RgbColor.new(
108
- red: 0,
109
- green: 0,
110
- blue: 0,
111
- alpha: 1
112
- )
102
+ argument
103
+ },
104
+ 'returns_sass_value()': lambda {
105
+ EmbeddedProtocol::Value.new(
106
+ rgb_color: EmbeddedProtocol::Value::RgbColor.new(
107
+ red: 0,
108
+ green: 0,
109
+ blue: 0,
110
+ alpha: 1
113
111
  )
114
- },
115
- 'returns_sass_map()': lambda {
116
- Sass::EmbeddedProtocol::Value.new(
117
- map: Sass::EmbeddedProtocol::Value::Map.new(
118
- entries: [
119
- Sass::EmbeddedProtocol::Value::Map::Entry.new(
120
- key: Sass::EmbeddedProtocol::Value.new(
121
- string: Sass::EmbeddedProtocol::Value::String.new(
122
- text: 'color',
123
- quoted: true
124
- )
125
- ),
126
- value: Sass::EmbeddedProtocol::Value.new(
127
- rgb_color: Sass::EmbeddedProtocol::Value::RgbColor.new(
128
- red: 0,
129
- green: 0,
130
- blue: 0,
131
- alpha: 1
132
- )
112
+ )
113
+ },
114
+ 'returns_sass_map()': lambda {
115
+ EmbeddedProtocol::Value.new(
116
+ map: EmbeddedProtocol::Value::Map.new(
117
+ entries: [
118
+ EmbeddedProtocol::Value::Map::Entry.new(
119
+ key: EmbeddedProtocol::Value.new(
120
+ string: EmbeddedProtocol::Value::String.new(
121
+ text: 'color',
122
+ quoted: true
123
+ )
124
+ ),
125
+ value: EmbeddedProtocol::Value.new(
126
+ rgb_color: EmbeddedProtocol::Value::RgbColor.new(
127
+ red: 0,
128
+ green: 0,
129
+ blue: 0,
130
+ alpha: 1
133
131
  )
134
132
  )
135
- ]
136
- )
133
+ )
134
+ ]
137
135
  )
138
- },
139
- 'returns_sass_list()': lambda {
140
- Sass::EmbeddedProtocol::Value.new(
141
- list: Sass::EmbeddedProtocol::Value::List.new(
142
- separator: Sass::EmbeddedProtocol::ListSeparator::COMMA,
143
- has_brackets: true,
144
- contents: [
145
- Sass::EmbeddedProtocol::Value.new(
146
- number: Sass::EmbeddedProtocol::Value::Number.new(
147
- value: 10
148
- )
149
- ),
150
- Sass::EmbeddedProtocol::Value.new(
151
- number: Sass::EmbeddedProtocol::Value::Number.new(
152
- value: 20
153
- )
154
- ),
155
- Sass::EmbeddedProtocol::Value.new(
156
- number: Sass::EmbeddedProtocol::Value::Number.new(
157
- value: 30
158
- )
136
+ )
137
+ },
138
+ 'returns_sass_list()': lambda {
139
+ EmbeddedProtocol::Value.new(
140
+ list: EmbeddedProtocol::Value::List.new(
141
+ separator: EmbeddedProtocol::ListSeparator::COMMA,
142
+ has_brackets: true,
143
+ contents: [
144
+ EmbeddedProtocol::Value.new(
145
+ number: EmbeddedProtocol::Value::Number.new(
146
+ value: 10
159
147
  )
160
- ]
161
- )
148
+ ),
149
+ EmbeddedProtocol::Value.new(
150
+ number: EmbeddedProtocol::Value::Number.new(
151
+ value: 20
152
+ )
153
+ ),
154
+ EmbeddedProtocol::Value.new(
155
+ number: EmbeddedProtocol::Value::Number.new(
156
+ value: 30
157
+ )
158
+ )
159
+ ]
162
160
  )
163
- }
164
- }
161
+ )
162
+ }
165
163
  })[:css]
166
164
  end
167
165
 
@@ -270,7 +268,7 @@ module Sass
270
268
  end
271
269
 
272
270
  def test_function_with_error
273
- assert_raises(Sass::CompilationError) do
271
+ assert_raises(RenderError) do
274
272
  render('div {url: function_that_raises_errors();}')
275
273
  end
276
274
  end
@@ -322,17 +320,15 @@ module Sass
322
320
  threads = []
323
321
  10.times do |i|
324
322
  threads << Thread.new(i) do |id|
325
- output = @embedded.render({
326
- data: 'div { url: test-function() }',
327
- functions: {
328
- 'test_function()': lambda {
329
- Sass::EmbeddedProtocol::Value.new(
330
- string: Sass::EmbeddedProtocol::Value::String.new(
331
- text: "{test_key1: 'test_value', test_key2: #{id}}",
332
- quoted: true
333
- )
323
+ output = @embedded.render(data: 'div { url: test-function() }',
324
+ functions: {
325
+ 'test_function()': lambda {
326
+ EmbeddedProtocol::Value.new(
327
+ string: EmbeddedProtocol::Value::String.new(
328
+ text: "{test_key1: 'test_value', test_key2: #{id}}",
329
+ quoted: true
334
330
  )
335
- }
331
+ )
336
332
  }
337
333
  })[:css]
338
334
  assert_match(/test_key1/, output)
@@ -346,17 +342,15 @@ module Sass
346
342
  end
347
343
 
348
344
  def test_pass_custom_functions_as_a_parameter
349
- output = @embedded.render({
350
- data: 'div { url: test-function(); }',
351
- functions: {
352
- 'test_function()': lambda {
353
- Sass::EmbeddedProtocol::Value.new(
354
- string: Sass::EmbeddedProtocol::Value::String.new(
355
- text: 'custom_function',
356
- quoted: true
357
- )
345
+ output = @embedded.render(data: 'div { url: test-function(); }',
346
+ functions: {
347
+ 'test_function()': lambda {
348
+ EmbeddedProtocol::Value.new(
349
+ string: EmbeddedProtocol::Value::String.new(
350
+ text: 'custom_function',
351
+ quoted: true
358
352
  )
359
- }
353
+ )
360
354
  }
361
355
  })[:css]
362
356
 
@@ -364,13 +358,11 @@ module Sass
364
358
  end
365
359
 
366
360
  def test_pass_incompatible_type_to_custom_functions
367
- assert_raises(CompilationError) do
368
- @embedded.render({
369
- data: 'div { url: test-function(); }',
370
- functions: {
371
- 'test_function()': lambda {
372
- Class.new
373
- }
361
+ assert_raises(RenderError) do
362
+ @embedded.render(data: 'div { url: test-function(); }',
363
+ functions: {
364
+ 'test_function()': lambda {
365
+ Class.new
374
366
  }
375
367
  })[:css]
376
368
  end