diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index 92c050e4..7a3fe1a0 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -225,14 +225,14 @@ module Capybara raise ArgumentError, "XPath expressions are not supported for the :class filter with CSS based selectors" end - if process_class || process_id - css_selectors = expr.split(',').map(&:rstrip) - expr = css_selectors.map do |sel| - sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if process_id + if process_id || process_class + expr = ::Capybara::Selector::CSS.split(expr).map do |sel| + sel += "##{::Capybara::Selector::CSS.escape(options[:id])}" if process_id sel += css_from_classes(Array(options[:class])) if process_class sel end.join(", ") end + expr end diff --git a/lib/capybara/selector/css.rb b/lib/capybara/selector/css.rb index 11a75a66..9893bdc7 100644 --- a/lib/capybara/selector/css.rb +++ b/lib/capybara/selector/css.rb @@ -16,12 +16,85 @@ module Capybara c =~ %r{[ -/:-~]} ? "\\#{c}" : format("\\%06x", c.ord) end + def self.split(css) + Splitter.new.split(css) + end + S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}' H = /[0-9a-fA-F]/ UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/ NONASCII = /[#{S}]/ ESCAPE = /#{UNICODE}|\\[ -~#{S}]/ - NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/ + NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/ + + class Splitter + def split(css) + selectors = [] + StringIO.open(css) do |str| + selector = "" + while (c = str.getc) + case c + when '[' + selector += parse_square(str) + when '(' + selector += parse_paren(str) + when '"', "'" + selector += parse_string(c, str) + when '\\' + selector += c + str.getc + when ',' + selectors << selector.strip + selector = "" + else + selector += c + end + end + selectors << selector.strip + end + selectors + end + + private + + def parse_square(strio) + parse_block('[', ']', strio) + end + + def parse_paren(strio) + parse_block('(', ')', strio) + end + + def parse_block(start, final, strio) + block = start + while (c = strio.getc) + case c + when final + return block + c + when '\\' + block += c + strio.getc + when '"', "'" + block += parse_string(c, strio) + else + block += c + end + end + raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found" + end + + def parse_string(quote, strio) + string = quote + while (c = strio.getc) + string += c + case c + when quote + return string + when '\\' + string += strio.getc + end + end + raise ArgumentError, 'Invalid CSS Selector - string end not found' + end + end end end end diff --git a/spec/css_splitter_spec.rb b/spec/css_splitter_spec.rb new file mode 100644 index 00000000..be749712 --- /dev/null +++ b/spec/css_splitter_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Capybara::Selector::CSS::Splitter do + let :splitter do + ::Capybara::Selector::CSS::Splitter.new + end + + context "split not needed" do + it "normal CSS selector" do + css = 'div[id="abc"]' + expect(splitter.split(css)).to eq [css] + end + + it "comma in strings" do + css = 'div[id="a,bc"]' + expect(splitter.split(css)).to eq [css] + end + + it "comma in pseudo-selector" do + css = 'div.class1:not(.class1, .class2)' + expect(splitter.split(css)).to eq [css] + end + end + + context "split needed" do + it "root level comma" do + css = 'div.class1, span, p.class2' + expect(splitter.split(css)).to eq ['div.class1', 'span', 'p.class2'] + end + + it "root level comma when quotes and pseudo selectors" do + css = 'div.class1[id="abc\\"def,ghi"]:not(.class3, .class4), span[id=\'a"c\\\'de\'], section, #abc\\,def' + expect(splitter.split(css)).to eq ['div.class1[id="abc\\"def,ghi"]:not(.class3, .class4)', 'span[id=\'a"c\\\'de\']', 'section', '#abc\\,def'] + end + end +end