sass-embedded 0.4.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9afb73da6288ecf0360685cc9ddb4c072eb8840da50acaa8a52b67f09998e6c
4
- data.tar.gz: 1a20d885e2e9f55ffd861f9428499f4c3bdea64a4aea81e44f356af8be69eccc
3
+ metadata.gz: b23c7315185f78d02c9776c433a60a3ea62ce88b80a877ec17c66d4565416b5e
4
+ data.tar.gz: ce45e36c77e6aa2d04e211e5a50d265b1095c33d3a695793b68dc181b9fcf71f
5
5
  SHA512:
6
- metadata.gz: 679cfc5b502de2fdc5b33c8117b7e1d68f94acd62c7afbe04ebb17f9bceacdc677e8b35197cf239f92fed4e3d523468d71798d899c360d8402be127bdb50ac48
7
- data.tar.gz: e1d698b3980914299ee2ce570bd1f109a875859c147f8930366df8e2dabcae94f0e0795339f6614f3f7828e365c191ba24554e30869e4bd106f565f1cbbb9af2
6
+ metadata.gz: 699daac722babfcca657ae536c65f31c7882273cec287d33c98bec4dbbef9c1ce38242fefaad89592de563495687ed151428bb1c9e331e7e85d4ee0eb023f1b3
7
+ data.tar.gz: b2cd65ecccfd4f07f8f280a493fe7484e819cce8210ae12fffd5ee954529a3fa3562d9a8f9a21ee2783ab98e36307f6421aedcdeea63ca832abf31ea7cd8d7b9
data/.rubocop.yml CHANGED
@@ -7,8 +7,5 @@ AllCops:
7
7
 
8
8
  NewCops: enable
9
9
 
10
- Layout/LineLength:
11
- Enabled: false
12
-
13
10
  Metrics:
14
11
  Enabled: false
data/README.md CHANGED
@@ -6,6 +6,12 @@ This is a Ruby library that implements the host side of the [Embedded Sass proto
6
6
 
7
7
  It exposes a Ruby API for Sass that's backed by a native [Dart Sass](https://sass-lang.com/dart-sass) executable.
8
8
 
9
+ ## Install
10
+
11
+ ``` sh
12
+ gem install sass-embedded
13
+ ```
14
+
9
15
  ## Usage
10
16
 
11
17
  ``` ruby
@@ -14,6 +20,26 @@ require "sass"
14
20
  Sass.render(file: "style.scss")
15
21
  ```
16
22
 
23
+ ## Options
24
+
25
+ `Sass.render()` support the following options:
26
+
27
+ - [`data`](https://sass-lang.com/documentation/js-api#data)
28
+ - [`file`](https://sass-lang.com/documentation/js-api#file)
29
+ - [`indented_syntax`](https://sass-lang.com/documentation/js-api#indentedsyntax)
30
+ - [`include_paths`](https://sass-lang.com/documentation/js-api#includepaths)
31
+ - [`output_style`](https://sass-lang.com/documentation/js-api#outputstyle)
32
+ - [`indent_type`](https://sass-lang.com/documentation/js-api#indenttype)
33
+ - [`indent_width`](https://sass-lang.com/documentation/js-api#indentwidth)
34
+ - [`linefeed`](https://sass-lang.com/documentation/js-api#linefeed)
35
+ - [`source_map`](https://sass-lang.com/documentation/js-api#sourcemap)
36
+ - [`out_file`](https://sass-lang.com/documentation/js-api#outfile)
37
+ - [`omit_source_map_url`](https://sass-lang.com/documentation/js-api#omitsourcemapurl)
38
+ - [`source_map_embed`](https://sass-lang.com/documentation/js-api#sourcemapembed)
39
+ - [`source_map_root`](https://sass-lang.com/documentation/js-api#sourcemaproot)
40
+ - [`functions`](https://sass-lang.com/documentation/js-api#functions)
41
+ - [`importer`](https://sass-lang.com/documentation/js-api#importer)
42
+
17
43
  ---
18
44
 
19
45
  Disclaimer: this is not an official Google product.
data/ext/extconf.rb CHANGED
@@ -8,12 +8,24 @@ require 'fileutils'
8
8
  require_relative '../lib/sass/platform'
9
9
 
10
10
  module Sass
11
- # Install dependencies for sass-embedded during gem install
11
+ # The dependency downloader. This downloads all the dependencies during gem
12
+ # installation. The companion Makefile then unpacks all downloaded
13
+ # dependencies. By default it downloads the latest release of each
14
+ # dependency from GitHub releases.
15
+ #
16
+ # It is possible to specify an alternative source or version of each
17
+ # dependency. Local sources can be used for offline installation.
18
+ #
19
+ # @example
20
+ # gem install sass-embedded -- \
21
+ # --with-protoc=file:///path/to/protoc-*.zip \
22
+ # --with-sass-embedded=file:///path/to/sass_embedded-*.(tar.gz|zip) \
23
+ # --with-sass-embedded-protocol=file:///path/to/embedded_sass.proto
12
24
  class Extconf
13
25
  def initialize
26
+ get_with_config('protoc', true) { latest_protoc }
14
27
  get_with_config('sass-embedded', true) { latest_sass_embedded }
15
28
  get_with_config('sass-embedded-protocol', true) { latest_sass_embedded_protocol }
16
- get_with_config('protoc', true) { latest_protoc }
17
29
  end
18
30
 
19
31
  private
data/lib/sass.rb CHANGED
@@ -1,50 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The Sass module
3
+ # The Sass module. This communicates with Embedded Dart Sass using
4
+ # the Embedded Sass protocol.
4
5
  module Sass
5
- # The global include_paths for Sass files. This is meant for plugins and
6
- # libraries to register the paths to their Sass stylesheets to that they may
7
- # be `@imported`. This include path is used by every instance of
8
- # {Sass::Embedded}. They are lower-precedence than any include paths passed
9
- # in via the `:include_paths` option.
10
- #
11
- # If the `SASS_PATH` environment variable is set,
12
- # the initial value of `include_paths` will be initialized based on that.
13
- # The variable should be a colon-separated list of path names
14
- # (semicolon-separated on Windows).
15
- #
16
- # @example
17
- # Sass.include_paths << File.dirname(__FILE__) + '/sass'
18
- # @return [Array<String, Pathname>]
19
- def self.include_paths
20
- @include_paths ||= if ENV['SASS_PATH']
21
- ENV['SASS_PATH'].split(File::PATH_SEPARATOR)
22
- else
23
- []
24
- end
25
- end
6
+ class << self
7
+ # The global {.include_paths} for Sass files. This is meant for plugins and
8
+ # libraries to register the paths to their Sass stylesheets to that they may
9
+ # be included via `@import` or `@use`. This include path is used by every
10
+ # instance of {Sass::Embedded}. They are lower-precedence than any include
11
+ # paths passed in via the `include_paths` option.
12
+ #
13
+ # If the `SASS_PATH` environment variable is set,
14
+ # the initial value of `include_paths` will be initialized based on that.
15
+ # The variable should be a colon-separated list of path names
16
+ # (semicolon-separated on Windows).
17
+ #
18
+ # @example
19
+ # Sass.include_paths << File.dirname(__FILE__) + '/sass'
20
+ # @return [Array]
21
+ def include_paths
22
+ @include_paths ||= if ENV['SASS_PATH']
23
+ ENV['SASS_PATH'].split(File::PATH_SEPARATOR)
24
+ else
25
+ []
26
+ end
27
+ end
28
+
29
+ # The global {.info} method. This instantiates a global {Embedded} instance
30
+ # and calls {Embedded#info}.
31
+ #
32
+ # @raise [ProtocolError]
33
+ def info
34
+ embedded.info
35
+ end
36
+
37
+ # The global {.render} method. This instantiates a global {Embedded} instance
38
+ # and calls {Embedded#render}.
39
+ #
40
+ # See {file:README.md#options} for supported options.
41
+ #
42
+ # @example
43
+ # Sass.render(data: 'h1 { font-size: 40px; }')
44
+ # @example
45
+ # Sass.render(file: 'style.css')
46
+ # @return [Result]
47
+ # @raise [ProtocolError]
48
+ # @raise [RenderError]
49
+ def render(**kwargs)
50
+ embedded.render(**kwargs)
51
+ end
52
+
53
+ private
54
+
55
+ def embedded
56
+ return @embedded if defined?(@embedded) && !@embedded.closed?
26
57
 
27
- # The global render methods. This method automatically instantiates a
28
- # global {Sass::Embedded} instance when invoked the first time and call
29
- # `:render` method on the instance thereafter. The global {Sass::Embedded}
30
- # is automatically closed via {Kernel.at_exit}.
31
- # @example
32
- # Sass.render(options)
33
- # @return [Hash]
34
- def self.render(**kwargs)
35
- unless defined? @embedded
36
58
  @embedded = Sass::Embedded.new
37
- at_exit do
38
- @embedded.close
39
- end
40
59
  end
41
- @embedded.render(**kwargs)
42
60
  end
43
61
  end
44
62
 
45
- require_relative 'sass/version'
46
- require_relative 'sass/error'
47
63
  require_relative 'sass/platform'
48
64
  require_relative 'sass/util'
65
+ require_relative 'sass/struct'
66
+ require_relative 'sass/result'
67
+ require_relative 'sass/error'
49
68
  require_relative 'sass/transport'
69
+ require_relative 'sass/observer'
70
+ require_relative 'sass/version'
71
+ require_relative 'sass/render'
50
72
  require_relative 'sass/embedded'
data/lib/sass/embedded.rb CHANGED
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require 'base64'
4
+ require 'json'
5
5
 
6
6
  module Sass
7
- # The interface for using dart-sass-embedded
7
+ # The {Embedded} host for using dart-sass-embedded. Each instance creates
8
+ # its own {Transport}.
9
+ #
10
+ # @example
11
+ # embedded = Sass::Embedded.new
12
+ # result = embedded.render(data: 'h1 { font-size: 40px; }')
13
+ # result = embedded.render(file: 'style.css')
14
+ # embedded.close
8
15
  class Embedded
9
16
  def initialize
10
17
  @transport = Transport.new
@@ -12,22 +19,28 @@ module Sass
12
19
  @id = 0
13
20
  end
14
21
 
22
+ # The {Embedded#info} method.
23
+ #
24
+ # @raise [ProtocolError]
15
25
  def info
16
- @transport.send EmbeddedProtocol::InboundMessage::VersionRequest.new(
17
- id: next_id
18
- )
26
+ @info ||= Version.new(@transport, next_id).fetch
19
27
  end
20
28
 
29
+ # The {Embedded#render} method.
30
+ #
31
+ # See {file:README.md#options} for supported options.
32
+ #
33
+ # @return [Result]
34
+ # @raise [ProtocolError]
35
+ # @raise [RenderError]
21
36
  def render(data: nil,
22
37
  file: nil,
23
38
  indented_syntax: false,
24
39
  include_paths: [],
25
40
  output_style: :expanded,
26
- # precision: 5,
27
41
  indent_type: :space,
28
42
  indent_width: 2,
29
43
  linefeed: :lf,
30
- # source_comments: false,
31
44
  source_map: false,
32
45
  out_file: nil,
33
46
  omit_source_map_url: false,
@@ -42,61 +55,41 @@ module Sass
42
55
  indent_width = parse_indent_width(indent_width)
43
56
  linefeed = parse_linefeed(linefeed)
44
57
 
45
- compilation_id = next_id
46
-
47
- renderer = Renderer.new(
48
- data: data,
49
- file: file,
50
- indented_syntax: indented_syntax,
51
- include_paths: include_paths,
52
- output_style: output_style,
53
- source_map: source_map,
54
- out_file: out_file,
55
- functions: functions,
56
- importer: importer
57
- )
58
-
59
- response = @transport.send renderer.compile_request(compilation_id), compilation_id
60
-
61
- loop do
62
- case response
63
- when EmbeddedProtocol::OutboundMessage::CompileResponse
64
- break
65
- when EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
66
- response = @transport.send renderer.canonicalize_response(response), compilation_id
67
- when EmbeddedProtocol::OutboundMessage::ImportRequest
68
- response = @transport.send renderer.import_response(response), compilation_id
69
- when EmbeddedProtocol::OutboundMessage::FunctionCallRequest
70
- response = @transport.send renderer.function_call_response(response), compilation_id
71
- when EmbeddedProtocol::ProtocolError
72
- raise ProtocolError, response.message
73
- else
74
- raise ProtocolError, "Unexpected packet received: #{response}"
75
- end
76
- end
77
-
78
- if response.failure
58
+ message = Render.new(@transport, next_id,
59
+ data: data,
60
+ file: file,
61
+ indented_syntax: indented_syntax,
62
+ include_paths: include_paths,
63
+ output_style: output_style,
64
+ source_map: source_map,
65
+ out_file: out_file,
66
+ functions: functions,
67
+ importer: importer).fetch
68
+
69
+ if message.failure
79
70
  raise RenderError.new(
80
- response.failure.message,
81
- response.failure.formatted,
82
- if response.failure.span&.url == ''
71
+ message.failure.message,
72
+ message.failure.formatted,
73
+ if message.failure.span.nil?
74
+ nil
75
+ elsif message.failure.span.url == ''
83
76
  'stdin'
84
77
  else
85
- Util.path(response.failure.span.url)
78
+ Util.path(message.failure.span.url)
86
79
  end,
87
- response.failure.span ? response.failure.span.start.line + 1 : nil,
88
- response.failure.span ? response.failure.span.start.column + 1 : nil,
80
+ message.failure.span ? message.failure.span.start.line + 1 : nil,
81
+ message.failure.span ? message.failure.span.start.column + 1 : nil,
89
82
  1
90
83
  )
91
84
  end
92
85
 
93
- map, source_map = post_process_map(map: response.success.source_map,
86
+ map, source_map = post_process_map(map: message.success.source_map,
94
87
  file: file,
95
88
  out_file: out_file,
96
89
  source_map: source_map,
97
90
  source_map_root: source_map_root)
98
91
 
99
- css = post_process_css(css: response.success.css,
92
+ css = post_process_css(css: message.success.css,
100
93
  indent_type: indent_type,
101
94
  indent_width: indent_width,
102
95
  linefeed: linefeed,
@@ -108,21 +101,17 @@ module Sass
108
101
 
109
102
  finish = Util.now
110
103
 
111
- {
112
- css: css,
113
- map: map,
114
- stats: {
115
- entry: file.nil? ? 'data' : file,
116
- start: start,
117
- end: finish,
118
- duration: finish - start
119
- }
120
- }
104
+ stats = Result::Stats.new(file.nil? ? 'data' : file, start, finish, finish - start)
105
+
106
+ Result.new(css, map, stats)
121
107
  end
122
108
 
123
109
  def close
124
110
  @transport.close
125
- nil
111
+ end
112
+
113
+ def closed?
114
+ @transport.closed?
126
115
  end
127
116
 
128
117
  private
@@ -205,7 +194,7 @@ module Sass
205
194
  when :tab
206
195
  "\t"
207
196
  else
208
- raise ArgumentError, 'indent_type must be :space or :tab'
197
+ raise ArgumentError, 'indent_type must be one of :space, :tab'
209
198
  end
210
199
  end
211
200
 
@@ -238,199 +227,5 @@ module Sass
238
227
  @id
239
228
  end
240
229
  end
241
-
242
- # Helper class that maintains render state
243
- class Renderer
244
- def initialize(data:,
245
- file:,
246
- indented_syntax:,
247
- include_paths:,
248
- output_style:,
249
- source_map:,
250
- out_file:,
251
- functions:,
252
- importer:)
253
- raise ArgumentError, 'either data or file must be set' if file.nil? && data.nil?
254
-
255
- @data = data
256
- @file = file
257
- @indented_syntax = indented_syntax
258
- @include_paths = include_paths
259
- @output_style = output_style
260
- @source_map = source_map
261
- @out_file = out_file
262
- @global_functions = functions.keys
263
- @functions = functions.transform_keys do |key|
264
- key.to_s.split('(')[0].chomp
265
- end
266
- @importer = importer
267
- @import_responses = {}
268
- end
269
-
270
- def compile_request(id)
271
- EmbeddedProtocol::InboundMessage::CompileRequest.new(
272
- id: id,
273
- string: string,
274
- path: path,
275
- style: style,
276
- source_map: source_map,
277
- importers: importers,
278
- global_functions: global_functions,
279
- alert_color: $stderr.tty?,
280
- alert_ascii: Platform::OS == 'windows'
281
- )
282
- end
283
-
284
- def canonicalize_response(canonicalize_request)
285
- url = Util.file_uri(File.absolute_path(canonicalize_request.url, (@file.nil? ? 'stdin' : @file)))
286
-
287
- begin
288
- result = @importer[canonicalize_request.importer_id].call canonicalize_request.url, @file
289
- raise result if result.is_a? StandardError
290
- rescue StandardError => e
291
- return EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
292
- id: canonicalize_request.id,
293
- error: e.message
294
- )
295
- end
296
-
297
- if result&.key? :contents
298
- @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
299
- id: canonicalize_request.id,
300
- success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
301
- contents: result[:contents],
302
- syntax: EmbeddedProtocol::Syntax::SCSS,
303
- source_map_url: nil
304
- )
305
- )
306
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
307
- id: canonicalize_request.id,
308
- url: url
309
- )
310
- elsif result&.key? :file
311
- canonicalized_url = Util.file_uri(result[:file])
312
-
313
- # TODO: FileImportRequest is not supported yet.
314
- # Workaround by reading contents and return it when server asks
315
- @import_responses[canonicalized_url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
316
- id: canonicalize_request.id,
317
- success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
318
- contents: File.read(result[:file]),
319
- syntax: EmbeddedProtocol::Syntax::SCSS,
320
- source_map_url: nil
321
- )
322
- )
323
-
324
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
325
- id: canonicalize_request.id,
326
- url: canonicalized_url
327
- )
328
- else
329
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
330
- id: canonicalize_request.id
331
- )
332
- end
333
- end
334
-
335
- def import_response(import_request)
336
- url = import_request.url
337
-
338
- if @import_responses.key? url
339
- @import_responses[url].id = import_request.id
340
- else
341
- @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
342
- id: import_request.id,
343
- error: "Failed to import: #{url}"
344
- )
345
- end
346
-
347
- @import_responses[url]
348
- end
349
-
350
- def function_call_response(function_call_request)
351
- EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
352
- id: function_call_request.id,
353
- success: @functions[function_call_request.name].call(*function_call_request.arguments)
354
- )
355
- rescue StandardError => e
356
- EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
357
- id: function_call_request.id,
358
- error: e.message
359
- )
360
- end
361
-
362
- private
363
-
364
- def syntax
365
- if @indented_syntax == true
366
- EmbeddedProtocol::Syntax::INDENTED
367
- else
368
- EmbeddedProtocol::Syntax::SCSS
369
- end
370
- end
371
-
372
- def url
373
- return if @file.nil?
374
-
375
- Util.file_uri @file
376
- end
377
-
378
- def string
379
- return if @data.nil?
380
-
381
- EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
382
- source: @data,
383
- url: url,
384
- syntax: syntax
385
- )
386
- end
387
-
388
- def path
389
- @file if @data.nil?
390
- end
391
-
392
- def style
393
- case @output_style&.to_sym
394
- when :expanded
395
- EmbeddedProtocol::OutputStyle::EXPANDED
396
- when :compressed
397
- EmbeddedProtocol::OutputStyle::COMPRESSED
398
- when :nested, :compact
399
- raise ArgumentError, "#{@output_style} is not a supported output_style"
400
- else
401
- raise ArgumentError, "#{@output_style} is not a valid utput_style"
402
- end
403
- end
404
-
405
- def source_map
406
- @source_map.is_a?(String) || (@source_map == true && !@out_file.nil?)
407
- end
408
-
409
- attr_reader :global_functions
410
-
411
- # Order
412
- # 1. Loading a file relative to the file in which the @use or @import appeared.
413
- # 2. Each custom importer.
414
- # 3. Loading a file relative to the current working directory.
415
- # 4. Each load path in includePaths
416
- # 5. Each load path specified in the SASS_PATH environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.
417
- def importers
418
- custom_importers = @importer.map.with_index do |_, id|
419
- EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
420
- importer_id: id
421
- )
422
- end
423
-
424
- include_path_importers = @include_paths
425
- .concat(Sass.include_paths)
426
- .map do |include_path|
427
- EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
428
- path: File.absolute_path(include_path)
429
- )
430
- end
431
-
432
- custom_importers.concat include_path_importers
433
- end
434
- end
435
230
  end
436
231
  end
data/lib/sass/error.rb CHANGED
@@ -1,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- class SassError < StandardError; end
4
+ class Error < StandardError; end
5
5
 
6
- class ProtocolError < SassError; end
6
+ class ProtocolError < Error; end
7
7
 
8
- # The error returned by {Sass.render}
9
- class RenderError < SassError
10
- attr_accessor :formatted, :file, :line, :column, :status
8
+ # The {Error} raised by {Embedded#render}.
9
+ class RenderError < Error
10
+ include Struct
11
+
12
+ attr_reader :formatted, :file, :line, :column, :status
11
13
 
12
14
  def initialize(message, formatted, file, line, column, status)
15
+ super(message)
13
16
  @formatted = formatted
14
17
  @file = file
15
18
  @line = line
16
19
  @column = column
17
20
  @status = status
18
- super(message)
19
21
  end
20
22
 
21
23
  def backtrace
data/lib/sass/info.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Observer} for {Embedded#info}.
5
+ class Version < Observer
6
+ def initialize(transport, id)
7
+ super(transport, id)
8
+ @transport.send EmbeddedProtocol::InboundMessage::VersionRequest.new(id: @id)
9
+ end
10
+
11
+ def update(error, message)
12
+ raise error unless error.nil?
13
+
14
+ case message
15
+ when EmbeddedProtocol::ProtocolError
16
+ raise ProtocolError, message.message
17
+ when EmbeddedProtocol::OutboundMessage::VersionResponse
18
+ return unless message.id == @id
19
+
20
+ Thread.new do
21
+ super(nil, message)
22
+ end
23
+ end
24
+ rescue StandardError => e
25
+ Thread.new do
26
+ super(e, nil)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Observer} for receiving messages from {Transport}.
5
+ class Observer
6
+ def initialize(transport, id)
7
+ raise NotImplementedError if instance_of? Observer
8
+
9
+ @transport = transport
10
+ @id = id
11
+ @mutex = Mutex.new
12
+ @condition_variable = ConditionVariable.new
13
+ @error = nil
14
+ @message = nil
15
+ @transport.add_observer self
16
+ end
17
+
18
+ def fetch
19
+ @mutex.synchronize do
20
+ @condition_variable.wait(@mutex) if @error.nil? && @message.nil?
21
+ end
22
+
23
+ raise @error unless @error.nil?
24
+
25
+ @message
26
+ end
27
+
28
+ def update(error, message)
29
+ @transport.delete_observer self
30
+ @mutex.synchronize do
31
+ @error = error
32
+ @message = message
33
+ @condition_variable.broadcast
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Observer} for {Embedded#render}.
5
+ class Render < Observer
6
+ def initialize(transport, id,
7
+ data:,
8
+ file:,
9
+ indented_syntax:,
10
+ include_paths:,
11
+ output_style:,
12
+ source_map:,
13
+ out_file:,
14
+ functions:,
15
+ importer:)
16
+ raise ArgumentError, 'either data or file must be set' if file.nil? && data.nil?
17
+
18
+ super(transport, id)
19
+
20
+ @data = data
21
+ @file = file
22
+ @indented_syntax = indented_syntax
23
+ @include_paths = include_paths
24
+ @output_style = output_style
25
+ @source_map = source_map
26
+ @out_file = out_file
27
+ @global_functions = functions.keys
28
+ @functions = functions.transform_keys do |key|
29
+ key.to_s.split('(')[0].chomp
30
+ end
31
+ @importer = importer
32
+ @import_responses = {}
33
+
34
+ @transport.send compile_request
35
+ end
36
+
37
+ def update(error, message)
38
+ raise error unless error.nil?
39
+
40
+ case message
41
+ when EmbeddedProtocol::ProtocolError
42
+ raise ProtocolError, message.message
43
+ when EmbeddedProtocol::OutboundMessage::CompileResponse
44
+ return unless message.id == @id
45
+
46
+ Thread.new do
47
+ super(nil, message)
48
+ end
49
+ when EmbeddedProtocol::OutboundMessage::LogEvent
50
+ # not implemented yet
51
+ when EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
52
+ return unless message['compilation_id'] == @id
53
+
54
+ Thread.new do
55
+ @transport.send canonicalize_response message
56
+ end
57
+ when EmbeddedProtocol::OutboundMessage::ImportRequest
58
+ return unless message['compilation_id'] == @id
59
+
60
+ Thread.new do
61
+ @transport.send import_response message
62
+ end
63
+ when EmbeddedProtocol::OutboundMessage::FileImportRequest
64
+ raise NotImplementedError, 'FileImportRequest is not implemented'
65
+ when EmbeddedProtocol::OutboundMessage::FunctionCallRequest
66
+ return unless message['compilation_id'] == @id
67
+
68
+ Thread.new do
69
+ @transport.send function_call_response message
70
+ end
71
+ end
72
+ rescue StandardError => e
73
+ Thread.new do
74
+ super(e, nil)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def compile_request
81
+ EmbeddedProtocol::InboundMessage::CompileRequest.new(
82
+ id: @id,
83
+ string: string,
84
+ path: path,
85
+ style: style,
86
+ source_map: source_map,
87
+ importers: importers,
88
+ global_functions: global_functions,
89
+ alert_color: $stderr.tty?,
90
+ alert_ascii: Platform::OS == 'windows'
91
+ )
92
+ end
93
+
94
+ def canonicalize_response(canonicalize_request)
95
+ url = Util.file_uri(File.absolute_path(canonicalize_request.url, (@file.nil? ? 'stdin' : @file)))
96
+
97
+ begin
98
+ result = @importer[canonicalize_request.importer_id].call canonicalize_request.url, @file
99
+ raise result if result.is_a? StandardError
100
+ rescue StandardError => e
101
+ return EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
102
+ id: canonicalize_request.id,
103
+ error: e.message
104
+ )
105
+ end
106
+
107
+ if result&.key? :contents
108
+ @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
109
+ id: canonicalize_request.id,
110
+ success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
111
+ contents: result[:contents],
112
+ syntax: EmbeddedProtocol::Syntax::SCSS,
113
+ source_map_url: nil
114
+ )
115
+ )
116
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
117
+ id: canonicalize_request.id,
118
+ url: url
119
+ )
120
+ elsif result&.key? :file
121
+ canonicalized_url = Util.file_uri(result[:file])
122
+
123
+ # TODO: FileImportRequest is not supported yet.
124
+ # Workaround by reading contents and return it when server asks
125
+ @import_responses[canonicalized_url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
126
+ id: canonicalize_request.id,
127
+ success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
128
+ contents: File.read(result[:file]),
129
+ syntax: EmbeddedProtocol::Syntax::SCSS,
130
+ source_map_url: nil
131
+ )
132
+ )
133
+
134
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
135
+ id: canonicalize_request.id,
136
+ url: canonicalized_url
137
+ )
138
+ else
139
+ EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
140
+ id: canonicalize_request.id
141
+ )
142
+ end
143
+ end
144
+
145
+ def import_response(import_request)
146
+ url = import_request.url
147
+
148
+ if @import_responses.key? url
149
+ @import_responses[url].id = import_request.id
150
+ else
151
+ @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
152
+ id: import_request.id,
153
+ error: "Failed to import: #{url}"
154
+ )
155
+ end
156
+
157
+ @import_responses[url]
158
+ end
159
+
160
+ def function_call_response(function_call_request)
161
+ EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
162
+ id: function_call_request.id,
163
+ success: @functions[function_call_request.name].call(*function_call_request.arguments)
164
+ )
165
+ rescue StandardError => e
166
+ EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
167
+ id: function_call_request.id,
168
+ error: e.message
169
+ )
170
+ end
171
+
172
+ def syntax
173
+ if @indented_syntax == true
174
+ EmbeddedProtocol::Syntax::INDENTED
175
+ else
176
+ EmbeddedProtocol::Syntax::SCSS
177
+ end
178
+ end
179
+
180
+ def url
181
+ return if @file.nil?
182
+
183
+ Util.file_uri @file
184
+ end
185
+
186
+ def string
187
+ return if @data.nil?
188
+
189
+ EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
190
+ source: @data,
191
+ url: url,
192
+ syntax: syntax
193
+ )
194
+ end
195
+
196
+ def path
197
+ @file if @data.nil?
198
+ end
199
+
200
+ def style
201
+ case @output_style&.to_sym
202
+ when :expanded
203
+ EmbeddedProtocol::OutputStyle::EXPANDED
204
+ when :compressed
205
+ EmbeddedProtocol::OutputStyle::COMPRESSED
206
+ else
207
+ raise ArgumentError, 'output_style must be one of :expanded, :compressed'
208
+ end
209
+ end
210
+
211
+ def source_map
212
+ @source_map.is_a?(String) || (@source_map == true && !@out_file.nil?)
213
+ end
214
+
215
+ attr_reader :global_functions
216
+
217
+ # Order
218
+ # 1. Loading a file relative to the file in which the @use or @import appeared.
219
+ # 2. Each custom importer.
220
+ # 3. Loading a file relative to the current working directory.
221
+ # 4. Each load path in includePaths
222
+ # 5. Each load path specified in the SASS_PATH environment variable, which should
223
+ # be semicolon-separated on Windows and colon-separated elsewhere.
224
+ def importers
225
+ custom_importers = @importer.map.with_index do |_, id|
226
+ EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
227
+ importer_id: id
228
+ )
229
+ end
230
+
231
+ include_path_importers = @include_paths
232
+ .concat(Sass.include_paths)
233
+ .map do |include_path|
234
+ EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
235
+ path: File.absolute_path(include_path)
236
+ )
237
+ end
238
+
239
+ custom_importers.concat include_path_importers
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Result} of {Embedded#render}.
5
+ class Result
6
+ include Struct
7
+
8
+ attr_reader :css, :map, :stats
9
+
10
+ def initialize(css, map, stats)
11
+ @css = css
12
+ @map = map
13
+ @stats = stats
14
+ end
15
+
16
+ # The {Stats} of {Embedded#render}.
17
+ class Stats
18
+ include Struct
19
+
20
+ attr_reader :entry, :start, :end, :duration
21
+
22
+ def initialize(entry, start, finish, duration)
23
+ @entry = entry
24
+ @start = start
25
+ @end = finish
26
+ @duration = duration
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # The {Struct} module.
5
+ module Struct
6
+ def [](key)
7
+ instance_variable_get("@#{key}".to_sym)
8
+ end
9
+
10
+ def to_h
11
+ instance_variables.map do |variable|
12
+ [variable[1..].to_sym, instance_variable_get(variable)]
13
+ end.to_h
14
+ end
15
+
16
+ def to_s
17
+ to_h.to_s
18
+ end
19
+ end
20
+ end
@@ -5,9 +5,9 @@ require 'observer'
5
5
  require_relative '../../ext/embedded_sass_pb'
6
6
 
7
7
  module Sass
8
- # The interface for communicating with dart-sass-embedded.
9
- # It handles message serialization and deserialization as well as
10
- # tracking concurrent request and response
8
+ # The {Observable} {Transport} for low level communication with
9
+ # `dart-sass-embedded` using protocol buffers via stdio. Received messages
10
+ # can be observed by an {Observer}.
11
11
  class Transport
12
12
  include Observable
13
13
 
@@ -17,45 +17,31 @@ module Sass
17
17
 
18
18
  PROTOCOL_ERROR_ID = 4_294_967_295
19
19
 
20
+ ONEOF_MESSAGE = EmbeddedProtocol::InboundMessage
21
+ .descriptor
22
+ .lookup_oneof('message')
23
+ .collect do |field_descriptor|
24
+ [field_descriptor.subtype, field_descriptor.name]
25
+ end.to_h
26
+
20
27
  def initialize
21
- @stdin_semaphore = Mutex.new
22
- @observerable_semaphore = Mutex.new
28
+ @observerable_mutex = Mutex.new
29
+ @stdin_mutex = Mutex.new
23
30
  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(DART_SASS_EMBEDDED)
24
31
  pipe @stderr, $stderr
25
32
  receive
26
33
  end
27
34
 
28
- def send(req, id)
29
- mutex = Mutex.new
30
- resource = ConditionVariable.new
31
-
32
- req_kind = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
33
-
34
- message = EmbeddedProtocol::InboundMessage.new(req_kind => req)
35
-
36
- error = nil
37
- res = nil
38
-
39
- @observerable_semaphore.synchronize do
40
- MessageObserver.new self, id do |e, r|
41
- mutex.synchronize do
42
- error = e
43
- res = r
44
-
45
- resource.signal
46
- end
47
- end
48
- end
49
-
50
- mutex.synchronize do
51
- write message.to_proto
52
-
53
- resource.wait(mutex)
35
+ def add_observer(*args)
36
+ @observerable_mutex.synchronize do
37
+ super(*args)
54
38
  end
39
+ end
55
40
 
56
- raise error if error
57
-
58
- res
41
+ def send(message)
42
+ write EmbeddedProtocol::InboundMessage.new(
43
+ ONEOF_MESSAGE[message.class.descriptor] => message
44
+ ).to_proto
59
45
  end
60
46
 
61
47
  def close
@@ -66,6 +52,10 @@ module Sass
66
52
  nil
67
53
  end
68
54
 
55
+ def closed?
56
+ @stdin.closed?
57
+ end
58
+
69
59
  private
70
60
 
71
61
  def receive
@@ -78,10 +68,11 @@ module Sass
78
68
  bits += 7
79
69
  break if byte <= 0x7f
80
70
  end
81
- changed
82
71
  payload = @stdout.read length
83
- @observerable_semaphore.synchronize do
84
- notify_observers nil, EmbeddedProtocol::OutboundMessage.decode(payload)
72
+ message = EmbeddedProtocol::OutboundMessage.decode payload
73
+ @observerable_mutex.synchronize do
74
+ changed
75
+ notify_observers nil, message[message.message.to_s]
85
76
  end
86
77
  rescue Interrupt
87
78
  break
@@ -100,7 +91,7 @@ module Sass
100
91
  rescue Interrupt
101
92
  break
102
93
  rescue IOError => e
103
- @observerable_semaphore.synchronize do
94
+ @observerable_mutex.synchronize do
104
95
  notify_observers e, nil
105
96
  end
106
97
  close
@@ -110,7 +101,7 @@ module Sass
110
101
  end
111
102
 
112
103
  def write(payload)
113
- @stdin_semaphore.synchronize do
104
+ @stdin_mutex.synchronize do
114
105
  length = payload.length
115
106
  while length.positive?
116
107
  @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
@@ -119,32 +110,5 @@ module Sass
119
110
  @stdin.write payload
120
111
  end
121
112
  end
122
-
123
- # The observer used to listen on messages from stdout, check if id
124
- # matches the given request id, and yield back to the given block.
125
- class MessageObserver
126
- def initialize(obs, id, &block)
127
- @obs = obs
128
- @id = id
129
- @block = block
130
- @obs.add_observer self
131
- end
132
-
133
- def update(error, message)
134
- if error
135
- @obs.delete_observer self
136
- @block.call error, nil
137
- elsif message.error&.id == Transport::PROTOCOL_ERROR_ID
138
- @obs.delete_observer self
139
- @block.call ProtocolError.new(message.error.message), nil
140
- else
141
- res = message[message.message.to_s]
142
- if (res['compilation_id'] || res['id']) == @id
143
- @obs.delete_observer self
144
- @block.call error, res
145
- end
146
- end
147
- end
148
- end
149
113
  end
150
114
  end
data/lib/sass/util.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module Sass
4
- # Utilities functions
6
+ # The {Util} module.
5
7
  module Util
6
8
  module_function
7
9
 
data/lib/sass/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.1'
5
5
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.email = ['i@ntk.me']
12
12
  spec.summary = 'Use dart-sass with Ruby!'
13
13
  spec.description = 'Use dart-sass with Ruby!'
14
- spec.homepage = 'https://github.com/ntkme/embedded-host-ruby'
14
+ spec.homepage = 'https://github.com/ntkme/sass-embedded-host-ruby'
15
15
  spec.license = 'MIT'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0")
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
 
22
- spec.required_ruby_version = '>= 2.6'
22
+ spec.required_ruby_version = '>= 2.6.0'
23
23
 
24
24
  spec.require_paths = ['lib']
25
25
 
@@ -12,6 +12,8 @@ module Sass
12
12
  @embedded.close
13
13
  end
14
14
 
15
+ # rubocop:disable Layout/LineLength
16
+
15
17
  def render(sass)
16
18
  @embedded.render(data: sass,
17
19
  functions: {
@@ -163,6 +165,8 @@ module Sass
163
165
  })[:css]
164
166
  end
165
167
 
168
+ # rubocop:enable Layout/LineLength
169
+
166
170
  def test_functions_may_return_sass_string_type
167
171
  assert_sass <<-SCSS, <<-CSS
168
172
  div { url: url(sass_return_path("foo.svg")); }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sass-embedded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - なつき
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-27 00:00:00.000000000 Z
11
+ date: 2021-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf
@@ -157,7 +157,12 @@ files:
157
157
  - lib/sass.rb
158
158
  - lib/sass/embedded.rb
159
159
  - lib/sass/error.rb
160
+ - lib/sass/info.rb
161
+ - lib/sass/observer.rb
160
162
  - lib/sass/platform.rb
163
+ - lib/sass/render.rb
164
+ - lib/sass/result.rb
165
+ - lib/sass/struct.rb
161
166
  - lib/sass/transport.rb
162
167
  - lib/sass/util.rb
163
168
  - lib/sass/version.rb
@@ -173,7 +178,7 @@ files:
173
178
  - test/render_test.rb
174
179
  - test/source_maps_test.rb
175
180
  - test/test_helper.rb
176
- homepage: https://github.com/ntkme/embedded-host-ruby
181
+ homepage: https://github.com/ntkme/sass-embedded-host-ruby
177
182
  licenses:
178
183
  - MIT
179
184
  metadata: {}
@@ -185,7 +190,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
185
190
  requirements:
186
191
  - - ">="
187
192
  - !ruby/object:Gem::Version
188
- version: '2.6'
193
+ version: 2.6.0
189
194
  required_rubygems_version: !ruby/object:Gem::Requirement
190
195
  requirements:
191
196
  - - ">="