eactionpack 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. data/CHANGELOG +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +469 -0
  4. data/RUNNING_UNIT_TESTS +24 -0
  5. data/Rakefile +146 -0
  6. data/install.rb +30 -0
  7. data/lib/action_controller.rb +79 -0
  8. data/lib/action_controller/assertions.rb +69 -0
  9. data/lib/action_controller/assertions/dom_assertions.rb +39 -0
  10. data/lib/action_controller/assertions/model_assertions.rb +20 -0
  11. data/lib/action_controller/assertions/response_assertions.rb +172 -0
  12. data/lib/action_controller/assertions/routing_assertions.rb +146 -0
  13. data/lib/action_controller/assertions/selector_assertions.rb +491 -0
  14. data/lib/action_controller/assertions/tag_assertions.rb +130 -0
  15. data/lib/action_controller/base.rb +1288 -0
  16. data/lib/action_controller/benchmarking.rb +94 -0
  17. data/lib/action_controller/caching.rb +72 -0
  18. data/lib/action_controller/caching/actions.rb +144 -0
  19. data/lib/action_controller/caching/fragments.rb +138 -0
  20. data/lib/action_controller/caching/pages.rb +154 -0
  21. data/lib/action_controller/caching/sql_cache.rb +18 -0
  22. data/lib/action_controller/caching/sweeping.rb +97 -0
  23. data/lib/action_controller/cgi_ext.rb +16 -0
  24. data/lib/action_controller/cgi_ext/cookie.rb +110 -0
  25. data/lib/action_controller/cgi_ext/query_extension.rb +22 -0
  26. data/lib/action_controller/cgi_ext/session.rb +73 -0
  27. data/lib/action_controller/cgi_ext/stdinput.rb +24 -0
  28. data/lib/action_controller/cgi_process.rb +223 -0
  29. data/lib/action_controller/components.rb +166 -0
  30. data/lib/action_controller/cookies.rb +96 -0
  31. data/lib/action_controller/dispatcher.rb +162 -0
  32. data/lib/action_controller/filters.rb +642 -0
  33. data/lib/action_controller/flash.rb +172 -0
  34. data/lib/action_controller/headers.rb +31 -0
  35. data/lib/action_controller/helpers.rb +221 -0
  36. data/lib/action_controller/http_authentication.rb +124 -0
  37. data/lib/action_controller/integration.rb +634 -0
  38. data/lib/action_controller/layout.rb +309 -0
  39. data/lib/action_controller/mime_responds.rb +173 -0
  40. data/lib/action_controller/mime_type.rb +186 -0
  41. data/lib/action_controller/mime_types.rb +20 -0
  42. data/lib/action_controller/polymorphic_routes.rb +191 -0
  43. data/lib/action_controller/record_identifier.rb +102 -0
  44. data/lib/action_controller/request.rb +764 -0
  45. data/lib/action_controller/request_forgery_protection.rb +140 -0
  46. data/lib/action_controller/request_profiler.rb +169 -0
  47. data/lib/action_controller/rescue.rb +258 -0
  48. data/lib/action_controller/resources.rb +572 -0
  49. data/lib/action_controller/response.rb +76 -0
  50. data/lib/action_controller/routing.rb +387 -0
  51. data/lib/action_controller/routing/builder.rb +203 -0
  52. data/lib/action_controller/routing/optimisations.rb +120 -0
  53. data/lib/action_controller/routing/recognition_optimisation.rb +162 -0
  54. data/lib/action_controller/routing/route.rb +240 -0
  55. data/lib/action_controller/routing/route_set.rb +436 -0
  56. data/lib/action_controller/routing/routing_ext.rb +46 -0
  57. data/lib/action_controller/routing/segments.rb +283 -0
  58. data/lib/action_controller/session/active_record_store.rb +340 -0
  59. data/lib/action_controller/session/cookie_store.rb +166 -0
  60. data/lib/action_controller/session/drb_server.rb +32 -0
  61. data/lib/action_controller/session/drb_store.rb +35 -0
  62. data/lib/action_controller/session/mem_cache_store.rb +98 -0
  63. data/lib/action_controller/session_management.rb +158 -0
  64. data/lib/action_controller/status_codes.rb +88 -0
  65. data/lib/action_controller/streaming.rb +155 -0
  66. data/lib/action_controller/templates/rescues/_request_and_response.erb +24 -0
  67. data/lib/action_controller/templates/rescues/_trace.erb +26 -0
  68. data/lib/action_controller/templates/rescues/diagnostics.erb +11 -0
  69. data/lib/action_controller/templates/rescues/layout.erb +29 -0
  70. data/lib/action_controller/templates/rescues/missing_template.erb +2 -0
  71. data/lib/action_controller/templates/rescues/routing_error.erb +10 -0
  72. data/lib/action_controller/templates/rescues/template_error.erb +21 -0
  73. data/lib/action_controller/templates/rescues/unknown_action.erb +2 -0
  74. data/lib/action_controller/test_case.rb +83 -0
  75. data/lib/action_controller/test_process.rb +526 -0
  76. data/lib/action_controller/url_rewriter.rb +142 -0
  77. data/lib/action_controller/vendor/html-scanner/html/document.rb +68 -0
  78. data/lib/action_controller/vendor/html-scanner/html/node.rb +537 -0
  79. data/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +173 -0
  80. data/lib/action_controller/vendor/html-scanner/html/selector.rb +828 -0
  81. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +105 -0
  82. data/lib/action_controller/vendor/html-scanner/html/version.rb +11 -0
  83. data/lib/action_controller/verification.rb +130 -0
  84. data/lib/action_pack.rb +24 -0
  85. data/lib/action_pack/version.rb +9 -0
  86. data/lib/action_view.rb +44 -0
  87. data/lib/action_view/base.rb +335 -0
  88. data/lib/action_view/helpers/active_record_helper.rb +276 -0
  89. data/lib/action_view/helpers/asset_tag_helper.rb +599 -0
  90. data/lib/action_view/helpers/atom_feed_helper.rb +143 -0
  91. data/lib/action_view/helpers/benchmark_helper.rb +33 -0
  92. data/lib/action_view/helpers/cache_helper.rb +40 -0
  93. data/lib/action_view/helpers/capture_helper.rb +161 -0
  94. data/lib/action_view/helpers/date_helper.rb +711 -0
  95. data/lib/action_view/helpers/debug_helper.rb +31 -0
  96. data/lib/action_view/helpers/form_helper.rb +767 -0
  97. data/lib/action_view/helpers/form_options_helper.rb +458 -0
  98. data/lib/action_view/helpers/form_tag_helper.rb +458 -0
  99. data/lib/action_view/helpers/javascript_helper.rb +148 -0
  100. data/lib/action_view/helpers/number_helper.rb +186 -0
  101. data/lib/action_view/helpers/record_identification_helper.rb +20 -0
  102. data/lib/action_view/helpers/record_tag_helper.rb +59 -0
  103. data/lib/action_view/helpers/sanitize_helper.rb +229 -0
  104. data/lib/action_view/helpers/tag_helper.rb +134 -0
  105. data/lib/action_view/helpers/text_helper.rb +507 -0
  106. data/lib/action_view/helpers/url_helper.rb +573 -0
  107. data/lib/action_view/inline_template.rb +20 -0
  108. data/lib/action_view/partial_template.rb +70 -0
  109. data/lib/action_view/partials.rb +158 -0
  110. data/lib/action_view/template.rb +125 -0
  111. data/lib/action_view/template_error.rb +110 -0
  112. data/lib/action_view/template_finder.rb +176 -0
  113. data/lib/action_view/template_handler.rb +34 -0
  114. data/lib/action_view/template_handlers/builder.rb +27 -0
  115. data/lib/action_view/template_handlers/compilable.rb +128 -0
  116. data/lib/action_view/template_handlers/erb.rb +56 -0
  117. data/lib/action_view/test_case.rb +58 -0
  118. data/lib/actionpack.rb +1 -0
  119. data/test/abstract_unit.rb +36 -0
  120. data/test/active_record_unit.rb +105 -0
  121. data/test/activerecord/active_record_store_test.rb +141 -0
  122. data/test/activerecord/render_partial_with_record_identification_test.rb +191 -0
  123. data/test/adv_attr_test.rb +20 -0
  124. data/test/controller/action_pack_assertions_test.rb +543 -0
  125. data/test/controller/addresses_render_test.rb +43 -0
  126. data/test/controller/assert_select_test.rb +331 -0
  127. data/test/controller/base_test.rb +219 -0
  128. data/test/controller/benchmark_test.rb +32 -0
  129. data/test/controller/caching_test.rb +581 -0
  130. data/test/controller/capture_test.rb +89 -0
  131. data/test/controller/cgi_test.rb +116 -0
  132. data/test/controller/components_test.rb +140 -0
  133. data/test/controller/content_type_test.rb +139 -0
  134. data/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb +0 -0
  135. data/test/controller/controller_fixtures/app/controllers/user_controller.rb +0 -0
  136. data/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb +0 -0
  137. data/test/controller/cookie_test.rb +146 -0
  138. data/test/controller/custom_handler_test.rb +45 -0
  139. data/test/controller/deprecation/deprecated_base_methods_test.rb +37 -0
  140. data/test/controller/dispatcher_test.rb +105 -0
  141. data/test/controller/fake_controllers.rb +33 -0
  142. data/test/controller/fake_models.rb +11 -0
  143. data/test/controller/filter_params_test.rb +49 -0
  144. data/test/controller/filters_test.rb +881 -0
  145. data/test/controller/flash_test.rb +146 -0
  146. data/test/controller/header_test.rb +14 -0
  147. data/test/controller/helper_test.rb +210 -0
  148. data/test/controller/html-scanner/cdata_node_test.rb +15 -0
  149. data/test/controller/html-scanner/document_test.rb +148 -0
  150. data/test/controller/html-scanner/node_test.rb +89 -0
  151. data/test/controller/html-scanner/sanitizer_test.rb +269 -0
  152. data/test/controller/html-scanner/tag_node_test.rb +238 -0
  153. data/test/controller/html-scanner/text_node_test.rb +50 -0
  154. data/test/controller/html-scanner/tokenizer_test.rb +131 -0
  155. data/test/controller/http_authentication_test.rb +54 -0
  156. data/test/controller/integration_test.rb +252 -0
  157. data/test/controller/integration_upload_test.rb +43 -0
  158. data/test/controller/layout_test.rb +255 -0
  159. data/test/controller/mime_responds_test.rb +514 -0
  160. data/test/controller/mime_type_test.rb +84 -0
  161. data/test/controller/new_render_test.rb +843 -0
  162. data/test/controller/polymorphic_routes_test.rb +174 -0
  163. data/test/controller/record_identifier_test.rb +139 -0
  164. data/test/controller/redirect_test.rb +289 -0
  165. data/test/controller/render_test.rb +484 -0
  166. data/test/controller/request_forgery_protection_test.rb +305 -0
  167. data/test/controller/request_test.rb +928 -0
  168. data/test/controller/rescue_test.rb +517 -0
  169. data/test/controller/resources_test.rb +873 -0
  170. data/test/controller/routing_test.rb +2464 -0
  171. data/test/controller/selector_test.rb +628 -0
  172. data/test/controller/send_file_test.rb +138 -0
  173. data/test/controller/session/cookie_store_test.rb +258 -0
  174. data/test/controller/session/mem_cache_store_test.rb +181 -0
  175. data/test/controller/session_fixation_test.rb +89 -0
  176. data/test/controller/session_management_test.rb +178 -0
  177. data/test/controller/test_test.rb +695 -0
  178. data/test/controller/url_rewriter_test.rb +310 -0
  179. data/test/controller/verification_test.rb +270 -0
  180. data/test/controller/view_paths_test.rb +140 -0
  181. data/test/controller/webservice_test.rb +229 -0
  182. data/test/fixtures/addresses/list.erb +1 -0
  183. data/test/fixtures/bad_customers/_bad_customer.html.erb +1 -0
  184. data/test/fixtures/companies.yml +24 -0
  185. data/test/fixtures/company.rb +10 -0
  186. data/test/fixtures/content_type/render_default_content_types_for_respond_to.rhtml +1 -0
  187. data/test/fixtures/content_type/render_default_for_js.js.erb +1 -0
  188. data/test/fixtures/content_type/render_default_for_rhtml.rhtml +1 -0
  189. data/test/fixtures/content_type/render_default_for_rxml.rxml +1 -0
  190. data/test/fixtures/customers/_customer.html.erb +1 -0
  191. data/test/fixtures/db_definitions/sqlite.sql +49 -0
  192. data/test/fixtures/developer.rb +9 -0
  193. data/test/fixtures/developers.yml +21 -0
  194. data/test/fixtures/developers_projects.yml +13 -0
  195. data/test/fixtures/fun/games/hello_world.erb +1 -0
  196. data/test/fixtures/functional_caching/_partial.erb +3 -0
  197. data/test/fixtures/functional_caching/fragment_cached.html.erb +2 -0
  198. data/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb +1 -0
  199. data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
  200. data/test/fixtures/good_customers/_good_customer.html.erb +1 -0
  201. data/test/fixtures/helpers/abc_helper.rb +5 -0
  202. data/test/fixtures/helpers/fun/games_helper.rb +3 -0
  203. data/test/fixtures/helpers/fun/pdf_helper.rb +3 -0
  204. data/test/fixtures/layout_tests/alt/hello.rhtml +1 -0
  205. data/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +1 -0
  206. data/test/fixtures/layout_tests/layouts/item.rhtml +1 -0
  207. data/test/fixtures/layout_tests/layouts/layout_test.rhtml +1 -0
  208. data/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb +1 -0
  209. data/test/fixtures/layout_tests/layouts/third_party_template_library.mab +1 -0
  210. data/test/fixtures/layout_tests/views/hello.rhtml +1 -0
  211. data/test/fixtures/layouts/block_with_layout.erb +3 -0
  212. data/test/fixtures/layouts/builder.builder +3 -0
  213. data/test/fixtures/layouts/partial_with_layout.erb +3 -0
  214. data/test/fixtures/layouts/standard.erb +1 -0
  215. data/test/fixtures/layouts/talk_from_action.erb +2 -0
  216. data/test/fixtures/layouts/yield.erb +2 -0
  217. data/test/fixtures/mascot.rb +3 -0
  218. data/test/fixtures/mascots.yml +4 -0
  219. data/test/fixtures/mascots/_mascot.html.erb +1 -0
  220. data/test/fixtures/multipart/binary_file +0 -0
  221. data/test/fixtures/multipart/boundary_problem_file +10 -0
  222. data/test/fixtures/multipart/bracketed_param +5 -0
  223. data/test/fixtures/multipart/large_text_file +10 -0
  224. data/test/fixtures/multipart/mixed_files +0 -0
  225. data/test/fixtures/multipart/mona_lisa.jpg +0 -0
  226. data/test/fixtures/multipart/single_parameter +5 -0
  227. data/test/fixtures/multipart/text_file +10 -0
  228. data/test/fixtures/override/test/hello_world.erb +1 -0
  229. data/test/fixtures/override2/layouts/test/sub.erb +1 -0
  230. data/test/fixtures/post_test/layouts/post.html.erb +1 -0
  231. data/test/fixtures/post_test/layouts/super_post.iphone.erb +1 -0
  232. data/test/fixtures/post_test/post/index.html.erb +1 -0
  233. data/test/fixtures/post_test/post/index.iphone.erb +1 -0
  234. data/test/fixtures/post_test/super_post/index.html.erb +1 -0
  235. data/test/fixtures/post_test/super_post/index.iphone.erb +1 -0
  236. data/test/fixtures/project.rb +3 -0
  237. data/test/fixtures/projects.yml +7 -0
  238. data/test/fixtures/public/404.html +1 -0
  239. data/test/fixtures/public/500.html +1 -0
  240. data/test/fixtures/public/images/rails.png +0 -0
  241. data/test/fixtures/public/javascripts/application.js +1 -0
  242. data/test/fixtures/public/javascripts/bank.js +1 -0
  243. data/test/fixtures/public/javascripts/robber.js +1 -0
  244. data/test/fixtures/public/javascripts/version.1.0.js +1 -0
  245. data/test/fixtures/public/stylesheets/bank.css +1 -0
  246. data/test/fixtures/public/stylesheets/robber.css +1 -0
  247. data/test/fixtures/public/stylesheets/version.1.0.css +1 -0
  248. data/test/fixtures/replies.yml +15 -0
  249. data/test/fixtures/reply.rb +7 -0
  250. data/test/fixtures/respond_to/all_types_with_layout.html.erb +1 -0
  251. data/test/fixtures/respond_to/custom_constant_handling_without_block.mobile.erb +1 -0
  252. data/test/fixtures/respond_to/iphone_with_html_response_type.html.erb +1 -0
  253. data/test/fixtures/respond_to/iphone_with_html_response_type.iphone.erb +1 -0
  254. data/test/fixtures/respond_to/layouts/missing.html.erb +1 -0
  255. data/test/fixtures/respond_to/layouts/standard.html.erb +1 -0
  256. data/test/fixtures/respond_to/layouts/standard.iphone.erb +1 -0
  257. data/test/fixtures/respond_to/using_defaults.html.erb +1 -0
  258. data/test/fixtures/respond_to/using_defaults.js.rjs +1 -0
  259. data/test/fixtures/respond_to/using_defaults.xml.builder +1 -0
  260. data/test/fixtures/respond_to/using_defaults_with_type_list.html.erb +1 -0
  261. data/test/fixtures/respond_to/using_defaults_with_type_list.js.rjs +1 -0
  262. data/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder +1 -0
  263. data/test/fixtures/scope/test/modgreet.erb +1 -0
  264. data/test/fixtures/shared.html.erb +1 -0
  265. data/test/fixtures/symlink_parent/symlinked_layout.erb +5 -0
  266. data/test/fixtures/test/_customer.erb +1 -0
  267. data/test/fixtures/test/_customer_counter.erb +1 -0
  268. data/test/fixtures/test/_customer_greeting.erb +1 -0
  269. data/test/fixtures/test/_form.erb +1 -0
  270. data/test/fixtures/test/_hash_greeting.erb +1 -0
  271. data/test/fixtures/test/_hash_object.erb +2 -0
  272. data/test/fixtures/test/_hello.builder +1 -0
  273. data/test/fixtures/test/_labelling_form.erb +1 -0
  274. data/test/fixtures/test/_layout_for_partial.html.erb +3 -0
  275. data/test/fixtures/test/_partial.erb +1 -0
  276. data/test/fixtures/test/_partial.html.erb +1 -0
  277. data/test/fixtures/test/_partial.js.erb +1 -0
  278. data/test/fixtures/test/_partial_for_use_in_layout.html.erb +1 -0
  279. data/test/fixtures/test/_partial_only.erb +1 -0
  280. data/test/fixtures/test/_person.erb +2 -0
  281. data/test/fixtures/test/_raise.html.erb +1 -0
  282. data/test/fixtures/test/action_talk_to_layout.erb +2 -0
  283. data/test/fixtures/test/block_content_for.erb +2 -0
  284. data/test/fixtures/test/calling_partial_with_layout.html.erb +1 -0
  285. data/test/fixtures/test/capturing.erb +4 -0
  286. data/test/fixtures/test/content_for.erb +2 -0
  287. data/test/fixtures/test/content_for_concatenated.erb +3 -0
  288. data/test/fixtures/test/content_for_with_parameter.erb +2 -0
  289. data/test/fixtures/test/delete_with_js.rjs +2 -0
  290. data/test/fixtures/test/dot.directory/render_file_with_ivar.erb +1 -0
  291. data/test/fixtures/test/enum_rjs_test.rjs +6 -0
  292. data/test/fixtures/test/erb_content_for.erb +2 -0
  293. data/test/fixtures/test/formatted_html_erb.html.erb +1 -0
  294. data/test/fixtures/test/formatted_xml_erb.builder +1 -0
  295. data/test/fixtures/test/formatted_xml_erb.html.erb +1 -0
  296. data/test/fixtures/test/formatted_xml_erb.xml.erb +1 -0
  297. data/test/fixtures/test/greeting.erb +1 -0
  298. data/test/fixtures/test/greeting.js.rjs +1 -0
  299. data/test/fixtures/test/hello.builder +4 -0
  300. data/test/fixtures/test/hello_world.erb +1 -0
  301. data/test/fixtures/test/hello_world_container.builder +3 -0
  302. data/test/fixtures/test/hello_world_from_rxml.builder +4 -0
  303. data/test/fixtures/test/hello_world_with_layout_false.erb +1 -0
  304. data/test/fixtures/test/hello_xml_world.builder +11 -0
  305. data/test/fixtures/test/list.erb +1 -0
  306. data/test/fixtures/test/non_erb_block_content_for.builder +4 -0
  307. data/test/fixtures/test/potential_conflicts.erb +4 -0
  308. data/test/fixtures/test/render_file_from_template.html.erb +1 -0
  309. data/test/fixtures/test/render_file_with_ivar.erb +1 -0
  310. data/test/fixtures/test/render_file_with_locals.erb +1 -0
  311. data/test/fixtures/test/render_to_string_test.erb +1 -0
  312. data/test/fixtures/test/update_element_with_capture.erb +9 -0
  313. data/test/fixtures/test/using_layout_around_block.html.erb +1 -0
  314. data/test/fixtures/topic.rb +3 -0
  315. data/test/fixtures/topics.yml +22 -0
  316. data/test/fixtures/topics/_topic.html.erb +1 -0
  317. data/test/template/active_record_helper_test.rb +268 -0
  318. data/test/template/asset_tag_helper_test.rb +514 -0
  319. data/test/template/atom_feed_helper_test.rb +179 -0
  320. data/test/template/benchmark_helper_test.rb +60 -0
  321. data/test/template/date_helper_test.rb +1791 -0
  322. data/test/template/deprecated_erb_variable_test.rb +9 -0
  323. data/test/template/erb_util_test.rb +24 -0
  324. data/test/template/form_helper_test.rb +885 -0
  325. data/test/template/form_options_helper_test.rb +1333 -0
  326. data/test/template/form_tag_helper_test.rb +272 -0
  327. data/test/template/javascript_helper_test.rb +73 -0
  328. data/test/template/number_helper_test.rb +97 -0
  329. data/test/template/record_tag_helper_test.rb +54 -0
  330. data/test/template/sanitize_helper_test.rb +48 -0
  331. data/test/template/tag_helper_test.rb +77 -0
  332. data/test/template/template_finder_test.rb +73 -0
  333. data/test/template/template_object_test.rb +95 -0
  334. data/test/template/test_test.rb +56 -0
  335. data/test/template/text_helper_test.rb +367 -0
  336. data/test/template/url_helper_test.rb +544 -0
  337. data/test/testing_sandbox.rb +15 -0
  338. metadata +469 -0
@@ -0,0 +1,142 @@
1
+ module ActionController
2
+ # Write URLs from arbitrary places in your codebase, such as your mailers.
3
+ #
4
+ # Example:
5
+ #
6
+ # class MyMailer
7
+ # include ActionController::UrlWriter
8
+ # default_url_options[:host] = 'www.basecamphq.com'
9
+ #
10
+ # def signup_url(token)
11
+ # url_for(:controller => 'signup', action => 'index', :token => token)
12
+ # end
13
+ # end
14
+ #
15
+ # In addition to providing +url_for+, named routes are also accessible after
16
+ # including UrlWriter.
17
+ module UrlWriter
18
+ # The default options for urls written by this writer. Typically a <tt>:host</tt>
19
+ # pair is provided.
20
+ mattr_accessor :default_url_options
21
+ self.default_url_options = {}
22
+
23
+ def self.included(base) #:nodoc:
24
+ ActionController::Routing::Routes.install_helpers(base)
25
+ base.mattr_accessor :default_url_options
26
+ base.default_url_options ||= default_url_options
27
+ end
28
+
29
+ # Generate a url based on the options provided, default_url_options and the
30
+ # routes defined in routes.rb. The following options are supported:
31
+ #
32
+ # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+.
33
+ # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
34
+ # * <tt>:host</tt> - Specifies the host the link should be targetted at.
35
+ # If <tt>:only_path</tt> is false, this option must be
36
+ # provided either explicitly, or via +default_url_options+.
37
+ # * <tt>:port</tt> - Optionally specify the port to connect to.
38
+ # * <tt>:anchor</tt> - An anchor name to be appended to the path.
39
+ # * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the
40
+ # +relative_url_root+ set in ActionController::AbstractRequest.relative_url_root.
41
+ # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
42
+ #
43
+ # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
44
+ # +url_for+ is forwarded to the Routes module.
45
+ #
46
+ # Examples:
47
+ #
48
+ # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing'
49
+ # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
50
+ # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/'
51
+ # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
52
+ def url_for(options)
53
+ options = self.class.default_url_options.merge(options)
54
+
55
+ url = ''
56
+
57
+ unless options.delete(:only_path)
58
+ url << (options.delete(:protocol) || 'http')
59
+ url << '://' unless url.match("://")
60
+
61
+ raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
62
+
63
+ url << options.delete(:host)
64
+ url << ":#{options.delete(:port)}" if options.key?(:port)
65
+ else
66
+ # Delete the unused options to prevent their appearance in the query string.
67
+ [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) }
68
+ end
69
+ trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
70
+ url << ActionController::AbstractRequest.relative_url_root.to_s unless options[:skip_relative_url_root]
71
+ anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
72
+ generated = Routing::Routes.generate(options, {})
73
+ url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
74
+ url << anchor if anchor
75
+
76
+ url
77
+ end
78
+ end
79
+
80
+ # Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
81
+ class UrlRewriter #:nodoc:
82
+ RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]
83
+ def initialize(request, parameters)
84
+ @request, @parameters = request, parameters
85
+ end
86
+
87
+ def rewrite(options = {})
88
+ rewrite_url(options)
89
+ end
90
+
91
+ def to_str
92
+ "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
93
+ end
94
+
95
+ alias_method :to_s, :to_str
96
+
97
+ private
98
+ # Given a path and options, returns a rewritten URL string
99
+ def rewrite_url(options)
100
+ rewritten_url = ""
101
+
102
+ unless options[:only_path]
103
+ rewritten_url << (options[:protocol] || @request.protocol)
104
+ rewritten_url << "://" unless rewritten_url.match("://")
105
+ rewritten_url << rewrite_authentication(options)
106
+ rewritten_url << (options[:host] || @request.host_with_port)
107
+ rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
108
+ end
109
+
110
+ path = rewrite_path(options)
111
+ rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
112
+ rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
113
+ rewritten_url << "##{options[:anchor]}" if options[:anchor]
114
+
115
+ rewritten_url
116
+ end
117
+
118
+ # Given a Hash of options, generates a route
119
+ def rewrite_path(options)
120
+ options = options.symbolize_keys
121
+ options.update(options[:params].symbolize_keys) if options[:params]
122
+
123
+ if (overwrite = options.delete(:overwrite_params))
124
+ options.update(@parameters.symbolize_keys)
125
+ options.update(overwrite.symbolize_keys)
126
+ end
127
+
128
+ RESERVED_OPTIONS.each { |k| options.delete(k) }
129
+
130
+ # Generates the query string, too
131
+ Routing::Routes.generate(options, @request.symbolized_path_parameters)
132
+ end
133
+
134
+ def rewrite_authentication(options)
135
+ if options[:user] && options[:password]
136
+ "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@"
137
+ else
138
+ ""
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,68 @@
1
+ require 'html/tokenizer'
2
+ require 'html/node'
3
+ require 'html/selector'
4
+ require 'html/sanitizer'
5
+
6
+ module HTML #:nodoc:
7
+ # A top-level HTMl document. You give it a body of text, and it will parse that
8
+ # text into a tree of nodes.
9
+ class Document #:nodoc:
10
+
11
+ # The root of the parsed document.
12
+ attr_reader :root
13
+
14
+ # Create a new Document from the given text.
15
+ def initialize(text, strict=false, xml=false)
16
+ tokenizer = Tokenizer.new(text)
17
+ @root = Node.new(nil)
18
+ node_stack = [ @root ]
19
+ while token = tokenizer.next
20
+ node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
21
+
22
+ node_stack.last.children << node unless node.tag? && node.closing == :close
23
+ if node.tag?
24
+ if node_stack.length > 1 && node.closing == :close
25
+ if node_stack.last.name == node.name
26
+ if node_stack.last.children.empty?
27
+ node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
28
+ end
29
+ node_stack.pop
30
+ else
31
+ open_start = node_stack.last.position - 20
32
+ open_start = 0 if open_start < 0
33
+ close_start = node.position - 20
34
+ close_start = 0 if close_start < 0
35
+ msg = <<EOF.strip
36
+ ignoring attempt to close #{node_stack.last.name} with #{node.name}
37
+ opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
38
+ closed at byte #{node.position}, line #{node.line}
39
+ attributes at open: #{node_stack.last.attributes.inspect}
40
+ text around open: #{text[open_start,40].inspect}
41
+ text around close: #{text[close_start,40].inspect}
42
+ EOF
43
+ strict ? raise(msg) : warn(msg)
44
+ end
45
+ elsif !node.childless?(xml) && node.closing != :close
46
+ node_stack.push node
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Search the tree for (and return) the first node that matches the given
53
+ # conditions. The conditions are interpreted differently for different node
54
+ # types, see HTML::Text#find and HTML::Tag#find.
55
+ def find(conditions)
56
+ @root.find(conditions)
57
+ end
58
+
59
+ # Search the tree for (and return) all nodes that match the given
60
+ # conditions. The conditions are interpreted differently for different node
61
+ # types, see HTML::Text#find and HTML::Tag#find.
62
+ def find_all(conditions)
63
+ @root.find_all(conditions)
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,537 @@
1
+ require 'strscan'
2
+
3
+ module HTML #:nodoc:
4
+
5
+ class Conditions < Hash #:nodoc:
6
+ def initialize(hash)
7
+ super()
8
+ hash = { :content => hash } unless Hash === hash
9
+ hash = keys_to_symbols(hash)
10
+ hash.each do |k,v|
11
+ case k
12
+ when :tag, :content then
13
+ # keys are valid, and require no further processing
14
+ when :attributes then
15
+ hash[k] = keys_to_strings(v)
16
+ when :parent, :child, :ancestor, :descendant, :sibling, :before,
17
+ :after
18
+ hash[k] = Conditions.new(v)
19
+ when :children
20
+ hash[k] = v = keys_to_symbols(v)
21
+ v.each do |k,v2|
22
+ case k
23
+ when :count, :greater_than, :less_than
24
+ # keys are valid, and require no further processing
25
+ when :only
26
+ v[k] = Conditions.new(v2)
27
+ else
28
+ raise "illegal key #{k.inspect} => #{v2.inspect}"
29
+ end
30
+ end
31
+ else
32
+ raise "illegal key #{k.inspect} => #{v.inspect}"
33
+ end
34
+ end
35
+ update hash
36
+ end
37
+
38
+ private
39
+
40
+ def keys_to_strings(hash)
41
+ hash.keys.inject({}) do |h,k|
42
+ h[k.to_s] = hash[k]
43
+ h
44
+ end
45
+ end
46
+
47
+ def keys_to_symbols(hash)
48
+ hash.keys.inject({}) do |h,k|
49
+ raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
50
+ h[k.to_sym] = hash[k]
51
+ h
52
+ end
53
+ end
54
+ end
55
+
56
+ # The base class of all nodes, textual and otherwise, in an HTML document.
57
+ class Node #:nodoc:
58
+ # The array of children of this node. Not all nodes have children.
59
+ attr_reader :children
60
+
61
+ # The parent node of this node. All nodes have a parent, except for the
62
+ # root node.
63
+ attr_reader :parent
64
+
65
+ # The line number of the input where this node was begun
66
+ attr_reader :line
67
+
68
+ # The byte position in the input where this node was begun
69
+ attr_reader :position
70
+
71
+ # Create a new node as a child of the given parent.
72
+ def initialize(parent, line=0, pos=0)
73
+ @parent = parent
74
+ @children = []
75
+ @line, @position = line, pos
76
+ end
77
+
78
+ # Return a textual representation of the node.
79
+ def to_s
80
+ s = ""
81
+ @children.each { |child| s << child.to_s }
82
+ s
83
+ end
84
+
85
+ # Return false (subclasses must override this to provide specific matching
86
+ # behavior.) +conditions+ may be of any type.
87
+ def match(conditions)
88
+ false
89
+ end
90
+
91
+ # Search the children of this node for the first node for which #find
92
+ # returns non +nil+. Returns the result of the #find call that succeeded.
93
+ def find(conditions)
94
+ conditions = validate_conditions(conditions)
95
+ @children.each do |child|
96
+ node = child.find(conditions)
97
+ return node if node
98
+ end
99
+ nil
100
+ end
101
+
102
+ # Search for all nodes that match the given conditions, and return them
103
+ # as an array.
104
+ def find_all(conditions)
105
+ conditions = validate_conditions(conditions)
106
+
107
+ matches = []
108
+ matches << self if match(conditions)
109
+ @children.each do |child|
110
+ matches.concat child.find_all(conditions)
111
+ end
112
+ matches
113
+ end
114
+
115
+ # Returns +false+. Subclasses may override this if they define a kind of
116
+ # tag.
117
+ def tag?
118
+ false
119
+ end
120
+
121
+ def validate_conditions(conditions)
122
+ Conditions === conditions ? conditions : Conditions.new(conditions)
123
+ end
124
+
125
+ def ==(node)
126
+ return false unless self.class == node.class && children.size == node.children.size
127
+
128
+ equivalent = true
129
+
130
+ children.size.times do |i|
131
+ equivalent &&= children[i] == node.children[i]
132
+ end
133
+
134
+ equivalent
135
+ end
136
+
137
+ class <<self
138
+ def parse(parent, line, pos, content, strict=true)
139
+ if content !~ /^<\S/
140
+ Text.new(parent, line, pos, content)
141
+ else
142
+ scanner = StringScanner.new(content)
143
+
144
+ unless scanner.skip(/</)
145
+ if strict
146
+ raise "expected <"
147
+ else
148
+ return Text.new(parent, line, pos, content)
149
+ end
150
+ end
151
+
152
+ if scanner.skip(/!\[CDATA\[/)
153
+ unless scanner.skip_until(/\]\]>/)
154
+ if strict
155
+ raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
156
+ else
157
+ scanner.skip_until(/\Z/)
158
+ end
159
+ end
160
+
161
+ return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
162
+ end
163
+
164
+ closing = ( scanner.scan(/\//) ? :close : nil )
165
+ return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
166
+ name.downcase!
167
+
168
+ unless closing
169
+ scanner.skip(/\s*/)
170
+ attributes = {}
171
+ while attr = scanner.scan(/[-\w:]+/)
172
+ value = true
173
+ if scanner.scan(/\s*=\s*/)
174
+ if delim = scanner.scan(/['"]/)
175
+ value = ""
176
+ while text = scanner.scan(/[^#{delim}\\]+|./)
177
+ case text
178
+ when "\\" then
179
+ value << text
180
+ value << scanner.getch
181
+ when delim
182
+ break
183
+ else value << text
184
+ end
185
+ end
186
+ else
187
+ value = scanner.scan(/[^\s>\/]+/)
188
+ end
189
+ end
190
+ attributes[attr.downcase] = value
191
+ scanner.skip(/\s*/)
192
+ end
193
+
194
+ closing = ( scanner.scan(/\//) ? :self : nil )
195
+ end
196
+
197
+ unless scanner.scan(/\s*>/)
198
+ if strict
199
+ raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
200
+ else
201
+ # throw away all text until we find what we're looking for
202
+ scanner.skip_until(/>/) or scanner.terminate
203
+ end
204
+ end
205
+
206
+ Tag.new(parent, line, pos, name, attributes, closing)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ # A node that represents text, rather than markup.
213
+ class Text < Node #:nodoc:
214
+
215
+ attr_reader :content
216
+
217
+ # Creates a new text node as a child of the given parent, with the given
218
+ # content.
219
+ def initialize(parent, line, pos, content)
220
+ super(parent, line, pos)
221
+ @content = content
222
+ end
223
+
224
+ # Returns the content of this node.
225
+ def to_s
226
+ @content
227
+ end
228
+
229
+ # Returns +self+ if this node meets the given conditions. Text nodes support
230
+ # conditions of the following kinds:
231
+ #
232
+ # * if +conditions+ is a string, it must be a substring of the node's
233
+ # content
234
+ # * if +conditions+ is a regular expression, it must match the node's
235
+ # content
236
+ # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
237
+ # is either a string or a regexp, and which is interpreted as described
238
+ # above.
239
+ def find(conditions)
240
+ match(conditions) && self
241
+ end
242
+
243
+ # Returns non-+nil+ if this node meets the given conditions, or +nil+
244
+ # otherwise. See the discussion of #find for the valid conditions.
245
+ def match(conditions)
246
+ case conditions
247
+ when String
248
+ @content == conditions
249
+ when Regexp
250
+ @content =~ conditions
251
+ when Hash
252
+ conditions = validate_conditions(conditions)
253
+
254
+ # Text nodes only have :content, :parent, :ancestor
255
+ unless (conditions.keys - [:content, :parent, :ancestor]).empty?
256
+ return false
257
+ end
258
+
259
+ match(conditions[:content])
260
+ else
261
+ nil
262
+ end
263
+ end
264
+
265
+ def ==(node)
266
+ return false unless super
267
+ content == node.content
268
+ end
269
+ end
270
+
271
+ # A CDATA node is simply a text node with a specialized way of displaying
272
+ # itself.
273
+ class CDATA < Text #:nodoc:
274
+ def to_s
275
+ "<![CDATA[#{super}]]>"
276
+ end
277
+ end
278
+
279
+ # A Tag is any node that represents markup. It may be an opening tag, a
280
+ # closing tag, or a self-closing tag. It has a name, and may have a hash of
281
+ # attributes.
282
+ class Tag < Node #:nodoc:
283
+
284
+ # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
285
+ attr_reader :closing
286
+
287
+ # Either +nil+, or a hash of attributes for this node.
288
+ attr_reader :attributes
289
+
290
+ # The name of this tag.
291
+ attr_reader :name
292
+
293
+ # Create a new node as a child of the given parent, using the given content
294
+ # to describe the node. It will be parsed and the node name, attributes and
295
+ # closing status extracted.
296
+ def initialize(parent, line, pos, name, attributes, closing)
297
+ super(parent, line, pos)
298
+ @name = name
299
+ @attributes = attributes
300
+ @closing = closing
301
+ end
302
+
303
+ # A convenience for obtaining an attribute of the node. Returns +nil+ if
304
+ # the node has no attributes.
305
+ def [](attr)
306
+ @attributes ? @attributes[attr] : nil
307
+ end
308
+
309
+ # Returns non-+nil+ if this tag can contain child nodes.
310
+ def childless?(xml = false)
311
+ return false if xml && @closing.nil?
312
+ !@closing.nil? ||
313
+ @name =~ /^(img|br|hr|link|meta|area|base|basefont|
314
+ col|frame|input|isindex|param)$/ox
315
+ end
316
+
317
+ # Returns a textual representation of the node
318
+ def to_s
319
+ if @closing == :close
320
+ "</#{@name}>"
321
+ else
322
+ s = "<#{@name}"
323
+ @attributes.each do |k,v|
324
+ s << " #{k}"
325
+ s << "=\"#{v}\"" if String === v
326
+ end
327
+ s << " /" if @closing == :self
328
+ s << ">"
329
+ @children.each { |child| s << child.to_s }
330
+ s << "</#{@name}>" if @closing != :self && !@children.empty?
331
+ s
332
+ end
333
+ end
334
+
335
+ # If either the node or any of its children meet the given conditions, the
336
+ # matching node is returned. Otherwise, +nil+ is returned. (See the
337
+ # description of the valid conditions in the +match+ method.)
338
+ def find(conditions)
339
+ match(conditions) && self || super
340
+ end
341
+
342
+ # Returns +true+, indicating that this node represents an HTML tag.
343
+ def tag?
344
+ true
345
+ end
346
+
347
+ # Returns +true+ if the node meets any of the given conditions. The
348
+ # +conditions+ parameter must be a hash of any of the following keys
349
+ # (all are optional):
350
+ #
351
+ # * <tt>:tag</tt>: the node name must match the corresponding value
352
+ # * <tt>:attributes</tt>: a hash. The node's values must match the
353
+ # corresponding values in the hash.
354
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
355
+ # corresponding hash.
356
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
357
+ # must meet the criteria described by the hash.
358
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
359
+ # meet the criteria described by the hash.
360
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
361
+ # must meet the criteria described by the hash.
362
+ # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
363
+ # meet the criteria described by the hash.
364
+ # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
365
+ # the criteria described by the hash, and at least one sibling must match.
366
+ # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
367
+ # the criteria described by the hash, and at least one sibling must match.
368
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
369
+ # keys:
370
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
371
+ # include) the number of children that match.
372
+ # ** <tt>:less_than</tt>: the number of matching children must be less than
373
+ # this number.
374
+ # ** <tt>:greater_than</tt>: the number of matching children must be
375
+ # greater than this number.
376
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
377
+ # to match on the children, and only matching children will be
378
+ # counted.
379
+ #
380
+ # Conditions are matched using the following algorithm:
381
+ #
382
+ # * if the condition is a string, it must be a substring of the value.
383
+ # * if the condition is a regexp, it must match the value.
384
+ # * if the condition is a number, the value must match number.to_s.
385
+ # * if the condition is +true+, the value must not be +nil+.
386
+ # * if the condition is +false+ or +nil+, the value must be +nil+.
387
+ #
388
+ # Usage:
389
+ #
390
+ # # test if the node is a "span" tag
391
+ # node.match :tag => "span"
392
+ #
393
+ # # test if the node's parent is a "div"
394
+ # node.match :parent => { :tag => "div" }
395
+ #
396
+ # # test if any of the node's ancestors are "table" tags
397
+ # node.match :ancestor => { :tag => "table" }
398
+ #
399
+ # # test if any of the node's immediate children are "em" tags
400
+ # node.match :child => { :tag => "em" }
401
+ #
402
+ # # test if any of the node's descendants are "strong" tags
403
+ # node.match :descendant => { :tag => "strong" }
404
+ #
405
+ # # test if the node has between 2 and 4 span tags as immediate children
406
+ # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
407
+ #
408
+ # # get funky: test to see if the node is a "div", has a "ul" ancestor
409
+ # # and an "li" parent (with "class" = "enum"), and whether or not it has
410
+ # # a "span" descendant that contains # text matching /hello world/:
411
+ # node.match :tag => "div",
412
+ # :ancestor => { :tag => "ul" },
413
+ # :parent => { :tag => "li",
414
+ # :attributes => { :class => "enum" } },
415
+ # :descendant => { :tag => "span",
416
+ # :child => /hello world/ }
417
+ def match(conditions)
418
+ conditions = validate_conditions(conditions)
419
+ # check content of child nodes
420
+ if conditions[:content]
421
+ if children.empty?
422
+ return false unless match_condition("", conditions[:content])
423
+ else
424
+ return false unless children.find { |child| child.match(conditions[:content]) }
425
+ end
426
+ end
427
+
428
+ # test the name
429
+ return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
430
+
431
+ # test attributes
432
+ (conditions[:attributes] || {}).each do |key, value|
433
+ return false unless match_condition(self[key], value)
434
+ end
435
+
436
+ # test parent
437
+ return false unless parent.match(conditions[:parent]) if conditions[:parent]
438
+
439
+ # test children
440
+ return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
441
+
442
+ # test ancestors
443
+ if conditions[:ancestor]
444
+ return false unless catch :found do
445
+ p = self
446
+ throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
447
+ end
448
+ end
449
+
450
+ # test descendants
451
+ if conditions[:descendant]
452
+ return false unless children.find do |child|
453
+ # test the child
454
+ child.match(conditions[:descendant]) ||
455
+ # test the child's descendants
456
+ child.match(:descendant => conditions[:descendant])
457
+ end
458
+ end
459
+
460
+ # count children
461
+ if opts = conditions[:children]
462
+ matches = children.select do |c|
463
+ (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
464
+ end
465
+
466
+ matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
467
+ opts.each do |key, value|
468
+ next if key == :only
469
+ case key
470
+ when :count
471
+ if Integer === value
472
+ return false if matches.length != value
473
+ else
474
+ return false unless value.include?(matches.length)
475
+ end
476
+ when :less_than
477
+ return false unless matches.length < value
478
+ when :greater_than
479
+ return false unless matches.length > value
480
+ else raise "unknown count condition #{key}"
481
+ end
482
+ end
483
+ end
484
+
485
+ # test siblings
486
+ if conditions[:sibling] || conditions[:before] || conditions[:after]
487
+ siblings = parent ? parent.children : []
488
+ self_index = siblings.index(self)
489
+
490
+ if conditions[:sibling]
491
+ return false unless siblings.detect do |s|
492
+ s != self && s.match(conditions[:sibling])
493
+ end
494
+ end
495
+
496
+ if conditions[:before]
497
+ return false unless siblings[self_index+1..-1].detect do |s|
498
+ s != self && s.match(conditions[:before])
499
+ end
500
+ end
501
+
502
+ if conditions[:after]
503
+ return false unless siblings[0,self_index].detect do |s|
504
+ s != self && s.match(conditions[:after])
505
+ end
506
+ end
507
+ end
508
+
509
+ true
510
+ end
511
+
512
+ def ==(node)
513
+ return false unless super
514
+ return false unless closing == node.closing && self.name == node.name
515
+ attributes == node.attributes
516
+ end
517
+
518
+ private
519
+ # Match the given value to the given condition.
520
+ def match_condition(value, condition)
521
+ case condition
522
+ when String
523
+ value && value == condition
524
+ when Regexp
525
+ value && value.match(condition)
526
+ when Numeric
527
+ value == condition.to_s
528
+ when true
529
+ !value.nil?
530
+ when false, nil
531
+ value.nil?
532
+ else
533
+ false
534
+ end
535
+ end
536
+ end
537
+ end