File: pseudo.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 (283 lines) | stat: -rw-r--r-- 11,581 bytes parent folder | download | duplicates (3)
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
# coding: utf-8
module Sass
  module Selector
    # A pseudoclass (e.g. `:visited`) or pseudoelement (e.g. `::first-line`)
    # selector. It can have arguments (e.g. `:nth-child(2n+1)`) which can
    # contain selectors (e.g. `:nth-child(2n+1 of .foo)`).
    class Pseudo < Simple
      # Some pseudo-class-syntax selectors are actually considered
      # pseudo-elements and must be treated differently. This is a list of such
      # selectors.
      #
      # @return [Set<String>]
      ACTUALLY_ELEMENTS = %w(after before first-line first-letter).to_set

      # Like \{#type}, but returns the type of selector this looks like, rather
      # than the type it is semantically. This only differs from type for
      # selectors in \{ACTUALLY\_ELEMENTS}.
      #
      # @return [Symbol]
      attr_reader :syntactic_type

      # The name of the selector.
      #
      # @return [String]
      attr_reader :name

      # The argument to the selector,
      # or `nil` if no argument was given.
      #
      # @return [String, nil]
      attr_reader :arg

      # The selector argument, or `nil` if no selector exists.
      #
      # If this and \{#arg\} are both set, \{#arg\} is considered a non-selector
      # prefix.
      #
      # @return [CommaSequence]
      attr_reader :selector

      # @param syntactic_type [Symbol] See \{#syntactic_type}
      # @param name [String] See \{#name}
      # @param arg [nil, String] See \{#arg}
      # @param selector [nil, CommaSequence] See \{#selector}
      def initialize(syntactic_type, name, arg, selector)
        @syntactic_type = syntactic_type
        @name = name
        @arg = arg
        @selector = selector
      end

      def unique?
        type == :class && normalized_name == 'root'
      end

      # Whether or not this selector should be hidden due to containing a
      # placeholder.
      def invisible?
        # :not() is a special case—if you eliminate all the placeholders from
        # it, it should match anything.
        name != 'not' && @selector && @selector.members.all? {|s| s.invisible?}
      end

      # Returns a copy of this with \{#selector} set to \{#new\_selector}.
      #
      # @param new_selector [CommaSequence]
      # @return [Array<Simple>]
      def with_selector(new_selector)
        result = Pseudo.new(syntactic_type, name, arg,
          CommaSequence.new(new_selector.members.map do |seq|
            next seq unless seq.members.length == 1
            sseq = seq.members.first
            next seq unless sseq.is_a?(SimpleSequence) && sseq.members.length == 1
            sel = sseq.members.first
            next seq unless sel.is_a?(Pseudo) && sel.selector

            case normalized_name
            when 'not'
              # In theory, if there's a nested :not its contents should be
              # unified with the return value. For example, if :not(.foo)
              # extends .bar, :not(.bar) should become .foo:not(.bar). However,
              # this is a narrow edge case and supporting it properly would make
              # this code and the code calling it a lot more complicated, so
              # it's not supported for now.
              next [] unless sel.normalized_name == 'matches'
              sel.selector.members
            when 'matches', 'any', 'current', 'nth-child', 'nth-last-child'
              # As above, we could theoretically support :not within :matches, but
              # doing so would require this method and its callers to handle much
              # more complex cases that likely aren't worth the pain.
              next [] unless sel.name == name && sel.arg == arg
              sel.selector.members
            when 'has', 'host', 'host-context', 'slotted'
              # We can't expand nested selectors here, because each layer adds an
              # additional layer of semantics. For example, `:has(:has(img))`
              # doesn't match `<div><img></div>` but `:has(img)` does.
              sel
            else
              []
            end
          end.flatten))

        # Older browsers support :not but only with a single complex selector.
        # In order to support those browsers, we break up the contents of a :not
        # unless it originally contained a selector list.
        return [result] unless normalized_name == 'not'
        return [result] if selector.members.length > 1
        result.selector.members.map do |seq|
          Pseudo.new(syntactic_type, name, arg, CommaSequence.new([seq]))
        end
      end

      # The type of the selector. `:class` if this is a pseudoclass selector,
      # `:element` if it's a pseudoelement.
      #
      # @return [Symbol]
      def type
        ACTUALLY_ELEMENTS.include?(normalized_name) ? :element : syntactic_type
      end

      # Like \{#name\}, but without any vendor prefix.
      #
      # @return [String]
      def normalized_name
        @normalized_name ||= name.gsub(/^-[a-zA-Z0-9]+-/, '')
      end

      # @see Selector#to_s
      def to_s(opts = {})
        # :not() is a special case, because :not(<nothing>) should match
        # everything.
        return '' if name == 'not' && @selector && @selector.members.all? {|m| m.invisible?}

        res = (syntactic_type == :class ? ":" : "::") + @name
        if @arg || @selector
          res << "("
          res << Sass::Util.strip_except_escapes(@arg) if @arg
          res << " " if @arg && @selector
          res << @selector.to_s(opts) if @selector
          res << ")"
        end
        res
      end

      # Returns `nil` if this is a pseudoelement selector
      # and `sels` contains a pseudoelement selector different than this one.
      #
      # @see SimpleSequence#unify
      def unify(sels)
        return if type == :element && sels.any? do |sel|
          sel.is_a?(Pseudo) && sel.type == :element &&
            (sel.name != name || sel.arg != arg || sel.selector != selector)
        end
        super
      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 = [])
        case normalized_name
        when 'matches', 'any'
          # :matches can be a superselector of another selector in one of two
          # ways. Either its constituent selectors can be a superset of those of
          # another :matches in the other selector, or any of its constituent
          # selectors can individually be a superselector of the other selector.
          (their_sseq.selector_pseudo_classes[normalized_name] || []).any? do |their_sel|
            next false unless their_sel.is_a?(Pseudo)
            next false unless their_sel.name == name
            selector.superselector?(their_sel.selector)
          end || selector.members.any? do |our_seq|
            their_seq = Sequence.new(parents + [their_sseq])
            our_seq.superselector?(their_seq)
          end
        when 'has', 'host', 'host-context', 'slotted'
          # Like :matches, :has (et al) can be a superselector of another
          # selector if its constituent selectors are a superset of those of
          # another :has in the other selector. However, the :matches other case
          # doesn't work, because :has refers to nested elements.
          (their_sseq.selector_pseudo_classes[normalized_name] || []).any? do |their_sel|
            next false unless their_sel.is_a?(Pseudo)
            next false unless their_sel.name == name
            selector.superselector?(their_sel.selector)
          end
        when 'not'
          selector.members.all? do |our_seq|
            their_sseq.members.any? do |their_sel|
              if their_sel.is_a?(Element) || their_sel.is_a?(Id)
                # `:not(a)` is a superselector of `h1` and `:not(#foo)` is a
                # superselector of `#bar`.
                our_sseq = our_seq.members.last
                next false unless our_sseq.is_a?(SimpleSequence)
                our_sseq.members.any? do |our_sel|
                  our_sel.class == their_sel.class && our_sel != their_sel
                end
              else
                next false unless their_sel.is_a?(Pseudo)
                next false unless their_sel.name == name
                # :not(X) is a superselector of :not(Y) exactly when Y is a
                # superselector of X.
                their_sel.selector.superselector?(CommaSequence.new([our_seq]))
              end
            end
          end
        when 'current'
          (their_sseq.selector_pseudo_classes['current'] || []).any? do |their_current|
            next false if their_current.name != name
            # Explicitly don't check for nested superselector relationships
            # here. :current(.foo) isn't always a superselector of
            # :current(.foo.bar), since it matches the *innermost* ancestor of
            # the current element that matches the selector. For example:
            #
            #     <div class="foo bar">
            #       <p class="foo">
            #         <span>current element</span>
            #       </p>
            #     </div>
            #
            # Here :current(.foo) would match the p element and *not* the div
            # element, whereas :current(.foo.bar) would match the div and not
            # the p.
            selector == their_current.selector
          end
        when 'nth-child', 'nth-last-child'
          their_sseq.members.any? do |their_sel|
            # This misses a few edge cases. For example, `:nth-child(n of X)`
            # is a superselector of `X`, and `:nth-child(2n of X)` is a
            # superselector of `:nth-child(4n of X)`. These seem rare enough
            # not to be worth worrying about, though.
            next false unless their_sel.is_a?(Pseudo)
            next false unless their_sel.name == name
            next false unless their_sel.arg == arg
            selector.superselector?(their_sel.selector)
          end
        else
          throw "[BUG] Unknown selector pseudo class #{name}"
        end
      end

      # @see AbstractSequence#specificity
      def specificity
        return 1 if type == :element
        return SPECIFICITY_BASE unless selector
        @specificity ||=
          if normalized_name == 'not'
            min = 0
            max = 0
            selector.members.each do |seq|
              spec = seq.specificity
              if spec.is_a?(Range)
                min = Sass::Util.max(spec.begin, min)
                max = Sass::Util.max(spec.end, max)
              else
                min = Sass::Util.max(spec, min)
                max = Sass::Util.max(spec, max)
              end
            end
            min == max ? max : (min..max)
          else
            min = 0
            max = 0
            selector.members.each do |seq|
              spec = seq.specificity
              if spec.is_a?(Range)
                min = Sass::Util.min(spec.begin, min)
                max = Sass::Util.max(spec.end, max)
              else
                min = Sass::Util.min(spec, min)
                max = Sass::Util.max(spec, max)
              end
            end
            min == max ? max : (min..max)
          end
      end
    end
  end
end