sass-embedded 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +47 -0
- data/.gitignore +45 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/README.md +19 -0
- data/Rakefile +14 -0
- data/ext/sass_embedded/.gitignore +2 -0
- data/ext/sass_embedded/Makefile +23 -0
- data/ext/sass_embedded/extconf.rb +72 -0
- data/lib/sass.rb +37 -0
- data/lib/sass/embedded/compiler.rb +250 -0
- data/lib/sass/embedded/transport.rb +147 -0
- data/lib/sass/error.rb +29 -0
- data/lib/sass/platform.rb +56 -0
- data/lib/sass/util.rb +23 -0
- data/lib/sass/version.rb +6 -0
- data/sass-embedded.gemspec +37 -0
- data/test/compiler_test.rb +278 -0
- data/test/custom_importer_test.rb +160 -0
- data/test/error_test.rb +33 -0
- data/test/functions_test.rb +375 -0
- data/test/output_style_test.rb +93 -0
- data/test/test_helper.rb +29 -0
- metadata +158 -0
@@ -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
|
data/lib/sass/version.rb
ADDED
@@ -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
|