sass-embedded 0.2.3 → 0.4.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: 5dc860ffde1f3fb6315c2a43972b245841e10e0b0b4a8979a5a4c7528063cd95
4
- data.tar.gz: aa76a73fdc4a252b1cd948b90452fa772282408289e99a4e0206c968a5e68898
3
+ metadata.gz: c03a96231c8305e27f3deb1a35e308808482159a5dd25b4c8191da9c1b25500d
4
+ data.tar.gz: d6ba46dd80433759554a8c83d1fe8a8b4fd66461ffb063c859011d4ab4e3b6cc
5
5
  SHA512:
6
- metadata.gz: b03c95f66214d6e095fd1e11bbeebe24d56443cf90107661007aef30006502296799c07e4c73974cfa0ca7debc7a807a120c86eb99441363af10f699a9cd0148
7
- data.tar.gz: b834e3e9e2a0eb7acdea42e04c1627edc86f2e146c0b96e79331c646b387f8df861fa59d0efeb46d9f3d176b388804d67b63515cc406efb8bb5d7a3c1d4391f6
6
+ metadata.gz: a4be81866a1672fd02da1cf1c66d08a2a0dd50cb002ee1b6fd0cce4d70c4e02222c11662a69307e7e33c58bf1dedc968d7ec72b2ebb3fa35296b1520896be2f5
7
+ data.tar.gz: 39b348a11dd2277bb43e7bd936b08462057caac6180cb5b0548384cb64da47d1255606d8ce0a0ab2d0a4d891efee26cff190568eec1a782426011c4fc15ac2b0
@@ -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,14 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '**/*_pb.rb'
4
+ - 'vendor/**/*'
5
+
6
+ TargetRubyVersion: 2.6
7
+
8
+ NewCops: enable
9
+
10
+ Layout/LineLength:
11
+ Enabled: false
12
+
13
+ Metrics:
14
+ 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/sass_embedded', 'distclean')
10
- require_relative 'ext/sass_embedded/extconf'
11
- system('make', '-C', 'ext/sass_embedded', 'install')
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
File without changes
data/ext/Makefile ADDED
@@ -0,0 +1,51 @@
1
+ ifeq ($(OS),Windows_NT)
2
+ RM = cmd /c del /f /q /s
3
+ else
4
+ RM = rm -fr
5
+ endif
6
+ EXTCONF = ruby -- extconf.rb --without-sass-embedded --without-sass-embedded-protocol --without-protoc
7
+
8
+ .PHONY: all
9
+ all: sass_embedded embedded_sass_pb.rb
10
+
11
+ .PHONY: install
12
+ install:
13
+
14
+ .PHONY: clean
15
+ clean:
16
+ $(RM) embedded_sass_pb.rb protoc sass_embedded
17
+
18
+ .PHONY: distclean
19
+ distclean: clean
20
+ $(RM) embedded_sass.proto protoc-*.zip sass_embedded-*.tar.gz
21
+
22
+ ifeq ($(OS),Windows_NT)
23
+ sass_embedded-*.zip:
24
+ else
25
+ sass_embedded-*.tar.gz:
26
+ endif
27
+ $(EXTCONF) --with-sass-embedded
28
+
29
+ ifeq ($(OS),Windows_NT)
30
+ sass_embedded: sass_embedded-*.zip
31
+ powershell -c "Get-Item $< | Expand-Archive -DestinationPath . -Force"
32
+ else
33
+ sass_embedded: sass_embedded-*.tar.gz
34
+ tar -vxzf $<
35
+ endif
36
+
37
+ protoc-*.zip:
38
+ $(EXTCONF) --with-protoc
39
+
40
+ protoc: protoc-*.zip
41
+ ifeq ($(OS),Windows_NT)
42
+ powershell -c "Get-Item $< | Expand-Archive -DestinationPath $@ -Force"
43
+ else
44
+ unzip -od $@ $<
45
+ endif
46
+
47
+ embedded_sass.proto:
48
+ $(EXTCONF) --with-sass-embedded-protocol
49
+
50
+ %_pb.rb: %.proto protoc
51
+ ./protoc/bin/protoc --ruby_out=. $<
data/ext/extconf.rb ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'mkmf'
5
+ require 'json'
6
+ require 'open-uri'
7
+ require 'fileutils'
8
+ require_relative '../lib/sass/platform'
9
+
10
+ module Sass
11
+ # Install dependencies for sass-embedded during gem install
12
+ class Extconf
13
+ def initialize
14
+ get_with_config('sass-embedded', true) { latest_sass_embedded }
15
+ get_with_config('sass-embedded-protocol', true) { latest_sass_embedded_protocol }
16
+ get_with_config('protoc', true) { latest_protoc }
17
+ end
18
+
19
+ private
20
+
21
+ def get_with_config(config, default)
22
+ val = with_config(config, default)
23
+ case val
24
+ when true
25
+ if block_given?
26
+ get yield
27
+ else
28
+ get default
29
+ end
30
+ when false
31
+ nil
32
+ else
33
+ get val
34
+ end
35
+ end
36
+
37
+ def get(uri_s)
38
+ uri = URI.parse(uri_s)
39
+ path = File.absolute_path(File.basename(uri.path), __dir__)
40
+ if uri.is_a?(URI::File) || uri.instance_of?(URI::Generic)
41
+ FileUtils.copy_file uri.path, path
42
+ elsif uri.respond_to? :open
43
+ uri.open do |source|
44
+ File.open(path, 'wb') do |destination|
45
+ destination.write source.read
46
+ end
47
+ end
48
+ else
49
+ raise
50
+ end
51
+ rescue StandardError
52
+ raise "Failed to get: #{uri}"
53
+ end
54
+
55
+ def latest_release(repo, prerelease: false)
56
+ if prerelease
57
+ headers = {}
58
+ headers['Authorization'] = "token #{ENV['GITHUB_TOKEN']}" if ENV['GITHUB_TOKEN']
59
+ URI.parse("https://api.github.com/repos/#{repo}/releases").open(headers) do |file|
60
+ JSON.parse(file.read)[0]['tag_name']
61
+ end
62
+ else
63
+ URI.parse("https://github.com/#{repo}/releases/latest").open do |file|
64
+ File.basename file.base_uri.to_s
65
+ end
66
+ end
67
+ end
68
+
69
+ def latest_sass_embedded
70
+ repo = 'sass/dart-sass-embedded'
71
+
72
+ # TODO, don't use prerelease once a release is available
73
+ tag_name = latest_release repo, prerelease: true
74
+
75
+ os = case Sass::Platform::OS
76
+ when 'darwin'
77
+ 'macos'
78
+ when 'linux'
79
+ 'linux'
80
+ when 'windows'
81
+ 'windows'
82
+ else
83
+ raise "Unsupported OS: #{Sass::Platform::OS}"
84
+ end
85
+
86
+ arch = case Sass::Platform::ARCH
87
+ when 'x86_64'
88
+ 'x64'
89
+ when 'i386'
90
+ 'ia32'
91
+ else
92
+ raise "Unsupported Arch: #{Sass::Platform::ARCH}"
93
+ end
94
+
95
+ ext = case os
96
+ when 'windows'
97
+ 'zip'
98
+ else
99
+ 'tar.gz'
100
+ end
101
+
102
+ "https://github.com/#{repo}/releases/download/#{tag_name}/sass_embedded-#{tag_name}-#{os}-#{arch}.#{ext}"
103
+ end
104
+
105
+ def latest_protoc
106
+ repo = 'protocolbuffers/protobuf'
107
+
108
+ tag_name = latest_release repo
109
+
110
+ os = case Sass::Platform::OS
111
+ when 'darwin'
112
+ 'osx'
113
+ when 'linux'
114
+ 'linux'
115
+ when 'windows'
116
+ 'win'
117
+ else
118
+ raise "Unsupported OS: #{Sass::Platform::OS}"
119
+ end
120
+
121
+ arch = case Sass::Platform::ARCH
122
+ when 'aarch64'
123
+ 'aarch_64'
124
+ when 'sparcv9'
125
+ 's390'
126
+ when 'i386'
127
+ 'x86_32'
128
+ when 'x86_64'
129
+ 'x86_64'
130
+ when 'powerpc64'
131
+ 'ppcle_64'
132
+ else
133
+ raise "Unsupported Arch: #{Sass::Platform::ARCH}"
134
+ end
135
+
136
+ os_arch = case os
137
+ when 'win'
138
+ os + arch.split('_').last
139
+ else
140
+ "#{os}-#{arch}"
141
+ end
142
+
143
+ ext = 'zip'
144
+
145
+ "https://github.com/#{repo}/releases/download/#{tag_name}/protoc-#{tag_name[1..]}-#{os_arch}.#{ext}"
146
+ end
147
+
148
+ def latest_sass_embedded_protocol
149
+ repo = 'sass/embedded-protocol'
150
+
151
+ # TODO: use latest release once available
152
+ # tag_name = latest_release repo
153
+ tag_name = 'HEAD'
154
+
155
+ "https://raw.githubusercontent.com/#{repo}/#{tag_name}/embedded_sass.proto"
156
+ end
157
+ end
158
+ end
159
+
160
+ Sass::Extconf.new
data/lib/sass.rb CHANGED
@@ -1,36 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The Sass module
3
4
  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
5
+ class << self
6
+ # The global include_paths for Sass files. This is meant for plugins and
7
+ # 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.
11
+ #
12
+ # If the `SASS_PATH` environment variable is set,
13
+ # the initial value of `include_paths` will be initialized based on that.
14
+ # The variable should be a colon-separated list of path names
15
+ # (semicolon-separated on Windows).
16
+ #
17
+ # @example
18
+ # Sass.include_paths << File.dirname(__FILE__) + '/sass'
19
+ # @return [Array<String, Pathname>]
20
+ def include_paths
21
+ @include_paths ||= if ENV['SASS_PATH']
22
+ ENV['SASS_PATH'].split(File::PATH_SEPARATOR)
23
+ else
24
+ []
25
+ end
26
+ end
27
+
28
+ def info
29
+ embedded.info
30
+ end
31
+
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}.
36
+ # @example
37
+ # Sass.render(options)
38
+ # @return [Hash]
39
+ def render(**kwargs)
40
+ embedded.render(**kwargs)
41
+ end
42
+
43
+ private
44
+
45
+ def embedded
46
+ return @embedded if defined?(@embedded) && !@embedded.closed?
25
47
 
26
- def self.render(options)
27
- unless defined? @embedded
28
48
  @embedded = Sass::Embedded.new
29
- at_exit do
30
- @embedded.close
31
- end
32
49
  end
33
- @embedded.render options
34
50
  end
35
51
  end
36
52
 
@@ -39,4 +55,5 @@ require_relative 'sass/error'
39
55
  require_relative 'sass/platform'
40
56
  require_relative 'sass/util'
41
57
  require_relative 'sass/transport'
58
+ require_relative 'sass/context'
42
59
  require_relative 'sass/embedded'
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sass
4
+ # An abstract context for maintaining state and observing transport
5
+ class Context
6
+ def initialize(transport, id)
7
+ raise NotImplementedError if instance_of? Context
8
+
9
+ @transport = transport
10
+ @id = id
11
+ @mutex = Mutex.new
12
+ @condition_variable = ConditionVariable.new
13
+ @response = nil
14
+ @error = nil
15
+ @transport.add_observer self
16
+ end
17
+
18
+ def fetch
19
+ @mutex.synchronize do
20
+ @condition_variable.wait(@mutex) if @error.nil? && @response.nil?
21
+ end
22
+
23
+ raise @error unless @error.nil?
24
+
25
+ @response
26
+ end
27
+
28
+ def update(error, message)
29
+ @transport.delete_observer self
30
+ @mutex.synchronize do
31
+ @error = error
32
+ @response = message
33
+ @condition_variable.broadcast
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/sass/embedded.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+ require 'base64'
5
+
3
6
  module Sass
7
+ # The interface for using dart-sass-embedded
4
8
  class Embedded
5
9
  def initialize
6
10
  @transport = Transport.new
@@ -8,198 +12,85 @@ module Sass
8
12
  @id = 0
9
13
  end
10
14
 
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
65
-
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
95
- 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
15
+ def info
16
+ @info ||= InfoContext.new(@transport, next_id).fetch
17
+ end
153
18
 
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
19
+ def render(data: nil,
20
+ file: nil,
21
+ indented_syntax: false,
22
+ include_paths: [],
23
+ output_style: :expanded,
24
+ # precision: 5,
25
+ indent_type: :space,
26
+ indent_width: 2,
27
+ linefeed: :lf,
28
+ # source_comments: false,
29
+ source_map: false,
30
+ out_file: nil,
31
+ omit_source_map_url: false,
32
+ # source_map_contents: false,
33
+ source_map_embed: false,
34
+ source_map_root: '',
35
+ functions: {},
36
+ importer: [])
37
+ start = Util.now
162
38
 
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
39
+ indent_type = parse_indent_type(indent_type)
40
+ indent_width = parse_indent_width(indent_width)
41
+ linefeed = parse_linefeed(linefeed)
176
42
 
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
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
184
53
 
185
54
  if response.failure
186
- raise Sass::CompilationError.new(
55
+ raise RenderError.new(
187
56
  response.failure.message,
188
57
  response.failure.formatted,
189
- response.failure.span ? response.failure.span.url : nil,
58
+ if response.failure.span.nil?
59
+ nil
60
+ elsif response.failure.span.url == ''
61
+ 'stdin'
62
+ else
63
+ Util.path(response.failure.span.url)
64
+ end,
190
65
  response.failure.span ? response.failure.span.start.line + 1 : nil,
191
66
  response.failure.span ? response.failure.span.start.column + 1 : nil,
192
67
  1
193
68
  )
194
69
  end
195
70
 
196
- finish = Sass::Util.now
71
+ map, source_map = post_process_map(map: response.success.source_map,
72
+ file: file,
73
+ out_file: out_file,
74
+ source_map: source_map,
75
+ source_map_root: source_map_root)
76
+
77
+ css = post_process_css(css: response.success.css,
78
+ indent_type: indent_type,
79
+ indent_width: indent_width,
80
+ linefeed: linefeed,
81
+ map: map,
82
+ out_file: out_file,
83
+ omit_source_map_url: omit_source_map_url,
84
+ source_map: source_map,
85
+ source_map_embed: source_map_embed)
86
+
87
+ finish = Util.now
197
88
 
198
89
  {
199
- css: response.success.css,
200
- map: response.success.source_map,
90
+ css: css,
91
+ map: map,
201
92
  stats: {
202
- entry: options[:file] || 'data',
93
+ entry: file.nil? ? 'data' : file,
203
94
  start: start,
204
95
  end: finish,
205
96
  duration: finish - start
@@ -209,21 +100,116 @@ module Sass
209
100
 
210
101
  def close
211
102
  @transport.close
212
- nil
103
+ end
104
+
105
+ def closed?
106
+ @transport.closed?
213
107
  end
214
108
 
215
109
  private
216
110
 
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
- }
111
+ def post_process_map(map:,
112
+ file:,
113
+ out_file:,
114
+ source_map:,
115
+ source_map_root:)
116
+ return if map.nil? || map.empty?
117
+
118
+ map_data = JSON.parse(map)
119
+
120
+ map_data['sourceRoot'] = source_map_root
121
+
122
+ source_map_path = if source_map.is_a? String
123
+ source_map
124
+ else
125
+ "#{out_file}.map"
126
+ end
127
+
128
+ source_map_dir = File.dirname(source_map_path)
129
+
130
+ if out_file
131
+ map_data['file'] = Util.relative(source_map_dir, out_file)
132
+ elsif file
133
+ ext = File.extname(file)
134
+ map_data['file'] = "#{file[0..(ext.empty? ? -1 : -ext.length - 1)]}.css"
135
+ else
136
+ map_data['file'] = 'stdin.css'
137
+ end
138
+
139
+ map_data['sources'].map! do |source|
140
+ if source.start_with? Util::FILE_PROTOCOL
141
+ Util.relative(source_map_dir, Util.path(source))
142
+ else
143
+ source
144
+ end
145
+ end
146
+
147
+ [-JSON.generate(map_data), source_map_path]
148
+ end
149
+
150
+ def post_process_css(css:,
151
+ indent_type:,
152
+ indent_width:,
153
+ linefeed:,
154
+ map:,
155
+ omit_source_map_url:,
156
+ out_file:,
157
+ source_map:,
158
+ source_map_embed:)
159
+ css = +css
160
+ if indent_width != 2 || indent_type.to_sym != :space
161
+ indent = indent_type * indent_width
162
+ css.gsub!(/^ +/) do |space|
163
+ indent * (space.length / 2)
164
+ end
165
+ end
166
+ css.gsub!("\n", linefeed) if linefeed != "\n"
167
+
168
+ unless map.nil? || omit_source_map_url == true
169
+ url = if source_map_embed
170
+ "data:application/json;base64,#{Base64.strict_encode64(map)}"
171
+ elsif out_file
172
+ Util.relative(File.dirname(out_file), source_map)
173
+ else
174
+ source_map
175
+ end
176
+ css += "#{linefeed}/*# sourceMappingURL=#{url} */"
177
+ end
178
+
179
+ -css
180
+ end
181
+
182
+ def parse_indent_type(indent_type)
183
+ case indent_type.to_sym
184
+ when :space
185
+ ' '
186
+ when :tab
187
+ "\t"
188
+ else
189
+ raise ArgumentError, 'indent_type must be :space or :tab'
190
+ end
191
+ end
192
+
193
+ def parse_indent_width(indent_width)
194
+ raise ArgumentError, 'indent_width must be an integer' unless indent_width.is_a? Integer
195
+ raise RangeError, 'indent_width must be in between 0 and 10 (inclusive)' unless indent_width.between? 0, 10
196
+
197
+ indent_width
198
+ end
199
+
200
+ def parse_linefeed(linefeed)
201
+ case linefeed.to_sym
202
+ when :lf
203
+ "\n"
204
+ when :lfcr
205
+ "\n\r"
206
+ when :cr
207
+ "\r"
208
+ when :crlf
209
+ "\r\n"
210
+ else
211
+ raise ArgumentError, 'linefeed must be one of :lf, :lfcr, :cr, :crlf'
212
+ end
227
213
  end
228
214
 
229
215
  def next_id
@@ -233,5 +219,276 @@ module Sass
233
219
  @id
234
220
  end
235
221
  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
236
493
  end
237
494
  end