parselly 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c245172165bcac6e4b24a355b6e26a4039960fb8654374587523c3041015bb96
4
- data.tar.gz: 1897eee14cb66e216422815883375837168e8850d17e87a98ca7a05873d18d58
3
+ metadata.gz: e1e4e245059433130b385388d399fe29355a1c679a15327a02cb3b49e69ae23b
4
+ data.tar.gz: ceb1167e8a32c25543b96988f754f76eb01fe287fc279a6ceb8dd26ffc258ca9
5
5
  SHA512:
6
- metadata.gz: 011ea12078d3311c28d00864167fa5cd7a5a9b1afd24feacccb4df2631b00e095aedf231793bd93bf7717b2bf99b7bfd28a6918d7b5d0f3e2794ac3d5f0faa6b
7
- data.tar.gz: 261006f641a09ecea004423a68601b0a7c4941d35cf13ddd0acd74dfc530a1de19a2e4d5d370cef8a366271a2a713756ba6ce3f7fd1b1fe8fbe464e534a3549a
6
+ metadata.gz: 33dab2f628019bbd51d482c53ae267f98914a5a561beb146ce57b3625f30535b661de168b2c6150faa79f16af8a49a8c15e7d6047a118577359fdd9334249a7f
7
+ data.tar.gz: 38803e8cc427a8eaa0b2a63f9446cf91334a368a7ea86908da80eb62cca3569a7b44f4c9536165a57f99048cf604dd948814b12dff25ac49c43ab637ec344324
@@ -4,6 +4,12 @@ require 'strscan'
4
4
 
5
5
  module Parselly
6
6
  class Lexer
7
+ Identifier = Struct.new(:value, :raw) do
8
+ def to_s
9
+ value
10
+ end
11
+ end
12
+
7
13
  TOKENS = {
8
14
  # Combinators
9
15
  '>' => :CHILD,
@@ -62,7 +68,7 @@ module Parselly
62
68
  skip_whitespace
63
69
  break if @scanner.eos?
64
70
 
65
- pos = { line: @line, column: @column }
71
+ pos = { line: @line, column: @column, offset: @scanner.pos }
66
72
 
67
73
  if (token = scan_string)
68
74
  @tokens << [:STRING, token, pos]
@@ -74,11 +80,11 @@ module Parselly
74
80
  @tokens << [:IDENT, token, pos]
75
81
  else
76
82
  char = @scanner.getch
77
- raise "Unexpected character: #{char} at #{pos[:line]}:#{pos[:column]}"
83
+ raise "Unexpected character: #{char} at #{pos[:line]}:#{pos[:column]} (offset #{pos[:offset]})"
78
84
  end
79
85
  end
80
86
 
81
- @tokens << [false, nil, { line: @line, column: @column }]
87
+ @tokens << [false, nil, { line: @line, column: @column, offset: @scanner.pos }]
82
88
  @tokens
83
89
  end
84
90
 
@@ -145,7 +151,8 @@ module Parselly
145
151
  ident = @scanner.matched
146
152
  update_position(ident)
147
153
  # Remove backslashes from escaped characters
148
- ident.gsub(ESCAPE_REGEX, '\1')
154
+ normalized = ident.gsub(ESCAPE_REGEX, '\1')
155
+ Identifier.new(normalized, ident)
149
156
  end
150
157
 
151
158
  def scan_number
data/lib/parselly/node.rb CHANGED
@@ -8,7 +8,7 @@ module Parselly
8
8
  # child nodes, parent reference, and source position.
9
9
  #
10
10
  # @example Creating a simple AST node
11
- # node = Parselly::Node.new(:type_selector, 'div', { line: 1, column: 1 })
11
+ # node = Parselly::Node.new(:type_selector, 'div', { line: 1, column: 1, offset: 0 })
12
12
  # node.add_child(Parselly::Node.new(:class_selector, 'container'))
13
13
  #
14
14
  # @example Traversing the AST
@@ -16,19 +16,31 @@ module Parselly
16
16
  # node.descendants # Returns array of all descendant nodes
17
17
  # node.siblings # Returns array of sibling nodes
18
18
  class Node
19
- attr_accessor :type, :value, :children, :parent, :position
19
+ attr_accessor :type, :value, :raw_value, :children, :parent, :position
20
20
 
21
21
  # Creates a new AST node.
22
22
  #
23
23
  # @param type [Symbol] the type of the node (e.g., :type_selector, :class_selector)
24
24
  # @param value [String, nil] optional value associated with the node
25
- # @param position [Hash] source position with :line and :column keys
26
- def initialize(type, value = nil, position = {})
25
+ # @param position [Hash] source position with :line, :column, and :offset keys
26
+ # @param line [Integer, nil] optional line number (keyword alternative)
27
+ # @param column [Integer, nil] optional column number (keyword alternative)
28
+ # @param offset [Integer, nil] optional offset (keyword alternative)
29
+ def initialize(type, value = nil, position = {}, raw_value: nil, line: nil, column: nil, offset: nil)
27
30
  @type = type
28
31
  @value = value
32
+ @raw_value = raw_value.nil? ? value : raw_value
29
33
  @children = []
30
34
  @parent = nil
31
- @position = position
35
+ unless position.nil? || position.is_a?(Hash)
36
+ raise ArgumentError, 'position must be a Hash'
37
+ end
38
+
39
+ resolved_position = position ? position.dup : {}
40
+ resolved_position[:line] = line unless line.nil?
41
+ resolved_position[:column] = column unless column.nil?
42
+ resolved_position[:offset] = offset unless offset.nil?
43
+ @position = resolved_position
32
44
  @descendants_cache = nil
33
45
  end
34
46
 
@@ -92,6 +104,31 @@ module Parselly
92
104
  @descendants_cache
93
105
  end
94
106
 
107
+ # Depth-first traversal of this node and its descendants.
108
+ #
109
+ # @return [Enumerator, Node] enumerator if no block, otherwise self
110
+ def each
111
+ return enum_for(:each) unless block_given?
112
+
113
+ stack = [self]
114
+ until stack.empty?
115
+ node = stack.pop
116
+ yield node
117
+ children = node.children
118
+ stack.concat(children.reverse) if children && !children.empty?
119
+ end
120
+
121
+ self
122
+ end
123
+
124
+ # Finds all nodes of a given type in this subtree.
125
+ #
126
+ # @param type [Symbol] the node type to match
127
+ # @return [Array<Node>] array of matching nodes
128
+ def find_all(type)
129
+ each.with_object([]) { |node, acc| acc << node if node.type == type }
130
+ end
131
+
95
132
  # Returns an array of sibling nodes (excluding self).
96
133
  #
97
134
  # @return [Array<Node>] array of sibling nodes, or empty array if no parent
@@ -230,6 +267,24 @@ module Parselly
230
267
  result
231
268
  end
232
269
 
270
+ # Extracts detailed attribute selector nodes from this node and its descendants.
271
+ #
272
+ # @return [Array<Hash>] array of attribute selector detail hashes
273
+ # Each hash contains :name, :operator (optional), and :value (optional) keys
274
+ def attribute_selectors
275
+ result = []
276
+
277
+ if type == :attribute_selector
278
+ result << extract_attribute_node(self)
279
+ end
280
+
281
+ descendants.each do |node|
282
+ result << extract_attribute_node(node) if node.type == :attribute_selector
283
+ end
284
+
285
+ result
286
+ end
287
+
233
288
  # Extracts all pseudo-classes and pseudo-elements from this node and its descendants.
234
289
  #
235
290
  # @return [Array<String>] array of pseudo-class and pseudo-element names
@@ -316,6 +371,36 @@ module Parselly
316
371
  info
317
372
  end
318
373
 
374
+ # Helper method to extract detailed attribute selector data.
375
+ #
376
+ # @param node [Node] an attribute_selector node
377
+ # @return [Hash] attribute selector detail hash
378
+ def extract_attribute_node(node)
379
+ info = {}
380
+
381
+ if node.value
382
+ info[:name] = node.value
383
+ info[:raw_name] = node.raw_value
384
+ return info
385
+ end
386
+
387
+ node.children.each do |child|
388
+ case child.type
389
+ when :attribute
390
+ info[:name] = child.value
391
+ info[:raw_name] = child.raw_value
392
+ when :equal_operator, :includes_operator, :dashmatch_operator,
393
+ :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
394
+ info[:operator] = child.value
395
+ when :value
396
+ info[:value] = child.value
397
+ info[:raw_value] = child.raw_value
398
+ end
399
+ end
400
+
401
+ info
402
+ end
403
+
319
404
  # Helper method to build an attribute selector string.
320
405
  #
321
406
  # @return [String] the attribute selector string
@@ -660,24 +660,102 @@ CAN_END_COMPOUND = Set[:IDENT, :STAR, :RPAREN, :RBRACKET].freeze
660
660
  CAN_START_COMPOUND = Set[:IDENT, :STAR, :DOT, :HASH, :LBRACKET, :COLON].freeze
661
661
  TYPE_SELECTOR_TYPES = Set[:IDENT, :STAR].freeze
662
662
  SUBCLASS_SELECTOR_TYPES = Set[:DOT, :HASH, :LBRACKET, :COLON].freeze
663
+ SUBCLASS_SELECTOR_END_TYPES = Set[:IDENT, :RBRACKET, :RPAREN].freeze
663
664
  NTH_PSEUDO_NAMES = Set['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type', 'nth-col', 'nth-last-col'].freeze
664
665
  AN_PLUS_B_REGEX = /^(even|odd|[+-]?\d*n(?:[+-]\d+)?|[+-]?n(?:[+-]\d+)?|\d+)$/.freeze
665
666
 
666
667
  module Parselly
667
668
  class Parser < Racc::Parser
668
669
 
669
- module_eval(<<'...end parser.y/module_eval...', 'parser.y', 272)
670
- def parse(input)
670
+ module_eval(<<'...end parser.y/module_eval...', 'parser.y', 279)
671
+ def parse(input, tolerant: false)
672
+ @tolerant = tolerant
673
+ @errors = []
674
+ @error_index = nil
675
+ @suppress_errors = false
671
676
  @lexer = Parselly::Lexer.new(input)
672
- @tokens = @lexer.tokenize
677
+ begin
678
+ @tokens = @lexer.tokenize
679
+ rescue RuntimeError => e
680
+ if tolerant
681
+ @errors << parse_error_from_exception(e)
682
+ return Parselly::ParseResult.new(nil, @errors)
683
+ end
684
+ raise
685
+ end
673
686
  preprocess_tokens!
674
687
  @index = 0
675
- @current_position = { line: 1, column: 1 }
688
+ @current_position = { line: 1, column: 1, offset: 0 }
689
+
690
+ if tolerant
691
+ ast = parse_with_recovery
692
+ normalize_an_plus_b(ast) if ast
693
+ return Parselly::ParseResult.new(ast, @errors)
694
+ end
695
+
676
696
  ast = do_parse
677
697
  normalize_an_plus_b(ast)
678
698
  ast
679
699
  end
680
700
 
701
+ def parse_with_recovery
702
+ do_parse
703
+ rescue Parselly::ParseError, RuntimeError
704
+ parse_partial_ast
705
+ end
706
+
707
+ def parse_partial_ast
708
+ return nil unless @tokens && !@tokens.empty?
709
+
710
+ eof_token = @tokens.last if @tokens.last && @tokens.last[0] == false
711
+ tokens = @tokens.dup
712
+ tokens.pop if eof_token
713
+ limit = @error_index || tokens.length
714
+
715
+ while limit > 0
716
+ truncated = tokens[0...limit]
717
+ truncated << eof_token if eof_token
718
+ begin
719
+ return parse_from_tokens(truncated, suppress_errors: true)
720
+ rescue Parselly::ParseError, RuntimeError
721
+ limit -= 1
722
+ end
723
+ end
724
+ nil
725
+ end
726
+
727
+ def parse_from_tokens(tokens, suppress_errors: false)
728
+ @tokens = tokens
729
+ @index = 0
730
+ @current_position = { line: 1, column: 1, offset: 0 }
731
+ @suppress_errors = suppress_errors
732
+ do_parse
733
+ ensure
734
+ @suppress_errors = false
735
+ end
736
+
737
+ def parse_error_from_exception(error)
738
+ line = nil
739
+ column = nil
740
+ offset = nil
741
+ if error.message =~ /at (\d+):(\d+)/
742
+ line = Regexp.last_match(1).to_i
743
+ column = Regexp.last_match(2).to_i
744
+ end
745
+ if error.message =~ /offset (\d+)/
746
+ offset = Regexp.last_match(1).to_i
747
+ end
748
+ { message: error.message, line: line, column: column, offset: offset }
749
+ end
750
+
751
+ def identifier_value(token)
752
+ token.respond_to?(:value) ? token.value : token
753
+ end
754
+
755
+ def identifier_raw(token)
756
+ token.respond_to?(:raw) ? token.raw : token
757
+ end
758
+
681
759
  def preprocess_tokens!
682
760
  return if @tokens.size <= 1
683
761
 
@@ -692,7 +770,7 @@ def preprocess_tokens!
692
770
  if i < last_idx
693
771
  next_token = @tokens[i + 1]
694
772
  if needs_descendant?(token, next_token)
695
- pos = { line: token[2][:line], column: token[2][:column] }
773
+ pos = { line: token[2][:line], column: token[2][:column], offset: token[2][:offset] }
696
774
  new_tokens[new_tokens_idx] = [:DESCENDANT, ' ', pos]
697
775
  new_tokens_idx += 1
698
776
  end
@@ -712,8 +790,11 @@ def needs_descendant?(current, next_tok)
712
790
  next_type = next_tok[0]
713
791
 
714
792
  # Type selector followed by subclass selector = same compound
715
- return false if TYPE_SELECTOR_TYPES.include?(current_type) &&
716
- SUBCLASS_SELECTOR_TYPES.include?(next_type)
793
+ # Subclass selector followed by subclass selector = same compound
794
+ if SUBCLASS_SELECTOR_TYPES.include?(next_type)
795
+ return false if TYPE_SELECTOR_TYPES.include?(current_type) ||
796
+ SUBCLASS_SELECTOR_END_TYPES.include?(current_type)
797
+ end
717
798
 
718
799
  CAN_END_COMPOUND.include?(current_type) && CAN_START_COMPOUND.include?(next_type)
719
800
  end
@@ -759,88 +840,101 @@ end
759
840
  def on_error(token_id, val, vstack)
760
841
  token_name = token_to_str(token_id) || '?'
761
842
  pos = @current_position || { line: '?', column: '?' }
762
- raise "Parse error: unexpected #{token_name} '#{val}' at #{pos[:line]}:#{pos[:column]}"
843
+ error = {
844
+ message: "Parse error: unexpected #{token_name} '#{val}' at #{pos[:line]}:#{pos[:column]}",
845
+ line: pos[:line],
846
+ column: pos[:column],
847
+ offset: pos[:offset]
848
+ }
849
+ if @tolerant
850
+ @errors << error unless @suppress_errors
851
+ @error_index ||= [@index - 1, 0].max
852
+ raise Parselly::ParseError, error
853
+ end
854
+ raise error[:message]
763
855
  end
764
856
  ...end parser.y/module_eval...
765
857
  ##### State transition tables begin ###
766
858
 
767
859
  racc_action_table = [
768
- 55, 57, 60, 14, 15, 8, 16, 35, 18, 67,
769
- 17, 19, 25, 26, 27, 28, 32, 36, 68, 79,
770
- 7, 33, 61, 14, 15, 8, 16, 43, 80, 72,
771
- 17, 71, 25, 26, 27, 28, 45, 46, 47, 48,
772
- 49, 50, 7, 54, 53, 14, 15, 8, 16, 34,
773
- 7, 37, 17, 14, 15, 8, 16, 19, 7, 51,
774
- 17, 14, 15, 8, 16, 82, 7, 52, 17, 14,
775
- 15, 8, 16, 88, 83, 65, 17, 14, 15, 66,
776
- 16, 69, 89, 70, 17, 14, 15, 73, 16, 77,
777
- 78, 81, 17, 25, 26, 27, 28, 25, 26, 27,
778
- 28, 73, 86, 87, 90, 91, 92, 93 ]
860
+ 45, 47, 50, 14, 15, 8, 16, 55, 18, 70,
861
+ 17, 69, 25, 26, 27, 28, 57, 58, 59, 60,
862
+ 61, 62, 51, 45, 47, 50, 14, 15, 8, 16,
863
+ 37, 19, 80, 17, 33, 25, 26, 27, 28, 34,
864
+ 38, 81, 83, 7, 35, 51, 14, 15, 8, 16,
865
+ 36, 84, 92, 17, 33, 25, 26, 27, 28, 65,
866
+ 7, 93, 39, 14, 15, 8, 16, 19, 66, 32,
867
+ 17, 33, 14, 15, 63, 16, 64, 7, 67, 17,
868
+ 14, 15, 8, 16, 68, 7, 71, 17, 14, 15,
869
+ 8, 16, 78, 32, 79, 17, 14, 15, 82, 16,
870
+ 71, 7, 87, 17, 14, 15, 8, 16, 76, 75,
871
+ 88, 17, 25, 26, 27, 28, 25, 26, 27, 28,
872
+ 89, 90, 91, 94, 95, 96, 97 ]
779
873
 
780
874
  racc_action_check = [
781
- 51, 51, 51, 51, 51, 51, 51, 17, 1, 55,
782
- 51, 2, 51, 51, 51, 51, 14, 17, 55, 70,
783
- 73, 15, 51, 73, 73, 73, 73, 34, 70, 61,
784
- 73, 61, 73, 73, 73, 73, 34, 34, 34, 34,
785
- 34, 34, 0, 44, 44, 0, 0, 0, 0, 16,
786
- 19, 18, 0, 19, 19, 19, 19, 20, 22, 35,
787
- 19, 22, 22, 22, 22, 72, 64, 36, 22, 64,
788
- 64, 64, 64, 81, 72, 53, 64, 4, 4, 54,
789
- 4, 56, 81, 60, 4, 30, 30, 62, 30, 67,
790
- 68, 71, 30, 3, 3, 3, 3, 23, 23, 23,
791
- 23, 74, 79, 80, 82, 83, 88, 89 ]
875
+ 33, 33, 33, 33, 33, 33, 33, 36, 1, 51,
876
+ 33, 51, 33, 33, 33, 33, 36, 36, 36, 36,
877
+ 36, 36, 33, 63, 63, 63, 63, 63, 63, 63,
878
+ 17, 2, 68, 63, 7, 63, 63, 63, 63, 14,
879
+ 17, 68, 70, 71, 15, 63, 71, 71, 71, 71,
880
+ 16, 70, 82, 71, 45, 71, 71, 71, 71, 45,
881
+ 0, 82, 18, 0, 0, 0, 0, 20, 45, 4,
882
+ 0, 32, 4, 4, 37, 4, 38, 19, 46, 4,
883
+ 19, 19, 19, 19, 50, 22, 52, 19, 22, 22,
884
+ 22, 22, 65, 30, 66, 22, 30, 30, 69, 30,
885
+ 72, 54, 75, 30, 54, 54, 54, 54, 56, 56,
886
+ 76, 54, 3, 3, 3, 3, 23, 23, 23, 23,
887
+ 77, 80, 81, 83, 84, 92, 93 ]
792
888
 
793
889
  racc_action_pointer = [
794
- 40, 8, -2, 79, 72, nil, nil, nil, nil, nil,
795
- nil, nil, nil, nil, 14, 19, 47, 5, 51, 48,
796
- 44, nil, 56, 83, nil, nil, nil, nil, nil, nil,
797
- 80, nil, nil, nil, 18, 49, 65, nil, nil, nil,
798
- nil, nil, nil, nil, 41, nil, nil, nil, nil, nil,
799
- nil, -2, nil, 66, 70, -6, 70, nil, nil, nil,
800
- 81, 27, 74, nil, 64, nil, nil, 85, 86, nil,
801
- 4, 89, 50, 18, 88, nil, nil, nil, nil, 98,
802
- 99, 58, 100, 101, nil, nil, nil, nil, 102, 103,
803
- nil, nil, nil, nil ]
890
+ 58, 8, 18, 98, 67, nil, nil, 24, nil, nil,
891
+ nil, nil, nil, nil, 37, 42, 48, 28, 62, 75,
892
+ 54, nil, 83, 102, nil, nil, nil, nil, nil, nil,
893
+ 91, nil, 61, -2, nil, nil, -2, 64, 74, nil,
894
+ nil, nil, nil, nil, nil, 44, 67, nil, nil, nil,
895
+ 82, 7, 73, nil, 99, nil, 106, nil, nil, nil,
896
+ nil, nil, nil, 21, nil, 88, 90, nil, 17, 96,
897
+ 27, 41, 87, nil, nil, 93, 101, 109, nil, nil,
898
+ 117, 118, 37, 119, 120, nil, nil, nil, nil, nil,
899
+ nil, nil, 121, 122, nil, nil, nil, nil ]
804
900
 
805
901
  racc_action_default = [
806
- -61, -61, -2, -6, -16, -14, -15, -19, -20, -21,
807
- -22, -23, -24, -25, -61, -61, -61, -61, -61, -61,
808
- -2, -4, -61, -6, -8, -9, -10, -11, -12, -13,
809
- -16, -18, -26, -27, -61, -37, -61, 94, -1, -3,
810
- -5, -7, -17, -28, -61, -31, -32, -33, -34, -35,
811
- -36, -61, -39, -61, -61, -19, -61, -40, -41, -42,
812
- -54, -61, -56, -59, -61, -29, -30, -61, -61, -38,
813
- -45, -61, -53, -61, -56, -58, -60, -46, -47, -61,
814
- -61, -50, -61, -61, -55, -57, -43, -44, -61, -61,
815
- -51, -52, -48, -49 ]
902
+ -62, -62, -2, -6, -16, -14, -15, -19, -20, -21,
903
+ -22, -23, -24, -25, -62, -62, -62, -62, -62, -62,
904
+ -2, -4, -62, -6, -8, -9, -10, -11, -12, -13,
905
+ -16, -18, -62, -62, -26, -27, -62, -37, -62, 98,
906
+ -1, -3, -5, -7, -17, -19, -62, -41, -42, -43,
907
+ -55, -62, -57, -60, -62, -28, -62, -31, -32, -33,
908
+ -34, -35, -36, -62, -40, -62, -62, -39, -46, -62,
909
+ -54, -62, -57, -59, -61, -62, -62, -62, -47, -48,
910
+ -62, -62, -51, -62, -62, -56, -58, -29, -30, -38,
911
+ -44, -45, -62, -62, -52, -53, -49, -50 ]
816
912
 
817
913
  racc_goto_table = [
818
- 2, 30, 31, 64, 62, 21, 75, 1, 40, 24,
819
- 29, 44, 56, 58, 59, nil, nil, nil, 85, 38,
820
- nil, nil, nil, 39, nil, 64, 84, 30, 42, 41,
821
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
914
+ 2, 46, 30, 31, 22, 24, 73, 1, 42, 21,
915
+ 29, 56, 85, nil, nil, nil, nil, nil, nil, 40,
916
+ nil, nil, nil, nil, 22, 43, 86, 41, 30, 44,
917
+ nil, 77, nil, nil, nil, nil, nil, nil, nil, nil,
822
918
  nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
823
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
824
- nil, nil, nil, nil, 76 ]
919
+ nil, nil, nil, nil, 74 ]
825
920
 
826
921
  racc_goto_check = [
827
- 2, 12, 13, 6, 23, 4, 25, 1, 5, 8,
828
- 10, 19, 20, 21, 22, nil, nil, nil, 25, 2,
829
- nil, nil, nil, 4, nil, 6, 23, 12, 13, 8,
830
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
831
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
922
+ 2, 20, 12, 13, 6, 8, 25, 1, 5, 4,
923
+ 10, 19, 23, nil, nil, nil, nil, nil, nil, 2,
924
+ nil, nil, nil, nil, 6, 8, 25, 4, 12, 13,
925
+ nil, 20, nil, nil, nil, nil, nil, nil, nil, nil,
832
926
  nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
833
927
  nil, nil, nil, nil, 2 ]
834
928
 
835
929
  racc_goto_pointer = [
836
- nil, 7, 0, nil, 3, -14, -48, nil, 6, nil,
837
- 6, nil, -3, -2, nil, nil, nil, nil, nil, -23,
838
- -39, -38, -37, -47, nil, -56 ]
930
+ nil, 7, 0, nil, 7, -14, 1, nil, 2, nil,
931
+ 6, nil, -2, -1, nil, nil, nil, nil, nil, -25,
932
+ -32, nil, nil, -59, nil, -46 ]
839
933
 
840
934
  racc_goto_default = [
841
- nil, nil, 63, 20, nil, 3, 22, 23, nil, 4,
935
+ nil, nil, 53, 20, nil, 3, 54, 23, nil, 4,
842
936
  nil, 5, 6, nil, 9, 10, 11, 12, 13, nil,
843
- nil, nil, nil, nil, 74, nil ]
937
+ nil, 48, 49, 52, 72, nil ]
844
938
 
845
939
  racc_reduce_table = [
846
940
  0, 0, :racc_error,
@@ -882,32 +976,33 @@ racc_reduce_table = [
882
976
  1, 45, :_reduce_36,
883
977
  2, 43, :_reduce_37,
884
978
  5, 43, :_reduce_38,
885
- 3, 44, :_reduce_39,
886
- 1, 46, :_reduce_40,
979
+ 4, 43, :_reduce_39,
980
+ 3, 44, :_reduce_40,
887
981
  1, 46, :_reduce_41,
888
982
  1, 46, :_reduce_42,
889
- 4, 47, :_reduce_43,
983
+ 1, 46, :_reduce_43,
890
984
  4, 47, :_reduce_44,
891
- 2, 47, :_reduce_45,
892
- 3, 47, :_reduce_46,
985
+ 4, 47, :_reduce_45,
986
+ 2, 47, :_reduce_46,
893
987
  3, 47, :_reduce_47,
894
- 5, 47, :_reduce_48,
988
+ 3, 47, :_reduce_48,
895
989
  5, 47, :_reduce_49,
896
- 3, 47, :_reduce_50,
897
- 4, 47, :_reduce_51,
990
+ 5, 47, :_reduce_50,
991
+ 3, 47, :_reduce_51,
898
992
  4, 47, :_reduce_52,
899
- 2, 47, :_reduce_53,
900
- 1, 47, :_reduce_54,
901
- 2, 50, :_reduce_55,
902
- 0, 51, :_reduce_56,
903
- 2, 51, :_reduce_57,
904
- 2, 48, :_reduce_58,
905
- 1, 49, :_reduce_59,
906
- 2, 49, :_reduce_60 ]
993
+ 4, 47, :_reduce_53,
994
+ 2, 47, :_reduce_54,
995
+ 1, 47, :_reduce_55,
996
+ 2, 50, :_reduce_56,
997
+ 0, 51, :_reduce_57,
998
+ 2, 51, :_reduce_58,
999
+ 2, 48, :_reduce_59,
1000
+ 1, 49, :_reduce_60,
1001
+ 2, 49, :_reduce_61 ]
907
1002
 
908
- racc_reduce_n = 61
1003
+ racc_reduce_n = 62
909
1004
 
910
- racc_shift_n = 94
1005
+ racc_shift_n = 98
911
1006
 
912
1007
  racc_token_table = {
913
1008
  false => 0,
@@ -1165,7 +1260,7 @@ module_eval(<<'.,.,', 'parser.y', 79)
1165
1260
 
1166
1261
  module_eval(<<'.,.,', 'parser.y', 84)
1167
1262
  def _reduce_19(val, _values, result)
1168
- result = Node.new(:type_selector, val[0], @current_position)
1263
+ result = Node.new(:type_selector, identifier_value(val[0]), @current_position, raw_value: identifier_raw(val[0]))
1169
1264
  result
1170
1265
  end
1171
1266
  .,.,
@@ -1214,21 +1309,21 @@ module_eval(<<'.,.,', 'parser.y', 99)
1214
1309
 
1215
1310
  module_eval(<<'.,.,', 'parser.y', 104)
1216
1311
  def _reduce_26(val, _values, result)
1217
- result = Node.new(:id_selector, val[1], @current_position)
1312
+ result = Node.new(:id_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
1218
1313
  result
1219
1314
  end
1220
1315
  .,.,
1221
1316
 
1222
1317
  module_eval(<<'.,.,', 'parser.y', 109)
1223
1318
  def _reduce_27(val, _values, result)
1224
- result = Node.new(:class_selector, val[1], @current_position)
1319
+ result = Node.new(:class_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
1225
1320
  result
1226
1321
  end
1227
1322
  .,.,
1228
1323
 
1229
1324
  module_eval(<<'.,.,', 'parser.y', 114)
1230
1325
  def _reduce_28(val, _values, result)
1231
- result = Node.new(:attribute_selector, val[1], @current_position)
1326
+ result = Node.new(:attribute_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
1232
1327
  result
1233
1328
  end
1234
1329
  .,.,
@@ -1236,7 +1331,7 @@ module_eval(<<'.,.,', 'parser.y', 114)
1236
1331
  module_eval(<<'.,.,', 'parser.y', 117)
1237
1332
  def _reduce_29(val, _values, result)
1238
1333
  result = Node.new(:attribute_selector, nil, @current_position)
1239
- result.add_child(Node.new(:attribute, val[1], @current_position))
1334
+ result.add_child(Node.new(:attribute, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])))
1240
1335
  result.add_child(val[2])
1241
1336
  result.add_child(Node.new(:value, val[3], @current_position))
1242
1337
 
@@ -1247,9 +1342,9 @@ module_eval(<<'.,.,', 'parser.y', 117)
1247
1342
  module_eval(<<'.,.,', 'parser.y', 124)
1248
1343
  def _reduce_30(val, _values, result)
1249
1344
  result = Node.new(:attribute_selector, nil, @current_position)
1250
- result.add_child(Node.new(:attribute, val[1], @current_position))
1345
+ result.add_child(Node.new(:attribute, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])))
1251
1346
  result.add_child(val[2])
1252
- result.add_child(Node.new(:value, val[3], @current_position))
1347
+ result.add_child(Node.new(:value, identifier_value(val[3]), @current_position, raw_value: identifier_raw(val[3])))
1253
1348
 
1254
1349
  result
1255
1350
  end
@@ -1299,14 +1394,14 @@ module_eval(<<'.,.,', 'parser.y', 143)
1299
1394
 
1300
1395
  module_eval(<<'.,.,', 'parser.y', 148)
1301
1396
  def _reduce_37(val, _values, result)
1302
- result = Node.new(:pseudo_class, val[1], @current_position)
1397
+ result = Node.new(:pseudo_class, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
1303
1398
  result
1304
1399
  end
1305
1400
  .,.,
1306
1401
 
1307
1402
  module_eval(<<'.,.,', 'parser.y', 151)
1308
1403
  def _reduce_38(val, _values, result)
1309
- fn = Node.new(:pseudo_function, val[1], @current_position)
1404
+ fn = Node.new(:pseudo_function, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
1310
1405
  fn.add_child(val[3])
1311
1406
  result = fn
1312
1407
 
@@ -1314,36 +1409,46 @@ module_eval(<<'.,.,', 'parser.y', 151)
1314
1409
  end
1315
1410
  .,.,
1316
1411
 
1317
- module_eval(<<'.,.,', 'parser.y', 159)
1412
+ module_eval(<<'.,.,', 'parser.y', 157)
1318
1413
  def _reduce_39(val, _values, result)
1319
- result = Node.new(:pseudo_element, val[2], @current_position)
1414
+ fn = Node.new(:pseudo_function, identifier_value(val[0]), @current_position, raw_value: identifier_raw(val[0]))
1415
+ fn.add_child(val[2])
1416
+ result = fn
1417
+
1320
1418
  result
1321
1419
  end
1322
1420
  .,.,
1323
1421
 
1324
- module_eval(<<'.,.,', 'parser.y', 164)
1422
+ module_eval(<<'.,.,', 'parser.y', 165)
1325
1423
  def _reduce_40(val, _values, result)
1326
- result = Node.new(:argument, val[0], @current_position)
1424
+ result = Node.new(:pseudo_element, identifier_value(val[2]), @current_position, raw_value: identifier_raw(val[2]))
1327
1425
  result
1328
1426
  end
1329
1427
  .,.,
1330
1428
 
1331
- module_eval(<<'.,.,', 'parser.y', 166)
1429
+ module_eval(<<'.,.,', 'parser.y', 170)
1332
1430
  def _reduce_41(val, _values, result)
1333
- result = val[0]
1431
+ result = Node.new(:argument, val[0], @current_position)
1334
1432
  result
1335
1433
  end
1336
1434
  .,.,
1337
1435
 
1338
- module_eval(<<'.,.,', 'parser.y', 168)
1436
+ module_eval(<<'.,.,', 'parser.y', 172)
1339
1437
  def _reduce_42(val, _values, result)
1340
1438
  result = val[0]
1341
1439
  result
1342
1440
  end
1343
1441
  .,.,
1344
1442
 
1345
- module_eval(<<'.,.,', 'parser.y', 175)
1443
+ module_eval(<<'.,.,', 'parser.y', 174)
1346
1444
  def _reduce_43(val, _values, result)
1445
+ result = val[0]
1446
+ result
1447
+ end
1448
+ .,.,
1449
+
1450
+ module_eval(<<'.,.,', 'parser.y', 181)
1451
+ def _reduce_44(val, _values, result)
1347
1452
  # Handle 'An+B' like '2n+1'
1348
1453
  result = Node.new(:an_plus_b, "#{val[0]}#{val[1]}+#{val[3]}", @current_position)
1349
1454
 
@@ -1351,8 +1456,8 @@ module_eval(<<'.,.,', 'parser.y', 175)
1351
1456
  end
1352
1457
  .,.,
1353
1458
 
1354
- module_eval(<<'.,.,', 'parser.y', 180)
1355
- def _reduce_44(val, _values, result)
1459
+ module_eval(<<'.,.,', 'parser.y', 186)
1460
+ def _reduce_45(val, _values, result)
1356
1461
  # Handle 'An-B' like '2n-1'
1357
1462
  result = Node.new(:an_plus_b, "#{val[0]}#{val[1]}-#{val[3]}", @current_position)
1358
1463
 
@@ -1360,8 +1465,8 @@ module_eval(<<'.,.,', 'parser.y', 180)
1360
1465
  end
1361
1466
  .,.,
1362
1467
 
1363
- module_eval(<<'.,.,', 'parser.y', 185)
1364
- def _reduce_45(val, _values, result)
1468
+ module_eval(<<'.,.,', 'parser.y', 191)
1469
+ def _reduce_46(val, _values, result)
1365
1470
  # Handle 'An' like '2n' or composite like '2n-1' (when '-1' is part of IDENT)
1366
1471
  result = Node.new(:an_plus_b, "#{val[0]}#{val[1]}", @current_position)
1367
1472
 
@@ -1369,8 +1474,8 @@ module_eval(<<'.,.,', 'parser.y', 185)
1369
1474
  end
1370
1475
  .,.,
1371
1476
 
1372
- module_eval(<<'.,.,', 'parser.y', 190)
1373
- def _reduce_46(val, _values, result)
1477
+ module_eval(<<'.,.,', 'parser.y', 196)
1478
+ def _reduce_47(val, _values, result)
1374
1479
  # Handle 'n+B' like 'n+5' or keywords followed by offset (rare but valid)
1375
1480
  result = Node.new(:an_plus_b, "#{val[0]}+#{val[2]}", @current_position)
1376
1481
 
@@ -1378,8 +1483,8 @@ module_eval(<<'.,.,', 'parser.y', 190)
1378
1483
  end
1379
1484
  .,.,
1380
1485
 
1381
- module_eval(<<'.,.,', 'parser.y', 195)
1382
- def _reduce_47(val, _values, result)
1486
+ module_eval(<<'.,.,', 'parser.y', 201)
1487
+ def _reduce_48(val, _values, result)
1383
1488
  # Handle 'n-B' like 'n-3'
1384
1489
  result = Node.new(:an_plus_b, "#{val[0]}-#{val[2]}", @current_position)
1385
1490
 
@@ -1387,8 +1492,8 @@ module_eval(<<'.,.,', 'parser.y', 195)
1387
1492
  end
1388
1493
  .,.,
1389
1494
 
1390
- module_eval(<<'.,.,', 'parser.y', 201)
1391
- def _reduce_48(val, _values, result)
1495
+ module_eval(<<'.,.,', 'parser.y', 207)
1496
+ def _reduce_49(val, _values, result)
1392
1497
  # Handle '-An+B' like '-2n+1'
1393
1498
  result = Node.new(:an_plus_b, "-#{val[1]}#{val[2]}+#{val[4]}", @current_position)
1394
1499
 
@@ -1396,8 +1501,8 @@ module_eval(<<'.,.,', 'parser.y', 201)
1396
1501
  end
1397
1502
  .,.,
1398
1503
 
1399
- module_eval(<<'.,.,', 'parser.y', 206)
1400
- def _reduce_49(val, _values, result)
1504
+ module_eval(<<'.,.,', 'parser.y', 212)
1505
+ def _reduce_50(val, _values, result)
1401
1506
  # Handle '-An-B' like '-2n-1'
1402
1507
  result = Node.new(:an_plus_b, "-#{val[1]}#{val[2]}-#{val[4]}", @current_position)
1403
1508
 
@@ -1405,8 +1510,8 @@ module_eval(<<'.,.,', 'parser.y', 206)
1405
1510
  end
1406
1511
  .,.,
1407
1512
 
1408
- module_eval(<<'.,.,', 'parser.y', 211)
1409
- def _reduce_50(val, _values, result)
1513
+ module_eval(<<'.,.,', 'parser.y', 217)
1514
+ def _reduce_51(val, _values, result)
1410
1515
  # Handle '-An' like '-2n' or composite like '-2n+1' (when '+1' is part of IDENT)
1411
1516
  result = Node.new(:an_plus_b, "-#{val[1]}#{val[2]}", @current_position)
1412
1517
 
@@ -1414,8 +1519,8 @@ module_eval(<<'.,.,', 'parser.y', 211)
1414
1519
  end
1415
1520
  .,.,
1416
1521
 
1417
- module_eval(<<'.,.,', 'parser.y', 216)
1418
- def _reduce_51(val, _values, result)
1522
+ module_eval(<<'.,.,', 'parser.y', 222)
1523
+ def _reduce_52(val, _values, result)
1419
1524
  # Handle '-n+B' like '-n+3'
1420
1525
  result = Node.new(:an_plus_b, "-#{val[1]}+#{val[3]}", @current_position)
1421
1526
 
@@ -1423,8 +1528,8 @@ module_eval(<<'.,.,', 'parser.y', 216)
1423
1528
  end
1424
1529
  .,.,
1425
1530
 
1426
- module_eval(<<'.,.,', 'parser.y', 221)
1427
- def _reduce_52(val, _values, result)
1531
+ module_eval(<<'.,.,', 'parser.y', 227)
1532
+ def _reduce_53(val, _values, result)
1428
1533
  # Handle '-n-B' like '-n-2'
1429
1534
  result = Node.new(:an_plus_b, "-#{val[1]}-#{val[3]}", @current_position)
1430
1535
 
@@ -1432,8 +1537,8 @@ module_eval(<<'.,.,', 'parser.y', 221)
1432
1537
  end
1433
1538
  .,.,
1434
1539
 
1435
- module_eval(<<'.,.,', 'parser.y', 226)
1436
- def _reduce_53(val, _values, result)
1540
+ module_eval(<<'.,.,', 'parser.y', 232)
1541
+ def _reduce_54(val, _values, result)
1437
1542
  # Handle '-n' or composite like '-n+3' (when '+3' is part of IDENT)
1438
1543
  result = Node.new(:an_plus_b, "-#{val[1]}", @current_position)
1439
1544
 
@@ -1441,8 +1546,8 @@ module_eval(<<'.,.,', 'parser.y', 226)
1441
1546
  end
1442
1547
  .,.,
1443
1548
 
1444
- module_eval(<<'.,.,', 'parser.y', 232)
1445
- def _reduce_54(val, _values, result)
1549
+ module_eval(<<'.,.,', 'parser.y', 238)
1550
+ def _reduce_55(val, _values, result)
1446
1551
  # Handle just a number like '3'
1447
1552
  result = Node.new(:an_plus_b, val[0].to_s, @current_position)
1448
1553
 
@@ -1450,29 +1555,29 @@ module_eval(<<'.,.,', 'parser.y', 232)
1450
1555
  end
1451
1556
  .,.,
1452
1557
 
1453
- module_eval(<<'.,.,', 'parser.y', 245)
1454
- def _reduce_55(val, _values, result)
1558
+ module_eval(<<'.,.,', 'parser.y', 251)
1559
+ def _reduce_56(val, _values, result)
1455
1560
  result = val
1456
1561
  result
1457
1562
  end
1458
1563
  .,.,
1459
1564
 
1460
- module_eval(<<'.,.,', 'parser.y', 245)
1461
- def _reduce_56(val, _values, result)
1565
+ module_eval(<<'.,.,', 'parser.y', 251)
1566
+ def _reduce_57(val, _values, result)
1462
1567
  result = val[1] ? val[1].unshift(val[0]) : val
1463
1568
  result
1464
1569
  end
1465
1570
  .,.,
1466
1571
 
1467
- module_eval(<<'.,.,', 'parser.y', 245)
1468
- def _reduce_57(val, _values, result)
1572
+ module_eval(<<'.,.,', 'parser.y', 251)
1573
+ def _reduce_58(val, _values, result)
1469
1574
  result = val[1] ? val[1].unshift(val[0]) : val
1470
1575
  result
1471
1576
  end
1472
1577
  .,.,
1473
1578
 
1474
- module_eval(<<'.,.,', 'parser.y', 240)
1475
- def _reduce_58(val, _values, result)
1579
+ module_eval(<<'.,.,', 'parser.y', 246)
1580
+ def _reduce_59(val, _values, result)
1476
1581
  result = Node.new(:selector_list, nil, @current_position)
1477
1582
  result.add_child(val[0])
1478
1583
  val[1].each { |pair| result.add_child(pair[1]) }
@@ -1481,15 +1586,15 @@ module_eval(<<'.,.,', 'parser.y', 240)
1481
1586
  end
1482
1587
  .,.,
1483
1588
 
1484
- module_eval(<<'.,.,', 'parser.y', 248)
1485
- def _reduce_59(val, _values, result)
1589
+ module_eval(<<'.,.,', 'parser.y', 254)
1590
+ def _reduce_60(val, _values, result)
1486
1591
  result = val[0]
1487
1592
  result
1488
1593
  end
1489
1594
  .,.,
1490
1595
 
1491
- module_eval(<<'.,.,', 'parser.y', 251)
1492
- def _reduce_60(val, _values, result)
1596
+ module_eval(<<'.,.,', 'parser.y', 257)
1597
+ def _reduce_61(val, _values, result)
1493
1598
  result = Node.new(:selector, nil, val[0].position)
1494
1599
  result.add_child(val[0])
1495
1600
  result.add_child(val[1])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parselly
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/parselly.rb CHANGED
@@ -8,6 +8,21 @@ require_relative 'parselly/parser'
8
8
  require_relative 'parselly/version'
9
9
 
10
10
  module Parselly
11
+ ParseResult = Struct.new(:ast, :errors)
12
+
13
+ class ParseError < StandardError
14
+ attr_reader :error
15
+
16
+ def initialize(error)
17
+ @error = error
18
+ super(error[:message])
19
+ end
20
+ end
21
+
22
+ def parse(selector, tolerant: false)
23
+ Parser.new.parse(selector, tolerant: tolerant)
24
+ end
25
+
11
26
  def sanitize(selector)
12
27
  scanner = StringScanner.new(selector)
13
28
  result = +''
@@ -46,5 +61,5 @@ module Parselly
46
61
  "\\#{char.ord.to_s(16)} "
47
62
  end
48
63
 
49
- module_function :sanitize, :escaped_hex
64
+ module_function :parse, :sanitize, :escaped_hex
50
65
  end
data/parser.y CHANGED
@@ -82,7 +82,7 @@ rule
82
82
 
83
83
  type_selector
84
84
  : IDENT
85
- { result = Node.new(:type_selector, val[0], @current_position) }
85
+ { result = Node.new(:type_selector, identifier_value(val[0]), @current_position, raw_value: identifier_raw(val[0])) }
86
86
  | STAR
87
87
  { result = Node.new(:universal_selector, '*', @current_position) }
88
88
  ;
@@ -102,30 +102,30 @@ rule
102
102
 
103
103
  id_selector
104
104
  : HASH IDENT
105
- { result = Node.new(:id_selector, val[1], @current_position) }
105
+ { result = Node.new(:id_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])) }
106
106
  ;
107
107
 
108
108
  class_selector
109
109
  : DOT IDENT
110
- { result = Node.new(:class_selector, val[1], @current_position) }
110
+ { result = Node.new(:class_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])) }
111
111
  ;
112
112
 
113
113
  attribute_selector
114
114
  : LBRACKET IDENT RBRACKET
115
- { result = Node.new(:attribute_selector, val[1], @current_position) }
115
+ { result = Node.new(:attribute_selector, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])) }
116
116
  | LBRACKET IDENT attr_matcher STRING RBRACKET
117
117
  {
118
118
  result = Node.new(:attribute_selector, nil, @current_position)
119
- result.add_child(Node.new(:attribute, val[1], @current_position))
119
+ result.add_child(Node.new(:attribute, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])))
120
120
  result.add_child(val[2])
121
121
  result.add_child(Node.new(:value, val[3], @current_position))
122
122
  }
123
123
  | LBRACKET IDENT attr_matcher IDENT RBRACKET
124
124
  {
125
125
  result = Node.new(:attribute_selector, nil, @current_position)
126
- result.add_child(Node.new(:attribute, val[1], @current_position))
126
+ result.add_child(Node.new(:attribute, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])))
127
127
  result.add_child(val[2])
128
- result.add_child(Node.new(:value, val[3], @current_position))
128
+ result.add_child(Node.new(:value, identifier_value(val[3]), @current_position, raw_value: identifier_raw(val[3])))
129
129
  }
130
130
  ;
131
131
 
@@ -146,18 +146,24 @@ rule
146
146
 
147
147
  pseudo_class_selector
148
148
  : COLON IDENT
149
- { result = Node.new(:pseudo_class, val[1], @current_position) }
149
+ { result = Node.new(:pseudo_class, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1])) }
150
150
  | COLON IDENT LPAREN any_value RPAREN
151
151
  {
152
- fn = Node.new(:pseudo_function, val[1], @current_position)
152
+ fn = Node.new(:pseudo_function, identifier_value(val[1]), @current_position, raw_value: identifier_raw(val[1]))
153
153
  fn.add_child(val[3])
154
154
  result = fn
155
155
  }
156
+ | IDENT LPAREN any_value RPAREN
157
+ {
158
+ fn = Node.new(:pseudo_function, identifier_value(val[0]), @current_position, raw_value: identifier_raw(val[0]))
159
+ fn.add_child(val[2])
160
+ result = fn
161
+ }
156
162
  ;
157
163
 
158
164
  pseudo_element_selector
159
165
  : COLON COLON IDENT
160
- { result = Node.new(:pseudo_element, val[2], @current_position) }
166
+ { result = Node.new(:pseudo_element, identifier_value(val[2]), @current_position, raw_value: identifier_raw(val[2])) }
161
167
  ;
162
168
 
163
169
  any_value
@@ -265,21 +271,99 @@ CAN_END_COMPOUND = Set[:IDENT, :STAR, :RPAREN, :RBRACKET].freeze
265
271
  CAN_START_COMPOUND = Set[:IDENT, :STAR, :DOT, :HASH, :LBRACKET, :COLON].freeze
266
272
  TYPE_SELECTOR_TYPES = Set[:IDENT, :STAR].freeze
267
273
  SUBCLASS_SELECTOR_TYPES = Set[:DOT, :HASH, :LBRACKET, :COLON].freeze
274
+ SUBCLASS_SELECTOR_END_TYPES = Set[:IDENT, :RBRACKET, :RPAREN].freeze
268
275
  NTH_PSEUDO_NAMES = Set['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type', 'nth-col', 'nth-last-col'].freeze
269
276
  AN_PLUS_B_REGEX = /^(even|odd|[+-]?\d*n(?:[+-]\d+)?|[+-]?n(?:[+-]\d+)?|\d+)$/.freeze
270
277
 
271
278
  ---- inner
272
- def parse(input)
279
+ def parse(input, tolerant: false)
280
+ @tolerant = tolerant
281
+ @errors = []
282
+ @error_index = nil
283
+ @suppress_errors = false
273
284
  @lexer = Parselly::Lexer.new(input)
274
- @tokens = @lexer.tokenize
285
+ begin
286
+ @tokens = @lexer.tokenize
287
+ rescue RuntimeError => e
288
+ if tolerant
289
+ @errors << parse_error_from_exception(e)
290
+ return Parselly::ParseResult.new(nil, @errors)
291
+ end
292
+ raise
293
+ end
275
294
  preprocess_tokens!
276
295
  @index = 0
277
- @current_position = { line: 1, column: 1 }
296
+ @current_position = { line: 1, column: 1, offset: 0 }
297
+
298
+ if tolerant
299
+ ast = parse_with_recovery
300
+ normalize_an_plus_b(ast) if ast
301
+ return Parselly::ParseResult.new(ast, @errors)
302
+ end
303
+
278
304
  ast = do_parse
279
305
  normalize_an_plus_b(ast)
280
306
  ast
281
307
  end
282
308
 
309
+ def parse_with_recovery
310
+ do_parse
311
+ rescue Parselly::ParseError, RuntimeError
312
+ parse_partial_ast
313
+ end
314
+
315
+ def parse_partial_ast
316
+ return nil unless @tokens && !@tokens.empty?
317
+
318
+ eof_token = @tokens.last if @tokens.last && @tokens.last[0] == false
319
+ tokens = @tokens.dup
320
+ tokens.pop if eof_token
321
+ limit = @error_index || tokens.length
322
+
323
+ while limit > 0
324
+ truncated = tokens[0...limit]
325
+ truncated << eof_token if eof_token
326
+ begin
327
+ return parse_from_tokens(truncated, suppress_errors: true)
328
+ rescue Parselly::ParseError, RuntimeError
329
+ limit -= 1
330
+ end
331
+ end
332
+ nil
333
+ end
334
+
335
+ def parse_from_tokens(tokens, suppress_errors: false)
336
+ @tokens = tokens
337
+ @index = 0
338
+ @current_position = { line: 1, column: 1, offset: 0 }
339
+ @suppress_errors = suppress_errors
340
+ do_parse
341
+ ensure
342
+ @suppress_errors = false
343
+ end
344
+
345
+ def parse_error_from_exception(error)
346
+ line = nil
347
+ column = nil
348
+ offset = nil
349
+ if error.message =~ /at (\d+):(\d+)/
350
+ line = Regexp.last_match(1).to_i
351
+ column = Regexp.last_match(2).to_i
352
+ end
353
+ if error.message =~ /offset (\d+)/
354
+ offset = Regexp.last_match(1).to_i
355
+ end
356
+ { message: error.message, line: line, column: column, offset: offset }
357
+ end
358
+
359
+ def identifier_value(token)
360
+ token.respond_to?(:value) ? token.value : token
361
+ end
362
+
363
+ def identifier_raw(token)
364
+ token.respond_to?(:raw) ? token.raw : token
365
+ end
366
+
283
367
  def preprocess_tokens!
284
368
  return if @tokens.size <= 1
285
369
 
@@ -294,7 +378,7 @@ def preprocess_tokens!
294
378
  if i < last_idx
295
379
  next_token = @tokens[i + 1]
296
380
  if needs_descendant?(token, next_token)
297
- pos = { line: token[2][:line], column: token[2][:column] }
381
+ pos = { line: token[2][:line], column: token[2][:column], offset: token[2][:offset] }
298
382
  new_tokens[new_tokens_idx] = [:DESCENDANT, ' ', pos]
299
383
  new_tokens_idx += 1
300
384
  end
@@ -314,8 +398,11 @@ def needs_descendant?(current, next_tok)
314
398
  next_type = next_tok[0]
315
399
 
316
400
  # Type selector followed by subclass selector = same compound
317
- return false if TYPE_SELECTOR_TYPES.include?(current_type) &&
318
- SUBCLASS_SELECTOR_TYPES.include?(next_type)
401
+ # Subclass selector followed by subclass selector = same compound
402
+ if SUBCLASS_SELECTOR_TYPES.include?(next_type)
403
+ return false if TYPE_SELECTOR_TYPES.include?(current_type) ||
404
+ SUBCLASS_SELECTOR_END_TYPES.include?(current_type)
405
+ end
319
406
 
320
407
  CAN_END_COMPOUND.include?(current_type) && CAN_START_COMPOUND.include?(next_type)
321
408
  end
@@ -361,5 +448,16 @@ end
361
448
  def on_error(token_id, val, vstack)
362
449
  token_name = token_to_str(token_id) || '?'
363
450
  pos = @current_position || { line: '?', column: '?' }
364
- raise "Parse error: unexpected #{token_name} '#{val}' at #{pos[:line]}:#{pos[:column]}"
451
+ error = {
452
+ message: "Parse error: unexpected #{token_name} '#{val}' at #{pos[:line]}:#{pos[:column]}",
453
+ line: pos[:line],
454
+ column: pos[:column],
455
+ offset: pos[:offset]
456
+ }
457
+ if @tolerant
458
+ @errors << error unless @suppress_errors
459
+ @error_index ||= [@index - 1, 0].max
460
+ raise Parselly::ParseError, error
461
+ end
462
+ raise error[:message]
365
463
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parselly
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -51,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  - !ruby/object:Gem::Version
52
52
  version: '0'
53
53
  requirements: []
54
- rubygems_version: 3.6.9
54
+ rubygems_version: 4.0.4
55
55
  specification_version: 4
56
56
  summary: Pure Ruby CSS selector parser.
57
57
  test_files: []