diff --git a/lib/webrat/core/matchers.rb b/lib/webrat/core/matchers.rb index 9b884293..1518c191 100644 --- a/lib/webrat/core/matchers.rb +++ b/lib/webrat/core/matchers.rb @@ -6,9 +6,10 @@ def initialize(expected, &block) # Require nokogiri and fall back on rexml begin require "nokogiri" + require "webrat/nokogiri" rescue LoadError => e if require "rexml/document" - require "merb-core/vendor/nokogiri/css" + require "webrat/vendor/nokogiri/css" warn("Standard REXML library is slow. Please consider installing nokogiri.\nUse \"sudo gem install nokogiri\"") end end diff --git a/lib/webrat/nokogiri.rb b/lib/webrat/nokogiri.rb new file mode 100644 index 00000000..b0aa7c5c --- /dev/null +++ b/lib/webrat/nokogiri.rb @@ -0,0 +1,15 @@ +module Nokogiri + module CSS + class XPathVisitor + + def visit_pseudo_class_text(node) + "@type='text'" + end + + def visit_pseudo_class_password(node) + "@type='password'" + end + + end + end +end \ No newline at end of file diff --git a/lib/webrat/vendor/nokogiri/css.rb b/lib/webrat/vendor/nokogiri/css.rb new file mode 100644 index 00000000..065e501d --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css.rb @@ -0,0 +1,6 @@ +require 'merb-core/vendor/nokogiri/css/node' +require 'merb-core/vendor/nokogiri/css/xpath_visitor' +require 'merb-core/vendor/nokogiri/css/generated_tokenizer' +require 'merb-core/vendor/nokogiri/css/generated_parser' +require 'merb-core/vendor/nokogiri/css/tokenizer' +require 'merb-core/vendor/nokogiri/css/parser' diff --git a/lib/webrat/vendor/nokogiri/css/generated_parser.rb b/lib/webrat/vendor/nokogiri/css/generated_parser.rb new file mode 100644 index 00000000..d9cda5d4 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/generated_parser.rb @@ -0,0 +1,653 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by racc 1.4.5 +# from racc grammer file "lib/nokogiri/css/parser.y". +# + +require 'racc/parser' + + +module Nokogiri + + module CSS + + class GeneratedParser < Racc::Parser + +##### racc 1.4.5 generates ### + +racc_reduce_table = [ + 0, 0, :racc_error, + 4, 46, :_reduce_1, + 1, 46, :_reduce_2, + 2, 49, :_reduce_3, + 2, 49, :_reduce_4, + 2, 49, :_reduce_5, + 1, 49, :_reduce_6, + 2, 49, :_reduce_7, + 2, 49, :_reduce_8, + 2, 50, :_reduce_9, + 2, 50, :_reduce_10, + 1, 50, :_reduce_none, + 2, 50, :_reduce_12, + 1, 50, :_reduce_13, + 3, 48, :_reduce_14, + 1, 48, :_reduce_none, + 2, 57, :_reduce_16, + 1, 51, :_reduce_17, + 1, 51, :_reduce_18, + 6, 55, :_reduce_19, + 6, 55, :_reduce_20, + 5, 55, :_reduce_21, + 2, 54, :_reduce_22, + 3, 54, :_reduce_23, + 3, 54, :_reduce_24, + 3, 54, :_reduce_25, + 1, 59, :_reduce_none, + 1, 59, :_reduce_none, + 4, 60, :_reduce_28, + 3, 60, :_reduce_29, + 2, 60, :_reduce_30, + 1, 60, :_reduce_31, + 2, 61, :_reduce_32, + 2, 61, :_reduce_33, + 1, 52, :_reduce_none, + 0, 52, :_reduce_none, + 2, 56, :_reduce_36, + 2, 56, :_reduce_37, + 2, 56, :_reduce_38, + 2, 56, :_reduce_39, + 1, 56, :_reduce_none, + 1, 56, :_reduce_none, + 1, 56, :_reduce_none, + 1, 56, :_reduce_none, + 1, 62, :_reduce_44, + 4, 58, :_reduce_45, + 4, 58, :_reduce_46, + 0, 58, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 1, 63, :_reduce_none, + 5, 53, :_reduce_55, + 1, 64, :_reduce_none, + 2, 47, :_reduce_none, + 0, 47, :_reduce_none ] + +racc_reduce_n = 59 + +racc_shift_n = 97 + +racc_action_table = [ + 5, 79, 81, 27, 10, 26, 94, 93, 42, 2, + 42, 30, 56, 5, 5, 28, 79, 81, 10, 42, + 42, 42, 47, 2, 51, 42, 42, 10, 82, 84, + 85, 52, 78, 10, 10, 7, 9, 12, 15, 42, + 10, 17, 77, 82, 84, 85, 10, 78, 7, 7, + 9, 12, 15, 42, 5, 17, 68, 77, 10, 9, + 69, 15, 10, 2, 17, 9, 9, 15, 15, 50, + 17, 17, 9, 5, 15, 26, 21, 17, 9, 54, + 15, 23, 63, 17, 37, 38, 39, 64, 42, 7, + 9, 12, 15, 44, 9, 17, 15, 42, 15, 17, + 76, 42, 86, 31, 42, 89, 42, 25, 7, 91, + 33, 92, 34, 35, 53, 42, 42 ] + +racc_action_check = [ + 0, 73, 73, 6, 0, 7, 90, 90, 63, 0, + 28, 7, 29, 17, 55, 6, 71, 71, 55, 33, + 34, 35, 17, 55, 22, 37, 38, 4, 73, 73, + 73, 23, 73, 1, 11, 0, 0, 0, 0, 42, + 67, 0, 73, 71, 71, 71, 14, 71, 17, 55, + 55, 55, 55, 44, 36, 55, 50, 71, 36, 4, + 52, 4, 16, 36, 4, 1, 11, 1, 11, 21, + 1, 11, 67, 41, 67, 5, 5, 67, 14, 27, + 14, 5, 41, 14, 13, 13, 13, 41, 64, 36, + 36, 36, 36, 16, 16, 36, 16, 65, 18, 16, + 69, 15, 72, 9, 75, 80, 83, 5, 41, 87, + 13, 88, 13, 13, 24, 93, 94 ] + +racc_action_pointer = [ + -2, 27, nil, nil, 21, 65, 3, -5, nil, 92, + nil, 28, nil, 77, 40, 92, 56, 11, 58, nil, + nil, 62, -18, 20, 72, nil, nil, 79, 1, -30, + nil, nil, nil, 10, 11, 12, 52, 16, 17, nil, + nil, 71, 30, nil, 44, nil, nil, nil, nil, nil, + 40, nil, 53, nil, nil, 12, nil, nil, nil, nil, + nil, nil, nil, -1, 79, 88, nil, 34, nil, 84, + nil, 13, 61, -2, nil, 95, nil, nil, nil, nil, + 64, nil, nil, 97, nil, nil, nil, 68, 69, nil, + -4, nil, nil, 106, 107, nil, nil ] + +racc_action_default = [ + -59, -42, -17, -13, -41, -59, -59, -59, -2, -59, + -44, -43, -18, -15, -40, -58, -35, -59, -11, -38, + -37, -31, -59, -26, -59, -22, -27, -59, -58, -59, + -26, -16, -39, -58, -58, -58, -59, -58, -58, -6, + -36, -59, -58, -34, -58, -9, -10, -33, -32, -12, + -59, -23, -30, -24, 97, -59, -25, -5, -8, -7, + -14, -3, -4, -58, -58, -58, -57, -59, -29, -59, + -1, -47, -59, -47, -56, -58, -28, -48, -52, -53, + -59, -54, -49, -58, -50, -51, -21, -59, -59, -19, + -59, -20, -55, -58, -58, -45, -46 ] + +racc_goto_table = [ + 41, 19, 8, 80, 20, 87, 48, 22, 6, 29, + 49, 32, 46, 55, 40, 45, 43, 36, 57, 58, + 59, 24, 61, 62, 75, nil, nil, 66, nil, 67, + 65, nil, nil, nil, nil, nil, nil, nil, 60, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 71, 72, + 73, nil, nil, nil, nil, nil, nil, 70, nil, nil, + 88, nil, nil, nil, nil, nil, nil, 74, 90, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 95, 96 ] + +racc_goto_check = [ + 2, 11, 3, 13, 11, 13, 9, 14, 1, 14, + 10, 11, 8, 2, 11, 7, 11, 4, 2, 2, + 2, 15, 2, 2, 19, nil, nil, 2, nil, 2, + 9, nil, nil, nil, nil, nil, nil, nil, 3, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 2, 2, + 2, nil, nil, nil, nil, nil, nil, 3, nil, nil, + 2, nil, nil, nil, nil, nil, nil, 11, 2, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + +racc_goto_pointer = [ + nil, 8, -15, 2, 4, nil, nil, -1, -4, -11, + -8, 0, nil, -68, 2, 16, nil, nil, nil, -43 ] + +racc_goto_default = [ + nil, nil, nil, nil, nil, 13, 16, nil, nil, 18, + 1, 3, 4, nil, nil, nil, 11, 14, 83, nil ] + +racc_token_table = { + false => 0, + Object.new => 1, + :FUNCTION => 2, + :INCLUDES => 3, + :DASHMATCH => 4, + :LBRACE => 5, + :HASH => 6, + :PLUS => 7, + :GREATER => 8, + :S => 9, + :STRING => 10, + :IDENT => 11, + :COMMA => 12, + :URI => 13, + :CDO => 14, + :CDC => 15, + :NUMBER => 16, + :PERCENTAGE => 17, + :LENGTH => 18, + :EMS => 19, + :EXS => 20, + :ANGLE => 21, + :TIME => 22, + :FREQ => 23, + :IMPORTANT_SYM => 24, + :IMPORT_SYM => 25, + :MEDIA_SYM => 26, + :PAGE_SYM => 27, + :CHARSET_SYM => 28, + :DIMENSION => 29, + :PREFIXMATCH => 30, + :SUFFIXMATCH => 31, + :SUBSTRINGMATCH => 32, + :TILDE => 33, + :NOT_EQUAL => 34, + :SLASH => 35, + :DOUBLESLASH => 36, + :NOT => 37, + "." => 38, + "*" => 39, + "[" => 40, + "]" => 41, + ")" => 42, + ":" => 43, + "=" => 44 } + +racc_use_result_var = true + +racc_nt_base = 45 + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ +'$end', +'error', +'FUNCTION', +'INCLUDES', +'DASHMATCH', +'LBRACE', +'HASH', +'PLUS', +'GREATER', +'S', +'STRING', +'IDENT', +'COMMA', +'URI', +'CDO', +'CDC', +'NUMBER', +'PERCENTAGE', +'LENGTH', +'EMS', +'EXS', +'ANGLE', +'TIME', +'FREQ', +'IMPORTANT_SYM', +'IMPORT_SYM', +'MEDIA_SYM', +'PAGE_SYM', +'CHARSET_SYM', +'DIMENSION', +'PREFIXMATCH', +'SUFFIXMATCH', +'SUBSTRINGMATCH', +'TILDE', +'NOT_EQUAL', +'SLASH', +'DOUBLESLASH', +'NOT', +'"."', +'"*"', +'"["', +'"]"', +'")"', +'":"', +'"="', +'$start', +'selector', +'s_0toN', +'simple_selector_1toN', +'combinator', +'simple_selector', +'element_name', +'hcap_0toN', +'negation', +'function', +'attrib', +'hcap_1toN', +'class', +'attrib_val_0or1', +'expr', +'an_plus_b', +'pseudo', +'attribute_id', +'eql_incl_dash', +'negation_arg'] + +Racc_debug_parser = false + +##### racc system variables end ##### + + # reduce 0 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 13 + def _reduce_1( val, _values, result ) + result = [val.first, val.last].flatten + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 13 + def _reduce_2( val, _values, result ) + result = val.flatten + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 16 + def _reduce_3( val, _values, result ) + result = :DIRECT_ADJACENT_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 17 + def _reduce_4( val, _values, result ) + result = :CHILD_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 18 + def _reduce_5( val, _values, result ) + result = :PRECEDING_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 19 + def _reduce_6( val, _values, result ) + result = :DESCENDANT_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 20 + def _reduce_7( val, _values, result ) + result = :DESCENDANT_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 21 + def _reduce_8( val, _values, result ) + result = :CHILD_SELECTOR + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 31 + def _reduce_9( val, _values, result ) + result = if val[1].nil? + val.first + else + Node.new(:CONDITIONAL_SELECTOR, [val.first, val[1]]) + end + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 34 + def _reduce_10( val, _values, result ) + result = Node.new(:CONDITIONAL_SELECTOR, val) + result + end +.,., + + # reduce 11 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 38 + def _reduce_12( val, _values, result ) + result = Node.new(:CONDITIONAL_SELECTOR, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 43 + def _reduce_13( val, _values, result ) + result = Node.new(:CONDITIONAL_SELECTOR, + [Node.new(:ELEMENT_NAME, ['*']), val.first] + ) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 48 + def _reduce_14( val, _values, result ) + result = Node.new(val[1], [val.first, val.last]) + result + end +.,., + + # reduce 15 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 51 + def _reduce_16( val, _values, result ) + result = Node.new(:CLASS_CONDITION, [val[1]]) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 54 + def _reduce_17( val, _values, result ) + result = Node.new(:ELEMENT_NAME, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 55 + def _reduce_18( val, _values, result ) + result = Node.new(:ELEMENT_NAME, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 63 + def _reduce_19( val, _values, result ) + result = Node.new(:ATTRIBUTE_CONDITION, + [Node.new(:ELEMENT_NAME, [val[2]])] + (val[4] || []) + ) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 68 + def _reduce_20( val, _values, result ) + result = Node.new(:ATTRIBUTE_CONDITION, + [val[2]] + (val[4] || []) + ) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 74 + def _reduce_21( val, _values, result ) + # Non standard, but hpricot supports it. + result = Node.new(:PSEUDO_CLASS, + [Node.new(:FUNCTION, ['nth-child(', val[2]])] + ) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 79 + def _reduce_22( val, _values, result ) + result = Node.new(:FUNCTION, [val.first.strip]) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 82 + def _reduce_23( val, _values, result ) + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 85 + def _reduce_24( val, _values, result ) + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 88 + def _reduce_25( val, _values, result ) + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + result + end +.,., + + # reduce 26 omitted + + # reduce 27 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 102 + def _reduce_28( val, _values, result ) + if val[1] == 'n' + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 113 + def _reduce_29( val, _values, result ) + # n+3, -n+3 + if val[0] == 'n' + val.unshift("1") + result = Node.new(:AN_PLUS_B, val) + elsif val[0] == '-n' + val[0] = 'n' + val.unshift("-1") + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 124 + def _reduce_30( val, _values, result ) + if val[1] == 'n' + val << "+" + val << "0" + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 136 + def _reduce_31( val, _values, result ) + if val[0] == 'even' + val = ["2","n","+","0"] + result = Node.new(:AN_PLUS_B, val) + elsif val[0] == 'odd' + val = ["2","n","+","1"] + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[0]}'" + end + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 141 + def _reduce_32( val, _values, result ) + result = Node.new(:PSEUDO_CLASS, [val[1]]) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 141 + def _reduce_33( val, _values, result ) + result = Node.new(:PSEUDO_CLASS, [val[1]]) + result + end +.,., + + # reduce 34 omitted + + # reduce 35 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 151 + def _reduce_36( val, _values, result ) + result = Node.new(:COMBINATOR, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 154 + def _reduce_37( val, _values, result ) + result = Node.new(:COMBINATOR, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 157 + def _reduce_38( val, _values, result ) + result = Node.new(:COMBINATOR, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 160 + def _reduce_39( val, _values, result ) + result = Node.new(:COMBINATOR, val) + result + end +.,., + + # reduce 40 omitted + + # reduce 41 omitted + + # reduce 42 omitted + + # reduce 43 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 166 + def _reduce_44( val, _values, result ) + result = Node.new(:ID, val) + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 169 + def _reduce_45( val, _values, result ) + result = [val.first, val[2]] + result + end +.,., + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 170 + def _reduce_46( val, _values, result ) + result = [val.first, val[2]] + result + end +.,., + + # reduce 47 omitted + + # reduce 48 omitted + + # reduce 49 omitted + + # reduce 50 omitted + + # reduce 51 omitted + + # reduce 52 omitted + + # reduce 53 omitted + + # reduce 54 omitted + +module_eval <<'.,.,', 'lib/nokogiri/css/parser.y', 186 + def _reduce_55( val, _values, result ) + result = Node.new(:NOT, [val[2]]) + result + end +.,., + + # reduce 56 omitted + + # reduce 57 omitted + + # reduce 58 omitted + + def _reduce_none( val, _values, result ) + result + end + + end # class GeneratedParser + + end # module CSS + +end # module Nokogiri diff --git a/lib/webrat/vendor/nokogiri/css/generated_tokenizer.rb b/lib/webrat/vendor/nokogiri/css/generated_tokenizer.rb new file mode 100644 index 00000000..334f2e2a --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/generated_tokenizer.rb @@ -0,0 +1,159 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by rex 1.0.1 +# from lexical definition file "lib/nokogiri/css/tokenizer.rex". +# + +module Nokogiri +module CSS +class GeneratedTokenizer + require 'strscan' + + class ScanError < StandardError ; end + + attr_reader :lineno + attr_reader :filename + + def scan_setup ; end + + def action &block + yield + end + + def scan_str( str ) + scan_evaluate str + do_parse + end + + def load_file( filename ) + @filename = filename + open(filename, "r") do |f| + scan_evaluate f.read + end + end + + def scan_file( filename ) + load_file filename + do_parse + end + + def next_token + @rex_tokens.shift + end + + def scan_evaluate( str ) + scan_setup + @rex_tokens = [] + @lineno = 1 + ss = StringScanner.new(str) + state = nil + until ss.eos? + text = ss.peek(1) + @lineno += 1 if text == "\n" + case state + when nil + case + when (text = ss.scan(/~=/i)) + @rex_tokens.push action { [:INCLUDES, text] } + + when (text = ss.scan(/\|=/i)) + @rex_tokens.push action { [:DASHMATCH, text] } + + when (text = ss.scan(/\^=/i)) + @rex_tokens.push action { [:PREFIXMATCH, text] } + + when (text = ss.scan(/\$=/i)) + @rex_tokens.push action { [:SUFFIXMATCH, text] } + + when (text = ss.scan(/\*=/i)) + @rex_tokens.push action { [:SUBSTRINGMATCH, text] } + + when (text = ss.scan(/!=/i)) + @rex_tokens.push action { [:NOT_EQUAL, text] } + + when (text = ss.scan(/[-]?([_a-z]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*\(\s*/i)) + @rex_tokens.push action { [:FUNCTION, text] } + + when (text = ss.scan(/@[-]?([_a-z]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*/i)) + @rex_tokens.push action { [:IDENT, text] } + + when (text = ss.scan(/[-]?([_a-z]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*/i)) + @rex_tokens.push action { [:IDENT, text] } + + when (text = ss.scan(/-?([0-9]+|[0-9]*\.[0-9]+)/i)) + @rex_tokens.push action { [:NUMBER, text] } + + when (text = ss.scan(/\#([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])+/i)) + @rex_tokens.push action { [:HASH, text] } + + when (text = ss.scan(/[\s\r\n\f]*\+/i)) + @rex_tokens.push action { [:PLUS, text] } + + when (text = ss.scan(/[\s\r\n\f]*>/i)) + @rex_tokens.push action { [:GREATER, text] } + + when (text = ss.scan(/[\s\r\n\f]*,/i)) + @rex_tokens.push action { [:COMMA, text] } + + when (text = ss.scan(/[\s\r\n\f]*~/i)) + @rex_tokens.push action { [:TILDE, text] } + + when (text = ss.scan(/\:not\(/i)) + @rex_tokens.push action { [:NOT, text] } + + when (text = ss.scan(/@[-]?([_a-z]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*/i)) + @rex_tokens.push action { [:ATKEYWORD, text] } + + when (text = ss.scan(/-?([0-9]+|[0-9]*\.[0-9]+)%/i)) + @rex_tokens.push action { [:PERCENTAGE, text] } + + when (text = ss.scan(/-?([0-9]+|[0-9]*\.[0-9]+)[-]?([_a-z]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])([_a-z0-9-]|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*/i)) + @rex_tokens.push action { [:DIMENSION, text] } + + when (text = ss.scan(//i)) + @rex_tokens.push action { [:CDC, text] } + + when (text = ss.scan(/[\s\r\n\f]*\/\//i)) + @rex_tokens.push action { [:DOUBLESLASH, text] } + + when (text = ss.scan(/[\s\r\n\f]*\//i)) + @rex_tokens.push action { [:SLASH, text] } + + when (text = ss.scan(/U\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?/i)) + @rex_tokens.push action {[:UNICODE_RANGE, text] } + + when (text = ss.scan(/\/\*(.|[\r\n])*?\*\//i)) + ; + + when (text = ss.scan(/[\s\t\r\n\f]+/i)) + @rex_tokens.push action { [:S, text] } + + when (text = ss.scan(/[\.*:\[\]=\)]/i)) + @rex_tokens.push action { [text, text] } + + when (text = ss.scan(/"([^\n\r\f"]|\\n|\r\n|\r|\f|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*"|'([^\n\r\f']|\\n|\r\n|\r|\f|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*'/i)) + @rex_tokens.push action { [:STRING, text] } + + when (text = ss.scan(/\"([^\n\r\f\"]|\\n|\r\n|\r|\f|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*|([^\n\r\f\']|\\n|\r\n|\r|\f|[^\0-\177]|\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])?|\\[^\n\r\f0-9a-f])*/i)) + @rex_tokens.push action { [:INVALID, text] } + + when (text = ss.scan(/./i)) + @rex_tokens.push action { [text, text] } + + else + text = ss.string[ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + else + raise ScanError, "undefined state: '" + state.to_s + "'" + end # case state + end # until ss + end # def scan_evaluate + +end # class +end +end diff --git a/lib/webrat/vendor/nokogiri/css/node.rb b/lib/webrat/vendor/nokogiri/css/node.rb new file mode 100644 index 00000000..17e5c186 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/node.rb @@ -0,0 +1,95 @@ +module Nokogiri + module CSS + class Node + attr_accessor :type, :value + def initialize type, value + @type = type + @value = value + end + + def accept visitor + visitor.send(:"visit_#{type.to_s.downcase}", self) + end + + def to_xpath prefix = '//', preprocess = true + self.preprocess! if preprocess + prefix + XPathVisitor.new.accept(self) + end + + def preprocess! + ### Deal with nth-child + matches = find_by_type( + [:CONDITIONAL_SELECTOR, + [:ELEMENT_NAME], + [:PSEUDO_CLASS, + [:FUNCTION] + ] + ] + ) + matches.each do |match| + if match.value[1].value[0].value[0] =~ /^nth-child/ + tag_name = match.value[0].value.first + match.value[0].value = ['*'] + match.value[1] = Node.new(:COMBINATOR, [ + match.value[1].value[0], + Node.new(:FUNCTION, ['self(', tag_name]) + ]) + end + if match.value[1].value[0].value[0] =~ /^nth-last-child/ + tag_name = match.value[0].value.first + match.value[0].value = ['*'] + match.value[1] = Node.new(:COMBINATOR, [ + match.value[1].value[0], + Node.new(:FUNCTION, ['self(', tag_name]) + ]) + end + end + + ### Deal with first-child, last-child + matches = find_by_type( + [:CONDITIONAL_SELECTOR, + [:ELEMENT_NAME], [:PSEUDO_CLASS] + ]) + matches.each do |match| + if ['first-child', 'last-child'].include?(match.value[1].value.first) + which = match.value[1].value.first.gsub(/-\w*$/, '') + tag_name = match.value[0].value.first + match.value[0].value = ['*'] + match.value[1] = Node.new(:COMBINATOR, [ + Node.new(:FUNCTION, ["#{which}("]), + Node.new(:FUNCTION, ['self(', tag_name]) + ]) + elsif 'only-child' == match.value[1].value.first + tag_name = match.value[0].value.first + match.value[0].value = ['*'] + match.value[1] = Node.new(:COMBINATOR, [ + Node.new(:FUNCTION, ["#{match.value[1].value.first}("]), + Node.new(:FUNCTION, ['self(', tag_name]) + ]) + end + end + + self + end + + def find_by_type(types) + matches = [] + matches << self if to_type == types + @value.each do |v| + matches += v.find_by_type(types) if v.respond_to?(:find_by_type) + end + matches + end + + def to_type + [@type] + @value.map { |n| + n.to_type if n.respond_to?(:to_type) + }.compact + end + + def to_a + [@type] + @value.map { |n| n.respond_to?(:to_a) ? n.to_a : [n] } + end + end + end +end diff --git a/lib/webrat/vendor/nokogiri/css/parser.rb b/lib/webrat/vendor/nokogiri/css/parser.rb new file mode 100644 index 00000000..efc0dbb4 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/parser.rb @@ -0,0 +1,24 @@ +module Nokogiri + module CSS + class Parser < GeneratedParser + class << self + def parse string + new.parse(string) + end + end + + def initialize + @tokenizer = Tokenizer.new + end + + def parse string + @tokenizer.scan string + do_parse + end + + def next_token + @tokenizer.next_token + end + end + end +end diff --git a/lib/webrat/vendor/nokogiri/css/parser.y b/lib/webrat/vendor/nokogiri/css/parser.y new file mode 100644 index 00000000..b8edb520 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/parser.y @@ -0,0 +1,198 @@ +class Nokogiri::CSS::GeneratedParser + +token FUNCTION INCLUDES DASHMATCH LBRACE HASH PLUS GREATER S STRING IDENT +token COMMA URI CDO CDC NUMBER PERCENTAGE LENGTH EMS EXS ANGLE TIME FREQ +token IMPORTANT_SYM IMPORT_SYM MEDIA_SYM PAGE_SYM CHARSET_SYM DIMENSION +token PREFIXMATCH SUFFIXMATCH SUBSTRINGMATCH TILDE NOT_EQUAL SLASH DOUBLESLASH +token NOT + +rule + selector + : selector COMMA s_0toN simple_selector_1toN { + result = [val.first, val.last].flatten + } + | simple_selector_1toN { result = val.flatten } + ; + combinator + : PLUS s_0toN { result = :DIRECT_ADJACENT_SELECTOR } + | GREATER s_0toN { result = :CHILD_SELECTOR } + | TILDE s_0toN { result = :PRECEDING_SELECTOR } + | S { result = :DESCENDANT_SELECTOR } + | DOUBLESLASH s_0toN { result = :DESCENDANT_SELECTOR } + | SLASH s_0toN { result = :CHILD_SELECTOR } + ; + simple_selector + : element_name hcap_0toN { + result = if val[1].nil? + val.first + else + Node.new(:CONDITIONAL_SELECTOR, [val.first, val[1]]) + end + } + | element_name negation { + result = Node.new(:CONDITIONAL_SELECTOR, val) + } + | function + | function attrib { + result = Node.new(:CONDITIONAL_SELECTOR, val) + } + | hcap_1toN { + result = Node.new(:CONDITIONAL_SELECTOR, + [Node.new(:ELEMENT_NAME, ['*']), val.first] + ) + } + ; + simple_selector_1toN + : simple_selector combinator simple_selector_1toN { + result = Node.new(val[1], [val.first, val.last]) + } + | simple_selector + ; + class + : '.' IDENT { result = Node.new(:CLASS_CONDITION, [val[1]]) } + ; + element_name + : IDENT { result = Node.new(:ELEMENT_NAME, val) } + | '*' { result = Node.new(:ELEMENT_NAME, val) } + ; + attrib + : '[' s_0toN IDENT s_0toN attrib_val_0or1 ']' { + result = Node.new(:ATTRIBUTE_CONDITION, + [Node.new(:ELEMENT_NAME, [val[2]])] + (val[4] || []) + ) + } + | '[' s_0toN function s_0toN attrib_val_0or1 ']' { + result = Node.new(:ATTRIBUTE_CONDITION, + [val[2]] + (val[4] || []) + ) + } + | '[' s_0toN NUMBER s_0toN ']' { + # Non standard, but hpricot supports it. + result = Node.new(:PSEUDO_CLASS, + [Node.new(:FUNCTION, ['nth-child(', val[2]])] + ) + } + ; + function + : FUNCTION ')' { + result = Node.new(:FUNCTION, [val.first.strip]) + } + | FUNCTION expr ')' { + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + } + | FUNCTION an_plus_b ')' { + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + } + | NOT expr ')' { + result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten) + } + ; + expr + : NUMBER + | STRING + ; + an_plus_b + : NUMBER IDENT PLUS NUMBER # 5n+3 -5n+3 + { + if val[1] == 'n' + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + } + | IDENT PLUS NUMBER { # n+3, -n+3 + if val[0] == 'n' + val.unshift("1") + result = Node.new(:AN_PLUS_B, val) + elsif val[0] == '-n' + val[0] = 'n' + val.unshift("-1") + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + } + | NUMBER IDENT # 5n, -5n + { + if val[1] == 'n' + val << "+" + val << "0" + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[1]}'" + end + } + | IDENT # even, odd + { + if val[0] == 'even' + val = ["2","n","+","0"] + result = Node.new(:AN_PLUS_B, val) + elsif val[0] == 'odd' + val = ["2","n","+","1"] + result = Node.new(:AN_PLUS_B, val) + else + raise Racc::ParseError, "parse error on IDENT '#{val[0]}'" + end + } + ; + pseudo + : ':' function { + result = Node.new(:PSEUDO_CLASS, [val[1]]) + } + | ':' IDENT { result = Node.new(:PSEUDO_CLASS, [val[1]]) } + ; + hcap_0toN + : hcap_1toN + | + ; + hcap_1toN + : attribute_id hcap_1toN { + result = Node.new(:COMBINATOR, val) + } + | class hcap_1toN { + result = Node.new(:COMBINATOR, val) + } + | attrib hcap_1toN { + result = Node.new(:COMBINATOR, val) + } + | pseudo hcap_1toN { + result = Node.new(:COMBINATOR, val) + } + | attribute_id + | class + | attrib + | pseudo + ; + attribute_id + : HASH { result = Node.new(:ID, val) } + ; + attrib_val_0or1 + : eql_incl_dash s_0toN IDENT s_0toN { result = [val.first, val[2]] } + | eql_incl_dash s_0toN STRING s_0toN { result = [val.first, val[2]] } + | + ; + eql_incl_dash + : '=' + | PREFIXMATCH + | SUFFIXMATCH + | SUBSTRINGMATCH + | NOT_EQUAL + | INCLUDES + | DASHMATCH + ; + negation + : NOT s_0toN negation_arg s_0toN ')' { + result = Node.new(:NOT, [val[2]]) + } + ; + negation_arg + : hcap_1toN + ; + s_0toN + : S s_0toN + | + ; +end + +---- header + diff --git a/lib/webrat/vendor/nokogiri/css/tokenizer.rb b/lib/webrat/vendor/nokogiri/css/tokenizer.rb new file mode 100644 index 00000000..61edc3d0 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/tokenizer.rb @@ -0,0 +1,9 @@ +module Nokogiri + module CSS + class Tokenizer < GeneratedTokenizer + def scan(str) + scan_evaluate(str) + end + end + end +end diff --git a/lib/webrat/vendor/nokogiri/css/tokenizer.rex b/lib/webrat/vendor/nokogiri/css/tokenizer.rex new file mode 100644 index 00000000..aa92b2da --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/tokenizer.rex @@ -0,0 +1,63 @@ +module Nokogiri +module CSS +class GeneratedTokenizer + +macro + nl \n|\r\n|\r|\f + w [\s\r\n\f]* + nonascii [^\\\\0-\\\\177] + num -?([0-9]+|[0-9]*\.[0-9]+) + unicode \\\\\\\\\[0-9a-f]{1,6}(\r\n|[\s\n\r\t\f])? + + escape {unicode}|\\\\\\\[^\n\r\f0-9a-f] + nmchar [_a-z0-9-]|{nonascii}|{escape} + nmstart [_a-z]|{nonascii}|{escape} + ident [-]?({nmstart})({nmchar})* + name ({nmchar})+ + string1 "([^\n\r\f"]|\\{nl}|{nonascii}|{escape})*" + string2 '([^\n\r\f']|\\{nl}|{nonascii}|{escape})*' + string {string1}|{string2} + invalid1 \"([^\n\r\f\\"]|\\{nl}|{nonascii}|{escape})* + invalid2 \'([^\n\r\f\\']|\\{nl}|{nonascii}|{escape})* + invalid {invalid1}|{invalid2} + Comment \/\*(.|[\r\n])*?\*\/ + +rule + +# [:state] pattern [actions] + + ~= { [:INCLUDES, text] } + \|= { [:DASHMATCH, text] } + \^= { [:PREFIXMATCH, text] } + \$= { [:SUFFIXMATCH, text] } + \*= { [:SUBSTRINGMATCH, text] } + != { [:NOT_EQUAL, text] } + {ident}\(\s* { [:FUNCTION, text] } + @{ident} { [:IDENT, text] } + {ident} { [:IDENT, text] } + {num} { [:NUMBER, text] } + \#{name} { [:HASH, text] } + {w}\+ { [:PLUS, text] } + {w}> { [:GREATER, text] } + {w}, { [:COMMA, text] } + {w}~ { [:TILDE, text] } + \:not\( { [:NOT, text] } + @{ident} { [:ATKEYWORD, text] } + {num}% { [:PERCENTAGE, text] } + {num}{ident} { [:DIMENSION, text] } + { [:CDC, text] } + {w}\/\/ { [:DOUBLESLASH, text] } + {w}\/ { [:SLASH, text] } + + U\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})? {[:UNICODE_RANGE, text] } + + {Comment} /* ignore comments */ + [\s\t\r\n\f]+ { [:S, text] } + [\.*:\[\]=\)] { [text, text] } + {string} { [:STRING, text] } + {invalid} { [:INVALID, text] } + . { [text, text] } +end +end +end diff --git a/lib/webrat/vendor/nokogiri/css/xpath_visitor.rb b/lib/webrat/vendor/nokogiri/css/xpath_visitor.rb new file mode 100644 index 00000000..33098870 --- /dev/null +++ b/lib/webrat/vendor/nokogiri/css/xpath_visitor.rb @@ -0,0 +1,159 @@ +module Nokogiri + module CSS + class XPathVisitor + def visit_function node + # note that nth-child and nth-last-child are preprocessed in css/node.rb. + case node.value.first + when /^text\(/ + 'child::text()' + when /^self\(/ + "self::#{node.value[1]}" + when /^(eq|nth|nth-of-type|nth-child)\(/ + if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :AN_PLUS_B + an_plus_b(node.value[1]) + else + "position() = " + node.value[1] + end + when /^(first|first-of-type)\(/ + "position() = 1" + when /^(last|last-of-type)\(/ + "position() = last()" + when /^(nth-last-child|nth-last-of-type)\(/ + "position() = last() - #{node.value[1]}" + when /^contains\(/ + "contains(., #{node.value[1]})" + when /^gt\(/ + "position() > #{node.value[1]}" + when /^only-child\(/ + "last() = 1" + else + node.value.first + ')' + end + end + + def visit_not node + 'not(' + node.value.first.accept(self) + ')' + end + + def visit_preceding_selector node + node.value.last.accept(self) + + '[preceding-sibling::' + + node.value.first.accept(self) + + ']' + end + + def visit_direct_adjacent_selector node + node.value.last.accept(self) + + '[preceding-sibling::' + + node.value.first.accept(self) + + '][position()=1]' + end + + def visit_id node + node.value.first =~ /^#(.*)$/ + "@id = '#{$1}'" + end + + def visit_attribute_condition node + attribute = if (node.value.first.type == :FUNCTION) or (node.value.first.value.first =~ /::/) + '' + else + '@' + end + attribute += node.value.first.accept(self) + + # Support non-standard css + attribute.gsub!(/^@@/, '@') + + return attribute unless node.value.length == 3 + + value = node.value.last + value = "'#{value}'" if value !~ /^['"]/ + + case node.value[1] + when '*=' + "contains(#{attribute}, #{value})" + when '^=' + "starts-with(#{attribute}, #{value})" + when '|=' + "#{attribute} = #{value} or starts-with(#{attribute}, concat(#{value}, '-'))" + when '~=' + "contains(concat(\" \", #{attribute}, \" \"),concat(\" \", #{value}, \" \"))" + when '$=' + "substring(#{attribute}, string-length(#{attribute}) - " + + "string-length(#{value}) + 1, string-length(#{value})) = #{value}" + else + attribute + " #{node.value[1]} " + "#{value}" + end + end + + def visit_pseudo_class node + if node.value.first.is_a?(Nokogiri::CSS::Node) and node.value.first.type == :FUNCTION + node.value.first.accept(self) + else + case node.value.first + when "first" then "position() = 1" + when "last" then "position() = last()" + when "first-of-type" then "position() = 1" + when "last-of-type" then "position() = last()" + when "only-of-type" then "last() = 1" + when "empty" then "not(node())" + when "parent" then "node()" + else + '1 = 1' + end + end + end + + def visit_class_condition node + "contains(concat(' ', @class, ' '),concat(' ', '#{node.value.first}', ' '))" + end + + def visit_combinator node + node.value.first.accept(self) + ' and ' + + node.value.last.accept(self) + end + + def visit_conditional_selector node + node.value.first.accept(self) + '[' + + node.value.last.accept(self) + ']' + end + + def visit_descendant_selector node + node.value.first.accept(self) + + '//' + + node.value.last.accept(self) + end + + def visit_child_selector node + node.value.first.accept(self) + + '/' + + node.value.last.accept(self) + end + + def visit_element_name node + node.value.first + end + + def accept node + node.accept(self) + end + + private + def an_plus_b node + raise ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}" unless node.value.size == 4 + + a = node.value[0].to_i + b = node.value[3].to_i + + if (b == 0) + return "(position() mod #{a}) = 0" + else + compare = (a < 0) ? "<=" : ">=" + return "(position() #{compare} #{b}) and (((position()-#{b}) mod #{a.abs}) = 0)" + end + end + + end + end +end diff --git a/spec/webrat/nokogiri_spec.rb b/spec/webrat/nokogiri_spec.rb new file mode 100644 index 00000000..ccbc62da --- /dev/null +++ b/spec/webrat/nokogiri_spec.rb @@ -0,0 +1,75 @@ +require File.expand_path(File.dirname(__FILE__) + "/../spec_helper") + +describe "Nokogiri Extension" do + include Webrat::Matchers + + def fail + raise_error(Spec::Expectations::ExpectationNotMetError) + end + + before(:each) do + @text_and_password = <<-HTML +
+ + + +
+ HTML + + @text_only = <<-HTML +
+ +
+ HTML + + @password_only = <<-HTML +
+ +
+ HTML + end + + describe ":text" do + it "passes have_selector(:text) if a node with type=text exists" do + @text_and_password.should have_selector(":text") + end + + it "passes not have_selector(:text) if no node with text=text exists" do + @password_only.should_not have_selector(":text") + end + + it "fails have_selector(:text) if no node with type=text exists" do + lambda { @password_only.should have_selector(":text") }.should fail + end + + it "fails not have_selector(:text) if a node with type=text exists" do + lambda { @text_only.should_not have_selector(":text") }.should fail + end + + it "works together with other selectors" do + @text_and_password.should have_selector("input:text[type*='te']") + end + end + + describe ":password" do + it "passes have_selector(:password) if a node with type=password exists" do + @text_and_password.should have_selector(":password") + end + + it "passes not have_selector(:text) if no node with text=text exists" do + @text_only.should_not have_selector(":password") + end + + it "fails have_selector(:password) if no node with type=password exists" do + lambda { @text_only.should have_selector(":password") }.should fail + end + + it "fails not have_selector(:password) if a node with type=password exists" do + lambda { @password_only.should_not have_selector(":password") }.should fail + end + + it "works together with other selectors" do + @text_and_password.should have_selector("input:password[type*='pa']") + end + end +end