summaryrefslogtreecommitdiff
path: root/vendor/bundle/ruby/3.4.0/gems/jekyll-sass-converter-3.1.0/lib/jekyll/converters/scss.rb
blob: 12132a335ca0b279352c3f6b4b823568ee0790ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# frozen_string_literal: true

# stdlib
require "json"

# 3rd party
require "addressable/uri"
require "sass-embedded"

# internal
require_relative "../source_map_page"

module Jekyll
  module Converters
    class Scss < Converter
      EXTENSION_PATTERN = %r!^\.scss$!i

      SyntaxError = Class.new(ArgumentError)

      safe true
      priority :low

      # This hook is triggered just before the method {#convert(content)} is executed, it
      # associates the Scss (and Sass) converters with their respective sass_page objects.
      Jekyll::Hooks.register :pages, :pre_render do |page|
        next unless page.is_a?(Jekyll::Page)

        page.converters.each do |converter|
          converter.associate_page(page) if converter.is_a?(Jekyll::Converters::Scss)
        end
      end

      # This hook is triggered just after the method {#convert(content)} has been executed, it
      # dissociates the Scss (and Sass) converters with their respective sass_page objects.
      Jekyll::Hooks.register :pages, :post_render do |page|
        next unless page.is_a?(Jekyll::Page)

        page.converters.each do |converter|
          converter.dissociate_page(page) if converter.is_a?(Jekyll::Converters::Scss)
        end
      end

      ALLOWED_STYLES = %w(expanded compressed).freeze

      # Associate this Converter with the "page" object that manages input and output files for
      # this converter.
      #
      # Note: changing the associated sass_page during the live time of this Converter instance
      # may result in inconsistent results.
      #
      # @param [Jekyll:Page] page The sass_page for which this object acts as converter.
      def associate_page(page)
        if @sass_page
          Jekyll.logger.debug "Sass Converter:",
                              "sass_page re-assigned: #{@sass_page.name} to #{page.name}"
          dissociate_page(page)
          return
        end
        @sass_page = page
      end

      # Dissociate this Converter with the "page" object.
      #
      # @param [Jekyll:Page] page The sass_page for which this object has acted as a converter.
      def dissociate_page(page)
        unless page.equal?(@sass_page)
          Jekyll.logger.debug "Sass Converter:",
                              "dissociating a page that was never associated #{page.name}"
        end

        @source_map_page = nil
        @sass_page = nil
        @site = nil
      end

      def matches(ext)
        ext =~ self.class::EXTENSION_PATTERN
      end

      def output_ext(_ext)
        ".css"
      end

      def safe?
        !!@config["safe"]
      end

      def jekyll_sass_configuration
        @jekyll_sass_configuration ||= begin
          options = @config["sass"] || {}
          unless options["style"].nil?
            options["style"] = options["style"].to_s.delete_prefix(":").to_sym
          end
          options
        end
      end

      def syntax
        :scss
      end

      def sass_dir
        return "_sass" if jekyll_sass_configuration["sass_dir"].to_s.empty?

        jekyll_sass_configuration["sass_dir"]
      end

      def sass_style
        style = jekyll_sass_configuration["style"]
        ALLOWED_STYLES.include?(style.to_s) ? style.to_sym : :expanded
      end

      def user_sass_load_paths
        Array(jekyll_sass_configuration["load_paths"])
      end

      def sass_dir_relative_to_site_source
        @sass_dir_relative_to_site_source ||=
          Jekyll.sanitized_path(site_source, sass_dir).delete_prefix("#{site.source}/")
      end

      # rubocop:disable Metrics/AbcSize
      def sass_load_paths
        paths = user_sass_load_paths + [sass_dir_relative_to_site_source]

        # Sanitize paths to prevent any attack vectors (.e.g. `/**/*`)
        paths.map! { |path| Jekyll.sanitized_path(site_source, path) } if safe?

        # Expand file globs (e.g. `node_modules/*/node_modules` )
        Dir.chdir(site_source) do
          paths = paths.flat_map { |path| Dir.glob(path) }

          paths.map! do |path|
            # Sanitize again in case globbing was able to do something crazy.
            safe? ? Jekyll.sanitized_path(site_source, path) : File.expand_path(path)
          end
        end

        paths.uniq!
        paths << site.theme.sass_path if site.theme&.sass_path
        paths.select { |path| File.directory?(path) }
      end
      # rubocop:enable Metrics/AbcSize

      def sass_configs
        {
          :load_paths                 => sass_load_paths,
          :charset                    => !associate_page_failed?,
          :source_map                 => sourcemap_required?,
          :source_map_include_sources => true,
          :style                      => sass_style,
          :syntax                     => syntax,
          :url                        => sass_file_url,
          :quiet_deps                 => quiet_deps_option,
          :verbose                    => verbose_option,
          :fatal_deprecations         => fatal_deprecations,
          :future_deprecations        => future_deprecations,
          :silence_deprecations       => silence_deprecations,
        }
      end

      def convert(content)
        output = ::Sass.compile_string(content, **sass_configs)
        result = output.css

        if sourcemap_required?
          source_map = process_source_map(output.source_map)
          generate_source_map_page(source_map)

          if (sm_url = source_mapping_url)
            result += "#{sass_style == :compressed ? "" : "\n\n"}/*# sourceMappingURL=#{sm_url} */"
          end
        end

        result
      rescue ::Sass::CompileError => e
        Jekyll.logger.error e.full_message
        if livereload?
          e.to_css # Render error message in browser window
        else
          raise SyntaxError, e.message
        end
      end

      private

      # The Page instance for which this object acts as a converter.
      attr_reader :sass_page

      def associate_page_failed?
        !sass_page
      end

      # Returns `true` if jekyll is serving with livereload.
      def livereload?
        !!(@config["serving"] && @config["livereload"])
      end

      # The URL of the input scss (or sass) file. This information will be used for error reporting.
      def sass_file_url
        return if associate_page_failed?

        file_url_from_path(Jekyll.sanitized_path(site_source, sass_page.relative_path))
      end

      # The value of the `sourcemap` option chosen by the user.
      #
      # This option controls when sourcemaps shall be generated or not.
      #
      # Returns the value of the `sourcemap`-option chosen by the user or ':always' by default.
      def sourcemap_option
        jekyll_sass_configuration.fetch("sourcemap", :always).to_sym
      end

      # Determines whether a sourcemap shall be generated or not.
      #
      # Returns `true` if a sourcemap shall be generated, `false` otherwise.
      def sourcemap_required?
        return false if associate_page_failed? || sourcemap_option == :never
        return true  if sourcemap_option == :always

        !(sourcemap_option == :development && Jekyll.env != "development")
      end

      def source_map_page
        return if associate_page_failed?

        @source_map_page ||= SourceMapPage.new(sass_page)
      end

      # Returns the directory that source map sources are relative to.
      def sass_source_root
        if associate_page_failed?
          site_source
        else
          Jekyll.sanitized_path(site_source, File.dirname(sass_page.relative_path))
        end
      end

      # Converts file urls in source map to relative paths.
      #
      # Returns processed source map string.
      def process_source_map(source_map)
        map_data = JSON.parse(source_map)
        unless associate_page_failed?
          map_data["file"] = Addressable::URI.encode("#{sass_page.basename}.css")
        end
        source_root_url = Addressable::URI.parse(file_url_from_path("#{sass_source_root}/"))
        map_data["sources"].map! do |s|
          s.start_with?("file:") ? Addressable::URI.parse(s).route_from(source_root_url).to_s : s
        end
        JSON.generate(map_data)
      end

      # Adds the source-map to the source-map-page and adds it to `site.pages`.
      def generate_source_map_page(source_map)
        return if associate_page_failed?

        source_map_page.source_map(source_map)
        site.pages << source_map_page
      end

      # Returns a source mapping url for given source-map.
      def source_mapping_url
        return if associate_page_failed?

        Addressable::URI.encode("#{sass_page.basename}.css.map")
      end

      def site
        associate_page_failed? ? Jekyll.sites.last : sass_page.site
      end

      def site_source
        site.source
      end

      def file_url_from_path(path)
        Addressable::URI.encode("file://#{path.start_with?("/") ? "" : "/"}#{path}")
      end

      # Returns the value of the `quiet_deps`-option chosen by the user or 'false' by default.
      def quiet_deps_option
        !!jekyll_sass_configuration.fetch("quiet_deps", false)
      end

      # Returns the value of the `verbose`-option chosen by the user or 'false' by default.
      def verbose_option
        !!jekyll_sass_configuration.fetch("verbose", false)
      end

      # Returns the value of the `fatal_deprecations`-option or '[]' by default.
      def fatal_deprecations
        Array(jekyll_sass_configuration["fatal_deprecations"])
      end

      # Returns the value of the `future_deprecations`-option or '[]' by default.
      def future_deprecations
        Array(jekyll_sass_configuration["future_deprecations"])
      end

      # Returns the value of the `silence_deprecations`-option or '[]' by default.
      def silence_deprecations
        Array(jekyll_sass_configuration["silence_deprecations"])
      end
    end
  end
end