sass-embedded 0.7.28 → 0.9.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,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../embedded_protocol'
4
+ require_relative 'observer'
5
+ require_relative 'util'
6
+
7
+ module Sass
8
+ class Embedded
9
+ # The {Observer} for {Embedded#render}.
10
+ class CompileContext
11
+ include Observer
12
+
13
+ def initialize(channel,
14
+ data:,
15
+ file:,
16
+ indented_syntax:,
17
+ include_paths:,
18
+ output_style:,
19
+ source_map:,
20
+ out_file:,
21
+ functions:,
22
+ importer:)
23
+ raise ArgumentError, 'either data or file must be set' if file.nil? && data.nil?
24
+
25
+ @data = data
26
+ @file = file
27
+ @indented_syntax = indented_syntax
28
+ @include_paths = include_paths
29
+ @output_style = output_style
30
+ @source_map = source_map
31
+ @out_file = out_file
32
+ @global_functions = functions.keys
33
+ @functions = functions.transform_keys do |key|
34
+ key.to_s.split('(')[0].chomp
35
+ end
36
+ @importer = importer
37
+ @import_responses = {}
38
+
39
+ super(channel)
40
+
41
+ send_message compile_request
42
+ end
43
+
44
+ def update(error, message)
45
+ raise error unless error.nil?
46
+
47
+ case message
48
+ when EmbeddedProtocol::OutboundMessage::CompileResponse
49
+ return unless message.id == id
50
+
51
+ Thread.new do
52
+ super(nil, message)
53
+ end
54
+ when EmbeddedProtocol::OutboundMessage::LogEvent
55
+ return unless message.compilation_id == id && $stderr.tty?
56
+
57
+ warn message.formatted
58
+ when EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
59
+ return unless message.compilation_id == id
60
+
61
+ Thread.new do
62
+ send_message canonicalize_response message
63
+ end
64
+ when EmbeddedProtocol::OutboundMessage::ImportRequest
65
+ return unless message.compilation_id == id
66
+
67
+ Thread.new do
68
+ send_message import_response message
69
+ end
70
+ when EmbeddedProtocol::OutboundMessage::FileImportRequest
71
+ raise NotImplementedError, 'FileImportRequest is not implemented'
72
+ when EmbeddedProtocol::OutboundMessage::FunctionCallRequest
73
+ return unless message.compilation_id == id
74
+
75
+ Thread.new do
76
+ send_message function_call_response message
77
+ end
78
+ end
79
+ rescue StandardError => e
80
+ Thread.new do
81
+ super(e, nil)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def compile_request
88
+ EmbeddedProtocol::InboundMessage::CompileRequest.new(
89
+ id: id,
90
+ string: string,
91
+ path: path,
92
+ style: style,
93
+ source_map: source_map,
94
+ importers: importers,
95
+ global_functions: global_functions,
96
+ alert_color: $stderr.tty?,
97
+ alert_ascii: Platform::OS == 'windows'
98
+ )
99
+ end
100
+
101
+ def canonicalize_response(canonicalize_request)
102
+ url = Util.file_uri_from_path(File.absolute_path(canonicalize_request.url, (@file.nil? ? 'stdin' : @file)))
103
+
104
+ begin
105
+ result = @importer[canonicalize_request.importer_id].call canonicalize_request.url, @file
106
+ raise result if result.is_a? StandardError
107
+ rescue StandardError => e
108
+ return EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
109
+ id: canonicalize_request.id,
110
+ error: e.message
111
+ )
112
+ end
113
+
114
+ if result&.key? :contents
115
+ @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
116
+ id: canonicalize_request.id,
117
+ success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
118
+ contents: result[:contents],
119
+ syntax: EmbeddedProtocol::Syntax::SCSS,
120
+ source_map_url: nil
121
+ )
122
+ )
123
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
124
+ id: canonicalize_request.id,
125
+ url: url
126
+ )
127
+ elsif result&.key? :file
128
+ canonicalized_url = Util.file_uri_from_path(File.absolute_path(result[:file]))
129
+
130
+ # TODO: FileImportRequest is not supported yet.
131
+ # Workaround by reading contents and return it when server asks
132
+ @import_responses[canonicalized_url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
133
+ id: canonicalize_request.id,
134
+ success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
135
+ contents: File.read(result[:file]),
136
+ syntax: EmbeddedProtocol::Syntax::SCSS,
137
+ source_map_url: nil
138
+ )
139
+ )
140
+
141
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
142
+ id: canonicalize_request.id,
143
+ url: canonicalized_url
144
+ )
145
+ else
146
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
147
+ id: canonicalize_request.id
148
+ )
149
+ end
150
+ end
151
+
152
+ def import_response(import_request)
153
+ url = import_request.url
154
+
155
+ if @import_responses.key? url
156
+ @import_responses[url].id = import_request.id
157
+ else
158
+ @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
159
+ id: import_request.id,
160
+ error: "Failed to import: #{url}"
161
+ )
162
+ end
163
+
164
+ @import_responses[url]
165
+ end
166
+
167
+ def function_call_response(function_call_request)
168
+ # TODO: convert argument_list to **kwargs
169
+ EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
170
+ id: function_call_request.id,
171
+ success: @functions[function_call_request.name].call(*function_call_request.arguments),
172
+ accessed_argument_lists: function_call_request.arguments
173
+ .filter { |argument| argument.value == :argument_list }
174
+ .map { |argument| argument.argument_list.id }
175
+ )
176
+ rescue StandardError => e
177
+ EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
178
+ id: function_call_request.id,
179
+ error: e.message
180
+ )
181
+ end
182
+
183
+ def syntax
184
+ if @indented_syntax == true
185
+ EmbeddedProtocol::Syntax::INDENTED
186
+ else
187
+ EmbeddedProtocol::Syntax::SCSS
188
+ end
189
+ end
190
+
191
+ def url
192
+ return if @file.nil?
193
+
194
+ Util.file_uri_from_path File.absolute_path @file
195
+ end
196
+
197
+ def string
198
+ return if @data.nil?
199
+
200
+ EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
201
+ source: @data,
202
+ url: url,
203
+ syntax: syntax
204
+ )
205
+ end
206
+
207
+ def path
208
+ @file if @data.nil?
209
+ end
210
+
211
+ def style
212
+ case @output_style&.to_sym
213
+ when :expanded
214
+ EmbeddedProtocol::OutputStyle::EXPANDED
215
+ when :compressed
216
+ EmbeddedProtocol::OutputStyle::COMPRESSED
217
+ else
218
+ raise ArgumentError, 'output_style must be one of :expanded, :compressed'
219
+ end
220
+ end
221
+
222
+ def source_map
223
+ @source_map.is_a?(String) || (@source_map == true && !@out_file.nil?)
224
+ end
225
+
226
+ attr_reader :global_functions
227
+
228
+ # Order
229
+ # 1. Loading a file relative to the file in which the @use or @import appeared.
230
+ # 2. Each custom importer.
231
+ # 3. Loading a file relative to the current working directory.
232
+ # 4. Each load path in includePaths
233
+ # 5. Each load path specified in the SASS_PATH environment variable, which should
234
+ # be semicolon-separated on Windows and colon-separated elsewhere.
235
+ def importers
236
+ custom_importers = @importer.map.with_index do |_, id|
237
+ EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
238
+ importer_id: id
239
+ )
240
+ end
241
+
242
+ include_path_importers = @include_paths
243
+ .concat(Embedded.include_paths)
244
+ .map do |include_path|
245
+ EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
246
+ path: File.absolute_path(include_path)
247
+ )
248
+ end
249
+
250
+ custom_importers.concat include_path_importers
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../platform'
4
+
5
+ module Sass
6
+ class Embedded
7
+ class Compiler
8
+ PATH = File.absolute_path(
9
+ "../../../../ext/sass/sass_embedded/dart-sass-embedded#{Platform::OS == 'windows' ? '.bat' : ''}", __dir__
10
+ )
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ class Compiler
6
+ REQUIREMENTS = '~> 1.0.0-beta.11'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'observer'
4
+ require 'open3'
5
+ require_relative '../embedded_protocol'
6
+ require_relative 'compiler/path'
7
+ require_relative 'error'
8
+
9
+ module Sass
10
+ class Embedded
11
+ # The {::Observable} {Compiler} for low level communication with
12
+ # `dart-sass-embedded` using protocol buffers via stdio. Received messages
13
+ # can be observed by an {Observer}.
14
+ class Compiler
15
+ include Observable
16
+
17
+ ONEOF_MESSAGE = EmbeddedProtocol::InboundMessage
18
+ .descriptor
19
+ .lookup_oneof('message')
20
+ .collect do |field_descriptor|
21
+ [field_descriptor.subtype, field_descriptor.name]
22
+ end.to_h
23
+
24
+ private_constant :ONEOF_MESSAGE
25
+
26
+ def initialize
27
+ @observerable_mutex = Mutex.new
28
+ @id = 0
29
+ @stdin_mutex = Mutex.new
30
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(PATH)
31
+
32
+ [@stdin, @stdout].each(&:binmode)
33
+
34
+ poll do
35
+ warn(@stderr.readline, uplevel: 1)
36
+ end
37
+ poll do
38
+ receive_proto read
39
+ end
40
+ end
41
+
42
+ def add_observer(*args)
43
+ @observerable_mutex.synchronize do
44
+ raise IOError, 'half-closed compiler' if half_closed?
45
+
46
+ super(*args)
47
+
48
+ id = @id
49
+ @id = @id.next
50
+ id
51
+ end
52
+ end
53
+
54
+ def delete_observer(*args)
55
+ @observerable_mutex.synchronize do
56
+ super(*args)
57
+
58
+ close if half_closed? && count_observers.zero?
59
+ end
60
+ end
61
+
62
+ def send_message(message)
63
+ write EmbeddedProtocol::InboundMessage.new(
64
+ ONEOF_MESSAGE[message.class.descriptor] => message
65
+ ).to_proto
66
+ end
67
+
68
+ def close
69
+ delete_observers
70
+
71
+ @stdin_mutex.synchronize do
72
+ @stdin.close unless @stdin.closed?
73
+ @stdout.close unless @stdout.closed?
74
+ @stderr.close unless @stderr.closed?
75
+ end
76
+
77
+ @wait_thread.value
78
+ end
79
+
80
+ def closed?
81
+ @stdin_mutex.synchronize do
82
+ @stdin.closed?
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def half_closed?
89
+ @id == EmbeddedProtocol::PROTOCOL_ERROR_ID
90
+ end
91
+
92
+ def poll
93
+ Thread.new do
94
+ loop do
95
+ yield
96
+ rescue StandardError => e
97
+ notify_observers(e, nil)
98
+ close
99
+ break
100
+ end
101
+ end
102
+ end
103
+
104
+ def notify_observers(*args)
105
+ @observerable_mutex.synchronize do
106
+ changed
107
+ super(*args)
108
+ end
109
+ end
110
+
111
+ def receive_proto(proto)
112
+ payload = EmbeddedProtocol::OutboundMessage.decode(proto)
113
+ message = payload[payload.message.to_s]
114
+ case message
115
+ when EmbeddedProtocol::ProtocolError
116
+ raise ProtocolError, message.message
117
+ else
118
+ notify_observers(nil, message)
119
+ end
120
+ end
121
+
122
+ def read
123
+ length = read_varint(@stdout)
124
+ @stdout.read(length)
125
+ end
126
+
127
+ def write(payload)
128
+ @stdin_mutex.synchronize do
129
+ write_varint(@stdin, payload.length)
130
+ @stdin.write payload
131
+ end
132
+ end
133
+
134
+ def read_varint(readable)
135
+ value = bits = 0
136
+ loop do
137
+ byte = readable.readbyte
138
+ value |= (byte & 0x7f) << bits
139
+ bits += 7
140
+ break if byte < 0x80
141
+ end
142
+ value
143
+ end
144
+
145
+ def write_varint(writeable, value)
146
+ bytes = []
147
+ until value < 0x80
148
+ bytes << (0x80 | (value & 0x7f))
149
+ value >>= 7
150
+ end
151
+ bytes << value
152
+ writeable.write bytes.pack('C*')
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ class Error < StandardError; end
6
+
7
+ class ProtocolError < Error; end
8
+
9
+ # The {Error} raised by {Embedded#render}.
10
+ class RenderError < Error
11
+ attr_reader :formatted, :file, :line, :column, :status
12
+
13
+ def initialize(message, formatted, file, line, column, status)
14
+ super(message)
15
+ @formatted = formatted
16
+ @file = file
17
+ @line = line
18
+ @column = column
19
+ @status = status
20
+ end
21
+
22
+ def backtrace
23
+ return nil if super.nil?
24
+
25
+ ["#{@file}:#{@line}:#{@column}"] + super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ # The {Observer} module for communicating with {Compiler}.
6
+ module Observer
7
+ def initialize(channel)
8
+ @mutex = Mutex.new
9
+ @condition_variable = ConditionVariable.new
10
+ @error = nil
11
+ @message = nil
12
+
13
+ @subscription = channel.subscribe(self)
14
+ end
15
+
16
+ def receive_message
17
+ @mutex.synchronize do
18
+ @condition_variable.wait(@mutex) if @error.nil? && @message.nil?
19
+ end
20
+
21
+ raise @error unless @error.nil?
22
+
23
+ @message
24
+ end
25
+
26
+ def update(error, message)
27
+ @subscription.unsubscribe
28
+ @mutex.synchronize do
29
+ @error = error
30
+ @message = message
31
+ @condition_variable.broadcast
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def id
38
+ @subscription.id
39
+ end
40
+
41
+ def send_message(*args)
42
+ @subscription.send_message(*args)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ module Platform
6
+ OS = case RbConfig::CONFIG['host_os'].downcase
7
+ when /linux/
8
+ 'linux'
9
+ when /darwin/
10
+ 'darwin'
11
+ when /freebsd/
12
+ 'freebsd'
13
+ when /netbsd/
14
+ 'netbsd'
15
+ when /openbsd/
16
+ 'openbsd'
17
+ when /dragonfly/
18
+ 'dragonflybsd'
19
+ when /sunos|solaris/
20
+ 'solaris'
21
+ when /mingw|mswin/
22
+ 'windows'
23
+ else
24
+ RbConfig::CONFIG['host_os'].downcase
25
+ end
26
+
27
+ OSVERSION = RbConfig::CONFIG['host_os'].gsub(/[^\d]/, '').to_i
28
+
29
+ CPU = RbConfig::CONFIG['host_cpu']
30
+
31
+ ARCH = case CPU.downcase
32
+ when /amd64|x86_64|x64/
33
+ 'x86_64'
34
+ when /i\d86|x86|i86pc/
35
+ 'i386'
36
+ when /ppc64|powerpc64/
37
+ 'powerpc64'
38
+ when /ppc|powerpc/
39
+ 'powerpc'
40
+ when /sparcv9|sparc64/
41
+ 'sparcv9'
42
+ when /arm64|aarch64/ # MacOS calls it "arm64", other operating systems "aarch64"
43
+ 'aarch64'
44
+ when /^arm/
45
+ if OS == 'darwin' # Ruby before 3.0 reports "arm" instead of "arm64" as host_cpu on darwin
46
+ 'aarch64'
47
+ else
48
+ 'arm'
49
+ end
50
+ else
51
+ RbConfig::CONFIG['host_cpu']
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'struct'
4
+
5
+ module Sass
6
+ class Embedded
7
+ # The {RenderResult} of {Embedded#render}.
8
+ class RenderResult
9
+ include Struct
10
+
11
+ attr_reader :css, :map, :stats
12
+
13
+ def initialize(css, map, stats)
14
+ @css = css
15
+ @map = map
16
+ @stats = stats
17
+ end
18
+ end
19
+
20
+ # The {RenderResultStats} of {Embedded#render}.
21
+ class RenderResultStats
22
+ include Struct
23
+
24
+ attr_reader :entry, :start, :end, :duration
25
+
26
+ def initialize(entry, start, finish, duration)
27
+ @entry = entry
28
+ @start = start
29
+ @end = finish
30
+ @duration = duration
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ class Embedded
5
+ # The {Struct} module.
6
+ module Struct
7
+ def [](key)
8
+ instance_variable_get("@#{key}".to_sym)
9
+ end
10
+
11
+ def to_h
12
+ instance_variables.map do |variable|
13
+ [variable[1..].to_sym, instance_variable_get(variable)]
14
+ end.to_h
15
+ end
16
+
17
+ def to_s
18
+ to_h.to_s
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'uri'
5
+
6
+ module Sass
7
+ class Embedded
8
+ # The {Util} module.
9
+ module Util
10
+ URI_PARSER = URI::Parser.new({ RESERVED: ';/?:@&=+$,' })
11
+
12
+ private_constant :URI_PARSER
13
+
14
+ module_function
15
+
16
+ def file_uri_from_path(path)
17
+ "file://#{Platform::OS == 'windows' ? File::SEPARATOR : ''}#{URI_PARSER.escape(path)}"
18
+ end
19
+
20
+ def path_from_file_uri(file_uri)
21
+ URI_PARSER.unescape(file_uri[(Platform::OS == 'windows' ? 8 : 7)..])
22
+ end
23
+
24
+ def relative_path(from, to)
25
+ Pathname.new(File.absolute_path(to)).relative_path_from(Pathname.new(File.absolute_path(from))).to_s
26
+ end
27
+
28
+ def now
29
+ (Time.now.to_f * 1000).to_i
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- VERSION = '0.7.28'
4
+ class Embedded
5
+ VERSION = '0.9.0'
6
+ end
5
7
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../embedded_protocol'
4
+ require_relative 'observer'
5
+
6
+ module Sass
7
+ class Embedded
8
+ # The {Observer} for {Embedded#info}.
9
+ class VersionContext
10
+ include Observer
11
+
12
+ def initialize(channel)
13
+ super(channel)
14
+
15
+ send_message EmbeddedProtocol::InboundMessage::VersionRequest.new(id: id)
16
+ end
17
+
18
+ def update(error, message)
19
+ raise error unless error.nil?
20
+
21
+ case message
22
+ when EmbeddedProtocol::OutboundMessage::VersionResponse
23
+ return unless message.id == id
24
+
25
+ Thread.new do
26
+ super(nil, message)
27
+ end
28
+ end
29
+ rescue StandardError => e
30
+ Thread.new do
31
+ super(e, nil)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end