sass-embedded 0.4.1 → 0.6.2

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: 44a04b95b0d29cda95382a09cb00512ff6c2bb3f2c93b04f6171678856f648b7
4
- data.tar.gz: 944fefc7cc7507a20ebea9154c412bd99cc5cf9e2d698d604bcf0dd71e287aeb
3
+ metadata.gz: 4d8fe5d208436fad17c2eb0b4542de995dff90b8fb69983b05db6d4519526ee5
4
+ data.tar.gz: d202c23bdca1b462fedda0284a9ea9a98103beacddcad969be5e0e9e0dbc24c0
5
5
  SHA512:
6
- metadata.gz: a6088c2f97962305cea6dadcf4e5b5cb24d8332b9de55a2c42ebcc7c1929661d84dc40598e4167b5c74489eeeb898e8f47cdb638c0492a63c22124b7b14f1641
7
- data.tar.gz: 2894b4ae45d5f2eea15d99a403b1641f6ec5192d79b06765b3c7d765aa11255c3513f2f754d8405a8c9b32dc51f6f6ace8caf928f412e6e1334c0d979c6ae0b8
6
+ metadata.gz: b512dcca31e35eec561f0a6b9dfab8c0d6232934d997e552ba4a1f3574ea9d3edb85ccb9b7ef6df4bfc85e486a05390d0301609515a05df9b05589c2e2348e41
7
+ data.tar.gz: 0a80a2df0b37ee916bb406b84d598c390d723a3d54500de9aed4b3fce7f129cdb047060442a0d47d4873b8954a88ca5c6b4d85d957a6e16806ada372adae2c5e
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,13 +1,14 @@
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
6
  class << self
6
- # The global include_paths for Sass files. This is meant for plugins and
7
+ # The global {.include_paths} for Sass files. This is meant for plugins and
7
8
  # libraries to register the paths to their Sass stylesheets to that they may
8
- # be `@imported`. This include path is used by every instance of
9
- # {Sass::Embedded}. They are lower-precedence than any include paths passed
10
- # in via the `:include_paths` option.
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.
11
12
  #
12
13
  # If the `SASS_PATH` environment variable is set,
13
14
  # the initial value of `include_paths` will be initialized based on that.
@@ -16,7 +17,7 @@ module Sass
16
17
  #
17
18
  # @example
18
19
  # Sass.include_paths << File.dirname(__FILE__) + '/sass'
19
- # @return [Array<String, Pathname>]
20
+ # @return [Array]
20
21
  def include_paths
21
22
  @include_paths ||= if ENV['SASS_PATH']
22
23
  ENV['SASS_PATH'].split(File::PATH_SEPARATOR)
@@ -25,17 +26,26 @@ module Sass
25
26
  end
26
27
  end
27
28
 
29
+ # The global {.info} method. This instantiates a global {Embedded} instance
30
+ # and calls {Embedded#info}.
31
+ #
32
+ # @raise [ProtocolError]
28
33
  def info
29
34
  embedded.info
30
35
  end
31
36
 
32
- # The global render methods. This method automatically instantiates a
33
- # global {Sass::Embedded} instance when invoked the first time and call
34
- # `:render` method on the instance thereafter. The global {Sass::Embedded}
35
- # is automatically closed via {Kernel.at_exit}.
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; }')
36
44
  # @example
37
- # Sass.render(options)
38
- # @return [Hash]
45
+ # Sass.render(file: 'style.css')
46
+ # @return [Result]
47
+ # @raise [ProtocolError]
48
+ # @raise [RenderError]
39
49
  def render(**kwargs)
40
50
  embedded.render(**kwargs)
41
51
  end
@@ -50,10 +60,13 @@ module Sass
50
60
  end
51
61
  end
52
62
 
53
- require_relative 'sass/version'
54
- require_relative 'sass/error'
55
63
  require_relative 'sass/platform'
56
64
  require_relative 'sass/util'
65
+ require_relative 'sass/struct'
66
+ require_relative 'sass/result'
67
+ require_relative 'sass/error'
57
68
  require_relative 'sass/transport'
58
- require_relative 'sass/context'
69
+ require_relative 'sass/observer'
70
+ require_relative 'sass/version'
71
+ require_relative 'sass/render'
59
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,20 +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
- @info ||= InfoContext.new(@transport, next_id).fetch
26
+ @info ||= Version.new(@transport, next_id).message
17
27
  end
18
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]
19
36
  def render(data: nil,
20
37
  file: nil,
21
38
  indented_syntax: false,
22
39
  include_paths: [],
23
40
  output_style: :expanded,
24
- # precision: 5,
25
41
  indent_type: :space,
26
42
  indent_width: 2,
27
43
  linefeed: :lf,
28
- # source_comments: false,
29
44
  source_map: false,
30
45
  out_file: nil,
31
46
  omit_source_map_url: false,
@@ -40,41 +55,41 @@ module Sass
40
55
  indent_width = parse_indent_width(indent_width)
41
56
  linefeed = parse_linefeed(linefeed)
42
57
 
43
- response = RenderContext.new(@transport, next_id,
44
- data: data,
45
- file: file,
46
- indented_syntax: indented_syntax,
47
- include_paths: include_paths,
48
- output_style: output_style,
49
- source_map: source_map,
50
- out_file: out_file,
51
- functions: functions,
52
- importer: importer).fetch
53
-
54
- 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).message
68
+
69
+ if message.failure
55
70
  raise RenderError.new(
56
- response.failure.message,
57
- response.failure.formatted,
58
- if response.failure.span.nil?
71
+ message.failure.message,
72
+ message.failure.formatted,
73
+ if message.failure.span.nil?
59
74
  nil
60
- elsif response.failure.span.url == ''
75
+ elsif message.failure.span.url == ''
61
76
  'stdin'
62
77
  else
63
- Util.path(response.failure.span.url)
78
+ Util.path(message.failure.span.url)
64
79
  end,
65
- response.failure.span ? response.failure.span.start.line + 1 : nil,
66
- 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,
67
82
  1
68
83
  )
69
84
  end
70
85
 
71
- map, source_map = post_process_map(map: response.success.source_map,
86
+ map, source_map = post_process_map(map: message.success.source_map,
72
87
  file: file,
73
88
  out_file: out_file,
74
89
  source_map: source_map,
75
90
  source_map_root: source_map_root)
76
91
 
77
- css = post_process_css(css: response.success.css,
92
+ css = post_process_css(css: message.success.css,
78
93
  indent_type: indent_type,
79
94
  indent_width: indent_width,
80
95
  linefeed: linefeed,
@@ -86,16 +101,9 @@ module Sass
86
101
 
87
102
  finish = Util.now
88
103
 
89
- {
90
- css: css,
91
- map: map,
92
- stats: {
93
- entry: file.nil? ? 'data' : file,
94
- start: start,
95
- end: finish,
96
- duration: finish - start
97
- }
98
- }
104
+ stats = Result::Stats.new(file.nil? ? 'data' : file, start, finish, finish - start)
105
+
106
+ Result.new(css, map, stats)
99
107
  end
100
108
 
101
109
  def close
@@ -186,7 +194,7 @@ module Sass
186
194
  when :tab
187
195
  "\t"
188
196
  else
189
- raise ArgumentError, 'indent_type must be :space or :tab'
197
+ raise ArgumentError, 'indent_type must be one of :space, :tab'
190
198
  end
191
199
  end
192
200
 
@@ -219,276 +227,5 @@ module Sass
219
227
  @id
220
228
  end
221
229
  end
222
-
223
- # InfoContext
224
- class InfoContext < Context
225
- def initialize(transport, id)
226
- super(transport, id)
227
- @transport.send EmbeddedProtocol::InboundMessage::VersionRequest.new(id: @id)
228
- end
229
-
230
- def update(error, message)
231
- raise error unless error.nil?
232
-
233
- response = message[message.message.to_s]
234
-
235
- case response
236
- when EmbeddedProtocol::ProtocolError
237
- raise ProtocolError, response.message
238
- when EmbeddedProtocol::OutboundMessage::VersionResponse
239
- return unless response.id == @id
240
-
241
- Thread.new do
242
- super(nil, response)
243
- end
244
- end
245
- rescue StandardError => e
246
- Thread.new do
247
- super(e, nil)
248
- end
249
- end
250
- end
251
-
252
- # RenderContext
253
- class RenderContext < Context
254
- def initialize(transport, id,
255
- data:,
256
- file:,
257
- indented_syntax:,
258
- include_paths:,
259
- output_style:,
260
- source_map:,
261
- out_file:,
262
- functions:,
263
- importer:)
264
- raise ArgumentError, 'either data or file must be set' if file.nil? && data.nil?
265
-
266
- super(transport, id)
267
-
268
- @data = data
269
- @file = file
270
- @indented_syntax = indented_syntax
271
- @include_paths = include_paths
272
- @output_style = output_style
273
- @source_map = source_map
274
- @out_file = out_file
275
- @global_functions = functions.keys
276
- @functions = functions.transform_keys do |key|
277
- key.to_s.split('(')[0].chomp
278
- end
279
- @importer = importer
280
- @import_responses = {}
281
-
282
- @transport.send compile_request
283
- end
284
-
285
- def update(error, message)
286
- raise error unless error.nil?
287
-
288
- response = message[message.message.to_s]
289
-
290
- case response
291
- when EmbeddedProtocol::ProtocolError
292
- raise ProtocolError, response.message
293
- when EmbeddedProtocol::OutboundMessage::CompileResponse
294
- return unless response.id == @id
295
-
296
- Thread.new do
297
- super(nil, response)
298
- end
299
- when EmbeddedProtocol::OutboundMessage::LogEvent
300
- # not implemented yet
301
- when EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
302
- return unless response['compilation_id'] == @id
303
-
304
- Thread.new do
305
- @transport.send canonicalize_response(response)
306
- end
307
- when EmbeddedProtocol::OutboundMessage::ImportRequest
308
- return unless response['compilation_id'] == @id
309
-
310
- Thread.new do
311
- @transport.send import_response(response)
312
- end
313
- when EmbeddedProtocol::OutboundMessage::FileImportRequest
314
- raise NotImplementedError, 'FileImportRequest is not implemented'
315
- when EmbeddedProtocol::OutboundMessage::FunctionCallRequest
316
- return unless response['compilation_id'] == @id
317
-
318
- Thread.new do
319
- @transport.send function_call_response(response)
320
- end
321
- end
322
- rescue StandardError => e
323
- Thread.new do
324
- super(e, nil)
325
- end
326
- end
327
-
328
- private
329
-
330
- def compile_request
331
- EmbeddedProtocol::InboundMessage::CompileRequest.new(
332
- id: @id,
333
- string: string,
334
- path: path,
335
- style: style,
336
- source_map: source_map,
337
- importers: importers,
338
- global_functions: global_functions,
339
- alert_color: $stderr.tty?,
340
- alert_ascii: Platform::OS == 'windows'
341
- )
342
- end
343
-
344
- def canonicalize_response(canonicalize_request)
345
- url = Util.file_uri(File.absolute_path(canonicalize_request.url, (@file.nil? ? 'stdin' : @file)))
346
-
347
- begin
348
- result = @importer[canonicalize_request.importer_id].call canonicalize_request.url, @file
349
- raise result if result.is_a? StandardError
350
- rescue StandardError => e
351
- return EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
352
- id: canonicalize_request.id,
353
- error: e.message
354
- )
355
- end
356
-
357
- if result&.key? :contents
358
- @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
359
- id: canonicalize_request.id,
360
- success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
361
- contents: result[:contents],
362
- syntax: EmbeddedProtocol::Syntax::SCSS,
363
- source_map_url: nil
364
- )
365
- )
366
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
367
- id: canonicalize_request.id,
368
- url: url
369
- )
370
- elsif result&.key? :file
371
- canonicalized_url = Util.file_uri(result[:file])
372
-
373
- # TODO: FileImportRequest is not supported yet.
374
- # Workaround by reading contents and return it when server asks
375
- @import_responses[canonicalized_url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
376
- id: canonicalize_request.id,
377
- success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
378
- contents: File.read(result[:file]),
379
- syntax: EmbeddedProtocol::Syntax::SCSS,
380
- source_map_url: nil
381
- )
382
- )
383
-
384
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
385
- id: canonicalize_request.id,
386
- url: canonicalized_url
387
- )
388
- else
389
- EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
390
- id: canonicalize_request.id
391
- )
392
- end
393
- end
394
-
395
- def import_response(import_request)
396
- url = import_request.url
397
-
398
- if @import_responses.key? url
399
- @import_responses[url].id = import_request.id
400
- else
401
- @import_responses[url] = EmbeddedProtocol::InboundMessage::ImportResponse.new(
402
- id: import_request.id,
403
- error: "Failed to import: #{url}"
404
- )
405
- end
406
-
407
- @import_responses[url]
408
- end
409
-
410
- def function_call_response(function_call_request)
411
- EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
412
- id: function_call_request.id,
413
- success: @functions[function_call_request.name].call(*function_call_request.arguments)
414
- )
415
- rescue StandardError => e
416
- EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
417
- id: function_call_request.id,
418
- error: e.message
419
- )
420
- end
421
-
422
- def syntax
423
- if @indented_syntax == true
424
- EmbeddedProtocol::Syntax::INDENTED
425
- else
426
- EmbeddedProtocol::Syntax::SCSS
427
- end
428
- end
429
-
430
- def url
431
- return if @file.nil?
432
-
433
- Util.file_uri @file
434
- end
435
-
436
- def string
437
- return if @data.nil?
438
-
439
- EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
440
- source: @data,
441
- url: url,
442
- syntax: syntax
443
- )
444
- end
445
-
446
- def path
447
- @file if @data.nil?
448
- end
449
-
450
- def style
451
- case @output_style&.to_sym
452
- when :expanded
453
- EmbeddedProtocol::OutputStyle::EXPANDED
454
- when :compressed
455
- EmbeddedProtocol::OutputStyle::COMPRESSED
456
- when :nested, :compact
457
- raise ArgumentError, "#{@output_style} is not a supported output_style"
458
- else
459
- raise ArgumentError, "#{@output_style} is not a valid utput_style"
460
- end
461
- end
462
-
463
- def source_map
464
- @source_map.is_a?(String) || (@source_map == true && !@out_file.nil?)
465
- end
466
-
467
- attr_reader :global_functions
468
-
469
- # Order
470
- # 1. Loading a file relative to the file in which the @use or @import appeared.
471
- # 2. Each custom importer.
472
- # 3. Loading a file relative to the current working directory.
473
- # 4. Each load path in includePaths
474
- # 5. Each load path specified in the SASS_PATH environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.
475
- def importers
476
- custom_importers = @importer.map.with_index do |_, id|
477
- EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
478
- importer_id: id
479
- )
480
- end
481
-
482
- include_path_importers = @include_paths
483
- .concat(Sass.include_paths)
484
- .map do |include_path|
485
- EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
486
- path: File.absolute_path(include_path)
487
- )
488
- end
489
-
490
- custom_importers.concat include_path_importers
491
- end
492
- end
493
230
  end
494
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
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sass
4
- # An abstract context for maintaining state and observing transport
5
- class Context
4
+ # The {Observer} for receiving messages from {Transport}.
5
+ class Observer
6
6
  def initialize(transport, id)
7
- raise NotImplementedError if instance_of? Context
7
+ raise NotImplementedError if instance_of? Observer
8
8
 
9
9
  @transport = transport
10
10
  @id = id
11
11
  @mutex = Mutex.new
12
12
  @condition_variable = ConditionVariable.new
13
- @response = nil
14
13
  @error = nil
14
+ @message = nil
15
15
  @transport.add_observer self
16
16
  end
17
17
 
18
- def fetch
18
+ def message
19
19
  @mutex.synchronize do
20
- @condition_variable.wait(@mutex) if @error.nil? && @response.nil?
20
+ @condition_variable.wait(@mutex) if @error.nil? && @message.nil?
21
21
  end
22
22
 
23
23
  raise @error unless @error.nil?
24
24
 
25
- @response
25
+ @message
26
26
  end
27
27
 
28
28
  def update(error, message)
29
29
  @transport.delete_observer self
30
30
  @mutex.synchronize do
31
31
  @error = error
32
- @response = message
32
+ @message = message
33
33
  @condition_variable.broadcast
34
34
  end
35
35
  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,24 +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
35
  def add_observer(*args)
29
- @observerable_semaphore.synchronize do
36
+ @observerable_mutex.synchronize do
30
37
  super(*args)
31
38
  end
32
39
  end
33
40
 
34
- def send(req)
35
- req_kind = req.class.name.split('::').last.gsub(/\B(?=[A-Z])/, '_').downcase
36
- message = EmbeddedProtocol::InboundMessage.new(req_kind => req)
37
- write message.to_proto
41
+ def send(message)
42
+ write EmbeddedProtocol::InboundMessage.new(
43
+ ONEOF_MESSAGE[message.class.descriptor] => message
44
+ ).to_proto
38
45
  end
39
46
 
40
47
  def close
@@ -63,9 +70,9 @@ module Sass
63
70
  end
64
71
  payload = @stdout.read length
65
72
  message = EmbeddedProtocol::OutboundMessage.decode payload
66
- @observerable_semaphore.synchronize do
73
+ @observerable_mutex.synchronize do
67
74
  changed
68
- notify_observers nil, message
75
+ notify_observers nil, message[message.message.to_s]
69
76
  end
70
77
  rescue Interrupt
71
78
  break
@@ -84,7 +91,7 @@ module Sass
84
91
  rescue Interrupt
85
92
  break
86
93
  rescue IOError => e
87
- @observerable_semaphore.synchronize do
94
+ @observerable_mutex.synchronize do
88
95
  notify_observers e, nil
89
96
  end
90
97
  close
@@ -94,7 +101,7 @@ module Sass
94
101
  end
95
102
 
96
103
  def write(payload)
97
- @stdin_semaphore.synchronize do
104
+ @stdin_mutex.synchronize do
98
105
  length = payload.length
99
106
  while length.positive?
100
107
  @stdin.write ((length > 0x7f ? 0x80 : 0) | (length & 0x7f)).chr
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.1'
4
+ VERSION = '0.6.2'
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")
@@ -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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sass-embedded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - なつき
@@ -155,10 +155,14 @@ files:
155
155
  - ext/Makefile
156
156
  - ext/extconf.rb
157
157
  - lib/sass.rb
158
- - lib/sass/context.rb
159
158
  - lib/sass/embedded.rb
160
159
  - lib/sass/error.rb
160
+ - lib/sass/info.rb
161
+ - lib/sass/observer.rb
161
162
  - lib/sass/platform.rb
163
+ - lib/sass/render.rb
164
+ - lib/sass/result.rb
165
+ - lib/sass/struct.rb
162
166
  - lib/sass/transport.rb
163
167
  - lib/sass/util.rb
164
168
  - lib/sass/version.rb
@@ -174,7 +178,7 @@ files:
174
178
  - test/render_test.rb
175
179
  - test/source_maps_test.rb
176
180
  - test/test_helper.rb
177
- homepage: https://github.com/ntkme/embedded-host-ruby
181
+ homepage: https://github.com/ntkme/sass-embedded-host-ruby
178
182
  licenses:
179
183
  - MIT
180
184
  metadata: {}