diff --git a/lib/syntax_tree/css/format.rb b/lib/syntax_tree/css/format.rb index cb73a16..b7b1620 100644 --- a/lib/syntax_tree/css/format.rb +++ b/lib/syntax_tree/css/format.rb @@ -76,6 +76,43 @@ 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.child_nodes.each_with_index do |child_node, j| + q.text(" ") unless j == 0 + child_node.format(q) + end + 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/lib/syntax_tree/css/pretty_print.rb b/lib/syntax_tree/css/pretty_print.rb index 8ae9256..0094006 100644 --- a/lib/syntax_tree/css/pretty_print.rb +++ b/lib/syntax_tree/css/pretty_print.rb @@ -421,6 +421,62 @@ 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 + node.child_nodes.each do |child| + q.breakable + q.pp(child) + end + 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..6a05b6f 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -71,9 +71,73 @@ def deconstruct_keys(keys) end end - 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 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 :child_nodes + + def initialize(child_nodes:) + @child_nodes = child_nodes + end + + def accept(visitor) + visitor.visit_complex_selector(self) + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { child_nodes: child_nodes } + end + end + + 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 @@ -267,27 +331,30 @@ def relative_selector_list # = [ ? ]* def complex_selector - left = compound_selector + child_nodes = [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) + if (c = maybe { combinator }) + child_nodes << c + end + if (s = maybe { compound_selector }) + child_nodes << s else break end end - left + if child_nodes.length > 1 + ComplexSelector.new(child_nodes: child_nodes) + else + child_nodes.first + end 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 @@ -296,6 +363,8 @@ def relative_selector # = [ ? * # [ * ]* ]! def compound_selector + consume_whitespace + type = maybe { type_selector } subclasses = [] @@ -332,6 +401,8 @@ def simple_selector # = '>' | '+' | '~' | [ '|' '|' ] def combinator + consume_whitespace + value = options do maybe { consume(">") } || diff --git a/lib/syntax_tree/css/visitor.rb b/lib/syntax_tree/css/visitor.rb index 4114913..79ca353 100644 --- a/lib/syntax_tree/css/visitor.rb +++ b/lib/syntax_tree/css/visitor.rb @@ -132,6 +132,15 @@ 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 + # Visit a Selectors::IdSelector node. alias visit_id_selector visit_child_nodes diff --git a/test/selectors_test.rb b/test/selectors_test.rb new file mode 100644 index 0000000..138131e --- /dev/null +++ b/test/selectors_test.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "test_helper" + +module SyntaxTree + module CSS + class SelectorsTest < Minitest::Spec + describe "parsing" do + 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 + + it "parses a complex selector" do + actual = parse_selectors("section>table") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + 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[ + 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" } }], + ] + ] + ] + end + end + + it "parses a complex selector with whitespace" do + actual = parse_selectors("section > table") + + assert_pattern do + actual => [ + Selectors::ComplexSelector[ + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + 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[ + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + 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[ + child_nodes: [ + Selectors::TypeSelector[value: { name: { value: "section" } }], + Selectors::Combinator[value: { value: ">" }], + Selectors::TypeSelector[value: { name: { value: "table" } }], + 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 + + private + + def parse_selectors(selectors) + css = selectors + " {}" + Parser.new(css).parse.rules.first.selectors + end + end + end +end