File: simple_sequence.rb

package info (click to toggle)
ruby-sass 3.7.4-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,396 kB
  • sloc: ruby: 32,443; sh: 26; makefile: 25
file content (348 lines) | stat: -rw-r--r-- 13,812 bytes parent folder | download | duplicates (4)
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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
module Sass
  module Selector
    # A unseparated sequence of selectors
    # that all apply to a single element.
    # For example, `.foo#bar[attr=baz]` is a simple sequence
    # of the selectors `.foo`, `#bar`, and `[attr=baz]`.
    class SimpleSequence < AbstractSequence
      # The array of individual selectors.
      #
      # @return [Array<Simple>]
      attr_accessor :members

      # The extending selectors that caused this selector sequence to be
      # generated. For example:
      #
      #     a.foo { ... }
      #     b.bar {@extend a}
      #     c.baz {@extend b}
      #
      # The generated selector `b.foo.bar` has `{b.bar}` as its `sources` set,
      # and the generated selector `c.foo.bar.baz` has `{b.bar, c.baz}` as its
      # `sources` set.
      #
      # This is populated during the {Sequence#do_extend} process.
      #
      # @return {Set<Sequence>}
      attr_accessor :sources

      # This sequence source range.
      #
      # @return [Sass::Source::Range]
      attr_accessor :source_range

      # @see \{#subject?}
      attr_writer :subject

      # Returns the element or universal selector in this sequence,
      # if it exists.
      #
      # @return [Element, Universal, nil]
      def base
        @base ||= (members.first if members.first.is_a?(Element) || members.first.is_a?(Universal))
      end

      def pseudo_elements
        @pseudo_elements ||= members.select {|sel| sel.is_a?(Pseudo) && sel.type == :element}
      end

      def selector_pseudo_classes
        @selector_pseudo_classes ||= members.
          select {|sel| sel.is_a?(Pseudo) && sel.type == :class && sel.selector}.
          group_by {|sel| sel.normalized_name}
      end

      # Returns the non-base, non-pseudo-element selectors in this sequence.
      #
      # @return [Set<Simple>]
      def rest
        @rest ||= Set.new(members - [base] - pseudo_elements)
      end

      # Whether or not this compound selector is the subject of the parent
      # selector; that is, whether it is prepended with `$` and represents the
      # actual element that will be selected.
      #
      # @return [Boolean]
      def subject?
        @subject
      end

      # @param selectors [Array<Simple>] See \{#members}
      # @param subject [Boolean] See \{#subject?}
      # @param source_range [Sass::Source::Range]
      def initialize(selectors, subject, source_range = nil)
        @members = selectors
        @subject = subject
        @sources = Set.new
        @source_range = source_range
      end

      # Resolves the {Parent} selectors within this selector
      # by replacing them with the given parent selector,
      # handling commas appropriately.
      #
      # @param super_cseq [CommaSequence] The parent selector
      # @return [CommaSequence] This selector, with parent references resolved
      # @raise [Sass::SyntaxError] If a parent selector is invalid
      def resolve_parent_refs(super_cseq)
        resolved_members = @members.map do |sel|
          next sel unless sel.is_a?(Pseudo) && sel.selector
          sel.with_selector(sel.selector.resolve_parent_refs(super_cseq, false))
        end.flatten

        # Parent selector only appears as the first selector in the sequence
        unless (parent = resolved_members.first).is_a?(Parent)
          return CommaSequence.new([Sequence.new([SimpleSequence.new(resolved_members, subject?)])])
        end

        return super_cseq if @members.size == 1 && parent.suffix.nil?

        CommaSequence.new(super_cseq.members.map do |super_seq|
          members = super_seq.members.dup
          newline = members.pop if members.last == "\n"
          unless members.last.is_a?(SimpleSequence)
            raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" +
              super_seq.to_s + '"')
          end

          parent_sub = members.last.members
          unless parent.suffix.nil?
            parent_sub = parent_sub.dup
            parent_sub[-1] = parent_sub.last.dup
            case parent_sub.last
            when Sass::Selector::Class, Sass::Selector::Id, Sass::Selector::Placeholder
              parent_sub[-1] = parent_sub.last.class.new(parent_sub.last.name + parent.suffix)
            when Sass::Selector::Element
              parent_sub[-1] = parent_sub.last.class.new(
                parent_sub.last.name + parent.suffix,
                parent_sub.last.namespace)
            when Sass::Selector::Pseudo
              if parent_sub.last.arg || parent_sub.last.selector
                raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" +
                  super_seq.to_s + '"')
              end
              parent_sub[-1] = Sass::Selector::Pseudo.new(
                parent_sub.last.type,
                parent_sub.last.name + parent.suffix,
                nil, nil)
            else
              raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" +
                super_seq.to_s + '"')
            end
          end

          Sequence.new(members[0...-1] +
            [SimpleSequence.new(parent_sub + resolved_members[1..-1], subject?)] +
            [newline].compact)
          end)
      end

      # Non-destructively extends this selector with the extensions specified in a hash
      # (which should come from {Sass::Tree::Visitors::Cssize}).
      #
      # @param extends [{Selector::Simple =>
      #                  Sass::Tree::Visitors::Cssize::Extend}]
      #   The extensions to perform on this selector
      # @param parent_directives [Array<Sass::Tree::DirectiveNode>]
      #   The directives containing this selector.
      # @param seen [Set<Array<Selector::Simple>>]
      #   The set of simple sequences that are currently being replaced.
      # @param original [Boolean]
      #   Whether this is the original selector being extended, as opposed to
      #   the result of a previous extension that's being re-extended.
      # @return [Array<Sequence>] A list of selectors generated
      #   by extending this selector with `extends`.
      # @see CommaSequence#do_extend
      def do_extend(extends, parent_directives, replace, seen)
        seen_with_pseudo_selectors = seen.dup

        modified_original = false
        members = self.members.map do |sel|
          next sel unless sel.is_a?(Pseudo) && sel.selector
          next sel if seen.include?([sel])
          extended = sel.selector.do_extend(extends, parent_directives, replace, seen, false)
          next sel if extended == sel.selector
          extended.members.reject! {|seq| seq.invisible?}

          # For `:not()`, we usually want to get rid of any complex
          # selectors because that will cause the selector to fail to
          # parse on all browsers at time of writing. We can keep them
          # if either the original selector had a complex selector, or
          # the result of extending has only complex selectors,
          # because either way we aren't breaking anything that isn't
          # already broken.
          if sel.normalized_name == 'not' &&
              (sel.selector.members.none? {|seq| seq.members.length > 1} &&
               extended.members.any? {|seq| seq.members.length == 1})
            extended.members.reject! {|seq| seq.members.length > 1}
          end

          modified_original = true
          result = sel.with_selector(extended)
          result.each {|new_sel| seen_with_pseudo_selectors << [new_sel]}
          result
        end.flatten

        groups = extends[members.to_set].group_by {|ex| ex.extender}.to_a
        groups.map! do |seq, group|
          sels = group.map {|e| e.target}.flatten
          # If A {@extend B} and C {...},
          # seq is A, sels is B, and self is C

          self_without_sel = Sass::Util.array_minus(members, sels)
          group.each {|e| e.success = true}
          unified = seq.members.last.unify(SimpleSequence.new(self_without_sel, subject?))
          next unless unified
          group.each {|e| check_directives_match!(e, parent_directives)}
          new_seq = Sequence.new(seq.members[0...-1] + [unified])
          new_seq.add_sources!(sources + [seq])
          [sels, new_seq]
        end
        groups.compact!
        groups.map! do |sels, seq|
          next [] if seen.include?(sels)
          seq.do_extend(
            extends, parent_directives, false, seen_with_pseudo_selectors + [sels], false)
        end
        groups.flatten!

        if modified_original || !replace || groups.empty?
          # First Law of Extend: the result of extending a selector should
          # (almost) always contain the base selector.
          #
          # See https://github.com/nex3/sass/issues/324.
          original = Sequence.new([SimpleSequence.new(members, @subject, source_range)])
          original.add_sources! sources
          groups.unshift original
        end
        groups.uniq!
        groups
      end

      # Unifies this selector with another {SimpleSequence}, returning
      # another `SimpleSequence` that is a subselector of both input
      # selectors.
      #
      # @param other [SimpleSequence]
      # @return [SimpleSequence, nil] A {SimpleSequence} matching both `sels` and this selector,
      #   or `nil` if this is impossible (e.g. unifying `#foo` and `#bar`)
      # @raise [Sass::SyntaxError] If this selector cannot be unified.
      #   This will only ever occur when a dynamic selector,
      #   such as {Parent} or {Interpolation}, is used in unification.
      #   Since these selectors should be resolved
      #   by the time extension and unification happen,
      #   this exception will only ever be raised as a result of programmer error
      def unify(other)
        sseq = members.inject(other.members) do |member, sel|
          return unless member
          sel.unify(member)
        end
        return unless sseq
        SimpleSequence.new(sseq, other.subject? || subject?)
      end

      # Returns whether or not this selector matches all elements
      # that the given selector matches (as well as possibly more).
      #
      # @example
      #   (.foo).superselector?(.foo.bar) #=> true
      #   (.foo).superselector?(.bar) #=> false
      # @param their_sseq [SimpleSequence]
      # @param parents [Array<SimpleSequence, String>] The parent selectors of `their_sseq`, if any.
      # @return [Boolean]
      def superselector?(their_sseq, parents = [])
        return false unless base.nil? || base.eql?(their_sseq.base)
        return false unless pseudo_elements.eql?(their_sseq.pseudo_elements)
        our_spcs = selector_pseudo_classes
        their_spcs = their_sseq.selector_pseudo_classes

        # Some psuedo-selectors can be subselectors of non-pseudo selectors.
        # Pull those out here so we can efficiently check against them below.
        their_subselector_pseudos = %w(matches any nth-child nth-last-child).
          map {|name| their_spcs[name] || []}.flatten

        # If `self`'s non-pseudo simple selectors aren't a subset of `their_sseq`'s,
        # it's definitely not a superselector. This also considers being matched
        # by `:matches` or `:any`.
        return false unless rest.all? do |our_sel|
          next true if our_sel.is_a?(Pseudo) && our_sel.selector
          next true if their_sseq.rest.include?(our_sel)
          their_subselector_pseudos.any? do |their_pseudo|
            their_pseudo.selector.members.all? do |their_seq|
              next false unless their_seq.members.length == 1
              their_sseq = their_seq.members.first
              next false unless their_sseq.is_a?(SimpleSequence)
              their_sseq.rest.include?(our_sel)
            end
          end
        end

        our_spcs.all? do |_name, pseudos|
          pseudos.all? {|pseudo| pseudo.superselector?(their_sseq, parents)}
        end
      end

      # @see Simple#to_s
      def to_s(opts = {})
        res = @members.map {|m| m.to_s(opts)}.join

        # :not(%foo) may resolve to the empty string, but it should match every
        # selector so we replace it with "*".
        res = '*' if res.empty?

        res << '!' if subject?
        res
      end

      # Returns a string representation of the sequence.
      # This is basically the selector string.
      #
      # @return [String]
      def inspect
        res = members.map {|m| m.inspect}.join
        res << '!' if subject?
        res
      end

      # Return a copy of this simple sequence with `sources` merged into the
      # {SimpleSequence#sources} set.
      #
      # @param sources [Set<Sequence>]
      # @return [SimpleSequence]
      def with_more_sources(sources)
        sseq = dup
        sseq.members = members.dup
        sseq.sources = self.sources | sources
        sseq
      end

      private

      def check_directives_match!(extend, parent_directives)
        dirs1 = extend.directives.map {|d| d.resolved_value}
        dirs2 = parent_directives.map {|d| d.resolved_value}
        return if Sass::Util.subsequence?(dirs1, dirs2)
        line = extend.node.line
        filename = extend.node.filename

        # TODO(nweiz): this should use the Sass stack trace of the extend node,
        # not the selector.
        raise Sass::SyntaxError.new(<<MESSAGE)
You may not @extend an outer selector from within #{extend.directives.last.name}.
You may only @extend selectors within the same directive.
From "@extend #{extend.target.join(', ')}" on line #{line}#{" of #{filename}" if filename}.
MESSAGE
      end

      def _hash
        [base, rest.hash].hash
      end

      def _eql?(other)
        other.base.eql?(base) && other.pseudo_elements == pseudo_elements &&
          other.rest.eql?(rest) && other.subject? == subject?
      end
    end
  end
end