herb 0.10.3-x86-linux-gnu → 0.11.0-x86-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 (788) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +16 -6
  3. data/README.md +36 -119
  4. data/Rakefile +2 -11
  5. data/config/action_view_helpers/actioncable/action_cable_helper/action_cable_meta_tag.yml +12 -8
  6. data/config/action_view_helpers/actionpack/content_security_policy/content_security_policy?.yml +9 -6
  7. data/config/action_view_helpers/actionpack/content_security_policy/content_security_policy_nonce.yml +9 -6
  8. data/config/action_view_helpers/actionpack/polymorphic_routes/polymorphic_path.yml +20 -12
  9. data/config/action_view_helpers/actionpack/polymorphic_routes/polymorphic_url.yml +30 -18
  10. data/config/action_view_helpers/actionpack/request_forgery_protection/form_authenticity_token.yml +15 -9
  11. data/config/action_view_helpers/actionpack/request_forgery_protection/protect_against_forgery?.yml +9 -6
  12. data/config/action_view_helpers/actiontext/content_helper/render_action_text_content.yml +15 -9
  13. data/config/action_view_helpers/actiontext/content_helper/sanitize_action_text_content.yml +15 -9
  14. data/config/action_view_helpers/actiontext/tag_helper/rich_textarea_tag.yml +34 -22
  15. data/config/action_view_helpers/actionview/active_model_helper/error_message.yml +9 -6
  16. data/config/action_view_helpers/actionview/active_model_helper/error_wrapping.yml +9 -6
  17. data/config/action_view_helpers/actionview/active_model_helper/object.yml +9 -6
  18. data/config/action_view_helpers/actionview/asset_tag_helper/apply_stylesheet_media_default.yml +9 -6
  19. data/config/action_view_helpers/actionview/asset_tag_helper/audio_tag.yml +40 -27
  20. data/config/action_view_helpers/actionview/asset_tag_helper/auto_discovery_link_tag.yml +37 -23
  21. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce.yml +9 -6
  22. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce_for_scripts.yml +9 -6
  23. data/config/action_view_helpers/actionview/asset_tag_helper/auto_include_nonce_for_styles.yml +9 -6
  24. data/config/action_view_helpers/actionview/asset_tag_helper/favicon_link_tag.yml +32 -22
  25. data/config/action_view_helpers/actionview/asset_tag_helper/image_decoding.yml +9 -6
  26. data/config/action_view_helpers/actionview/asset_tag_helper/image_loading.yml +9 -6
  27. data/config/action_view_helpers/actionview/asset_tag_helper/image_tag.yml +67 -46
  28. data/config/action_view_helpers/actionview/asset_tag_helper/javascript_include_tag.yml +58 -39
  29. data/config/action_view_helpers/actionview/asset_tag_helper/picture_tag.yml +22 -14
  30. data/config/action_view_helpers/actionview/asset_tag_helper/preload_link_tag.yml +45 -30
  31. data/config/action_view_helpers/actionview/asset_tag_helper/preload_links_header.yml +9 -6
  32. data/config/action_view_helpers/actionview/asset_tag_helper/stylesheet_link_tag.yml +49 -30
  33. data/config/action_view_helpers/actionview/asset_tag_helper/video_tag.yml +53 -35
  34. data/config/action_view_helpers/actionview/asset_url_helper/asset_path.yml +16 -10
  35. data/config/action_view_helpers/actionview/asset_url_helper/asset_url.yml +16 -10
  36. data/config/action_view_helpers/actionview/asset_url_helper/audio_path.yml +16 -10
  37. data/config/action_view_helpers/actionview/asset_url_helper/audio_url.yml +16 -10
  38. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_extname.yml +9 -6
  39. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_host.yml +9 -6
  40. data/config/action_view_helpers/actionview/asset_url_helper/compute_asset_path.yml +9 -6
  41. data/config/action_view_helpers/actionview/asset_url_helper/font_path.yml +16 -10
  42. data/config/action_view_helpers/actionview/asset_url_helper/font_url.yml +16 -10
  43. data/config/action_view_helpers/actionview/asset_url_helper/image_path.yml +16 -10
  44. data/config/action_view_helpers/actionview/asset_url_helper/image_url.yml +16 -10
  45. data/config/action_view_helpers/actionview/asset_url_helper/javascript_path.yml +16 -10
  46. data/config/action_view_helpers/actionview/asset_url_helper/javascript_url.yml +16 -10
  47. data/config/action_view_helpers/actionview/asset_url_helper/public_compute_asset_path.yml +9 -6
  48. data/config/action_view_helpers/actionview/asset_url_helper/stylesheet_path.yml +16 -10
  49. data/config/action_view_helpers/actionview/asset_url_helper/stylesheet_url.yml +16 -10
  50. data/config/action_view_helpers/actionview/asset_url_helper/video_path.yml +16 -10
  51. data/config/action_view_helpers/actionview/asset_url_helper/video_url.yml +16 -10
  52. data/config/action_view_helpers/actionview/atom_feed_helper/atom_feed.yml +52 -27
  53. data/config/action_view_helpers/actionview/cache_helper/cache.yml +25 -15
  54. data/config/action_view_helpers/actionview/cache_helper/cache_fragment_name.yml +9 -6
  55. data/config/action_view_helpers/actionview/cache_helper/cache_if.yml +20 -12
  56. data/config/action_view_helpers/actionview/cache_helper/cache_unless.yml +20 -12
  57. data/config/action_view_helpers/actionview/cache_helper/caching_predicate.yml +9 -6
  58. data/config/action_view_helpers/actionview/cache_helper/digest_path_from_template.yml +9 -6
  59. data/config/action_view_helpers/actionview/cache_helper/fragment_for.yml +9 -6
  60. data/config/action_view_helpers/actionview/cache_helper/fragment_name_with_digest.yml +9 -6
  61. data/config/action_view_helpers/actionview/cache_helper/read_fragment_for.yml +9 -6
  62. data/config/action_view_helpers/actionview/cache_helper/uncacheable.yml +9 -6
  63. data/config/action_view_helpers/actionview/cache_helper/write_fragment_for.yml +9 -6
  64. data/config/action_view_helpers/actionview/capture_helper/capture.yml +9 -6
  65. data/config/action_view_helpers/actionview/capture_helper/content_for.yml +20 -12
  66. data/config/action_view_helpers/actionview/capture_helper/content_for_predicate.yml +15 -9
  67. data/config/action_view_helpers/actionview/capture_helper/provide.yml +15 -9
  68. data/config/action_view_helpers/actionview/capture_helper/with_output_buffer.yml +9 -6
  69. data/config/action_view_helpers/actionview/content_exfiltration_prevention_helper/prepend_content_exfiltration_prevention.yml +9 -6
  70. data/config/action_view_helpers/actionview/content_exfiltration_prevention_helper/prevent_content_exfiltration.yml +9 -6
  71. data/config/action_view_helpers/actionview/controller_helper/action_name.yml +9 -6
  72. data/config/action_view_helpers/actionview/controller_helper/assign_controller.yml +9 -6
  73. data/config/action_view_helpers/actionview/controller_helper/controller.yml +9 -6
  74. data/config/action_view_helpers/actionview/controller_helper/controller_name.yml +9 -6
  75. data/config/action_view_helpers/actionview/controller_helper/controller_path.yml +9 -6
  76. data/config/action_view_helpers/actionview/controller_helper/cookies.yml +9 -6
  77. data/config/action_view_helpers/actionview/controller_helper/flash.yml +9 -6
  78. data/config/action_view_helpers/actionview/controller_helper/headers.yml +9 -6
  79. data/config/action_view_helpers/actionview/controller_helper/logger.yml +9 -6
  80. data/config/action_view_helpers/actionview/controller_helper/params.yml +9 -6
  81. data/config/action_view_helpers/actionview/controller_helper/request.yml +9 -6
  82. data/config/action_view_helpers/actionview/controller_helper/request_forgery_protection_token.yml +9 -6
  83. data/config/action_view_helpers/actionview/controller_helper/response.yml +9 -6
  84. data/config/action_view_helpers/actionview/controller_helper/session.yml +9 -6
  85. data/config/action_view_helpers/actionview/csp_helper/csp_meta_tag.yml +18 -11
  86. data/config/action_view_helpers/actionview/csrf_helper/csrf_meta_tags.yml +16 -10
  87. data/config/action_view_helpers/actionview/date_helper/date_select.yml +85 -51
  88. data/config/action_view_helpers/actionview/date_helper/datetime_select.yml +65 -39
  89. data/config/action_view_helpers/actionview/date_helper/distance_of_time_in_words.yml +30 -18
  90. data/config/action_view_helpers/actionview/date_helper/relative_time_in_words.yml +35 -21
  91. data/config/action_view_helpers/actionview/date_helper/select_date.yml +50 -30
  92. data/config/action_view_helpers/actionview/date_helper/select_datetime.yml +65 -39
  93. data/config/action_view_helpers/actionview/date_helper/select_day.yml +40 -24
  94. data/config/action_view_helpers/actionview/date_helper/select_hour.yml +50 -30
  95. data/config/action_view_helpers/actionview/date_helper/select_minute.yml +40 -24
  96. data/config/action_view_helpers/actionview/date_helper/select_month.yml +60 -36
  97. data/config/action_view_helpers/actionview/date_helper/select_second.yml +35 -21
  98. data/config/action_view_helpers/actionview/date_helper/select_time.yml +55 -33
  99. data/config/action_view_helpers/actionview/date_helper/select_year.yml +45 -27
  100. data/config/action_view_helpers/actionview/date_helper/time_ago_in_words.yml +21 -13
  101. data/config/action_view_helpers/actionview/date_helper/time_select.yml +60 -36
  102. data/config/action_view_helpers/actionview/date_helper/time_tag.yml +32 -20
  103. data/config/action_view_helpers/actionview/debug_helper/debug.yml +15 -9
  104. data/config/action_view_helpers/actionview/form_builder/button.yml +60 -0
  105. data/config/action_view_helpers/actionview/form_builder/submit.yml +48 -0
  106. data/config/action_view_helpers/actionview/form_helper/apply_form_for_options.yml +9 -6
  107. data/config/action_view_helpers/actionview/form_helper/checkbox.yml +41 -25
  108. data/config/action_view_helpers/actionview/form_helper/color_field.yml +25 -15
  109. data/config/action_view_helpers/actionview/form_helper/date_field.yml +35 -21
  110. data/config/action_view_helpers/actionview/form_helper/datetime_field.yml +41 -25
  111. data/config/action_view_helpers/actionview/form_helper/email_field.yml +25 -15
  112. data/config/action_view_helpers/actionview/form_helper/emitted_hidden_id.yml +9 -6
  113. data/config/action_view_helpers/actionview/form_helper/fields.yml +37 -18
  114. data/config/action_view_helpers/actionview/form_helper/fields_for.yml +33 -15
  115. data/config/action_view_helpers/actionview/form_helper/file_field.yml +45 -27
  116. data/config/action_view_helpers/actionview/form_helper/form_for.yml +45 -24
  117. data/config/action_view_helpers/actionview/form_helper/form_with.yml +56 -30
  118. data/config/action_view_helpers/actionview/form_helper/form_with_generates_ids.yml +9 -6
  119. data/config/action_view_helpers/actionview/form_helper/form_with_generates_remote_forms.yml +9 -6
  120. data/config/action_view_helpers/actionview/form_helper/hidden_field.yml +25 -15
  121. data/config/action_view_helpers/actionview/form_helper/label.yml +52 -27
  122. data/config/action_view_helpers/actionview/form_helper/month_field.yml +25 -15
  123. data/config/action_view_helpers/actionview/form_helper/multipart.yml +9 -6
  124. data/config/action_view_helpers/actionview/form_helper/multiple_file_field_include_hidden.yml +9 -6
  125. data/config/action_view_helpers/actionview/form_helper/number_field.yml +45 -27
  126. data/config/action_view_helpers/actionview/form_helper/password_field.yml +25 -15
  127. data/config/action_view_helpers/actionview/form_helper/radio_button.yml +30 -18
  128. data/config/action_view_helpers/actionview/form_helper/range_field.yml +45 -27
  129. data/config/action_view_helpers/actionview/form_helper/search_field.yml +25 -15
  130. data/config/action_view_helpers/actionview/form_helper/telephone_field.yml +26 -16
  131. data/config/action_view_helpers/actionview/form_helper/text_field.yml +25 -15
  132. data/config/action_view_helpers/actionview/form_helper/textarea.yml +31 -19
  133. data/config/action_view_helpers/actionview/form_helper/time_field.yml +40 -24
  134. data/config/action_view_helpers/actionview/form_helper/url_field.yml +25 -15
  135. data/config/action_view_helpers/actionview/form_helper/week_field.yml +25 -15
  136. data/config/action_view_helpers/actionview/form_options_helper/collection_checkboxes.yml +58 -31
  137. data/config/action_view_helpers/actionview/form_options_helper/collection_radio_buttons.yml +57 -30
  138. data/config/action_view_helpers/actionview/form_options_helper/collection_select.yml +65 -39
  139. data/config/action_view_helpers/actionview/form_options_helper/grouped_collection_select.yml +65 -39
  140. data/config/action_view_helpers/actionview/form_options_helper/grouped_options_for_select.yml +35 -21
  141. data/config/action_view_helpers/actionview/form_options_helper/option_groups_from_collection_for_select.yml +40 -24
  142. data/config/action_view_helpers/actionview/form_options_helper/options_for_select.yml +20 -12
  143. data/config/action_view_helpers/actionview/form_options_helper/options_from_collection_for_select.yml +30 -18
  144. data/config/action_view_helpers/actionview/form_options_helper/select.yml +60 -36
  145. data/config/action_view_helpers/actionview/form_options_helper/time_zone_options_for_select.yml +25 -15
  146. data/config/action_view_helpers/actionview/form_options_helper/time_zone_select.yml +50 -30
  147. data/config/action_view_helpers/actionview/form_options_helper/weekday_options_for_select.yml +30 -18
  148. data/config/action_view_helpers/actionview/form_options_helper/weekday_select.yml +30 -18
  149. data/config/action_view_helpers/actionview/form_tag_helper/button_tag.yml +39 -21
  150. data/config/action_view_helpers/actionview/form_tag_helper/checkbox_tag.yml +33 -21
  151. data/config/action_view_helpers/actionview/form_tag_helper/color_field_tag.yml +26 -17
  152. data/config/action_view_helpers/actionview/form_tag_helper/date_field_tag.yml +27 -17
  153. data/config/action_view_helpers/actionview/form_tag_helper/datetime_field_tag.yml +28 -18
  154. data/config/action_view_helpers/actionview/form_tag_helper/default_enforce_utf8.yml +9 -6
  155. data/config/action_view_helpers/actionview/form_tag_helper/email_field_tag.yml +27 -17
  156. data/config/action_view_helpers/actionview/form_tag_helper/embed_authenticity_token_in_remote_forms.yml +9 -6
  157. data/config/action_view_helpers/actionview/form_tag_helper/field_id.yml +30 -18
  158. data/config/action_view_helpers/actionview/form_tag_helper/field_name.yml +30 -18
  159. data/config/action_view_helpers/actionview/form_tag_helper/field_set_tag.yml +23 -15
  160. data/config/action_view_helpers/actionview/form_tag_helper/file_field_tag.yml +35 -22
  161. data/config/action_view_helpers/actionview/form_tag_helper/form_tag.yml +38 -24
  162. data/config/action_view_helpers/actionview/form_tag_helper/hidden_field_tag.yml +27 -17
  163. data/config/action_view_helpers/actionview/form_tag_helper/image_submit_tag.yml +31 -21
  164. data/config/action_view_helpers/actionview/form_tag_helper/label_tag.yml +33 -17
  165. data/config/action_view_helpers/actionview/form_tag_helper/month_field_tag.yml +27 -17
  166. data/config/action_view_helpers/actionview/form_tag_helper/number_field_tag.yml +47 -29
  167. data/config/action_view_helpers/actionview/form_tag_helper/password_field_tag.yml +27 -17
  168. data/config/action_view_helpers/actionview/form_tag_helper/radio_button_tag.yml +32 -20
  169. data/config/action_view_helpers/actionview/form_tag_helper/range_field_tag.yml +47 -29
  170. data/config/action_view_helpers/actionview/form_tag_helper/search_field_tag.yml +27 -17
  171. data/config/action_view_helpers/actionview/form_tag_helper/select_tag.yml +47 -29
  172. data/config/action_view_helpers/actionview/form_tag_helper/submit_tag.yml +33 -21
  173. data/config/action_view_helpers/actionview/form_tag_helper/telephone_field_tag.yml +28 -18
  174. data/config/action_view_helpers/actionview/form_tag_helper/text_field_tag.yml +27 -17
  175. data/config/action_view_helpers/actionview/form_tag_helper/textarea_tag.yml +31 -20
  176. data/config/action_view_helpers/actionview/form_tag_helper/time_field_tag.yml +27 -17
  177. data/config/action_view_helpers/actionview/form_tag_helper/url_field_tag.yml +27 -17
  178. data/config/action_view_helpers/actionview/form_tag_helper/utf8_enforcer_tag.yml +9 -6
  179. data/config/action_view_helpers/actionview/form_tag_helper/week_field_tag.yml +28 -18
  180. data/config/action_view_helpers/actionview/javascript_helper/escape_javascript.yml +16 -10
  181. data/config/action_view_helpers/actionview/javascript_helper/javascript_cdata_section.yml +9 -6
  182. data/config/action_view_helpers/actionview/javascript_helper/javascript_tag.yml +41 -27
  183. data/config/action_view_helpers/actionview/number_helper/number_to_currency.yml +29 -18
  184. data/config/action_view_helpers/actionview/number_helper/number_to_human.yml +25 -15
  185. data/config/action_view_helpers/actionview/number_helper/number_to_human_size.yml +20 -12
  186. data/config/action_view_helpers/actionview/number_helper/number_to_percentage.yml +20 -12
  187. data/config/action_view_helpers/actionview/number_helper/number_to_phone.yml +33 -20
  188. data/config/action_view_helpers/actionview/number_helper/number_with_delimiter.yml +21 -13
  189. data/config/action_view_helpers/actionview/number_helper/number_with_precision.yml +25 -15
  190. data/config/action_view_helpers/actionview/output_safety_helper/raw.yml +15 -9
  191. data/config/action_view_helpers/actionview/output_safety_helper/safe_join.yml +20 -12
  192. data/config/action_view_helpers/actionview/output_safety_helper/to_sentence.yml +24 -15
  193. data/config/action_view_helpers/actionview/record_identifier/dom_class.yml +20 -12
  194. data/config/action_view_helpers/actionview/record_identifier/dom_id.yml +20 -12
  195. data/config/action_view_helpers/actionview/record_identifier/dom_target.yml +15 -9
  196. data/config/action_view_helpers/actionview/record_identifier/model_name_from_record_or_class.yml +9 -6
  197. data/config/action_view_helpers/actionview/rendering_helper/render.yml +108 -22
  198. data/config/action_view_helpers/actionview/sanitize_helper/sanitize.yml +25 -15
  199. data/config/action_view_helpers/actionview/sanitize_helper/sanitize_css.yml +15 -9
  200. data/config/action_view_helpers/actionview/sanitize_helper/sanitizer_vendor.yml +9 -6
  201. data/config/action_view_helpers/actionview/sanitize_helper/strip_links.yml +15 -9
  202. data/config/action_view_helpers/actionview/sanitize_helper/strip_tags.yml +15 -9
  203. data/config/action_view_helpers/actionview/tag_helper/cdata_section.yml +15 -9
  204. data/config/action_view_helpers/actionview/tag_helper/content_tag.yml +59 -38
  205. data/config/action_view_helpers/actionview/tag_helper/escape_once.yml +15 -9
  206. data/config/action_view_helpers/actionview/tag_helper/tag.yml +54 -31
  207. data/config/action_view_helpers/actionview/tag_helper/token_list.yml +16 -10
  208. data/config/action_view_helpers/actionview/text_helper/concat.yml +15 -9
  209. data/config/action_view_helpers/actionview/text_helper/current_cycle.yml +15 -9
  210. data/config/action_view_helpers/actionview/text_helper/cycle.yml +23 -14
  211. data/config/action_view_helpers/actionview/text_helper/excerpt.yml +33 -20
  212. data/config/action_view_helpers/actionview/text_helper/highlight.yml +37 -18
  213. data/config/action_view_helpers/actionview/text_helper/pluralize.yml +30 -18
  214. data/config/action_view_helpers/actionview/text_helper/reset_cycle.yml +15 -9
  215. data/config/action_view_helpers/actionview/text_helper/safe_concat.yml +9 -6
  216. data/config/action_view_helpers/actionview/text_helper/simple_format.yml +33 -20
  217. data/config/action_view_helpers/actionview/text_helper/truncate.yml +38 -23
  218. data/config/action_view_helpers/actionview/text_helper/word_wrap.yml +25 -15
  219. data/config/action_view_helpers/actionview/translation_helper/debug_missing_translation.yml +9 -6
  220. data/config/action_view_helpers/actionview/translation_helper/localize.yml +26 -16
  221. data/config/action_view_helpers/actionview/translation_helper/translate.yml +31 -19
  222. data/config/action_view_helpers/actionview/url_helper/button_to.yml +57 -33
  223. data/config/action_view_helpers/actionview/url_helper/button_to_generates_button_tag.yml +9 -6
  224. data/config/action_view_helpers/actionview/url_helper/current_page?.yml +25 -15
  225. data/config/action_view_helpers/actionview/url_helper/link_to.yml +76 -53
  226. data/config/action_view_helpers/actionview/url_helper/link_to_if.yml +54 -20
  227. data/config/action_view_helpers/actionview/url_helper/link_to_unless.yml +54 -20
  228. data/config/action_view_helpers/actionview/url_helper/link_to_unless_current.yml +49 -17
  229. data/config/action_view_helpers/actionview/url_helper/mail_to.yml +60 -34
  230. data/config/action_view_helpers/actionview/url_helper/phone_to.yml +40 -22
  231. data/config/action_view_helpers/actionview/url_helper/sms_to.yml +45 -25
  232. data/config/action_view_helpers/actionview/url_helper/url_for.yml +15 -9
  233. data/config/action_view_helpers/importmap-rails/importmap_tags_helper/javascript_import_module_tag.yml +32 -0
  234. data/config/action_view_helpers/importmap-rails/importmap_tags_helper/javascript_importmap_module_preload_tags.yml +39 -0
  235. data/config/action_view_helpers/importmap-rails/importmap_tags_helper/javascript_importmap_tags.yml +35 -0
  236. data/config/action_view_helpers/importmap-rails/importmap_tags_helper/javascript_inline_importmap_tag.yml +32 -0
  237. data/config/action_view_helpers/importmap-rails/importmap_tags_helper/javascript_module_preload_tag.yml +35 -0
  238. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_cache.yml +9 -6
  239. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_cache_tag.yml +12 -8
  240. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_preview.yml +9 -6
  241. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_exempts_page_from_preview_tag.yml +12 -8
  242. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_page_requires_reload.yml +9 -6
  243. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_page_requires_reload_tag.yml +12 -8
  244. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refresh_method_tag.yml +17 -11
  245. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refresh_scroll_tag.yml +17 -11
  246. data/config/action_view_helpers/turbo-rails/drive_helper/turbo_refreshes_with.yml +20 -12
  247. data/config/action_view_helpers/turbo-rails/frames_helper/turbo_frame_tag.yml +54 -33
  248. data/config/action_view_helpers/turbo-rails/includes_helper/turbo_include_tags.yml +9 -6
  249. data/config/action_view_helpers/turbo-rails/streams_action_helper/turbo_stream_action_tag.yml +32 -20
  250. data/config/action_view_helpers/turbo-rails/streams_action_helper/turbo_stream_refresh_tag.yml +18 -11
  251. data/config/action_view_helpers/turbo-rails/streams_helper/turbo_stream.yml +9 -6
  252. data/config/action_view_helpers/turbo-rails/streams_helper/turbo_stream_from.yml +28 -18
  253. data/config/html_elements.yml +597 -0
  254. data/config/options.yml +1 -0
  255. data/config/slots/components.yml +52 -0
  256. data/config/state/kind.yml +61 -0
  257. data/config/state/operators.yml +27 -0
  258. data/config/state/predicates.yml +39 -0
  259. data/config/state/transforms.yml +18 -0
  260. data/config/view_context_methods/actionview/base/annotate_rendered_view_with_filenames.yml +4 -4
  261. data/config/view_context_methods/actionview/base/assign.yml +4 -4
  262. data/config/view_context_methods/actionview/base/assigns.yml +4 -4
  263. data/config/view_context_methods/actionview/base/automatically_disable_submit_tag.yml +4 -4
  264. data/config/view_context_methods/actionview/base/compiled_method_container.yml +4 -4
  265. data/config/view_context_methods/actionview/base/config.yml +4 -4
  266. data/config/view_context_methods/actionview/base/default_form_builder.yml +4 -4
  267. data/config/view_context_methods/actionview/base/default_formats.yml +4 -4
  268. data/config/view_context_methods/actionview/base/field_error_proc.yml +4 -4
  269. data/config/view_context_methods/actionview/base/formats.yml +4 -4
  270. data/config/view_context_methods/actionview/base/in_rendering_context.yml +4 -4
  271. data/config/view_context_methods/actionview/base/locale.yml +4 -4
  272. data/config/view_context_methods/actionview/base/lookup_context.yml +4 -4
  273. data/config/view_context_methods/actionview/base/output_buffer.yml +4 -4
  274. data/config/view_context_methods/actionview/base/prefix_partial_path_with_controller_namespace.yml +4 -4
  275. data/config/view_context_methods/actionview/base/remove_hidden_field_autocomplete.yml +4 -4
  276. data/config/view_context_methods/actionview/base/streaming_completion_on_exception.yml +4 -4
  277. data/config/view_context_methods/actionview/base/view_flow.yml +4 -4
  278. data/config/view_context_methods/actionview/base/view_paths.yml +4 -4
  279. data/config/view_context_methods/actionview/base/view_renderer.yml +4 -4
  280. data/config/view_context_methods/actionview/template/local_assigns.yml +4 -4
  281. data/config/view_context_methods/activemodel/conversion/convert_to_model.yml +4 -4
  282. data/config/view_context_methods/activesupport/benchmarkable/benchmark.yml +4 -4
  283. data/config.yml +1240 -790
  284. data/exe/herb +2 -0
  285. data/ext/herb/error_helpers.c +355 -17
  286. data/ext/herb/error_helpers.h +1 -1
  287. data/ext/herb/extconf.rb +4 -2
  288. data/ext/herb/extension.c +137 -89
  289. data/ext/herb/extension_helpers.c +81 -42
  290. data/ext/herb/extension_helpers.h +4 -1
  291. data/ext/herb/nodes.c +826 -358
  292. data/ext/herb/nodes.h +3 -2
  293. data/ext/herb/parser_options_helpers.c +151 -0
  294. data/ext/herb/parser_options_helpers.h +14 -0
  295. data/herb.gemspec +2 -2
  296. data/lib/herb/3.2/herb.so +0 -0
  297. data/lib/herb/3.3/herb.so +0 -0
  298. data/lib/herb/3.4/herb.so +0 -0
  299. data/lib/herb/4.0/herb.so +0 -0
  300. data/lib/herb/action_view/helper_registry.rb +1358 -62
  301. data/lib/herb/analysis/partial_declaration.rb +140 -0
  302. data/lib/herb/analysis/partial_index.rb +189 -0
  303. data/lib/herb/analysis/partial_resolution.rb +161 -0
  304. data/lib/herb/analysis/partial_resolver.rb +144 -0
  305. data/lib/herb/analysis/project_index.rb +152 -0
  306. data/lib/herb/{action_view → analysis}/render_analyzer.rb +287 -77
  307. data/lib/herb/analysis/render_graph/ancestor_chain.rb +27 -0
  308. data/lib/herb/analysis/render_graph/builder.rb +376 -0
  309. data/lib/herb/analysis/render_graph/call_frame.rb +9 -0
  310. data/lib/herb/analysis/render_graph/call_site_location.rb +23 -0
  311. data/lib/herb/analysis/render_graph/inferred_signature.rb +21 -0
  312. data/lib/herb/analysis/render_graph/partial_call_site.rb +40 -0
  313. data/lib/herb/analysis/render_graph/partial_context.rb +46 -0
  314. data/lib/herb/analysis/render_graph/template_roots.rb +31 -0
  315. data/lib/herb/analysis/render_graph.rb +285 -0
  316. data/lib/herb/analysis/ruby_locals_index/local.rb +25 -0
  317. data/lib/herb/analysis/ruby_locals_index/named_reference.rb +22 -0
  318. data/lib/herb/analysis/ruby_locals_index/offset_table.rb +44 -0
  319. data/lib/herb/analysis/ruby_locals_index/reference_collector.rb +80 -0
  320. data/lib/herb/analysis/ruby_locals_index/state_locals.rb +49 -0
  321. data/lib/herb/analysis/ruby_locals_index.rb +165 -0
  322. data/lib/herb/analysis/ruby_reference.rb +67 -0
  323. data/lib/herb/analysis/template_dependencies/dependency_collector.rb +179 -0
  324. data/lib/herb/analysis/template_dependencies/node_dependency_collector.rb +309 -0
  325. data/lib/herb/analysis/template_dependencies.rb +562 -0
  326. data/lib/herb/ast/erb_render_node.rb +26 -94
  327. data/lib/herb/ast/helpers.rb +90 -10
  328. data/lib/herb/ast/herb_directive_node.rb +18 -0
  329. data/lib/herb/ast/node.rb +32 -2
  330. data/lib/herb/ast/nodes.rb +1342 -105
  331. data/lib/herb/backend.rb +73 -0
  332. data/lib/herb/bootstrap.rb +3 -7
  333. data/lib/herb/cli.rb +550 -32
  334. data/lib/herb/colors.rb +14 -0
  335. data/lib/herb/configuration.rb +71 -35
  336. data/lib/herb/defaults.yml +2 -1
  337. data/lib/herb/dev/assets.rb +57 -0
  338. data/lib/herb/dev/classifier.rb +84 -0
  339. data/lib/herb/dev/embedded.rb +27 -0
  340. data/lib/herb/dev/pipeline.rb +305 -0
  341. data/lib/herb/dev/protocol.rb +221 -0
  342. data/lib/herb/dev/runner.rb +175 -175
  343. data/lib/herb/dev/server.rb +89 -26
  344. data/lib/herb/dev/server_entry.rb +41 -6
  345. data/lib/herb/dev/watcher.rb +181 -0
  346. data/lib/herb/dev.rb +163 -0
  347. data/lib/herb/diagnostic/formatter.rb +88 -0
  348. data/lib/herb/diagnostic.rb +172 -0
  349. data/lib/herb/diff/operation.rb +36 -0
  350. data/lib/herb/diff/result.rb +62 -0
  351. data/lib/herb/diff.rb +10 -0
  352. data/lib/herb/engine/collector.rb +62 -0
  353. data/lib/herb/engine/compiler.rb +346 -108
  354. data/lib/herb/engine/component_tags/constant_resolver.rb +28 -0
  355. data/lib/herb/engine/component_tags/partial_resolver.rb +54 -0
  356. data/lib/herb/engine/component_tags/resolver.rb +32 -0
  357. data/lib/herb/engine/component_tags/visitor.rb +231 -0
  358. data/lib/herb/engine/css_inliner/inliner.rb +101 -0
  359. data/lib/herb/engine/css_inliner/visitor.rb +174 -0
  360. data/lib/herb/engine/errors.rb +122 -0
  361. data/lib/herb/engine/helpers.rb +82 -0
  362. data/lib/herb/engine/inline_render/inliner.rb +122 -0
  363. data/lib/herb/engine/inline_render/visitor.rb +267 -0
  364. data/lib/herb/engine/runtime/entry.rb +62 -0
  365. data/lib/herb/engine/runtime/error_page.rb +362 -0
  366. data/lib/herb/engine/runtime/html_safe_assertions.rb +217 -0
  367. data/lib/herb/engine/runtime/journal/summary.rb +227 -0
  368. data/lib/herb/engine/runtime/journal.rb +379 -0
  369. data/lib/herb/engine/runtime/middleware.rb +212 -0
  370. data/lib/herb/engine/runtime/observations.rb +73 -0
  371. data/lib/herb/engine/runtime/report.rb +204 -0
  372. data/lib/herb/engine/runtime/session.rb +437 -0
  373. data/lib/herb/engine/runtime.rb +21 -0
  374. data/lib/herb/engine/scoped_style/channel.rb +66 -0
  375. data/lib/herb/engine/scoped_style/collector.rb +74 -0
  376. data/lib/herb/engine/scoped_style/visitor.rb +533 -0
  377. data/lib/herb/engine/slots/annotation.rb +98 -0
  378. data/lib/herb/engine/slots/bindings.rb +61 -0
  379. data/lib/herb/engine/slots/callee.rb +118 -0
  380. data/lib/herb/engine/slots/component_definitions.rb +67 -0
  381. data/lib/herb/engine/slots/components/async.rb +24 -0
  382. data/lib/herb/engine/slots/components/base.rb +132 -0
  383. data/lib/herb/engine/slots/components/deferred.rb +81 -0
  384. data/lib/herb/engine/slots/components/fallback.rb +27 -0
  385. data/lib/herb/engine/slots/components/fragment.rb +99 -0
  386. data/lib/herb/engine/slots/components/lazy.rb +24 -0
  387. data/lib/herb/engine/slots/components.rb +52 -0
  388. data/lib/herb/engine/slots/dependencies/channel.rb +72 -0
  389. data/lib/herb/engine/slots/dependencies.rb +269 -0
  390. data/lib/herb/engine/slots/dynamics_compiler.rb +854 -0
  391. data/lib/herb/engine/slots/identifier.rb +35 -0
  392. data/lib/herb/engine/slots/manifest/channel.rb +74 -0
  393. data/lib/herb/engine/slots/manifest/collector.rb +75 -0
  394. data/lib/herb/engine/slots/markers.rb +128 -0
  395. data/lib/herb/engine/slots/render_bindings.rb +141 -0
  396. data/lib/herb/engine/slots/schema_compiler.rb +94 -0
  397. data/lib/herb/engine/slots/state_anchor.rb +94 -0
  398. data/lib/herb/engine/slots/state_compiler.rb +1326 -0
  399. data/lib/herb/engine/slots/state_directives.rb +854 -0
  400. data/lib/herb/engine/slots/state_kinds.rb +54 -0
  401. data/lib/herb/engine/slots/state_operators.rb +36 -0
  402. data/lib/herb/engine/slots/state_overrides.rb +79 -0
  403. data/lib/herb/engine/slots/state_predicates.rb +65 -0
  404. data/lib/herb/engine/slots/state_transforms.rb +42 -0
  405. data/lib/herb/engine/slots/statics.rb +185 -0
  406. data/lib/herb/engine/slots/subtree.rb +82 -0
  407. data/lib/herb/engine/slots/subtree_compiler.rb +255 -0
  408. data/lib/herb/engine/slots/types.rb +42 -0
  409. data/lib/herb/engine/slots/visitor.rb +2256 -0
  410. data/lib/herb/engine/validators/accessibility_validator.rb +3 -3
  411. data/lib/herb/engine/validators/base.rb +36 -0
  412. data/lib/herb/engine/validators/generator_template_validator.rb +39 -0
  413. data/lib/herb/engine/validators/nesting_validator.rb +3 -3
  414. data/lib/herb/engine/validators/render_validator.rb +13 -33
  415. data/lib/herb/engine/validators/security_validator.rb +3 -17
  416. data/lib/herb/engine/validators.rb +43 -0
  417. data/lib/herb/engine/visitors/auto_close_omitted_tags_visitor.rb +48 -0
  418. data/lib/herb/engine/visitors/content_for_visitor.rb +127 -0
  419. data/lib/herb/engine/{debug_visitor.rb → visitors/debug_visitor.rb} +121 -93
  420. data/lib/herb/engine/visitors/html_safe_assertions_visitor.rb +281 -0
  421. data/lib/herb/engine/visitors/instrumentation_visitor.rb +290 -0
  422. data/lib/herb/engine/visitors/optimize_visitor.rb +728 -0
  423. data/lib/herb/engine/visitors/remove_comments_visitor.rb +106 -0
  424. data/lib/herb/engine/visitors/source_attribution_visitor.rb +177 -0
  425. data/lib/herb/engine.rb +218 -257
  426. data/lib/herb/ensure_installed.rb +43 -0
  427. data/lib/herb/errors.rb +646 -157
  428. data/lib/herb/fingerprint.rb +82 -0
  429. data/lib/herb/html/elements.rb +18 -0
  430. data/lib/herb/html/util.rb +21 -2
  431. data/lib/herb/lex_result.rb +0 -10
  432. data/lib/herb/locate.rb +163 -0
  433. data/lib/herb/location.rb +20 -0
  434. data/lib/herb/parse_result.rb +17 -11
  435. data/lib/herb/parser_options.rb +65 -46
  436. data/lib/herb/position.rb +19 -0
  437. data/lib/herb/prism_inspect.rb +1 -4
  438. data/lib/herb/project.rb +31 -18
  439. data/lib/herb/range.rb +5 -0
  440. data/lib/herb/result.rb +2 -2
  441. data/lib/herb/ruby_program.rb +110 -0
  442. data/lib/herb/source_path.rb +232 -0
  443. data/lib/herb/token.rb +14 -3
  444. data/lib/herb/version.rb +1 -1
  445. data/lib/herb/visitor/context/origin.rb +52 -0
  446. data/lib/herb/visitor/context/replacements.rb +59 -0
  447. data/lib/herb/visitor/context.rb +169 -0
  448. data/lib/herb/visitor/context_aware.rb +45 -0
  449. data/lib/herb/visitor/diagnostics.rb +148 -0
  450. data/lib/herb/visitor/experimental.rb +33 -0
  451. data/lib/herb/visitor/parser_option_requirements.rb +151 -0
  452. data/lib/herb/visitor/stack.rb +192 -0
  453. data/lib/herb/visitor.rb +47 -1
  454. data/lib/herb/warnings.rb +25 -0
  455. data/lib/herb.rb +74 -63
  456. data/sig/herb/action_view/helper_registry.rbs +82 -10
  457. data/sig/herb/analysis/partial_declaration.rbs +80 -0
  458. data/sig/herb/analysis/partial_index.rbs +66 -0
  459. data/sig/herb/analysis/partial_resolution.rbs +57 -0
  460. data/sig/herb/analysis/partial_resolver.rbs +76 -0
  461. data/sig/herb/analysis/project_index.rbs +57 -0
  462. data/sig/herb/{action_view → analysis}/render_analyzer.rbs +25 -4
  463. data/sig/herb/analysis/render_graph/ancestor_chain.rbs +22 -0
  464. data/sig/herb/analysis/render_graph/builder.rbs +180 -0
  465. data/sig/herb/analysis/render_graph/call_frame.rbs +26 -0
  466. data/sig/herb/analysis/render_graph/call_site_location.rbs +28 -0
  467. data/sig/herb/analysis/render_graph/inferred_signature.rbs +27 -0
  468. data/sig/herb/analysis/render_graph/partial_call_site.rbs +36 -0
  469. data/sig/herb/analysis/render_graph/partial_context.rbs +31 -0
  470. data/sig/herb/analysis/render_graph/template_roots.rbs +32 -0
  471. data/sig/herb/analysis/render_graph.rbs +83 -0
  472. data/sig/herb/analysis/ruby_locals_index/local.rbs +21 -0
  473. data/sig/herb/analysis/ruby_locals_index/named_reference.rbs +20 -0
  474. data/sig/herb/analysis/ruby_locals_index/offset_table.rbs +26 -0
  475. data/sig/herb/analysis/ruby_locals_index/reference_collector.rbs +40 -0
  476. data/sig/herb/analysis/ruby_locals_index/state_locals.rbs +19 -0
  477. data/sig/herb/analysis/ruby_locals_index.rbs +62 -0
  478. data/sig/herb/analysis/ruby_reference.rbs +28 -0
  479. data/sig/herb/analysis/template_dependencies/dependency_collector.rbs +52 -0
  480. data/sig/herb/analysis/template_dependencies/node_dependency_collector.rbs +81 -0
  481. data/sig/herb/analysis/template_dependencies.rbs +125 -0
  482. data/sig/herb/ast/erb_render_node.rbs +10 -2
  483. data/sig/herb/ast/helpers.rbs +31 -1
  484. data/sig/herb/ast/herb_directive_node.rbs +13 -0
  485. data/sig/herb/ast/node.rbs +9 -0
  486. data/sig/herb/ast/nodes.rbs +662 -89
  487. data/sig/herb/backend.rbs +52 -0
  488. data/sig/herb/colors.rbs +3 -0
  489. data/sig/herb/configuration.rbs +27 -3
  490. data/sig/herb/dev/assets.rbs +27 -0
  491. data/sig/herb/dev/classifier.rbs +57 -0
  492. data/sig/herb/dev/embedded.rbs +21 -0
  493. data/sig/herb/dev/pipeline.rbs +118 -0
  494. data/sig/herb/dev/protocol.rbs +78 -0
  495. data/sig/herb/dev/runner.rbs +61 -23
  496. data/sig/herb/dev/server.rbs +49 -22
  497. data/sig/herb/dev/server_entry.rbs +46 -21
  498. data/sig/herb/dev/watcher.rbs +84 -0
  499. data/sig/herb/dev.rbs +58 -0
  500. data/sig/herb/diagnostic/formatter.rbs +28 -0
  501. data/sig/herb/diagnostic.rbs +88 -0
  502. data/sig/herb/diff/operation.rbs +36 -0
  503. data/sig/herb/diff/result.rbs +36 -0
  504. data/sig/herb/diff.rbs +6 -0
  505. data/sig/herb/engine/collector.rbs +48 -0
  506. data/sig/herb/engine/compiler.rbs +62 -23
  507. data/sig/herb/engine/component_tags/constant_resolver.rbs +19 -0
  508. data/sig/herb/engine/component_tags/partial_resolver.rbs +30 -0
  509. data/sig/herb/engine/component_tags/resolver.rbs +25 -0
  510. data/sig/herb/engine/component_tags/visitor.rbs +69 -0
  511. data/sig/herb/engine/css_inliner/inliner.rbs +65 -0
  512. data/sig/herb/engine/css_inliner/visitor.rbs +87 -0
  513. data/sig/herb/engine/errors.rbs +68 -0
  514. data/sig/herb/engine/helpers.rbs +28 -0
  515. data/sig/herb/engine/inline_render/inliner.rbs +39 -0
  516. data/sig/herb/engine/inline_render/visitor.rbs +104 -0
  517. data/sig/herb/engine/runtime/entry.rbs +43 -0
  518. data/sig/herb/engine/runtime/error_page.rbs +115 -0
  519. data/sig/herb/engine/runtime/html_safe_assertions.rbs +99 -0
  520. data/sig/herb/engine/runtime/journal/summary.rbs +172 -0
  521. data/sig/herb/engine/runtime/journal.rbs +126 -0
  522. data/sig/herb/engine/runtime/middleware.rbs +96 -0
  523. data/sig/herb/engine/runtime/observations.rbs +38 -0
  524. data/sig/herb/engine/runtime/report.rbs +97 -0
  525. data/sig/herb/engine/runtime/session.rbs +233 -0
  526. data/sig/herb/engine/runtime.rbs +14 -0
  527. data/sig/herb/engine/scoped_style/channel.rbs +45 -0
  528. data/sig/herb/engine/scoped_style/collector.rbs +45 -0
  529. data/sig/herb/engine/scoped_style/visitor.rbs +220 -0
  530. data/sig/herb/engine/slots/annotation.rbs +72 -0
  531. data/sig/herb/engine/slots/bindings.rbs +34 -0
  532. data/sig/herb/engine/slots/callee.rbs +62 -0
  533. data/sig/herb/engine/slots/component_definitions.rbs +19 -0
  534. data/sig/herb/engine/slots/components/async.rbs +20 -0
  535. data/sig/herb/engine/slots/components/base.rbs +60 -0
  536. data/sig/herb/engine/slots/components/deferred.rbs +29 -0
  537. data/sig/herb/engine/slots/components/fallback.rbs +21 -0
  538. data/sig/herb/engine/slots/components/fragment.rbs +34 -0
  539. data/sig/herb/engine/slots/components/lazy.rbs +20 -0
  540. data/sig/herb/engine/slots/components.rbs +30 -0
  541. data/sig/herb/engine/slots/dependencies/channel.rbs +51 -0
  542. data/sig/herb/engine/slots/dependencies.rbs +94 -0
  543. data/sig/herb/engine/slots/dynamics_compiler.rbs +349 -0
  544. data/sig/herb/engine/slots/identifier.rbs +19 -0
  545. data/sig/herb/engine/slots/manifest/channel.rbs +49 -0
  546. data/sig/herb/engine/slots/manifest/collector.rbs +45 -0
  547. data/sig/herb/engine/slots/markers.rbs +85 -0
  548. data/sig/herb/engine/slots/render_bindings.rbs +69 -0
  549. data/sig/herb/engine/slots/schema_compiler.rbs +62 -0
  550. data/sig/herb/engine/slots/state_anchor.rbs +41 -0
  551. data/sig/herb/engine/slots/state_compiler.rbs +258 -0
  552. data/sig/herb/engine/slots/state_directives.rbs +278 -0
  553. data/sig/herb/engine/slots/state_kinds.rbs +24 -0
  554. data/sig/herb/engine/slots/state_operators.rbs +19 -0
  555. data/sig/herb/engine/slots/state_overrides.rbs +33 -0
  556. data/sig/herb/engine/slots/state_predicates.rbs +16 -0
  557. data/sig/herb/engine/slots/state_transforms.rbs +15 -0
  558. data/sig/herb/engine/slots/statics.rbs +67 -0
  559. data/sig/herb/engine/slots/subtree.rbs +49 -0
  560. data/sig/herb/engine/slots/subtree_compiler.rbs +130 -0
  561. data/sig/herb/engine/slots/types.rbs +46 -0
  562. data/sig/herb/engine/slots/visitor.rbs +599 -0
  563. data/sig/herb/engine/validators/accessibility_validator.rbs +1 -1
  564. data/sig/herb/engine/validators/base.rbs +28 -0
  565. data/sig/herb/engine/validators/generator_template_validator.rbs +19 -0
  566. data/sig/herb/engine/validators/nesting_validator.rbs +1 -1
  567. data/sig/herb/engine/validators/render_validator.rbs +2 -6
  568. data/sig/herb/engine/validators/security_validator.rbs +1 -3
  569. data/sig/herb/engine/validators.rbs +21 -0
  570. data/sig/herb/engine/visitors/auto_close_omitted_tags_visitor.rbs +26 -0
  571. data/sig/herb/engine/visitors/content_for_visitor.rbs +52 -0
  572. data/sig/herb/engine/{debug_visitor.rbs → visitors/debug_visitor.rbs} +26 -15
  573. data/sig/herb/engine/visitors/html_safe_assertions_visitor.rbs +100 -0
  574. data/sig/herb/engine/visitors/instrumentation_visitor.rbs +111 -0
  575. data/sig/herb/engine/visitors/optimize_visitor.rbs +246 -0
  576. data/sig/herb/engine/visitors/remove_comments_visitor.rbs +66 -0
  577. data/sig/herb/engine/visitors/source_attribution_visitor.rbs +90 -0
  578. data/sig/herb/engine.rbs +47 -41
  579. data/sig/herb/ensure_installed.rbs +9 -0
  580. data/sig/herb/errors.rbs +293 -0
  581. data/sig/herb/fingerprint.rbs +37 -0
  582. data/sig/herb/html/elements.rbs +19 -0
  583. data/sig/herb/html/util.rbs +15 -1
  584. data/sig/herb/lex_result.rbs +0 -6
  585. data/sig/herb/locate.rbs +89 -0
  586. data/sig/herb/location.rbs +12 -0
  587. data/sig/herb/parse_result.rbs +10 -8
  588. data/sig/herb/parser_options.rbs +47 -21
  589. data/sig/herb/position.rbs +11 -0
  590. data/sig/herb/range.rbs +3 -0
  591. data/sig/herb/ruby_program.rbs +51 -0
  592. data/sig/herb/source_path.rbs +132 -0
  593. data/sig/herb/token.rbs +5 -2
  594. data/sig/herb/visitor/context/origin.rbs +51 -0
  595. data/sig/herb/visitor/context/replacements.rbs +39 -0
  596. data/sig/herb/visitor/context.rbs +83 -0
  597. data/sig/herb/visitor/context_aware.rbs +32 -0
  598. data/sig/herb/visitor/diagnostics.rbs +82 -0
  599. data/sig/herb/visitor/experimental.rbs +23 -0
  600. data/sig/herb/visitor/parser_option_requirements.rbs +76 -0
  601. data/sig/herb/visitor/stack.rbs +91 -0
  602. data/sig/herb/visitor.rbs +22 -0
  603. data/sig/herb/warnings.rbs +10 -0
  604. data/sig/herb.rbs +27 -10
  605. data/sig/serialized_ast_errors.rbs +58 -1
  606. data/sig/serialized_ast_nodes.rbs +58 -2
  607. data/src/analyze/action_view/attribute_extraction_helpers.c +73 -36
  608. data/src/analyze/action_view/generated_handlers.c +37 -1
  609. data/src/analyze/action_view/generated_handlers.h +2 -1
  610. data/src/analyze/action_view/helper_registry.c +1353 -58
  611. data/src/analyze/action_view/registry.c +10 -17
  612. data/src/analyze/action_view/stylesheet_link_tag.c +40 -0
  613. data/src/analyze/action_view/tag_helper_node_builders.c +57 -0
  614. data/src/analyze/action_view/tag_helpers.c +423 -93
  615. data/src/analyze/analyze.c +301 -64
  616. data/src/analyze/analyze_helpers.c +420 -23
  617. data/src/analyze/builders.c +79 -51
  618. data/src/analyze/conditional_elements.c +13 -8
  619. data/src/analyze/conditional_open_tags.c +11 -5
  620. data/src/analyze/control_type.c +16 -1
  621. data/src/analyze/herb_directives.c +701 -0
  622. data/src/analyze/invalid_structures.c +11 -7
  623. data/src/analyze/iteration_nodes.c +306 -0
  624. data/src/analyze/missing_end.c +116 -32
  625. data/src/analyze/parse_errors.c +8 -3
  626. data/src/analyze/postfix_conditionals.c +98 -63
  627. data/src/analyze/prism_annotate.c +13 -4
  628. data/src/analyze/render_nodes.c +345 -123
  629. data/src/analyze/strict_locals.c +30 -26
  630. data/src/analyze/ternary_conditionals.c +95 -70
  631. data/src/analyze/transform.c +43 -1
  632. data/src/ast/ast_node.c +3 -13
  633. data/src/ast/ast_nodes.c +911 -112
  634. data/src/ast/ast_pretty_print.c +161 -2
  635. data/src/diff/herb_diff.c +8 -0
  636. data/src/diff/herb_diff_attributes.c +1 -0
  637. data/src/diff/herb_diff_children.c +4 -3
  638. data/src/diff/herb_diff_helpers.c +7 -1
  639. data/src/diff/herb_diff_nodes.c +186 -71
  640. data/src/diff/herb_hash_tree.c +99 -1
  641. data/src/errors.c +1096 -128
  642. data/src/extract.c +30 -8
  643. data/src/herb.c +30 -11
  644. data/src/include/analyze/action_view/attribute_extraction_helpers.h +1 -1
  645. data/src/include/analyze/action_view/helper_registry.h +28 -4
  646. data/src/include/analyze/action_view/tag_helper_handler.h +1 -1
  647. data/src/include/analyze/action_view/tag_helper_node_builders.h +7 -0
  648. data/src/include/analyze/analyze.h +11 -0
  649. data/src/include/analyze/builders.h +16 -0
  650. data/src/include/analyze/helpers.h +23 -2
  651. data/src/include/analyze/herb_directive_kinds.h +41 -0
  652. data/src/include/analyze/herb_directives.h +14 -0
  653. data/src/include/analyze/iteration_nodes.h +11 -0
  654. data/src/include/analyze/prism_annotate.h +3 -3
  655. data/src/include/analyze/ternary_conditionals.h +2 -0
  656. data/src/include/ast/ast_node.h +1 -5
  657. data/src/include/ast/ast_nodes.h +76 -2
  658. data/src/include/ast/ast_pretty_print.h +1 -1
  659. data/src/include/diff/herb_diff.h +11 -0
  660. data/src/include/errors.h +156 -39
  661. data/src/include/extract.h +11 -0
  662. data/src/include/herb.h +6 -0
  663. data/src/include/lexer/erb_openers.h +29 -0
  664. data/src/include/lexer/lexer_peek_helpers.h +8 -1
  665. data/src/include/lexer/lexer_struct.h +4 -0
  666. data/src/include/lexer/token.h +3 -0
  667. data/src/include/lexer/token_struct.h +8 -6
  668. data/src/include/lib/hb_allocator.h +2 -1
  669. data/src/include/lib/hb_array.h +2 -0
  670. data/src/include/lib/hb_foreach.h +1 -1
  671. data/src/include/lib/hb_string.h +7 -0
  672. data/src/include/parser/dot_notation.h +1 -1
  673. data/src/include/parser/foreign_content_elements.h +32 -0
  674. data/src/include/parser/parser.h +16 -30
  675. data/src/include/parser/parser_helpers.h +13 -11
  676. data/src/include/parser/parser_options.h +41 -0
  677. data/src/include/prism/ruby_parser.h +6 -0
  678. data/src/include/util/html_elements.h +85 -0
  679. data/src/include/util/html_util.h +1 -0
  680. data/src/include/version.h +1 -1
  681. data/src/lexer/lexer_peek_helpers.c +15 -1
  682. data/src/lexer/token.c +83 -1
  683. data/src/lexer.c +196 -12
  684. data/src/lib/hb_allocator.c +2 -0
  685. data/src/lib/hb_array.c +26 -0
  686. data/src/lib/hb_string.c +26 -0
  687. data/src/main.c +1 -1
  688. data/src/parser/dot_notation.c +4 -3
  689. data/src/parser/match_tags.c +66 -1
  690. data/src/parser/parser_helpers.c +69 -37
  691. data/src/parser.c +606 -383
  692. data/src/parser_options_defaults.c +30 -0
  693. data/src/prism/prism_helpers.c +3 -0
  694. data/src/prism/ruby_parser.c +49 -0
  695. data/src/util/html_util.c +11 -58
  696. data/src/visitor.c +77 -1
  697. data/templates/ext/herb/error_helpers.c.erb +1 -1
  698. data/templates/ext/herb/nodes.c.erb +18 -13
  699. data/templates/ext/herb/nodes.h.erb +2 -1
  700. data/templates/ext/herb/parser_options_helpers.c.erb +69 -0
  701. data/templates/ext/herb/parser_options_helpers.h.erb +11 -0
  702. data/templates/java/nodes.c.erb +20 -16
  703. data/templates/java/nodes.h.erb +3 -2
  704. data/templates/java/org/herb/ParserOptions.java.erb +24 -0
  705. data/templates/java/org/herb/ast/HelperRegistry.java.erb +67 -31
  706. data/templates/java/org/herb/ast/HelperType.java.erb +29 -0
  707. data/templates/java/org/herb/ast/Nodes.java.erb +11 -7
  708. data/templates/java/parser_options_helpers.c.erb +45 -0
  709. data/templates/java/parser_options_helpers.h.erb +10 -0
  710. data/templates/javascript/packages/client/src/state-kinds.ts.erb +49 -0
  711. data/templates/javascript/packages/client/src/state-operators.ts.erb +15 -0
  712. data/templates/javascript/packages/client/src/state-predicates.ts.erb +27 -0
  713. data/templates/javascript/packages/client/src/state-transforms.ts.erb +19 -0
  714. data/templates/javascript/packages/core/src/action-view-helpers.ts.erb +47 -0
  715. data/templates/javascript/packages/core/src/built-in-components.ts.erb +67 -0
  716. data/templates/javascript/packages/core/src/errors.ts.erb +9 -3
  717. data/templates/javascript/packages/core/src/foreign-content-elements.ts.erb +18 -0
  718. data/templates/javascript/packages/core/src/html-constants.ts.erb +32 -0
  719. data/templates/javascript/packages/core/src/html-elements.ts.erb +41 -0
  720. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +11 -7
  721. data/templates/javascript/packages/core/src/nodes.ts.erb +82 -19
  722. data/templates/javascript/packages/core/src/parser-options.ts.erb +41 -0
  723. data/templates/javascript/packages/core/src/visitor.ts.erb +1 -1
  724. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +3 -3
  725. data/templates/javascript/packages/node/extension/nodes.cpp.erb +17 -12
  726. data/templates/javascript/packages/node/extension/nodes.h.erb +3 -3
  727. data/templates/javascript/packages/node/extension/parser_options_helpers.cpp.erb +41 -0
  728. data/templates/javascript/packages/node/extension/parser_options_helpers.h.erb +12 -0
  729. data/templates/lib/herb/action_view/helper_registry.rb.erb +77 -3
  730. data/templates/lib/herb/ast/nodes.rb.erb +16 -6
  731. data/templates/lib/herb/engine/slots/component_definitions.rb.erb +38 -0
  732. data/templates/lib/herb/engine/slots/state_kinds.rb.erb +37 -0
  733. data/templates/lib/herb/engine/slots/state_operators.rb.erb +26 -0
  734. data/templates/lib/herb/engine/slots/state_predicates.rb.erb +39 -0
  735. data/templates/lib/herb/engine/slots/state_transforms.rb.erb +27 -0
  736. data/templates/lib/herb/errors.rb.erb +34 -6
  737. data/templates/lib/herb/html/elements.rb.erb +12 -0
  738. data/templates/lib/herb/parser_options.rb.erb +32 -0
  739. data/templates/lib/herb/visitor.rb.erb +7 -1
  740. data/templates/rust/herb-printer/src/printer_visitor.rs.erb +14 -0
  741. data/templates/rust/src/action_view_helpers.rs.erb +38 -0
  742. data/templates/rust/src/ast/nodes.rs.erb +24 -31
  743. data/templates/rust/src/errors.rs.erb +1 -1
  744. data/templates/rust/src/nodes.rs.erb +58 -26
  745. data/templates/rust/src/parser_options.rs.erb +16 -0
  746. data/templates/rust/src/prism/generated_deserialize.rs.erb +109 -0
  747. data/templates/rust/src/prism/generated_prettyprint.rs.erb +153 -0
  748. data/templates/rust/src/union_types.rs.erb +7 -1
  749. data/templates/rust/src/visitor.rs.erb +1 -1
  750. data/templates/src/analyze/action_view/helper_registry.c.erb +22 -0
  751. data/templates/src/analyze/missing_end.c.erb +13 -5
  752. data/templates/src/ast/ast_nodes.c.erb +41 -2
  753. data/templates/src/diff/herb_diff_nodes.c.erb +44 -8
  754. data/templates/src/errors.c.erb +8 -3
  755. data/templates/src/include/analyze/action_view/helper_registry.h.erb +19 -0
  756. data/templates/src/include/analyze/herb_directive_kinds.h.erb +25 -0
  757. data/templates/src/include/errors.h.erb +3 -1
  758. data/templates/src/include/parser/foreign_content_elements.h.erb +23 -0
  759. data/templates/src/include/parser/parser_options.h.erb +21 -0
  760. data/templates/src/include/util/html_elements.h.erb +28 -0
  761. data/templates/src/parser_options_defaults.c.erb +9 -0
  762. data/templates/template.rb +564 -9
  763. data/templates/wasm/error_helpers.cpp.erb +4 -4
  764. data/templates/wasm/nodes.cpp.erb +12 -12
  765. data/templates/wasm/nodes.h.erb +3 -3
  766. data/templates/wasm/parser_options_helpers.cpp.erb +25 -0
  767. data/templates/wasm/parser_options_helpers.h.erb +12 -0
  768. metadata +306 -27
  769. data/config/action_view_helpers/actionview/form_helper/button.yml +0 -38
  770. data/config/action_view_helpers/actionview/form_helper/submit.yml +0 -35
  771. data/lib/herb/ast.rb +0 -7
  772. data/lib/herb/diff_operation.rb +0 -34
  773. data/lib/herb/diff_result.rb +0 -59
  774. data/lib/herb/engine/error_formatter.rb +0 -432
  775. data/lib/herb/engine/parser_error_overlay.rb +0 -773
  776. data/lib/herb/engine/validation_error_overlay.rb +0 -182
  777. data/lib/herb/engine/validation_errors.rb +0 -65
  778. data/lib/herb/engine/validator.rb +0 -80
  779. data/sig/herb/ast.rbs +0 -6
  780. data/sig/herb/diff_operation.rbs +0 -34
  781. data/sig/herb/diff_result.rbs +0 -34
  782. data/sig/herb/engine/debug.rbs +0 -38
  783. data/sig/herb/engine/error_formatter.rbs +0 -47
  784. data/sig/herb/engine/parser_error_overlay.rbs +0 -41
  785. data/sig/herb/engine/validation_error_overlay.rbs +0 -35
  786. data/sig/herb/engine/validation_errors.rbs +0 -45
  787. data/sig/herb/engine/validator.rbs +0 -41
  788. data/sig/herb_c_extension.rbs +0 -10
@@ -0,0 +1,1326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "did_you_mean"
4
+
5
+ require_relative "state_directives"
6
+ require_relative "state_overrides"
7
+
8
+ module Herb
9
+ class Engine
10
+ module Slots
11
+ # Compiles the `herb:state` directives a template declares.
12
+ #
13
+ # A directive names states with their defaults. This turns them into assignments the template
14
+ # runs, classifies the conditionals and boolean attributes that read them, folds the counts
15
+ # that count over a collection, and builds the tables a page needs to resolve any of it for
16
+ # itself. It knows nothing about markers; it asks the visitor which slot a node is.
17
+ #
18
+ class StateCompiler
19
+ STATE_KEYWORDS = {
20
+ "if" => :if,
21
+ "elsif" => :elsif,
22
+ "unless" => :unless,
23
+ "case" => :case,
24
+ "when" => :when,
25
+ }.freeze #: Hash[String, Symbol]
26
+
27
+ SEEDS_LOCAL = "_herb_seeds" #: String
28
+ OVERRIDES_LOCAL = "_herb_state_overrides" #: String
29
+ BARE_IDENTIFIER = /\A[a-z_][a-zA-Z0-9_]*\z/ #: Regexp
30
+ BINDABLE_ATTRIBUTES = ["value", "checked", "selected"].freeze #: Array[String]
31
+ BINDABLE_ELEMENTS = ["input", "textarea", "select", "option"].freeze #: Array[String]
32
+ COLLECTION_SLOT_TYPES = [:collection, :keyed].freeze #: Array[Symbol]
33
+
34
+ attr_reader :state_presence #: Hash[Integer, untyped]
35
+ attr_reader :state_values #: Hash[Integer, untyped]
36
+
37
+ #: (untyped) -> void
38
+ def initialize(visitor)
39
+ @visitor = visitor
40
+ @strict_locals = {} #: Hash[String, Symbol]
41
+ @near_missed = Set.new #: Set[Integer]
42
+ @region_states = {} #: Hash[String, StateDirectives::Declaration]
43
+ item_states = {} #: Hash[untyped, Hash[String, StateDirectives::Declaration]]
44
+ @item_states = item_states.compare_by_identity
45
+ @state_directives = [] #: Array[Hash[Symbol, untyped]]
46
+ state_conditionals = {} #: Hash[untyped, Hash[Symbol, untyped]]
47
+ @state_conditionals = state_conditionals.compare_by_identity
48
+ state_signatures = {} #: Hash[untyped, String]
49
+ @state_signatures = state_signatures.compare_by_identity
50
+ @state_counts = [] #: Array[Hash[Symbol, untyped]]
51
+ @state_presence = {} #: Hash[Integer, untyped]
52
+ @state_values = {} #: Hash[Integer, untyped]
53
+ @server_reads = {} #: Hash[String, Array[Hash[String, untyped]]]
54
+ @internal_states = {} #: Hash[String, StateDirectives::Declaration]
55
+ end
56
+
57
+ #: () -> bool
58
+ def any?
59
+ !(@region_states.empty? && @item_states.empty? && @internal_states.empty?)
60
+ end
61
+
62
+ #: () -> String
63
+ def declare_internal_block
64
+ name = "_herb_block_#{@internal_states.size}"
65
+
66
+ @internal_states[name] = StateDirectives::Declaration.new(name: name, kind: :boolean, default: "false", derived: nil, line: nil, column: nil)
67
+
68
+ name
69
+ end
70
+
71
+ #: (String) -> String
72
+ def internal_assignment(name)
73
+ declaration = @internal_states.fetch(name)
74
+ assignment = state_assignment(declaration)
75
+
76
+ "#{overrides_prelude}; #{assignment}"
77
+ end
78
+
79
+ #: () -> Array[String]
80
+ def count_signatures
81
+ @state_counts.filter_map { |count|
82
+ index = @visitor.index_for(count[:collection])
83
+
84
+ next unless index
85
+
86
+ [count[:name], index, count[:by], count[:when] && StateDirectives.condition_source(count[:when])].inspect
87
+ }
88
+ end
89
+
90
+ #: () -> Hash[String, untyped]?
91
+ def manifest
92
+ declared = state_declarations
93
+ names = declared[:region].map { |declaration| declaration[:name] } + declared[:items].values.flatten.map { |declaration| declaration[:name] } + @internal_states.keys
94
+
95
+ return nil if names.empty?
96
+
97
+ reads = {} #: Hash[String, Array[Integer]]
98
+ declarations = declared[:region].map { |declaration| declared_entry(declaration, "region") } + declared[:items].flat_map { |index, list| list.map { |declaration| declared_entry(declaration, index) } }
99
+
100
+ declarations += @internal_states.values.map { |declaration| declared_entry(declaration.to_h, "region").merge("internal" => true) }
101
+
102
+ @visitor.slots.each do |slot|
103
+ next unless slot.valued?
104
+
105
+ name = slot.expression.to_s.strip.delete_suffix("?")
106
+
107
+ unless names.include?(name)
108
+ warn_near_miss(slot, name, names)
109
+
110
+ next
111
+ end
112
+
113
+ scope_and_depth = @visitor.scope_of(@visitor.slot_nodes[slot.index])
114
+
115
+ next if scope_and_depth && @visitor.block_locals(scope_and_depth[0]).include?(name)
116
+
117
+ (reads[name] ||= []) << slot.index
118
+
119
+ warn_unbindable(slot, name) if slot.interpolated?
120
+ end
121
+
122
+ presence = {} #: Hash[String, untyped]
123
+
124
+ state_presence.each do |index, read|
125
+ presence[index.to_s] = StateDirectives.condition_entry(read)
126
+ StateDirectives.read_names(read).each { |name| (reads[name] ||= []) << index }
127
+
128
+ warn_computed_presence(index, read)
129
+ end
130
+
131
+ computed = {} #: Hash[String, untyped]
132
+
133
+ state_values.each do |index, read|
134
+ computed[index.to_s] = StateDirectives.condition_entry(read)
135
+ StateDirectives.read_names(read).each { |name| (reads[name] ||= []) << index }
136
+ end
137
+
138
+ conditionals = {} #: Hash[String, Hash[String, untyped]]
139
+
140
+ state_conditional_entries.each do |index, info|
141
+ conditionals[index.to_s] = { "arms" => info[:arms], "else" => info[:else] }
142
+ end
143
+
144
+ state_count_entries.each do |count|
145
+ declaration = declarations.find { |entry| entry["name"] == count[:name] && entry["scope"] == "region" }
146
+
147
+ next unless declaration
148
+
149
+ declaration["count"] = { "collection" => count[:collection], "when" => count[:when], "by" => count[:by] }
150
+ end
151
+
152
+ {
153
+ "version" => @visitor.version,
154
+ "declarations" => declarations,
155
+ "reads" => reads,
156
+ "conditionals" => conditionals,
157
+ "presence" => presence,
158
+ "computed" => computed,
159
+ "server" => { "branches" => branch_orphans(reads, computed, presence, steerable_names(declarations)), "reads" => @server_reads },
160
+ "fragments" => fragment_entries,
161
+ }
162
+ end
163
+
164
+ #: (untyped, String) -> void
165
+ def warn_unbindable(slot, name)
166
+ return unless bindable_shape?(slot)
167
+ return if @visitor.listener_writes?(slot.index, [name])
168
+
169
+ place = slot.attribute ? "`#{slot.attribute}`" : "content"
170
+
171
+ @visitor.slot_warning(
172
+ "`#{name}` shares this `<#{slot.tag}>`'s #{place} with other text, so typing here will not write the state back. State writes still update the whole #{place}.",
173
+ @visitor.slot_nodes[slot.index]&.location,
174
+ :binding,
175
+ suggestion: "Make `#{name}` the only #{slot.attribute ? "value of `#{slot.attribute}`" : "content of the `<#{slot.tag}>`"} to bind it, or write the state from your own listener."
176
+ )
177
+ end
178
+
179
+ #: (untyped) -> bool
180
+ def bindable_shape?(slot)
181
+ if slot.attribute
182
+ BINDABLE_ATTRIBUTES.include?(slot.attribute) && BINDABLE_ELEMENTS.include?(slot.tag.to_s)
183
+ else
184
+ slot.tag == "textarea"
185
+ end
186
+ end
187
+
188
+ #: (Integer, untyped) -> void
189
+ def warn_computed_presence(index, read)
190
+ slot = @visitor.slots[index]
191
+
192
+ return unless slot && bindable_shape?(slot)
193
+ return if bare_presence?(read)
194
+ return if @visitor.listener_writes?(index, StateDirectives.read_names(read))
195
+
196
+ @visitor.slot_warning(
197
+ "`#{slot.attribute}` on this `<#{slot.tag}>` follows `#{StateDirectives.condition_source(read)}`, so using the control cannot write a state back. The attribute still updates when the states change.",
198
+ @visitor.slot_nodes[index]&.location,
199
+ :binding,
200
+ suggestion: "Drive `#{slot.attribute}` with a single state to bind it, or write the states from your own listener."
201
+ )
202
+ end
203
+
204
+ #: (untyped) -> bool
205
+ def bare_presence?(read)
206
+ read.is_a?(StateDirectives::Read) && read.operator.nil? && read.transform.nil? && read.comparand.nil? && read.against.nil?
207
+ end
208
+
209
+ #: (untyped, String, Array[String]) -> void
210
+ def warn_near_miss(slot, name, names)
211
+ return unless BARE_IDENTIFIER.match?(name)
212
+ return if @strict_locals.key?(name)
213
+
214
+ suggestions = DidYouMean::SpellChecker.new(dictionary: names).correct(name)
215
+
216
+ return if suggestions.empty?
217
+
218
+ @near_missed << slot.index
219
+
220
+ @visitor.slot_warning(
221
+ "`#{name}` reads like the state `#{suggestions.first}` but is not declared, so the server owns it and the client can never fill it.",
222
+ @visitor.slot_nodes[slot.index]&.location,
223
+ :unknown,
224
+ suggestion: "Spell the state's name, or declare `#{name}` in `herb:state`."
225
+ )
226
+ end
227
+
228
+ #: () -> Hash[String, untyped]
229
+ def fragment_entries
230
+ refetchable = @server_reads.values.flatten.to_set { |entry| entry["index"] }
231
+ entries = {} #: Hash[String, untyped]
232
+
233
+ @visitor.fragment_indexes.each do |index|
234
+ inside = (@visitor.slots_by_branch(index)[0] || []).select { |slot_index| refetchable.include?(slot_index) }
235
+
236
+ entries[index.to_s] = { "fallback" => 1, "reads" => inside }.merge(@visitor.fragment_timing_for(index)) unless inside.empty?
237
+ end
238
+
239
+ @visitor.deferred_entries.each do |index, info|
240
+ inside = (@visitor.slots_by_branch(index)[0] || []).select { |slot_index| refetchable.include?(slot_index) }
241
+
242
+ entries[index.to_s] = { "mode" => info[:mode], "state" => info[:state], "fallback" => 1, "reads" => inside }.merge(info[:timing])
243
+ end
244
+
245
+ entries
246
+ end
247
+
248
+ #: (Array[Hash[String, untyped]]) -> Set[String]
249
+ def steerable_names(declarations)
250
+ declarations.filter_map { |entry| entry["name"] if entry["scope"] == "region" && !entry["derived"] && !entry["count"] }.to_set
251
+ end
252
+
253
+ #: (untyped) -> Array[String]
254
+ def condition_names(condition)
255
+ if condition.is_a?(Hash)
256
+ parts = condition["all"] || condition["any"]
257
+
258
+ return parts.to_a.flat_map { |part| condition_names(part) }
259
+ end
260
+
261
+ return [] unless condition.is_a?(Array)
262
+
263
+ names = [] #: Array[String]
264
+ names << condition[0] if condition[0].is_a?(String)
265
+
266
+ comparand = condition[1]
267
+ names << comparand["state"] if comparand.is_a?(Hash) && comparand["state"].is_a?(String)
268
+
269
+ names
270
+ end
271
+
272
+ #: (Hash[Symbol, untyped], Set[String]) -> bool
273
+ def steered_conditional?(info, steerable)
274
+ names = info[:arms].flat_map { |arm| condition_names(arm["condition"]) }
275
+
276
+ !names.empty? && names.all? { |name| steerable.include?(name) }
277
+ end
278
+
279
+ #: (Integer, Hash[Symbol, untyped]) -> Set[Integer]
280
+ def default_branch_slots(index, info)
281
+ none = Set.new #: Set[Integer]
282
+ node = @visitor.slot_nodes[index]
283
+
284
+ return none unless node
285
+
286
+ scope, = @visitor.scope_of(node)
287
+ states = states_for(scope)
288
+
289
+ info[:arms].each do |arm|
290
+ verdict = static_condition(arm["condition"], states)
291
+
292
+ return none if verdict.nil?
293
+ next unless verdict
294
+ return none if arm["branch"].nil?
295
+
296
+ return branch_slot_set(index, arm.fetch("branch"))
297
+ end
298
+
299
+ return none if info[:else].nil?
300
+
301
+ branch_slot_set(index, info.fetch(:else))
302
+ end
303
+
304
+ #: (Integer, Integer) -> Set[Integer]
305
+ def branch_slot_set(index, branch)
306
+ slots = @visitor.slots_by_branch(index)[branch] || [] #: Array[Integer]
307
+
308
+ slots.to_set
309
+ end
310
+
311
+ #: (untyped, Hash[String, StateDirectives::Declaration]) -> bool?
312
+ def static_condition(condition, states)
313
+ return static_combo(condition, states) if condition.is_a?(Hash)
314
+ return nil unless condition.is_a?(Array)
315
+ return nil if condition[3]
316
+
317
+ name, comparand, operator = condition
318
+ value = static_default(name, states)
319
+
320
+ return nil if value == :__herb_undecided
321
+
322
+ if comparand.nil?
323
+ case operator
324
+ when nil then return !value.nil? && value != false
325
+ when "blank" then return value.nil? || value == false || value == ""
326
+ when "present" then return !(value.nil? || value == false || value == "")
327
+ else return nil
328
+ end
329
+ end
330
+
331
+ return nil unless comparand.is_a?(Hash)
332
+
333
+ against = static_comparand(comparand, states)
334
+
335
+ return nil if against == :__herb_undecided
336
+
337
+ compare_statics(value, against, operator || "==")
338
+ end
339
+
340
+ #: (Hash[String, untyped], Hash[String, StateDirectives::Declaration]) -> untyped
341
+ def static_comparand(comparand, states)
342
+ return comparand["value"] if comparand.key?("value")
343
+ return :__herb_undecided if comparand["transform"] || !comparand["state"].is_a?(String)
344
+
345
+ static_default(comparand.fetch("state"), states)
346
+ end
347
+
348
+ #: (Hash[String, untyped], Hash[String, StateDirectives::Declaration]) -> bool?
349
+ def static_combo(condition, states)
350
+ parts = (condition["all"] || condition["any"]).to_a.map { |part| static_condition(part, states) }
351
+
352
+ if condition.key?("all")
353
+ return false if parts.any?(false)
354
+ return true if parts.all?(true)
355
+ else
356
+ return true if parts.any?(true)
357
+ return false if parts.all?(false)
358
+ end
359
+
360
+ nil
361
+ end
362
+
363
+ #: (String, Hash[String, StateDirectives::Declaration]) -> untyped
364
+ def static_default(name, states)
365
+ declaration = states[name]
366
+
367
+ return :__herb_undecided if declaration.nil? || declaration.derived
368
+ return :__herb_undecided if @state_counts.any? { |count| count[:name] == name }
369
+ return :__herb_undecided unless StateDirectives.literal?(declaration.default)
370
+
371
+ StateDirectives.literal_value(declaration.default)
372
+ end
373
+
374
+ #: (untyped, untyped, String) -> bool?
375
+ def compare_statics(value, against, operator)
376
+ case operator
377
+ when "==" then value == against
378
+ when "!=" then value != against
379
+ when ">", ">=", "<", "<="
380
+ return nil unless value.is_a?(Integer) && against.is_a?(Integer)
381
+
382
+ value.public_send(operator, against)
383
+ end
384
+ end
385
+
386
+ #: (Hash[String, Array[Integer]], Hash[String, untyped], Hash[String, untyped], Set[String]) -> void
387
+ def branch_orphans(reads, computed, presence, steerable)
388
+ branches = {} #: Hash[String, Array[Hash[String, untyped]]]
389
+
390
+ return branches unless @visitor.client?
391
+
392
+ covered = (reads.values.flatten + computed.keys.map(&:to_i) + presence.keys.map(&:to_i)).to_set
393
+ refetchable = @server_reads.values.flatten.to_set { |entry| entry["index"] }
394
+ seen = Set.new #: Set[Integer]
395
+
396
+ state_conditional_entries.each do |index, info|
397
+ steered = steered_conditional?(info, steerable)
398
+ served = default_branch_slots(index, info)
399
+
400
+ @visitor.slots_inside(index).each do |inside|
401
+ next unless seen.add?(inside)
402
+
403
+ slot = @visitor.slots[inside]
404
+
405
+ next unless slot
406
+ next unless slot.valued? || refetchable.include?(inside)
407
+ next if covered.include?(inside)
408
+ next if @near_missed.include?(inside)
409
+
410
+ (branches[index.to_s] ||= []) << { "index" => inside, "node_path" => slot.node_path }
411
+
412
+ next if steered
413
+ next if served.include?(inside)
414
+ next if refetchable.include?(inside)
415
+
416
+ expression = slot.expression.to_s.strip
417
+
418
+ @visitor.slot_warning(
419
+ "`<%= #{expression} %>` sits inside a branch the client can show on its own, but its value comes from the server. The server only computes values for the branch it renders, so showing this branch ahead of the server leaves the value empty.",
420
+ @visitor.slot_nodes[inside]&.location,
421
+ :branch,
422
+ suggestion: "Declare a state holding the value, or move `<%= #{expression} %>` out of the conditional."
423
+ )
424
+ end
425
+ end
426
+
427
+ branches
428
+ end
429
+
430
+ #: (Hash[Symbol, untyped], (String | Integer)) -> Hash[String, untyped]
431
+ def declared_entry(declaration, scope)
432
+ entry = declaration.transform_keys(&:to_s).merge("scope" => scope)
433
+ derived = entry["derived"]
434
+
435
+ entry["derived"] = StateDirectives.condition_entry(derived) if derived
436
+ entry["value"] = StateDirectives.literal_value(entry["default"]) if !derived && StateDirectives.literal?(entry["default"])
437
+
438
+ entry
439
+ end
440
+
441
+ #: (String, String?) -> void
442
+ def declare_local(name, source)
443
+ @strict_locals[name] = local_kind(source)
444
+ end
445
+
446
+ #: () -> Array[Hash[Symbol, untyped]]
447
+ def state_count_entries
448
+ @state_counts.filter_map { |count|
449
+ index = @visitor.index_for(count[:collection])
450
+
451
+ next unless index
452
+
453
+ read = count[:when]
454
+
455
+ {
456
+ name: count[:name],
457
+ collection: index,
458
+ by: count[:by],
459
+ when: read ? StateDirectives.condition_entry(read) : nil,
460
+ }
461
+ }
462
+ end
463
+
464
+ #: () -> Hash[Symbol, untyped]
465
+ def state_declarations
466
+ items = {} #: Hash[Integer, Array[Hash[Symbol, untyped]]]
467
+
468
+ @item_states.each do |scope, declarations|
469
+ index = @visitor.index_for(scope)
470
+
471
+ items[index] = declarations.values.map(&:to_h) if index
472
+ end
473
+
474
+ { region: @region_states.values.map(&:to_h), items: items }
475
+ end
476
+
477
+ #: () -> Array[String]
478
+ def seeded_region_states
479
+ @region_states.values.select { |declaration| StateDirectives.seeded?(declaration) }.map(&:name)
480
+ end
481
+
482
+ #: () -> Hash[Integer, Array[String]]
483
+ def seeded_item_states
484
+ seeded = {} #: Hash[Integer, Array[String]]
485
+
486
+ @item_states.each do |scope, declarations|
487
+ index = @visitor.index_for(scope)
488
+
489
+ next unless index
490
+
491
+ names = declarations.values.select { |declaration| StateDirectives.seeded?(declaration) }.map(&:name)
492
+
493
+ seeded[index] = names unless names.empty?
494
+ end
495
+
496
+ seeded
497
+ end
498
+
499
+ #: () -> Array[Hash[Symbol, untyped]]
500
+ def state_entries
501
+ declared = state_declarations
502
+
503
+ entries = declared[:region].map { |declaration| declaration.merge(scope: :region) }
504
+
505
+ declared[:items].sort.each do |index, declarations|
506
+ entries += declarations.map { |declaration| declaration.merge(scope: index) }
507
+ end
508
+
509
+ entries
510
+ end
511
+
512
+ #: () -> Hash[Integer, String]
513
+ def state_conditional_signatures
514
+ resolved = {} #: Hash[Integer, String]
515
+
516
+ @state_signatures.each do |node, signature|
517
+ index = @visitor.index_for(node)
518
+
519
+ resolved[index] = signature if index
520
+ end
521
+
522
+ resolved
523
+ end
524
+
525
+ #: () -> Hash[Integer, Hash[Symbol, untyped]]
526
+ def state_conditional_entries
527
+ resolved = {} #: Hash[Integer, Hash[Symbol, untyped]]
528
+
529
+ @state_conditionals.each do |node, info|
530
+ index = @visitor.index_for(node)
531
+
532
+ resolved[index] = info if index
533
+ end
534
+
535
+ resolved
536
+ end
537
+
538
+ #: (untyped, Array[untyped]) -> void
539
+ def register_state_directive(node, parent)
540
+ return unless node.is_a?(Herb::AST::HerbStateDirectiveNode)
541
+
542
+ scope = @visitor.current_collection
543
+ declared = StateDirectives.parse(node, @strict_locals, visitor: @visitor, enclosing: scope ? @region_states : {})
544
+ empty = {} #: Hash[String, StateDirectives::Declaration]
545
+ bucket = scope ? (@item_states[scope] ||= empty) : @region_states
546
+
547
+ start = node.location&.start
548
+
549
+ declared.each do |declaration|
550
+ spelled = declaration_location(declaration) || node.location
551
+
552
+ if @strict_locals.key?(declaration.name)
553
+ next @visitor.slot_error("`#{declaration.name}` is both a strict local and a state. A local comes from the caller and a state is owned by the client, so the name has two owners.", spelled, :declaration, suggestion: "Rename one of the two.")
554
+ end
555
+
556
+ if bucket.key?(declaration.name)
557
+ next @visitor.slot_error("The state `#{declaration.name}` is declared twice in the same scope.", spelled, :declaration, suggestion: "Remove one of the two declarations.")
558
+ end
559
+
560
+ shadowed = scope ? @region_states.key?(declaration.name) : @item_states.values.any? { |declarations| declarations.key?(declaration.name) }
561
+
562
+ if shadowed
563
+ next @visitor.slot_error("The state `#{declaration.name}` is declared in both an item and its region, so a later read could mean either one.", spelled, :declaration, suggestion: "Give them different names, like `item_#{declaration.name}` for the one inside the loop.")
564
+ end
565
+
566
+ bucket[declaration.name] = declaration.line ? declaration : declaration.with(line: start&.line, column: start&.column)
567
+ end
568
+
569
+ @state_directives << { node: node, parent: parent, scope: scope, inline: @visitor.inline? }
570
+ end
571
+
572
+ #: (StateDirectives::Declaration) -> Herb::Location?
573
+ def declaration_location(declaration)
574
+ line = declaration.line
575
+
576
+ return nil unless line
577
+
578
+ column = declaration.column || 0
579
+
580
+ Herb::Location.from(line, column, line, column + declaration.name.length)
581
+ end
582
+
583
+ #: (String?) -> Symbol
584
+ def local_kind(source)
585
+ return :seeded if source.nil? || source.to_s.strip.empty?
586
+
587
+ result = Prism.parse(source.to_s.strip)
588
+
589
+ return :seeded if result.failure?
590
+
591
+ body = result.value.statements.body
592
+
593
+ return :seeded unless body.one?
594
+
595
+ StateDirectives::KINDS[body.first.class.name] || :seeded
596
+ end
597
+
598
+ #: (untyped) -> Hash[String, StateDirectives::Declaration]
599
+ def states_for(scope)
600
+ region = @region_states.merge(@internal_states)
601
+ states = scope ? region.merge(@item_states[scope] || {}) : region
602
+ none = [] #: Array[String]
603
+ shadowed = scope ? @visitor.block_locals(scope) : none
604
+
605
+ shadowed.empty? ? states : states.except(*shadowed)
606
+ end
607
+
608
+ #: () -> void
609
+ def apply_states
610
+ @state_directives.each do |directive|
611
+ parent = directive[:parent]
612
+ position = parent.index(directive[:node])
613
+
614
+ next unless position
615
+
616
+ scope = directive[:scope]
617
+ bucket = scope ? (@item_states[scope] || {}) : @region_states
618
+ assignments = bucket.values.map { |declaration| state_assignment(declaration) }.join("; ")
619
+ assignments = "#{overrides_prelude}; #{assignments}"
620
+ seeds = directive[:inline] || @visitor.degraded? ? nil : seeds_marker(bucket.values)
621
+
622
+ parent[position] = @visitor.record_assignment(@visitor.erb_code_node(seeds ? "; #{assignments}; #{seeds}" : "; #{assignments}"))
623
+ end
624
+
625
+ if @region_states.empty? && @item_states.empty? && @internal_states.empty?
626
+ check_fragments
627
+
628
+ return
629
+ end
630
+
631
+ classify_state_conditionals
632
+ check_state_value_reads
633
+ check_collection_reads
634
+ check_state_count_reads
635
+ check_fragments
636
+ check_deferred_placement
637
+ end
638
+
639
+ #: () -> void
640
+ def check_deferred_placement
641
+ collections = @visitor.slots.select { |slot| slot.type == :collection }
642
+
643
+ @visitor.deferred_entries.each do |index, info|
644
+ slot = @visitor.slots[index]
645
+
646
+ next unless slot
647
+ next if collections.none? { |collection| slot.node_path.first(collection.node_path.length) == collection.node_path }
648
+
649
+ @visitor.slot_error(
650
+ "A `<#{info[:mode].capitalize}>` sits inside a collection, and a deferred block cannot stand per item yet.",
651
+ @visitor.slot_nodes[index]&.location,
652
+ :component,
653
+ suggestion: "Move the block outside the loop, or defer around the whole collection."
654
+ )
655
+ end
656
+ end
657
+
658
+ #: () -> void
659
+ def check_fragments
660
+ refetchable = @server_reads.values.flatten.to_set { |entry| entry["index"] }
661
+
662
+ @visitor.fragment_indexes.each do |index|
663
+ inside = @visitor.slots_by_branch(index)[0] || []
664
+
665
+ next if inside.any? { |slot_index| refetchable.include?(slot_index) }
666
+
667
+ @visitor.slot_warning(
668
+ "Nothing inside this `<Fragment>` is derived on the server, so its `<Fallback>` can never appear.",
669
+ @visitor.slot_nodes[index]&.location,
670
+ :component,
671
+ suggestion: "Compute something with a declared state inside the fragment, or unwrap it."
672
+ )
673
+ end
674
+ end
675
+
676
+ #: (Array[StateDirectives::Declaration]) -> String?
677
+ def seeds_marker(declarations)
678
+ return nil unless @visitor.marking?
679
+
680
+ seeded = declarations.select { |declaration| StateDirectives.seeded?(declaration) }
681
+ supplied = declarations.select { |declaration| overridable?(declaration) }
682
+
683
+ return nil if seeded.empty? && supplied.empty?
684
+
685
+ static = seeded.map { |declaration| "#{declaration.name.inspect} => #{declaration.name}" }.join(", ")
686
+ overridable = supplied.map { |declaration| "#{declaration.name.inspect} => #{declaration.name}" }.join(", ")
687
+ seeds = "#{Markers.seeds_expression(static)}.merge(#{Markers.seeds_expression(overridable)}.slice(*(#{OVERRIDES_LOCAL} || {}).keys))"
688
+ marker = "#{@visitor.bufvar} << ::Herb::Engine.raw(#{@visitor.markers.seeds_open_prefix.inspect} + ::JSON.generate(#{SEEDS_LOCAL}).gsub(\"--\", \"-\\\\u002d\") + #{@visitor.markers.seeds_open_suffix.inspect})"
689
+ marker = "#{marker} unless #{SEEDS_LOCAL}.empty?" if seeded.empty?
690
+
691
+ "#{SEEDS_LOCAL} = #{seeds}; #{marker}"
692
+ end
693
+
694
+ #: (StateDirectives::Declaration) -> String
695
+ def state_assignment(declaration)
696
+ source = declaration.default
697
+
698
+ if declaration.derived
699
+ StateDirectives.read_names(declaration.derived).each do |name|
700
+ source = StateDirectives.rewrite_reads(source, name)
701
+ end
702
+ end
703
+
704
+ default = declaration.kind == :boolean ? "!!(#{source})" : source
705
+
706
+ return "#{declaration.name} = #{default}" unless overridable?(declaration)
707
+
708
+ "#{declaration.name} = ::Herb::Engine::Slots::StateOverrides.fetch(#{OVERRIDES_LOCAL}, #{declaration.name.inspect}, #{declaration.kind.inspect}) { #{default} }"
709
+ end
710
+
711
+ #: (StateDirectives::Declaration) -> bool
712
+ def overridable?(declaration)
713
+ return false if declaration.derived
714
+
715
+ @state_counts.none? { |count| count[:name] == declaration.name }
716
+ end
717
+
718
+ #: (String) -> StateDirectives::Declaration?
719
+ def region_declaration(name)
720
+ @region_states[name]
721
+ end
722
+
723
+ #: () -> Array[String]
724
+ def region_state_names
725
+ @region_states.keys
726
+ end
727
+
728
+ #: (String) -> bool
729
+ def counted_state?(name)
730
+ @state_counts.any? { |count| count[:name] == name }
731
+ end
732
+
733
+ #: () -> String
734
+ def overrides_prelude
735
+ "#{OVERRIDES_LOCAL} = ::Herb::Engine::Slots::StateOverrides.resolve((#{StateOverrides::HOOK} if defined?(#{StateOverrides::HOOK})), #{@visitor.identifier.inspect})"
736
+ end
737
+
738
+ #: () -> void
739
+ def classify_state_conditionals
740
+ @visitor.slot_nodes.each do |node|
741
+ index = @visitor.index_for(node)
742
+
743
+ next unless index && @visitor.slots[index].type == :conditional
744
+
745
+ scope, = @visitor.scope_of(node)
746
+ states = states_for(scope)
747
+
748
+ next if states.empty?
749
+
750
+ info = state_conditional_for(node, states)
751
+
752
+ next unless info
753
+
754
+ @state_signatures[node] = info.delete(:signature)
755
+ @state_conditionals[node] = info
756
+ end
757
+ end
758
+
759
+ #: (untyped, Hash[String, StateDirectives::Declaration]) -> Hash[Symbol, untyped]?
760
+ def state_conditional_for(node, states)
761
+ return state_case_for(node, states) if node.is_a?(Herb::AST::ERBCaseNode)
762
+ return state_unless_for(node, states) if node.is_a?(Herb::AST::ERBUnlessNode)
763
+
764
+ return nil unless node.is_a?(Herb::AST::ERBIfNode)
765
+
766
+ chain = @visitor.conditional_chain(node)
767
+ else_position = chain.index { |arm| arm.is_a?(Herb::AST::ERBElseNode) }
768
+ conditions = else_position ? chain.take(else_position) : chain
769
+
770
+ arms = [] #: Array[untyped]
771
+ sources = [] #: Array[String]
772
+
773
+ conditions.each_with_index do |arm, branch|
774
+ expression = condition_expression(arm)
775
+ read = StateDirectives.condition_read(expression, states, @visitor, condition_anchor(arm, expression), resolved_ruby(arm))
776
+
777
+ return nil if read == :reported
778
+
779
+ if read == :computed
780
+ return @visitor.slot_error("`#{expression}` computes with the state `#{mentioned_state(expression, states)&.name}`. The client resolves each condition itself and cannot run Ruby to pick a branch.", condition_anchor(arm, expression).location, :read, suggestion: read_advice(mentioned_state(expression, states)))
781
+ end
782
+
783
+ unless read.is_a?(StateDirectives::Read) || read.is_a?(StateDirectives::Combo)
784
+ return nil if arms.empty?
785
+
786
+ return @visitor.slot_error("`#{expression}` sits in a state-driven conditional but reads no state. The client resolves every arm, so an arm it cannot answer would never be chosen.", condition_anchor(arm, expression).location, :conditional, suggestion: "Read a state in this arm, or move this branch into its own conditional.")
787
+ end
788
+
789
+ dead = StateDirectives.never_falsy_read(read)
790
+
791
+ return presence_error(expression, dead, states, condition_anchor(arm, expression), "true") if dead
792
+
793
+ StateDirectives.read_names(read).each { |name| rewrite_predicate(arm, name) }
794
+
795
+ arms << arm_entry(read, branch)
796
+ sources << "#{StateDirectives.condition_source(read)}@#{branch}"
797
+ end
798
+
799
+ return nil if arms.empty?
800
+
801
+ { arms: arms, else: else_position, signature: signature_of(sources, else_position) }
802
+ end
803
+
804
+ #: (Array[String], Integer?) -> String
805
+ def signature_of(sources, else_position)
806
+ (sources + ["else@#{else_position}"]).join("|")
807
+ end
808
+
809
+ #: (untyped, Integer?) -> Hash[String, untyped]
810
+ def arm_entry(read, branch)
811
+ { "branch" => branch, "condition" => StateDirectives.condition_entry(read) }
812
+ end
813
+
814
+ #: (untyped, Hash[String, StateDirectives::Declaration]) -> Hash[Symbol, untyped]?
815
+ def state_unless_for(node, states)
816
+ expression = condition_expression(node)
817
+ read = StateDirectives.condition_read(expression, states, @visitor, condition_anchor(node, expression), resolved_ruby(node))
818
+
819
+ return nil if read.nil? || read == :reported
820
+
821
+ if read == :computed
822
+ return @visitor.slot_error("`unless #{expression}` computes with the state `#{mentioned_state(expression, states)&.name}`. The client resolves each condition itself and cannot run Ruby to pick a branch.", condition_anchor(node, expression).location, :read, suggestion: read_advice(mentioned_state(expression, states)))
823
+ end
824
+
825
+ return nil unless read.is_a?(StateDirectives::Read) || read.is_a?(StateDirectives::Combo)
826
+
827
+ dead = StateDirectives.never_falsy_read(read)
828
+
829
+ return presence_error("unless #{expression}", dead, states, condition_anchor(node, expression), "false") if dead
830
+
831
+ chain = @visitor.conditional_chain(node)
832
+ else_position = chain.index { |arm| arm.is_a?(Herb::AST::ERBElseNode) }
833
+
834
+ StateDirectives.read_names(read).each { |name| rewrite_predicate(node, name) }
835
+
836
+ {
837
+ arms: [arm_entry(read, else_position)],
838
+ else: 0,
839
+ signature: signature_of(["!#{StateDirectives.condition_source(read)}@#{else_position}"], 0),
840
+ }
841
+ end
842
+
843
+ #: (String, StateDirectives::Read, Hash[String, StateDirectives::Declaration], StateAnchor, String) -> nil
844
+ def presence_error(spelled, read, states, anchor, answer)
845
+ declaration = states.fetch(read.name)
846
+
847
+ @visitor.slot_error(
848
+ "`#{spelled}` reads #{StateDirectives.subject_phrase(read)} as a presence. Only `nil` and `false` are falsy in Ruby, so the condition is always #{answer}.",
849
+ anchor.location,
850
+ :read,
851
+ suggestion: "#{StateDirectives.predicate_advice(read.kind, read.name)}compare it to a literal#{StateDirectives.default_example(declaration, "#{read.name} == ")}, or declare it as a boolean."
852
+ )
853
+ end
854
+
855
+ #: (untyped, Hash[String, StateDirectives::Declaration]) -> Hash[Symbol, untyped]?
856
+ def state_case_for(node, states)
857
+ subject_source = condition_expression(node)
858
+ read = StateDirectives.condition_read(subject_source, states, @visitor, condition_anchor(node, subject_source), resolved_ruby(node))
859
+
860
+ return nil if read.nil? || read == :reported
861
+
862
+ unless read.is_a?(StateDirectives::Read) && read.comparand.nil? && read.operator.nil? && read.transform.nil?
863
+ return @visitor.slot_error("`case #{subject_source}` switches on something other than a bare state read. The client resolves a `case` by looking the state up.", condition_anchor(node, subject_source).location, :conditional, suggestion: "Switch on the state itself, like `case #{mentioned_state(subject_source, states)&.name}`.")
864
+ end
865
+
866
+ rewrite_predicate(node, read.name)
867
+
868
+ declaration = states.fetch(read.name)
869
+ arms = [] #: Array[untyped]
870
+ sources = [] #: Array[String]
871
+
872
+ node.conditions.each_with_index do |arm, branch|
873
+ list = condition_expression(arm)
874
+ comparands = StateDirectives.when_comparands(list, declaration)
875
+
876
+ if comparands == :computed
877
+ return @visitor.slot_error("`when #{list}` on the state `#{read.name}` has a comparand that is not a literal. The client resolves a `when` by lookup.", condition_anchor(arm, list).location, :conditional, suggestion: "List literals instead#{StateDirectives.default_example(declaration, "when ")}.")
878
+ end
879
+
880
+ if comparands == :mismatched
881
+ return @visitor.slot_error("`when #{list}` compares the #{declaration.kind.to_s.capitalize} state `#{read.name}` against a literal of another type, so it can never match.", condition_anchor(arm, list).location, :compare, suggestion: "Use #{StateDirectives.kind_article(declaration.kind)} literal in every arm#{StateDirectives.default_example(declaration, "when ")}.")
882
+ end
883
+
884
+ next unless comparands.is_a?(Array)
885
+
886
+ comparands.each do |comparand|
887
+ arms << { "branch" => branch, "condition" => [read.name, { "value" => StateDirectives.literal_value(comparand) }] }
888
+ sources << "#{read.name}==#{comparand}@#{branch}"
889
+ end
890
+ end
891
+
892
+ else_branch = node.else_clause ? node.conditions.size : nil
893
+
894
+ { arms: arms, else: else_branch, signature: signature_of(sources, else_branch) }
895
+ end
896
+
897
+ #: (String, Hash[String, StateDirectives::Declaration]) -> StateDirectives::Declaration?
898
+ def mentioned_state(expression, states)
899
+ states.each_value.find { |declaration| StateDirectives.mentions_any?(expression, { declaration.name => declaration }) }
900
+ end
901
+
902
+ #: (StateDirectives::Declaration?) -> String
903
+ def read_advice(declaration)
904
+ return "Read the state bare, or compare it to a literal." unless declaration
905
+
906
+ name = declaration.name
907
+
908
+ return "Read `#{name}` bare, like `<% if #{name} %>`, or as `#{name}?`." if declaration.kind == :boolean
909
+
910
+ "Read `#{name}` bare, like `<% if #{name} %>`, or compare it to a literal#{StateDirectives.default_example(declaration, "#{name} == ")}."
911
+ end
912
+
913
+ #: (untyped, String) -> StateAnchor
914
+ def condition_anchor(node, expression)
915
+ content = node.content if node.respond_to?(:content)
916
+
917
+ return StateAnchor.new(node.location) unless content.is_a?(Herb::Token)
918
+
919
+ StateAnchor.new(content.location, token: content, expression: expression)
920
+ end
921
+
922
+ #: (untyped) -> String
923
+ def condition_expression(node)
924
+ source = node.content&.value.to_s.strip
925
+ keyword = source[/\A[a-z]+/]
926
+
927
+ STATE_KEYWORDS.key?(keyword.to_s) ? source.delete_prefix(keyword.to_s).strip : source
928
+ end
929
+
930
+ #: (untyped) -> untyped
931
+ def resolved_ruby(node)
932
+ @visitor.ruby_at(node.content&.location) if node.respond_to?(:content) && node.content.is_a?(Herb::Token)
933
+ end
934
+
935
+ #: (untyped) -> untyped
936
+ def register_count_fold(node)
937
+ return nil if @visitor.continuation?(node)
938
+ return nil unless node.respond_to?(:subsequent) && node.subsequent.nil?
939
+ return nil unless @visitor.in_item_body?
940
+
941
+ scope = @visitor.current_collection
942
+
943
+ return nil unless scope
944
+
945
+ states = states_for(scope)
946
+
947
+ return nil if states.empty?
948
+
949
+ body = node.respond_to?(:statements) ? node.statements : nil #: untyped
950
+
951
+ return nil unless body.is_a?(Array)
952
+
953
+ significant = body.reject { |child| @visitor.blank_child?(child) }
954
+
955
+ return nil unless significant.one?
956
+
957
+ assignment = significant.first
958
+
959
+ return nil unless assignment.is_a?(Herb::AST::ERBContentNode) && assignment.tag_opening&.value == "<%"
960
+
961
+ increment = StateDirectives.fold_increment(assignment.content&.value.to_s)
962
+
963
+ return nil unless increment && states.key?(increment.name)
964
+
965
+ read = StateDirectives.condition_read(condition_expression(node), states, @visitor, condition_anchor(node, condition_expression(node)), resolved_ruby(node))
966
+
967
+ return nil unless read.is_a?(StateDirectives::Read) || read.is_a?(StateDirectives::Combo)
968
+
969
+ register_count(increment, when_read: read, location: node.location)
970
+ StateDirectives.read_names(read).each { |name| rewrite_predicate(node, name) }
971
+
972
+ node
973
+ end
974
+
975
+ #: (untyped) -> void
976
+ def check_state_assignment(node)
977
+ return unless node.is_a?(Herb::AST::ERBContentNode)
978
+
979
+ tag = node.tag_opening&.value
980
+
981
+ return unless ["<%", "<%=", "<%=="].include?(tag)
982
+
983
+ scope = @visitor.current_collection
984
+ states = states_for(scope)
985
+
986
+ return if states.empty?
987
+
988
+ content = node.content&.value.to_s
989
+ assigned = StateDirectives.assigned_state_names(content, states) - @internal_states.keys
990
+
991
+ return if assigned.empty?
992
+
993
+ if tag == "<%" && scope && @visitor.in_item_body?
994
+ increment = StateDirectives.fold_increment(content)
995
+
996
+ if increment && assigned == [increment.name]
997
+ register_count(increment, when_read: nil, location: node.location)
998
+
999
+ return
1000
+ end
1001
+ end
1002
+
1003
+ @visitor.slot_error("`#{content.strip}` assigns the state `#{assigned.first}`. The client never sees a server-side write, so the value it holds would drift from the one the server rendered.", node.location, :assignment, suggestion: "Seed the initial value in the declaration, derive it from other states, count items with `#{assigned.first} += 1` behind a state condition in a keyed loop, or write it at runtime with `data-herb-set` or `state.set`.")
1004
+ end
1005
+
1006
+ #: (StateDirectives::FoldIncrement, when_read: untyped, location: Herb::Location?) -> void
1007
+ def register_count(increment, when_read:, location:)
1008
+ name = increment.name
1009
+ declaration = @region_states[name]
1010
+
1011
+ unless declaration
1012
+ return @visitor.slot_error("`#{name} += #{increment.by}` counts into `#{name}`, which is an item state. A count lives once per region, not once per item.", location, :count, suggestion: "Declare `#{name}` at the top of the template, outside the loop.")
1013
+ end
1014
+
1015
+ if declaration.derived
1016
+ return @visitor.slot_error("`#{name} += #{increment.by}` counts into `#{name}`, which is derived from `#{declaration.default}`. A state is either derived or counted, never both.", location, :count, suggestion: "Drop the derivation from `#{name}`, or count into a second state.")
1017
+ end
1018
+
1019
+ unless declaration.kind == :integer
1020
+ return @visitor.slot_error("`#{name} += #{increment.by}` counts into the #{declaration.kind.to_s.capitalize} state `#{name}`. A count is a number.", location, :count, suggestion: "Declare `#{name}` as an Integer, like `(#{name}: 0)`.")
1021
+ end
1022
+
1023
+ if @state_counts.any? { |count| count[:name] == name }
1024
+ return @visitor.slot_error("`#{name}` is counted twice. One state holds one count.", location, :count, suggestion: "Declare a second state for the second count.")
1025
+ end
1026
+
1027
+ if @visitor.recorded_expressions.any? { |expression| StateDirectives.mentions_any?(expression, { name => declaration }) }
1028
+ return @visitor.slot_error("`#{name}` is read before its count is complete. The server renders that read mid-count and the client cannot keep it current.", location, :count, suggestion: "Move the read below the loop that counts `#{name}`.")
1029
+ end
1030
+
1031
+ @state_counts << { name: name, by: increment.by, collection: @visitor.current_collection, when: when_read }
1032
+ end
1033
+
1034
+ #: () -> void
1035
+ def check_state_count_reads
1036
+ @state_counts.each do |count|
1037
+ @visitor.slot_nodes.each do |node|
1038
+ index = @visitor.index_for(node)
1039
+
1040
+ next unless index
1041
+
1042
+ scope, = @visitor.scope_of(node)
1043
+
1044
+ next unless scope.equal?(count[:collection])
1045
+
1046
+ expression = @visitor.slots[index].expression.to_s
1047
+
1048
+ next if expression.strip.empty?
1049
+
1050
+ mentioned = {} #: Hash[String, untyped]
1051
+ mentioned[count[:name]] = true
1052
+
1053
+ next unless StateDirectives.mentions_any?(expression, mentioned)
1054
+
1055
+ next @visitor.slot_error("`#{count[:name]}` is read inside the loop that counts it. The count is complete only after the loop.", node.location, :count, suggestion: "Move the read below the loop.")
1056
+ end
1057
+ end
1058
+ end
1059
+
1060
+ #: (untyped, String) -> void
1061
+ def rewrite_predicate(node, name)
1062
+ return if rewrite_literal_predicate(node, name)
1063
+
1064
+ token = predicate_token_for(node)
1065
+
1066
+ return unless token
1067
+
1068
+ token.value.replace(StateDirectives.rewrite_reads(token.value, name, @visitor.ruby_at(token.location)))
1069
+ rescue FrozenError
1070
+ nil
1071
+ end
1072
+
1073
+ #: (untyped, String) -> untyped
1074
+ def rewrite_literal_predicate(node, name)
1075
+ return nil unless node.is_a?(Herb::AST::HTMLAttributeNode)
1076
+
1077
+ children = node.value&.children
1078
+
1079
+ return nil unless children.is_a?(Array)
1080
+
1081
+ position = children.index { |child| child.is_a?(Herb::AST::RubyLiteralNode) }
1082
+
1083
+ return nil unless position
1084
+
1085
+ literal = children.fetch(position) #: untyped
1086
+ rewritten = StateDirectives.rewrite_reads(literal.content.to_s, name, @visitor.ruby_at(literal.location))
1087
+ replacement = Herb::AST::RubyLiteralNode.build(content: rewritten, location: literal.location)
1088
+
1089
+ children[position] = replacement
1090
+
1091
+ replacement
1092
+ end
1093
+
1094
+ #: (untyped) -> untyped
1095
+ def predicate_token_for(node)
1096
+ content = node.content if node.respond_to?(:content)
1097
+
1098
+ return content if content.is_a?(Herb::Token)
1099
+ return nil unless node.respond_to?(:value)
1100
+
1101
+ outputs = (node.value&.children || []).grep(Herb::AST::ERBContentNode)
1102
+
1103
+ outputs.one? ? outputs.fetch(0).content : nil
1104
+ end
1105
+
1106
+ #: (untyped) -> Array[untyped]
1107
+ def interpolation_children(node)
1108
+ if node.is_a?(Herb::AST::HTMLElementNode)
1109
+ Array(node.body)
1110
+ elsif node.respond_to?(:value)
1111
+ node.value&.children || []
1112
+ else
1113
+ [] #: Array[untyped]
1114
+ end
1115
+ end
1116
+
1117
+ #: (untyped, Integer, untyped) -> void
1118
+ def register_segment_reads(node, index, slot)
1119
+ segments = interpolation_children(node).select { |child| child.is_a?(Herb::AST::ERBIfNode) || child.is_a?(Herb::AST::ERBUnlessNode) || child.is_a?(Herb::AST::ERBCaseNode) }
1120
+
1121
+ return if segments.empty?
1122
+
1123
+ code = segments.map { |segment| segment_code(segment) }.join(" ")
1124
+ mentioned = @region_states.each_value.select { |declaration| mentioned_state(code, { declaration.name => declaration }) } # steep:ignore UnannotatedEmptyCollection
1125
+
1126
+ return if mentioned.empty?
1127
+
1128
+ entry = { "index" => index, "node_path" => slot.node_path }
1129
+
1130
+ mentioned.each do |declaration|
1131
+ @server_reads[declaration.name] ||= [] # steep:ignore UnannotatedEmptyCollection
1132
+ @server_reads.fetch(declaration.name) << entry
1133
+ end
1134
+ end
1135
+
1136
+ #: (untyped) -> String
1137
+ def segment_code(node)
1138
+ collected = [] #: Array[String]
1139
+
1140
+ gather = lambda do |current|
1141
+ content = current.respond_to?(:content) ? current.content : nil #: untyped
1142
+
1143
+ collected << content.value.to_s if content.respond_to?(:value)
1144
+
1145
+ none = [] #: Array[untyped]
1146
+ children = current.respond_to?(:child_nodes) ? current.child_nodes : none
1147
+
1148
+ children.compact.each { |child| gather.call(child) }
1149
+ end
1150
+
1151
+ gather.call(node)
1152
+
1153
+ collected.join(" ")
1154
+ end
1155
+
1156
+ #: (untyped, Hash[String, StateDirectives::Declaration]) -> void
1157
+ def check_interpolated_state_read(node, states)
1158
+ children = interpolation_children(node)
1159
+
1160
+ outputs = children.grep(Herb::AST::ERBContentNode)
1161
+ read = outputs.map { |output| output.content&.value.to_s.strip }.find { |expression| StateDirectives.mentions_any?(expression, states) }
1162
+
1163
+ return unless read
1164
+
1165
+ if node.is_a?(Herb::AST::HTMLElementNode)
1166
+ @visitor.slot_error("`#{read}` reads a state inside a `<#{node.tag_name&.value}>` that mixes other dynamic parts. A state write cannot supply the other values.", node.location, :read, suggestion: "Give the state its own element, or its own output outside this one.")
1167
+ else
1168
+ @visitor.slot_error("`#{read}` reads a state inside an interpolated attribute that mixes other dynamic parts. A state write cannot supply the other values.", node.location, :read, suggestion: "Give the state its own attribute, or its own output outside this one.")
1169
+ end
1170
+ end
1171
+
1172
+ #: () -> void
1173
+ def check_state_value_reads
1174
+ @visitor.slot_nodes.each do |node|
1175
+ index = @visitor.index_for(node)
1176
+
1177
+ next unless index
1178
+
1179
+ slot = @visitor.slots[index]
1180
+
1181
+ next unless slot.valued? || slot.presence?
1182
+
1183
+ scope, = @visitor.scope_of(node)
1184
+ states = states_for(scope)
1185
+
1186
+ next if states.empty?
1187
+
1188
+ if slot.interpolated? && slot.expression.to_s.strip.empty?
1189
+ check_interpolated_state_read(node, states)
1190
+ register_segment_reads(node, index, slot)
1191
+
1192
+ next
1193
+ end
1194
+
1195
+ expression = slot.expression.to_s.strip
1196
+ expression = predicate_token_for(node)&.value.to_s.strip if expression.empty?
1197
+
1198
+ next if expression.empty?
1199
+ next unless StateDirectives.mentions_any?(expression, states)
1200
+
1201
+ next if refuse_mixed_boolean(node, slot, expression)
1202
+ next if convert_boolean_attribute(node, index, slot, expression, states)
1203
+
1204
+ if states.key?(expression) || states.key?(expression.delete_suffix("?"))
1205
+ rewrite_predicate(node, expression.delete_suffix("?")) if expression.end_with?("?")
1206
+
1207
+ next
1208
+ end
1209
+
1210
+ register_state_value(node, index, slot, expression, states)
1211
+ end
1212
+ end
1213
+
1214
+ #: (untyped, Integer, untyped, String, Hash[String, StateDirectives::Declaration]) -> void
1215
+ def register_state_value(node, index, slot, expression, states)
1216
+ read = slot.valued? ? StateDirectives.condition_read(expression, states, @visitor, condition_anchor(node, expression), resolved_ruby(node)) : nil
1217
+
1218
+ return if read == :reported
1219
+
1220
+ unless read.is_a?(StateDirectives::Read) || read.is_a?(StateDirectives::Combo)
1221
+ return register_server_read(node, index, slot, expression, states)
1222
+ end
1223
+
1224
+ StateDirectives.read_names(read).each { |name| rewrite_predicate(node, name) }
1225
+
1226
+ @state_values[index] = read
1227
+ end
1228
+
1229
+ #: () -> void
1230
+ def check_collection_reads
1231
+ @visitor.slot_nodes.each do |node|
1232
+ index = @visitor.index_for(node)
1233
+
1234
+ next unless index
1235
+
1236
+ slot = @visitor.slots[index]
1237
+
1238
+ next unless COLLECTION_SLOT_TYPES.include?(slot.type)
1239
+
1240
+ expression = (slot.type == :keyed ? slot.key_expression : slot.expression).to_s.strip
1241
+
1242
+ next if expression.empty?
1243
+
1244
+ mentioned = @region_states.each_value.select { |declaration| mentioned_state(expression, { declaration.name => declaration }) } # steep:ignore UnannotatedEmptyCollection
1245
+
1246
+ next if mentioned.empty?
1247
+
1248
+ entry = { "index" => index, "node_path" => slot.node_path }
1249
+
1250
+ mentioned.each do |declaration|
1251
+ @server_reads[declaration.name] ||= [] # steep:ignore UnannotatedEmptyCollection
1252
+ @server_reads.fetch(declaration.name) << entry
1253
+ end
1254
+ end
1255
+ end
1256
+
1257
+ #: (untyped, Integer, untyped, String, Hash[String, StateDirectives::Declaration]) -> nil
1258
+ def register_server_read(node, index, slot, expression, states)
1259
+ mentioned = @region_states.each_value.select { |declaration| mentioned_state(expression, { declaration.name => declaration }) } # steep:ignore UnannotatedEmptyCollection
1260
+
1261
+ if mentioned.empty?
1262
+ return @visitor.slot_error("`#{expression}` computes with the state `#{mentioned_state(expression, states)&.name}`, which lives on an item. The server cannot be asked for an item's answer yet.", condition_anchor(node, expression).location, :read, suggestion: "Show the value with `<%= #{mentioned_state(expression, states)&.name} %>`, or declare a second state for the computed answer and set it from app code.")
1263
+ end
1264
+
1265
+ entry = { "index" => index, "node_path" => slot.node_path }
1266
+
1267
+ mentioned.each do |declaration|
1268
+ @server_reads[declaration.name] ||= [] # steep:ignore UnannotatedEmptyCollection
1269
+ @server_reads.fetch(declaration.name) << entry
1270
+ end
1271
+
1272
+ nil
1273
+ end
1274
+
1275
+ #: (untyped, untyped, String) -> Symbol?
1276
+ def refuse_mixed_boolean(node, slot, expression)
1277
+ return nil unless slot.type == :attribute_interpolation
1278
+ return nil unless slot.attribute && Herb::HTML::Util.boolean_attribute?(slot.attribute)
1279
+
1280
+ @visitor.slot_error(
1281
+ "`#{slot.attribute}` on this `<#{slot.tag}>` mixes `#{expression}` with other text. A boolean attribute follows presence and any value keeps it present, so it could never turn off.",
1282
+ node.location,
1283
+ :read,
1284
+ suggestion: "Make `<%= #{expression} %>` the whole value of `#{slot.attribute}`."
1285
+ )
1286
+
1287
+ :reported
1288
+ end
1289
+
1290
+ #: (untyped, Integer, untyped, String, Hash[String, StateDirectives::Declaration]) -> untyped
1291
+ def convert_boolean_attribute(node, index, slot, expression, states)
1292
+ return nil unless slot.type == :attribute || (slot.type == :boolean_attribute && !@state_presence.key?(index))
1293
+ return nil unless slot.attribute && Herb::HTML::Util.boolean_attribute?(slot.attribute)
1294
+
1295
+ read = StateDirectives.condition_read(expression, states, @visitor, condition_anchor(node, expression), resolved_ruby(node))
1296
+
1297
+ return :reported if read == :reported
1298
+
1299
+ return nil unless read.is_a?(StateDirectives::Read) || read.is_a?(StateDirectives::Combo)
1300
+
1301
+ if read.is_a?(StateDirectives::Read) && read.comparand.nil? && read.against.nil? && read.operator.nil? && !StateKinds::FALSY.include?(read.kind)
1302
+ @visitor.slot_error("`#{slot.attribute}=\"<%= #{expression} %>\"` reads #{StateDirectives.subject_phrase(read)} as a presence. Only `nil` and `false` are falsy in Ruby, so the attribute could never turn off.", condition_anchor(node, expression).location, :read, suggestion: "#{StateDirectives.predicate_advice(read.kind, read.name)}compare it to a literal#{StateDirectives.default_example(states.fetch(read.name), "#{read.name} == ")}, or declare it as a boolean.")
1303
+
1304
+ return :reported
1305
+ end
1306
+
1307
+ open_tag = @visitor.open_tag_for(node)
1308
+ children = open_tag&.children
1309
+ position = children&.find_index { |child| child.equal?(node) }
1310
+
1311
+ return nil unless position
1312
+
1313
+ condition = StateDirectives.condition_source(read)
1314
+ spacing = open_tag.is_a?(Herb::AST::ERBOpenTagNode) ? " " : ""
1315
+ replacement = @visitor.erb_output_node(%[("#{spacing}#{slot.attribute}" if #{condition})])
1316
+
1317
+ children[position] = replacement
1318
+
1319
+ @visitor.replace_slot(index, slot.with(type: :boolean_attribute))
1320
+ @visitor.assign_index(replacement, index)
1321
+ @state_presence[index] = read
1322
+ end
1323
+ end
1324
+ end
1325
+ end
1326
+ end