sass-embedded 0.1.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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "observer"
5
+ require_relative "../../../ext/sass_embedded/embedded_sass_pb.rb"
6
+
7
+ module Sass
8
+ module Embedded
9
+ class Transport
10
+
11
+ include Observable
12
+
13
+ DART_SASS_EMBEDDED = File.absolute_path("../../../ext/sass_embedded/sass_embedded/dart-sass-embedded#{Sass::Platform::OS == 'windows' ? '.bat' : ''}", __dir__)
14
+
15
+ PROTOCOL_ERROR_ID = 4294967295
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
+ begin
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 {
35
+ notify_observers nil, Sass::EmbeddedProtocol::OutboundMessage.decode(payload)
36
+ }
37
+ rescue Interrupt
38
+ break
39
+ rescue IOError, EOFError => error
40
+ notify_observers error, nil
41
+ close
42
+ break
43
+ end
44
+ end
45
+ end
46
+
47
+ Thread.new do
48
+ loop do
49
+ begin
50
+ $stderr.puts @stderr.read
51
+ rescue Interrupt
52
+ break
53
+ rescue IOError, EOFErrorr => error
54
+ @observerable_semaphore.synchronize {
55
+ notify_observers error, nil
56
+ }
57
+ close
58
+ break
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def send req, id
65
+ mutex = Mutex.new
66
+ resource = ConditionVariable.new
67
+
68
+ req_name = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, "_").downcase
69
+
70
+ message = Sass::EmbeddedProtocol::InboundMessage.new(req_name.to_sym => req)
71
+
72
+ error = nil
73
+ res = nil
74
+
75
+ @observerable_semaphore.synchronize {
76
+ MessageObserver.new self, id do |_error, _res|
77
+ mutex.synchronize {
78
+ error = _error
79
+ res = _res
80
+
81
+ resource.signal
82
+ }
83
+ end
84
+ }
85
+
86
+ mutex.synchronize {
87
+ write message.to_proto
88
+
89
+ resource.wait(mutex)
90
+ }
91
+
92
+ raise error if error
93
+ res
94
+ end
95
+
96
+ def close
97
+ begin
98
+ delete_observers
99
+ @stdin.close
100
+ @stdout.close
101
+ @stderr.close
102
+ rescue
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def write proto
109
+ @stdin_semaphore.synchronize {
110
+ length = proto.length
111
+ while length > 0
112
+ @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
113
+ length >>= 7
114
+ end
115
+ @stdin.write proto
116
+ }
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ class MessageObserver
123
+ def initialize obs, id, &block
124
+ @obs = obs
125
+ @id = id
126
+ @block = block
127
+ @obs.add_observer self
128
+ end
129
+
130
+ def update error, message
131
+ if error
132
+ @obs.delete_observer self
133
+ @block.call error, nil
134
+ elsif message.error&.id == Sass::Embedded::Transport::PROTOCOL_ERROR_ID
135
+ @obs.delete_observer self
136
+ @block.call Sass::ProtocolError.new(message.error.message), nil
137
+ else
138
+ res = message[message.message.to_s]
139
+ if (res['compilation_id'] == @id || res['id'] == @id)
140
+ @obs.delete_observer self
141
+ @block.call error, res
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/sass/error.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+
5
+ class BaseError < StandardError; end
6
+ class ProtocolError < BaseError; end
7
+ class NotRenderedError < BaseError; end
8
+ class InvalidStyleError < BaseError; end
9
+ class UnsupportedValue < BaseError; end
10
+
11
+ class CompilationError < BaseError
12
+
13
+ attr_accessor :formatted, :file, :line, :column, :status
14
+
15
+ def initialize(message, formatted, file, line, column, status)
16
+ @formatted = formatted
17
+ @file = file
18
+ @line = line
19
+ @column = column
20
+ @status = status
21
+ super(message)
22
+ end
23
+
24
+ def backtrace
25
+ return nil if super.nil?
26
+ ["#{@file}:#{@line}:#{@column}"] + super
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+
5
+ module Sass
6
+ module Platform
7
+
8
+ OS = case RbConfig::CONFIG['host_os'].downcase
9
+ when /linux/
10
+ "linux"
11
+ when /darwin/
12
+ "darwin"
13
+ when /freebsd/
14
+ "freebsd"
15
+ when /netbsd/
16
+ "netbsd"
17
+ when /openbsd/
18
+ "openbsd"
19
+ when /dragonfly/
20
+ "dragonflybsd"
21
+ when /sunos|solaris/
22
+ "solaris"
23
+ when /mingw|mswin/
24
+ "windows"
25
+ else
26
+ RbConfig::CONFIG['host_os'].downcase
27
+ end
28
+
29
+ OSVERSION = RbConfig::CONFIG['host_os'].gsub(/[^\d]/, '').to_i
30
+
31
+ CPU = RbConfig::CONFIG['host_cpu']
32
+
33
+ ARCH = case CPU.downcase
34
+ when /amd64|x86_64|x64/
35
+ "x86_64"
36
+ when /i\d86|x86|i86pc/
37
+ "i386"
38
+ when /ppc64|powerpc64/
39
+ "powerpc64"
40
+ when /ppc|powerpc/
41
+ "powerpc"
42
+ when /sparcv9|sparc64/
43
+ "sparcv9"
44
+ when /arm64|aarch64/ # MacOS calls it "arm64", other operating systems "aarch64"
45
+ "aarch64"
46
+ when /^arm/
47
+ if OS == "darwin" # Ruby before 3.0 reports "arm" instead of "arm64" as host_cpu on darwin
48
+ "aarch64"
49
+ else
50
+ "arm"
51
+ end
52
+ else
53
+ RbConfig::CONFIG['host_cpu']
54
+ end
55
+ end
56
+ end
data/lib/sass/util.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ module Util
5
+ extend self
6
+
7
+ def file_uri path
8
+ absolute_path = File.absolute_path(path)
9
+
10
+ if !absolute_path.start_with?('/')
11
+ components = absolute_path.split File::SEPARATOR
12
+ components[0] = components[0].split(':').first.downcase
13
+ absolute_path = components.join File::SEPARATOR
14
+ end
15
+
16
+ 'file://' + absolute_path
17
+ end
18
+
19
+ def now
20
+ (Time.now.to_f * 1000).to_i
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ VERSION = "0.1.0"
5
+ end
6
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "sass/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+
9
+ spec.name = "sass-embedded"
10
+ spec.version = Sass::VERSION
11
+ spec.authors = ["なつき"]
12
+ spec.email = ["i@ntk.me"]
13
+ spec.summary = "Use dart-sass with Ruby!"
14
+ spec.description = "Use dart-sass with Ruby!"
15
+ spec.homepage = "https://github.com/ntkme/embedded-host-ruby"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.extensions = ['ext/sass_embedded/extconf.rb']
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+
23
+
24
+ spec.required_ruby_version = ">= 2.0.0"
25
+
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.platform = Gem::Platform::RUBY
29
+
30
+ spec.add_dependency "google-protobuf", "~> 3.17.0"
31
+
32
+ spec.add_development_dependency "bundler"
33
+ spec.add_development_dependency "minitest", "~> 5.14.4"
34
+ spec.add_development_dependency "minitest-around"
35
+ spec.add_development_dependency "rake"
36
+ spec.add_development_dependency "rake-compiler"
37
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ module Sass
6
+ class CompilerTest < MiniTest::Test
7
+ include TempFileTest
8
+
9
+ def setup
10
+ @compiler = Embedded::Compiler.new
11
+ end
12
+
13
+ def teardown
14
+ end
15
+
16
+ def render(data)
17
+ @compiler.render({ data: data })[:css]
18
+ end
19
+
20
+ def test_line_comments
21
+ skip 'not supported'
22
+
23
+ template = <<-SCSS
24
+ .foo {
25
+ baz: bang; }
26
+ SCSS
27
+ expected_output = <<-CSS
28
+ /* line 1, stdin */
29
+ .foo {
30
+ baz: bang;
31
+ }
32
+ CSS
33
+ output = @compiler.render({
34
+ data: template,
35
+ source_comments: true
36
+ })
37
+ assert_equal expected_output, output[:css]
38
+ end
39
+
40
+ def test_one_line_comments
41
+ assert_equal <<CSS.chomp, render(<<SCSS)
42
+ .foo {
43
+ baz: bang;
44
+ }
45
+ CSS
46
+ .foo {// bar: baz;}
47
+ baz: bang; //}
48
+ }
49
+ SCSS
50
+ assert_equal <<CSS.chomp, render(<<SCSS)
51
+ .foo bar[val="//"] {
52
+ baz: bang;
53
+ }
54
+ CSS
55
+ .foo bar[val="//"] {
56
+ baz: bang; //}
57
+ }
58
+ SCSS
59
+ end
60
+
61
+ def test_variables
62
+ assert_equal <<CSS.chomp, render(<<SCSS)
63
+ blat {
64
+ a: foo;
65
+ }
66
+ CSS
67
+ $var: foo;
68
+
69
+ blat {a: $var}
70
+ SCSS
71
+
72
+ assert_equal <<CSS.chomp, render(<<SCSS)
73
+ foo {
74
+ a: 2;
75
+ b: 6;
76
+ }
77
+ CSS
78
+ foo {
79
+ $var: 2;
80
+ $another-var: 4;
81
+ a: $var;
82
+ b: $var + $another-var;}
83
+ SCSS
84
+ end
85
+
86
+ def test_precision
87
+ skip 'not supported'
88
+
89
+ template = <<-SCSS
90
+ $var: 1;
91
+ .foo {
92
+ baz: $var / 3; }
93
+ SCSS
94
+ expected_output = <<-CSS.chomp
95
+ .foo {
96
+ baz: 0.33333333;
97
+ }
98
+ CSS
99
+ output = @compiler.render({
100
+ data: template,
101
+ precision: 8
102
+ })
103
+ assert_equal expected_output, output
104
+ end
105
+
106
+ def test_precision_not_specified
107
+ template = <<-SCSS
108
+ $var: 1;
109
+ .foo {
110
+ baz: $var / 3; }
111
+ SCSS
112
+ expected_output = <<-CSS.chomp
113
+ .foo {
114
+ baz: 0.3333333333;
115
+ }
116
+ CSS
117
+ output = render(template)
118
+ assert_equal expected_output, output
119
+ end
120
+
121
+ def test_source_map
122
+ temp_dir('admin')
123
+
124
+ temp_file('admin/text-color.scss', <<SCSS)
125
+ p {
126
+ color: red;
127
+ }
128
+ SCSS
129
+ temp_file('style.scss', <<SCSS)
130
+ @use 'admin/text-color';
131
+
132
+ p {
133
+ padding: 20px;
134
+ }
135
+ SCSS
136
+ output = @compiler.render({
137
+ data: File.read("style.scss"),
138
+ source_map: "style.scss.map"
139
+ })
140
+
141
+ assert output[:map].start_with? '{"version":3,'
142
+ end
143
+
144
+ def test_no_source_map
145
+ output = @compiler.render({
146
+ data: "$size: 30px;"
147
+ })
148
+ assert_equal "", output[:map]
149
+ end
150
+
151
+ def test_include_paths
152
+ temp_dir("included_1")
153
+ temp_dir("included_2")
154
+
155
+ temp_file("included_1/import_parent.scss", "$s: 30px;")
156
+ temp_file("included_2/import.scss", "@use 'import_parent' as *; $size: $s;")
157
+ temp_file("styles.scss", "@use 'import.scss' as *; .hi { width: $size; }")
158
+
159
+ assert_equal ".hi {\n width: 30px;\n}", @compiler.render({
160
+ data: File.read("styles.scss"),
161
+ include_paths: [ "included_1", "included_2" ]
162
+ })[:css]
163
+ end
164
+
165
+ def test_global_include_paths
166
+ skip 'race condition in test'
167
+
168
+ temp_dir("included_1")
169
+ temp_dir("included_2")
170
+
171
+ temp_file("included_1/import_parent.scss", "$s: 30px;")
172
+ temp_file("included_2/import.scss", "@use 'import_parent'; $size: $s;")
173
+ temp_file("styles.scss", "@use 'import.scss'; .hi { width: $size; }")
174
+
175
+ ::Sass.include_paths << "included_1"
176
+ ::Sass.include_paths << "included_2"
177
+
178
+ assert_equal ".hi {\n width: 30px;\n}", render(File.read("styles.scss"))
179
+ end
180
+
181
+ def test_env_include_paths
182
+ skip 'race condition in test'
183
+
184
+ expected_include_paths = [ "included_3", "included_4" ]
185
+ ::Sass.instance_eval { @include_paths = nil }
186
+ ENV['SASS_PATH'] = expected_include_paths.join(File::PATH_SEPARATOR)
187
+ assert_equal expected_include_paths, ::Sass.include_paths
188
+ ::Sass.include_paths.clear
189
+ end
190
+
191
+ def test_include_paths_not_configured
192
+ temp_dir("included_5")
193
+ temp_dir("included_6")
194
+ temp_file("included_5/import_parent.scss", "$s: 30px;")
195
+ temp_file("included_6/import.scss", "@use 'import_parent'; $size: $s;")
196
+ temp_file("styles.scss", "@use 'import.scss'; .hi { width: $size; }")
197
+
198
+ assert_raises(CompilationError) do
199
+ render(File.read("styles.scss"))
200
+ end
201
+ end
202
+
203
+ def test_sass_variation
204
+ sass = <<SASS
205
+ $size: 30px
206
+ .foo
207
+ width: $size
208
+ SASS
209
+
210
+ css = <<CSS.chomp
211
+ .foo {
212
+ width: 30px;
213
+ }
214
+ CSS
215
+
216
+ assert_equal css, @compiler.render({ data: sass, indented_syntax: true })[:css]
217
+ assert_raises(CompilationError) do
218
+ @compiler.render({ data: sass, indented_syntax: false })
219
+ end
220
+ end
221
+
222
+ def test_inline_source_maps
223
+ skip 'not supported'
224
+
225
+ template = <<-SCSS
226
+ .foo {
227
+ baz: bang; }
228
+ SCSS
229
+ expected_output = <<-CSS
230
+ /* line 1, stdin */
231
+ .foo {
232
+ baz: bang; }
233
+ CSS
234
+
235
+ output = @compiler.render({
236
+ data: template,
237
+ source_map: ".",
238
+ source_map_embed: true,
239
+ source_map_contents: true
240
+ })[:css]
241
+
242
+ assert_match /sourceMappingURL/, output
243
+ assert_match /.foo/, output
244
+ end
245
+
246
+ def test_empty_template
247
+ output = render('')
248
+ assert_equal '', output
249
+ end
250
+
251
+ def test_import_plain_css
252
+ temp_file("test.css", ".something{color: red}")
253
+ expected_output = <<-CSS.chomp
254
+ .something {
255
+ color: red;
256
+ }
257
+ CSS
258
+
259
+ output = render("@use 'test';")
260
+ assert_equal expected_output, output
261
+ end
262
+
263
+ def test_concurrency
264
+ 10.times do
265
+ threads = []
266
+ 10.times do |i|
267
+ threads << Thread.new(i) do |id|
268
+ output = @compiler.render({
269
+ data: "div { width: #{id} }",
270
+ })[:css]
271
+ assert_match /#{id}/, output
272
+ end
273
+ end
274
+ threads.each(&:join)
275
+ end
276
+ end
277
+ end
278
+ end