sass-embedded 0.2.1 → 0.4.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: edeb1d7a1a3ba0d1485671e579478b4b1bc5d372350a25869cd4b0305bd2f356
4
- data.tar.gz: 9eda21b2bdb2d7f278d03c9e5091eeec13956d9e0216ebb4a415988f8ac90b62
3
+ metadata.gz: c9afb73da6288ecf0360685cc9ddb4c072eb8840da50acaa8a52b67f09998e6c
4
+ data.tar.gz: 1a20d885e2e9f55ffd861f9428499f4c3bdea64a4aea81e44f356af8be69eccc
5
5
  SHA512:
6
- metadata.gz: 2cc608b5597a0c820633f1dc386de5e9125b00ed99004c5237e0223d5a181d4782da4b2025983f86fad47409ef738e1a5954e810f35e9f443dd319035b2aa4dc
7
- data.tar.gz: a45ad64a3670c8d6f2136a287773eed2f77abaeb6911173931af8570fa1edeeed53a747ebc747288dc5b1941c08eb9ddc5c410ede3c536c380dd3297e0bf8092
6
+ metadata.gz: 679cfc5b502de2fdc5b33c8117b7e1d68f94acd62c7afbe04ebb17f9bceacdc677e8b35197cf239f92fed4e3d523468d71798d899c360d8402be127bdb50ac48
7
+ data.tar.gz: e1d698b3980914299ee2ce570bd1f109a875859c147f8930366df8e2dabcae94f0e0795339f6614f3f7828e365c191ba24554e30869e4bd106f565f1cbbb9af2
@@ -27,7 +27,7 @@ jobs:
27
27
  bundler-cache: true
28
28
 
29
29
  - name: Download dart-sass-embedded
30
- run: bundle exec rake download
30
+ run: bundle exec rake extconf
31
31
  env:
32
32
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33
33
 
@@ -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
@@ -1,5 +1,7 @@
1
1
  # Embedded Sass Host for Ruby
2
2
 
3
+ [![build](https://github.com/ntkme/embedded-host-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ntkme/embedded-host-ruby/actions/workflows/build.yml)
4
+
3
5
  This is a Ruby library that implements the host side of the [Embedded Sass protocol](https://github.com/sass/sass-embedded-protocol).
4
6
 
5
7
  It exposes a Ruby API for Sass that's backed by a native [Dart Sass](https://sass-lang.com/dart-sass) executable.
@@ -9,9 +11,7 @@ It exposes a Ruby API for Sass that's backed by a native [Dart Sass](https://sas
9
11
  ``` ruby
10
12
  require "sass"
11
13
 
12
- Sass.render({
13
- file: "style.scss"
14
- })
14
+ Sass.render(file: "style.scss")
15
15
  ```
16
16
 
17
17
  ---
data/Rakefile CHANGED
@@ -2,15 +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
- task :download do
9
- require_relative 'ext/sass_embedded/extconf'
8
+ task :extconf do
9
+ system('make', '-C', 'ext')
10
10
  end
11
11
 
12
12
  desc 'Run all tests'
13
- task :test do
13
+ task test: :extconf do
14
14
  $LOAD_PATH.unshift('lib', 'test')
15
15
  Dir.glob('./test/**/*_test.rb').sort.each { |f| require f }
16
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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The Sass module
3
4
  module Sass
4
5
  # The global include_paths for Sass files. This is meant for plugins and
5
6
  # libraries to register the paths to their Sass stylesheets to that they may
@@ -13,7 +14,7 @@ module Sass
13
14
  # (semicolon-separated on Windows).
14
15
  #
15
16
  # @example
16
- # Sass.include_paths << File.dirname(__FILE__ + '/sass')
17
+ # Sass.include_paths << File.dirname(__FILE__) + '/sass'
17
18
  # @return [Array<String, Pathname>]
18
19
  def self.include_paths
19
20
  @include_paths ||= if ENV['SASS_PATH']
@@ -23,14 +24,21 @@ module Sass
23
24
  end
24
25
  end
25
26
 
26
- def self.render(options)
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)
27
35
  unless defined? @embedded
28
36
  @embedded = Sass::Embedded.new
29
37
  at_exit do
30
38
  @embedded.close
31
39
  end
32
40
  end
33
- @embedded.render options
41
+ @embedded.render(**kwargs)
34
42
  end
35
43
  end
36
44
 
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,107 @@ 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]
15
+ def info
16
+ @transport.send EmbeddedProtocol::InboundMessage::VersionRequest.new(
17
+ id: next_id
18
+ )
19
+ end
25
20
 
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
21
+ def render(data: nil,
22
+ file: nil,
23
+ indented_syntax: false,
24
+ include_paths: [],
25
+ output_style: :expanded,
26
+ # precision: 5,
27
+ indent_type: :space,
28
+ indent_width: 2,
29
+ linefeed: :lf,
30
+ # source_comments: false,
31
+ source_map: false,
32
+ out_file: nil,
33
+ omit_source_map_url: false,
34
+ # source_map_contents: false,
35
+ source_map_embed: false,
36
+ source_map_root: '',
37
+ functions: {},
38
+ importer: [])
39
+ start = Util.now
36
40
 
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
41
+ indent_type = parse_indent_type(indent_type)
42
+ indent_width = parse_indent_width(indent_width)
43
+ linefeed = parse_linefeed(linefeed)
65
44
 
66
45
  compilation_id = next_id
67
46
 
68
- compile_request = Sass::EmbeddedProtocol::InboundMessage::CompileRequest.new(
69
- id: compilation_id,
70
- string: string,
71
- path: path,
72
- style: style,
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,
73
53
  source_map: source_map,
74
- importers: importers,
75
- global_functions: options[:functions] ? signatures : [],
76
- alert_color: true,
77
- alert_ascii: true
54
+ out_file: out_file,
55
+ functions: functions,
56
+ importer: importer
78
57
  )
79
58
 
80
- response = @transport.send compile_request, compilation_id
81
-
82
- file = options[:file] || 'stdin'
83
- canonicalizations = {}
84
- imports = {}
59
+ response = @transport.send renderer.compile_request(compilation_id), compilation_id
85
60
 
86
61
  loop do
87
62
  case response
88
- when Sass::EmbeddedProtocol::OutboundMessage::CompileResponse
63
+ when EmbeddedProtocol::OutboundMessage::CompileResponse
89
64
  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
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
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
180
73
  else
181
- raise Sass::ProtocolError, "Unexpected packet received: #{response}"
74
+ raise ProtocolError, "Unexpected packet received: #{response}"
182
75
  end
183
76
  end
184
77
 
185
78
  if response.failure
186
- raise Sass::CompilationError.new(
79
+ raise RenderError.new(
187
80
  response.failure.message,
188
81
  response.failure.formatted,
189
- response.failure.span ? response.failure.span.url : nil,
82
+ if response.failure.span&.url == ''
83
+ 'stdin'
84
+ else
85
+ Util.path(response.failure.span.url)
86
+ end,
190
87
  response.failure.span ? response.failure.span.start.line + 1 : nil,
191
88
  response.failure.span ? response.failure.span.start.column + 1 : nil,
192
89
  1
193
90
  )
194
91
  end
195
92
 
196
- finish = Sass::Util.now
93
+ map, source_map = post_process_map(map: response.success.source_map,
94
+ file: file,
95
+ out_file: out_file,
96
+ source_map: source_map,
97
+ source_map_root: source_map_root)
98
+
99
+ css = post_process_css(css: response.success.css,
100
+ indent_type: indent_type,
101
+ indent_width: indent_width,
102
+ linefeed: linefeed,
103
+ map: map,
104
+ out_file: out_file,
105
+ omit_source_map_url: omit_source_map_url,
106
+ source_map: source_map,
107
+ source_map_embed: source_map_embed)
108
+
109
+ finish = Util.now
197
110
 
198
111
  {
199
- css: response.success.css,
200
- map: response.success.source_map,
112
+ css: css,
113
+ map: map,
201
114
  stats: {
202
- entry: options[:file] || 'data',
115
+ entry: file.nil? ? 'data' : file,
203
116
  start: start,
204
117
  end: finish,
205
118
  duration: finish - start
@@ -209,20 +122,113 @@ module Sass
209
122
 
210
123
  def close
211
124
  @transport.close
125
+ nil
212
126
  end
213
127
 
214
128
  private
215
129
 
216
- def info
217
- version_response = @transport.send Sass::EmbeddedProtocol::InboundMessage::VersionRequest.new(
218
- id: next_id
219
- )
220
- {
221
- compiler_version: version_response.compiler_version,
222
- protocol_version: version_response.protocol_version,
223
- implementation_name: version_response.implementation_name,
224
- implementation_version: version_response.implementation_version
225
- }
130
+ def post_process_map(map:,
131
+ file:,
132
+ out_file:,
133
+ source_map:,
134
+ source_map_root:)
135
+ return if map.nil? || map.empty?
136
+
137
+ map_data = JSON.parse(map)
138
+
139
+ map_data['sourceRoot'] = source_map_root
140
+
141
+ source_map_path = if source_map.is_a? String
142
+ source_map
143
+ else
144
+ "#{out_file}.map"
145
+ end
146
+
147
+ source_map_dir = File.dirname(source_map_path)
148
+
149
+ if out_file
150
+ map_data['file'] = Util.relative(source_map_dir, out_file)
151
+ elsif file
152
+ ext = File.extname(file)
153
+ map_data['file'] = "#{file[0..(ext.empty? ? -1 : -ext.length - 1)]}.css"
154
+ else
155
+ map_data['file'] = 'stdin.css'
156
+ end
157
+
158
+ map_data['sources'].map! do |source|
159
+ if source.start_with? Util::FILE_PROTOCOL
160
+ Util.relative(source_map_dir, Util.path(source))
161
+ else
162
+ source
163
+ end
164
+ end
165
+
166
+ [-JSON.generate(map_data), source_map_path]
167
+ end
168
+
169
+ def post_process_css(css:,
170
+ indent_type:,
171
+ indent_width:,
172
+ linefeed:,
173
+ map:,
174
+ omit_source_map_url:,
175
+ out_file:,
176
+ source_map:,
177
+ source_map_embed:)
178
+ css = +css
179
+ if indent_width != 2 || indent_type.to_sym != :space
180
+ indent = indent_type * indent_width
181
+ css.gsub!(/^ +/) do |space|
182
+ indent * (space.length / 2)
183
+ end
184
+ end
185
+ css.gsub!("\n", linefeed) if linefeed != "\n"
186
+
187
+ unless map.nil? || omit_source_map_url == true
188
+ url = if source_map_embed
189
+ "data:application/json;base64,#{Base64.strict_encode64(map)}"
190
+ elsif out_file
191
+ Util.relative(File.dirname(out_file), source_map)
192
+ else
193
+ source_map
194
+ end
195
+ css += "#{linefeed}/*# sourceMappingURL=#{url} */"
196
+ end
197
+
198
+ -css
199
+ end
200
+
201
+ def parse_indent_type(indent_type)
202
+ case indent_type.to_sym
203
+ when :space
204
+ ' '
205
+ when :tab
206
+ "\t"
207
+ else
208
+ raise ArgumentError, 'indent_type must be :space or :tab'
209
+ end
210
+ end
211
+
212
+ def parse_indent_width(indent_width)
213
+ raise ArgumentError, 'indent_width must be an integer' unless indent_width.is_a? Integer
214
+ raise RangeError, 'indent_width must be in between 0 and 10 (inclusive)' unless indent_width.between? 0, 10
215
+
216
+ indent_width
217
+ end
218
+
219
+ def parse_linefeed(linefeed)
220
+ case linefeed.to_sym
221
+ when :lf
222
+ "\n"
223
+ when :lfcr
224
+ "\n\r"
225
+ when :cr
226
+ "\r"
227
+ when :crlf
228
+ "\r\n"
229
+ else
230
+ raise ArgumentError, 'linefeed must be one of :lf, :lfcr, :cr, :crlf'
231
+ end
226
232
  end
227
233
 
228
234
  def next_id
@@ -232,5 +238,199 @@ module Sass
232
238
  @id
233
239
  end
234
240
  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
235
435
  end
236
436
  end