sass-embedded 0.2.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e345b143aa337d47b0f28d6d58d2a5636f84fb69b4f5ed739b6613ab39327a2d
4
- data.tar.gz: 72c0bae0108414a5a989ea6b2f5eac86406eb83faa9c3fa1f1f642e609930afd
3
+ metadata.gz: d1086f5483670120b40e9741187e5150a5af9e9ad5d38159d6d57459edec350b
4
+ data.tar.gz: d6afead31f102a04ad4d11c82e1381b597c285ae0206564640763173e2b67fc6
5
5
  SHA512:
6
- metadata.gz: 478a33de83120ace9665efd401d9988a956a291ed19b905f77d129486fd3e796bb90a66d6e3ee4f8f1fde47eddbb285918e84f5d8795d2a9f3546c82c431ef88
7
- data.tar.gz: d9512132067039d511bafc684132ae010238b0b7b9ad1c37e3d7c6a4cb6463caa620eed87b05af3fc671c9200246f81af591b82d8c0d01b38429cafb87667394
6
+ metadata.gz: c8c534da41310d6b7e6228624d26a6e3dd94bccc4b67b2e9a25bff82251cb3b4a5d312c58ec1e498f944731cc99f4fdcb66b117278640cee2fc3d81f95924f66
7
+ data.tar.gz: 6d8808fc65dbb7fc4b43976beb6c8573635d874e9d72a7949f20eda5406e582ea0933a99f844256ec284e3ecf1e8e6f02b967f50854fde661470bf6834550abb
@@ -41,4 +41,4 @@ jobs:
41
41
 
42
42
  - name: Test Gem
43
43
  run: |-
44
- ruby -r sass -e "puts Sass.render({ data: 'h1 { color: black; }' })[:css]"
44
+ ruby -r sass -e "puts Sass.render(data: 'h1 { color: black; }')[:css]"
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '**/*_pb.rb'
4
+ - 'vendor/**/*'
5
+
6
+ TargetRubyVersion: 2.6
7
+
8
+ NewCops: enable
9
+
10
+ Metrics:
11
+ Enabled: false
data/README.md CHANGED
@@ -11,9 +11,7 @@ It exposes a Ruby API for Sass that's backed by a native [Dart Sass](https://sas
11
11
  ``` ruby
12
12
  require "sass"
13
13
 
14
- Sass.render({
15
- file: "style.scss"
16
- })
14
+ Sass.render(file: "style.scss")
17
15
  ```
18
16
 
19
17
  ---
data/Rakefile CHANGED
@@ -2,17 +2,22 @@
2
2
 
3
3
  require 'bundler/gem_tasks'
4
4
 
5
- task default: :test
5
+ task default: %i[rubocop test]
6
6
 
7
7
  desc 'Download dart-sass-embedded'
8
8
  task :extconf do
9
- system('make', '-C', 'ext', 'distclean')
10
- require_relative 'ext/extconf'
11
9
  system('make', '-C', 'ext')
12
10
  end
13
11
 
14
12
  desc 'Run all tests'
15
- task :test do
13
+ task test: :extconf do
16
14
  $LOAD_PATH.unshift('lib', 'test')
17
15
  Dir.glob('./test/**/*_test.rb').sort.each { |f| require f }
18
16
  end
17
+
18
+ begin
19
+ require 'rubocop/rake_task'
20
+ RuboCop::RakeTask.new
21
+ rescue LoadError
22
+ nil
23
+ end
data/ext/extconf.rb CHANGED
@@ -8,11 +8,24 @@ require 'fileutils'
8
8
  require_relative '../lib/sass/platform'
9
9
 
10
10
  module Sass
11
+ # The dependency downloader. This will download all the dependencies
12
+ # during gem installation, and the Makefile will unpack all downloaded
13
+ # dependencies. By default it will download 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 \
11
24
  class Extconf
12
25
  def initialize
26
+ get_with_config('protoc', true) { latest_protoc }
13
27
  get_with_config('sass-embedded', true) { latest_sass_embedded }
14
28
  get_with_config('sass-embedded-protocol', true) { latest_sass_embedded_protocol }
15
- get_with_config('protoc', true) { latest_protoc }
16
29
  end
17
30
 
18
31
  private
@@ -141,7 +154,7 @@ module Sass
141
154
 
142
155
  ext = 'zip'
143
156
 
144
- "https://github.com/#{repo}/releases/download/#{tag_name}/protoc-#{tag_name[1..-1]}-#{os_arch}.#{ext}"
157
+ "https://github.com/#{repo}/releases/download/#{tag_name}/protoc-#{tag_name[1..]}-#{os_arch}.#{ext}"
145
158
  end
146
159
 
147
160
  def latest_sass_embedded_protocol
data/lib/sass.rb CHANGED
@@ -1,36 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The Sass module. This module will communicate with Embedded Dart Sass using
4
+ # the Embedded Sass protocol.
3
5
  module Sass
4
- # The global include_paths for Sass files. This is meant for plugins and
5
- # libraries to register the paths to their Sass stylesheets to that they may
6
- # be `@imported`. This include path is used by every instance of
7
- # {Sass::Embedded}. They are lower-precedence than any include paths passed
8
- # in via the `:include_paths` option.
9
- #
10
- # If the `SASS_PATH` environment variable is set,
11
- # the initial value of `include_paths` will be initialized based on that.
12
- # The variable should be a colon-separated list of path names
13
- # (semicolon-separated on Windows).
14
- #
15
- # @example
16
- # Sass.include_paths << File.dirname(__FILE__) + '/sass'
17
- # @return [Array<String, Pathname>]
18
- def self.include_paths
19
- @include_paths ||= if ENV['SASS_PATH']
20
- ENV['SASS_PATH'].split(File::PATH_SEPARATOR)
21
- else
22
- []
23
- end
24
- 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 `@imported`. This include path is used by every instance of
10
+ # {Sass::Embedded}. They are lower-precedence than any include paths passed
11
+ # 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<String, Pathname>]
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
+ def info
30
+ embedded.info
31
+ end
32
+
33
+ # The global render method. This method automatically instantiates a
34
+ # global {Sass::Embedded} instance when invoked the first time and call
35
+ # `:render` method on the instance thereafter. See {Sass::Embedded#render}
36
+ # for supported options.
37
+ # @example
38
+ # Sass.render(data: 'h1 { font-size: 40px; }')
39
+ # @example
40
+ # Sass.render(file: 'style.css')
41
+ # @return [Hash]
42
+ def render(**kwargs)
43
+ embedded.render(**kwargs)
44
+ end
45
+
46
+ private
47
+
48
+ def embedded
49
+ return @embedded if defined?(@embedded) && !@embedded.closed?
25
50
 
26
- def self.render(options)
27
- unless defined? @embedded
28
51
  @embedded = Sass::Embedded.new
29
- at_exit do
30
- @embedded.close
31
- end
32
52
  end
33
- @embedded.render options
34
53
  end
35
54
  end
36
55
 
@@ -39,4 +58,7 @@ require_relative 'sass/error'
39
58
  require_relative 'sass/platform'
40
59
  require_relative 'sass/util'
41
60
  require_relative 'sass/transport'
61
+ require_relative 'sass/observer'
62
+ require_relative 'sass/info'
63
+ require_relative 'sass/render'
42
64
  require_relative 'sass/embedded'
data/lib/sass/embedded.rb CHANGED
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
4
+ require 'json'
5
+
3
6
  module Sass
7
+ # The {Embedded} user interface for using dart-sass-embedded. Each instance
8
+ # will create its own {Transport}.
9
+ # @example
10
+ # embedded = Sass::Embedded.new
11
+ # result = embedded.render(data: 'h1 { font-size: 40px; }')
12
+ # result = embedded.render(file: 'style.css')
13
+ # embedded.close
4
14
  class Embedded
5
15
  def initialize
6
16
  @transport = Transport.new
@@ -8,198 +18,85 @@ module Sass
8
18
  @id = 0
9
19
  end
10
20
 
11
- def render(options)
12
- start = Sass::Util.now
13
-
14
- raise Sass::NotRenderedError, 'Either :data or :file must be set.' if options[:file].nil? && options[:data].nil?
15
-
16
- string = if options[:data]
17
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
18
- source: options[:data],
19
- url: options[:file] ? Sass::Util.file_uri(options[:file]) : 'stdin',
20
- syntax: options[:indented_syntax] == true ? Sass::EmbeddedProtocol::Syntax::INDENTED : Sass::EmbeddedProtocol::Syntax::SCSS
21
- )
22
- end
23
-
24
- path = options[:data] ? nil : options[:file]
25
-
26
- style = case options[:output_style]&.to_sym
27
- when :expanded, nil
28
- Sass::EmbeddedProtocol::OutputStyle::EXPANDED
29
- when :compressed
30
- Sass::EmbeddedProtocol::OutputStyle::COMPRESSED
31
- when :nested, :compact
32
- raise Sass::UnsupportedValue, "#{options[:output_style]} is not a supported :output_style"
33
- else
34
- raise Sass::InvalidStyleError, "#{options[:output_style]} is not a valid :output_style"
35
- end
36
-
37
- source_map = options[:source_map].is_a?(String) || (options[:source_map] == true && !options[:out_file].nil?)
38
-
39
- # 1. Loading a file relative to the file in which the @use or @import appeared.
40
- # 2. Each custom importer.
41
- # 3. Loading a file relative to the current working directory.
42
- # 4. Each load path in includePaths
43
- # 5. Each load path specified in the SASS_PATH environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.
44
- importers = (if options[:importer]
45
- [
46
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(importer_id: 0)
47
- ]
48
- else
49
- []
50
- end).concat(
51
- (options[:include_paths] || []).concat(Sass.include_paths)
52
- .map do |include_path|
53
- Sass::EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
54
- path: File.absolute_path(include_path)
55
- )
56
- end
57
- )
58
-
59
- signatures = []
60
- functions = {}
61
- options[:functions]&.each do |signature, function|
62
- signatures.push signature
63
- functions[signature.to_s.split('(')[0].chomp] = function
64
- end
21
+ def info
22
+ @info ||= Info.new(@transport, next_id).fetch
23
+ end
65
24
 
66
- compilation_id = next_id
67
-
68
- compile_request = Sass::EmbeddedProtocol::InboundMessage::CompileRequest.new(
69
- id: compilation_id,
70
- string: string,
71
- path: path,
72
- style: style,
73
- source_map: source_map,
74
- importers: importers,
75
- global_functions: options[:functions] ? signatures : [],
76
- alert_color: true,
77
- alert_ascii: true
78
- )
79
-
80
- response = @transport.send compile_request, compilation_id
81
-
82
- file = options[:file] || 'stdin'
83
- canonicalizations = {}
84
- imports = {}
85
-
86
- loop do
87
- case response
88
- when Sass::EmbeddedProtocol::OutboundMessage::CompileResponse
89
- break
90
- when Sass::EmbeddedProtocol::OutboundMessage::CanonicalizeRequest
91
- url = Sass::Util.file_uri(File.absolute_path(response.url, File.dirname(file)))
92
-
93
- if canonicalizations.key? url
94
- canonicalizations[url].id = response.id
25
+ def render(data: nil,
26
+ file: nil,
27
+ indented_syntax: false,
28
+ include_paths: [],
29
+ output_style: :expanded,
30
+ # precision: 5,
31
+ indent_type: :space,
32
+ indent_width: 2,
33
+ linefeed: :lf,
34
+ # source_comments: false,
35
+ source_map: false,
36
+ out_file: nil,
37
+ omit_source_map_url: false,
38
+ # source_map_contents: false,
39
+ source_map_embed: false,
40
+ source_map_root: '',
41
+ functions: {},
42
+ importer: [])
43
+ start = Util.now
44
+
45
+ indent_type = parse_indent_type(indent_type)
46
+ indent_width = parse_indent_width(indent_width)
47
+ linefeed = parse_linefeed(linefeed)
48
+
49
+ result = Render.new(@transport, next_id,
50
+ data: data,
51
+ file: file,
52
+ indented_syntax: indented_syntax,
53
+ include_paths: include_paths,
54
+ output_style: output_style,
55
+ source_map: source_map,
56
+ out_file: out_file,
57
+ functions: functions,
58
+ importer: importer).fetch
59
+
60
+ if result.failure
61
+ raise RenderError.new(
62
+ result.failure.message,
63
+ result.failure.formatted,
64
+ if result.failure.span.nil?
65
+ nil
66
+ elsif result.failure.span.url == ''
67
+ 'stdin'
95
68
  else
96
- resolved = nil
97
- options[:importer].each do |importer|
98
- begin
99
- resolved = importer.call response.url, file
100
- rescue StandardError => e
101
- resolved = e
102
- end
103
- break if resolved
104
- end
105
- if resolved.nil?
106
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
107
- id: response.id,
108
- url: url
109
- )
110
- elsif resolved.is_a? StandardError
111
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
112
- id: response.id,
113
- error: resolved.message
114
- )
115
- elsif resolved.key? :contents
116
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
117
- id: response.id,
118
- url: url
119
- )
120
- imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
121
- id: response.id,
122
- success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
123
- contents: resolved[:contents],
124
- syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
125
- source_map_url: nil
126
- )
127
- )
128
- elsif resolved.key? :file
129
- canonicalized_url = Sass::Util.file_uri(resolved[:file])
130
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
131
- id: response.id,
132
- url: canonicalized_url
133
- )
134
- imports[canonicalized_url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
135
- id: response.id,
136
- success: Sass::EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
137
- contents: File.read(resolved[:file]),
138
- syntax: Sass::EmbeddedProtocol::Syntax::SCSS,
139
- source_map_url: nil
140
- )
141
- )
142
- else
143
- canonicalizations[url] = Sass::EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
144
- id: response.id,
145
- error: "Unexpected value returned from importer: #{resolved}"
146
- )
147
- end
148
- end
149
-
150
- response = @transport.send canonicalizations[url], compilation_id
151
- when Sass::EmbeddedProtocol::OutboundMessage::ImportRequest
152
- url = response.url
153
-
154
- if imports.key? url
155
- imports[url].id = response.id
156
- else
157
- imports[url] = Sass::EmbeddedProtocol::InboundMessage::ImportResponse.new(
158
- id: response.id,
159
- error: "Failed to import: #{url}"
160
- )
161
- end
162
-
163
- response = @transport.send imports[url], compilation_id
164
- when Sass::EmbeddedProtocol::OutboundMessage::FunctionCallRequest
165
- begin
166
- message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
167
- id: response.id,
168
- success: functions[response.name].call(*response.arguments)
169
- )
170
- rescue StandardError => e
171
- message = Sass::EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
172
- id: response.id,
173
- error: e.message
174
- )
175
- end
176
-
177
- response = @transport.send message, compilation_id
178
- when Sass::EmbeddedProtocol::ProtocolError
179
- raise Sass::ProtocolError, response.message
180
- else
181
- raise Sass::ProtocolError, "Unexpected packet received: #{response}"
182
- end
183
- end
184
-
185
- if response.failure
186
- raise Sass::CompilationError.new(
187
- response.failure.message,
188
- response.failure.formatted,
189
- response.failure.span ? response.failure.span.url : nil,
190
- response.failure.span ? response.failure.span.start.line + 1 : nil,
191
- response.failure.span ? response.failure.span.start.column + 1 : nil,
69
+ Util.path(result.failure.span.url)
70
+ end,
71
+ result.failure.span ? result.failure.span.start.line + 1 : nil,
72
+ result.failure.span ? result.failure.span.start.column + 1 : nil,
192
73
  1
193
74
  )
194
75
  end
195
76
 
196
- finish = Sass::Util.now
77
+ map, source_map = post_process_map(map: result.success.source_map,
78
+ file: file,
79
+ out_file: out_file,
80
+ source_map: source_map,
81
+ source_map_root: source_map_root)
82
+
83
+ css = post_process_css(css: result.success.css,
84
+ indent_type: indent_type,
85
+ indent_width: indent_width,
86
+ linefeed: linefeed,
87
+ map: map,
88
+ out_file: out_file,
89
+ omit_source_map_url: omit_source_map_url,
90
+ source_map: source_map,
91
+ source_map_embed: source_map_embed)
92
+
93
+ finish = Util.now
197
94
 
198
95
  {
199
- css: response.success.css,
200
- map: response.success.source_map,
96
+ css: css,
97
+ map: map,
201
98
  stats: {
202
- entry: options[:file] || 'data',
99
+ entry: file.nil? ? 'data' : file,
203
100
  start: start,
204
101
  end: finish,
205
102
  duration: finish - start
@@ -209,21 +106,116 @@ module Sass
209
106
 
210
107
  def close
211
108
  @transport.close
212
- nil
109
+ end
110
+
111
+ def closed?
112
+ @transport.closed?
213
113
  end
214
114
 
215
115
  private
216
116
 
217
- def info
218
- version_response = @transport.send Sass::EmbeddedProtocol::InboundMessage::VersionRequest.new(
219
- id: next_id
220
- )
221
- {
222
- compiler_version: version_response.compiler_version,
223
- protocol_version: version_response.protocol_version,
224
- implementation_name: version_response.implementation_name,
225
- implementation_version: version_response.implementation_version
226
- }
117
+ def post_process_map(map:,
118
+ file:,
119
+ out_file:,
120
+ source_map:,
121
+ source_map_root:)
122
+ return if map.nil? || map.empty?
123
+
124
+ map_data = JSON.parse(map)
125
+
126
+ map_data['sourceRoot'] = source_map_root
127
+
128
+ source_map_path = if source_map.is_a? String
129
+ source_map
130
+ else
131
+ "#{out_file}.map"
132
+ end
133
+
134
+ source_map_dir = File.dirname(source_map_path)
135
+
136
+ if out_file
137
+ map_data['file'] = Util.relative(source_map_dir, out_file)
138
+ elsif file
139
+ ext = File.extname(file)
140
+ map_data['file'] = "#{file[0..(ext.empty? ? -1 : -ext.length - 1)]}.css"
141
+ else
142
+ map_data['file'] = 'stdin.css'
143
+ end
144
+
145
+ map_data['sources'].map! do |source|
146
+ if source.start_with? Util::FILE_PROTOCOL
147
+ Util.relative(source_map_dir, Util.path(source))
148
+ else
149
+ source
150
+ end
151
+ end
152
+
153
+ [-JSON.generate(map_data), source_map_path]
154
+ end
155
+
156
+ def post_process_css(css:,
157
+ indent_type:,
158
+ indent_width:,
159
+ linefeed:,
160
+ map:,
161
+ omit_source_map_url:,
162
+ out_file:,
163
+ source_map:,
164
+ source_map_embed:)
165
+ css = +css
166
+ if indent_width != 2 || indent_type.to_sym != :space
167
+ indent = indent_type * indent_width
168
+ css.gsub!(/^ +/) do |space|
169
+ indent * (space.length / 2)
170
+ end
171
+ end
172
+ css.gsub!("\n", linefeed) if linefeed != "\n"
173
+
174
+ unless map.nil? || omit_source_map_url == true
175
+ url = if source_map_embed
176
+ "data:application/json;base64,#{Base64.strict_encode64(map)}"
177
+ elsif out_file
178
+ Util.relative(File.dirname(out_file), source_map)
179
+ else
180
+ source_map
181
+ end
182
+ css += "#{linefeed}/*# sourceMappingURL=#{url} */"
183
+ end
184
+
185
+ -css
186
+ end
187
+
188
+ def parse_indent_type(indent_type)
189
+ case indent_type.to_sym
190
+ when :space
191
+ ' '
192
+ when :tab
193
+ "\t"
194
+ else
195
+ raise ArgumentError, 'indent_type must be one of :space, :tab'
196
+ end
197
+ end
198
+
199
+ def parse_indent_width(indent_width)
200
+ raise ArgumentError, 'indent_width must be an integer' unless indent_width.is_a? Integer
201
+ raise RangeError, 'indent_width must be in between 0 and 10 (inclusive)' unless indent_width.between? 0, 10
202
+
203
+ indent_width
204
+ end
205
+
206
+ def parse_linefeed(linefeed)
207
+ case linefeed.to_sym
208
+ when :lf
209
+ "\n"
210
+ when :lfcr
211
+ "\n\r"
212
+ when :cr
213
+ "\r"
214
+ when :crlf
215
+ "\r\n"
216
+ else
217
+ raise ArgumentError, 'linefeed must be one of :lf, :lfcr, :cr, :crlf'
218
+ end
227
219
  end
228
220
 
229
221
  def next_id