herb 0.9.7-arm-linux-gnu → 0.10.1-arm-linux-gnu

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. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/config/action_view_helpers/actioncable/action_cable_helper/action_cable_meta_tag.yml +20 -0
  4. data/config/action_view_helpers/actionpack/content_security_policy/content_security_policy?.yml +16 -0
  5. data/config/action_view_helpers/actionpack/content_security_policy/content_security_policy_nonce.yml +16 -0
  6. data/config/action_view_helpers/actionpack/polymorphic_routes/polymorphic_path.yml +27 -0
  7. data/config/action_view_helpers/actionpack/polymorphic_routes/polymorphic_url.yml +33 -0
  8. data/config/action_view_helpers/actionpack/request_forgery_protection/form_authenticity_token.yml +19 -0
  9. data/config/action_view_helpers/actionpack/request_forgery_protection/protect_against_forgery?.yml +16 -0
  10. data/config/action_view_helpers/actiontext/content_helper/render_action_text_content.yml +21 -0
  11. data/config/action_view_helpers/actiontext/content_helper/sanitize_action_text_content.yml +21 -0
  12. data/config/action_view_helpers/actiontext/tag_helper/rich_textarea_tag.yml +46 -0
  13. data/config/action_view_helpers/actionview/active_model_helper/error_message.yml +16 -0
  14. data/config/action_view_helpers/actionview/active_model_helper/error_wrapping.yml +16 -0
  15. data/config/action_view_helpers/actionview/active_model_helper/object.yml +16 -0
  16. data/config/action_view_helpers/actionview/asset_tag_helper/apply_stylesheet_media_default.yml +16 -0
  17. data/config/action_view_helpers/actionview/asset_tag_helper/audio_tag.yml +45 -0
  18. data/config/action_view_helpers/actionview/asset_tag_helper/auto_discovery_link_tag.yml +44 -0
  19. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce.yml +16 -0
  20. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce_for_scripts.yml +16 -0
  21. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce_for_styles.yml +16 -0
  22. data/config/action_view_helpers/actionview/asset_tag_helper/favicon_link_tag.yml +46 -0
  23. data/config/action_view_helpers/actionview/asset_tag_helper/image_decoding.yml +16 -0
  24. data/config/action_view_helpers/actionview/asset_tag_helper/image_loading.yml +16 -0
  25. data/config/action_view_helpers/actionview/asset_tag_helper/image_tag.yml +68 -0
  26. data/config/action_view_helpers/actionview/asset_tag_helper/javascript_include_tag.yml +59 -0
  27. data/config/action_view_helpers/actionview/asset_tag_helper/picture_tag.yml +29 -0
  28. data/config/action_view_helpers/actionview/asset_tag_helper/preload_link_tag.yml +50 -0
  29. data/config/action_view_helpers/actionview/asset_tag_helper/preload_links_header.yml +16 -0
  30. data/config/action_view_helpers/actionview/asset_tag_helper/stylesheet_link_tag.yml +50 -0
  31. data/config/action_view_helpers/actionview/asset_tag_helper/video_tag.yml +55 -0
  32. data/config/action_view_helpers/actionview/asset_url_helper/asset_path.yml +23 -0
  33. data/config/action_view_helpers/actionview/asset_url_helper/asset_url.yml +23 -0
  34. data/config/action_view_helpers/actionview/asset_url_helper/audio_path.yml +23 -0
  35. data/config/action_view_helpers/actionview/asset_url_helper/audio_url.yml +23 -0
  36. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_extname.yml +16 -0
  37. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_host.yml +16 -0
  38. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_path.yml +16 -0
  39. data/config/action_view_helpers/actionview/asset_url_helper/font_path.yml +23 -0
  40. data/config/action_view_helpers/actionview/asset_url_helper/font_url.yml +23 -0
  41. data/config/action_view_helpers/actionview/asset_url_helper/image_path.yml +23 -0
  42. data/config/action_view_helpers/actionview/asset_url_helper/image_url.yml +23 -0
  43. data/config/action_view_helpers/actionview/asset_url_helper/javascript_path.yml +23 -0
  44. data/config/action_view_helpers/actionview/asset_url_helper/javascript_url.yml +23 -0
  45. data/config/action_view_helpers/actionview/asset_url_helper/public_compute_asset_path.yml +16 -0
  46. data/config/action_view_helpers/actionview/asset_url_helper/stylesheet_path.yml +23 -0
  47. data/config/action_view_helpers/actionview/asset_url_helper/stylesheet_url.yml +23 -0
  48. data/config/action_view_helpers/actionview/asset_url_helper/video_path.yml +23 -0
  49. data/config/action_view_helpers/actionview/asset_url_helper/video_url.yml +23 -0
  50. data/config/action_view_helpers/actionview/atom_feed_helper/atom_feed.yml +40 -0
  51. data/config/action_view_helpers/actionview/cache_helper/cache.yml +28 -0
  52. data/config/action_view_helpers/actionview/cache_helper/cache_fragment_name.yml +16 -0
  53. data/config/action_view_helpers/actionview/cache_helper/cache_if.yml +27 -0
  54. data/config/action_view_helpers/actionview/cache_helper/cache_unless.yml +27 -0
  55. data/config/action_view_helpers/actionview/cache_helper/caching_predicate.yml +16 -0
  56. data/config/action_view_helpers/actionview/cache_helper/digest_path_from_template.yml +16 -0
  57. data/config/action_view_helpers/actionview/cache_helper/fragment_for.yml +16 -0
  58. data/config/action_view_helpers/actionview/cache_helper/fragment_name_with_digest.yml +16 -0
  59. data/config/action_view_helpers/actionview/cache_helper/read_fragment_for.yml +16 -0
  60. data/config/action_view_helpers/actionview/cache_helper/uncacheable.yml +16 -0
  61. data/config/action_view_helpers/actionview/cache_helper/write_fragment_for.yml +16 -0
  62. data/config/action_view_helpers/actionview/capture_helper/capture.yml +16 -0
  63. data/config/action_view_helpers/actionview/capture_helper/content_for.yml +24 -0
  64. data/config/action_view_helpers/actionview/capture_helper/content_for_predicate.yml +21 -0
  65. data/config/action_view_helpers/actionview/capture_helper/provide.yml +21 -0
  66. data/config/action_view_helpers/actionview/capture_helper/with_output_buffer.yml +16 -0
  67. data/config/action_view_helpers/actionview/content_exfiltration_prevention_helper/prepend_content_exfiltration_prevention.yml +16 -0
  68. data/config/action_view_helpers/actionview/content_exfiltration_prevention_helper/prevent_content_exfiltration.yml +16 -0
  69. data/config/action_view_helpers/actionview/controller_helper/action_name.yml +16 -0
  70. data/config/action_view_helpers/actionview/controller_helper/assign_controller.yml +16 -0
  71. data/config/action_view_helpers/actionview/controller_helper/controller.yml +16 -0
  72. data/config/action_view_helpers/actionview/controller_helper/controller_name.yml +16 -0
  73. data/config/action_view_helpers/actionview/controller_helper/controller_path.yml +16 -0
  74. data/config/action_view_helpers/actionview/controller_helper/cookies.yml +16 -0
  75. data/config/action_view_helpers/actionview/controller_helper/flash.yml +16 -0
  76. data/config/action_view_helpers/actionview/controller_helper/headers.yml +16 -0
  77. data/config/action_view_helpers/actionview/controller_helper/logger.yml +18 -0
  78. data/config/action_view_helpers/actionview/controller_helper/params.yml +16 -0
  79. data/config/action_view_helpers/actionview/controller_helper/request.yml +16 -0
  80. data/config/action_view_helpers/actionview/controller_helper/request_forgery_protection_token.yml +16 -0
  81. data/config/action_view_helpers/actionview/controller_helper/response.yml +16 -0
  82. data/config/action_view_helpers/actionview/controller_helper/session.yml +16 -0
  83. data/config/action_view_helpers/actionview/csp_helper/csp_meta_tag.yml +23 -0
  84. data/config/action_view_helpers/actionview/csrf_helper/csrf_meta_tags.yml +23 -0
  85. data/config/action_view_helpers/actionview/date_helper/date_select.yml +71 -0
  86. data/config/action_view_helpers/actionview/date_helper/datetime_select.yml +59 -0
  87. data/config/action_view_helpers/actionview/date_helper/distance_of_time_in_words.yml +33 -0
  88. data/config/action_view_helpers/actionview/date_helper/relative_time_in_words.yml +36 -0
  89. data/config/action_view_helpers/actionview/date_helper/select_date.yml +49 -0
  90. data/config/action_view_helpers/actionview/date_helper/select_datetime.yml +58 -0
  91. data/config/action_view_helpers/actionview/date_helper/select_day.yml +42 -0
  92. data/config/action_view_helpers/actionview/date_helper/select_hour.yml +48 -0
  93. data/config/action_view_helpers/actionview/date_helper/select_minute.yml +42 -0
  94. data/config/action_view_helpers/actionview/date_helper/select_month.yml +54 -0
  95. data/config/action_view_helpers/actionview/date_helper/select_second.yml +39 -0
  96. data/config/action_view_helpers/actionview/date_helper/select_time.yml +52 -0
  97. data/config/action_view_helpers/actionview/date_helper/select_year.yml +45 -0
  98. data/config/action_view_helpers/actionview/date_helper/time_ago_in_words.yml +26 -0
  99. data/config/action_view_helpers/actionview/date_helper/time_select.yml +56 -0
  100. data/config/action_view_helpers/actionview/date_helper/time_tag.yml +37 -0
  101. data/config/action_view_helpers/actionview/debug_helper/debug.yml +21 -0
  102. data/config/action_view_helpers/actionview/form_helper/apply_form_for_options.yml +16 -0
  103. data/config/action_view_helpers/actionview/form_helper/button.yml +38 -0
  104. data/config/action_view_helpers/actionview/form_helper/checkbox.yml +49 -0
  105. data/config/action_view_helpers/actionview/form_helper/color_field.yml +32 -0
  106. data/config/action_view_helpers/actionview/form_helper/date_field.yml +38 -0
  107. data/config/action_view_helpers/actionview/form_helper/datetime_field.yml +43 -0
  108. data/config/action_view_helpers/actionview/form_helper/email_field.yml +32 -0
  109. data/config/action_view_helpers/actionview/form_helper/emitted_hidden_id.yml +16 -0
  110. data/config/action_view_helpers/actionview/form_helper/fields.yml +31 -0
  111. data/config/action_view_helpers/actionview/form_helper/fields_for.yml +33 -0
  112. data/config/action_view_helpers/actionview/form_helper/file_field.yml +44 -0
  113. data/config/action_view_helpers/actionview/form_helper/form_for.yml +41 -0
  114. data/config/action_view_helpers/actionview/form_helper/form_with.yml +42 -0
  115. data/config/action_view_helpers/actionview/form_helper/form_with_generates_ids.yml +16 -0
  116. data/config/action_view_helpers/actionview/form_helper/form_with_generates_remote_forms.yml +16 -0
  117. data/config/action_view_helpers/actionview/form_helper/hidden_field.yml +32 -0
  118. data/config/action_view_helpers/actionview/form_helper/label.yml +47 -0
  119. data/config/action_view_helpers/actionview/form_helper/month_field.yml +32 -0
  120. data/config/action_view_helpers/actionview/form_helper/multipart.yml +16 -0
  121. data/config/action_view_helpers/actionview/form_helper/multiple_file_field_include_hidden.yml +16 -0
  122. data/config/action_view_helpers/actionview/form_helper/number_field.yml +44 -0
  123. data/config/action_view_helpers/actionview/form_helper/password_field.yml +32 -0
  124. data/config/action_view_helpers/actionview/form_helper/radio_button.yml +37 -0
  125. data/config/action_view_helpers/actionview/form_helper/range_field.yml +44 -0
  126. data/config/action_view_helpers/actionview/form_helper/search_field.yml +32 -0
  127. data/config/action_view_helpers/actionview/form_helper/submit.yml +35 -0
  128. data/config/action_view_helpers/actionview/form_helper/telephone_field.yml +34 -0
  129. data/config/action_view_helpers/actionview/form_helper/text_field.yml +32 -0
  130. data/config/action_view_helpers/actionview/form_helper/textarea.yml +37 -0
  131. data/config/action_view_helpers/actionview/form_helper/time_field.yml +41 -0
  132. data/config/action_view_helpers/actionview/form_helper/url_field.yml +32 -0
  133. data/config/action_view_helpers/actionview/form_helper/week_field.yml +32 -0
  134. data/config/action_view_helpers/actionview/form_options_helper/collection_checkboxes.yml +58 -0
  135. data/config/action_view_helpers/actionview/form_options_helper/collection_radio_buttons.yml +56 -0
  136. data/config/action_view_helpers/actionview/form_options_helper/collection_select.yml +65 -0
  137. data/config/action_view_helpers/actionview/form_options_helper/grouped_collection_select.yml +69 -0
  138. data/config/action_view_helpers/actionview/form_options_helper/grouped_options_for_select.yml +39 -0
  139. data/config/action_view_helpers/actionview/form_options_helper/option_groups_from_collection_for_select.yml +47 -0
  140. data/config/action_view_helpers/actionview/form_options_helper/options_for_select.yml +27 -0
  141. data/config/action_view_helpers/actionview/form_options_helper/options_from_collection_for_select.yml +37 -0
  142. data/config/action_view_helpers/actionview/form_options_helper/select.yml +59 -0
  143. data/config/action_view_helpers/actionview/form_options_helper/time_zone_options_for_select.yml +34 -0
  144. data/config/action_view_helpers/actionview/form_options_helper/time_zone_select.yml +53 -0
  145. data/config/action_view_helpers/actionview/form_options_helper/weekday_options_for_select.yml +31 -0
  146. data/config/action_view_helpers/actionview/form_options_helper/weekday_select.yml +38 -0
  147. data/config/action_view_helpers/actionview/form_tag_helper/button_tag.yml +39 -0
  148. data/config/action_view_helpers/actionview/form_tag_helper/checkbox_tag.yml +45 -0
  149. data/config/action_view_helpers/actionview/form_tag_helper/color_field_tag.yml +38 -0
  150. data/config/action_view_helpers/actionview/form_tag_helper/date_field_tag.yml +37 -0
  151. data/config/action_view_helpers/actionview/form_tag_helper/datetime_field_tag.yml +39 -0
  152. data/config/action_view_helpers/actionview/form_tag_helper/default_enforce_utf8.yml +16 -0
  153. data/config/action_view_helpers/actionview/form_tag_helper/email_field_tag.yml +37 -0
  154. data/config/action_view_helpers/actionview/form_tag_helper/embed_authenticity_token_in_remote_forms.yml +16 -0
  155. data/config/action_view_helpers/actionview/form_tag_helper/field_id.yml +32 -0
  156. data/config/action_view_helpers/actionview/form_tag_helper/field_name.yml +32 -0
  157. data/config/action_view_helpers/actionview/form_tag_helper/field_set_tag.yml +34 -0
  158. data/config/action_view_helpers/actionview/form_tag_helper/file_field_tag.yml +41 -0
  159. data/config/action_view_helpers/actionview/form_tag_helper/form_tag.yml +42 -0
  160. data/config/action_view_helpers/actionview/form_tag_helper/hidden_field_tag.yml +37 -0
  161. data/config/action_view_helpers/actionview/form_tag_helper/image_submit_tag.yml +40 -0
  162. data/config/action_view_helpers/actionview/form_tag_helper/label_tag.yml +38 -0
  163. data/config/action_view_helpers/actionview/form_tag_helper/month_field_tag.yml +37 -0
  164. data/config/action_view_helpers/actionview/form_tag_helper/number_field_tag.yml +49 -0
  165. data/config/action_view_helpers/actionview/form_tag_helper/password_field_tag.yml +38 -0
  166. data/config/action_view_helpers/actionview/form_tag_helper/radio_button_tag.yml +42 -0
  167. data/config/action_view_helpers/actionview/form_tag_helper/range_field_tag.yml +49 -0
  168. data/config/action_view_helpers/actionview/form_tag_helper/search_field_tag.yml +37 -0
  169. data/config/action_view_helpers/actionview/form_tag_helper/select_tag.yml +49 -0
  170. data/config/action_view_helpers/actionview/form_tag_helper/submit_tag.yml +39 -0
  171. data/config/action_view_helpers/actionview/form_tag_helper/telephone_field_tag.yml +39 -0
  172. data/config/action_view_helpers/actionview/form_tag_helper/text_field_tag.yml +37 -0
  173. data/config/action_view_helpers/actionview/form_tag_helper/textarea_tag.yml +43 -0
  174. data/config/action_view_helpers/actionview/form_tag_helper/time_field_tag.yml +37 -0
  175. data/config/action_view_helpers/actionview/form_tag_helper/url_field_tag.yml +37 -0
  176. data/config/action_view_helpers/actionview/form_tag_helper/utf8_enforcer_tag.yml +16 -0
  177. data/config/action_view_helpers/actionview/form_tag_helper/week_field_tag.yml +37 -0
  178. data/config/action_view_helpers/actionview/javascript_helper/escape_javascript.yml +23 -0
  179. data/config/action_view_helpers/actionview/javascript_helper/javascript_cdata_section.yml +16 -0
  180. data/config/action_view_helpers/actionview/javascript_helper/javascript_tag.yml +45 -0
  181. data/config/action_view_helpers/actionview/number_helper/number_to_currency.yml +36 -0
  182. data/config/action_view_helpers/actionview/number_helper/number_to_human.yml +27 -0
  183. data/config/action_view_helpers/actionview/number_helper/number_to_human_size.yml +24 -0
  184. data/config/action_view_helpers/actionview/number_helper/number_to_percentage.yml +24 -0
  185. data/config/action_view_helpers/actionview/number_helper/number_to_phone.yml +34 -0
  186. data/config/action_view_helpers/actionview/number_helper/number_with_delimiter.yml +29 -0
  187. data/config/action_view_helpers/actionview/number_helper/number_with_precision.yml +27 -0
  188. data/config/action_view_helpers/actionview/output_safety_helper/raw.yml +21 -0
  189. data/config/action_view_helpers/actionview/output_safety_helper/safe_join.yml +26 -0
  190. data/config/action_view_helpers/actionview/output_safety_helper/to_sentence.yml +33 -0
  191. data/config/action_view_helpers/actionview/record_identifier/dom_class.yml +27 -0
  192. data/config/action_view_helpers/actionview/record_identifier/dom_id.yml +27 -0
  193. data/config/action_view_helpers/actionview/record_identifier/dom_target.yml +22 -0
  194. data/config/action_view_helpers/actionview/record_identifier/model_name_from_record_or_class.yml +16 -0
  195. data/config/action_view_helpers/actionview/rendering_helper/render.yml +34 -0
  196. data/config/action_view_helpers/actionview/sanitize_helper/sanitize.yml +27 -0
  197. data/config/action_view_helpers/actionview/sanitize_helper/sanitize_css.yml +21 -0
  198. data/config/action_view_helpers/actionview/sanitize_helper/sanitizer_vendor.yml +16 -0
  199. data/config/action_view_helpers/actionview/sanitize_helper/strip_links.yml +21 -0
  200. data/config/action_view_helpers/actionview/sanitize_helper/strip_tags.yml +21 -0
  201. data/config/action_view_helpers/actionview/tag_helper/cdata_section.yml +21 -0
  202. data/config/action_view_helpers/actionview/tag_helper/content_tag.yml +64 -0
  203. data/config/action_view_helpers/actionview/tag_helper/escape_once.yml +21 -0
  204. data/config/action_view_helpers/actionview/tag_helper/tag.yml +47 -0
  205. data/config/action_view_helpers/actionview/tag_helper/token_list.yml +24 -0
  206. data/config/action_view_helpers/actionview/text_helper/concat.yml +21 -0
  207. data/config/action_view_helpers/actionview/text_helper/current_cycle.yml +22 -0
  208. data/config/action_view_helpers/actionview/text_helper/cycle.yml +31 -0
  209. data/config/action_view_helpers/actionview/text_helper/excerpt.yml +39 -0
  210. data/config/action_view_helpers/actionview/text_helper/highlight.yml +35 -0
  211. data/config/action_view_helpers/actionview/text_helper/pluralize.yml +32 -0
  212. data/config/action_view_helpers/actionview/text_helper/reset_cycle.yml +22 -0
  213. data/config/action_view_helpers/actionview/text_helper/safe_concat.yml +16 -0
  214. data/config/action_view_helpers/actionview/text_helper/simple_format.yml +40 -0
  215. data/config/action_view_helpers/actionview/text_helper/truncate.yml +40 -0
  216. data/config/action_view_helpers/actionview/text_helper/word_wrap.yml +27 -0
  217. data/config/action_view_helpers/actionview/translation_helper/debug_missing_translation.yml +16 -0
  218. data/config/action_view_helpers/actionview/translation_helper/localize.yml +29 -0
  219. data/config/action_view_helpers/actionview/translation_helper/translate.yml +32 -0
  220. data/config/action_view_helpers/actionview/url_helper/button_to.yml +55 -0
  221. data/config/action_view_helpers/actionview/url_helper/button_to_generates_button_tag.yml +16 -0
  222. data/config/action_view_helpers/actionview/url_helper/current_page?.yml +28 -0
  223. data/config/action_view_helpers/actionview/url_helper/link_to.yml +76 -0
  224. data/config/action_view_helpers/actionview/url_helper/link_to_if.yml +42 -0
  225. data/config/action_view_helpers/actionview/url_helper/link_to_unless.yml +42 -0
  226. data/config/action_view_helpers/actionview/url_helper/link_to_unless_current.yml +37 -0
  227. data/config/action_view_helpers/actionview/url_helper/mail_to.yml +56 -0
  228. data/config/action_view_helpers/actionview/url_helper/phone_to.yml +44 -0
  229. data/config/action_view_helpers/actionview/url_helper/sms_to.yml +47 -0
  230. data/config/action_view_helpers/actionview/url_helper/url_for.yml +22 -0
  231. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_cache.yml +16 -0
  232. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_cache_tag.yml +20 -0
  233. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_preview.yml +16 -0
  234. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_preview_tag.yml +20 -0
  235. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_page_requires_reload.yml +16 -0
  236. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_page_requires_reload_tag.yml +20 -0
  237. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refresh_method_tag.yml +26 -0
  238. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refresh_scroll_tag.yml +26 -0
  239. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refreshes_with.yml +22 -0
  240. data/config/action_view_helpers/turbo-rails/frames_helper/turbo_frame_tag.yml +50 -0
  241. data/config/action_view_helpers/turbo-rails/includes_helper/turbo_include_tags.yml +16 -0
  242. data/config/action_view_helpers/turbo-rails/streams_action_helper/turbo_stream_action_tag.yml +34 -0
  243. data/config/action_view_helpers/turbo-rails/streams_action_helper/turbo_stream_refresh_tag.yml +23 -0
  244. data/config/action_view_helpers/turbo-rails/streams_helper/turbo_stream.yml +16 -0
  245. data/config/action_view_helpers/turbo-rails/streams_helper/turbo_stream_from.yml +33 -0
  246. data/config/options.yml +14 -0
  247. data/config/view_context_methods/actionview/base/annotate_rendered_view_with_filenames.yml +8 -0
  248. data/config/view_context_methods/actionview/base/assign.yml +8 -0
  249. data/config/view_context_methods/actionview/base/assigns.yml +8 -0
  250. data/config/view_context_methods/actionview/base/automatically_disable_submit_tag.yml +8 -0
  251. data/config/view_context_methods/actionview/base/compiled_method_container.yml +8 -0
  252. data/config/view_context_methods/actionview/base/config.yml +8 -0
  253. data/config/view_context_methods/actionview/base/default_form_builder.yml +8 -0
  254. data/config/view_context_methods/actionview/base/default_formats.yml +8 -0
  255. data/config/view_context_methods/actionview/base/field_error_proc.yml +8 -0
  256. data/config/view_context_methods/actionview/base/formats.yml +8 -0
  257. data/config/view_context_methods/actionview/base/in_rendering_context.yml +8 -0
  258. data/config/view_context_methods/actionview/base/locale.yml +8 -0
  259. data/config/view_context_methods/actionview/base/lookup_context.yml +8 -0
  260. data/config/view_context_methods/actionview/base/output_buffer.yml +8 -0
  261. data/config/view_context_methods/actionview/base/prefix_partial_path_with_controller_namespace.yml +10 -0
  262. data/config/view_context_methods/actionview/base/remove_hidden_field_autocomplete.yml +8 -0
  263. data/config/view_context_methods/actionview/base/streaming_completion_on_exception.yml +8 -0
  264. data/config/view_context_methods/actionview/base/view_flow.yml +8 -0
  265. data/config/view_context_methods/actionview/base/view_paths.yml +8 -0
  266. data/config/view_context_methods/actionview/base/view_renderer.yml +8 -0
  267. data/config/view_context_methods/actionview/template/local_assigns.yml +8 -0
  268. data/config/view_context_methods/activemodel/conversion/convert_to_model.yml +8 -0
  269. data/config/view_context_methods/activesupport/benchmarkable/benchmark.yml +8 -0
  270. data/ext/herb/extconf.rb +1 -0
  271. data/ext/herb/extension.c +108 -0
  272. data/herb.gemspec +2 -1
  273. data/lib/herb/3.2/herb.so +0 -0
  274. data/lib/herb/3.3/herb.so +0 -0
  275. data/lib/herb/3.4/herb.so +0 -0
  276. data/lib/herb/4.0/herb.so +0 -0
  277. data/lib/herb/action_view/render_analyzer.rb +1057 -0
  278. data/lib/herb/ast/erb_render_node.rb +155 -0
  279. data/lib/herb/bootstrap.rb +0 -1
  280. data/lib/herb/cli.rb +253 -19
  281. data/lib/herb/colors.rb +18 -0
  282. data/lib/herb/configuration.rb +49 -13
  283. data/lib/herb/defaults.yml +3 -0
  284. data/lib/herb/dev/runner.rb +445 -0
  285. data/lib/herb/dev/server.rb +207 -0
  286. data/lib/herb/dev/server_entry.rb +128 -0
  287. data/lib/herb/diff_operation.rb +34 -0
  288. data/lib/herb/diff_result.rb +59 -0
  289. data/lib/herb/engine/compiler.rb +56 -3
  290. data/lib/herb/engine/validators/render_validator.rb +92 -0
  291. data/lib/herb/engine.rb +58 -4
  292. data/lib/herb/html/util.rb +16 -0
  293. data/lib/herb/project.rb +1 -6
  294. data/lib/herb/version.rb +1 -1
  295. data/lib/herb.rb +41 -5
  296. data/sig/herb/action_view/render_analyzer.rbs +122 -0
  297. data/sig/herb/ast/erb_render_node.rbs +29 -0
  298. data/sig/herb/colors.rbs +12 -0
  299. data/sig/herb/configuration.rbs +20 -1
  300. data/sig/herb/dev/runner.rbs +59 -0
  301. data/sig/herb/dev/server.rbs +50 -0
  302. data/sig/herb/dev/server_entry.rbs +51 -0
  303. data/sig/herb/diff_operation.rbs +34 -0
  304. data/sig/herb/diff_result.rbs +34 -0
  305. data/sig/herb/engine/compiler.rbs +6 -0
  306. data/sig/herb/engine/validators/render_validator.rbs +21 -0
  307. data/sig/herb/engine.rbs +15 -0
  308. data/sig/herb/html/util.rbs +13 -0
  309. data/sig/herb.rbs +12 -2
  310. data/sig/herb_c_extension.rbs +1 -1
  311. data/sig/vendor/did_you_mean.rbs +6 -0
  312. data/sig/vendor/parallel.rbs +4 -0
  313. data/src/analyze/action_view/attribute_extraction_helpers.c +3 -2
  314. data/src/diff/herb_diff.c +137 -0
  315. data/src/diff/herb_diff_attributes.c +207 -0
  316. data/src/diff/herb_diff_children.c +518 -0
  317. data/src/diff/herb_diff_helpers.c +114 -0
  318. data/src/diff/herb_diff_nodes.c +707 -0
  319. data/src/diff/herb_hash.c +42 -0
  320. data/src/diff/herb_hash_index_map.c +47 -0
  321. data/src/diff/herb_hash_map.c +104 -0
  322. data/src/diff/herb_hash_tree.c +680 -0
  323. data/src/include/diff/herb_diff.h +118 -0
  324. data/src/include/diff/herb_hash.h +25 -0
  325. data/src/include/diff/herb_hash_index_map.h +32 -0
  326. data/src/include/diff/herb_hash_map.h +30 -0
  327. data/src/include/herb.h +1 -0
  328. data/src/include/version.h +1 -1
  329. data/templates/javascript/packages/core/src/config.ts.erb +43 -0
  330. data/templates/rust/src/ast/nodes.rs.erb +1 -1
  331. data/templates/rust/src/config.rs.erb +50 -0
  332. data/templates/src/diff/herb_diff_helpers.c.erb +38 -0
  333. data/templates/src/diff/herb_diff_nodes.c.erb +224 -0
  334. data/templates/src/diff/herb_hash_tree.c.erb +147 -0
  335. data/templates/template.rb +4 -4
  336. metadata +307 -4
  337. data/lib/herb/3.0/herb.so +0 -0
  338. data/lib/herb/3.1/herb.so +0 -0
@@ -0,0 +1,445 @@
1
+ # frozen_string_literal: true
2
+ # typed: ignore
3
+
4
+ require_relative "../colors"
5
+
6
+ module Herb
7
+ module Dev
8
+ class Runner
9
+ include Herb::Colors
10
+
11
+ PATCHABLE_TYPES = ["text_changed", "attribute_value_changed", "attribute_added", "attribute_removed"].freeze #: Array[String]
12
+
13
+ def self.can_patch?(operations)
14
+ operations.all? { |operation|
15
+ next false unless PATCHABLE_TYPES.include?(operation.type.to_s)
16
+ next false if operation.new_node&.type&.to_s&.include?("ERB")
17
+ next false if operation.old_node&.type&.to_s&.include?("ERB")
18
+
19
+ true
20
+ }
21
+ end
22
+
23
+ CLEAR_SCREEN = "\e[2J\e[H"
24
+ HIDE_CURSOR = "\e[?25l"
25
+ SHOW_CURSOR = "\e[?25h"
26
+
27
+ def initialize(path: ".", cli: nil)
28
+ @path = path
29
+ @cli = cli
30
+ end
31
+
32
+ def run
33
+ require_cruise
34
+ require_relative "server"
35
+
36
+ unless File.directory?(@path)
37
+ puts "Not a directory: '#{@path}'."
38
+ exit(1)
39
+ end
40
+
41
+ config = Herb::Configuration.load(@path)
42
+ expanded_path = File.realpath(File.expand_path(config.project_root || @path))
43
+
44
+ check_existing_server(expanded_path)
45
+ port = find_port
46
+
47
+ print CLEAR_SCREEN
48
+ print HIDE_CURSOR
49
+ print_header(config, expanded_path)
50
+
51
+ file_states = index_files(config, @path)
52
+
53
+ websocket = Herb::Dev::Server.new(port: port, project_path: expanded_path)
54
+ websocket.start
55
+
56
+ puts " #{fg("WebSocket:".ljust(11), 245)}#{fg("ws://localhost:#{websocket.port}", 250)}"
57
+ puts
58
+ puts " #{fg("Ready!", 42)} #{fg("Watching for changes...", 241)}"
59
+ puts
60
+
61
+ watch_files(config, expanded_path, websocket, file_states)
62
+ rescue Interrupt
63
+ websocket&.stop
64
+ print SHOW_CURSOR
65
+ puts
66
+ puts "Stopped."
67
+ exit(0)
68
+ ensure
69
+ websocket&.stop
70
+ print SHOW_CURSOR
71
+ end
72
+
73
+ def stop
74
+ require_relative "server"
75
+
76
+ entries = Herb::Dev::ServerEntry.all
77
+
78
+ if entries.empty?
79
+ puts "No herb dev servers running."
80
+ exit(0)
81
+ end
82
+
83
+ entries.each do |entry|
84
+ entry.stop!
85
+ puts "Stopped herb dev server for #{entry.project_name} (PID: #{entry.pid}, port: #{entry.port})"
86
+ end
87
+
88
+ exit(0)
89
+ end
90
+
91
+ def restart
92
+ require_relative "server"
93
+
94
+ Herb::Dev::ServerEntry.all.each(&:stop!)
95
+ sleep 0.5
96
+ run
97
+ end
98
+
99
+ def status
100
+ require_relative "server"
101
+
102
+ entries = Herb::Dev::ServerEntry.all
103
+
104
+ if entries.empty?
105
+ puts "No herb dev servers running."
106
+ else
107
+ entries.each do |entry|
108
+ puts "#{entry.project_name} — PID: #{entry.pid}, port: #{entry.port}, started: #{entry.started_at}"
109
+ end
110
+ end
111
+
112
+ exit(0)
113
+ end
114
+
115
+ private
116
+
117
+ def pluralize(count, word)
118
+ "#{count} #{word}#{"s" unless count == 1}"
119
+ end
120
+
121
+ def require_cruise
122
+ require "cruise"
123
+ rescue LoadError
124
+ abort <<~MESSAGE
125
+ The 'cruise' gem is required for the Herb Dev Server.
126
+
127
+ Install it:
128
+ gem install cruise
129
+
130
+ or add to your Gemfile:
131
+ bundle add cruise
132
+ MESSAGE
133
+ end
134
+
135
+ def check_existing_server(expanded_path)
136
+ existing = Herb::Dev::ServerEntry.find_by_project(expanded_path)
137
+
138
+ return unless existing
139
+
140
+ puts "Herb dev server is already running for this project (PID: #{existing.pid}, port: #{existing.port})."
141
+ puts
142
+ puts " herb dev stop Stop the running server"
143
+ puts " herb dev restart Restart the server"
144
+ exit(1)
145
+ end
146
+
147
+ def find_port
148
+ port = Herb::Dev::Server::DEFAULT_PORT
149
+ port_owner = Herb::Dev::ServerEntry.find_by_port(port)
150
+
151
+ if port_owner
152
+ port = Herb::Dev::Server.find_available_port(port + 1)
153
+ abort "No available ports found" unless port
154
+ end
155
+
156
+ port
157
+ end
158
+
159
+ def print_header(config, expanded_path)
160
+ puts
161
+ puts fg_bg(" \u{1F33F} Herb Dev Server ", 255, 28)
162
+ puts
163
+ puts " #{fg("\u26A0\uFE0F Experimental:", 214)} #{fg("The dev server is experimental and may not work correctly in all cases.", 241)}"
164
+ puts
165
+
166
+ puts " #{fg("Herb:".ljust(11), 245)}#{fg(Herb::VERSION, 250)}"
167
+ puts " #{fg("Project:".ljust(11), 245)}#{fg(expanded_path, 250)}"
168
+ puts " #{fg("PID:".ljust(11), 245)}#{fg(Process.pid, 250)} #{fg("(#{File.join(Herb::Dev::ServerEntry::SERVERS_DIR, "#{Process.pid}.json")})", 241)}"
169
+
170
+ if config.config_path
171
+ relative_config = config.config_path.to_s.delete_prefix("#{expanded_path}/")
172
+ puts " #{fg("Config:".ljust(11), 245)}#{fg(relative_config, 250)}"
173
+ else
174
+ puts " #{fg("Config:".ljust(11), 245)}#{fg("(defaults)", 241)}"
175
+ end
176
+ end
177
+
178
+ def index_files(config, path)
179
+ puts " #{fg("Indexing files...", 241)}"
180
+
181
+ file_states = {}
182
+ initial_files = config.find_files(path)
183
+
184
+ initial_files.each do |file_path|
185
+ file_states[file_path] = File.read(file_path)
186
+ rescue StandardError
187
+ # skip files that can't be read
188
+ end
189
+
190
+ print "\e[1A\e[2K"
191
+ puts " #{fg("Files:".ljust(11), 245)}#{fg("#{file_states.size} templates indexed", 250)}"
192
+
193
+ file_states
194
+ end
195
+
196
+ def watch_files(config, expanded_path, websocket, file_states)
197
+ include_patterns = config.file_include_patterns
198
+ exclude_patterns = config.file_exclude_patterns
199
+ first_change = true
200
+ errored_files = Set.new
201
+
202
+ Thread.new do
203
+ $stdin.gets(nil)
204
+ Thread.main.raise(Interrupt)
205
+ rescue IOError, Errno::EBADF
206
+ Thread.main.raise(Interrupt)
207
+ end
208
+
209
+ Cruise.watch(expanded_path, only: ["created", "modified", "removed"]) do |event|
210
+ file_path = event.path
211
+ relative_path = file_path.delete_prefix("#{expanded_path}/")
212
+
213
+ next if config.path_excluded?(relative_path, exclude_patterns)
214
+ next unless config.path_included?(relative_path, include_patterns)
215
+
216
+ if first_change
217
+ print "\e[2A\e[J"
218
+ puts " #{fg("Recent changes:", 245)}"
219
+ puts
220
+ first_change = false
221
+ end
222
+
223
+ timestamp = fg(Time.now.strftime("%H:%M:%S.%L"), 241)
224
+ display_path = fg(relative_path, 250)
225
+
226
+ case event.kind
227
+ when "created", "modified"
228
+ handle_file_change(file_path, relative_path, file_states, errored_files, websocket, timestamp, display_path)
229
+ when "removed"
230
+ file_states.delete(file_path)
231
+ badge = bold(fg("- removed", 196))
232
+ puts " #{timestamp} #{badge} #{display_path}"
233
+ end
234
+ end
235
+ end
236
+
237
+ def handle_file_change(file_path, relative_path, file_states, errored_files, websocket, timestamp, display_path)
238
+ return unless File.exist?(file_path)
239
+
240
+ current_content = File.read(file_path)
241
+ previous_content = file_states[file_path]
242
+
243
+ if previous_content.nil?
244
+ file_states[file_path] = current_content
245
+ badge = bold(fg("+ added ", 42))
246
+ puts " #{timestamp} #{badge} #{display_path}"
247
+ return
248
+ end
249
+
250
+ return if previous_content == current_content && !errored_files.include?(file_path)
251
+
252
+ current_parse = Herb.parse(current_content, strict: true, analyze: true)
253
+
254
+ if current_parse.errors.any?
255
+ broadcast_errors(file_path, relative_path, current_parse, previous_content, errored_files, websocket, timestamp, display_path)
256
+ return
257
+ end
258
+
259
+ if errored_files.delete?(file_path)
260
+ broadcast_fixed(file_path, relative_path, current_content, file_states, websocket, timestamp, display_path)
261
+ return
262
+ end
263
+
264
+ handle_diff(file_path, relative_path, current_content, previous_content, file_states, websocket, timestamp, display_path)
265
+ end
266
+
267
+ def broadcast_errors(file_path, relative_path, current_parse, previous_content, errored_files, websocket, timestamp, display_path)
268
+ current_errors = current_parse.errors
269
+
270
+ previous_parse = Herb.parse(previous_content, strict: true, analyze: true)
271
+ previous_errors = previous_parse.errors
272
+
273
+ new_errors = current_errors.select { |error|
274
+ previous_errors.none? { |previous_error|
275
+ previous_error.error_name == error.error_name && previous_error.location.start.line == error.location.start.line
276
+ }
277
+ }
278
+
279
+ badge = bold(fg("\u{2717} error ", 196))
280
+ puts " #{timestamp} #{badge} #{display_path} #{fg("(#{pluralize(current_errors.size, "error")})", 241)}"
281
+
282
+ new_errors.each do |error|
283
+ location = fg("#{relative_path}:#{error.location.start.line}:#{error.location.start.column}", 241)
284
+ puts " #{fg(error.error_name, 196)} #{location}"
285
+ puts " #{fg(error.message, 250)}" if error.message && !error.message.empty?
286
+ end
287
+
288
+ errored_files.add(file_path)
289
+
290
+ if websocket.client_count.positive?
291
+ websocket.broadcast({
292
+ type: "error",
293
+ file: relative_path,
294
+ errors: current_errors.map { |error|
295
+ {
296
+ name: error.error_name,
297
+ message: error.message,
298
+ line: error.location.start.line,
299
+ column: error.location.start.column,
300
+ }
301
+ },
302
+ })
303
+ end
304
+
305
+ puts
306
+ end
307
+
308
+ def broadcast_fixed(file_path, relative_path, current_content, file_states, websocket, timestamp, display_path)
309
+ file_states[file_path] = current_content
310
+ badge = bold(fg("\u{2713} fixed ", 42))
311
+ puts " #{timestamp} #{badge} #{display_path}"
312
+
313
+ if websocket.client_count.positive?
314
+ websocket.broadcast({ type: "fixed", file: relative_path })
315
+ end
316
+
317
+ puts
318
+ end
319
+
320
+ def handle_diff(file_path, relative_path, current_content, previous_content, file_states, websocket, timestamp, display_path)
321
+ diff_result = Herb.diff(previous_content, current_content)
322
+ file_states[file_path] = current_content
323
+
324
+ return if diff_result.identical?
325
+
326
+ operations = diff_result.operations
327
+ can_patch = self.class.can_patch?(operations)
328
+
329
+ if can_patch && websocket.client_count.positive?
330
+ patch_operations = operations.map do |operation|
331
+ {
332
+ type: operation.type.to_s,
333
+ path: operation.path,
334
+ old_value: extract_node_value(operation.old_node),
335
+ new_value: extract_node_value(operation.new_node),
336
+ old_node_type: operation.old_node&.type,
337
+ new_node_type: operation.new_node&.type,
338
+ }
339
+ end
340
+
341
+ websocket.broadcast({
342
+ type: "patch",
343
+ file: relative_path,
344
+ operations: patch_operations,
345
+ })
346
+ elsif !can_patch && websocket.client_count.positive?
347
+ websocket.broadcast({
348
+ type: "reload",
349
+ file: relative_path,
350
+ })
351
+ end
352
+
353
+ print_diff_summary(operations, can_patch, websocket, timestamp, display_path)
354
+ end
355
+
356
+ def print_diff_summary(operations, can_patch, websocket, timestamp, display_path)
357
+ badge = if can_patch
358
+ bold(fg("\u{2713} patch ", 42))
359
+ else
360
+ bold(fg("\u{21BB} reload ", 214))
361
+ end
362
+
363
+ clients_label = websocket.client_count.positive? ? " #{fg("[#{pluralize(websocket.client_count, "client")}]", 241)}" : ""
364
+ puts " #{timestamp} #{badge} #{display_path} #{fg("(#{pluralize(operations.size, "operation")})", 241)}#{clients_label}"
365
+
366
+ operations.each_with_index do |operation, index|
367
+ type = operation.type.to_s
368
+
369
+ type_color = case type
370
+ when "node_inserted" then 114
371
+ when "node_removed", "attribute_removed" then 168
372
+ when "node_replaced", "tag_name_changed" then 173
373
+ when "node_wrapped", "node_unwrapped", "attribute_added", "attribute_value_changed" then 75
374
+ when "node_moved" then 73
375
+ when "text_changed" then 186
376
+ when "erb_content_changed" then 176
377
+ else 241
378
+ end
379
+
380
+ type_label = type.tr("_", " ")
381
+ index_label = fg("##{index + 1}", 241)
382
+ path_label = fg("[#{operation.path.join(", ")}]", 241)
383
+ indent = " "
384
+
385
+ puts "#{indent}#{index_label} #{bold(fg(type_label, type_color))} #{path_label}"
386
+
387
+ print_diff_node(indent, "-", 168, operation.old_node, type) if operation.old_node
388
+ print_diff_node(indent, "+", 114, operation.new_node, type) if operation.new_node
389
+ end
390
+
391
+ puts
392
+ end
393
+
394
+ def print_diff_node(indent, sign, color, node, _type)
395
+ value = extract_node_value(node)
396
+
397
+ if value
398
+ value.split("\n").each do |line|
399
+ puts "#{indent} #{fg(sign, color)} #{fg(line, color)}"
400
+ end
401
+ else
402
+ label = node.type.to_s.sub("AST_", "").sub("_NODE", "")
403
+ location = node.location ? " (#{node.location.start.line}:#{node.location.start.column})" : ""
404
+ puts "#{indent} #{fg(sign, color)} #{fg("#{label}#{location}", color)}"
405
+ end
406
+ end
407
+
408
+ def extract_node_value(node)
409
+ return nil unless node
410
+
411
+ if node.is_a?(Herb::AST::HTMLTextNode)
412
+ return node.content&.to_s
413
+ end
414
+
415
+ if node.is_a?(Herb::AST::ERBContentNode)
416
+ return node.content&.value&.to_s
417
+ end
418
+
419
+ if node.is_a?(Herb::AST::HTMLElementNode)
420
+ name = node.tag_name
421
+ name = name.value if name.respond_to?(:value)
422
+ return "<#{name}>"
423
+ end
424
+
425
+ if node.is_a?(Herb::AST::HTMLAttributeNode)
426
+ parts = []
427
+
428
+ if node.name.respond_to?(:children)
429
+ parts << node.name.children.map { |child| child.respond_to?(:content) ? child.content.to_s : "" }.join
430
+ end
431
+
432
+ if node.value.respond_to?(:children)
433
+ value = node.value.children.map { |child| child.respond_to?(:content) ? child.content.to_s : "" }.join
434
+ parts << "=\"#{value}\""
435
+ end
436
+
437
+ result = parts.join
438
+ return result.empty? ? nil : result
439
+ end
440
+
441
+ nil
442
+ end
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "socket"
5
+ require "json"
6
+
7
+ begin
8
+ require "websocket"
9
+ rescue LoadError
10
+ require "bundler/inline"
11
+
12
+ gemfile do
13
+ source "https://rubygems.org"
14
+ gem "websocket"
15
+ end
16
+
17
+ require "websocket"
18
+ end
19
+
20
+ require_relative "server_entry"
21
+
22
+ module Herb
23
+ module Dev
24
+ class Server
25
+ DEFAULT_PORT = 8592
26
+ HANDSHAKE_TIMEOUT = 5
27
+
28
+ attr_reader :port, :project_path
29
+
30
+ Client = Data.define(:socket, :version, :mutex)
31
+
32
+ def initialize(port: DEFAULT_PORT, project_path: nil)
33
+ @port = port
34
+ @project_path = project_path
35
+ @clients = [] #: Array[Client]
36
+ @mutex = Mutex.new
37
+ @server = nil
38
+ @accept_thread = nil
39
+ @entry = nil
40
+ end
41
+
42
+ def start
43
+ @entry = ServerEntry.new(pid: Process.pid, port: @port, project: @project_path)
44
+ @entry.save
45
+ @server = TCPServer.new("0.0.0.0", @port)
46
+
47
+ @accept_thread = Thread.new do
48
+ loop do
49
+ socket = @server.accept
50
+ Thread.new(socket) { |s| handle_connection(s) }
51
+ rescue IOError
52
+ break
53
+ end
54
+ end
55
+ end
56
+
57
+ def stop
58
+ @mutex.synchronize do
59
+ @clients.each { |client| safely_close(client.socket) }
60
+ @clients.clear
61
+ end
62
+
63
+ safely_close(@server)
64
+
65
+ @accept_thread&.kill
66
+ @entry&.remove
67
+ end
68
+
69
+ def broadcast(message)
70
+ data = message.is_a?(String) ? message : JSON.generate(message)
71
+
72
+ failed_clients = []
73
+ clients_snapshot = @mutex.synchronize { @clients.dup }
74
+
75
+ clients_snapshot.each do |client|
76
+ frame = WebSocket::Frame::Outgoing::Server.new(version: client.version, data: data, type: :text)
77
+
78
+ client.mutex.synchronize do
79
+ client.socket.write(frame.to_s)
80
+ client.socket.flush
81
+ end
82
+ rescue StandardError
83
+ safely_close(client.socket)
84
+
85
+ failed_clients << client
86
+ end
87
+
88
+ return unless failed_clients.any?
89
+
90
+ @mutex.synchronize do
91
+ failed_clients.each { |client| @clients.delete(client) }
92
+ end
93
+ end
94
+
95
+ def client_count
96
+ @mutex.synchronize { @clients.size }
97
+ end
98
+
99
+ def self.port_available?(port)
100
+ server = TCPServer.new("0.0.0.0", port)
101
+ server.close
102
+
103
+ true
104
+ rescue Errno::EADDRINUSE
105
+ false
106
+ end
107
+
108
+ def self.find_available_port(starting_port = DEFAULT_PORT)
109
+ port = starting_port
110
+
111
+ loop do
112
+ return port if port_available?(port)
113
+
114
+ port += 1
115
+ break if port > starting_port + 100
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ private
122
+
123
+ def safely_close(resource)
124
+ resource&.close
125
+ rescue StandardError
126
+ nil
127
+ end
128
+
129
+ def handle_connection(socket)
130
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
131
+
132
+ handshake = WebSocket::Handshake::Server.new
133
+
134
+ until handshake.finished?
135
+ readable = socket.wait_readable(HANDSHAKE_TIMEOUT)
136
+
137
+ unless readable
138
+ socket.close
139
+ return
140
+ end
141
+
142
+ data = socket.read_nonblock(4096, exception: false)
143
+ break if data.nil? || data == :wait_readable
144
+
145
+ data.each_byte { |byte| handshake << byte.chr }
146
+ end
147
+
148
+ unless handshake.valid?
149
+ socket.close
150
+ return
151
+ end
152
+
153
+ socket.write(handshake.to_s)
154
+ socket.flush
155
+
156
+ welcome = WebSocket::Frame::Outgoing::Server.new(
157
+ version: handshake.version,
158
+ data: JSON.generate({ type: "welcome", project: @project_path }),
159
+ type: :text
160
+ )
161
+
162
+ socket.write(welcome.to_s)
163
+ socket.flush
164
+
165
+ client = Client.new(socket: socket, version: handshake.version, mutex: Mutex.new)
166
+ @mutex.synchronize { @clients << client }
167
+
168
+ frame_parser = WebSocket::Frame::Incoming::Server.new(version: handshake.version)
169
+
170
+ loop do
171
+ chunk = socket.readpartial(4096)
172
+
173
+ frame_parser << chunk
174
+
175
+ while (frame = frame_parser.next)
176
+ case frame.type
177
+ when :close
178
+ close_frame = WebSocket::Frame::Outgoing::Server.new(version: handshake.version, data: "", type: :close)
179
+
180
+ begin
181
+ client.mutex.synchronize { socket.write(close_frame.to_s) }
182
+ rescue StandardError
183
+ nil
184
+ end
185
+
186
+ return
187
+ when :ping
188
+ pong = WebSocket::Frame::Outgoing::Server.new(version: handshake.version, data: frame.data, type: :pong)
189
+
190
+ client.mutex.synchronize { socket.write(pong.to_s) }
191
+ when :text
192
+ nil
193
+ end
194
+ end
195
+ end
196
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
197
+ # client disconnected
198
+ rescue StandardError => e
199
+ warn "[herb-dev-server] connection error: #{e.class}: #{e.message}"
200
+ ensure
201
+ @mutex.synchronize { @clients.delete_if { |client| client.socket == socket } }
202
+
203
+ safely_close(socket)
204
+ end
205
+ end
206
+ end
207
+ end