From 63f2235e83fbc0695bc18f204a4d31cf1764d2a3 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 13:22:53 -0400 Subject: [PATCH 1/7] test: start ad-hoc test coverage for CSS selectors --- test/selectors_test.rb | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/selectors_test.rb diff --git a/test/selectors_test.rb b/test/selectors_test.rb new file mode 100644 index 0000000..e57220a --- /dev/null +++ b/test/selectors_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "test_helper" + +module SyntaxTree + module CSS + class SelectorsTest < Minitest::Spec + it "parses a simple class selector" do + actual = parse_selectors(".flex") + + assert_pattern do + actual => [Selectors::ClassSelector[value: { value: "flex" }]] + end + end + + it "parses a compound class selector" do + actual = parse_selectors(".flex.text-xl") + + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + subclasses: [ + Selectors::ClassSelector[value: { value: "flex" }], + Selectors::ClassSelector[value: { value: "text-xl" }] + ] + ] + ] + end + end + + it "parses a compound selector" do + actual = parse_selectors("div.flex") + + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + type: { value: { name: { value: "div" } } }, + subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], + pseudo_elements: [] + ] + ] + end + end + + it "parses a compound selector with a pseudo-element" do + actual = parse_selectors("div.flex::first-line") + + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + type: { value: { name: { value: "div" } } }, + subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], + pseudo_elements: [ + [ + Selectors::PseudoElementSelector[ + Selectors::PseudoClassSelector[ + value: { value: "first-line" } + ] + ], + [] + ] + ] + ] + ] + end + end + + private + + def parse_selectors(selectors) + css = selectors + " {}" + Parser.new(css).parse.rules.first.selectors + end + end + end +end From e942b9eb83285968d5c0f98f8c7b8be4f9096f64 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 11:38:07 -0400 Subject: [PATCH 2/7] inherit Selectors::CompoundSelector from Node and improve its formatting --- lib/syntax_tree/css/pretty_print.rb | 38 +++++++++++++++++++++++++++++ lib/syntax_tree/css/selectors.rb | 29 +++++++++++++++++++++- lib/syntax_tree/css/visitor.rb | 3 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lib/syntax_tree/css/pretty_print.rb b/lib/syntax_tree/css/pretty_print.rb index 8ae9256..0fe59db 100644 --- a/lib/syntax_tree/css/pretty_print.rb +++ b/lib/syntax_tree/css/pretty_print.rb @@ -421,6 +421,44 @@ def visit_wqname(node) end end + # Visit a Selectors::CompoundSelector node. + def visit_compound_selector(node) + token("compound-selector") do + q.breakable + q.pp(node.type) + + q.breakable + q.text("(subclasses") + + if node.subclasses.any? + q.nest(2) do + q.breakable + q.seplist(node.subclasses) { |subclass| q.pp(subclass) } + end + + q.breakable("") + end + + q.text(")") + + q.breakable("") + q.text("(pseudo-elements") + + if node.pseudo_elements.any? + q.nest(2) do + q.breakable + q.seplist(node.pseudo_elements) do |pseudo_element| + q.pp(pseudo_element) + end + end + + q.breakable("") + end + + q.text(")") + end + end + private def token(name) diff --git a/lib/syntax_tree/css/selectors.rb b/lib/syntax_tree/css/selectors.rb index bce5a37..ff68097 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -73,7 +73,34 @@ def deconstruct_keys(keys) Combinator = Struct.new(:value, keyword_init: true) ComplexSelector = Struct.new(:left, :combinator, :right, keyword_init: true) - CompoundSelector = Struct.new(:type, :subclasses, :pseudo_elements, keyword_init: true) + + class CompoundSelector < Node + attr_reader :type, :subclasses, :pseudo_elements + + def initialize(type:, subclasses:, pseudo_elements:) + @type = type + @subclasses = subclasses + @pseudo_elements = pseudo_elements + end + + def accept(visitor) + visitor.visit_compound_selector(self) + end + + def child_nodes + [type, subclasses, pseudo_elements].flatten + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + type: type, + subclasses: subclasses, + pseudo_elements: pseudo_elements + } + end + end # The ID of an element, e.g., #foo # https://www.w3.org/TR/selectors-4/#typedef-id-selector diff --git a/lib/syntax_tree/css/visitor.rb b/lib/syntax_tree/css/visitor.rb index 4114913..e651381 100644 --- a/lib/syntax_tree/css/visitor.rb +++ b/lib/syntax_tree/css/visitor.rb @@ -132,6 +132,9 @@ def visit_child_nodes(node) # Visit a Selectors::ClassSelector node. alias visit_class_selector visit_child_nodes + # Visit a Selectors::CompoundSelector node. + alias visit_compound_selector visit_child_nodes + # Visit a Selectors::IdSelector node. alias visit_id_selector visit_child_nodes From 1a50f58842e85dbe399d2ef22b8a3d603d862c85 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 13:08:40 -0400 Subject: [PATCH 3/7] inherit Selectors::ComplexSelector and ::Combinator from Node and improve their formatting --- lib/syntax_tree/css/pretty_print.rb | 22 ++++++++++++++ lib/syntax_tree/css/selectors.rb | 47 +++++++++++++++++++++++++++-- lib/syntax_tree/css/visitor.rb | 6 ++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/syntax_tree/css/pretty_print.rb b/lib/syntax_tree/css/pretty_print.rb index 0fe59db..ceb42ba 100644 --- a/lib/syntax_tree/css/pretty_print.rb +++ b/lib/syntax_tree/css/pretty_print.rb @@ -421,6 +421,28 @@ def visit_wqname(node) end end + # Visit a Selectors::Combinator node. + def visit_combinator(node) + token("combinator") do + q.breakable + q.pp(node.value) + end + end + + # Visit a Selectors::ComplexSelector node. + def visit_complex_selector(node) + token("complex-selector") do + q.breakable + q.pp(node.left) + + q.breakable + q.pp(node.combinator) + + q.breakable + q.pp(node.right) + end + end + # Visit a Selectors::CompoundSelector node. def visit_compound_selector(node) token("compound-selector") do diff --git a/lib/syntax_tree/css/selectors.rb b/lib/syntax_tree/css/selectors.rb index ff68097..de5e585 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -71,8 +71,51 @@ def deconstruct_keys(keys) end end - Combinator = Struct.new(:value, keyword_init: true) - ComplexSelector = Struct.new(:left, :combinator, :right, keyword_init: true) + class Combinator < Node + attr_reader :value + + def initialize(value:) + @value = value + end + + def accept(visitor) + visitor.visit_combinator(self) + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value } + end + end + + class ComplexSelector < Node + attr_reader :left, :combinator, :right + + def initialize(left:, combinator:, right:) + @left = left + @combinator = combinator + @right = right + end + + def accept(visitor) + visitor.visit_complex_selector(self) + end + + def child_nodes + [left, combinator, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, combinator: combinator, right: right } + end + end class CompoundSelector < Node attr_reader :type, :subclasses, :pseudo_elements diff --git a/lib/syntax_tree/css/visitor.rb b/lib/syntax_tree/css/visitor.rb index e651381..79ca353 100644 --- a/lib/syntax_tree/css/visitor.rb +++ b/lib/syntax_tree/css/visitor.rb @@ -132,6 +132,12 @@ def visit_child_nodes(node) # Visit a Selectors::ClassSelector node. alias visit_class_selector visit_child_nodes + # Visit a Selectors::Combinator node. + alias visit_combinator visit_child_nodes + + # Visit a Selectors::ComplexSelector node. + alias visit_complex_selector visit_child_nodes + # Visit a Selectors::CompoundSelector node. alias visit_compound_selector visit_child_nodes From 6a62cd4c3858c8d22239de792a5f5ba536e3afa4 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 13:17:25 -0400 Subject: [PATCH 4/7] fix: parse Selectors::ComplexSelectors properly - avoid clobbering the combinator method with a local variable - return a recursive tree of complex selectors Partial fix for #25 --- lib/syntax_tree/css/selectors.rb | 22 ++++++++-------------- test/selectors_test.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/syntax_tree/css/selectors.rb b/lib/syntax_tree/css/selectors.rb index de5e585..cdebec6 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -339,25 +339,19 @@ def relative_selector_list def complex_selector left = compound_selector - loop do - if (combinator = maybe { combinator }) - ComplexSelector.new(left: left, combinator: combinator, right: compound_selector) - elsif (right = maybe { compound_selector }) - ComplexSelector.new(left: left, combinator: nil, right: right) - else - break - end + if (c = maybe { combinator }) + ComplexSelector.new(left: left, combinator: c, right: complex_selector) + elsif (right = maybe { complex_selector }) + ComplexSelector.new(left: left, combinator: nil, right: right) + else + left end - - left end # = ? def relative_selector - combinator = maybe { combinator } - - if combinator - RelativeSelector.new(combinator: combinator, complex_selector: complex_selector) + if (c = maybe { combinator }) + RelativeSelector.new(combinator: c, complex_selector: complex_selector) else complex_selector end diff --git a/test/selectors_test.rb b/test/selectors_test.rb index e57220a..1a844f8 100644 --- a/test/selectors_test.rb +++ b/test/selectors_test.rb @@ -65,6 +65,38 @@ class SelectorsTest < Minitest::Spec end end + it "parses a complex selector" do + actual = parse_selectors("section>table") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "table" } }] + ] + ] + end + end + + it "parses a complex selector with many selectors" do + actual = parse_selectors("section>table>tr") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "table" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + ] + ] + ] + end + end + private def parse_selectors(selectors) From bb5fcc04ed84d03b12ff02e9063de95d3693200e Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 13:38:44 -0400 Subject: [PATCH 5/7] fix: handle selector combinators surrounded by whitespace Partial fix for #25 --- lib/syntax_tree/css/selectors.rb | 4 +++ test/selectors_test.rb | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/lib/syntax_tree/css/selectors.rb b/lib/syntax_tree/css/selectors.rb index cdebec6..e7244d1 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -337,6 +337,8 @@ def relative_selector_list # = [ ? ]* def complex_selector + consume_whitespace + left = compound_selector if (c = maybe { combinator }) @@ -396,6 +398,8 @@ def simple_selector # = '>' | '+' | '~' | [ '|' '|' ] def combinator + consume_whitespace + value = options do maybe { consume(">") } || diff --git a/test/selectors_test.rb b/test/selectors_test.rb index 1a844f8..91f0110 100644 --- a/test/selectors_test.rb +++ b/test/selectors_test.rb @@ -97,6 +97,52 @@ class SelectorsTest < Minitest::Spec end end + it "parses a complex selector with whitespace" do + actual = parse_selectors("section > table") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "table" } }], + ] + ] + end + end + + it "parses a complex selector with implicit descendant combinator" do + actual = parse_selectors("section table") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: nil, + right: Selectors::TypeSelector[value: { name: { value: "table" } }], + ] + ] + end + end + + it "parses a complex complex selector" do + actual = parse_selectors("section > table tr") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "table" } }], + combinator: nil, + right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + ] + ] + ] + end + end + private def parse_selectors(selectors) From a6d9b010b5d307385758ff0cc013bc044143a571 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 25 May 2024 14:30:47 -0400 Subject: [PATCH 6/7] fix: basic selector formatting --- lib/syntax_tree/css/format.rb | 42 +++++++ test/selectors_test.rb | 215 +++++++++++++++++++--------------- 2 files changed, 160 insertions(+), 97 deletions(-) diff --git a/lib/syntax_tree/css/format.rb b/lib/syntax_tree/css/format.rb index cb73a16..95113f2 100644 --- a/lib/syntax_tree/css/format.rb +++ b/lib/syntax_tree/css/format.rb @@ -76,6 +76,48 @@ def visit_type_selector(node) node.value.format(q) end end + + # Visit a Selectors::ClassSelector node. + def visit_class_selector(node) + q.text(".") + node.value.format(q) + end + + # Visit a Selectors::Combinator node. + def visit_combinator(node) + node.value.format(q) + end + + # Visit a Selectors::ComplexSelector node. + def visit_complex_selector(node) + q.group do + node.left.format(q) + + if node.combinator + q.text(" ") + node.combinator.format(q) + end + + q.text(" ") + node.right.format(q) + end + end + + # Visit a Selectors::CompoundSelector node. + def visit_compound_selector(node) + q.group do + node.type.format(q) if node.type + node.subclasses.each do |subclass| + subclass.format(q) + end + # TODO: pseudo-elements + end + end + + def visit_wqname(node) + node.prefix.format(q) if node.prefix + node.name.format(q) + end end end end diff --git a/test/selectors_test.rb b/test/selectors_test.rb index 91f0110..43d23e1 100644 --- a/test/selectors_test.rb +++ b/test/selectors_test.rb @@ -5,141 +5,162 @@ module SyntaxTree module CSS class SelectorsTest < Minitest::Spec - it "parses a simple class selector" do - actual = parse_selectors(".flex") + describe "parsing" do + it "parses a simple class selector" do + actual = parse_selectors(".flex") - assert_pattern do - actual => [Selectors::ClassSelector[value: { value: "flex" }]] + assert_pattern do + actual => [Selectors::ClassSelector[value: { value: "flex" }]] + end end - end - it "parses a compound class selector" do - actual = parse_selectors(".flex.text-xl") + it "parses a compound class selector" do + actual = parse_selectors(".flex.text-xl") - assert_pattern do - actual => [ - Selectors::CompoundSelector[ - subclasses: [ - Selectors::ClassSelector[value: { value: "flex" }], - Selectors::ClassSelector[value: { value: "text-xl" }] + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + subclasses: [ + Selectors::ClassSelector[value: { value: "flex" }], + Selectors::ClassSelector[value: { value: "text-xl" }] + ] ] ] - ] + end end - end - it "parses a compound selector" do - actual = parse_selectors("div.flex") + it "parses a compound selector" do + actual = parse_selectors("div.flex") - assert_pattern do - actual => [ - Selectors::CompoundSelector[ - type: { value: { name: { value: "div" } } }, - subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], - pseudo_elements: [] + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + type: { value: { name: { value: "div" } } }, + subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], + pseudo_elements: [] + ] ] - ] + end end - end - it "parses a compound selector with a pseudo-element" do - actual = parse_selectors("div.flex::first-line") - - assert_pattern do - actual => [ - Selectors::CompoundSelector[ - type: { value: { name: { value: "div" } } }, - subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], - pseudo_elements: [ - [ - Selectors::PseudoElementSelector[ - Selectors::PseudoClassSelector[ - value: { value: "first-line" } - ] - ], - [] + it "parses a compound selector with a pseudo-element" do + actual = parse_selectors("div.flex::first-line") + + assert_pattern do + actual => [ + Selectors::CompoundSelector[ + type: { value: { name: { value: "div" } } }, + subclasses: [Selectors::ClassSelector[value: { value: "flex" }]], + pseudo_elements: [ + [ + Selectors::PseudoElementSelector[ + Selectors::PseudoClassSelector[ + value: { value: "first-line" } + ] + ], + [] + ] ] ] ] - ] + end end - end - it "parses a complex selector" do - actual = parse_selectors("section>table") + it "parses a complex selector" do + actual = parse_selectors("section>table") - assert_pattern do - actual => [ - Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "table" } }] + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "table" } }] + ] ] - ] + end end - end - it "parses a complex selector with many selectors" do - actual = parse_selectors("section>table>tr") + it "parses a complex selector with many selectors" do + actual = parse_selectors("section>table>tr") - assert_pattern do - actual => [ - Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "table" } }], + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + right: Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "table" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + ] ] ] - ] + end end - end - it "parses a complex selector with whitespace" do - actual = parse_selectors("section > table") + it "parses a complex selector with whitespace" do + actual = parse_selectors("section > table") - assert_pattern do - actual => [ - Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "table" } }], + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::TypeSelector[value: { name: { value: "table" } }], + ] ] - ] + end end - end - it "parses a complex selector with implicit descendant combinator" do - actual = parse_selectors("section table") + it "parses a complex selector with implicit descendant combinator" do + actual = parse_selectors("section table") - assert_pattern do - actual => [ - Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: nil, - right: Selectors::TypeSelector[value: { name: { value: "table" } }], + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: nil, + right: Selectors::TypeSelector[value: { name: { value: "table" } }], + ] ] - ] + end end - end - it "parses a complex complex selector" do - actual = parse_selectors("section > table tr") + it "parses a complex complex selector" do + actual = parse_selectors("section > table tr") - assert_pattern do - actual => [ - Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "table" } }], - combinator: nil, - right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "section" } }], + combinator: { value: { value: ">" } }, + right: Selectors::ComplexSelector[ + left: Selectors::TypeSelector[value: { name: { value: "table" } }], + combinator: nil, + right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + ] ] ] - ] + end + end + + end + + describe "formatting" do + it "formats complex selectors" do + assert_selector_format(".outer section.foo>table.bar tr", ".outer section.foo > table.bar tr") + end + + private + + def assert_selector_format(selectors, expected) + selectors = parse_selectors(selectors) + + io = StringIO.new + selectors.each do |selector| + selector.format(::PrettyPrint.new(io)) + assert_equal(expected, io.string) + end end end From 45b5ece453a19e0b0c4d5f561363e5cba30e4599 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 26 May 2024 22:10:17 -0400 Subject: [PATCH 7/7] Flatten the AST of ComplexSelector --- lib/syntax_tree/css/format.rb | 11 ++----- lib/syntax_tree/css/pretty_print.rb | 12 +++----- lib/syntax_tree/css/selectors.rb | 37 ++++++++++++----------- test/selectors_test.rb | 46 ++++++++++++++++------------- 4 files changed, 52 insertions(+), 54 deletions(-) diff --git a/lib/syntax_tree/css/format.rb b/lib/syntax_tree/css/format.rb index 95113f2..b7b1620 100644 --- a/lib/syntax_tree/css/format.rb +++ b/lib/syntax_tree/css/format.rb @@ -91,15 +91,10 @@ def visit_combinator(node) # Visit a Selectors::ComplexSelector node. def visit_complex_selector(node) q.group do - node.left.format(q) - - if node.combinator - q.text(" ") - node.combinator.format(q) + node.child_nodes.each_with_index do |child_node, j| + q.text(" ") unless j == 0 + child_node.format(q) end - - q.text(" ") - node.right.format(q) end end diff --git a/lib/syntax_tree/css/pretty_print.rb b/lib/syntax_tree/css/pretty_print.rb index ceb42ba..0094006 100644 --- a/lib/syntax_tree/css/pretty_print.rb +++ b/lib/syntax_tree/css/pretty_print.rb @@ -432,14 +432,10 @@ def visit_combinator(node) # Visit a Selectors::ComplexSelector node. def visit_complex_selector(node) token("complex-selector") do - q.breakable - q.pp(node.left) - - q.breakable - q.pp(node.combinator) - - q.breakable - q.pp(node.right) + node.child_nodes.each do |child| + q.breakable + q.pp(child) + end end end diff --git a/lib/syntax_tree/css/selectors.rb b/lib/syntax_tree/css/selectors.rb index e7244d1..6a05b6f 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -94,26 +94,20 @@ def deconstruct_keys(keys) end class ComplexSelector < Node - attr_reader :left, :combinator, :right + attr_reader :child_nodes - def initialize(left:, combinator:, right:) - @left = left - @combinator = combinator - @right = right + def initialize(child_nodes:) + @child_nodes = child_nodes end def accept(visitor) visitor.visit_complex_selector(self) end - def child_nodes - [left, combinator, right] - end - alias deconstruct child_nodes def deconstruct_keys(keys) - { left: left, combinator: combinator, right: right } + { child_nodes: child_nodes } end end @@ -337,16 +331,23 @@ def relative_selector_list # = [ ? ]* def complex_selector - consume_whitespace + child_nodes = [compound_selector] - left = compound_selector + loop do + if (c = maybe { combinator }) + child_nodes << c + end + if (s = maybe { compound_selector }) + child_nodes << s + else + break + end + end - if (c = maybe { combinator }) - ComplexSelector.new(left: left, combinator: c, right: complex_selector) - elsif (right = maybe { complex_selector }) - ComplexSelector.new(left: left, combinator: nil, right: right) + if child_nodes.length > 1 + ComplexSelector.new(child_nodes: child_nodes) else - left + child_nodes.first end end @@ -362,6 +363,8 @@ def relative_selector # = [ ? * # [ * ]* ]! def compound_selector + consume_whitespace + type = maybe { type_selector } subclasses = [] diff --git a/test/selectors_test.rb b/test/selectors_test.rb index 43d23e1..138131e 100644 --- a/test/selectors_test.rb +++ b/test/selectors_test.rb @@ -72,9 +72,11 @@ class SelectorsTest < Minitest::Spec assert_pattern do actual => [ Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "table" } }] + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "table" } }] + ] ] ] end @@ -86,12 +88,12 @@ class SelectorsTest < Minitest::Spec assert_pattern do actual => [ Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "table" } }], - combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "table" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "tr" } }], ] ] ] @@ -104,9 +106,11 @@ class SelectorsTest < Minitest::Spec assert_pattern do actual => [ Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::TypeSelector[value: { name: { value: "table" } }], + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "table" } }], + ] ] ] end @@ -118,9 +122,10 @@ class SelectorsTest < Minitest::Spec assert_pattern do actual => [ Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: nil, - right: Selectors::TypeSelector[value: { name: { value: "table" } }], + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::TypeSelector[value: { name: { value: "table" } }], + ] ] ] end @@ -132,12 +137,11 @@ class SelectorsTest < Minitest::Spec assert_pattern do actual => [ Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "section" } }], - combinator: { value: { value: ">" } }, - right: Selectors::ComplexSelector[ - left: Selectors::TypeSelector[value: { name: { value: "table" } }], - combinator: nil, - right: Selectors::TypeSelector[value: { name: { value: "tr" } }] + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "table" } }], + Selectors::TypeSelector[value: { name: { value: "tr" } }] ] ] ]