sass-embedded 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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