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.
- 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
|