From e6d02dbd28ed87def5b391d1e5a686fd75570b85 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Jun 2024 10:11:54 -0400 Subject: [PATCH] Split up combinators logic --- Gemfile.lock | 1 + lib/syntax_tree/css/format.rb | 110 +++++++++++--------- lib/syntax_tree/css/pretty_print.rb | 150 +++++++++++++++++----------- lib/syntax_tree/css/selectors.rb | 118 +++++++++++++++------- lib/syntax_tree/css/visitor.rb | 16 ++- test/selectors_test.rb | 9 +- 6 files changed, 260 insertions(+), 144 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b027863..90a520b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,6 +22,7 @@ GEM prettier_print (>= 1.2.0) PLATFORMS + arm64-darwin-23 x86_64-darwin-21 x86_64-linux diff --git a/lib/syntax_tree/css/format.rb b/lib/syntax_tree/css/format.rb index 3eb2a18..02e5059 100644 --- a/lib/syntax_tree/css/format.rb +++ b/lib/syntax_tree/css/format.rb @@ -42,13 +42,13 @@ def visit_delim_token(node) q.text(node.value) end - # Visit an IdentToken node. - def visit_ident_token(node) + # Visit a HashToken node. + def visit_hash_token(node) q.text(node.value) end - # Visit a HashToken node. - def visit_hash_token(node) + # Visit an IdentToken node. + def visit_ident_token(node) q.text(node.value) end @@ -70,28 +70,79 @@ def visit_style_rule(node) end end + # Visit a Selectors::SubsequentSiblingCombinator node. + def visit_subsequent_sibling_combinator(node) + q.text(" ") + node.value.format(q) + q.text(" ") + end + #------------------------------------------------------------------------- # Selector nodes #------------------------------------------------------------------------- - # Visit a Selectors::TypeSelector node. - def visit_type_selector(node) + # Visit a Selectors::ChildCombinator node. + def visit_child_combinator(node) + q.text(" ") + node.value.format(q) + q.text(" ") + end + + # Visit a Selectors::ClassSelector node. + def visit_class_selector(node) + q.text(".") + node.value.format(q) + end + + # Visit a Selectors::ColumnSiblingCombinator node. + def visit_column_sibling_combinator(node) + q.text(" ") + node.value.each { |value| value.format(q) } + q.text(" ") + end + + # Visit a Selectors::ComplexSelector node. + def visit_complex_selector(node) q.group do - node.prefix.format(q) if node.prefix - node.value.format(q) + node.child_nodes.each do |child_node| + child_node.format(q) + end + end + end + + # Visit a Selectors::CompoundSelector node. + def visit_compound_selector(node) + q.group do + node.child_nodes.each do |child_node| + child_node.format(q) + end end end + # Visit a Selectors::DescendantCombinator node. + def visit_descendant_combinator(node) + q.text(" ") + end + # Visit a Selectors::IdSelector node. def visit_id_selector(node) q.text("#") node.value.format(q) end - # Visit a Selectors::ClassSelector node. - def visit_class_selector(node) - q.text(".") + # Visit a Selectors::NextSiblingCombinator node. + def visit_next_sibling_combinator(node) + q.text(" ") node.value.format(q) + q.text(" ") + end + + # Visit a Selectors::TypeSelector node. + def visit_type_selector(node) + q.group do + node.prefix&.format(q) + node.value.format(q) + end end # Visit a Selectors::PseudoClassSelector node. @@ -116,42 +167,9 @@ def visit_pseudo_element_selector(node) node.value.format(q) end - # Visit a Selectors::Combinator node. - def visit_combinator(node) - case node.value - when WhitespaceToken - q.text(" ") - when Array - q.text(" ") - node.value.each { |val| val.format(q) } - q.text(" ") - else - q.text(" ") - node.value.format(q) - q.text(" ") - end - end - - # Visit a Selectors::ComplexSelector node. - def visit_complex_selector(node) - q.group do - node.child_nodes.each_with_index do |child_node, j| - child_node.format(q) - end - end - end - - # Visit a Selectors::CompoundSelector node. - def visit_compound_selector(node) - q.group do - node.child_nodes.each do |node_| - node_.format(q) - end - end - end - + # Visit a Selectors::WqName node. def visit_wqname(node) - node.prefix.format(q) if node.prefix + node.prefix&.format(q) node.name.format(q) end end diff --git a/lib/syntax_tree/css/pretty_print.rb b/lib/syntax_tree/css/pretty_print.rb index d5d93e3..8e10a1a 100644 --- a/lib/syntax_tree/css/pretty_print.rb +++ b/lib/syntax_tree/css/pretty_print.rb @@ -350,6 +350,73 @@ def visit_whitespace_token(node) # Selector nodes #------------------------------------------------------------------------- + # Visit a Selectors::ChildCombinator node. + def visit_child_combinator(node) + token("child-combinator") do + q.breakable + q.pp(node.value) + end + end + + # Visit a Selectors::ColumnSiblingCombinator node. + def visit_column_sibling_combinator(node) + token("column-sibling-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 + token("type") do + q.breakable + q.pp(node.type) + end + + 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 + # Visit a Selectors::ClassSelector node. def visit_class_selector(node) token("class-selector") do @@ -358,6 +425,14 @@ def visit_class_selector(node) end end + # Visit a Selectors::DescendantCombinator node. + def visit_descendant_combinator(node) + token("descendant-combinator") do + q.breakable + q.pp(node.value) + end + end + # Visit a Selectors::IdSelector node. def visit_id_selector(node) token("id-selector") do @@ -366,6 +441,14 @@ def visit_id_selector(node) end end + # Visit a Selectors::NextSiblingCombinator node. + def visit_next_sibling_combinator(node) + token("next-sibling-combinator") do + q.breakable + q.pp(node.value) + end + end + # Visit a Selectors::PseudoClassSelector node. def visit_pseudo_class_selector(node) token("pseudo-class-selector") do @@ -404,6 +487,14 @@ def visit_pseudo_element_selector(node) end end + # Visit a Selectors::SubsequentSiblingCombinator node. + def visit_subsequent_sibling_combinator(node) + token("subsequent-sibling-combinator") do + q.breakable + q.pp(node.value) + end + end + # Visit a Selectors::TypeSelector node. def visit_type_selector(node) token("type-selector") do @@ -430,65 +521,6 @@ def visit_wqname(node) end end - # Visit a Selectors::Combinator node. - def visit_combinator(node) - token(node.class::PP_NAME) 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 - token("type") do - q.breakable - q.pp(node.type) - end - - 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 f15df10..84e8f51 100644 --- a/lib/syntax_tree/css/selectors.rb +++ b/lib/syntax_tree/css/selectors.rb @@ -71,6 +71,8 @@ def deconstruct_keys(keys) end end + # A joiner between two selectors. + # https://www.w3.org/TR/selectors-4/#combinators class Combinator < Node attr_reader :value @@ -79,7 +81,7 @@ def initialize(value:) end def accept(visitor) - visitor.visit_combinator(self) + raise NoMethodError, __method__ end def child_nodes @@ -91,36 +93,65 @@ def child_nodes def deconstruct_keys(keys) { value: value } end + + def type + raise NoMethodError, __method__ + end end # §15.1 https://www.w3.org/TR/selectors-4/#descendant-combinators class DescendantCombinator < Combinator - TOKEN = WhitespaceToken - PP_NAME = "descendant-combinator" + def accept(visitor) + visitor.visit_descendant_combinator(self) + end + + def type + "descendant-combinator" + end end # §15.2 https://www.w3.org/TR/selectors-4/#child-combinators class ChildCombinator < Combinator - TOKEN = ">" - PP_NAME = "child-combinator" + def accept(visitor) + visitor.visit_child_combinator(self) + end + + def type + "child-combinator" + end end # §15.3 https://www.w3.org/TR/selectors-4/#adjacent-sibling-combinators class NextSiblingCombinator < Combinator - TOKEN = "+" - PP_NAME = "next-sibling-combinator" + def accept(visitor) + visitor.visit_next_sibling_combinator(self) + end + + def type + "next-sibling-combinator" + end end # §15.4 https://www.w3.org/TR/selectors-4/#general-sibling-combinators class SubsequentSiblingCombinator < Combinator - TOKEN = "~" - PP_NAME = "subsequent-sibling-combinator" + def accept(visitor) + visitor.visit_subsequent_sibling_combinator(self) + end + + def type + "subsequent-sibling-combinator" + end end # §16.1 https://www.w3.org/TR/selectors-4/#the-column-combinator class ColumnSiblingCombinator < Combinator - TOKEN = ["|", "|"] - PP_NAME = "column-sibling-combinator" + def accept(visitor) + visitor.visit_column_sibling_combinator(self) + end + + def type + "column-sibling-combinator" + end end class ComplexSelector < Node @@ -430,11 +461,11 @@ def simple_selector # = '>' | '+' | '~' | [ '|' '|' ] def combinator options do - maybe { consume_combinator(ChildCombinator) } || - maybe { consume_combinator(NextSiblingCombinator) } || - maybe { consume_combinator(SubsequentSiblingCombinator) } || - maybe { consume_combinator(ColumnSiblingCombinator) } || - maybe { consume_combinator(DescendantCombinator) } + maybe { consume_child_combinator } || + maybe { consume_next_sibling_combinator } || + maybe { consume_subsequent_sibling_combinator } || + maybe { consume_column_sibling_combinator } || + maybe { consume_descendant_combinator } end end @@ -579,30 +610,47 @@ def one_or_more end end - def consume_combinator(combinator_class) - eat_whitespace = (combinator_class::TOKEN != WhitespaceToken) + def consume_child_combinator + consume_whitespace + result = consume(">") + consume_whitespace + ChildCombinator.new(value: result) + end + + def consume_next_sibling_combinator + consume_whitespace + result = consume("+") + consume_whitespace + NextSiblingCombinator.new(value: result) + end - consume_whitespace if eat_whitespace - result = consume(*combinator_class::TOKEN) - consume_whitespace if eat_whitespace + def consume_subsequent_sibling_combinator + consume_whitespace + result = consume("~") + consume_whitespace + SubsequentSiblingCombinator.new(value: result) + end - combinator_class.new(value: result) + def consume_column_sibling_combinator + consume_whitespace + result = [consume("|"), consume("|")] + consume_whitespace + ColumnSiblingCombinator.new(value: result) end - def consume(*values) - result = - values.map do |value| - case [value, tokens.peek] - in [String, DelimToken[value: token_value]] if value == token_value - tokens.next - in [Class, token] if token.is_a?(value) - tokens.next - in [_, token] - raise MissingTokenError, "Expected #{value} but got #{token.inspect}" - end - end + def consume_descendant_combinator + DescendantCombinator.new(value: consume(WhitespaceToken)) + end - result.size == 1 ? result.first : result + def consume(value) + case [value, tokens.peek] + in [String, DelimToken[value: token_value]] if value == token_value + tokens.next + in [Class, token] if token.is_a?(value) + tokens.next + in [_, token] + raise MissingTokenError, "Expected #{value} but got #{token.inspect}" + end end def maybe diff --git a/lib/syntax_tree/css/visitor.rb b/lib/syntax_tree/css/visitor.rb index 79ca353..6a3599d 100644 --- a/lib/syntax_tree/css/visitor.rb +++ b/lib/syntax_tree/css/visitor.rb @@ -129,11 +129,14 @@ def visit_child_nodes(node) # Selector nodes #------------------------------------------------------------------------- + # Visit a Selectors::ChildCombinator node. + alias visit_child_combinator visit_child_nodes + # 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::ColumnSiblingCombinator node. + alias visit_column_sibling_combinator visit_child_nodes # Visit a Selectors::ComplexSelector node. alias visit_complex_selector visit_child_nodes @@ -141,9 +144,15 @@ def visit_child_nodes(node) # Visit a Selectors::CompoundSelector node. alias visit_compound_selector visit_child_nodes + # Visit a Selectors::DescendantCombinator node. + alias visit_descendant_combinator visit_child_nodes + # Visit a Selectors::IdSelector node. alias visit_id_selector visit_child_nodes + # Visit a Selectors::NextSiblingCombinator node. + alias visit_next_sibling_combinator visit_child_nodes + # Visit a Selectors::PseudoClassFunction node. alias visit_pseudo_class_function visit_child_nodes @@ -153,6 +162,9 @@ def visit_child_nodes(node) # Visit a Selectors::PseudoElementSelector node. alias visit_pseudo_element_selector visit_child_nodes + # Visit a Selectors::SubsequentSiblingCombinator node. + alias visit_subsequent_sibling_combinator visit_child_nodes + # Visit a Selectors::TypeSelector node. alias visit_type_selector visit_child_nodes diff --git a/test/selectors_test.rb b/test/selectors_test.rb index 659d061..3c99a50 100644 --- a/test/selectors_test.rb +++ b/test/selectors_test.rb @@ -317,8 +317,13 @@ def assert_selector_format(selectors, expected) private def parse_selectors(selectors) - css = selectors + " {}" - Parser.new(css).parse.rules.first.selectors + rule = Parser.new("#{selectors} {}").parse.rules.first + + # Pretty-print and format so that we have coverage. + PP.pp(rule, +"") + PrettierPrint.format(+"", 80) { |q| rule.format(q) } + + rule.selectors end end end