sass-embedded 0.8.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.12'
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.8.0'
4
+ class Embedded
5
+ VERSION = '0.9.1'
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