actionpack-2.3.17-rack-upgrade 2.3.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (441) hide show
  1. data/CHANGELOG +5250 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +409 -0
  4. data/RUNNING_UNIT_TESTS +24 -0
  5. data/Rakefile +158 -0
  6. data/install.rb +30 -0
  7. data/lib/action_controller.rb +113 -0
  8. data/lib/action_controller/assertions/dom_assertions.rb +55 -0
  9. data/lib/action_controller/assertions/model_assertions.rb +21 -0
  10. data/lib/action_controller/assertions/response_assertions.rb +169 -0
  11. data/lib/action_controller/assertions/routing_assertions.rb +146 -0
  12. data/lib/action_controller/assertions/selector_assertions.rb +638 -0
  13. data/lib/action_controller/assertions/tag_assertions.rb +127 -0
  14. data/lib/action_controller/base.rb +1425 -0
  15. data/lib/action_controller/benchmarking.rb +107 -0
  16. data/lib/action_controller/caching.rb +71 -0
  17. data/lib/action_controller/caching/actions.rb +177 -0
  18. data/lib/action_controller/caching/fragments.rb +120 -0
  19. data/lib/action_controller/caching/pages.rb +152 -0
  20. data/lib/action_controller/caching/sweeper.rb +45 -0
  21. data/lib/action_controller/caching/sweeping.rb +55 -0
  22. data/lib/action_controller/cgi_ext.rb +15 -0
  23. data/lib/action_controller/cgi_ext/cookie.rb +112 -0
  24. data/lib/action_controller/cgi_ext/query_extension.rb +22 -0
  25. data/lib/action_controller/cgi_ext/stdinput.rb +24 -0
  26. data/lib/action_controller/cgi_process.rb +77 -0
  27. data/lib/action_controller/cookies.rb +197 -0
  28. data/lib/action_controller/dispatcher.rb +133 -0
  29. data/lib/action_controller/failsafe.rb +87 -0
  30. data/lib/action_controller/filters.rb +680 -0
  31. data/lib/action_controller/flash.rb +213 -0
  32. data/lib/action_controller/headers.rb +33 -0
  33. data/lib/action_controller/helpers.rb +225 -0
  34. data/lib/action_controller/http_authentication.rb +309 -0
  35. data/lib/action_controller/integration.rb +708 -0
  36. data/lib/action_controller/layout.rb +286 -0
  37. data/lib/action_controller/middleware_stack.rb +119 -0
  38. data/lib/action_controller/middlewares.rb +14 -0
  39. data/lib/action_controller/mime_responds.rb +193 -0
  40. data/lib/action_controller/mime_type.rb +212 -0
  41. data/lib/action_controller/mime_types.rb +21 -0
  42. data/lib/action_controller/params_parser.rb +77 -0
  43. data/lib/action_controller/performance_test.rb +15 -0
  44. data/lib/action_controller/polymorphic_routes.rb +189 -0
  45. data/lib/action_controller/rack_lint_patch.rb +36 -0
  46. data/lib/action_controller/record_identifier.rb +104 -0
  47. data/lib/action_controller/reloader.rb +54 -0
  48. data/lib/action_controller/request.rb +518 -0
  49. data/lib/action_controller/request_forgery_protection.rb +116 -0
  50. data/lib/action_controller/rescue.rb +183 -0
  51. data/lib/action_controller/resources.rb +682 -0
  52. data/lib/action_controller/response.rb +237 -0
  53. data/lib/action_controller/routing.rb +388 -0
  54. data/lib/action_controller/routing/builder.rb +197 -0
  55. data/lib/action_controller/routing/optimisations.rb +130 -0
  56. data/lib/action_controller/routing/recognition_optimisation.rb +167 -0
  57. data/lib/action_controller/routing/route.rb +265 -0
  58. data/lib/action_controller/routing/route_set.rb +503 -0
  59. data/lib/action_controller/routing/routing_ext.rb +49 -0
  60. data/lib/action_controller/routing/segments.rb +343 -0
  61. data/lib/action_controller/session/abstract_store.rb +276 -0
  62. data/lib/action_controller/session/cookie_store.rb +240 -0
  63. data/lib/action_controller/session/mem_cache_store.rb +60 -0
  64. data/lib/action_controller/session_management.rb +54 -0
  65. data/lib/action_controller/status_codes.rb +88 -0
  66. data/lib/action_controller/streaming.rb +181 -0
  67. data/lib/action_controller/string_coercion.rb +29 -0
  68. data/lib/action_controller/templates/rescues/_request_and_response.erb +24 -0
  69. data/lib/action_controller/templates/rescues/_trace.erb +26 -0
  70. data/lib/action_controller/templates/rescues/diagnostics.erb +11 -0
  71. data/lib/action_controller/templates/rescues/layout.erb +29 -0
  72. data/lib/action_controller/templates/rescues/missing_template.erb +2 -0
  73. data/lib/action_controller/templates/rescues/routing_error.erb +10 -0
  74. data/lib/action_controller/templates/rescues/template_error.erb +21 -0
  75. data/lib/action_controller/templates/rescues/unknown_action.erb +2 -0
  76. data/lib/action_controller/test_case.rb +209 -0
  77. data/lib/action_controller/test_process.rb +580 -0
  78. data/lib/action_controller/translation.rb +13 -0
  79. data/lib/action_controller/uploaded_file.rb +44 -0
  80. data/lib/action_controller/url_rewriter.rb +229 -0
  81. data/lib/action_controller/vendor/html-scanner.rb +16 -0
  82. data/lib/action_controller/vendor/html-scanner/html/document.rb +68 -0
  83. data/lib/action_controller/vendor/html-scanner/html/node.rb +537 -0
  84. data/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +173 -0
  85. data/lib/action_controller/vendor/html-scanner/html/selector.rb +828 -0
  86. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +105 -0
  87. data/lib/action_controller/vendor/html-scanner/html/version.rb +11 -0
  88. data/lib/action_controller/verification.rb +130 -0
  89. data/lib/action_pack.rb +24 -0
  90. data/lib/action_pack/version.rb +9 -0
  91. data/lib/action_view.rb +58 -0
  92. data/lib/action_view/base.rb +362 -0
  93. data/lib/action_view/helpers.rb +61 -0
  94. data/lib/action_view/helpers/active_record_helper.rb +305 -0
  95. data/lib/action_view/helpers/asset_tag_helper.rb +695 -0
  96. data/lib/action_view/helpers/atom_feed_helper.rb +198 -0
  97. data/lib/action_view/helpers/benchmark_helper.rb +54 -0
  98. data/lib/action_view/helpers/cache_helper.rb +39 -0
  99. data/lib/action_view/helpers/capture_helper.rb +136 -0
  100. data/lib/action_view/helpers/csrf_helper.rb +14 -0
  101. data/lib/action_view/helpers/date_helper.rb +989 -0
  102. data/lib/action_view/helpers/debug_helper.rb +38 -0
  103. data/lib/action_view/helpers/form_helper.rb +1118 -0
  104. data/lib/action_view/helpers/form_options_helper.rb +599 -0
  105. data/lib/action_view/helpers/form_tag_helper.rb +490 -0
  106. data/lib/action_view/helpers/javascript_helper.rb +208 -0
  107. data/lib/action_view/helpers/number_helper.rb +308 -0
  108. data/lib/action_view/helpers/prototype_helper.rb +1305 -0
  109. data/lib/action_view/helpers/raw_output_helper.rb +9 -0
  110. data/lib/action_view/helpers/record_identification_helper.rb +20 -0
  111. data/lib/action_view/helpers/record_tag_helper.rb +58 -0
  112. data/lib/action_view/helpers/sanitize_helper.rb +251 -0
  113. data/lib/action_view/helpers/scriptaculous_helper.rb +226 -0
  114. data/lib/action_view/helpers/tag_helper.rb +151 -0
  115. data/lib/action_view/helpers/text_helper.rb +597 -0
  116. data/lib/action_view/helpers/translation_helper.rb +67 -0
  117. data/lib/action_view/helpers/url_helper.rb +637 -0
  118. data/lib/action_view/inline_template.rb +19 -0
  119. data/lib/action_view/locale/en.yml +117 -0
  120. data/lib/action_view/partials.rb +241 -0
  121. data/lib/action_view/paths.rb +77 -0
  122. data/lib/action_view/reloadable_template.rb +117 -0
  123. data/lib/action_view/renderable.rb +109 -0
  124. data/lib/action_view/renderable_partial.rb +53 -0
  125. data/lib/action_view/template.rb +252 -0
  126. data/lib/action_view/template_error.rb +99 -0
  127. data/lib/action_view/template_handler.rb +34 -0
  128. data/lib/action_view/template_handlers.rb +48 -0
  129. data/lib/action_view/template_handlers/builder.rb +17 -0
  130. data/lib/action_view/template_handlers/erb.rb +25 -0
  131. data/lib/action_view/template_handlers/rjs.rb +13 -0
  132. data/lib/action_view/test_case.rb +162 -0
  133. data/lib/actionpack.rb +2 -0
  134. data/test/abstract_unit.rb +78 -0
  135. data/test/active_record_unit.rb +104 -0
  136. data/test/activerecord/active_record_store_test.rb +221 -0
  137. data/test/activerecord/render_partial_with_record_identification_test.rb +188 -0
  138. data/test/adv_attr_test.rb +20 -0
  139. data/test/controller/action_pack_assertions_test.rb +545 -0
  140. data/test/controller/addresses_render_test.rb +37 -0
  141. data/test/controller/assert_select_test.rb +735 -0
  142. data/test/controller/base_test.rb +217 -0
  143. data/test/controller/benchmark_test.rb +32 -0
  144. data/test/controller/caching_test.rb +743 -0
  145. data/test/controller/capture_test.rb +66 -0
  146. data/test/controller/content_type_test.rb +178 -0
  147. data/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb +0 -0
  148. data/test/controller/controller_fixtures/app/controllers/user_controller.rb +0 -0
  149. data/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb +0 -0
  150. data/test/controller/cookie_test.rb +208 -0
  151. data/test/controller/deprecation/deprecated_base_methods_test.rb +32 -0
  152. data/test/controller/dispatcher_test.rb +144 -0
  153. data/test/controller/dom_assertions_test.rb +53 -0
  154. data/test/controller/failsafe_test.rb +60 -0
  155. data/test/controller/fake_controllers.rb +33 -0
  156. data/test/controller/fake_models.rb +19 -0
  157. data/test/controller/filter_params_test.rb +52 -0
  158. data/test/controller/filters_test.rb +885 -0
  159. data/test/controller/flash_test.rb +174 -0
  160. data/test/controller/header_test.rb +14 -0
  161. data/test/controller/helper_test.rb +224 -0
  162. data/test/controller/html-scanner/cdata_node_test.rb +15 -0
  163. data/test/controller/html-scanner/document_test.rb +148 -0
  164. data/test/controller/html-scanner/node_test.rb +89 -0
  165. data/test/controller/html-scanner/sanitizer_test.rb +281 -0
  166. data/test/controller/html-scanner/tag_node_test.rb +238 -0
  167. data/test/controller/html-scanner/text_node_test.rb +50 -0
  168. data/test/controller/html-scanner/tokenizer_test.rb +131 -0
  169. data/test/controller/http_basic_authentication_test.rb +113 -0
  170. data/test/controller/http_digest_authentication_test.rb +254 -0
  171. data/test/controller/integration_test.rb +526 -0
  172. data/test/controller/layout_test.rb +215 -0
  173. data/test/controller/localized_templates_test.rb +24 -0
  174. data/test/controller/logging_test.rb +46 -0
  175. data/test/controller/middleware_stack_test.rb +90 -0
  176. data/test/controller/mime_responds_test.rb +536 -0
  177. data/test/controller/mime_type_test.rb +93 -0
  178. data/test/controller/output_escaping_test.rb +19 -0
  179. data/test/controller/polymorphic_routes_test.rb +297 -0
  180. data/test/controller/rack_test.rb +308 -0
  181. data/test/controller/record_identifier_test.rb +139 -0
  182. data/test/controller/redirect_test.rb +285 -0
  183. data/test/controller/reloader_test.rb +125 -0
  184. data/test/controller/render_test.rb +1783 -0
  185. data/test/controller/request/json_params_parsing_test.rb +65 -0
  186. data/test/controller/request/multipart_params_parsing_test.rb +177 -0
  187. data/test/controller/request/query_string_parsing_test.rb +129 -0
  188. data/test/controller/request/test_request_test.rb +35 -0
  189. data/test/controller/request/url_encoded_params_parsing_test.rb +146 -0
  190. data/test/controller/request/xml_params_parsing_test.rb +103 -0
  191. data/test/controller/request_forgery_protection_test.rb +233 -0
  192. data/test/controller/request_test.rb +398 -0
  193. data/test/controller/rescue_test.rb +541 -0
  194. data/test/controller/resources_test.rb +1393 -0
  195. data/test/controller/routing_test.rb +2592 -0
  196. data/test/controller/selector_test.rb +628 -0
  197. data/test/controller/send_file_test.rb +171 -0
  198. data/test/controller/session/abstract_store_test.rb +64 -0
  199. data/test/controller/session/cookie_store_test.rb +354 -0
  200. data/test/controller/session/mem_cache_store_test.rb +187 -0
  201. data/test/controller/session/test_session_test.rb +58 -0
  202. data/test/controller/test_test.rb +700 -0
  203. data/test/controller/translation_test.rb +26 -0
  204. data/test/controller/url_rewriter_test.rb +395 -0
  205. data/test/controller/verification_test.rb +270 -0
  206. data/test/controller/view_paths_test.rb +141 -0
  207. data/test/controller/webservice_test.rb +273 -0
  208. data/test/fixtures/_top_level_partial.html.erb +1 -0
  209. data/test/fixtures/_top_level_partial_only.erb +1 -0
  210. data/test/fixtures/addresses/list.erb +1 -0
  211. data/test/fixtures/alternate_helpers/foo_helper.rb +3 -0
  212. data/test/fixtures/bad_customers/_bad_customer.html.erb +1 -0
  213. data/test/fixtures/companies.yml +24 -0
  214. data/test/fixtures/company.rb +10 -0
  215. data/test/fixtures/content_type/render_default_content_types_for_respond_to.rhtml +1 -0
  216. data/test/fixtures/content_type/render_default_for_rhtml.rhtml +1 -0
  217. data/test/fixtures/content_type/render_default_for_rjs.rjs +1 -0
  218. data/test/fixtures/content_type/render_default_for_rxml.rxml +1 -0
  219. data/test/fixtures/customers/_customer.html.erb +1 -0
  220. data/test/fixtures/db_definitions/sqlite.sql +49 -0
  221. data/test/fixtures/developer.rb +9 -0
  222. data/test/fixtures/developers.yml +21 -0
  223. data/test/fixtures/developers/_developer.erb +1 -0
  224. data/test/fixtures/developers_projects.yml +13 -0
  225. data/test/fixtures/failsafe/500.html +1 -0
  226. data/test/fixtures/fun/games/_game.erb +1 -0
  227. data/test/fixtures/fun/games/hello_world.erb +1 -0
  228. data/test/fixtures/fun/serious/games/_game.erb +1 -0
  229. data/test/fixtures/functional_caching/_partial.erb +3 -0
  230. data/test/fixtures/functional_caching/formatted_fragment_cached.html.erb +3 -0
  231. data/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs +6 -0
  232. data/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder +5 -0
  233. data/test/fixtures/functional_caching/fragment_cached.html.erb +2 -0
  234. data/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb +1 -0
  235. data/test/fixtures/functional_caching/inline_fragment_cached.html.erb +2 -0
  236. data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
  237. data/test/fixtures/good_customers/_good_customer.html.erb +1 -0
  238. data/test/fixtures/helpers/abc_helper.rb +5 -0
  239. data/test/fixtures/helpers/fun/games_helper.rb +3 -0
  240. data/test/fixtures/helpers/fun/pdf_helper.rb +3 -0
  241. data/test/fixtures/layout_tests/abs_path_layout.rhtml +1 -0
  242. data/test/fixtures/layout_tests/alt/hello.rhtml +1 -0
  243. data/test/fixtures/layout_tests/alt/layouts/alt.rhtml +0 -0
  244. data/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +1 -0
  245. data/test/fixtures/layout_tests/layouts/item.rhtml +1 -0
  246. data/test/fixtures/layout_tests/layouts/layout_test.rhtml +1 -0
  247. data/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb +1 -0
  248. data/test/fixtures/layout_tests/layouts/third_party_template_library.mab +1 -0
  249. data/test/fixtures/layout_tests/views/hello.rhtml +1 -0
  250. data/test/fixtures/layouts/_column.html.erb +2 -0
  251. data/test/fixtures/layouts/block_with_layout.erb +3 -0
  252. data/test/fixtures/layouts/builder.builder +3 -0
  253. data/test/fixtures/layouts/default_html.html.erb +1 -0
  254. data/test/fixtures/layouts/partial_with_layout.erb +3 -0
  255. data/test/fixtures/layouts/standard.erb +1 -0
  256. data/test/fixtures/layouts/talk_from_action.erb +2 -0
  257. data/test/fixtures/layouts/xhr.html.erb +2 -0
  258. data/test/fixtures/layouts/yield.erb +2 -0
  259. data/test/fixtures/localized/hello_world.de.html +1 -0
  260. data/test/fixtures/localized/hello_world.en.html +1 -0
  261. data/test/fixtures/mascot.rb +3 -0
  262. data/test/fixtures/mascots.yml +4 -0
  263. data/test/fixtures/mascots/_mascot.html.erb +1 -0
  264. data/test/fixtures/multipart/binary_file +0 -0
  265. data/test/fixtures/multipart/boundary_problem_file +10 -0
  266. data/test/fixtures/multipart/bracketed_param +5 -0
  267. data/test/fixtures/multipart/empty +10 -0
  268. data/test/fixtures/multipart/hello.txt +1 -0
  269. data/test/fixtures/multipart/large_text_file +10 -0
  270. data/test/fixtures/multipart/mixed_files +0 -0
  271. data/test/fixtures/multipart/mona_lisa.jpg +0 -0
  272. data/test/fixtures/multipart/none +9 -0
  273. data/test/fixtures/multipart/single_parameter +5 -0
  274. data/test/fixtures/multipart/text_file +10 -0
  275. data/test/fixtures/override/test/hello_world.erb +1 -0
  276. data/test/fixtures/override2/layouts/test/sub.erb +1 -0
  277. data/test/fixtures/post_test/layouts/post.html.erb +1 -0
  278. data/test/fixtures/post_test/layouts/super_post.iphone.erb +1 -0
  279. data/test/fixtures/post_test/post/index.html.erb +1 -0
  280. data/test/fixtures/post_test/post/index.iphone.erb +1 -0
  281. data/test/fixtures/post_test/super_post/index.html.erb +1 -0
  282. data/test/fixtures/post_test/super_post/index.iphone.erb +1 -0
  283. data/test/fixtures/project.rb +3 -0
  284. data/test/fixtures/projects.yml +7 -0
  285. data/test/fixtures/projects/_project.erb +1 -0
  286. data/test/fixtures/public/404.html +1 -0
  287. data/test/fixtures/public/500.da.html +1 -0
  288. data/test/fixtures/public/500.html +1 -0
  289. data/test/fixtures/public/absolute/test.css +23 -0
  290. data/test/fixtures/public/absolute/test.js +63 -0
  291. data/test/fixtures/public/images/rails.png +0 -0
  292. data/test/fixtures/public/javascripts/application.js +1 -0
  293. data/test/fixtures/public/javascripts/bank.js +1 -0
  294. data/test/fixtures/public/javascripts/controls.js +1 -0
  295. data/test/fixtures/public/javascripts/dragdrop.js +1 -0
  296. data/test/fixtures/public/javascripts/effects.js +1 -0
  297. data/test/fixtures/public/javascripts/prototype.js +1 -0
  298. data/test/fixtures/public/javascripts/robber.js +1 -0
  299. data/test/fixtures/public/javascripts/subdir/subdir.js +1 -0
  300. data/test/fixtures/public/javascripts/version.1.0.js +1 -0
  301. data/test/fixtures/public/stylesheets/bank.css +1 -0
  302. data/test/fixtures/public/stylesheets/robber.css +1 -0
  303. data/test/fixtures/public/stylesheets/subdir/subdir.css +1 -0
  304. data/test/fixtures/public/stylesheets/version.1.0.css +1 -0
  305. data/test/fixtures/quiz/questions/_question.html.erb +1 -0
  306. data/test/fixtures/replies.yml +15 -0
  307. data/test/fixtures/replies/_reply.erb +1 -0
  308. data/test/fixtures/reply.rb +7 -0
  309. data/test/fixtures/respond_to/all_types_with_layout.html.erb +1 -0
  310. data/test/fixtures/respond_to/all_types_with_layout.js.rjs +1 -0
  311. data/test/fixtures/respond_to/custom_constant_handling_without_block.mobile.erb +1 -0
  312. data/test/fixtures/respond_to/iphone_with_html_response_type.html.erb +1 -0
  313. data/test/fixtures/respond_to/iphone_with_html_response_type.iphone.erb +1 -0
  314. data/test/fixtures/respond_to/layouts/missing.html.erb +1 -0
  315. data/test/fixtures/respond_to/layouts/standard.html.erb +1 -0
  316. data/test/fixtures/respond_to/layouts/standard.iphone.erb +1 -0
  317. data/test/fixtures/respond_to/using_defaults.html.erb +1 -0
  318. data/test/fixtures/respond_to/using_defaults.js.rjs +1 -0
  319. data/test/fixtures/respond_to/using_defaults.xml.builder +1 -0
  320. data/test/fixtures/respond_to/using_defaults_with_type_list.html.erb +1 -0
  321. data/test/fixtures/respond_to/using_defaults_with_type_list.js.rjs +1 -0
  322. data/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder +1 -0
  323. data/test/fixtures/scope/test/modgreet.erb +1 -0
  324. data/test/fixtures/session_autoload_test/session_autoload_test/foo.rb +10 -0
  325. data/test/fixtures/shared.html.erb +1 -0
  326. data/test/fixtures/symlink_parent/symlinked_layout.erb +5 -0
  327. data/test/fixtures/test/_counter.html.erb +1 -0
  328. data/test/fixtures/test/_customer.erb +1 -0
  329. data/test/fixtures/test/_customer_counter.erb +1 -0
  330. data/test/fixtures/test/_customer_counter_with_as.erb +1 -0
  331. data/test/fixtures/test/_customer_greeting.erb +1 -0
  332. data/test/fixtures/test/_customer_with_var.erb +1 -0
  333. data/test/fixtures/test/_form.erb +1 -0
  334. data/test/fixtures/test/_from_helper.erb +1 -0
  335. data/test/fixtures/test/_hash_greeting.erb +1 -0
  336. data/test/fixtures/test/_hash_object.erb +2 -0
  337. data/test/fixtures/test/_hello.builder +1 -0
  338. data/test/fixtures/test/_labelling_form.erb +1 -0
  339. data/test/fixtures/test/_layout_for_block_with_args.html.erb +3 -0
  340. data/test/fixtures/test/_layout_for_partial.html.erb +3 -0
  341. data/test/fixtures/test/_local_inspector.html.erb +1 -0
  342. data/test/fixtures/test/_one.html.erb +1 -0
  343. data/test/fixtures/test/_partial.erb +1 -0
  344. data/test/fixtures/test/_partial.html.erb +1 -0
  345. data/test/fixtures/test/_partial.js.erb +1 -0
  346. data/test/fixtures/test/_partial_for_use_in_layout.html.erb +1 -0
  347. data/test/fixtures/test/_partial_only.erb +1 -0
  348. data/test/fixtures/test/_partial_with_only_html_version.html.erb +1 -0
  349. data/test/fixtures/test/_person.erb +2 -0
  350. data/test/fixtures/test/_raise.html.erb +1 -0
  351. data/test/fixtures/test/_two.html.erb +1 -0
  352. data/test/fixtures/test/_utf8_partial.html.erb +1 -0
  353. data/test/fixtures/test/_utf8_partial_magic.html.erb +2 -0
  354. data/test/fixtures/test/action_talk_to_layout.erb +2 -0
  355. data/test/fixtures/test/array_translation.erb +1 -0
  356. data/test/fixtures/test/calling_partial_with_layout.html.erb +1 -0
  357. data/test/fixtures/test/capturing.erb +4 -0
  358. data/test/fixtures/test/content_for.erb +2 -0
  359. data/test/fixtures/test/content_for_concatenated.erb +3 -0
  360. data/test/fixtures/test/content_for_with_parameter.erb +2 -0
  361. data/test/fixtures/test/delete_with_js.rjs +2 -0
  362. data/test/fixtures/test/dont_pick_me +1 -0
  363. data/test/fixtures/test/dot.directory/render_file_with_ivar.erb +1 -0
  364. data/test/fixtures/test/enum_rjs_test.rjs +6 -0
  365. data/test/fixtures/test/formatted_html_erb.html.erb +1 -0
  366. data/test/fixtures/test/formatted_xml_erb.builder +1 -0
  367. data/test/fixtures/test/formatted_xml_erb.html.erb +1 -0
  368. data/test/fixtures/test/formatted_xml_erb.xml.erb +1 -0
  369. data/test/fixtures/test/greeting.erb +1 -0
  370. data/test/fixtures/test/greeting.js.rjs +1 -0
  371. data/test/fixtures/test/hello.builder +4 -0
  372. data/test/fixtures/test/hello_world.da.html.erb +1 -0
  373. data/test/fixtures/test/hello_world.erb +1 -0
  374. data/test/fixtures/test/hello_world.erb~ +1 -0
  375. data/test/fixtures/test/hello_world.pt-BR.html.erb +1 -0
  376. data/test/fixtures/test/hello_world_container.builder +3 -0
  377. data/test/fixtures/test/hello_world_from_rxml.builder +4 -0
  378. data/test/fixtures/test/hello_world_with_layout_false.erb +1 -0
  379. data/test/fixtures/test/hello_xml_world.builder +11 -0
  380. data/test/fixtures/test/hyphen-ated.erb +1 -0
  381. data/test/fixtures/test/implicit_content_type.atom.builder +2 -0
  382. data/test/fixtures/test/list.erb +1 -0
  383. data/test/fixtures/test/malformed/malformed.en.html.erb~ +1 -0
  384. data/test/fixtures/test/malformed/malformed.erb~ +1 -0
  385. data/test/fixtures/test/malformed/malformed.html.erb~ +1 -0
  386. data/test/fixtures/test/nested_layout.erb +3 -0
  387. data/test/fixtures/test/non_erb_block_content_for.builder +4 -0
  388. data/test/fixtures/test/potential_conflicts.erb +4 -0
  389. data/test/fixtures/test/render_explicit_html_template.js.rjs +1 -0
  390. data/test/fixtures/test/render_file_from_template.html.erb +1 -0
  391. data/test/fixtures/test/render_file_with_ivar.erb +1 -0
  392. data/test/fixtures/test/render_file_with_locals.erb +1 -0
  393. data/test/fixtures/test/render_implicit_html_template.js.rjs +1 -0
  394. data/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb +1 -0
  395. data/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb +1 -0
  396. data/test/fixtures/test/render_implicit_js_template_without_layout.js.erb +1 -0
  397. data/test/fixtures/test/render_to_string_test.erb +1 -0
  398. data/test/fixtures/test/scoped_array_translation.erb +1 -0
  399. data/test/fixtures/test/sub_template_raise.html.erb +1 -0
  400. data/test/fixtures/test/template.erb +1 -0
  401. data/test/fixtures/test/translation.erb +1 -0
  402. data/test/fixtures/test/update_element_with_capture.erb +9 -0
  403. data/test/fixtures/test/using_layout_around_block.html.erb +1 -0
  404. data/test/fixtures/test/using_layout_around_block_with_args.html.erb +1 -0
  405. data/test/fixtures/test/utf8.html.erb +4 -0
  406. data/test/fixtures/test/utf8_magic.html.erb +5 -0
  407. data/test/fixtures/test/utf8_magic_with_bare_partial.html.erb +5 -0
  408. data/test/fixtures/topic.rb +3 -0
  409. data/test/fixtures/topics.yml +22 -0
  410. data/test/fixtures/topics/_topic.html.erb +1 -0
  411. data/test/template/active_record_helper_i18n_test.rb +51 -0
  412. data/test/template/active_record_helper_test.rb +302 -0
  413. data/test/template/asset_tag_helper_test.rb +770 -0
  414. data/test/template/atom_feed_helper_test.rb +315 -0
  415. data/test/template/benchmark_helper_test.rb +86 -0
  416. data/test/template/compiled_templates_test.rb +204 -0
  417. data/test/template/date_helper_i18n_test.rb +121 -0
  418. data/test/template/date_helper_test.rb +2603 -0
  419. data/test/template/erb_util_test.rb +36 -0
  420. data/test/template/form_helper_test.rb +1447 -0
  421. data/test/template/form_options_helper_i18n_test.rb +27 -0
  422. data/test/template/form_options_helper_test.rb +811 -0
  423. data/test/template/form_tag_helper_test.rb +356 -0
  424. data/test/template/javascript_helper_test.rb +106 -0
  425. data/test/template/number_helper_i18n_test.rb +69 -0
  426. data/test/template/number_helper_test.rb +132 -0
  427. data/test/template/prototype_helper_test.rb +639 -0
  428. data/test/template/raw_output_helper_test.rb +21 -0
  429. data/test/template/record_tag_helper_test.rb +58 -0
  430. data/test/template/render_test.rb +329 -0
  431. data/test/template/sanitize_helper_test.rb +57 -0
  432. data/test/template/scriptaculous_helper_test.rb +90 -0
  433. data/test/template/tag_helper_test.rb +98 -0
  434. data/test/template/template_test.rb +32 -0
  435. data/test/template/test_test.rb +54 -0
  436. data/test/template/text_helper_test.rb +601 -0
  437. data/test/template/translation_helper_test.rb +95 -0
  438. data/test/template/url_helper_test.rb +641 -0
  439. data/test/testing_sandbox.rb +15 -0
  440. data/test/view/test_case_test.rb +176 -0
  441. metadata +519 -0
@@ -0,0 +1,173 @@
1
+ module HTML
2
+ class Sanitizer
3
+ def sanitize(text, options = {})
4
+ return text unless sanitizeable?(text)
5
+ tokenize(text, options).join
6
+ end
7
+
8
+ def sanitizeable?(text)
9
+ !(text.nil? || text.empty? || !text.index("<"))
10
+ end
11
+
12
+ protected
13
+ def tokenize(text, options)
14
+ tokenizer = HTML::Tokenizer.new(text)
15
+ result = []
16
+ while token = tokenizer.next
17
+ node = Node.parse(nil, 0, 0, token, false)
18
+ process_node node, result, options
19
+ end
20
+ result
21
+ end
22
+
23
+ def process_node(node, result, options)
24
+ result << node.to_s
25
+ end
26
+ end
27
+
28
+ class FullSanitizer < Sanitizer
29
+ def sanitize(text, options = {})
30
+ result = super
31
+ # strip any comments, and if they have a newline at the end (ie. line with
32
+ # only a comment) strip that too
33
+ result.gsub!(/<!--(.*?)-->[\n]?/m, "") if result
34
+ # Recurse - handle all dirty nested tags
35
+ result == text ? result : sanitize(result, options)
36
+ end
37
+
38
+ def process_node(node, result, options)
39
+ result << node.to_s if node.class == HTML::Text
40
+ end
41
+ end
42
+
43
+ class LinkSanitizer < FullSanitizer
44
+ cattr_accessor :included_tags, :instance_writer => false
45
+ self.included_tags = Set.new(%w(a href))
46
+
47
+ def sanitizeable?(text)
48
+ !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
49
+ end
50
+
51
+ protected
52
+ def process_node(node, result, options)
53
+ result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
54
+ end
55
+ end
56
+
57
+ class WhiteListSanitizer < Sanitizer
58
+ [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
59
+ :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
60
+ class_inheritable_accessor attr, :instance_writer => false
61
+ end
62
+
63
+ # A regular expression of the valid characters used to separate protocols like
64
+ # the ':' in 'http://foo.com'
65
+ self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
66
+
67
+ # Specifies a Set of HTML attributes that can have URIs.
68
+ self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
69
+
70
+ # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
71
+ # to just escaping harmless tags like &lt;font&gt;
72
+ self.bad_tags = Set.new(%w(script))
73
+
74
+ # Specifies the default Set of tags that the #sanitize helper will allow unscathed.
75
+ self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
76
+ sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dt dd abbr
77
+ acronym a img blockquote del ins))
78
+
79
+ # Specifies the default Set of html attributes that the #sanitize helper will leave
80
+ # in the allowed tag.
81
+ self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
82
+
83
+ # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
84
+ self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
85
+ feed svn urn aim rsync tag ssh sftp rtsp afs))
86
+
87
+ # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
88
+ self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
89
+ border-color border-left-color border-right-color border-top-color clear color cursor direction display
90
+ elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
91
+ overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
92
+ speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
93
+ width))
94
+
95
+ # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
96
+ self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
97
+ collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
98
+ nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
99
+
100
+ # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
101
+ self.shorthand_css_properties = Set.new(%w(background border margin padding))
102
+
103
+ # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
104
+ def sanitize_css(style)
105
+ # disallow urls
106
+ style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
107
+
108
+ # gauntlet
109
+ if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ ||
110
+ style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*$/
111
+ return ''
112
+ end
113
+
114
+ clean = []
115
+ style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
116
+ if allowed_css_properties.include?(prop.downcase)
117
+ clean << prop + ': ' + val + ';'
118
+ elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
119
+ unless val.split().any? do |keyword|
120
+ !allowed_css_keywords.include?(keyword) &&
121
+ keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
122
+ end
123
+ clean << prop + ': ' + val + ';'
124
+ end
125
+ end
126
+ end
127
+ clean.join(' ')
128
+ end
129
+
130
+ protected
131
+ def tokenize(text, options)
132
+ options[:parent] = []
133
+ options[:attributes] ||= allowed_attributes
134
+ options[:tags] ||= allowed_tags
135
+ super
136
+ end
137
+
138
+ def process_node(node, result, options)
139
+ result << case node
140
+ when HTML::Tag
141
+ if node.closing == :close
142
+ options[:parent].shift
143
+ else
144
+ options[:parent].unshift node.name
145
+ end
146
+
147
+ process_attributes_for node, options
148
+
149
+ options[:tags].include?(node.name) ? node : nil
150
+ else
151
+ bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
152
+ end
153
+ end
154
+
155
+ def process_attributes_for(node, options)
156
+ return unless node.attributes
157
+ node.attributes.keys.each do |attr_name|
158
+ value = node.attributes[attr_name].to_s
159
+
160
+ if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
161
+ node.attributes.delete(attr_name)
162
+ else
163
+ node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
164
+ end
165
+ end
166
+ end
167
+
168
+ def contains_bad_protocols?(attr_name, value)
169
+ uri_attributes.include?(attr_name) &&
170
+ (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first))
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,828 @@
1
+ #--
2
+ # Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
3
+ # Under MIT and/or CC By license.
4
+ #++
5
+
6
+ module HTML
7
+
8
+ # Selects HTML elements using CSS 2 selectors.
9
+ #
10
+ # The +Selector+ class uses CSS selector expressions to match and select
11
+ # HTML elements.
12
+ #
13
+ # For example:
14
+ # selector = HTML::Selector.new "form.login[action=/login]"
15
+ # creates a new selector that matches any +form+ element with the class
16
+ # +login+ and an attribute +action+ with the value <tt>/login</tt>.
17
+ #
18
+ # === Matching Elements
19
+ #
20
+ # Use the #match method to determine if an element matches the selector.
21
+ #
22
+ # For simple selectors, the method returns an array with that element,
23
+ # or +nil+ if the element does not match. For complex selectors (see below)
24
+ # the method returns an array with all matched elements, of +nil+ if no
25
+ # match found.
26
+ #
27
+ # For example:
28
+ # if selector.match(element)
29
+ # puts "Element is a login form"
30
+ # end
31
+ #
32
+ # === Selecting Elements
33
+ #
34
+ # Use the #select method to select all matching elements starting with
35
+ # one element and going through all children in depth-first order.
36
+ #
37
+ # This method returns an array of all matching elements, an empty array
38
+ # if no match is found
39
+ #
40
+ # For example:
41
+ # selector = HTML::Selector.new "input[type=text]"
42
+ # matches = selector.select(element)
43
+ # matches.each do |match|
44
+ # puts "Found text field with name #{match.attributes['name']}"
45
+ # end
46
+ #
47
+ # === Expressions
48
+ #
49
+ # Selectors can match elements using any of the following criteria:
50
+ # * <tt>name</tt> -- Match an element based on its name (tag name).
51
+ # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
52
+ # to match any element.
53
+ # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
54
+ # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
55
+ # * <tt>.class</tt> -- Match an element based on its class name, all
56
+ # class names if more than one specified.
57
+ # * <tt>[attr]</tt> -- Match an element that has the specified attribute.
58
+ # * <tt>[attr=value]</tt> -- Match an element that has the specified
59
+ # attribute and value. (More operators are supported see below)
60
+ # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
61
+ # such as <tt>:nth-child</tt> and <tt>:empty</tt>.
62
+ # * <tt>:not(expr)</tt> -- Match an element that does not match the
63
+ # negation expression.
64
+ #
65
+ # When using a combination of the above, the element name comes first
66
+ # followed by identifier, class names, attributes, pseudo classes and
67
+ # negation in any order. Do not separate these parts with spaces!
68
+ # Space separation is used for descendant selectors.
69
+ #
70
+ # For example:
71
+ # selector = HTML::Selector.new "form.login[action=/login]"
72
+ # The matched element must be of type +form+ and have the class +login+.
73
+ # It may have other classes, but the class +login+ is required to match.
74
+ # It must also have an attribute called +action+ with the value
75
+ # <tt>/login</tt>.
76
+ #
77
+ # This selector will match the following element:
78
+ # <form class="login form" method="post" action="/login">
79
+ # but will not match the element:
80
+ # <form method="post" action="/logout">
81
+ #
82
+ # === Attribute Values
83
+ #
84
+ # Several operators are supported for matching attributes:
85
+ # * <tt>name</tt> -- The element must have an attribute with that name.
86
+ # * <tt>name=value</tt> -- The element must have an attribute with that
87
+ # name and value.
88
+ # * <tt>name^=value</tt> -- The attribute value must start with the
89
+ # specified value.
90
+ # * <tt>name$=value</tt> -- The attribute value must end with the
91
+ # specified value.
92
+ # * <tt>name*=value</tt> -- The attribute value must contain the
93
+ # specified value.
94
+ # * <tt>name~=word</tt> -- The attribute value must contain the specified
95
+ # word (space separated).
96
+ # * <tt>name|=word</tt> -- The attribute value must start with specified
97
+ # word.
98
+ #
99
+ # For example, the following two selectors match the same element:
100
+ # #my_id
101
+ # [id=my_id]
102
+ # and so do the following two selectors:
103
+ # .my_class
104
+ # [class~=my_class]
105
+ #
106
+ # === Alternatives, siblings, children
107
+ #
108
+ # Complex selectors use a combination of expressions to match elements:
109
+ # * <tt>expr1 expr2</tt> -- Match any element against the second expression
110
+ # if it has some parent element that matches the first expression.
111
+ # * <tt>expr1 > expr2</tt> -- Match any element against the second expression
112
+ # if it is the child of an element that matches the first expression.
113
+ # * <tt>expr1 + expr2</tt> -- Match any element against the second expression
114
+ # if it immediately follows an element that matches the first expression.
115
+ # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
116
+ # that comes after an element that matches the first expression.
117
+ # * <tt>expr1, expr2</tt> -- Match any element against the first expression,
118
+ # or against the second expression.
119
+ #
120
+ # Since children and sibling selectors may match more than one element given
121
+ # the first element, the #match method may return more than one match.
122
+ #
123
+ # === Pseudo classes
124
+ #
125
+ # Pseudo classes were introduced in CSS 3. They are most often used to select
126
+ # elements in a given position:
127
+ # * <tt>:root</tt> -- Match the element only if it is the root element
128
+ # (no parent element).
129
+ # * <tt>:empty</tt> -- Match the element only if it has no child elements,
130
+ # and no text content.
131
+ # * <tt>:only-child</tt> -- Match the element if it is the only child (element)
132
+ # of its parent element.
133
+ # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
134
+ # of its parent element and its type.
135
+ # * <tt>:first-child</tt> -- Match the element if it is the first child (element)
136
+ # of its parent element.
137
+ # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
138
+ # of its parent element of its type.
139
+ # * <tt>:last-child</tt> -- Match the element if it is the last child (element)
140
+ # of its parent element.
141
+ # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
142
+ # of its parent element of its type.
143
+ # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
144
+ # of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
145
+ # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
146
+ # in each group of <tt>a</tt> child elements of its parent element.
147
+ # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
148
+ # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
149
+ # elements of its parent element.
150
+ # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
151
+ # Same as <tt>:nth-child(2n+1)</tt>.
152
+ # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
153
+ # fourth). Same as <tt>:nth-child(2n+2)</tt>.
154
+ # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
155
+ # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
156
+ # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
157
+ # only elements of its type.
158
+ # * <tt>:not(selector)</tt> -- Match the element only if the element does not
159
+ # match the simple selector.
160
+ #
161
+ # As you can see, <tt>:nth-child<tt> pseudo class and its variant can get quite
162
+ # tricky and the CSS specification doesn't do a much better job explaining it.
163
+ # But after reading the examples and trying a few combinations, it's easy to
164
+ # figure out.
165
+ #
166
+ # For example:
167
+ # table tr:nth-child(odd)
168
+ # Selects every second row in the table starting with the first one.
169
+ #
170
+ # div p:nth-child(4)
171
+ # Selects the fourth paragraph in the +div+, but not if the +div+ contains
172
+ # other elements, since those are also counted.
173
+ #
174
+ # div p:nth-of-type(4)
175
+ # Selects the fourth paragraph in the +div+, counting only paragraphs, and
176
+ # ignoring all other elements.
177
+ #
178
+ # div p:nth-of-type(-n+4)
179
+ # Selects the first four paragraphs, ignoring all others.
180
+ #
181
+ # And you can always select an element that matches one set of rules but
182
+ # not another using <tt>:not</tt>. For example:
183
+ # p:not(.post)
184
+ # Matches all paragraphs that do not have the class <tt>.post</tt>.
185
+ #
186
+ # === Substitution Values
187
+ #
188
+ # You can use substitution with identifiers, class names and element values.
189
+ # A substitution takes the form of a question mark (<tt>?</tt>) and uses the
190
+ # next value in the argument list following the CSS expression.
191
+ #
192
+ # The substitution value may be a string or a regular expression. All other
193
+ # values are converted to strings.
194
+ #
195
+ # For example:
196
+ # selector = HTML::Selector.new "#?", /^\d+$/
197
+ # matches any element whose identifier consists of one or more digits.
198
+ #
199
+ # See http://www.w3.org/TR/css3-selectors/
200
+ class Selector
201
+
202
+
203
+ # An invalid selector.
204
+ class InvalidSelectorError < StandardError #:nodoc:
205
+ end
206
+
207
+
208
+ class << self
209
+
210
+ # :call-seq:
211
+ # Selector.for_class(cls) => selector
212
+ #
213
+ # Creates a new selector for the given class name.
214
+ def for_class(cls)
215
+ self.new([".?", cls])
216
+ end
217
+
218
+
219
+ # :call-seq:
220
+ # Selector.for_id(id) => selector
221
+ #
222
+ # Creates a new selector for the given id.
223
+ def for_id(id)
224
+ self.new(["#?", id])
225
+ end
226
+
227
+ end
228
+
229
+
230
+ # :call-seq:
231
+ # Selector.new(string, [values ...]) => selector
232
+ #
233
+ # Creates a new selector from a CSS 2 selector expression.
234
+ #
235
+ # The first argument is the selector expression. All other arguments
236
+ # are used for value substitution.
237
+ #
238
+ # Throws InvalidSelectorError is the selector expression is invalid.
239
+ def initialize(selector, *values)
240
+ raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
241
+ @source = ""
242
+ values = values[0] if values.size == 1 && values[0].is_a?(Array)
243
+
244
+ # We need a copy to determine if we failed to parse, and also
245
+ # preserve the original pass by-ref statement.
246
+ statement = selector.strip.dup
247
+
248
+ # Create a simple selector, along with negation.
249
+ simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
250
+
251
+ @alternates = []
252
+ @depends = nil
253
+
254
+ # Alternative selector.
255
+ if statement.sub!(/^\s*,\s*/, "")
256
+ second = Selector.new(statement, values)
257
+ @alternates << second
258
+ # If there are alternate selectors, we group them in the top selector.
259
+ if alternates = second.instance_variable_get(:@alternates)
260
+ second.instance_variable_set(:@alternates, [])
261
+ @alternates.concat alternates
262
+ end
263
+ @source << " , " << second.to_s
264
+ # Sibling selector: create a dependency into second selector that will
265
+ # match element immediately following this one.
266
+ elsif statement.sub!(/^\s*\+\s*/, "")
267
+ second = next_selector(statement, values)
268
+ @depends = lambda do |element, first|
269
+ if element = next_element(element)
270
+ second.match(element, first)
271
+ end
272
+ end
273
+ @source << " + " << second.to_s
274
+ # Adjacent selector: create a dependency into second selector that will
275
+ # match all elements following this one.
276
+ elsif statement.sub!(/^\s*~\s*/, "")
277
+ second = next_selector(statement, values)
278
+ @depends = lambda do |element, first|
279
+ matches = []
280
+ while element = next_element(element)
281
+ if subset = second.match(element, first)
282
+ if first && !subset.empty?
283
+ matches << subset.first
284
+ break
285
+ else
286
+ matches.concat subset
287
+ end
288
+ end
289
+ end
290
+ matches.empty? ? nil : matches
291
+ end
292
+ @source << " ~ " << second.to_s
293
+ # Child selector: create a dependency into second selector that will
294
+ # match a child element of this one.
295
+ elsif statement.sub!(/^\s*>\s*/, "")
296
+ second = next_selector(statement, values)
297
+ @depends = lambda do |element, first|
298
+ matches = []
299
+ element.children.each do |child|
300
+ if child.tag? && subset = second.match(child, first)
301
+ if first && !subset.empty?
302
+ matches << subset.first
303
+ break
304
+ else
305
+ matches.concat subset
306
+ end
307
+ end
308
+ end
309
+ matches.empty? ? nil : matches
310
+ end
311
+ @source << " > " << second.to_s
312
+ # Descendant selector: create a dependency into second selector that
313
+ # will match all descendant elements of this one. Note,
314
+ elsif statement =~ /^\s+\S+/ && statement != selector
315
+ second = next_selector(statement, values)
316
+ @depends = lambda do |element, first|
317
+ matches = []
318
+ stack = element.children.reverse
319
+ while node = stack.pop
320
+ next unless node.tag?
321
+ if subset = second.match(node, first)
322
+ if first && !subset.empty?
323
+ matches << subset.first
324
+ break
325
+ else
326
+ matches.concat subset
327
+ end
328
+ elsif children = node.children
329
+ stack.concat children.reverse
330
+ end
331
+ end
332
+ matches.empty? ? nil : matches
333
+ end
334
+ @source << " " << second.to_s
335
+ else
336
+ # The last selector is where we check that we parsed
337
+ # all the parts.
338
+ unless statement.empty? || statement.strip.empty?
339
+ raise ArgumentError, "Invalid selector: #{statement}"
340
+ end
341
+ end
342
+ end
343
+
344
+
345
+ # :call-seq:
346
+ # match(element, first?) => array or nil
347
+ #
348
+ # Matches an element against the selector.
349
+ #
350
+ # For a simple selector this method returns an array with the
351
+ # element if the element matches, nil otherwise.
352
+ #
353
+ # For a complex selector (sibling and descendant) this method
354
+ # returns an array with all matching elements, nil if no match is
355
+ # found.
356
+ #
357
+ # Use +first_only=true+ if you are only interested in the first element.
358
+ #
359
+ # For example:
360
+ # if selector.match(element)
361
+ # puts "Element is a login form"
362
+ # end
363
+ def match(element, first_only = false)
364
+ # Match element if no element name or element name same as element name
365
+ if matched = (!@tag_name || @tag_name == element.name)
366
+ # No match if one of the attribute matches failed
367
+ for attr in @attributes
368
+ if element.attributes[attr[0]] !~ attr[1]
369
+ matched = false
370
+ break
371
+ end
372
+ end
373
+ end
374
+
375
+ # Pseudo class matches (nth-child, empty, etc).
376
+ if matched
377
+ for pseudo in @pseudo
378
+ unless pseudo.call(element)
379
+ matched = false
380
+ break
381
+ end
382
+ end
383
+ end
384
+
385
+ # Negation. Same rules as above, but we fail if a match is made.
386
+ if matched && @negation
387
+ for negation in @negation
388
+ if negation[:tag_name] == element.name
389
+ matched = false
390
+ else
391
+ for attr in negation[:attributes]
392
+ if element.attributes[attr[0]] =~ attr[1]
393
+ matched = false
394
+ break
395
+ end
396
+ end
397
+ end
398
+ if matched
399
+ for pseudo in negation[:pseudo]
400
+ if pseudo.call(element)
401
+ matched = false
402
+ break
403
+ end
404
+ end
405
+ end
406
+ break unless matched
407
+ end
408
+ end
409
+
410
+ # If element matched but depends on another element (child,
411
+ # sibling, etc), apply the dependent matches instead.
412
+ if matched && @depends
413
+ matches = @depends.call(element, first_only)
414
+ else
415
+ matches = matched ? [element] : nil
416
+ end
417
+
418
+ # If this selector is part of the group, try all the alternative
419
+ # selectors (unless first_only).
420
+ if !first_only || !matches
421
+ @alternates.each do |alternate|
422
+ break if matches && first_only
423
+ if subset = alternate.match(element, first_only)
424
+ if matches
425
+ matches.concat subset
426
+ else
427
+ matches = subset
428
+ end
429
+ end
430
+ end
431
+ end
432
+
433
+ matches
434
+ end
435
+
436
+
437
+ # :call-seq:
438
+ # select(root) => array
439
+ #
440
+ # Selects and returns an array with all matching elements, beginning
441
+ # with one node and traversing through all children depth-first.
442
+ # Returns an empty array if no match is found.
443
+ #
444
+ # The root node may be any element in the document, or the document
445
+ # itself.
446
+ #
447
+ # For example:
448
+ # selector = HTML::Selector.new "input[type=text]"
449
+ # matches = selector.select(element)
450
+ # matches.each do |match|
451
+ # puts "Found text field with name #{match.attributes['name']}"
452
+ # end
453
+ def select(root)
454
+ matches = []
455
+ stack = [root]
456
+ while node = stack.pop
457
+ if node.tag? && subset = match(node, false)
458
+ subset.each do |match|
459
+ matches << match unless matches.any? { |item| item.equal?(match) }
460
+ end
461
+ elsif children = node.children
462
+ stack.concat children.reverse
463
+ end
464
+ end
465
+ matches
466
+ end
467
+
468
+
469
+ # Similar to #select but returns the first matching element. Returns +nil+
470
+ # if no element matches the selector.
471
+ def select_first(root)
472
+ stack = [root]
473
+ while node = stack.pop
474
+ if node.tag? && subset = match(node, true)
475
+ return subset.first if !subset.empty?
476
+ elsif children = node.children
477
+ stack.concat children.reverse
478
+ end
479
+ end
480
+ nil
481
+ end
482
+
483
+
484
+ def to_s #:nodoc:
485
+ @source
486
+ end
487
+
488
+
489
+ # Return the next element after this one. Skips sibling text nodes.
490
+ #
491
+ # With the +name+ argument, returns the next element with that name,
492
+ # skipping other sibling elements.
493
+ def next_element(element, name = nil)
494
+ if siblings = element.parent.children
495
+ found = false
496
+ siblings.each do |node|
497
+ if node.equal?(element)
498
+ found = true
499
+ elsif found && node.tag?
500
+ return node if (name.nil? || node.name == name)
501
+ end
502
+ end
503
+ end
504
+ nil
505
+ end
506
+
507
+
508
+ protected
509
+
510
+
511
+ # Creates a simple selector given the statement and array of
512
+ # substitution values.
513
+ #
514
+ # Returns a hash with the values +tag_name+, +attributes+,
515
+ # +pseudo+ (classes) and +negation+.
516
+ #
517
+ # Called the first time with +can_negate+ true to allow
518
+ # negation. Called a second time with false since negation
519
+ # cannot be negated.
520
+ def simple_selector(statement, values, can_negate = true)
521
+ tag_name = nil
522
+ attributes = []
523
+ pseudo = []
524
+ negation = []
525
+
526
+ # Element name. (Note that in negation, this can come at
527
+ # any order, but for simplicity we allow if only first).
528
+ statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
529
+ match.strip!
530
+ tag_name = match.downcase unless match == "*"
531
+ @source << match
532
+ "" # Remove
533
+ end
534
+
535
+ # Get identifier, class, attribute name, pseudo or negation.
536
+ while true
537
+ # Element identifier.
538
+ next if statement.sub!(/^#(\?|[\w\-]+)/) do |match|
539
+ id = $1
540
+ if id == "?"
541
+ id = values.shift
542
+ end
543
+ @source << "##{id}"
544
+ id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
545
+ attributes << ["id", id]
546
+ "" # Remove
547
+ end
548
+
549
+ # Class name.
550
+ next if statement.sub!(/^\.([\w\-]+)/) do |match|
551
+ class_name = $1
552
+ @source << ".#{class_name}"
553
+ class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
554
+ attributes << ["class", class_name]
555
+ "" # Remove
556
+ end
557
+
558
+ # Attribute value.
559
+ next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
560
+ name, equality, value = $1, $2, $3
561
+ if value == "?"
562
+ value = values.shift
563
+ else
564
+ # Handle single and double quotes.
565
+ value.strip!
566
+ if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
567
+ value = value[1..-2]
568
+ end
569
+ end
570
+ @source << "[#{name}#{equality}'#{value}']"
571
+ attributes << [name.downcase.strip, attribute_match(equality, value)]
572
+ "" # Remove
573
+ end
574
+
575
+ # Root element only.
576
+ next if statement.sub!(/^:root/) do |match|
577
+ pseudo << lambda do |element|
578
+ element.parent.nil? || !element.parent.tag?
579
+ end
580
+ @source << ":root"
581
+ "" # Remove
582
+ end
583
+
584
+ # Nth-child including last and of-type.
585
+ next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
586
+ reverse = $1 == "last-"
587
+ of_type = $2 == "of-type"
588
+ @source << ":nth-#{$1}#{$2}("
589
+ case $3
590
+ when "odd"
591
+ pseudo << nth_child(2, 1, of_type, reverse)
592
+ @source << "odd)"
593
+ when "even"
594
+ pseudo << nth_child(2, 2, of_type, reverse)
595
+ @source << "even)"
596
+ when /^(\d+|\?)$/ # b only
597
+ b = ($1 == "?" ? values.shift : $1).to_i
598
+ pseudo << nth_child(0, b, of_type, reverse)
599
+ @source << "#{b})"
600
+ when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
601
+ a = ($1 == "?" ? values.shift :
602
+ $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
603
+ b = ($2 == "?" ? values.shift : $2).to_i
604
+ pseudo << nth_child(a, b, of_type, reverse)
605
+ @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
606
+ else
607
+ raise ArgumentError, "Invalid nth-child #{match}"
608
+ end
609
+ "" # Remove
610
+ end
611
+ # First/last child (of type).
612
+ next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match|
613
+ reverse = $1 == "last"
614
+ of_type = $2 == "of-type"
615
+ pseudo << nth_child(0, 1, of_type, reverse)
616
+ @source << ":#{$1}-#{$2}"
617
+ "" # Remove
618
+ end
619
+ # Only child (of type).
620
+ next if statement.sub!(/^:only-(child|of-type)/) do |match|
621
+ of_type = $1 == "of-type"
622
+ pseudo << only_child(of_type)
623
+ @source << ":only-#{$1}"
624
+ "" # Remove
625
+ end
626
+
627
+ # Empty: no child elements or meaningful content (whitespaces
628
+ # are ignored).
629
+ next if statement.sub!(/^:empty/) do |match|
630
+ pseudo << lambda do |element|
631
+ empty = true
632
+ for child in element.children
633
+ if child.tag? || !child.content.strip.empty?
634
+ empty = false
635
+ break
636
+ end
637
+ end
638
+ empty
639
+ end
640
+ @source << ":empty"
641
+ "" # Remove
642
+ end
643
+ # Content: match the text content of the element, stripping
644
+ # leading and trailing spaces.
645
+ next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match|
646
+ content = $1
647
+ if content == "?"
648
+ content = values.shift
649
+ elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
650
+ content = content[1..-2]
651
+ end
652
+ @source << ":content('#{content}')"
653
+ content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
654
+ pseudo << lambda do |element|
655
+ text = ""
656
+ for child in element.children
657
+ unless child.tag?
658
+ text << child.content
659
+ end
660
+ end
661
+ text.strip =~ content
662
+ end
663
+ "" # Remove
664
+ end
665
+
666
+ # Negation. Create another simple selector to handle it.
667
+ if statement.sub!(/^:not\(\s*/, "")
668
+ raise ArgumentError, "Double negatives are not missing feature" unless can_negate
669
+ @source << ":not("
670
+ negation << simple_selector(statement, values, false)
671
+ raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
672
+ @source << ")"
673
+ next
674
+ end
675
+
676
+ # No match: moving on.
677
+ break
678
+ end
679
+
680
+ # Return hash. The keys are mapped to instance variables.
681
+ {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
682
+ end
683
+
684
+
685
+ # Create a regular expression to match an attribute value based
686
+ # on the equality operator (=, ^=, |=, etc).
687
+ def attribute_match(equality, value)
688
+ regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
689
+ case equality
690
+ when "=" then
691
+ # Match the attribute value in full
692
+ Regexp.new("^#{regexp}$")
693
+ when "~=" then
694
+ # Match a space-separated word within the attribute value
695
+ Regexp.new("(^|\s)#{regexp}($|\s)")
696
+ when "^="
697
+ # Match the beginning of the attribute value
698
+ Regexp.new("^#{regexp}")
699
+ when "$="
700
+ # Match the end of the attribute value
701
+ Regexp.new("#{regexp}$")
702
+ when "*="
703
+ # Match substring of the attribute value
704
+ regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
705
+ when "|=" then
706
+ # Match the first space-separated item of the attribute value
707
+ Regexp.new("^#{regexp}($|\s)")
708
+ else
709
+ raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
710
+ # Match all attributes values (existence check)
711
+ //
712
+ end
713
+ end
714
+
715
+
716
+ # Returns a lambda that can match an element against the nth-child
717
+ # pseudo class, given the following arguments:
718
+ # * +a+ -- Value of a part.
719
+ # * +b+ -- Value of b part.
720
+ # * +of_type+ -- True to test only elements of this type (of-type).
721
+ # * +reverse+ -- True to count in reverse order (last-).
722
+ def nth_child(a, b, of_type, reverse)
723
+ # a = 0 means select at index b, if b = 0 nothing selected
724
+ return lambda { |element| false } if a == 0 && b == 0
725
+ # a < 0 and b < 0 will never match against an index
726
+ return lambda { |element| false } if a < 0 && b < 0
727
+ b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
728
+ b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
729
+ lambda do |element|
730
+ # Element must be inside parent element.
731
+ return false unless element.parent && element.parent.tag?
732
+ index = 0
733
+ # Get siblings, reverse if counting from last.
734
+ siblings = element.parent.children
735
+ siblings = siblings.reverse if reverse
736
+ # Match element name if of-type, otherwise ignore name.
737
+ name = of_type ? element.name : nil
738
+ found = false
739
+ for child in siblings
740
+ # Skip text nodes/comments.
741
+ if child.tag? && (name == nil || child.name == name)
742
+ if a == 0
743
+ # Shortcut when a == 0 no need to go past count
744
+ if index == b
745
+ found = child.equal?(element)
746
+ break
747
+ end
748
+ elsif a < 0
749
+ # Only look for first b elements
750
+ break if index > b
751
+ if child.equal?(element)
752
+ found = (index % a) == 0
753
+ break
754
+ end
755
+ else
756
+ # Otherwise, break if child found and count == an+b
757
+ if child.equal?(element)
758
+ found = (index % a) == b
759
+ break
760
+ end
761
+ end
762
+ index += 1
763
+ end
764
+ end
765
+ found
766
+ end
767
+ end
768
+
769
+
770
+ # Creates a only child lambda. Pass +of-type+ to only look at
771
+ # elements of its type.
772
+ def only_child(of_type)
773
+ lambda do |element|
774
+ # Element must be inside parent element.
775
+ return false unless element.parent && element.parent.tag?
776
+ name = of_type ? element.name : nil
777
+ other = false
778
+ for child in element.parent.children
779
+ # Skip text nodes/comments.
780
+ if child.tag? && (name == nil || child.name == name)
781
+ unless child.equal?(element)
782
+ other = true
783
+ break
784
+ end
785
+ end
786
+ end
787
+ !other
788
+ end
789
+ end
790
+
791
+
792
+ # Called to create a dependent selector (sibling, descendant, etc).
793
+ # Passes the remainder of the statement that will be reduced to zero
794
+ # eventually, and array of substitution values.
795
+ #
796
+ # This method is called from four places, so it helps to put it here
797
+ # for reuse. The only logic deals with the need to detect comma
798
+ # separators (alternate) and apply them to the selector group of the
799
+ # top selector.
800
+ def next_selector(statement, values)
801
+ second = Selector.new(statement, values)
802
+ # If there are alternate selectors, we group them in the top selector.
803
+ if alternates = second.instance_variable_get(:@alternates)
804
+ second.instance_variable_set(:@alternates, [])
805
+ @alternates.concat alternates
806
+ end
807
+ second
808
+ end
809
+
810
+ end
811
+
812
+
813
+ # See HTML::Selector.new
814
+ def self.selector(statement, *values)
815
+ Selector.new(statement, *values)
816
+ end
817
+
818
+
819
+ class Tag
820
+
821
+ def select(selector, *values)
822
+ selector = HTML::Selector.new(selector, values)
823
+ selector.select(self)
824
+ end
825
+
826
+ end
827
+
828
+ end