herb 0.10.4 → 0.11.0

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 (784) 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/action_view/helper_registry.rb +1358 -62
  297. data/lib/herb/analysis/partial_declaration.rb +140 -0
  298. data/lib/herb/analysis/partial_index.rb +189 -0
  299. data/lib/herb/analysis/partial_resolution.rb +161 -0
  300. data/lib/herb/analysis/partial_resolver.rb +144 -0
  301. data/lib/herb/analysis/project_index.rb +152 -0
  302. data/lib/herb/{action_view → analysis}/render_analyzer.rb +287 -77
  303. data/lib/herb/analysis/render_graph/ancestor_chain.rb +27 -0
  304. data/lib/herb/analysis/render_graph/builder.rb +376 -0
  305. data/lib/herb/analysis/render_graph/call_frame.rb +9 -0
  306. data/lib/herb/analysis/render_graph/call_site_location.rb +23 -0
  307. data/lib/herb/analysis/render_graph/inferred_signature.rb +21 -0
  308. data/lib/herb/analysis/render_graph/partial_call_site.rb +40 -0
  309. data/lib/herb/analysis/render_graph/partial_context.rb +46 -0
  310. data/lib/herb/analysis/render_graph/template_roots.rb +31 -0
  311. data/lib/herb/analysis/render_graph.rb +285 -0
  312. data/lib/herb/analysis/ruby_locals_index/local.rb +25 -0
  313. data/lib/herb/analysis/ruby_locals_index/named_reference.rb +22 -0
  314. data/lib/herb/analysis/ruby_locals_index/offset_table.rb +44 -0
  315. data/lib/herb/analysis/ruby_locals_index/reference_collector.rb +80 -0
  316. data/lib/herb/analysis/ruby_locals_index/state_locals.rb +49 -0
  317. data/lib/herb/analysis/ruby_locals_index.rb +165 -0
  318. data/lib/herb/analysis/ruby_reference.rb +67 -0
  319. data/lib/herb/analysis/template_dependencies/dependency_collector.rb +179 -0
  320. data/lib/herb/analysis/template_dependencies/node_dependency_collector.rb +309 -0
  321. data/lib/herb/analysis/template_dependencies.rb +562 -0
  322. data/lib/herb/ast/erb_render_node.rb +26 -94
  323. data/lib/herb/ast/helpers.rb +90 -10
  324. data/lib/herb/ast/herb_directive_node.rb +18 -0
  325. data/lib/herb/ast/node.rb +32 -2
  326. data/lib/herb/ast/nodes.rb +1342 -105
  327. data/lib/herb/backend.rb +73 -0
  328. data/lib/herb/bootstrap.rb +3 -7
  329. data/lib/herb/cli.rb +550 -32
  330. data/lib/herb/colors.rb +14 -0
  331. data/lib/herb/configuration.rb +71 -35
  332. data/lib/herb/defaults.yml +2 -1
  333. data/lib/herb/dev/assets.rb +57 -0
  334. data/lib/herb/dev/classifier.rb +84 -0
  335. data/lib/herb/dev/embedded.rb +27 -0
  336. data/lib/herb/dev/pipeline.rb +305 -0
  337. data/lib/herb/dev/protocol.rb +221 -0
  338. data/lib/herb/dev/runner.rb +175 -175
  339. data/lib/herb/dev/server.rb +89 -26
  340. data/lib/herb/dev/server_entry.rb +41 -6
  341. data/lib/herb/dev/watcher.rb +181 -0
  342. data/lib/herb/dev.rb +163 -0
  343. data/lib/herb/diagnostic/formatter.rb +88 -0
  344. data/lib/herb/diagnostic.rb +172 -0
  345. data/lib/herb/diff/operation.rb +36 -0
  346. data/lib/herb/diff/result.rb +62 -0
  347. data/lib/herb/diff.rb +10 -0
  348. data/lib/herb/engine/collector.rb +62 -0
  349. data/lib/herb/engine/compiler.rb +346 -108
  350. data/lib/herb/engine/component_tags/constant_resolver.rb +28 -0
  351. data/lib/herb/engine/component_tags/partial_resolver.rb +54 -0
  352. data/lib/herb/engine/component_tags/resolver.rb +32 -0
  353. data/lib/herb/engine/component_tags/visitor.rb +231 -0
  354. data/lib/herb/engine/css_inliner/inliner.rb +101 -0
  355. data/lib/herb/engine/css_inliner/visitor.rb +174 -0
  356. data/lib/herb/engine/errors.rb +122 -0
  357. data/lib/herb/engine/helpers.rb +82 -0
  358. data/lib/herb/engine/inline_render/inliner.rb +122 -0
  359. data/lib/herb/engine/inline_render/visitor.rb +267 -0
  360. data/lib/herb/engine/runtime/entry.rb +62 -0
  361. data/lib/herb/engine/runtime/error_page.rb +362 -0
  362. data/lib/herb/engine/runtime/html_safe_assertions.rb +217 -0
  363. data/lib/herb/engine/runtime/journal/summary.rb +227 -0
  364. data/lib/herb/engine/runtime/journal.rb +379 -0
  365. data/lib/herb/engine/runtime/middleware.rb +212 -0
  366. data/lib/herb/engine/runtime/observations.rb +73 -0
  367. data/lib/herb/engine/runtime/report.rb +204 -0
  368. data/lib/herb/engine/runtime/session.rb +437 -0
  369. data/lib/herb/engine/runtime.rb +21 -0
  370. data/lib/herb/engine/scoped_style/channel.rb +66 -0
  371. data/lib/herb/engine/scoped_style/collector.rb +74 -0
  372. data/lib/herb/engine/scoped_style/visitor.rb +533 -0
  373. data/lib/herb/engine/slots/annotation.rb +98 -0
  374. data/lib/herb/engine/slots/bindings.rb +61 -0
  375. data/lib/herb/engine/slots/callee.rb +118 -0
  376. data/lib/herb/engine/slots/component_definitions.rb +67 -0
  377. data/lib/herb/engine/slots/components/async.rb +24 -0
  378. data/lib/herb/engine/slots/components/base.rb +132 -0
  379. data/lib/herb/engine/slots/components/deferred.rb +81 -0
  380. data/lib/herb/engine/slots/components/fallback.rb +27 -0
  381. data/lib/herb/engine/slots/components/fragment.rb +99 -0
  382. data/lib/herb/engine/slots/components/lazy.rb +24 -0
  383. data/lib/herb/engine/slots/components.rb +52 -0
  384. data/lib/herb/engine/slots/dependencies/channel.rb +72 -0
  385. data/lib/herb/engine/slots/dependencies.rb +269 -0
  386. data/lib/herb/engine/slots/dynamics_compiler.rb +854 -0
  387. data/lib/herb/engine/slots/identifier.rb +35 -0
  388. data/lib/herb/engine/slots/manifest/channel.rb +74 -0
  389. data/lib/herb/engine/slots/manifest/collector.rb +75 -0
  390. data/lib/herb/engine/slots/markers.rb +128 -0
  391. data/lib/herb/engine/slots/render_bindings.rb +141 -0
  392. data/lib/herb/engine/slots/schema_compiler.rb +94 -0
  393. data/lib/herb/engine/slots/state_anchor.rb +94 -0
  394. data/lib/herb/engine/slots/state_compiler.rb +1326 -0
  395. data/lib/herb/engine/slots/state_directives.rb +854 -0
  396. data/lib/herb/engine/slots/state_kinds.rb +54 -0
  397. data/lib/herb/engine/slots/state_operators.rb +36 -0
  398. data/lib/herb/engine/slots/state_overrides.rb +79 -0
  399. data/lib/herb/engine/slots/state_predicates.rb +65 -0
  400. data/lib/herb/engine/slots/state_transforms.rb +42 -0
  401. data/lib/herb/engine/slots/statics.rb +185 -0
  402. data/lib/herb/engine/slots/subtree.rb +82 -0
  403. data/lib/herb/engine/slots/subtree_compiler.rb +255 -0
  404. data/lib/herb/engine/slots/types.rb +42 -0
  405. data/lib/herb/engine/slots/visitor.rb +2256 -0
  406. data/lib/herb/engine/validators/accessibility_validator.rb +3 -3
  407. data/lib/herb/engine/validators/base.rb +36 -0
  408. data/lib/herb/engine/validators/generator_template_validator.rb +39 -0
  409. data/lib/herb/engine/validators/nesting_validator.rb +3 -3
  410. data/lib/herb/engine/validators/render_validator.rb +13 -33
  411. data/lib/herb/engine/validators/security_validator.rb +3 -17
  412. data/lib/herb/engine/validators.rb +43 -0
  413. data/lib/herb/engine/visitors/auto_close_omitted_tags_visitor.rb +48 -0
  414. data/lib/herb/engine/visitors/content_for_visitor.rb +127 -0
  415. data/lib/herb/engine/{debug_visitor.rb → visitors/debug_visitor.rb} +121 -93
  416. data/lib/herb/engine/visitors/html_safe_assertions_visitor.rb +281 -0
  417. data/lib/herb/engine/visitors/instrumentation_visitor.rb +290 -0
  418. data/lib/herb/engine/visitors/optimize_visitor.rb +728 -0
  419. data/lib/herb/engine/visitors/remove_comments_visitor.rb +106 -0
  420. data/lib/herb/engine/visitors/source_attribution_visitor.rb +177 -0
  421. data/lib/herb/engine.rb +218 -257
  422. data/lib/herb/ensure_installed.rb +43 -0
  423. data/lib/herb/errors.rb +646 -157
  424. data/lib/herb/fingerprint.rb +82 -0
  425. data/lib/herb/html/elements.rb +18 -0
  426. data/lib/herb/html/util.rb +21 -2
  427. data/lib/herb/lex_result.rb +0 -10
  428. data/lib/herb/locate.rb +163 -0
  429. data/lib/herb/location.rb +20 -0
  430. data/lib/herb/parse_result.rb +17 -11
  431. data/lib/herb/parser_options.rb +65 -46
  432. data/lib/herb/position.rb +19 -0
  433. data/lib/herb/prism_inspect.rb +1 -4
  434. data/lib/herb/project.rb +28 -15
  435. data/lib/herb/range.rb +5 -0
  436. data/lib/herb/result.rb +2 -2
  437. data/lib/herb/ruby_program.rb +110 -0
  438. data/lib/herb/source_path.rb +232 -0
  439. data/lib/herb/token.rb +14 -3
  440. data/lib/herb/version.rb +1 -1
  441. data/lib/herb/visitor/context/origin.rb +52 -0
  442. data/lib/herb/visitor/context/replacements.rb +59 -0
  443. data/lib/herb/visitor/context.rb +169 -0
  444. data/lib/herb/visitor/context_aware.rb +45 -0
  445. data/lib/herb/visitor/diagnostics.rb +148 -0
  446. data/lib/herb/visitor/experimental.rb +33 -0
  447. data/lib/herb/visitor/parser_option_requirements.rb +151 -0
  448. data/lib/herb/visitor/stack.rb +192 -0
  449. data/lib/herb/visitor.rb +47 -1
  450. data/lib/herb/warnings.rb +25 -0
  451. data/lib/herb.rb +74 -63
  452. data/sig/herb/action_view/helper_registry.rbs +82 -10
  453. data/sig/herb/analysis/partial_declaration.rbs +80 -0
  454. data/sig/herb/analysis/partial_index.rbs +66 -0
  455. data/sig/herb/analysis/partial_resolution.rbs +57 -0
  456. data/sig/herb/analysis/partial_resolver.rbs +76 -0
  457. data/sig/herb/analysis/project_index.rbs +57 -0
  458. data/sig/herb/{action_view → analysis}/render_analyzer.rbs +25 -4
  459. data/sig/herb/analysis/render_graph/ancestor_chain.rbs +22 -0
  460. data/sig/herb/analysis/render_graph/builder.rbs +180 -0
  461. data/sig/herb/analysis/render_graph/call_frame.rbs +26 -0
  462. data/sig/herb/analysis/render_graph/call_site_location.rbs +28 -0
  463. data/sig/herb/analysis/render_graph/inferred_signature.rbs +27 -0
  464. data/sig/herb/analysis/render_graph/partial_call_site.rbs +36 -0
  465. data/sig/herb/analysis/render_graph/partial_context.rbs +31 -0
  466. data/sig/herb/analysis/render_graph/template_roots.rbs +32 -0
  467. data/sig/herb/analysis/render_graph.rbs +83 -0
  468. data/sig/herb/analysis/ruby_locals_index/local.rbs +21 -0
  469. data/sig/herb/analysis/ruby_locals_index/named_reference.rbs +20 -0
  470. data/sig/herb/analysis/ruby_locals_index/offset_table.rbs +26 -0
  471. data/sig/herb/analysis/ruby_locals_index/reference_collector.rbs +40 -0
  472. data/sig/herb/analysis/ruby_locals_index/state_locals.rbs +19 -0
  473. data/sig/herb/analysis/ruby_locals_index.rbs +62 -0
  474. data/sig/herb/analysis/ruby_reference.rbs +28 -0
  475. data/sig/herb/analysis/template_dependencies/dependency_collector.rbs +52 -0
  476. data/sig/herb/analysis/template_dependencies/node_dependency_collector.rbs +81 -0
  477. data/sig/herb/analysis/template_dependencies.rbs +125 -0
  478. data/sig/herb/ast/erb_render_node.rbs +10 -2
  479. data/sig/herb/ast/helpers.rbs +31 -1
  480. data/sig/herb/ast/herb_directive_node.rbs +13 -0
  481. data/sig/herb/ast/node.rbs +9 -0
  482. data/sig/herb/ast/nodes.rbs +662 -89
  483. data/sig/herb/backend.rbs +52 -0
  484. data/sig/herb/colors.rbs +3 -0
  485. data/sig/herb/configuration.rbs +27 -3
  486. data/sig/herb/dev/assets.rbs +27 -0
  487. data/sig/herb/dev/classifier.rbs +57 -0
  488. data/sig/herb/dev/embedded.rbs +21 -0
  489. data/sig/herb/dev/pipeline.rbs +118 -0
  490. data/sig/herb/dev/protocol.rbs +78 -0
  491. data/sig/herb/dev/runner.rbs +61 -23
  492. data/sig/herb/dev/server.rbs +49 -22
  493. data/sig/herb/dev/server_entry.rbs +46 -21
  494. data/sig/herb/dev/watcher.rbs +84 -0
  495. data/sig/herb/dev.rbs +58 -0
  496. data/sig/herb/diagnostic/formatter.rbs +28 -0
  497. data/sig/herb/diagnostic.rbs +88 -0
  498. data/sig/herb/diff/operation.rbs +36 -0
  499. data/sig/herb/diff/result.rbs +36 -0
  500. data/sig/herb/diff.rbs +6 -0
  501. data/sig/herb/engine/collector.rbs +48 -0
  502. data/sig/herb/engine/compiler.rbs +62 -23
  503. data/sig/herb/engine/component_tags/constant_resolver.rbs +19 -0
  504. data/sig/herb/engine/component_tags/partial_resolver.rbs +30 -0
  505. data/sig/herb/engine/component_tags/resolver.rbs +25 -0
  506. data/sig/herb/engine/component_tags/visitor.rbs +69 -0
  507. data/sig/herb/engine/css_inliner/inliner.rbs +65 -0
  508. data/sig/herb/engine/css_inliner/visitor.rbs +87 -0
  509. data/sig/herb/engine/errors.rbs +68 -0
  510. data/sig/herb/engine/helpers.rbs +28 -0
  511. data/sig/herb/engine/inline_render/inliner.rbs +39 -0
  512. data/sig/herb/engine/inline_render/visitor.rbs +104 -0
  513. data/sig/herb/engine/runtime/entry.rbs +43 -0
  514. data/sig/herb/engine/runtime/error_page.rbs +115 -0
  515. data/sig/herb/engine/runtime/html_safe_assertions.rbs +99 -0
  516. data/sig/herb/engine/runtime/journal/summary.rbs +172 -0
  517. data/sig/herb/engine/runtime/journal.rbs +126 -0
  518. data/sig/herb/engine/runtime/middleware.rbs +96 -0
  519. data/sig/herb/engine/runtime/observations.rbs +38 -0
  520. data/sig/herb/engine/runtime/report.rbs +97 -0
  521. data/sig/herb/engine/runtime/session.rbs +233 -0
  522. data/sig/herb/engine/runtime.rbs +14 -0
  523. data/sig/herb/engine/scoped_style/channel.rbs +45 -0
  524. data/sig/herb/engine/scoped_style/collector.rbs +45 -0
  525. data/sig/herb/engine/scoped_style/visitor.rbs +220 -0
  526. data/sig/herb/engine/slots/annotation.rbs +72 -0
  527. data/sig/herb/engine/slots/bindings.rbs +34 -0
  528. data/sig/herb/engine/slots/callee.rbs +62 -0
  529. data/sig/herb/engine/slots/component_definitions.rbs +19 -0
  530. data/sig/herb/engine/slots/components/async.rbs +20 -0
  531. data/sig/herb/engine/slots/components/base.rbs +60 -0
  532. data/sig/herb/engine/slots/components/deferred.rbs +29 -0
  533. data/sig/herb/engine/slots/components/fallback.rbs +21 -0
  534. data/sig/herb/engine/slots/components/fragment.rbs +34 -0
  535. data/sig/herb/engine/slots/components/lazy.rbs +20 -0
  536. data/sig/herb/engine/slots/components.rbs +30 -0
  537. data/sig/herb/engine/slots/dependencies/channel.rbs +51 -0
  538. data/sig/herb/engine/slots/dependencies.rbs +94 -0
  539. data/sig/herb/engine/slots/dynamics_compiler.rbs +349 -0
  540. data/sig/herb/engine/slots/identifier.rbs +19 -0
  541. data/sig/herb/engine/slots/manifest/channel.rbs +49 -0
  542. data/sig/herb/engine/slots/manifest/collector.rbs +45 -0
  543. data/sig/herb/engine/slots/markers.rbs +85 -0
  544. data/sig/herb/engine/slots/render_bindings.rbs +69 -0
  545. data/sig/herb/engine/slots/schema_compiler.rbs +62 -0
  546. data/sig/herb/engine/slots/state_anchor.rbs +41 -0
  547. data/sig/herb/engine/slots/state_compiler.rbs +258 -0
  548. data/sig/herb/engine/slots/state_directives.rbs +278 -0
  549. data/sig/herb/engine/slots/state_kinds.rbs +24 -0
  550. data/sig/herb/engine/slots/state_operators.rbs +19 -0
  551. data/sig/herb/engine/slots/state_overrides.rbs +33 -0
  552. data/sig/herb/engine/slots/state_predicates.rbs +16 -0
  553. data/sig/herb/engine/slots/state_transforms.rbs +15 -0
  554. data/sig/herb/engine/slots/statics.rbs +67 -0
  555. data/sig/herb/engine/slots/subtree.rbs +49 -0
  556. data/sig/herb/engine/slots/subtree_compiler.rbs +130 -0
  557. data/sig/herb/engine/slots/types.rbs +46 -0
  558. data/sig/herb/engine/slots/visitor.rbs +599 -0
  559. data/sig/herb/engine/validators/accessibility_validator.rbs +1 -1
  560. data/sig/herb/engine/validators/base.rbs +28 -0
  561. data/sig/herb/engine/validators/generator_template_validator.rbs +19 -0
  562. data/sig/herb/engine/validators/nesting_validator.rbs +1 -1
  563. data/sig/herb/engine/validators/render_validator.rbs +2 -6
  564. data/sig/herb/engine/validators/security_validator.rbs +1 -3
  565. data/sig/herb/engine/validators.rbs +21 -0
  566. data/sig/herb/engine/visitors/auto_close_omitted_tags_visitor.rbs +26 -0
  567. data/sig/herb/engine/visitors/content_for_visitor.rbs +52 -0
  568. data/sig/herb/engine/{debug_visitor.rbs → visitors/debug_visitor.rbs} +26 -15
  569. data/sig/herb/engine/visitors/html_safe_assertions_visitor.rbs +100 -0
  570. data/sig/herb/engine/visitors/instrumentation_visitor.rbs +111 -0
  571. data/sig/herb/engine/visitors/optimize_visitor.rbs +246 -0
  572. data/sig/herb/engine/visitors/remove_comments_visitor.rbs +66 -0
  573. data/sig/herb/engine/visitors/source_attribution_visitor.rbs +90 -0
  574. data/sig/herb/engine.rbs +47 -41
  575. data/sig/herb/ensure_installed.rbs +9 -0
  576. data/sig/herb/errors.rbs +293 -0
  577. data/sig/herb/fingerprint.rbs +37 -0
  578. data/sig/herb/html/elements.rbs +19 -0
  579. data/sig/herb/html/util.rbs +15 -1
  580. data/sig/herb/lex_result.rbs +0 -6
  581. data/sig/herb/locate.rbs +89 -0
  582. data/sig/herb/location.rbs +12 -0
  583. data/sig/herb/parse_result.rbs +10 -8
  584. data/sig/herb/parser_options.rbs +47 -21
  585. data/sig/herb/position.rbs +11 -0
  586. data/sig/herb/range.rbs +3 -0
  587. data/sig/herb/ruby_program.rbs +51 -0
  588. data/sig/herb/source_path.rbs +132 -0
  589. data/sig/herb/token.rbs +5 -2
  590. data/sig/herb/visitor/context/origin.rbs +51 -0
  591. data/sig/herb/visitor/context/replacements.rbs +39 -0
  592. data/sig/herb/visitor/context.rbs +83 -0
  593. data/sig/herb/visitor/context_aware.rbs +32 -0
  594. data/sig/herb/visitor/diagnostics.rbs +82 -0
  595. data/sig/herb/visitor/experimental.rbs +23 -0
  596. data/sig/herb/visitor/parser_option_requirements.rbs +76 -0
  597. data/sig/herb/visitor/stack.rbs +91 -0
  598. data/sig/herb/visitor.rbs +22 -0
  599. data/sig/herb/warnings.rbs +10 -0
  600. data/sig/herb.rbs +27 -10
  601. data/sig/serialized_ast_errors.rbs +58 -1
  602. data/sig/serialized_ast_nodes.rbs +58 -2
  603. data/src/analyze/action_view/attribute_extraction_helpers.c +73 -36
  604. data/src/analyze/action_view/generated_handlers.c +37 -1
  605. data/src/analyze/action_view/generated_handlers.h +2 -1
  606. data/src/analyze/action_view/helper_registry.c +1353 -58
  607. data/src/analyze/action_view/registry.c +10 -17
  608. data/src/analyze/action_view/stylesheet_link_tag.c +40 -0
  609. data/src/analyze/action_view/tag_helper_node_builders.c +57 -0
  610. data/src/analyze/action_view/tag_helpers.c +423 -93
  611. data/src/analyze/analyze.c +301 -64
  612. data/src/analyze/analyze_helpers.c +420 -23
  613. data/src/analyze/builders.c +79 -51
  614. data/src/analyze/conditional_elements.c +13 -8
  615. data/src/analyze/conditional_open_tags.c +11 -5
  616. data/src/analyze/control_type.c +16 -1
  617. data/src/analyze/herb_directives.c +701 -0
  618. data/src/analyze/invalid_structures.c +11 -7
  619. data/src/analyze/iteration_nodes.c +306 -0
  620. data/src/analyze/missing_end.c +116 -32
  621. data/src/analyze/parse_errors.c +8 -3
  622. data/src/analyze/postfix_conditionals.c +98 -63
  623. data/src/analyze/prism_annotate.c +13 -4
  624. data/src/analyze/render_nodes.c +345 -123
  625. data/src/analyze/strict_locals.c +30 -26
  626. data/src/analyze/ternary_conditionals.c +95 -70
  627. data/src/analyze/transform.c +43 -1
  628. data/src/ast/ast_node.c +3 -13
  629. data/src/ast/ast_nodes.c +911 -112
  630. data/src/ast/ast_pretty_print.c +161 -2
  631. data/src/diff/herb_diff.c +8 -0
  632. data/src/diff/herb_diff_attributes.c +1 -0
  633. data/src/diff/herb_diff_children.c +4 -3
  634. data/src/diff/herb_diff_helpers.c +7 -1
  635. data/src/diff/herb_diff_nodes.c +186 -71
  636. data/src/diff/herb_hash_tree.c +99 -1
  637. data/src/errors.c +1096 -128
  638. data/src/extract.c +30 -8
  639. data/src/herb.c +30 -11
  640. data/src/include/analyze/action_view/attribute_extraction_helpers.h +1 -1
  641. data/src/include/analyze/action_view/helper_registry.h +28 -4
  642. data/src/include/analyze/action_view/tag_helper_handler.h +1 -1
  643. data/src/include/analyze/action_view/tag_helper_node_builders.h +7 -0
  644. data/src/include/analyze/analyze.h +11 -0
  645. data/src/include/analyze/builders.h +16 -0
  646. data/src/include/analyze/helpers.h +23 -2
  647. data/src/include/analyze/herb_directive_kinds.h +41 -0
  648. data/src/include/analyze/herb_directives.h +14 -0
  649. data/src/include/analyze/iteration_nodes.h +11 -0
  650. data/src/include/analyze/prism_annotate.h +3 -3
  651. data/src/include/analyze/ternary_conditionals.h +2 -0
  652. data/src/include/ast/ast_node.h +1 -5
  653. data/src/include/ast/ast_nodes.h +76 -2
  654. data/src/include/ast/ast_pretty_print.h +1 -1
  655. data/src/include/diff/herb_diff.h +11 -0
  656. data/src/include/errors.h +156 -39
  657. data/src/include/extract.h +11 -0
  658. data/src/include/herb.h +6 -0
  659. data/src/include/lexer/erb_openers.h +29 -0
  660. data/src/include/lexer/lexer_peek_helpers.h +8 -1
  661. data/src/include/lexer/lexer_struct.h +4 -0
  662. data/src/include/lexer/token.h +3 -0
  663. data/src/include/lexer/token_struct.h +8 -6
  664. data/src/include/lib/hb_allocator.h +2 -1
  665. data/src/include/lib/hb_array.h +2 -0
  666. data/src/include/lib/hb_foreach.h +1 -1
  667. data/src/include/lib/hb_string.h +7 -0
  668. data/src/include/parser/dot_notation.h +1 -1
  669. data/src/include/parser/foreign_content_elements.h +32 -0
  670. data/src/include/parser/parser.h +16 -30
  671. data/src/include/parser/parser_helpers.h +13 -11
  672. data/src/include/parser/parser_options.h +41 -0
  673. data/src/include/prism/ruby_parser.h +6 -0
  674. data/src/include/util/html_elements.h +85 -0
  675. data/src/include/util/html_util.h +1 -0
  676. data/src/include/version.h +1 -1
  677. data/src/lexer/lexer_peek_helpers.c +15 -1
  678. data/src/lexer/token.c +83 -1
  679. data/src/lexer.c +196 -12
  680. data/src/lib/hb_allocator.c +2 -0
  681. data/src/lib/hb_array.c +26 -0
  682. data/src/lib/hb_string.c +26 -0
  683. data/src/main.c +1 -1
  684. data/src/parser/dot_notation.c +4 -3
  685. data/src/parser/match_tags.c +66 -1
  686. data/src/parser/parser_helpers.c +69 -37
  687. data/src/parser.c +606 -383
  688. data/src/parser_options_defaults.c +30 -0
  689. data/src/prism/prism_helpers.c +3 -0
  690. data/src/prism/ruby_parser.c +49 -0
  691. data/src/util/html_util.c +11 -58
  692. data/src/visitor.c +77 -1
  693. data/templates/ext/herb/error_helpers.c.erb +1 -1
  694. data/templates/ext/herb/nodes.c.erb +18 -13
  695. data/templates/ext/herb/nodes.h.erb +2 -1
  696. data/templates/ext/herb/parser_options_helpers.c.erb +69 -0
  697. data/templates/ext/herb/parser_options_helpers.h.erb +11 -0
  698. data/templates/java/nodes.c.erb +20 -16
  699. data/templates/java/nodes.h.erb +3 -2
  700. data/templates/java/org/herb/ParserOptions.java.erb +24 -0
  701. data/templates/java/org/herb/ast/HelperRegistry.java.erb +67 -31
  702. data/templates/java/org/herb/ast/HelperType.java.erb +29 -0
  703. data/templates/java/org/herb/ast/Nodes.java.erb +11 -7
  704. data/templates/java/parser_options_helpers.c.erb +45 -0
  705. data/templates/java/parser_options_helpers.h.erb +10 -0
  706. data/templates/javascript/packages/client/src/state-kinds.ts.erb +49 -0
  707. data/templates/javascript/packages/client/src/state-operators.ts.erb +15 -0
  708. data/templates/javascript/packages/client/src/state-predicates.ts.erb +27 -0
  709. data/templates/javascript/packages/client/src/state-transforms.ts.erb +19 -0
  710. data/templates/javascript/packages/core/src/action-view-helpers.ts.erb +47 -0
  711. data/templates/javascript/packages/core/src/built-in-components.ts.erb +67 -0
  712. data/templates/javascript/packages/core/src/errors.ts.erb +9 -3
  713. data/templates/javascript/packages/core/src/foreign-content-elements.ts.erb +18 -0
  714. data/templates/javascript/packages/core/src/html-constants.ts.erb +32 -0
  715. data/templates/javascript/packages/core/src/html-elements.ts.erb +41 -0
  716. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +11 -7
  717. data/templates/javascript/packages/core/src/nodes.ts.erb +82 -19
  718. data/templates/javascript/packages/core/src/parser-options.ts.erb +41 -0
  719. data/templates/javascript/packages/core/src/visitor.ts.erb +1 -1
  720. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +3 -3
  721. data/templates/javascript/packages/node/extension/nodes.cpp.erb +17 -12
  722. data/templates/javascript/packages/node/extension/nodes.h.erb +3 -3
  723. data/templates/javascript/packages/node/extension/parser_options_helpers.cpp.erb +41 -0
  724. data/templates/javascript/packages/node/extension/parser_options_helpers.h.erb +12 -0
  725. data/templates/lib/herb/action_view/helper_registry.rb.erb +77 -3
  726. data/templates/lib/herb/ast/nodes.rb.erb +16 -6
  727. data/templates/lib/herb/engine/slots/component_definitions.rb.erb +38 -0
  728. data/templates/lib/herb/engine/slots/state_kinds.rb.erb +37 -0
  729. data/templates/lib/herb/engine/slots/state_operators.rb.erb +26 -0
  730. data/templates/lib/herb/engine/slots/state_predicates.rb.erb +39 -0
  731. data/templates/lib/herb/engine/slots/state_transforms.rb.erb +27 -0
  732. data/templates/lib/herb/errors.rb.erb +34 -6
  733. data/templates/lib/herb/html/elements.rb.erb +12 -0
  734. data/templates/lib/herb/parser_options.rb.erb +32 -0
  735. data/templates/lib/herb/visitor.rb.erb +7 -1
  736. data/templates/rust/herb-printer/src/printer_visitor.rs.erb +14 -0
  737. data/templates/rust/src/action_view_helpers.rs.erb +38 -0
  738. data/templates/rust/src/ast/nodes.rs.erb +24 -31
  739. data/templates/rust/src/errors.rs.erb +1 -1
  740. data/templates/rust/src/nodes.rs.erb +58 -26
  741. data/templates/rust/src/parser_options.rs.erb +16 -0
  742. data/templates/rust/src/prism/generated_deserialize.rs.erb +109 -0
  743. data/templates/rust/src/prism/generated_prettyprint.rs.erb +153 -0
  744. data/templates/rust/src/union_types.rs.erb +7 -1
  745. data/templates/rust/src/visitor.rs.erb +1 -1
  746. data/templates/src/analyze/action_view/helper_registry.c.erb +22 -0
  747. data/templates/src/analyze/missing_end.c.erb +13 -5
  748. data/templates/src/ast/ast_nodes.c.erb +41 -2
  749. data/templates/src/diff/herb_diff_nodes.c.erb +44 -8
  750. data/templates/src/errors.c.erb +8 -3
  751. data/templates/src/include/analyze/action_view/helper_registry.h.erb +19 -0
  752. data/templates/src/include/analyze/herb_directive_kinds.h.erb +25 -0
  753. data/templates/src/include/errors.h.erb +3 -1
  754. data/templates/src/include/parser/foreign_content_elements.h.erb +23 -0
  755. data/templates/src/include/parser/parser_options.h.erb +21 -0
  756. data/templates/src/include/util/html_elements.h.erb +28 -0
  757. data/templates/src/parser_options_defaults.c.erb +9 -0
  758. data/templates/template.rb +564 -9
  759. data/templates/wasm/error_helpers.cpp.erb +4 -4
  760. data/templates/wasm/nodes.cpp.erb +12 -12
  761. data/templates/wasm/nodes.h.erb +3 -3
  762. data/templates/wasm/parser_options_helpers.cpp.erb +25 -0
  763. data/templates/wasm/parser_options_helpers.h.erb +12 -0
  764. metadata +306 -27
  765. data/config/action_view_helpers/actionview/form_helper/button.yml +0 -38
  766. data/config/action_view_helpers/actionview/form_helper/submit.yml +0 -35
  767. data/lib/herb/ast.rb +0 -7
  768. data/lib/herb/diff_operation.rb +0 -34
  769. data/lib/herb/diff_result.rb +0 -59
  770. data/lib/herb/engine/error_formatter.rb +0 -432
  771. data/lib/herb/engine/parser_error_overlay.rb +0 -773
  772. data/lib/herb/engine/validation_error_overlay.rb +0 -182
  773. data/lib/herb/engine/validation_errors.rb +0 -65
  774. data/lib/herb/engine/validator.rb +0 -80
  775. data/sig/herb/ast.rbs +0 -6
  776. data/sig/herb/diff_operation.rbs +0 -34
  777. data/sig/herb/diff_result.rbs +0 -34
  778. data/sig/herb/engine/debug.rbs +0 -38
  779. data/sig/herb/engine/error_formatter.rbs +0 -47
  780. data/sig/herb/engine/parser_error_overlay.rbs +0 -41
  781. data/sig/herb/engine/validation_error_overlay.rbs +0 -35
  782. data/sig/herb/engine/validation_errors.rbs +0 -45
  783. data/sig/herb/engine/validator.rbs +0 -41
  784. data/sig/herb_c_extension.rbs +0 -10
@@ -0,0 +1,2256 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "digest"
5
+
6
+ require_relative "../../ruby_program"
7
+ require_relative "../../visitor"
8
+ require_relative "../../visitor/context_aware"
9
+ require_relative "../../visitor/diagnostics"
10
+ require_relative "../../visitor/experimental"
11
+ require_relative "annotation"
12
+ require_relative "identifier"
13
+ require_relative "manifest/channel"
14
+ require_relative "components"
15
+ require_relative "types"
16
+ require_relative "state_compiler"
17
+ require_relative "markers"
18
+ require_relative "statics"
19
+ require_relative "state_directives"
20
+ require_relative "render_bindings"
21
+ require_relative "callee"
22
+
23
+ module Herb
24
+ class Engine
25
+ module Slots
26
+ # Records where a template's dynamic content ends up, and marks those places in the output so
27
+ # something outside Ruby can find them again:
28
+ #
29
+ # visitor = Herb::Engine::Slots::Visitor.new
30
+ #
31
+ # Herb::Engine.new(source, visitors: [visitor], filename: "app/views/posts/index.html.erb")
32
+ #
33
+ # visitor.slots
34
+ #
35
+ # Templates can also ask for this themselves with a `<%# herb:slots %>` directive, and say who
36
+ # renders their branches with `<%# herb:slots client %>`. Deciding what that means belongs to
37
+ # whoever builds the stack, so the directive is only read here:
38
+ #
39
+ # mode = Herb::Engine::Slots::Visitor.directive_mode(source)
40
+ # visitors = mode ? [Herb::Engine::Slots::Visitor.new(mode: mode)] : []
41
+ #
42
+ class Visitor < Herb::Visitor
43
+ STATE_FAMILIES = [:read, :compare, :declaration, :conditional, :count, :assignment, :unknown, :binding].freeze #: Array[Symbol]
44
+
45
+ extend Herb::Visitor::Experimental
46
+ include Herb::Visitor::ContextAware
47
+ include Herb::Visitor::Diagnostics
48
+
49
+ recommended_parser_option iteration_nodes: true, render_nodes: true, strict_locals: true, prism_program: true
50
+ required_parser_option action_view_helpers: true, track_locations: true, herb_directives: true
51
+ experimental "Slots are experimental. Their markers, payload and client API may change."
52
+
53
+ attr_accessor :bufvar #: String
54
+ attr_reader :markers #: Markers
55
+ attr_reader :slots #: Array[Slot]
56
+ attr_reader :states #: StateCompiler
57
+ attr_reader :document #: untyped
58
+ attr_reader :warnings #: Array[Herb::Warnings::Warning]
59
+ attr_reader :static_markup #: String?
60
+ attr_reader :slot_nodes #: Array[untyped]
61
+
62
+ SLOTS_DIRECTIVE = /<%#-?\s*herb:slots\b(?<mode>[^%]*?)-?%>/ #: Regexp
63
+ MODE_OPTION = /\b(server|client)\b/ #: Regexp
64
+ MODES = [:server, :client].freeze #: Array[Symbol]
65
+ DELIVERIES = [:inline, :hoist, :none].freeze #: Array[Symbol]
66
+ COVERED = "@_herb_covered" #: String
67
+ OCCURRENCES = "@_herb_region_occurrences" #: String
68
+ OCCURRENCE = "_herb_occurrence" #: String
69
+ NAME_ATTRIBUTE = "data-herb-name" #: String
70
+ KEY_ATTRIBUTE = "herb-key" #: String
71
+
72
+ CAPTURING = /\b(?:content_for|provide|capture)\b/ #: Regexp
73
+ LISTENER_ATTRIBUTES = ["data-herb-set", "data-herb-toggle", "data-herb-increment", "data-herb-decrement", "data-herb-reset"].freeze #: Array[String]
74
+ OPEN_TAG_TYPES = [Herb::AST::HTMLOpenTagNode, Herb::AST::ERBOpenTagNode].freeze #: Array[Herb::AST::HTMLOpenTagNode|Herb::AST::ERBOpenTagNode]
75
+ BRANCH_BODY_PROPERTIES = [:statements, :body, :children, :conditions].freeze #: Array[Symbol]
76
+ BRANCH_CONTINUATION_PROPERTIES = [:subsequent, :else_clause, :rescue_clause, :ensure_clause].freeze #: Array[Symbol]
77
+
78
+ Slot = Data.define(
79
+ :index, #: Integer
80
+ :type, #: Symbol
81
+ :node_path, #: Array[Integer]
82
+ :expression, #: String?
83
+ :attribute, #: String?
84
+ :key_source, #: Symbol?
85
+ :key_expression, #: String?
86
+ :name, #: String?
87
+ :tag, #: String?
88
+ :merged_paths #: Array[Array[Integer]]
89
+ )
90
+
91
+ class Slot
92
+ #: () -> bool
93
+ def structural? = Types.structural?(type)
94
+
95
+ #: () -> bool
96
+ def attribute? = Types.attribute?(type)
97
+
98
+ #: () -> bool
99
+ def anchored? = Types.element_anchored?(type)
100
+
101
+ #: () -> bool
102
+ def nameable? = Types.nameable?(type)
103
+
104
+ #: () -> bool
105
+ def valued? = Types.valued?(type)
106
+
107
+ #: () -> bool
108
+ def presence? = type == :boolean_attribute
109
+
110
+ #: () -> bool
111
+ def interpolated? = Types.interpolated?(type)
112
+ end
113
+
114
+ #: (String) -> bool
115
+ def self.directive?(source)
116
+ SLOTS_DIRECTIVE.match?(source)
117
+ end
118
+
119
+ #: (String) -> Symbol?
120
+ def self.directive_mode(source)
121
+ match = SLOTS_DIRECTIVE.match(source)
122
+
123
+ return nil unless match
124
+
125
+ named = MODE_OPTION.match(match[:mode])
126
+ mode = named && named[1]
127
+
128
+ mode ? mode.to_sym : :server
129
+ end
130
+
131
+ #: () -> bool
132
+ def self.reads_erb_source?
133
+ true
134
+ end
135
+
136
+ #: (?markers: Markers, ?mode: Symbol, ?identifier: (Symbol | ^(String) -> String), ?mark: bool, ?deliver: Symbol, ?fatal: bool, ?static_markup: bool) -> void
137
+ def initialize(markers: Markers.new, mode: :server, identifier: :path, mark: true, deliver: :hoist, fatal: true, static_markup: false)
138
+ super(fatal: fatal)
139
+
140
+ raise ArgumentError, "`mode: #{mode.inspect}` is not a slot mode. Pass one of #{MODES.map(&:inspect).join(", ")}." unless MODES.include?(mode)
141
+ raise ArgumentError, "`deliver: #{deliver.inspect}` is not a delivery. Pass one of #{DELIVERIES.map(&:inspect).join(", ")}." unless DELIVERIES.include?(deliver)
142
+
143
+ @markers = markers
144
+ @mark = mark
145
+ @degraded = false
146
+ @deliver = deliver
147
+ @capture_static_markup = static_markup
148
+ @static_markup = nil #: String?
149
+ @bufvar = "_buf"
150
+ @mode = mode
151
+ @statics = {} #: Hash[String, String]?
152
+ @identify = identifier
153
+
154
+ @slots = [] #: Array[Slot]
155
+ @warnings = [] #: Array[Herb::Warnings::Warning]
156
+ @path = [] #: Array[Integer]
157
+
158
+ @annotations = [] #: Array[Annotation]
159
+
160
+ standing = {} #: Hash[untyped, Annotation]
161
+ annotation_of = {} #: Hash[untyped, Annotation]
162
+ element_anchored = {} #: Hash[untyped, Array[Annotation]]
163
+ continuations = {} #: Hash[untyped, bool]
164
+ indices = {} #: Hash[untyped, Integer]
165
+
166
+ @standing = standing.compare_by_identity
167
+ @annotation_of = annotation_of.compare_by_identity
168
+ @element_anchored = element_anchored.compare_by_identity
169
+ @continuations = continuations.compare_by_identity
170
+ @indices = indices.compare_by_identity
171
+
172
+ @slot_nodes = [] #: Array[untyped]
173
+ @tag_stack = [] #: Array[String]
174
+ @named_elements = [] #: Array[Hash[Symbol, untyped]]
175
+ attribute_open_tags = {} #: Hash[untyped, untyped]
176
+ @attribute_open_tags = attribute_open_tags.compare_by_identity
177
+ render_bindings = {} #: Hash[untyped, Hash[Symbol, untyped]]
178
+ @render_bindings = render_bindings.compare_by_identity
179
+ @interpolated_attributes = [] #: Array[untyped]
180
+ @states = StateCompiler.new(self)
181
+ @collection_nodes = [] #: Array[untyped]
182
+ @collection_body_depths = [] #: Array[Integer]
183
+ keyed_elements = {} #: Hash[untyped, String]
184
+ @keyed_elements = keyed_elements.compare_by_identity
185
+ @container_depth = 0
186
+
187
+ @in_attribute = false
188
+ @in_open_tag = false
189
+ @displaced = [] #: Array[untyped]
190
+ @in_html_comment = false
191
+ @in_html_doctype = false
192
+ @raw_text_depth = 0
193
+ @rcdata_depth = 0
194
+ @rcdata_mixed_depth = 0
195
+ @rcdata_interpolated_depth = 0
196
+ @interpolated_rcdata = [] #: Array[untyped]
197
+ @listener_stack = [] #: Array[Array[String]]
198
+ listener_written = {} #: Hash[untyped, Array[String]]
199
+ @listener_written = listener_written.compare_by_identity
200
+ @current_open_tag = nil
201
+
202
+ fragment_nodes = {} #: Hash[untyped, Hash[String, untyped]]
203
+ fragment_fallbacks = {} #: Hash[untyped, Array[untyped]]
204
+ deferred_nodes = {} #: Hash[untyped, Hash[Symbol, untyped]]
205
+ assignment_nodes = {} #: Hash[untyped, bool]
206
+
207
+ @fragment_nodes = fragment_nodes.compare_by_identity
208
+ @fragment_fallbacks = fragment_fallbacks.compare_by_identity
209
+ @exposed_fallbacks = [] #: Array[untyped]
210
+ @deferred_nodes = deferred_nodes.compare_by_identity
211
+ @assignment_nodes = assignment_nodes.compare_by_identity
212
+ end
213
+
214
+ #: () -> bool
215
+ def degraded?
216
+ @degraded
217
+ end
218
+
219
+ #: () -> bool
220
+ def client?
221
+ @mode == :client
222
+ end
223
+
224
+ #: () -> void
225
+ def state_overrides!
226
+ @state_overrides = true
227
+ end
228
+
229
+ #: () -> bool
230
+ def state_overrides?
231
+ !!@state_overrides
232
+ end
233
+
234
+ #: (Integer, Array[String]) -> bool
235
+ def listener_writes?(index, names)
236
+ written = @listener_written[@slot_nodes[index]]
237
+
238
+ !written.nil? && written.intersect?(names)
239
+ end
240
+
241
+ #: (Integer) -> Array[Integer]
242
+ def slots_inside(index)
243
+ node = @slot_nodes[index]
244
+
245
+ return [] unless node
246
+
247
+ branch_bodies(node).flat_map { |body| slot_indices_within(body) }.uniq
248
+ end
249
+
250
+ #: (untyped) -> bool
251
+ def fragment?(node)
252
+ @fragment_nodes.key?(node)
253
+ end
254
+
255
+ #: () -> Array[Integer]
256
+ def fragment_indexes
257
+ @fragment_nodes.keys.filter_map { |node| @indices[node] }
258
+ end
259
+
260
+ #: (Integer) -> Hash[String, untyped]
261
+ def fragment_timing_for(index)
262
+ node = @slot_nodes[index]
263
+
264
+ return {} unless node
265
+
266
+ @fragment_nodes[node] || {}
267
+ end
268
+
269
+ #: (Array[untyped]) -> Array[untyped]
270
+ def transform_component_children(children)
271
+ children.flat_map { |child| transformed(child) }
272
+ end
273
+
274
+ #: (untyped, Array[untyped], Hash[String, untyped]) -> void
275
+ def record_fragment(node, fallback_children, timing)
276
+ @fragment_nodes[node] = timing
277
+ @fragment_fallbacks[node] = fallback_children
278
+ end
279
+
280
+ #: (Array[untyped]) -> void
281
+ def register_fallback_reads(children)
282
+ children.each do |child|
283
+ if child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child) && fallback_state_read?(child)
284
+ record_slot(child, :child)
285
+
286
+ next
287
+ end
288
+
289
+ BRANCH_BODY_PROPERTIES.each do |property|
290
+ next unless child.respond_to?(property)
291
+
292
+ array = child.send(property)
293
+
294
+ register_fallback_reads(array) if array.is_a?(Array)
295
+ end
296
+ end
297
+ end
298
+
299
+ #: (untyped) -> bool
300
+ def fallback_state_read?(node)
301
+ name = expression_for(node).to_s.strip
302
+
303
+ return false unless name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
304
+
305
+ state_declarations[:region].any? { |entry| entry[:name] == name && !entry[:derived] }
306
+ end
307
+
308
+ #: (untyped, mode: String, state: String, timing: Hash[String, Integer]) -> void
309
+ def record_deferred(node, mode:, state:, timing:)
310
+ @deferred_nodes[node] = { mode: mode, state: state, timing: timing }
311
+ end
312
+
313
+ #: (untyped) -> untyped
314
+ def record_assignment(node)
315
+ @assignment_nodes[node] = true
316
+
317
+ node
318
+ end
319
+
320
+ #: (untyped) -> bool
321
+ def assignment_node?(node)
322
+ @assignment_nodes.key?(node)
323
+ end
324
+
325
+ #: () -> Hash[Integer, Hash[Symbol, untyped]]
326
+ def deferred_entries
327
+ entries = {} #: Hash[Integer, Hash[Symbol, untyped]]
328
+
329
+ @deferred_nodes.each do |node, info|
330
+ index = @indices[node]
331
+
332
+ entries[index] = info if index
333
+ end
334
+
335
+ entries
336
+ end
337
+
338
+ #: (Integer) -> Array[Array[Integer]]
339
+ def slots_by_branch(index)
340
+ node = @slot_nodes[index]
341
+
342
+ return [] unless node
343
+
344
+ branch_bodies(node).map { |body| slot_indices_within(body) }
345
+ end
346
+
347
+ #: (String, Herb::Location?, Symbol, ?suggestion: String?) -> nil
348
+ def slot_error(message, location, family, suggestion: nil)
349
+ error(message, location, code: diagnostic_code_for(family), suggestion: suggestion)
350
+
351
+ @degraded = true
352
+
353
+ nil
354
+ end
355
+
356
+ #: (String, untyped, Symbol, ?suggestion: String?) -> void
357
+ def slot_warning(message, location, family, suggestion: nil)
358
+ warning(message, location, code: diagnostic_code_for(family), suggestion: suggestion)
359
+ end
360
+
361
+ #: (Symbol) -> String
362
+ def diagnostic_code_for(family)
363
+ STATE_FAMILIES.include?(family) ? "herb-state-#{family}" : "herb-slots-#{family}"
364
+ end
365
+
366
+ #: () -> String
367
+ def version
368
+ @version ||= Digest::SHA256.hexdigest(version_projection.join(",")).slice(0, 8).to_s
369
+ end
370
+
371
+ #: () -> Hash[Symbol, untyped]
372
+ def state_declarations
373
+ @states.state_declarations
374
+ end
375
+
376
+ #: () -> Array[Hash[Symbol, untyped]]
377
+ def state_entries
378
+ @states.state_entries
379
+ end
380
+
381
+ #: () -> Hash[Integer, Hash[Symbol, untyped]]
382
+ def state_conditional_entries
383
+ @states.state_conditional_entries
384
+ end
385
+
386
+ #: () -> Hash[Integer, String]
387
+ def state_conditional_signatures
388
+ @states.state_conditional_signatures
389
+ end
390
+
391
+ #: () -> Array[Hash[Symbol, untyped]]
392
+ def state_count_entries
393
+ @states.state_count_entries
394
+ end
395
+
396
+ #: () -> Hash[Integer, untyped]
397
+ def state_presence
398
+ @states.state_presence
399
+ end
400
+
401
+ #: () -> Hash[Integer, untyped]
402
+ def state_values
403
+ @states.state_values
404
+ end
405
+
406
+ #: () -> Array[String]
407
+ def seeded_region_states
408
+ @states.seeded_region_states
409
+ end
410
+
411
+ #: () -> Hash[Integer, Array[String]]
412
+ def seeded_item_states
413
+ @states.seeded_item_states
414
+ end
415
+
416
+ #: () -> untyped
417
+ def current_collection
418
+ @collection_nodes.last
419
+ end
420
+
421
+ #: (untyped) -> Array[String]
422
+ def block_locals(scope)
423
+ nodes = @collection_nodes.include?(scope) ? @collection_nodes.take(@collection_nodes.index(scope) + 1) : [scope]
424
+
425
+ nodes.compact.flat_map { |node| parameter_names(node) }
426
+ end
427
+
428
+ #: (untyped) -> Array[String]
429
+ def parameter_names(node)
430
+ return [] unless node.respond_to?(:block_arguments)
431
+
432
+ (node.block_arguments || []).filter_map { |parameter| parameter.name&.value&.to_s if parameter.respond_to?(:name) }
433
+ end
434
+
435
+ #: () -> bool
436
+ def in_item_body?
437
+ @container_depth == @collection_body_depths.last
438
+ end
439
+
440
+ #: () -> bool
441
+ def inline?
442
+ @in_open_tag || @in_attribute
443
+ end
444
+
445
+ #: (untyped) -> untyped
446
+ def open_tag_for(node)
447
+ @attribute_open_tags[node]
448
+ end
449
+
450
+ #: (untyped) -> untyped
451
+ def scope_of(node)
452
+ annotation = @annotation_of[node]
453
+
454
+ annotation && [annotation.scope, annotation.depth]
455
+ end
456
+
457
+ #: (Integer, Slot) -> void
458
+ def replace_slot(index, slot)
459
+ @slots[index] = slot
460
+ end
461
+
462
+ #: (untyped, Integer) -> void
463
+ def assign_index(node, index)
464
+ @indices[node] = index
465
+ end
466
+
467
+ #: () -> bool
468
+ def marking?
469
+ @mark
470
+ end
471
+
472
+ #: () -> Array[String]
473
+ def version_projection
474
+ conditionals = state_conditional_signatures
475
+
476
+ slots = @slots.map { |slot|
477
+ [
478
+ slot.index,
479
+ slot.type,
480
+ slot.node_path,
481
+ slot.attribute,
482
+ slot.name,
483
+ slot.key_source,
484
+ conditionals[slot.index]
485
+ ].inspect
486
+ }
487
+
488
+ declarations = state_entries.map { |entry|
489
+ [entry[:name], entry[:kind], entry[:default], entry[:scope], entry[:derived] && StateDirectives.condition_source(entry[:derived])].inspect
490
+ }
491
+
492
+ counts = @states.count_signatures
493
+
494
+ bindings = manifest_bindings.map { |index, entry| [index, entry["identifier"], entry["states"]].inspect }
495
+
496
+ slots + declarations + counts + bindings
497
+ end
498
+
499
+ #: () -> String
500
+ def inspect
501
+ parts = [self.class.name, @mode.to_s] #: Array[String]
502
+
503
+ parts << "#{@identify} ids" unless @identify == :path
504
+ parts << "deliver=#{@deliver}" unless @deliver == :hoist
505
+ parts << "fatal=#{fatal?}" unless fatal?
506
+
507
+ "#<#{parts.join(" ")}>"
508
+ end
509
+
510
+ #: () -> String
511
+ def identifier
512
+ @identifier ||= Identifier.new(@identify).call(context.relative_file_path)
513
+ end
514
+
515
+ #: () -> Hash[String, untyped]
516
+ def manifest
517
+ {
518
+ "file" => context.relative_file_path,
519
+ "identifier" => identifier,
520
+ "version" => version,
521
+ "names" => manifest_names,
522
+ "parts" => manifest_parts,
523
+ "bindings" => manifest_bindings,
524
+ "states" => @states.manifest,
525
+ }
526
+ end
527
+
528
+ #: () -> Hash[String, Hash[String, untyped]]
529
+ def manifest_bindings
530
+ bindings = {} #: Hash[String, Hash[String, untyped]]
531
+
532
+ @render_bindings.each do |node, binding|
533
+ index = @indices[node]
534
+
535
+ next unless index
536
+ next if binding.fetch(:bound).empty?
537
+
538
+ bindings[index.to_s] = {
539
+ "identifier" => binding.fetch(:identifier),
540
+ "partial" => binding.fetch(:partial),
541
+ "states" => binding.fetch(:bound),
542
+ }
543
+ end
544
+
545
+ bindings
546
+ end
547
+
548
+ #: (String) -> bool
549
+ def counted_state?(name)
550
+ @states.counted_state?(name)
551
+ end
552
+
553
+ #: () -> Hash[String, Integer]
554
+ def manifest_names
555
+ names = {} #: Hash[String, Integer]
556
+
557
+ @slots.each do |slot|
558
+ name = slot.name
559
+
560
+ names[name] = slot.index if name
561
+ end
562
+
563
+ names
564
+ end
565
+
566
+ #: () -> Hash[String, Array[String]]
567
+ def manifest_parts
568
+ parts = {} #: Hash[String, Array[String]]
569
+
570
+ @interpolated_attributes.each do |node|
571
+ index = @indices[node]
572
+
573
+ next unless index
574
+
575
+ segments = attribute_segments(node)
576
+
577
+ next unless segments
578
+
579
+ parts[index.to_s] = segments
580
+ end
581
+
582
+ @interpolated_rcdata.each do |node|
583
+ index = @indices[node]
584
+
585
+ next unless index
586
+
587
+ segments = rcdata_segments(node.body)
588
+
589
+ next unless segments
590
+
591
+ parts[index.to_s] = segments
592
+ end
593
+
594
+ parts
595
+ end
596
+
597
+ #: () -> Hash[Symbol, untyped]
598
+ def schema
599
+ {
600
+ file: context.relative_file_path,
601
+ identifier: identifier,
602
+ version: version,
603
+ slots: slot_entries,
604
+ }
605
+ end
606
+
607
+ #: () -> Array[Hash[Symbol, untyped]]
608
+ def slot_entries
609
+ @slot_entries ||= @slots.map { |slot|
610
+ entry = { index: slot.index, type: slot.type, node_path: slot.node_path }
611
+ entry = entry.merge(attribute: slot.attribute) if slot.attribute
612
+ entry = entry.merge(key_source: slot.key_source) if slot.key_source
613
+ entry = entry.merge(name: slot.name) if slot.name
614
+
615
+ entry
616
+ }
617
+ end
618
+
619
+ #: () -> Array[String]
620
+ def recorded_expressions
621
+ @annotations.map { |annotation| annotation.expression.to_s }
622
+ end
623
+
624
+ #: (untyped) -> Integer?
625
+ def index_for(node)
626
+ @indices[node]
627
+ end
628
+
629
+ #: () -> String
630
+ def occurrence_expression
631
+ "((#{OCCURRENCES} ||= ::Hash.new(0))[#{identifier.inspect}] += 1) - 1"
632
+ end
633
+
634
+ #: (Herb::Location?) -> Herb::RubyProgram::Resolved?
635
+ def ruby_at(location)
636
+ return nil unless location
637
+
638
+ unless @ruby_program_built
639
+ @ruby_program_built = true
640
+ @ruby_program = @document ? Herb::RubyProgram.for(@document) : nil
641
+ end
642
+
643
+ @ruby_program&.resolve(location)
644
+ end
645
+
646
+ def visit_document_node(node)
647
+ @document = node
648
+
649
+ if declares_slots?(node)
650
+ transform_components(node)
651
+ detect_keyed_elements(node)
652
+ end
653
+
654
+ visit_children_with_paths(node.children)
655
+
656
+ @fragment_fallbacks.each_value { |body| register_fallback_reads(body) }
657
+
658
+ collapse_invariant_conditionals
659
+ apply_names
660
+ number_slots
661
+
662
+ @states.apply_states
663
+
664
+ wrap_bound_renders
665
+ expose_fallbacks(node)
666
+ end
667
+
668
+ #: () -> Hash[String, String]?
669
+ def statics
670
+ @statics&.dup
671
+ end
672
+
673
+ #: (untyped) -> void
674
+ def finish(node)
675
+ withdraw_fallbacks(node)
676
+ follow_replacements
677
+
678
+ return unless @mark
679
+
680
+ return wrap_region(node) if @degraded
681
+
682
+ insert_markers(node)
683
+ wrap_displaced
684
+
685
+ @static_markup = Statics.new(@standing, @element_anchored).markup(node.children) if @capture_static_markup
686
+
687
+ wrap_region(node)
688
+ append_statics(node)
689
+ deliver_manifest(node)
690
+ end
691
+
692
+ #: () -> void
693
+ def follow_replacements
694
+ replacements = context.replacements
695
+
696
+ return unless replacements.any?
697
+
698
+ replacements.each do |replacement, original|
699
+ annotations = @standing[original]
700
+ @standing[replacement] = annotations if annotations
701
+
702
+ index = @indices[original]
703
+ @indices[replacement] = index if index
704
+ end
705
+ end
706
+
707
+ #: (untyped) -> void
708
+ def deliver_manifest(document_node)
709
+ return if @deliver == :none
710
+
711
+ built = manifest
712
+
713
+ return if built["names"].empty? && built["parts"].empty? && built["states"].nil? && built["bindings"].empty?
714
+
715
+ json = JSON.generate(built, script_safe: true)
716
+ key = "#{identifier}:#{version}"
717
+
718
+ if @deliver == :hoist
719
+ document_node.children.push(erb_code_node("::Herb::Engine::Slots::Manifest::Channel.record(#{key.dump}, #{json.dump})"))
720
+
721
+ return
722
+ end
723
+
724
+ document_node.children.unshift(erb_code_node("#{COVERED} ||= {}"))
725
+
726
+ ref = "#{COVERED}[#{"manifest:#{key}".inspect}]"
727
+
728
+ document_node.children.push(
729
+ erb_code_node("unless #{ref}"),
730
+ text_node(@markers.manifests_open),
731
+ text_node("{#{JSON.generate(key)}:#{json}}"),
732
+ text_node(@markers.manifests_close),
733
+ erb_code_node("#{ref} = true"),
734
+ erb_code_node("end")
735
+ )
736
+ end
737
+
738
+ def visit_html_element_node(node)
739
+ record_slot(node, :keyed) if @keyed_elements.key?(node)
740
+
741
+ tag_name = node.tag_name&.value&.downcase.to_s
742
+ raw_text = Herb::HTML::Util.raw_text_element?(tag_name)
743
+ rcdata = Herb::HTML::Util.rcdata_element?(tag_name)
744
+ segments = rcdata && mixed_rcdata_body?(node.body) ? rcdata_segments(node.body) : nil
745
+ mixed = rcdata && segments.nil? && mixed_rcdata_body?(node.body)
746
+
747
+ @raw_text_depth += 1 if raw_text
748
+ @rcdata_depth += 1 if rcdata
749
+ @rcdata_mixed_depth += 1 if mixed
750
+ @rcdata_interpolated_depth += 1 if segments
751
+
752
+ previous_open_tag = @current_open_tag
753
+ @current_open_tag = node.open_tag
754
+ @tag_stack.push(tag_name)
755
+ @listener_stack.push(listener_names_for(node))
756
+
757
+ if segments
758
+ record_slot(node, :raw_text_interpolation)
759
+ @interpolated_rcdata << node
760
+ end
761
+
762
+ name_attribute = attributes_for(node).find { |attribute| attribute_name_for(attribute)&.downcase == NAME_ATTRIBUTE }
763
+ base = @annotations.size
764
+ base_scope = @collection_nodes.last
765
+ base_depth = @container_depth
766
+
767
+ visit(node.open_tag) if node.open_tag
768
+ visit_children_with_paths(node.body)
769
+ visit(node.close_tag) if node.close_tag
770
+
771
+ record_named_element(node, name_attribute, base, base_scope, base_depth) if name_attribute
772
+
773
+ @listener_stack.pop
774
+ @tag_stack.pop
775
+ @current_open_tag = previous_open_tag
776
+
777
+ @rcdata_interpolated_depth -= 1 if segments
778
+ @rcdata_mixed_depth -= 1 if mixed
779
+ @rcdata_depth -= 1 if rcdata
780
+ @raw_text_depth -= 1 if raw_text
781
+ end
782
+
783
+ def visit_erb_strict_locals_node(node)
784
+ (node.locals || []).each do |parameter|
785
+ name = parameter.name&.value.to_s
786
+ next if name.empty?
787
+
788
+ @states.declare_local(name, parameter.default_value&.content)
789
+ end
790
+
791
+ super
792
+ end
793
+
794
+ def visit_html_open_tag_node(node)
795
+ @in_open_tag = true
796
+ super
797
+ @in_open_tag = false
798
+ end
799
+
800
+ def visit_erb_open_tag_node(node)
801
+ @in_open_tag = true
802
+ super
803
+ @in_open_tag = false
804
+ end
805
+
806
+ def visit_html_attribute_node(node)
807
+ if dynamic?(node)
808
+ record_slot(node, attribute_type_for(node))
809
+ @attribute_open_tags[node] = @current_open_tag
810
+ @interpolated_attributes << node if attribute_type_for(node) == :attribute_interpolation
811
+ end
812
+
813
+ @in_attribute = true
814
+ super
815
+ @in_attribute = false
816
+ end
817
+
818
+ def visit_html_comment_node(node)
819
+ record_slot(node, :comment) if dynamic?(node)
820
+
821
+ @in_html_comment = true
822
+ super
823
+ @in_html_comment = false
824
+ end
825
+
826
+ def visit_html_doctype_node(node)
827
+ @in_html_doctype = true
828
+ super
829
+ @in_html_doctype = false
830
+ end
831
+
832
+ def visit_erb_content_node(node)
833
+ record_slot(node, erb_outputs?(node) ? :child : nil)
834
+
835
+ super
836
+ end
837
+
838
+ def visit_erb_render_node(node)
839
+ bind_render_states(node) if node.bound_state?
840
+
841
+ record_slot(node, erb_outputs?(node) ? :child : nil)
842
+
843
+ super
844
+ end
845
+
846
+ #: (untyped) -> void
847
+ def bind_render_states(node)
848
+ location = node.location
849
+
850
+ unless node.static_partial?
851
+ slot_error("`state:` needs a partial the compiler can see, and this render picks its template at runtime.", location, :binding, suggestion: "Render the partial by its literal name, like `render \"shared/card\", state: { ... }`.")
852
+
853
+ return
854
+ end
855
+
856
+ if node.end_node
857
+ slot_error("`state:` cannot bind a partial rendered with a block, since the binding wraps the whole call.", location, :binding, suggestion: "Render the partial without a block, or move the block's content into the partial.")
858
+
859
+ return
860
+ end
861
+
862
+ if inline? || @in_html_comment || !erb_outputs?(node)
863
+ slot_error("`state:` binds a partial rendered as page content, not one inside an attribute, a comment or a silent tag.", location, :binding, suggestion: "Render the partial with `<%= render ... %>` in the page body.")
864
+
865
+ return
866
+ end
867
+
868
+ token = node.content
869
+ analysis = RenderBindings.analyze(node)
870
+
871
+ unless token && analysis
872
+ slot_error("`state:` on this render could not be read.", location, :binding, suggestion: "Write it as `state: { name: value }` after the partial's locals.")
873
+
874
+ return
875
+ end
876
+
877
+ analysis.problems.each { |message, suggestion, where| slot_error(message, where || location, :binding, suggestion: suggestion) }
878
+
879
+ return unless analysis.problems.empty?
880
+
881
+ partial = node.partial_path.to_s
882
+ twice = node.local_names & analysis.entries.map(&:name)
883
+
884
+ analysis.entries.each do |entry|
885
+ next unless twice.include?(entry.name)
886
+
887
+ slot_error("`#{entry.name}` goes to `#{partial}` both as a local and as a state, and the state assignment would overwrite the local.", entry.location || location, :binding, suggestion: "Pass `#{entry.name}` one way. A local is a snapshot the partial reads, a state is a value the client can write.")
888
+ end
889
+
890
+ return unless twice.empty?
891
+
892
+ unless partial.include?("/")
893
+ slot_error("`state:` needs the partial's directory in its name, since `#{partial}` resolves through the controller's view paths at render and the compiler cannot follow that.", location, :binding, suggestion: "Name it from the view root, like `render \"#{File.basename(File.dirname(context.relative_file_path))}/#{partial}\", state: { ... }`.")
894
+
895
+ return
896
+ end
897
+
898
+ relative, absolute = child_template_for(node)
899
+
900
+ unless relative && absolute
901
+ similar = similar_partials_for(node).map { |name| "`#{name}`" }
902
+ advice = similar.empty? ? "Check the partial's path." : "Did you mean #{similar.join(", ")}?"
903
+
904
+ slot_error("`#{partial}` could not be found from `#{context.relative_file_path}`, and `state:` needs a partial the compiler can open.", location, :binding, suggestion: advice)
905
+
906
+ return
907
+ end
908
+
909
+ callee = Callee.for(absolute)
910
+
911
+ slot_warning("`#{partial}` could not be compiled from `#{context.relative_file_path}`, so its states go unchecked.", location, :binding, suggestion: "A partial that renders its own caller is checked from the other side only.") unless callee
912
+
913
+ failed = callee&.errors&.first
914
+
915
+ if failed
916
+ slot_error("`#{partial}` did not compile, so `state:` cannot check its states. Its compile said #{failed.message}", location, :binding, suggestion: "Fix the partial first.")
917
+
918
+ return
919
+ end
920
+
921
+ bound = {} #: Hash[String, String]
922
+ seeded = {} #: Hash[String, String]
923
+
924
+ analysis.entries.each { |entry| classify_binding(entry, partial, callee, bound, seeded, entry.location || location) }
925
+
926
+ return if @degraded
927
+
928
+ token.value.replace(analysis.stripped)
929
+
930
+ @render_bindings[node] = { identifier: Identifier.new(@identify).call(relative), partial: partial, bound: bound, seeded: seeded }
931
+ rescue FrozenError
932
+ slot_error("`state:` on this render could not be rewritten, since the template's source was frozen before the compile could edit it.", node.location, :binding, suggestion: "Report this with the template that raised it.")
933
+ end
934
+
935
+ #: (RenderBindings::Entry, String, Callee?, Hash[String, String], Hash[String, String], Herb::Location?) -> void
936
+ def classify_binding(entry, partial, callee, bound, seeded, location)
937
+ child = callee&.declaration(entry.name)
938
+
939
+ if callee && child.nil?
940
+ known = callee.names.map { |name| "`#{name}`" }.join(", ")
941
+ advice = known.empty? ? "Declare `#{entry.name}` in the partial with `<%# herb:state (#{entry.name}: ...) %>`." : "The partial declares #{known}."
942
+
943
+ slot_error("`#{partial}` declares no state `#{entry.name}`, so `state:` has nothing to bind it to.", location, :binding, suggestion: advice)
944
+
945
+ return
946
+ end
947
+
948
+ if child && (child.derived || child.counted)
949
+ slot_error("`#{entry.name}` is a #{child.derived ? "derived" : "counted"} state of `#{partial}`, and the partial computes it itself.", location, :binding, suggestion: "Bind or seed the states it computes from instead.")
950
+
951
+ return
952
+ end
953
+
954
+ candidate = entry.candidate
955
+ declaration = candidate ? @states.region_declaration(candidate) : nil
956
+
957
+ if declaration && candidate
958
+ if declaration.derived || @states.counted_state?(candidate)
959
+ slot_error("`#{candidate}` is a #{declaration.derived ? "derived" : "counted"} state, and a binding needs a state the partial can write back to.", location, :binding, suggestion: "Bind a plain state, or bind the states `#{candidate}` computes from.")
960
+
961
+ return
962
+ end
963
+
964
+ if child && !compatible_kinds?(child.kind, declaration.kind)
965
+ slot_error("`#{entry.name}` on `#{partial}` is #{with_article(child.kind)} state and `#{candidate}` is #{with_article(declaration.kind)} one.", location, :binding, suggestion: "Bind a state of the same kind, or change the partial's default.")
966
+
967
+ return
968
+ end
969
+
970
+ bound[entry.name] = candidate
971
+
972
+ return
973
+ end
974
+
975
+ warn_binding_near_miss(candidate, location) if candidate
976
+
977
+ reason = RenderBindings::REFUSED[entry.kind]
978
+
979
+ if reason
980
+ slot_error("`#{entry.source}` seeds `#{entry.name}` on `#{partial}` with #{StateKinds::ARTICLES.fetch(entry.kind.to_s)}. #{reason}", location, :binding, suggestion: "Pass it as a local instead.")
981
+
982
+ return
983
+ end
984
+
985
+ if child && !compatible_kinds?(child.kind, entry.kind)
986
+ slot_error("`#{entry.name}` on `#{partial}` is #{with_article(child.kind)} state, and `#{entry.source}` seeds it with #{with_article(entry.kind)} value.", location, :binding, suggestion: "Seed it with #{with_article(child.kind)} value.")
987
+
988
+ return
989
+ end
990
+
991
+ seeded[entry.name] = entry.source
992
+ end
993
+
994
+ #: (Symbol, Symbol) -> bool
995
+ def compatible_kinds?(declared, given)
996
+ return true if declared == given
997
+ return true if declared == :seeded || given == :seeded
998
+
999
+ declared == :nil || given == :nil
1000
+ end
1001
+
1002
+ #: (Symbol) -> String
1003
+ def with_article(kind)
1004
+ spelled = kind.to_s
1005
+
1006
+ spelled.start_with?("a", "e", "i", "o", "u") ? "an #{spelled}" : "a #{spelled}"
1007
+ end
1008
+
1009
+ #: (String, Herb::Location?) -> void
1010
+ def warn_binding_near_miss(candidate, location)
1011
+ suggestions = DidYouMean::SpellChecker.new(dictionary: @states.region_state_names).correct(candidate)
1012
+
1013
+ return if suggestions.empty?
1014
+
1015
+ slot_warning("`#{candidate}` is not a state of this template, so it seeds the partial once and never follows a change. Did you mean `#{suggestions.first}`?", location, :binding, suggestion: "Bind `#{suggestions.first}` to keep the two in step.")
1016
+ end
1017
+
1018
+ #: (untyped) -> [String?, Pathname?]
1019
+ def child_template_for(node)
1020
+ resolved = context.resolver.resolve(node.partial_path.to_s, from: context.file_path)
1021
+
1022
+ resolved ? [resolved.identifier, resolved.path] : [nil, nil]
1023
+ end
1024
+
1025
+ #: (untyped) -> Array[String]
1026
+ def similar_partials_for(node)
1027
+ context.resolver.similar(node.partial_path.to_s, from: context.file_path)
1028
+ end
1029
+
1030
+ #: () -> void
1031
+ def wrap_bound_renders
1032
+ return if @degraded
1033
+
1034
+ @render_bindings.each do |node, binding|
1035
+ token = node.content
1036
+
1037
+ next unless token
1038
+
1039
+ bound = binding.fetch(:bound).map { |child, parent| "#{child.inspect} => #{parent}" }.join(", ")
1040
+ seeded = binding.fetch(:seeded).map { |child, expression| "#{child.inspect} => (#{expression})" }.join(", ")
1041
+ call = token.value.strip
1042
+
1043
+ token.value.replace(" ::Herb::Engine::Slots::Bindings.with(#{binding.fetch(:identifier).inspect}, bound: { #{bound} }, seeded: { #{seeded} }) { #{call} } ")
1044
+ rescue FrozenError
1045
+ next
1046
+ end
1047
+ end
1048
+
1049
+ def visit_erb_yield_node(node)
1050
+ record_slot(node, :child)
1051
+
1052
+ super
1053
+ end
1054
+
1055
+ def visit_erb_if_node(node)
1056
+ unless fragment?(node)
1057
+ return if @states.register_count_fold(node)
1058
+ return if convert_helper_boolean_attribute(node)
1059
+ end
1060
+
1061
+ record_slot(node, :conditional) unless continuation?(node)
1062
+
1063
+ visit_branching_node(node)
1064
+ end
1065
+
1066
+ def visit_erb_unless_node(node)
1067
+ record_slot(node, :conditional) unless continuation?(node)
1068
+
1069
+ visit_branching_node(node)
1070
+ end
1071
+
1072
+ def visit_erb_else_node(node)
1073
+ register_branch_directives(node)
1074
+ super
1075
+ end
1076
+
1077
+ def visit_erb_when_node(node)
1078
+ register_branch_directives(node)
1079
+ super
1080
+ end
1081
+
1082
+ #: (untyped) -> void
1083
+ def register_branch_directives(node)
1084
+ BRANCH_BODY_PROPERTIES.each do |property|
1085
+ next unless node.respond_to?(property)
1086
+
1087
+ body = node.send(property)
1088
+ next unless body.is_a?(Array)
1089
+
1090
+ body.each { |child| @states.register_state_directive(child, body) }
1091
+ end
1092
+ end
1093
+
1094
+ def visit_erb_case_node(node)
1095
+ record_slot(node, :conditional) unless continuation?(node)
1096
+
1097
+ visit_branching_node(node)
1098
+ end
1099
+
1100
+ def visit_erb_block_node(node)
1101
+ record_slot(node, :block) if erb_outputs?(node)
1102
+
1103
+ @displaced << node if CAPTURING.match?(expression_for(node).to_s)
1104
+
1105
+ visit_branching_node(node)
1106
+ end
1107
+
1108
+ def visit_erb_iteration_block_node(node)
1109
+ visit_collection_node(node)
1110
+ end
1111
+
1112
+ def visit_erb_while_node(node)
1113
+ visit_collection_node(node)
1114
+ end
1115
+
1116
+ def visit_erb_until_node(node)
1117
+ visit_collection_node(node)
1118
+ end
1119
+
1120
+ def visit_erb_for_node(node)
1121
+ visit_collection_node(node)
1122
+ end
1123
+
1124
+ def visit_collection_node(node)
1125
+ record_slot(node, :collection)
1126
+
1127
+ @collection_nodes.push(node)
1128
+ @collection_body_depths.push(@container_depth + 1)
1129
+ visit_branching_node(node)
1130
+ @collection_body_depths.pop
1131
+ @collection_nodes.pop
1132
+ end
1133
+
1134
+ #: (untyped) -> bool
1135
+ def continuation?(node)
1136
+ @continuations.key?(node)
1137
+ end
1138
+
1139
+ #: (untyped) -> bool
1140
+ def exhaustive?(node)
1141
+ current = continuation_of(node) #: untyped
1142
+ last = current #: untyped
1143
+
1144
+ while current
1145
+ last = current
1146
+ current = continuation_of(current)
1147
+ end
1148
+
1149
+ last.is_a?(Herb::AST::ERBElseNode)
1150
+ end
1151
+
1152
+ #: (untyped) -> Array[untyped]
1153
+ def conditional_chain(node)
1154
+ chain = [] #: Array[untyped]
1155
+ current = node #: untyped
1156
+
1157
+ while current
1158
+ chain << current
1159
+ current = continuation_of(current)
1160
+ end
1161
+
1162
+ chain
1163
+ end
1164
+
1165
+ #: (untyped) -> bool
1166
+ def blank_child?(node)
1167
+ return true if node.is_a?(Herb::AST::WhitespaceNode)
1168
+
1169
+ if node.is_a?(Herb::AST::HTMLTextNode) || node.is_a?(Herb::AST::LiteralNode)
1170
+ return node.content.to_s.strip.empty?
1171
+ end
1172
+
1173
+ false
1174
+ end
1175
+
1176
+ #: (untyped) -> untyped
1177
+ def continuation_of(node)
1178
+ subsequent = node.respond_to?(:subsequent) ? node.subsequent : nil
1179
+
1180
+ subsequent || (node.respond_to?(:else_clause) ? node.else_clause : nil)
1181
+ end
1182
+
1183
+ def text_node(content)
1184
+ Herb::AST::HTMLTextNode.build(content: content.dup)
1185
+ end
1186
+
1187
+ def erb_code_node(code)
1188
+ Herb::AST::ERBContentNode.build(
1189
+ tag_opening: token(:erb_start, "<%"),
1190
+ content: token(:erb_content, " #{code} "),
1191
+ tag_closing: token(:erb_end, "%>"),
1192
+ valid: true
1193
+ )
1194
+ end
1195
+
1196
+ def erb_output_node(code)
1197
+ Herb::AST::ERBContentNode.build(
1198
+ tag_opening: token(:erb_start, "<%="),
1199
+ content: token(:erb_content, " #{code} "),
1200
+ tag_closing: token(:erb_end, "%>"),
1201
+ valid: true
1202
+ )
1203
+ end
1204
+
1205
+ private
1206
+
1207
+ def visit_children_with_paths(children)
1208
+ return unless children.is_a?(Array)
1209
+
1210
+ children.each_with_index do |child, index|
1211
+ @states.register_state_directive(child, children)
1212
+ @states.check_state_assignment(child)
1213
+
1214
+ @path.push(index)
1215
+ visit(child)
1216
+ @path.pop
1217
+ end
1218
+ end
1219
+
1220
+ def visit_branching_node(node)
1221
+ @container_depth += 1
1222
+
1223
+ BRANCH_BODY_PROPERTIES.each do |property|
1224
+ next unless node.respond_to?(property)
1225
+
1226
+ visit_children_with_paths(node.send(property))
1227
+ end
1228
+
1229
+ BRANCH_CONTINUATION_PROPERTIES.each do |property|
1230
+ next unless node.respond_to?(property)
1231
+
1232
+ child = node.send(property)
1233
+ next unless child
1234
+
1235
+ @continuations[child] = true
1236
+ visit(child)
1237
+ end
1238
+
1239
+ @container_depth -= 1
1240
+ end
1241
+
1242
+ #: (untyped) -> bool
1243
+ def declares_slots?(node)
1244
+ node.children.any? { |child|
1245
+ child.is_a?(Herb::AST::HerbDirectiveNode) && child.directive == "slots"
1246
+ }
1247
+ end
1248
+
1249
+ #: (untyped) -> void
1250
+ def transform_components(node)
1251
+ return unless node.is_a?(Herb::AST::Node)
1252
+
1253
+ BRANCH_BODY_PROPERTIES.each do |property|
1254
+ next unless node.respond_to?(property)
1255
+
1256
+ value = node.send(property)
1257
+
1258
+ case value
1259
+ when Array
1260
+ if property == :conditions
1261
+ value.each { |arm| transform_components(arm) }
1262
+ else
1263
+ value.replace(value.flat_map { |child| transformed(child) })
1264
+ end
1265
+ when Herb::AST::Node
1266
+ transform_components(value)
1267
+ end
1268
+ end
1269
+
1270
+ BRANCH_CONTINUATION_PROPERTIES.each do |property|
1271
+ next unless node.respond_to?(property)
1272
+
1273
+ continuation = node.send(property)
1274
+
1275
+ transform_components(continuation) if continuation
1276
+ end
1277
+ end
1278
+
1279
+ #: (untyped) -> Array[untyped]
1280
+ def transformed(child)
1281
+ unless Components.element?(child)
1282
+ transform_components(child)
1283
+
1284
+ return [child]
1285
+ end
1286
+
1287
+ name = child.tag_name&.value.to_s
1288
+ component = Components.for(name)
1289
+
1290
+ return component.transform(child, self) if component
1291
+
1292
+ slot_error("`<#{name}>` is not a component Herb knows.", child.location, :component, suggestion: "The built-in components are #{Components::BUILT_IN.map { |built_in| "`<#{built_in}>`" }.join(" and ")}.")
1293
+
1294
+ [child]
1295
+ end
1296
+
1297
+ #: () -> void
1298
+ def collapse_invariant_conditionals
1299
+ @standing.each_value do |annotation|
1300
+ next unless annotation.type == :conditional
1301
+ next if fragment?(annotation.node)
1302
+ next unless exhaustive?(annotation.node)
1303
+
1304
+ bodies = branch_bodies(annotation.node)
1305
+ next unless bodies.size > 1
1306
+
1307
+ signatures = bodies.filter_map { |body| Statics.new(@standing, @element_anchored).signature(body) }
1308
+ next unless signatures.size == bodies.size
1309
+
1310
+ shapes = signatures.map { |shape, _| shape }
1311
+ next unless shapes.uniq.one?
1312
+ next if branching_shape?(shapes.fetch(0))
1313
+
1314
+ positions = signatures.map { |_, found| found }
1315
+ shared = positions.fetch(0)
1316
+
1317
+ annotation.dropped = true
1318
+
1319
+ shared.each { |survivor| survivor.covered << annotation }
1320
+
1321
+ positions.drop(1).each do |found|
1322
+ found.each_with_index do |other, position|
1323
+ survivor = shared.fetch(position)
1324
+
1325
+ other.merged_into = survivor
1326
+ survivor.covered << other
1327
+ end
1328
+ end
1329
+ end
1330
+
1331
+ @standing.delete_if { |_node, annotation| annotation.dropped }
1332
+ end
1333
+
1334
+ #: (untyped) -> Array[Integer]
1335
+ def slot_indices_within(nodes)
1336
+ children = Array(nodes) #: Array[untyped]
1337
+
1338
+ children.compact.flat_map { |child|
1339
+ next [] unless child.is_a?(Herb::AST::Node)
1340
+
1341
+ inside = slot_indices_within(child.compact_child_nodes)
1342
+ index = @indices[child]
1343
+
1344
+ index ? [index, *inside] : inside
1345
+ }
1346
+ end
1347
+
1348
+ #: (String) -> bool
1349
+ def branching_shape?(shape)
1350
+ Types::STRUCTURAL.any? { |type| shape.include?("\u0000#{type}\u0000") }
1351
+ end
1352
+
1353
+ #: () -> void
1354
+ def number_slots
1355
+ surviving = @annotations.reject(&:gone?)
1356
+
1357
+ surviving.each_with_index { |annotation, index| annotation.index = index }
1358
+
1359
+ @slots = surviving.each_with_index.map { |annotation, index| annotation.to_slot(index) }
1360
+ @slot_nodes = surviving.map(&:node)
1361
+
1362
+ @annotations.each do |annotation|
1363
+ next if annotation.dropped
1364
+
1365
+ @indices[annotation.node] = annotation.survivor.index
1366
+ end
1367
+ end
1368
+
1369
+ #: (untyped, Symbol?) -> void
1370
+ def record_slot(node, type)
1371
+ return unless type
1372
+
1373
+ type = anchored_type_for(type)
1374
+
1375
+ return unless type
1376
+ return if @in_html_comment || @in_html_doctype
1377
+ return if type == :raw_text && @rcdata_interpolated_depth.positive?
1378
+ return refuse_mixed_rcdata(node) if type == :raw_text && @rcdata_mixed_depth.positive?
1379
+
1380
+ key_source, key_expression = keys_for(node, type)
1381
+
1382
+ annotation = Annotation.new(
1383
+ node: node,
1384
+ type: type,
1385
+ node_path: @path.dup,
1386
+ expression: expression_for(node),
1387
+ attribute: attribute_name_for(node),
1388
+ key_source: key_source,
1389
+ key_expression: key_expression,
1390
+ tag: @tag_stack.last,
1391
+ scope: @collection_nodes.last,
1392
+ depth: @container_depth
1393
+ )
1394
+
1395
+ @annotations << annotation
1396
+ @annotation_of[node] = annotation
1397
+
1398
+ written = @listener_stack.flatten
1399
+ @listener_written[node] = written unless written.empty?
1400
+
1401
+ if type == :collection && key_source == :index
1402
+ @warnings << Herb::Warnings::UnkeyedCollectionWarning.new(
1403
+ node.location,
1404
+ expression_for(node),
1405
+ tag_name: keyable_tag_name_for(node)
1406
+ )
1407
+ end
1408
+
1409
+ if !annotation.anchored?
1410
+ @standing[node] = annotation
1411
+ elsif @current_open_tag
1412
+ anchored = @element_anchored[@current_open_tag] || [] #: Array[Annotation]
1413
+ anchored << annotation
1414
+
1415
+ @element_anchored[@current_open_tag] = anchored
1416
+ end
1417
+ end
1418
+
1419
+ #: (Symbol) -> Symbol?
1420
+ def anchored_type_for(type)
1421
+ return type if type == :raw_text_interpolation
1422
+ return type if Types.attribute_value?(type)
1423
+
1424
+ return nil if @in_attribute
1425
+ return :element if @in_open_tag
1426
+ return nil if @raw_text_depth.positive?
1427
+ return :raw_text if @rcdata_depth.positive?
1428
+
1429
+ type
1430
+ end
1431
+
1432
+ #: (Array[untyped]) -> Array[String]?
1433
+ def rcdata_segments(children)
1434
+ segments = [+""]
1435
+
1436
+ Array(children).each do |child|
1437
+ case child
1438
+ when Herb::AST::HTMLTextNode, Herb::AST::LiteralNode
1439
+ segments.last << child.content.to_s
1440
+ when Herb::AST::WhitespaceNode
1441
+ segments.last << child.value&.value.to_s
1442
+ when Herb::AST::ERBContentNode
1443
+ return nil unless erb_outputs?(child)
1444
+
1445
+ segments << +""
1446
+ else
1447
+ return nil
1448
+ end
1449
+ end
1450
+
1451
+ segments.size > 1 ? segments : nil
1452
+ end
1453
+
1454
+ #: (untyped) -> Array[String]
1455
+ def listener_names_for(node)
1456
+ attributes_for(node).flat_map { |attribute|
1457
+ name = attribute_name_for(attribute)&.downcase
1458
+
1459
+ next [] unless name && LISTENER_ATTRIBUTES.include?(name)
1460
+
1461
+ value = static_attribute_value(attribute)
1462
+
1463
+ next [] unless value
1464
+
1465
+ value.split.filter_map { |clause|
1466
+ rest = clause.split("->", 2).last.to_s
1467
+ target = name == "data-herb-set" ? rest.split("=", 2).first : rest
1468
+ cleaned = target.to_s.strip
1469
+
1470
+ cleaned unless cleaned.empty?
1471
+ }
1472
+ }
1473
+ end
1474
+
1475
+ #: (untyped) -> String?
1476
+ def rcdata_expression_for(node)
1477
+ outputs = Array(node.body).grep(Herb::AST::ERBContentNode) #: Array[untyped]
1478
+
1479
+ return nil unless outputs.one?
1480
+
1481
+ outputs.fetch(0).content&.value&.strip
1482
+ end
1483
+
1484
+ #: (Array[untyped]) -> bool
1485
+ def mixed_rcdata_body?(children)
1486
+ outputs = children.count { |child| child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child) }
1487
+
1488
+ return false if outputs.zero?
1489
+ return true if outputs > 1
1490
+
1491
+ children.any? { |child|
1492
+ (child.is_a?(Herb::AST::HTMLTextNode) || child.is_a?(Herb::AST::LiteralNode)) && !child.content.to_s.strip.empty?
1493
+ }
1494
+ end
1495
+
1496
+ #: (untyped) -> nil
1497
+ def refuse_mixed_rcdata(node)
1498
+ expression = expression_for(node)
1499
+ tag = @tag_stack.last
1500
+
1501
+ slot_warning(
1502
+ "`<%= #{expression} %>` shares the `<#{tag}>` content with other text. The client writes this content as one piece and would discard the rest, so the expression stays server-rendered and never updates.",
1503
+ node.location,
1504
+ :content,
1505
+ suggestion: "Make `<%= #{expression} %>` the only content of the `<#{tag}>`, or move the other text outside it."
1506
+ )
1507
+
1508
+ nil
1509
+ end
1510
+
1511
+ #: (untyped) -> Symbol
1512
+ def attribute_type_for(node)
1513
+ return :element unless attribute_name_for(node)
1514
+
1515
+ children = node.value&.children || []
1516
+
1517
+ children.one? ? :attribute : :attribute_interpolation
1518
+ end
1519
+
1520
+ #: (untyped) -> untyped
1521
+ def convert_helper_boolean_attribute(node)
1522
+ open_tag = @current_open_tag
1523
+
1524
+ return nil unless @in_open_tag && open_tag.is_a?(Herb::AST::ERBOpenTagNode)
1525
+ return nil if continuation?(node)
1526
+ return nil unless node.respond_to?(:subsequent) && node.subsequent.nil?
1527
+
1528
+ children = open_tag.children
1529
+
1530
+ position = children.index { |child| child.equal?(node) }
1531
+ return nil unless position
1532
+
1533
+ statements = node.statements
1534
+ return nil unless statements.is_a?(Array) && statements.one?
1535
+
1536
+ attribute = statements.fetch(0)
1537
+ return nil unless attribute.is_a?(Herb::AST::HTMLAttributeNode) && !dynamic?(attribute)
1538
+
1539
+ name = attribute_name_for(attribute)
1540
+ return nil unless name && Herb::HTML::Util.boolean_attribute?(name)
1541
+
1542
+ condition = @states.condition_expression(node)
1543
+ replacement = erb_output_node(%[(" #{name}" if #{condition})])
1544
+
1545
+ children[position] = replacement
1546
+ record_slot(replacement, :attribute)
1547
+
1548
+ annotation = @annotation_of[replacement]
1549
+ return nil unless annotation
1550
+
1551
+ annotation.type = :boolean_attribute
1552
+ annotation.attribute = name
1553
+ annotation.expression = condition
1554
+
1555
+ @attribute_open_tags[replacement] = open_tag
1556
+
1557
+ replacement
1558
+ end
1559
+
1560
+ #: (untyped, untyped, Integer, untyped, Integer) -> void
1561
+ def record_named_element(node, name_attribute, base, base_scope, base_depth)
1562
+ name = static_attribute_value(name_attribute)
1563
+
1564
+ unless name && !name.empty?
1565
+ return slot_error("`#{NAME_ATTRIBUTE}` on `<#{node.tag_name&.value}>` is computed or empty. A slot name is an address the browser looks up, so it has to be known before the page renders.", name_attribute.location, :name, suggestion: "Give it a static, non-empty value.")
1566
+ end
1567
+
1568
+ candidates = @annotations[base..].to_a.select { |annotation|
1569
+ annotation.scope.equal?(base_scope) && annotation.depth == base_depth && annotation.nameable?
1570
+ }
1571
+
1572
+ @named_elements << {
1573
+ node: node,
1574
+ open_tag: node.open_tag,
1575
+ attribute: name_attribute,
1576
+ name: name,
1577
+ candidates: candidates,
1578
+ scope: base_scope,
1579
+ }
1580
+ end
1581
+
1582
+ #: (untyped) -> String?
1583
+ def static_attribute_value(attribute)
1584
+ children = attribute.value&.children || []
1585
+
1586
+ return nil unless children.all?(Herb::AST::LiteralNode)
1587
+
1588
+ children.map { |child| child.content.to_s }.join.strip
1589
+ end
1590
+
1591
+ #: () -> void
1592
+ def apply_names
1593
+ taken = {} #: Hash[untyped, Hash[String, Annotation]]
1594
+
1595
+ @named_elements.each { |named| bind_name(named, taken[named[:scope]] ||= {}) }
1596
+ end
1597
+
1598
+ #: (Hash[Symbol, untyped], Hash[String, Annotation]) -> void
1599
+ def bind_name(named, scope_names)
1600
+ name = named[:name] #: String
1601
+ annotation = resolve_named_slot(named)
1602
+
1603
+ return unless annotation
1604
+
1605
+ if scope_names.key?(name)
1606
+ return slot_error("Two slots in the same scope are both named `#{name}`. A slot name is an address, and two slots cannot share one.", name_location(named), :name, suggestion: "Give one of them a different name.")
1607
+ end
1608
+
1609
+ if (existing = annotation.name)
1610
+ return slot_error("`#{NAME_ATTRIBUTE}=\"#{name}\"` claims the slot already named `#{existing}`. Both elements hold the same slot.", name_location(named), :name, suggestion: "Keep one of the two names, or wrap what each should address in its own element.")
1611
+ end
1612
+
1613
+ if attribute_conflict?(name, annotation, named[:scope])
1614
+ return slot_error("The name `#{name}` collides with the `#{name}` attribute slot in the same scope. An attribute slot is already addressable by its attribute.", name_location(named), :name, suggestion: "Drop the name, or rename it.")
1615
+ end
1616
+
1617
+ scope_names[name] = annotation
1618
+
1619
+ annotation.name = name
1620
+ end
1621
+
1622
+ #: (Hash[Symbol, untyped]) -> Annotation?
1623
+ def resolve_named_slot(named)
1624
+ name = named[:name]
1625
+ tag = named[:node].tag_name&.value
1626
+ candidates = named[:candidates].reject(&:dropped).map(&:survivor).uniq
1627
+
1628
+ if candidates.empty?
1629
+ return slot_error("`#{NAME_ATTRIBUTE}=\"#{name}\"` on `<#{tag}>` names no slot, since the element holds nothing dynamic.", name_location(named), :name, suggestion: "Move the name onto an element that wraps an ERB output, or remove it.")
1630
+ end
1631
+
1632
+ unless candidates.one?
1633
+ return slot_error("`#{NAME_ATTRIBUTE}=\"#{name}\"` on `<#{tag}>` is ambiguous between #{candidates.size} slots.", name_location(named), :name, suggestion: "Wrap the one it should name in its own element.")
1634
+ end
1635
+
1636
+ candidates.fetch(0)
1637
+ end
1638
+
1639
+ #: (Hash[Symbol, untyped]) -> Herb::Location?
1640
+ def name_location(named)
1641
+ named[:attribute]&.location || named[:node]&.location
1642
+ end
1643
+
1644
+ #: (String, Annotation, untyped) -> bool
1645
+ def attribute_conflict?(name, annotation, scope)
1646
+ @annotations.any? { |other|
1647
+ !other.gone? && other.attribute == name && !other.equal?(annotation) && other.scope.equal?(scope)
1648
+ }
1649
+ end
1650
+
1651
+ #: (untyped) -> void
1652
+ def detect_keyed_elements(node)
1653
+ iteration = node.is_a?(Herb::AST::ERBIterationBlockNode) || node.is_a?(Herb::AST::ERBForNode) || node.is_a?(Herb::AST::ERBWhileNode) || node.is_a?(Herb::AST::ERBUntilNode)
1654
+
1655
+ BRANCH_BODY_PROPERTIES.each do |property|
1656
+ next unless node.respond_to?(property)
1657
+
1658
+ array = node.send(property)
1659
+
1660
+ next unless array.is_a?(Array)
1661
+
1662
+ sole_element = iteration && array.grep(Herb::AST::HTMLElementNode).one?
1663
+
1664
+ array.each do |child|
1665
+ detect_keyed_elements(child)
1666
+
1667
+ next unless child.is_a?(Herb::AST::HTMLElementNode)
1668
+ next if sole_element
1669
+
1670
+ expression = keyed_element_expression(child)
1671
+
1672
+ next unless expression
1673
+
1674
+ remove_herb_key_attribute(child)
1675
+
1676
+ @keyed_elements[child] = expression
1677
+ end
1678
+ end
1679
+ end
1680
+
1681
+ #: (untyped) -> String?
1682
+ def keyed_element_expression(element)
1683
+ attribute = attributes_for(element).find { |candidate| attribute_name_for(candidate)&.downcase == KEY_ATTRIBUTE }
1684
+
1685
+ return nil unless attribute
1686
+
1687
+ key_expression_for(attribute)
1688
+ end
1689
+
1690
+ #: (untyped) -> void
1691
+ def remove_herb_key_attribute(element)
1692
+ open_tag = element.open_tag
1693
+
1694
+ return unless OPEN_TAG_TYPES.any? { |type| open_tag.is_a?(type) }
1695
+
1696
+ children = open_tag.children
1697
+ at = children.index { |child| attribute_name_for(child)&.downcase == KEY_ATTRIBUTE }
1698
+
1699
+ return unless at
1700
+
1701
+ children.delete_at(at)
1702
+ children.delete_at(at - 1) if at.positive? && children[at - 1].is_a?(Herb::AST::WhitespaceNode)
1703
+ end
1704
+
1705
+ #: (untyped, Symbol) -> [Symbol?, String?]
1706
+ def keys_for(node, type)
1707
+ return key_for(node) if type == :collection
1708
+ return [:herb_key, @keyed_elements.fetch(node)] if type == :keyed
1709
+
1710
+ [nil, nil]
1711
+ end
1712
+
1713
+ #: (untyped) -> [Symbol, String?]
1714
+ def key_for(node)
1715
+ body = collection_body(node)
1716
+
1717
+ directive = key_directive_in(body)
1718
+ return [:directive, directive] if directive
1719
+
1720
+ elements = body.grep(Herb::AST::HTMLElementNode)
1721
+ return [:index, nil] unless elements.one?
1722
+
1723
+ attributes = attributes_for(elements.first)
1724
+
1725
+ [KEY_ATTRIBUTE, "id"].each do |name|
1726
+ attribute = attributes.find { |candidate| attribute_name_for(candidate)&.downcase == name }
1727
+ next unless attribute
1728
+
1729
+ expression = key_expression_for(attribute)
1730
+ next unless expression
1731
+
1732
+ return [name == KEY_ATTRIBUTE ? :herb_key : :id, expression]
1733
+ end
1734
+
1735
+ [:index, nil]
1736
+ end
1737
+
1738
+ #: (untyped) -> String?
1739
+ def keyable_tag_name_for(node)
1740
+ elements = collection_body(node).grep(Herb::AST::HTMLElementNode)
1741
+ return nil unless elements.one?
1742
+
1743
+ elements.first.tag_name&.value&.downcase
1744
+ end
1745
+
1746
+ #: (Array[untyped]) -> String?
1747
+ def key_directive_in(body)
1748
+ body.each do |child|
1749
+ next unless child.is_a?(Herb::AST::HerbDirectiveNode)
1750
+ next unless child.directive == "key"
1751
+
1752
+ expression = child.argument
1753
+
1754
+ return expression unless expression.empty?
1755
+ end
1756
+
1757
+ nil
1758
+ end
1759
+
1760
+ #: (untyped) -> String?
1761
+ def key_expression_for(attribute)
1762
+ parts = key_parts_for(attribute)
1763
+
1764
+ return nil if parts.nil?
1765
+ return nil unless parts.any? { |kind, _| kind == :expression }
1766
+ return parts.first[1] if parts.one?
1767
+
1768
+ interpolated = parts.map { |kind, value|
1769
+ kind == :expression ? "\#{#{value}}" : escape_key_literal(value)
1770
+ }
1771
+
1772
+ %("#{interpolated.join}")
1773
+ end
1774
+
1775
+ #: (untyped) -> Array[[Symbol, String]]?
1776
+ def key_parts_for(attribute)
1777
+ children = attribute.value&.children || []
1778
+ return nil if children.empty?
1779
+
1780
+ children.map { |child|
1781
+ case child
1782
+ when Herb::AST::ERBContentNode
1783
+ expression = child.content&.value.to_s.strip
1784
+ return nil if expression.empty?
1785
+
1786
+ [:expression, expression]
1787
+ when Herb::AST::LiteralNode
1788
+ [:literal, child.content.to_s]
1789
+ when Herb::AST::RubyLiteralNode
1790
+ expression = child.content.to_s.strip
1791
+ return nil if expression.empty?
1792
+
1793
+ [:expression, expression]
1794
+ else
1795
+ return nil
1796
+ end
1797
+ }
1798
+ end
1799
+
1800
+ #: (String) -> String
1801
+ def escape_key_literal(literal)
1802
+ literal.gsub(/["\\]|\#\{/) { |match| "\\#{match}" }
1803
+ end
1804
+
1805
+ #: (untyped) -> Array[untyped]
1806
+ def collection_body(node)
1807
+ BRANCH_BODY_PROPERTIES.filter_map { |property|
1808
+ node.send(property) if node.respond_to?(property)
1809
+ }.flatten
1810
+ end
1811
+
1812
+ #: (untyped) -> Array[Herb::AST::HTMLAttributeNode]
1813
+ def attributes_for(element)
1814
+ open_tag = element.open_tag
1815
+ return [] unless OPEN_TAG_TYPES.any? { |type| open_tag.is_a?(type) }
1816
+
1817
+ open_tag.children.grep(Herb::AST::HTMLAttributeNode)
1818
+ end
1819
+
1820
+ #: (untyped) -> String?
1821
+ def attribute_name_for(node)
1822
+ return nil unless node.is_a?(Herb::AST::HTMLAttributeNode)
1823
+
1824
+ children = node.name&.children || []
1825
+
1826
+ return nil if children.empty?
1827
+ return nil unless children.all?(Herb::AST::LiteralNode)
1828
+
1829
+ children.filter_map { |child| child.content if child.respond_to?(:content) }.join
1830
+ end
1831
+
1832
+ #: (untyped) -> bool
1833
+ def dynamic?(node)
1834
+ return true if node.type.to_s.start_with?("AST_ERB_")
1835
+ return true if node.is_a?(Herb::AST::RubyLiteralNode)
1836
+
1837
+ node.compact_child_nodes.any? { |child| dynamic?(child) }
1838
+ end
1839
+
1840
+ def expression_for(node)
1841
+ return attribute_expression_for(node) if node.is_a?(Herb::AST::HTMLAttributeNode)
1842
+ return rcdata_expression_for(node) if node.is_a?(Herb::AST::HTMLElementNode)
1843
+ return nil unless node.respond_to?(:content)
1844
+
1845
+ content = node.content
1846
+ value = content.respond_to?(:value) ? content.value : content
1847
+
1848
+ value&.to_s&.strip
1849
+ end
1850
+
1851
+ #: (untyped) -> String?
1852
+ def attribute_expression_for(node)
1853
+ children = node.value&.children || []
1854
+ literal = children.grep(Herb::AST::RubyLiteralNode)
1855
+
1856
+ return literal.fetch(0).content.to_s.strip if literal.one? && children.one?
1857
+ return nil unless children.all? { |child| child.is_a?(Herb::AST::LiteralNode) || child.is_a?(Herb::AST::ERBContentNode) }
1858
+
1859
+ outputs = children.grep(Herb::AST::ERBContentNode)
1860
+
1861
+ return nil unless outputs.one?
1862
+ return nil unless erb_outputs?(outputs.fetch(0))
1863
+
1864
+ outputs.fetch(0).content&.value&.strip
1865
+ end
1866
+
1867
+ def insert_markers(node)
1868
+ anchored = content_anchor_index(node)
1869
+
1870
+ each_child_array(node) do |array|
1871
+ index = 0
1872
+
1873
+ while index < array.size
1874
+ child = array[index]
1875
+ slot_index = @standing[child]&.survivor&.index
1876
+
1877
+ insert_markers(child)
1878
+ wrap_items(child, slot_index)
1879
+ mark_branches(child, slot_index)
1880
+
1881
+ if slot_index && slot_index != anchored
1882
+ if @slots[slot_index].type == :keyed
1883
+ park_keyed(slot_index, child)
1884
+
1885
+ array.insert(
1886
+ index,
1887
+ text_node(@markers.keyed_open_prefix(slot_index)),
1888
+ erb_code_node("#{@bufvar} << ::Herb::Engine.raw((#{@slots[slot_index].key_expression}).to_s)"),
1889
+ text_node(@markers.keyed_open_suffix)
1890
+ )
1891
+ array.insert(index + 4, comment_node(@markers.slot_close(slot_index)))
1892
+
1893
+ index += 5
1894
+ else
1895
+ array.insert(index, comment_node(@markers.slot_open(slot_index, @slots[slot_index].type)))
1896
+ array.insert(index + 2, comment_node(@markers.slot_close(slot_index)))
1897
+
1898
+ index += 3
1899
+ end
1900
+ else
1901
+ index += 1
1902
+ end
1903
+ end
1904
+ end
1905
+
1906
+ anchor_attributes(node, anchored)
1907
+ end
1908
+
1909
+ #: (untyped) -> Integer?
1910
+ def content_anchor_index(node)
1911
+ return nil unless node.is_a?(Herb::AST::HTMLElementNode)
1912
+
1913
+ body = node.body
1914
+ return nil unless body.is_a?(Array) && body.size == 1
1915
+
1916
+ annotation = @standing[body[0]]&.survivor
1917
+ return nil unless annotation
1918
+ return nil unless annotation.type == :child
1919
+ return nil if @render_bindings.key?(body[0])
1920
+
1921
+ annotation.index
1922
+ end
1923
+
1924
+ #: (untyped, Integer?) -> void
1925
+ def mark_branches(node, slot_index)
1926
+ return unless slot_index
1927
+ return unless @slots[slot_index].type == :conditional
1928
+
1929
+ branch_bodies(node).each_with_index do |body, branch_index|
1930
+ body.unshift(text_node(@markers.branch(slot_index, branch_index)))
1931
+
1932
+ park_branch(@markers.statics_key(slot_index, branch_index), body)
1933
+ end
1934
+ end
1935
+
1936
+ #: (String, Array[untyped]) -> void
1937
+ def park_branch(key, body)
1938
+ statics = @statics
1939
+ return unless statics
1940
+
1941
+ markup = Statics.new(@standing).markup(body)
1942
+ return unless markup
1943
+
1944
+ statics[key] = markup
1945
+ end
1946
+
1947
+ #: (Herb::AST::DocumentNode) -> void
1948
+ def append_statics(document_node)
1949
+ return unless client?
1950
+
1951
+ statics = @statics
1952
+ entries = (statics || {}).sort_by { |key, _| key.split(":").map(&:to_i) } #: Array[[String, (String | Array[untyped])]]
1953
+
1954
+ withheld = deferred_entries.keys.to_set { |index| "#{index}:0" }
1955
+ entries.reject! { |key, _| withheld.include?(key) }
1956
+
1957
+ entries.concat(fallback_statics_entries)
1958
+
1959
+ return if entries.empty?
1960
+
1961
+ seen = entries.map { |key, _| "#{COVERED}[#{covered_key(key).inspect}]" }.join(" && ")
1962
+
1963
+ nodes = [
1964
+ erb_code_node("unless #{seen}"),
1965
+ text_node(@markers.statics_open(identifier, version))
1966
+ ] #: Array[Herb::AST::Node]
1967
+
1968
+ entries.each do |key, parked|
1969
+ reference = "#{COVERED}[#{covered_key(key).inspect}]"
1970
+
1971
+ nodes.push(erb_code_node("unless #{reference}"))
1972
+
1973
+ if parked.is_a?(String)
1974
+ nodes.push(text_node(parked))
1975
+ else
1976
+ nodes.concat(parked)
1977
+ end
1978
+
1979
+ nodes.push(erb_code_node("#{reference} = true"), erb_code_node("end"))
1980
+ end
1981
+
1982
+ nodes.push(text_node(@markers.statics_close), erb_code_node("end"))
1983
+
1984
+ document_node.children.unshift(erb_code_node("#{COVERED} ||= {}"))
1985
+ document_node.children.concat(nodes)
1986
+ end
1987
+
1988
+ #: (Herb::AST::DocumentNode) -> void
1989
+ def expose_fallbacks(document_node)
1990
+ return if @fragment_fallbacks.empty?
1991
+
1992
+ @exposed_fallbacks = @fragment_fallbacks.each_value.flat_map { |body| body } #: Array[untyped]
1993
+
1994
+ document_node.children.concat(@exposed_fallbacks)
1995
+
1996
+ nil
1997
+ end
1998
+
1999
+ #: (Herb::AST::DocumentNode) -> void
2000
+ def withdraw_fallbacks(document_node)
2001
+ exposed = @exposed_fallbacks
2002
+
2003
+ return if exposed.empty?
2004
+
2005
+ document_node.children.reject! { |child| exposed.any? { |node| node.equal?(child) } }
2006
+ @exposed_fallbacks = [] #: Array[untyped]
2007
+
2008
+ nil
2009
+ end
2010
+
2011
+ #: () -> Array[[String, Array[untyped]]]
2012
+ def fallback_statics_entries
2013
+ @fragment_fallbacks.filter_map { |node, body|
2014
+ index = @indices[node]
2015
+
2016
+ next unless index
2017
+
2018
+ [@markers.statics_key(index, 1), [text_node(@markers.branch(index, 1)), *marked_fallback(body)]]
2019
+ }
2020
+ end
2021
+
2022
+ #: (Array[untyped]) -> Array[untyped]
2023
+ def marked_fallback(children)
2024
+ children.flat_map do |child|
2025
+ insert_markers(child)
2026
+
2027
+ slot_index = @standing[child]&.survivor&.index
2028
+
2029
+ next [child] unless slot_index
2030
+
2031
+ [
2032
+ comment_node(@markers.slot_open(slot_index, @slots[slot_index].type)),
2033
+ child,
2034
+ comment_node(@markers.slot_close(slot_index))
2035
+ ]
2036
+ end
2037
+ end
2038
+
2039
+ #: (String) -> String
2040
+ def covered_key(key)
2041
+ "#{identifier}:#{key}"
2042
+ end
2043
+
2044
+ #: (untyped) -> Array[Array[Herb::AST::Node]]
2045
+ def branch_bodies(node)
2046
+ bodies = [] #: Array[Array[Herb::AST::Node]]
2047
+
2048
+ if node.respond_to?(:conditions)
2049
+ node.conditions.each { |arm| bodies << arm.statements if arm.respond_to?(:statements) }
2050
+ elsif node.respond_to?(:statements)
2051
+ bodies << node.statements
2052
+ end
2053
+
2054
+ current = continuation_of(node) #: untyped
2055
+
2056
+ while (branch = current)
2057
+ bodies << branch.statements if branch.respond_to?(:statements)
2058
+
2059
+ current = continuation_of(branch)
2060
+ end
2061
+
2062
+ bodies
2063
+ end
2064
+
2065
+ #: (Herb::AST::Node, Integer?) -> void
2066
+ def wrap_items(node, slot_index)
2067
+ return unless slot_index
2068
+
2069
+ slot = @slots[slot_index]
2070
+ return unless slot.type == :collection && slot.key_expression
2071
+
2072
+ BRANCH_BODY_PROPERTIES.each do |property|
2073
+ next unless node.respond_to?(property)
2074
+
2075
+ body = node.send(property)
2076
+ next unless body.is_a?(Array) && !body.empty?
2077
+
2078
+ key = @markers.item_statics_key(slot_index)
2079
+ parked = park_item(key, body)
2080
+
2081
+ body.unshift(
2082
+ text_node(@markers.item_open_prefix(slot_index)),
2083
+ erb_code_node("#{@bufvar} << ::Herb::Engine.raw((#{slot.key_expression}).to_s)"),
2084
+ text_node(@markers.item_open_suffix)
2085
+ )
2086
+
2087
+ body.insert(3, erb_code_node(%(#{COVERED}[#{covered_key(key).inspect}] = true))) if parked && client?
2088
+
2089
+ body.push(text_node(@markers.item_close(slot_index)))
2090
+ end
2091
+ end
2092
+
2093
+ #: (untyped) -> Array[String]?
2094
+ def attribute_segments(node)
2095
+ segments = [+""]
2096
+
2097
+ (node.value&.children || []).each do |child|
2098
+ case child
2099
+ when Herb::AST::LiteralNode
2100
+ segments.last << child.content.to_s
2101
+ when Herb::AST::ERBContentNode
2102
+ return nil unless erb_outputs?(child)
2103
+
2104
+ segments << +""
2105
+ when Herb::AST::ERBIfNode, Herb::AST::ERBUnlessNode, Herb::AST::ERBCaseNode
2106
+ segments << +""
2107
+ else
2108
+ return nil
2109
+ end
2110
+ end
2111
+
2112
+ segments.size > 1 ? segments : nil
2113
+ end
2114
+
2115
+ #: (Integer, untyped) -> void
2116
+ def park_keyed(slot_index, element)
2117
+ statics = @statics
2118
+ return unless statics
2119
+
2120
+ markup = Statics.new(@standing).markup([element.open_tag, *element.body, element.close_tag].compact)
2121
+ return unless markup
2122
+
2123
+ statics[@markers.item_statics_key(slot_index)] = "#{@markers.branch(slot_index, Markers::ITEM_STATICS)}#{markup}"
2124
+ end
2125
+
2126
+ #: (untyped, untyped) -> Array[untyped]?
2127
+ def park_item(key, body)
2128
+ statics = @statics
2129
+ return unless statics
2130
+
2131
+ slot_index = key.split(":").first.to_i
2132
+ markup = Statics.new(@standing).markup(body)
2133
+ return unless markup
2134
+
2135
+ statics[key] = [
2136
+ @markers.branch(slot_index, Markers::ITEM_STATICS),
2137
+ @markers.item_open_prefix(slot_index),
2138
+ @markers.item_open_suffix,
2139
+ markup,
2140
+ @markers.item_close(slot_index)
2141
+ ].join
2142
+ end
2143
+
2144
+ def anchor_attributes(node, content_index = nil)
2145
+ return unless node.is_a?(Herb::AST::HTMLElementNode)
2146
+
2147
+ open_tag = node.open_tag
2148
+ targets = open_tags_for(open_tag)
2149
+
2150
+ return if targets.empty?
2151
+
2152
+ anchors = (@element_anchored[open_tag] || []).map { |annotation|
2153
+ slot = @slots.fetch(annotation.survivor.index)
2154
+
2155
+ [slot.index, slot.type, anchor_name_for(slot)]
2156
+ }
2157
+
2158
+ anchors << [content_index, :child, nil] if content_index
2159
+
2160
+ return if anchors.empty?
2161
+
2162
+ value = @markers.element_anchors(anchors)
2163
+
2164
+ targets.each { |target| target.children << attribute_node("data-herb-slot", value) }
2165
+ end
2166
+
2167
+ #: (Slot) -> String?
2168
+ def anchor_name_for(slot)
2169
+ name = slot.attribute
2170
+
2171
+ return nil if name.nil? || name.include?(",")
2172
+
2173
+ name
2174
+ end
2175
+
2176
+ #: (Herb::AST::Node) { (Array[untyped]) -> void } -> void
2177
+ def each_child_array(node)
2178
+ BRANCH_BODY_PROPERTIES.each do |property|
2179
+ next unless node.respond_to?(property)
2180
+
2181
+ array = node.send(property)
2182
+ yield array if array.is_a?(Array)
2183
+ end
2184
+
2185
+ (BRANCH_CONTINUATION_PROPERTIES + [:end_node, :open_tag]).each do |property|
2186
+ next unless node.respond_to?(property)
2187
+
2188
+ child = node.send(property)
2189
+ insert_markers(child) if child
2190
+ end
2191
+ end
2192
+
2193
+ #: (Herb::AST::DocumentNode) -> void
2194
+ def wrap_region(document_node)
2195
+ document_node.children.unshift(
2196
+ erb_code_node("#{OCCURRENCE} = #{occurrence_expression}"),
2197
+ *region_open
2198
+ )
2199
+
2200
+ document_node.children.push(comment_node(@markers.region_close(identifier)))
2201
+ end
2202
+
2203
+ #: () -> void
2204
+ def wrap_displaced
2205
+ @displaced.each do |node|
2206
+ BRANCH_BODY_PROPERTIES.each do |property|
2207
+ next unless node.respond_to?(property)
2208
+
2209
+ body = node.send(property)
2210
+ next unless body.is_a?(Array) && !body.empty?
2211
+
2212
+ body.unshift(*region_open)
2213
+ body.push(comment_node(@markers.region_close(identifier)))
2214
+ end
2215
+ end
2216
+ end
2217
+
2218
+ def region_open
2219
+ [
2220
+ text_node(@markers.region_open_prefix(identifier, version)),
2221
+ erb_output_node(OCCURRENCE),
2222
+ text_node(@markers.region_open_suffix)
2223
+ ]
2224
+ end
2225
+
2226
+ def comment_node(text)
2227
+ Herb::AST::HTMLCommentNode.build(
2228
+ comment_start: token(:html_comment_start, text),
2229
+ comment_end: token(:html_comment_end, "")
2230
+ )
2231
+ end
2232
+
2233
+ def attribute_node(name, value)
2234
+ name_node = Herb::AST::HTMLAttributeNameNode.build(children: [literal(name)])
2235
+
2236
+ value_node = Herb::AST::HTMLAttributeValueNode.build(
2237
+ open_quote: token(:quote, '"'),
2238
+ children: [literal(value)],
2239
+ close_quote: token(:quote, '"'),
2240
+ quoted: true
2241
+ )
2242
+
2243
+ Herb::AST::HTMLAttributeNode.build(name: name_node, equals: token(:equals, "="), value: value_node)
2244
+ end
2245
+
2246
+ def literal(content)
2247
+ Herb::AST::LiteralNode.build(content: content.dup)
2248
+ end
2249
+
2250
+ def token(type, value)
2251
+ Herb::Token.new(value.dup, Herb::Range.zero, Herb::Location.zero, type.to_s)
2252
+ end
2253
+ end
2254
+ end
2255
+ end
2256
+ end