herb 0.10.4-x86_64-linux-gnu → 0.11.1-x86_64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (793) 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 +1243 -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 +859 -374
  292. data/ext/herb/nodes.h +3 -2
  293. data/ext/herb/parser_options_helpers.c +151 -0
  294. data/ext/herb/parser_options_helpers.h +14 -0
  295. data/herb.gemspec +2 -2
  296. data/lib/herb/3.2/herb.so +0 -0
  297. data/lib/herb/3.3/herb.so +0 -0
  298. data/lib/herb/3.4/herb.so +0 -0
  299. data/lib/herb/4.0/herb.so +0 -0
  300. data/lib/herb/action_view/helper_registry.rb +1358 -62
  301. data/lib/herb/analysis/partial_declaration.rb +140 -0
  302. data/lib/herb/analysis/partial_index.rb +189 -0
  303. data/lib/herb/analysis/partial_resolution.rb +161 -0
  304. data/lib/herb/analysis/partial_resolver.rb +144 -0
  305. data/lib/herb/analysis/project_index.rb +152 -0
  306. data/lib/herb/{action_view → analysis}/render_analyzer.rb +287 -77
  307. data/lib/herb/analysis/render_graph/ancestor_chain.rb +27 -0
  308. data/lib/herb/analysis/render_graph/builder.rb +376 -0
  309. data/lib/herb/analysis/render_graph/call_frame.rb +9 -0
  310. data/lib/herb/analysis/render_graph/call_site_location.rb +23 -0
  311. data/lib/herb/analysis/render_graph/inferred_signature.rb +21 -0
  312. data/lib/herb/analysis/render_graph/partial_call_site.rb +40 -0
  313. data/lib/herb/analysis/render_graph/partial_context.rb +46 -0
  314. data/lib/herb/analysis/render_graph/template_roots.rb +31 -0
  315. data/lib/herb/analysis/render_graph.rb +285 -0
  316. data/lib/herb/analysis/ruby_locals_index/local.rb +25 -0
  317. data/lib/herb/analysis/ruby_locals_index/named_reference.rb +22 -0
  318. data/lib/herb/analysis/ruby_locals_index/offset_table.rb +44 -0
  319. data/lib/herb/analysis/ruby_locals_index/reference_collector.rb +80 -0
  320. data/lib/herb/analysis/ruby_locals_index/state_locals.rb +49 -0
  321. data/lib/herb/analysis/ruby_locals_index.rb +165 -0
  322. data/lib/herb/analysis/ruby_reference.rb +67 -0
  323. data/lib/herb/analysis/template_dependencies/dependency_collector.rb +179 -0
  324. data/lib/herb/analysis/template_dependencies/node_dependency_collector.rb +309 -0
  325. data/lib/herb/analysis/template_dependencies.rb +562 -0
  326. data/lib/herb/ast/erb_render_node.rb +26 -94
  327. data/lib/herb/ast/helpers.rb +90 -10
  328. data/lib/herb/ast/herb_directive_node.rb +18 -0
  329. data/lib/herb/ast/node.rb +32 -2
  330. data/lib/herb/ast/nodes.rb +1368 -105
  331. data/lib/herb/backend.rb +73 -0
  332. data/lib/herb/bootstrap.rb +8 -8
  333. data/lib/herb/cli.rb +550 -32
  334. data/lib/herb/colors.rb +14 -0
  335. data/lib/herb/configuration.rb +71 -35
  336. data/lib/herb/defaults.yml +2 -1
  337. data/lib/herb/dev/assets.rb +57 -0
  338. data/lib/herb/dev/classifier.rb +84 -0
  339. data/lib/herb/dev/embedded.rb +27 -0
  340. data/lib/herb/dev/pipeline.rb +305 -0
  341. data/lib/herb/dev/protocol.rb +221 -0
  342. data/lib/herb/dev/runner.rb +180 -175
  343. data/lib/herb/dev/server.rb +89 -26
  344. data/lib/herb/dev/server_entry.rb +41 -6
  345. data/lib/herb/dev/watcher.rb +181 -0
  346. data/lib/herb/dev.rb +163 -0
  347. data/lib/herb/diagnostic/formatter.rb +88 -0
  348. data/lib/herb/diagnostic.rb +172 -0
  349. data/lib/herb/diff/operation.rb +36 -0
  350. data/lib/herb/diff/result.rb +62 -0
  351. data/lib/herb/diff.rb +10 -0
  352. data/lib/herb/engine/collector.rb +62 -0
  353. data/lib/herb/engine/compiler.rb +387 -113
  354. data/lib/herb/engine/component_tags/constant_resolver.rb +28 -0
  355. data/lib/herb/engine/component_tags/partial_resolver.rb +54 -0
  356. data/lib/herb/engine/component_tags/resolver.rb +32 -0
  357. data/lib/herb/engine/component_tags/visitor.rb +231 -0
  358. data/lib/herb/engine/css_inliner/inliner.rb +101 -0
  359. data/lib/herb/engine/css_inliner/visitor.rb +174 -0
  360. data/lib/herb/engine/errors.rb +122 -0
  361. data/lib/herb/engine/helpers.rb +82 -0
  362. data/lib/herb/engine/inline_render/inliner.rb +122 -0
  363. data/lib/herb/engine/inline_render/visitor.rb +267 -0
  364. data/lib/herb/engine/runtime/entry.rb +62 -0
  365. data/lib/herb/engine/runtime/error_page.rb +362 -0
  366. data/lib/herb/engine/runtime/html_safe_assertions.rb +217 -0
  367. data/lib/herb/engine/runtime/journal/summary.rb +227 -0
  368. data/lib/herb/engine/runtime/journal.rb +379 -0
  369. data/lib/herb/engine/runtime/middleware.rb +212 -0
  370. data/lib/herb/engine/runtime/observations.rb +73 -0
  371. data/lib/herb/engine/runtime/report.rb +204 -0
  372. data/lib/herb/engine/runtime/session.rb +437 -0
  373. data/lib/herb/engine/runtime.rb +21 -0
  374. data/lib/herb/engine/scoped_style/channel.rb +66 -0
  375. data/lib/herb/engine/scoped_style/collector.rb +74 -0
  376. data/lib/herb/engine/scoped_style/visitor.rb +533 -0
  377. data/lib/herb/engine/slots/annotation.rb +98 -0
  378. data/lib/herb/engine/slots/bindings.rb +61 -0
  379. data/lib/herb/engine/slots/callee.rb +118 -0
  380. data/lib/herb/engine/slots/component_definitions.rb +67 -0
  381. data/lib/herb/engine/slots/components/async.rb +24 -0
  382. data/lib/herb/engine/slots/components/base.rb +132 -0
  383. data/lib/herb/engine/slots/components/deferred.rb +81 -0
  384. data/lib/herb/engine/slots/components/fallback.rb +27 -0
  385. data/lib/herb/engine/slots/components/fragment.rb +99 -0
  386. data/lib/herb/engine/slots/components/lazy.rb +24 -0
  387. data/lib/herb/engine/slots/components.rb +52 -0
  388. data/lib/herb/engine/slots/dependencies/channel.rb +72 -0
  389. data/lib/herb/engine/slots/dependencies.rb +269 -0
  390. data/lib/herb/engine/slots/dynamics_compiler.rb +864 -0
  391. data/lib/herb/engine/slots/identifier.rb +35 -0
  392. data/lib/herb/engine/slots/manifest/channel.rb +74 -0
  393. data/lib/herb/engine/slots/manifest/collector.rb +75 -0
  394. data/lib/herb/engine/slots/markers.rb +152 -0
  395. data/lib/herb/engine/slots/render_bindings.rb +141 -0
  396. data/lib/herb/engine/slots/schema_compiler.rb +94 -0
  397. data/lib/herb/engine/slots/state_anchor.rb +94 -0
  398. data/lib/herb/engine/slots/state_compiler.rb +1325 -0
  399. data/lib/herb/engine/slots/state_directives.rb +854 -0
  400. data/lib/herb/engine/slots/state_kinds.rb +54 -0
  401. data/lib/herb/engine/slots/state_operators.rb +36 -0
  402. data/lib/herb/engine/slots/state_overrides.rb +79 -0
  403. data/lib/herb/engine/slots/state_predicates.rb +65 -0
  404. data/lib/herb/engine/slots/state_transforms.rb +42 -0
  405. data/lib/herb/engine/slots/statics.rb +185 -0
  406. data/lib/herb/engine/slots/subtree.rb +82 -0
  407. data/lib/herb/engine/slots/subtree_compiler.rb +255 -0
  408. data/lib/herb/engine/slots/types.rb +42 -0
  409. data/lib/herb/engine/slots/visitor.rb +2267 -0
  410. data/lib/herb/engine/validators/accessibility_validator.rb +3 -3
  411. data/lib/herb/engine/validators/base.rb +36 -0
  412. data/lib/herb/engine/validators/generator_template_validator.rb +39 -0
  413. data/lib/herb/engine/validators/nesting_validator.rb +3 -3
  414. data/lib/herb/engine/validators/render_validator.rb +13 -33
  415. data/lib/herb/engine/validators/security_validator.rb +3 -17
  416. data/lib/herb/engine/validators.rb +43 -0
  417. data/lib/herb/engine/visitors/auto_close_omitted_tags_visitor.rb +48 -0
  418. data/lib/herb/engine/visitors/content_for_visitor.rb +127 -0
  419. data/lib/herb/engine/{debug_visitor.rb → visitors/debug_visitor.rb} +121 -93
  420. data/lib/herb/engine/visitors/html_safe_assertions_visitor.rb +281 -0
  421. data/lib/herb/engine/visitors/instrumentation_visitor.rb +290 -0
  422. data/lib/herb/engine/visitors/optimize_visitor.rb +728 -0
  423. data/lib/herb/engine/visitors/remove_comments_visitor.rb +106 -0
  424. data/lib/herb/engine/visitors/source_attribution_visitor.rb +177 -0
  425. data/lib/herb/engine.rb +218 -259
  426. data/lib/herb/ensure_installed.rb +43 -0
  427. data/lib/herb/errors.rb +646 -157
  428. data/lib/herb/fingerprint.rb +82 -0
  429. data/lib/herb/html/elements.rb +18 -0
  430. data/lib/herb/html/util.rb +21 -2
  431. data/lib/herb/lex_result.rb +0 -10
  432. data/lib/herb/locate.rb +163 -0
  433. data/lib/herb/location.rb +20 -0
  434. data/lib/herb/parse_result.rb +17 -11
  435. data/lib/herb/parser_options.rb +65 -46
  436. data/lib/herb/position.rb +19 -0
  437. data/lib/herb/prism_inspect.rb +1 -4
  438. data/lib/herb/project.rb +28 -15
  439. data/lib/herb/range.rb +5 -0
  440. data/lib/herb/result.rb +2 -2
  441. data/lib/herb/ruby_program.rb +110 -0
  442. data/lib/herb/source_path.rb +232 -0
  443. data/lib/herb/token.rb +14 -3
  444. data/lib/herb/version.rb +1 -1
  445. data/lib/herb/visitor/context/origin.rb +52 -0
  446. data/lib/herb/visitor/context/replacements.rb +59 -0
  447. data/lib/herb/visitor/context.rb +169 -0
  448. data/lib/herb/visitor/context_aware.rb +45 -0
  449. data/lib/herb/visitor/diagnostics.rb +148 -0
  450. data/lib/herb/visitor/experimental.rb +33 -0
  451. data/lib/herb/visitor/parser_option_requirements.rb +151 -0
  452. data/lib/herb/visitor/stack.rb +192 -0
  453. data/lib/herb/visitor.rb +47 -1
  454. data/lib/herb/warnings.rb +25 -0
  455. data/lib/herb.rb +74 -63
  456. data/sig/herb/action_view/helper_registry.rbs +82 -10
  457. data/sig/herb/analysis/partial_declaration.rbs +80 -0
  458. data/sig/herb/analysis/partial_index.rbs +66 -0
  459. data/sig/herb/analysis/partial_resolution.rbs +57 -0
  460. data/sig/herb/analysis/partial_resolver.rbs +76 -0
  461. data/sig/herb/analysis/project_index.rbs +57 -0
  462. data/sig/herb/{action_view → analysis}/render_analyzer.rbs +25 -4
  463. data/sig/herb/analysis/render_graph/ancestor_chain.rbs +22 -0
  464. data/sig/herb/analysis/render_graph/builder.rbs +180 -0
  465. data/sig/herb/analysis/render_graph/call_frame.rbs +26 -0
  466. data/sig/herb/analysis/render_graph/call_site_location.rbs +28 -0
  467. data/sig/herb/analysis/render_graph/inferred_signature.rbs +27 -0
  468. data/sig/herb/analysis/render_graph/partial_call_site.rbs +36 -0
  469. data/sig/herb/analysis/render_graph/partial_context.rbs +31 -0
  470. data/sig/herb/analysis/render_graph/template_roots.rbs +32 -0
  471. data/sig/herb/analysis/render_graph.rbs +83 -0
  472. data/sig/herb/analysis/ruby_locals_index/local.rbs +21 -0
  473. data/sig/herb/analysis/ruby_locals_index/named_reference.rbs +20 -0
  474. data/sig/herb/analysis/ruby_locals_index/offset_table.rbs +26 -0
  475. data/sig/herb/analysis/ruby_locals_index/reference_collector.rbs +40 -0
  476. data/sig/herb/analysis/ruby_locals_index/state_locals.rbs +19 -0
  477. data/sig/herb/analysis/ruby_locals_index.rbs +62 -0
  478. data/sig/herb/analysis/ruby_reference.rbs +28 -0
  479. data/sig/herb/analysis/template_dependencies/dependency_collector.rbs +52 -0
  480. data/sig/herb/analysis/template_dependencies/node_dependency_collector.rbs +81 -0
  481. data/sig/herb/analysis/template_dependencies.rbs +125 -0
  482. data/sig/herb/ast/erb_render_node.rbs +10 -2
  483. data/sig/herb/ast/helpers.rbs +31 -1
  484. data/sig/herb/ast/herb_directive_node.rbs +13 -0
  485. data/sig/herb/ast/node.rbs +9 -0
  486. data/sig/herb/ast/nodes.rbs +668 -89
  487. data/sig/herb/backend.rbs +52 -0
  488. data/sig/herb/bootstrap.rbs +2 -0
  489. data/sig/herb/colors.rbs +3 -0
  490. data/sig/herb/configuration.rbs +27 -3
  491. data/sig/herb/dev/assets.rbs +27 -0
  492. data/sig/herb/dev/classifier.rbs +57 -0
  493. data/sig/herb/dev/embedded.rbs +21 -0
  494. data/sig/herb/dev/pipeline.rbs +118 -0
  495. data/sig/herb/dev/protocol.rbs +78 -0
  496. data/sig/herb/dev/runner.rbs +64 -23
  497. data/sig/herb/dev/server.rbs +49 -22
  498. data/sig/herb/dev/server_entry.rbs +46 -21
  499. data/sig/herb/dev/watcher.rbs +84 -0
  500. data/sig/herb/dev.rbs +58 -0
  501. data/sig/herb/diagnostic/formatter.rbs +28 -0
  502. data/sig/herb/diagnostic.rbs +88 -0
  503. data/sig/herb/diff/operation.rbs +36 -0
  504. data/sig/herb/diff/result.rbs +36 -0
  505. data/sig/herb/diff.rbs +6 -0
  506. data/sig/herb/engine/collector.rbs +48 -0
  507. data/sig/herb/engine/compiler.rbs +74 -24
  508. data/sig/herb/engine/component_tags/constant_resolver.rbs +19 -0
  509. data/sig/herb/engine/component_tags/partial_resolver.rbs +30 -0
  510. data/sig/herb/engine/component_tags/resolver.rbs +25 -0
  511. data/sig/herb/engine/component_tags/visitor.rbs +69 -0
  512. data/sig/herb/engine/css_inliner/inliner.rbs +65 -0
  513. data/sig/herb/engine/css_inliner/visitor.rbs +87 -0
  514. data/sig/herb/engine/errors.rbs +68 -0
  515. data/sig/herb/engine/helpers.rbs +28 -0
  516. data/sig/herb/engine/inline_render/inliner.rbs +39 -0
  517. data/sig/herb/engine/inline_render/visitor.rbs +104 -0
  518. data/sig/herb/engine/runtime/entry.rbs +43 -0
  519. data/sig/herb/engine/runtime/error_page.rbs +115 -0
  520. data/sig/herb/engine/runtime/html_safe_assertions.rbs +99 -0
  521. data/sig/herb/engine/runtime/journal/summary.rbs +172 -0
  522. data/sig/herb/engine/runtime/journal.rbs +126 -0
  523. data/sig/herb/engine/runtime/middleware.rbs +96 -0
  524. data/sig/herb/engine/runtime/observations.rbs +38 -0
  525. data/sig/herb/engine/runtime/report.rbs +97 -0
  526. data/sig/herb/engine/runtime/session.rbs +233 -0
  527. data/sig/herb/engine/runtime.rbs +14 -0
  528. data/sig/herb/engine/scoped_style/channel.rbs +45 -0
  529. data/sig/herb/engine/scoped_style/collector.rbs +45 -0
  530. data/sig/herb/engine/scoped_style/visitor.rbs +220 -0
  531. data/sig/herb/engine/slots/annotation.rbs +72 -0
  532. data/sig/herb/engine/slots/bindings.rbs +34 -0
  533. data/sig/herb/engine/slots/callee.rbs +62 -0
  534. data/sig/herb/engine/slots/component_definitions.rbs +19 -0
  535. data/sig/herb/engine/slots/components/async.rbs +20 -0
  536. data/sig/herb/engine/slots/components/base.rbs +60 -0
  537. data/sig/herb/engine/slots/components/deferred.rbs +29 -0
  538. data/sig/herb/engine/slots/components/fallback.rbs +21 -0
  539. data/sig/herb/engine/slots/components/fragment.rbs +34 -0
  540. data/sig/herb/engine/slots/components/lazy.rbs +20 -0
  541. data/sig/herb/engine/slots/components.rbs +30 -0
  542. data/sig/herb/engine/slots/dependencies/channel.rbs +51 -0
  543. data/sig/herb/engine/slots/dependencies.rbs +94 -0
  544. data/sig/herb/engine/slots/dynamics_compiler.rbs +352 -0
  545. data/sig/herb/engine/slots/identifier.rbs +19 -0
  546. data/sig/herb/engine/slots/manifest/channel.rbs +49 -0
  547. data/sig/herb/engine/slots/manifest/collector.rbs +45 -0
  548. data/sig/herb/engine/slots/markers.rbs +99 -0
  549. data/sig/herb/engine/slots/render_bindings.rbs +69 -0
  550. data/sig/herb/engine/slots/schema_compiler.rbs +62 -0
  551. data/sig/herb/engine/slots/state_anchor.rbs +41 -0
  552. data/sig/herb/engine/slots/state_compiler.rbs +258 -0
  553. data/sig/herb/engine/slots/state_directives.rbs +278 -0
  554. data/sig/herb/engine/slots/state_kinds.rbs +24 -0
  555. data/sig/herb/engine/slots/state_operators.rbs +19 -0
  556. data/sig/herb/engine/slots/state_overrides.rbs +33 -0
  557. data/sig/herb/engine/slots/state_predicates.rbs +16 -0
  558. data/sig/herb/engine/slots/state_transforms.rbs +15 -0
  559. data/sig/herb/engine/slots/statics.rbs +67 -0
  560. data/sig/herb/engine/slots/subtree.rbs +49 -0
  561. data/sig/herb/engine/slots/subtree_compiler.rbs +130 -0
  562. data/sig/herb/engine/slots/types.rbs +46 -0
  563. data/sig/herb/engine/slots/visitor.rbs +603 -0
  564. data/sig/herb/engine/validators/accessibility_validator.rbs +1 -1
  565. data/sig/herb/engine/validators/base.rbs +28 -0
  566. data/sig/herb/engine/validators/generator_template_validator.rbs +19 -0
  567. data/sig/herb/engine/validators/nesting_validator.rbs +1 -1
  568. data/sig/herb/engine/validators/render_validator.rbs +2 -6
  569. data/sig/herb/engine/validators/security_validator.rbs +1 -3
  570. data/sig/herb/engine/validators.rbs +21 -0
  571. data/sig/herb/engine/visitors/auto_close_omitted_tags_visitor.rbs +26 -0
  572. data/sig/herb/engine/visitors/content_for_visitor.rbs +52 -0
  573. data/sig/herb/engine/{debug_visitor.rbs → visitors/debug_visitor.rbs} +26 -15
  574. data/sig/herb/engine/visitors/html_safe_assertions_visitor.rbs +100 -0
  575. data/sig/herb/engine/visitors/instrumentation_visitor.rbs +111 -0
  576. data/sig/herb/engine/visitors/optimize_visitor.rbs +246 -0
  577. data/sig/herb/engine/visitors/remove_comments_visitor.rbs +66 -0
  578. data/sig/herb/engine/visitors/source_attribution_visitor.rbs +90 -0
  579. data/sig/herb/engine.rbs +47 -41
  580. data/sig/herb/ensure_installed.rbs +9 -0
  581. data/sig/herb/errors.rbs +293 -0
  582. data/sig/herb/fingerprint.rbs +37 -0
  583. data/sig/herb/html/elements.rbs +19 -0
  584. data/sig/herb/html/util.rbs +15 -1
  585. data/sig/herb/lex_result.rbs +0 -6
  586. data/sig/herb/locate.rbs +89 -0
  587. data/sig/herb/location.rbs +12 -0
  588. data/sig/herb/parse_result.rbs +10 -8
  589. data/sig/herb/parser_options.rbs +47 -21
  590. data/sig/herb/position.rbs +11 -0
  591. data/sig/herb/range.rbs +3 -0
  592. data/sig/herb/ruby_program.rbs +51 -0
  593. data/sig/herb/source_path.rbs +132 -0
  594. data/sig/herb/token.rbs +5 -2
  595. data/sig/herb/visitor/context/origin.rbs +51 -0
  596. data/sig/herb/visitor/context/replacements.rbs +39 -0
  597. data/sig/herb/visitor/context.rbs +83 -0
  598. data/sig/herb/visitor/context_aware.rbs +32 -0
  599. data/sig/herb/visitor/diagnostics.rbs +82 -0
  600. data/sig/herb/visitor/experimental.rbs +23 -0
  601. data/sig/herb/visitor/parser_option_requirements.rbs +76 -0
  602. data/sig/herb/visitor/stack.rbs +91 -0
  603. data/sig/herb/visitor.rbs +22 -0
  604. data/sig/herb/warnings.rbs +10 -0
  605. data/sig/herb.rbs +27 -10
  606. data/sig/serialized_ast_errors.rbs +58 -1
  607. data/sig/serialized_ast_nodes.rbs +59 -2
  608. data/src/analyze/action_view/attribute_extraction_helpers.c +73 -36
  609. data/src/analyze/action_view/generated_handlers.c +37 -1
  610. data/src/analyze/action_view/generated_handlers.h +2 -1
  611. data/src/analyze/action_view/helper_registry.c +1353 -58
  612. data/src/analyze/action_view/registry.c +10 -17
  613. data/src/analyze/action_view/stylesheet_link_tag.c +40 -0
  614. data/src/analyze/action_view/tag_helper_node_builders.c +63 -3
  615. data/src/analyze/action_view/tag_helpers.c +497 -111
  616. data/src/analyze/analyze.c +306 -89
  617. data/src/analyze/analyze_helpers.c +502 -243
  618. data/src/analyze/analyzed_ruby.c +2 -0
  619. data/src/analyze/builders.c +80 -51
  620. data/src/analyze/conditional_elements.c +13 -8
  621. data/src/analyze/conditional_open_tags.c +11 -5
  622. data/src/analyze/control_type.c +30 -5
  623. data/src/analyze/herb_directives.c +701 -0
  624. data/src/analyze/invalid_structures.c +11 -7
  625. data/src/analyze/iteration_nodes.c +306 -0
  626. data/src/analyze/missing_end.c +116 -32
  627. data/src/analyze/parse_errors.c +8 -3
  628. data/src/analyze/postfix_conditionals.c +98 -63
  629. data/src/analyze/prism_annotate.c +45 -13
  630. data/src/analyze/render_nodes.c +345 -123
  631. data/src/analyze/strict_locals.c +30 -26
  632. data/src/analyze/ternary_conditionals.c +95 -70
  633. data/src/analyze/transform.c +43 -1
  634. data/src/ast/ast_node.c +3 -13
  635. data/src/ast/ast_nodes.c +914 -113
  636. data/src/ast/ast_pretty_print.c +166 -3
  637. data/src/diff/herb_diff.c +8 -0
  638. data/src/diff/herb_diff_attributes.c +1 -0
  639. data/src/diff/herb_diff_children.c +4 -3
  640. data/src/diff/herb_diff_helpers.c +7 -1
  641. data/src/diff/herb_diff_nodes.c +186 -71
  642. data/src/diff/herb_hash_tree.c +99 -1
  643. data/src/errors.c +1096 -128
  644. data/src/extract.c +227 -144
  645. data/src/herb.c +30 -11
  646. data/src/include/analyze/action_view/attribute_extraction_helpers.h +1 -1
  647. data/src/include/analyze/action_view/helper_registry.h +28 -4
  648. data/src/include/analyze/action_view/tag_helper_handler.h +1 -1
  649. data/src/include/analyze/action_view/tag_helper_node_builders.h +7 -0
  650. data/src/include/analyze/analyze.h +11 -22
  651. data/src/include/analyze/analyzed_ruby.h +25 -0
  652. data/src/include/analyze/builders.h +16 -0
  653. data/src/include/analyze/helpers.h +25 -24
  654. data/src/include/analyze/herb_directive_kinds.h +41 -0
  655. data/src/include/analyze/herb_directives.h +14 -0
  656. data/src/include/analyze/iteration_nodes.h +11 -0
  657. data/src/include/analyze/prism_annotate.h +3 -3
  658. data/src/include/analyze/ternary_conditionals.h +2 -0
  659. data/src/include/ast/ast_node.h +1 -5
  660. data/src/include/ast/ast_nodes.h +78 -3
  661. data/src/include/ast/ast_pretty_print.h +1 -1
  662. data/src/include/diff/herb_diff.h +11 -0
  663. data/src/include/errors.h +156 -39
  664. data/src/include/extract.h +11 -0
  665. data/src/include/herb.h +6 -0
  666. data/src/include/lexer/erb_openers.h +29 -0
  667. data/src/include/lexer/lexer.h +1 -0
  668. data/src/include/lexer/lexer_peek_helpers.h +8 -1
  669. data/src/include/lexer/lexer_struct.h +4 -0
  670. data/src/include/lexer/token.h +3 -0
  671. data/src/include/lexer/token_struct.h +8 -6
  672. data/src/include/lib/hb_allocator.h +2 -1
  673. data/src/include/lib/hb_array.h +2 -0
  674. data/src/include/lib/hb_foreach.h +1 -1
  675. data/src/include/lib/hb_string.h +7 -0
  676. data/src/include/parser/dot_notation.h +1 -1
  677. data/src/include/parser/foreign_content_elements.h +32 -0
  678. data/src/include/parser/parser.h +16 -30
  679. data/src/include/parser/parser_helpers.h +13 -11
  680. data/src/include/parser/parser_options.h +41 -0
  681. data/src/include/prism/herb_prism_node.h +6 -1
  682. data/src/include/prism/ruby_parser.h +6 -0
  683. data/src/include/util/html_elements.h +85 -0
  684. data/src/include/util/html_util.h +1 -0
  685. data/src/include/version.h +1 -1
  686. data/src/lexer/lexer_peek_helpers.c +15 -1
  687. data/src/lexer/token.c +83 -1
  688. data/src/lexer.c +228 -12
  689. data/src/lib/hb_allocator.c +2 -0
  690. data/src/lib/hb_array.c +26 -0
  691. data/src/lib/hb_string.c +26 -0
  692. data/src/main.c +1 -1
  693. data/src/parser/dot_notation.c +4 -3
  694. data/src/parser/match_tags.c +66 -1
  695. data/src/parser/parser_helpers.c +69 -37
  696. data/src/parser.c +606 -383
  697. data/src/parser_options_defaults.c +30 -0
  698. data/src/prism/prism_helpers.c +94 -0
  699. data/src/prism/ruby_parser.c +49 -0
  700. data/src/util/html_util.c +11 -58
  701. data/src/visitor.c +77 -1
  702. data/templates/ext/herb/error_helpers.c.erb +1 -1
  703. data/templates/ext/herb/nodes.c.erb +19 -14
  704. data/templates/ext/herb/nodes.h.erb +2 -1
  705. data/templates/ext/herb/parser_options_helpers.c.erb +69 -0
  706. data/templates/ext/herb/parser_options_helpers.h.erb +11 -0
  707. data/templates/java/nodes.c.erb +21 -17
  708. data/templates/java/nodes.h.erb +3 -2
  709. data/templates/java/org/herb/ParserOptions.java.erb +24 -0
  710. data/templates/java/org/herb/ast/HelperRegistry.java.erb +67 -31
  711. data/templates/java/org/herb/ast/HelperType.java.erb +29 -0
  712. data/templates/java/org/herb/ast/Nodes.java.erb +11 -7
  713. data/templates/java/parser_options_helpers.c.erb +45 -0
  714. data/templates/java/parser_options_helpers.h.erb +10 -0
  715. data/templates/javascript/packages/client/src/state-kinds.ts.erb +49 -0
  716. data/templates/javascript/packages/client/src/state-operators.ts.erb +15 -0
  717. data/templates/javascript/packages/client/src/state-predicates.ts.erb +27 -0
  718. data/templates/javascript/packages/client/src/state-transforms.ts.erb +19 -0
  719. data/templates/javascript/packages/core/src/action-view-helpers.ts.erb +47 -0
  720. data/templates/javascript/packages/core/src/built-in-components.ts.erb +67 -0
  721. data/templates/javascript/packages/core/src/errors.ts.erb +9 -3
  722. data/templates/javascript/packages/core/src/foreign-content-elements.ts.erb +18 -0
  723. data/templates/javascript/packages/core/src/html-constants.ts.erb +32 -0
  724. data/templates/javascript/packages/core/src/html-elements.ts.erb +41 -0
  725. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +11 -7
  726. data/templates/javascript/packages/core/src/nodes.ts.erb +103 -20
  727. data/templates/javascript/packages/core/src/parser-options.ts.erb +41 -0
  728. data/templates/javascript/packages/core/src/visitor.ts.erb +1 -1
  729. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +3 -3
  730. data/templates/javascript/packages/node/extension/nodes.cpp.erb +18 -13
  731. data/templates/javascript/packages/node/extension/nodes.h.erb +3 -3
  732. data/templates/javascript/packages/node/extension/parser_options_helpers.cpp.erb +41 -0
  733. data/templates/javascript/packages/node/extension/parser_options_helpers.h.erb +12 -0
  734. data/templates/lib/herb/action_view/helper_registry.rb.erb +77 -3
  735. data/templates/lib/herb/ast/nodes.rb.erb +16 -6
  736. data/templates/lib/herb/engine/slots/component_definitions.rb.erb +38 -0
  737. data/templates/lib/herb/engine/slots/state_kinds.rb.erb +37 -0
  738. data/templates/lib/herb/engine/slots/state_operators.rb.erb +26 -0
  739. data/templates/lib/herb/engine/slots/state_predicates.rb.erb +39 -0
  740. data/templates/lib/herb/engine/slots/state_transforms.rb.erb +27 -0
  741. data/templates/lib/herb/errors.rb.erb +34 -6
  742. data/templates/lib/herb/html/elements.rb.erb +12 -0
  743. data/templates/lib/herb/parser_options.rb.erb +32 -0
  744. data/templates/lib/herb/visitor.rb.erb +7 -1
  745. data/templates/rust/herb-printer/src/printer_visitor.rs.erb +14 -0
  746. data/templates/rust/src/action_view_helpers.rs.erb +38 -0
  747. data/templates/rust/src/ast/nodes.rs.erb +21 -31
  748. data/templates/rust/src/errors.rs.erb +1 -1
  749. data/templates/rust/src/nodes.rs.erb +58 -26
  750. data/templates/rust/src/parser_options.rs.erb +16 -0
  751. data/templates/rust/src/prism/generated_deserialize.rs.erb +109 -0
  752. data/templates/rust/src/prism/generated_prettyprint.rs.erb +153 -0
  753. data/templates/rust/src/union_types.rs.erb +7 -1
  754. data/templates/rust/src/visitor.rs.erb +1 -1
  755. data/templates/src/analyze/action_view/helper_registry.c.erb +22 -0
  756. data/templates/src/analyze/missing_end.c.erb +13 -5
  757. data/templates/src/ast/ast_nodes.c.erb +41 -2
  758. data/templates/src/diff/herb_diff_nodes.c.erb +44 -8
  759. data/templates/src/errors.c.erb +8 -3
  760. data/templates/src/include/analyze/action_view/helper_registry.h.erb +19 -0
  761. data/templates/src/include/analyze/herb_directive_kinds.h.erb +25 -0
  762. data/templates/src/include/errors.h.erb +3 -1
  763. data/templates/src/include/parser/foreign_content_elements.h.erb +23 -0
  764. data/templates/src/include/parser/parser_options.h.erb +21 -0
  765. data/templates/src/include/util/html_elements.h.erb +28 -0
  766. data/templates/src/parser_options_defaults.c.erb +9 -0
  767. data/templates/template.rb +564 -9
  768. data/templates/wasm/error_helpers.cpp.erb +4 -4
  769. data/templates/wasm/nodes.cpp.erb +17 -27
  770. data/templates/wasm/nodes.h.erb +3 -3
  771. data/templates/wasm/parser_options_helpers.cpp.erb +25 -0
  772. data/templates/wasm/parser_options_helpers.h.erb +12 -0
  773. metadata +306 -27
  774. data/config/action_view_helpers/actionview/form_helper/button.yml +0 -38
  775. data/config/action_view_helpers/actionview/form_helper/submit.yml +0 -35
  776. data/lib/herb/ast.rb +0 -7
  777. data/lib/herb/diff_operation.rb +0 -34
  778. data/lib/herb/diff_result.rb +0 -59
  779. data/lib/herb/engine/error_formatter.rb +0 -432
  780. data/lib/herb/engine/parser_error_overlay.rb +0 -773
  781. data/lib/herb/engine/validation_error_overlay.rb +0 -182
  782. data/lib/herb/engine/validation_errors.rb +0 -65
  783. data/lib/herb/engine/validator.rb +0 -80
  784. data/sig/herb/ast.rbs +0 -6
  785. data/sig/herb/diff_operation.rbs +0 -34
  786. data/sig/herb/diff_result.rbs +0 -34
  787. data/sig/herb/engine/debug.rbs +0 -38
  788. data/sig/herb/engine/error_formatter.rbs +0 -47
  789. data/sig/herb/engine/parser_error_overlay.rbs +0 -41
  790. data/sig/herb/engine/validation_error_overlay.rbs +0 -35
  791. data/sig/herb/engine/validation_errors.rbs +0 -45
  792. data/sig/herb/engine/validator.rbs +0 -41
  793. data/sig/herb_c_extension.rbs +0 -10
@@ -0,0 +1,2267 @@
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
+ #: (Array[untyped], Integer) -> String
1206
+ def attribute_spacing(children, position)
1207
+ position.positive? && children[position - 1].is_a?(Herb::AST::WhitespaceNode) ? "" : " "
1208
+ end
1209
+
1210
+ private
1211
+
1212
+ def visit_children_with_paths(children)
1213
+ return unless children.is_a?(Array)
1214
+
1215
+ children.each_with_index do |child, index|
1216
+ @states.register_state_directive(child, children)
1217
+ @states.check_state_assignment(child)
1218
+
1219
+ @path.push(index)
1220
+ record_slot(child, :child) if child.is_a?(Herb::AST::RubyLiteralNode)
1221
+ visit(child)
1222
+ @path.pop
1223
+ end
1224
+ end
1225
+
1226
+ def visit_branching_node(node)
1227
+ @container_depth += 1
1228
+
1229
+ BRANCH_BODY_PROPERTIES.each do |property|
1230
+ next unless node.respond_to?(property)
1231
+
1232
+ visit_children_with_paths(node.send(property))
1233
+ end
1234
+
1235
+ BRANCH_CONTINUATION_PROPERTIES.each do |property|
1236
+ next unless node.respond_to?(property)
1237
+
1238
+ child = node.send(property)
1239
+ next unless child
1240
+
1241
+ @continuations[child] = true
1242
+ visit(child)
1243
+ end
1244
+
1245
+ @container_depth -= 1
1246
+ end
1247
+
1248
+ #: (untyped) -> bool
1249
+ def declares_slots?(node)
1250
+ node.children.any? { |child|
1251
+ child.is_a?(Herb::AST::HerbDirectiveNode) && child.directive == "slots"
1252
+ }
1253
+ end
1254
+
1255
+ #: (untyped) -> void
1256
+ def transform_components(node)
1257
+ return unless node.is_a?(Herb::AST::Node)
1258
+
1259
+ BRANCH_BODY_PROPERTIES.each do |property|
1260
+ next unless node.respond_to?(property)
1261
+
1262
+ value = node.send(property)
1263
+
1264
+ case value
1265
+ when Array
1266
+ if property == :conditions
1267
+ value.each { |arm| transform_components(arm) }
1268
+ else
1269
+ value.replace(value.flat_map { |child| transformed(child) })
1270
+ end
1271
+ when Herb::AST::Node
1272
+ transform_components(value)
1273
+ end
1274
+ end
1275
+
1276
+ BRANCH_CONTINUATION_PROPERTIES.each do |property|
1277
+ next unless node.respond_to?(property)
1278
+
1279
+ continuation = node.send(property)
1280
+
1281
+ transform_components(continuation) if continuation
1282
+ end
1283
+ end
1284
+
1285
+ #: (untyped) -> Array[untyped]
1286
+ def transformed(child)
1287
+ unless Components.element?(child)
1288
+ transform_components(child)
1289
+
1290
+ return [child]
1291
+ end
1292
+
1293
+ name = child.tag_name&.value.to_s
1294
+ component = Components.for(name)
1295
+
1296
+ return component.transform(child, self) if component
1297
+
1298
+ 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 ")}.")
1299
+
1300
+ [child]
1301
+ end
1302
+
1303
+ #: () -> void
1304
+ def collapse_invariant_conditionals
1305
+ @standing.each_value do |annotation|
1306
+ next unless annotation.type == :conditional
1307
+ next if fragment?(annotation.node)
1308
+ next unless exhaustive?(annotation.node)
1309
+
1310
+ bodies = branch_bodies(annotation.node)
1311
+ next unless bodies.size > 1
1312
+
1313
+ signatures = bodies.filter_map { |body| Statics.new(@standing, @element_anchored).signature(body) }
1314
+ next unless signatures.size == bodies.size
1315
+
1316
+ shapes = signatures.map { |shape, _| shape }
1317
+ next unless shapes.uniq.one?
1318
+ next if branching_shape?(shapes.fetch(0))
1319
+
1320
+ positions = signatures.map { |_, found| found }
1321
+ shared = positions.fetch(0)
1322
+
1323
+ annotation.dropped = true
1324
+
1325
+ shared.each { |survivor| survivor.covered << annotation }
1326
+
1327
+ positions.drop(1).each do |found|
1328
+ found.each_with_index do |other, position|
1329
+ survivor = shared.fetch(position)
1330
+
1331
+ other.merged_into = survivor
1332
+ survivor.covered << other
1333
+ end
1334
+ end
1335
+ end
1336
+
1337
+ @standing.delete_if { |_node, annotation| annotation.dropped }
1338
+ end
1339
+
1340
+ #: (untyped) -> Array[Integer]
1341
+ def slot_indices_within(nodes)
1342
+ children = Array(nodes) #: Array[untyped]
1343
+
1344
+ children.compact.flat_map { |child|
1345
+ next [] unless child.is_a?(Herb::AST::Node)
1346
+
1347
+ inside = slot_indices_within(child.compact_child_nodes)
1348
+ index = @indices[child]
1349
+
1350
+ index ? [index, *inside] : inside
1351
+ }
1352
+ end
1353
+
1354
+ #: (String) -> bool
1355
+ def branching_shape?(shape)
1356
+ Types::STRUCTURAL.any? { |type| shape.include?("\u0000#{type}\u0000") }
1357
+ end
1358
+
1359
+ #: () -> void
1360
+ def number_slots
1361
+ surviving = @annotations.reject(&:gone?)
1362
+
1363
+ surviving.each_with_index { |annotation, index| annotation.index = index }
1364
+
1365
+ @slots = surviving.each_with_index.map { |annotation, index| annotation.to_slot(index) }
1366
+ @slot_nodes = surviving.map(&:node)
1367
+
1368
+ @annotations.each do |annotation|
1369
+ next if annotation.dropped
1370
+
1371
+ @indices[annotation.node] = annotation.survivor.index
1372
+ end
1373
+ end
1374
+
1375
+ #: (untyped, Symbol?) -> void
1376
+ def record_slot(node, type)
1377
+ return unless type
1378
+
1379
+ type = anchored_type_for(type)
1380
+
1381
+ return unless type
1382
+ return if @in_html_comment || @in_html_doctype
1383
+ return if type == :raw_text && @rcdata_interpolated_depth.positive?
1384
+ return refuse_mixed_rcdata(node) if type == :raw_text && @rcdata_mixed_depth.positive?
1385
+
1386
+ key_source, key_expression = keys_for(node, type)
1387
+
1388
+ annotation = Annotation.new(
1389
+ node: node,
1390
+ type: type,
1391
+ node_path: @path.dup,
1392
+ expression: expression_for(node),
1393
+ attribute: attribute_name_for(node),
1394
+ key_source: key_source,
1395
+ key_expression: key_expression,
1396
+ tag: @tag_stack.last,
1397
+ scope: @collection_nodes.last,
1398
+ depth: @container_depth
1399
+ )
1400
+
1401
+ @annotations << annotation
1402
+ @annotation_of[node] = annotation
1403
+
1404
+ written = @listener_stack.flatten
1405
+ @listener_written[node] = written unless written.empty?
1406
+
1407
+ if type == :collection && key_source == :index
1408
+ @warnings << Herb::Warnings::UnkeyedCollectionWarning.new(
1409
+ node.location,
1410
+ expression_for(node),
1411
+ tag_name: keyable_tag_name_for(node)
1412
+ )
1413
+ end
1414
+
1415
+ if !annotation.anchored?
1416
+ @standing[node] = annotation
1417
+ elsif @current_open_tag
1418
+ anchored = @element_anchored[@current_open_tag] || [] #: Array[Annotation]
1419
+ anchored << annotation
1420
+
1421
+ @element_anchored[@current_open_tag] = anchored
1422
+ end
1423
+ end
1424
+
1425
+ #: (Symbol) -> Symbol?
1426
+ def anchored_type_for(type)
1427
+ return type if type == :raw_text_interpolation
1428
+ return type if Types.attribute_value?(type)
1429
+
1430
+ return nil if @in_attribute
1431
+ return :element if @in_open_tag
1432
+ return nil if @raw_text_depth.positive?
1433
+ return :raw_text if @rcdata_depth.positive?
1434
+
1435
+ type
1436
+ end
1437
+
1438
+ #: (Array[untyped]) -> Array[String]?
1439
+ def rcdata_segments(children)
1440
+ segments = [+""]
1441
+
1442
+ Array(children).each do |child|
1443
+ case child
1444
+ when Herb::AST::HTMLTextNode, Herb::AST::LiteralNode
1445
+ segments.last << child.content.to_s
1446
+ when Herb::AST::WhitespaceNode
1447
+ segments.last << child.value&.value.to_s
1448
+ when Herb::AST::ERBContentNode
1449
+ return nil unless erb_outputs?(child)
1450
+
1451
+ segments << +""
1452
+ else
1453
+ return nil
1454
+ end
1455
+ end
1456
+
1457
+ segments.size > 1 ? segments : nil
1458
+ end
1459
+
1460
+ #: (untyped) -> Array[String]
1461
+ def listener_names_for(node)
1462
+ attributes_for(node).flat_map { |attribute|
1463
+ name = attribute_name_for(attribute)&.downcase
1464
+
1465
+ next [] unless name && LISTENER_ATTRIBUTES.include?(name)
1466
+
1467
+ value = static_attribute_value(attribute)
1468
+
1469
+ next [] unless value
1470
+
1471
+ value.split.filter_map { |clause|
1472
+ rest = clause.split("->", 2).last.to_s
1473
+ target = name == "data-herb-set" ? rest.split("=", 2).first : rest
1474
+ cleaned = target.to_s.strip
1475
+
1476
+ cleaned unless cleaned.empty?
1477
+ }
1478
+ }
1479
+ end
1480
+
1481
+ #: (untyped) -> String?
1482
+ def rcdata_expression_for(node)
1483
+ outputs = Array(node.body).grep(Herb::AST::ERBContentNode) #: Array[untyped]
1484
+
1485
+ return nil unless outputs.one?
1486
+
1487
+ outputs.fetch(0).content&.value&.strip
1488
+ end
1489
+
1490
+ #: (Array[untyped]) -> bool
1491
+ def mixed_rcdata_body?(children)
1492
+ outputs = children.count { |child| child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child) }
1493
+
1494
+ return false if outputs.zero?
1495
+ return true if outputs > 1
1496
+
1497
+ children.any? { |child|
1498
+ (child.is_a?(Herb::AST::HTMLTextNode) || child.is_a?(Herb::AST::LiteralNode)) && !child.content.to_s.strip.empty?
1499
+ }
1500
+ end
1501
+
1502
+ #: (untyped) -> nil
1503
+ def refuse_mixed_rcdata(node)
1504
+ expression = expression_for(node)
1505
+ tag = @tag_stack.last
1506
+
1507
+ slot_warning(
1508
+ "`<%= #{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.",
1509
+ node.location,
1510
+ :content,
1511
+ suggestion: "Make `<%= #{expression} %>` the only content of the `<#{tag}>`, or move the other text outside it."
1512
+ )
1513
+
1514
+ nil
1515
+ end
1516
+
1517
+ #: (untyped) -> Symbol
1518
+ def attribute_type_for(node)
1519
+ return :element unless attribute_name_for(node)
1520
+
1521
+ children = node.value&.children || []
1522
+
1523
+ children.one? ? :attribute : :attribute_interpolation
1524
+ end
1525
+
1526
+ #: (untyped) -> untyped
1527
+ def convert_helper_boolean_attribute(node)
1528
+ open_tag = @current_open_tag
1529
+
1530
+ return nil unless @in_open_tag && (open_tag.is_a?(Herb::AST::ERBOpenTagNode) || open_tag.is_a?(Herb::AST::HTMLOpenTagNode))
1531
+ return nil if continuation?(node)
1532
+ return nil unless node.respond_to?(:subsequent) && node.subsequent.nil?
1533
+
1534
+ children = open_tag.children
1535
+
1536
+ position = children.index { |child| child.equal?(node) }
1537
+ return nil unless position
1538
+
1539
+ statements = node.statements
1540
+ return nil unless statements.is_a?(Array) && statements.one?
1541
+
1542
+ attribute = statements.fetch(0)
1543
+ return nil unless attribute.is_a?(Herb::AST::HTMLAttributeNode) && !dynamic?(attribute)
1544
+
1545
+ name = attribute_name_for(attribute)
1546
+ return nil unless name && Herb::HTML::Util.boolean_attribute?(name)
1547
+
1548
+ condition = @states.condition_expression(node)
1549
+ replacement = erb_output_node(%[("#{attribute_spacing(children, position)}#{name}" if #{condition})])
1550
+
1551
+ children[position] = replacement
1552
+ record_slot(replacement, :attribute)
1553
+
1554
+ annotation = @annotation_of[replacement]
1555
+ return nil unless annotation
1556
+
1557
+ annotation.type = :boolean_attribute
1558
+ annotation.attribute = name
1559
+ annotation.expression = condition
1560
+
1561
+ @attribute_open_tags[replacement] = open_tag
1562
+
1563
+ replacement
1564
+ end
1565
+
1566
+ #: (untyped, untyped, Integer, untyped, Integer) -> void
1567
+ def record_named_element(node, name_attribute, base, base_scope, base_depth)
1568
+ name = static_attribute_value(name_attribute)
1569
+
1570
+ unless name && !name.empty?
1571
+ 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.")
1572
+ end
1573
+
1574
+ candidates = @annotations[base..].to_a.select { |annotation|
1575
+ annotation.scope.equal?(base_scope) && annotation.depth == base_depth && annotation.nameable?
1576
+ }
1577
+
1578
+ @named_elements << {
1579
+ node: node,
1580
+ open_tag: node.open_tag,
1581
+ attribute: name_attribute,
1582
+ name: name,
1583
+ candidates: candidates,
1584
+ scope: base_scope,
1585
+ }
1586
+ end
1587
+
1588
+ #: (untyped) -> String?
1589
+ def static_attribute_value(attribute)
1590
+ children = attribute.value&.children || []
1591
+
1592
+ return nil unless children.all?(Herb::AST::LiteralNode)
1593
+
1594
+ children.map { |child| child.content.to_s }.join.strip
1595
+ end
1596
+
1597
+ #: () -> void
1598
+ def apply_names
1599
+ taken = {} #: Hash[untyped, Hash[String, Annotation]]
1600
+
1601
+ @named_elements.each { |named| bind_name(named, taken[named[:scope]] ||= {}) }
1602
+ end
1603
+
1604
+ #: (Hash[Symbol, untyped], Hash[String, Annotation]) -> void
1605
+ def bind_name(named, scope_names)
1606
+ name = named[:name] #: String
1607
+ annotation = resolve_named_slot(named)
1608
+
1609
+ return unless annotation
1610
+
1611
+ if scope_names.key?(name)
1612
+ 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.")
1613
+ end
1614
+
1615
+ if (existing = annotation.name)
1616
+ 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.")
1617
+ end
1618
+
1619
+ if attribute_conflict?(name, annotation, named[:scope])
1620
+ 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.")
1621
+ end
1622
+
1623
+ scope_names[name] = annotation
1624
+
1625
+ annotation.name = name
1626
+ end
1627
+
1628
+ #: (Hash[Symbol, untyped]) -> Annotation?
1629
+ def resolve_named_slot(named)
1630
+ name = named[:name]
1631
+ tag = named[:node].tag_name&.value
1632
+ candidates = named[:candidates].reject(&:dropped).map(&:survivor).uniq
1633
+
1634
+ if candidates.empty?
1635
+ 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.")
1636
+ end
1637
+
1638
+ unless candidates.one?
1639
+ 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.")
1640
+ end
1641
+
1642
+ candidates.fetch(0)
1643
+ end
1644
+
1645
+ #: (Hash[Symbol, untyped]) -> Herb::Location?
1646
+ def name_location(named)
1647
+ named[:attribute]&.location || named[:node]&.location
1648
+ end
1649
+
1650
+ #: (String, Annotation, untyped) -> bool
1651
+ def attribute_conflict?(name, annotation, scope)
1652
+ @annotations.any? { |other|
1653
+ !other.gone? && other.attribute == name && !other.equal?(annotation) && other.scope.equal?(scope)
1654
+ }
1655
+ end
1656
+
1657
+ #: (untyped) -> void
1658
+ def detect_keyed_elements(node)
1659
+ 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)
1660
+
1661
+ BRANCH_BODY_PROPERTIES.each do |property|
1662
+ next unless node.respond_to?(property)
1663
+
1664
+ array = node.send(property)
1665
+
1666
+ next unless array.is_a?(Array)
1667
+
1668
+ sole_element = iteration && array.grep(Herb::AST::HTMLElementNode).one?
1669
+
1670
+ array.each do |child|
1671
+ detect_keyed_elements(child)
1672
+
1673
+ next unless child.is_a?(Herb::AST::HTMLElementNode)
1674
+ next if sole_element
1675
+
1676
+ expression = keyed_element_expression(child)
1677
+
1678
+ next unless expression
1679
+
1680
+ remove_herb_key_attribute(child)
1681
+
1682
+ @keyed_elements[child] = expression
1683
+ end
1684
+ end
1685
+ end
1686
+
1687
+ #: (untyped) -> String?
1688
+ def keyed_element_expression(element)
1689
+ attribute = attributes_for(element).find { |candidate| attribute_name_for(candidate)&.downcase == KEY_ATTRIBUTE }
1690
+
1691
+ return nil unless attribute
1692
+
1693
+ key_expression_for(attribute)
1694
+ end
1695
+
1696
+ #: (untyped) -> void
1697
+ def remove_herb_key_attribute(element)
1698
+ open_tag = element.open_tag
1699
+
1700
+ return unless OPEN_TAG_TYPES.any? { |type| open_tag.is_a?(type) }
1701
+
1702
+ children = open_tag.children
1703
+ at = children.index { |child| attribute_name_for(child)&.downcase == KEY_ATTRIBUTE }
1704
+
1705
+ return unless at
1706
+
1707
+ children.delete_at(at)
1708
+ children.delete_at(at - 1) if at.positive? && children[at - 1].is_a?(Herb::AST::WhitespaceNode)
1709
+ end
1710
+
1711
+ #: (untyped, Symbol) -> [Symbol?, String?]
1712
+ def keys_for(node, type)
1713
+ return key_for(node) if type == :collection
1714
+ return [:herb_key, @keyed_elements.fetch(node)] if type == :keyed
1715
+
1716
+ [nil, nil]
1717
+ end
1718
+
1719
+ #: (untyped) -> [Symbol, String?]
1720
+ def key_for(node)
1721
+ body = collection_body(node)
1722
+
1723
+ directive = key_directive_in(body)
1724
+ return [:directive, directive] if directive
1725
+
1726
+ elements = body.grep(Herb::AST::HTMLElementNode)
1727
+ return [:index, nil] unless elements.one?
1728
+
1729
+ attributes = attributes_for(elements.first)
1730
+
1731
+ [KEY_ATTRIBUTE, "id"].each do |name|
1732
+ attribute = attributes.find { |candidate| attribute_name_for(candidate)&.downcase == name }
1733
+ next unless attribute
1734
+
1735
+ expression = key_expression_for(attribute)
1736
+ next unless expression
1737
+
1738
+ return [name == KEY_ATTRIBUTE ? :herb_key : :id, expression]
1739
+ end
1740
+
1741
+ [:index, nil]
1742
+ end
1743
+
1744
+ #: (untyped) -> String?
1745
+ def keyable_tag_name_for(node)
1746
+ elements = collection_body(node).grep(Herb::AST::HTMLElementNode)
1747
+ return nil unless elements.one?
1748
+
1749
+ elements.first.tag_name&.value&.downcase
1750
+ end
1751
+
1752
+ #: (Array[untyped]) -> String?
1753
+ def key_directive_in(body)
1754
+ body.each do |child|
1755
+ next unless child.is_a?(Herb::AST::HerbDirectiveNode)
1756
+ next unless child.directive == "key"
1757
+
1758
+ expression = child.argument
1759
+
1760
+ return expression unless expression.empty?
1761
+ end
1762
+
1763
+ nil
1764
+ end
1765
+
1766
+ #: (untyped) -> String?
1767
+ def key_expression_for(attribute)
1768
+ parts = key_parts_for(attribute)
1769
+
1770
+ return nil if parts.nil?
1771
+ return nil unless parts.any? { |kind, _| kind == :expression }
1772
+ return parts.first[1] if parts.one?
1773
+
1774
+ interpolated = parts.map { |kind, value|
1775
+ kind == :expression ? "\#{#{value}}" : escape_key_literal(value)
1776
+ }
1777
+
1778
+ %("#{interpolated.join}")
1779
+ end
1780
+
1781
+ #: (untyped) -> Array[[Symbol, String]]?
1782
+ def key_parts_for(attribute)
1783
+ children = attribute.value&.children || []
1784
+ return nil if children.empty?
1785
+
1786
+ children.map { |child|
1787
+ case child
1788
+ when Herb::AST::ERBContentNode
1789
+ expression = child.content&.value.to_s.strip
1790
+ return nil if expression.empty?
1791
+
1792
+ [:expression, expression]
1793
+ when Herb::AST::LiteralNode
1794
+ [:literal, child.content.to_s]
1795
+ when Herb::AST::RubyLiteralNode
1796
+ expression = child.content.to_s.strip
1797
+ return nil if expression.empty?
1798
+
1799
+ [:expression, expression]
1800
+ else
1801
+ return nil
1802
+ end
1803
+ }
1804
+ end
1805
+
1806
+ #: (String) -> String
1807
+ def escape_key_literal(literal)
1808
+ literal.gsub(/["\\]|\#\{/) { |match| "\\#{match}" }
1809
+ end
1810
+
1811
+ #: (untyped) -> Array[untyped]
1812
+ def collection_body(node)
1813
+ BRANCH_BODY_PROPERTIES.filter_map { |property|
1814
+ node.send(property) if node.respond_to?(property)
1815
+ }.flatten
1816
+ end
1817
+
1818
+ #: (untyped) -> Array[Herb::AST::HTMLAttributeNode]
1819
+ def attributes_for(element)
1820
+ open_tag = element.open_tag
1821
+ return [] unless OPEN_TAG_TYPES.any? { |type| open_tag.is_a?(type) }
1822
+
1823
+ open_tag.children.grep(Herb::AST::HTMLAttributeNode)
1824
+ end
1825
+
1826
+ #: (untyped) -> String?
1827
+ def attribute_name_for(node)
1828
+ return nil unless node.is_a?(Herb::AST::HTMLAttributeNode)
1829
+
1830
+ children = node.name&.children || []
1831
+
1832
+ return nil if children.empty?
1833
+ return nil unless children.all?(Herb::AST::LiteralNode)
1834
+
1835
+ children.filter_map { |child| child.content if child.respond_to?(:content) }.join
1836
+ end
1837
+
1838
+ #: (untyped) -> bool
1839
+ def dynamic?(node)
1840
+ return true if node.type.to_s.start_with?("AST_ERB_")
1841
+ return true if node.is_a?(Herb::AST::RubyLiteralNode)
1842
+
1843
+ node.compact_child_nodes.any? { |child| dynamic?(child) }
1844
+ end
1845
+
1846
+ def expression_for(node)
1847
+ return attribute_expression_for(node) if node.is_a?(Herb::AST::HTMLAttributeNode)
1848
+ return rcdata_expression_for(node) if node.is_a?(Herb::AST::HTMLElementNode)
1849
+ return nil unless node.respond_to?(:content)
1850
+
1851
+ content = node.content
1852
+ value = content.respond_to?(:value) ? content.value : content
1853
+
1854
+ value&.to_s&.strip
1855
+ end
1856
+
1857
+ #: (untyped) -> String?
1858
+ def attribute_expression_for(node)
1859
+ children = node.value&.children || []
1860
+ literal = children.grep(Herb::AST::RubyLiteralNode)
1861
+
1862
+ return literal.fetch(0).content.to_s.strip if literal.one? && children.one?
1863
+ return nil unless children.all? { |child| child.is_a?(Herb::AST::LiteralNode) || child.is_a?(Herb::AST::ERBContentNode) }
1864
+
1865
+ outputs = children.grep(Herb::AST::ERBContentNode)
1866
+
1867
+ return nil unless outputs.one?
1868
+ return nil unless erb_outputs?(outputs.fetch(0))
1869
+
1870
+ outputs.fetch(0).content&.value&.strip
1871
+ end
1872
+
1873
+ #: (untyped, ?Symbol) -> untyped
1874
+ def insert_markers(node, namespace = :html)
1875
+ anchored = content_anchor_index(node)
1876
+ namespace = Markers.namespace_within(namespace, node.tag_name&.value&.downcase.to_s) if node.is_a?(Herb::AST::HTMLElementNode)
1877
+
1878
+ each_child_array(node, namespace) do |array|
1879
+ index = 0
1880
+
1881
+ while index < array.size
1882
+ child = array[index]
1883
+ slot_index = @standing[child]&.survivor&.index
1884
+
1885
+ insert_markers(child, namespace)
1886
+ wrap_items(child, slot_index, namespace)
1887
+ mark_branches(child, slot_index, namespace)
1888
+
1889
+ if slot_index && slot_index != anchored
1890
+ if @slots[slot_index].type == :keyed
1891
+ park_keyed(slot_index, child, namespace)
1892
+
1893
+ array.insert(
1894
+ index,
1895
+ text_node(@markers.keyed_open_prefix(slot_index)),
1896
+ erb_code_node("#{@bufvar} << ::Herb::Engine.raw((#{@slots[slot_index].key_expression}).to_s)"),
1897
+ text_node(@markers.keyed_open_suffix)
1898
+ )
1899
+ array.insert(index + 4, comment_node(@markers.slot_close(slot_index)))
1900
+
1901
+ index += 5
1902
+ else
1903
+ array.insert(index, comment_node(@markers.slot_open(slot_index, @slots[slot_index].type)))
1904
+ array.insert(index + 2, comment_node(@markers.slot_close(slot_index)))
1905
+
1906
+ index += 3
1907
+ end
1908
+ else
1909
+ index += 1
1910
+ end
1911
+ end
1912
+ end
1913
+
1914
+ anchor_attributes(node, anchored)
1915
+ end
1916
+
1917
+ #: (untyped) -> Integer?
1918
+ def content_anchor_index(node)
1919
+ return nil unless node.is_a?(Herb::AST::HTMLElementNode)
1920
+
1921
+ body = node.body
1922
+ return nil unless body.is_a?(Array) && body.size == 1
1923
+
1924
+ annotation = @standing[body[0]]&.survivor
1925
+ return nil unless annotation
1926
+ return nil unless annotation.type == :child
1927
+ return nil if @render_bindings.key?(body[0])
1928
+
1929
+ annotation.index
1930
+ end
1931
+
1932
+ #: (untyped, Integer?, ?Symbol) -> void
1933
+ def mark_branches(node, slot_index, namespace = :html)
1934
+ return unless slot_index
1935
+ return unless @slots[slot_index].type == :conditional
1936
+
1937
+ branch_bodies(node).each_with_index do |body, branch_index|
1938
+ marker = @markers.branch(slot_index, branch_index)
1939
+
1940
+ park_branch(@markers.statics_key(slot_index, branch_index), marker, body, namespace)
1941
+
1942
+ body.unshift(text_node(marker))
1943
+ end
1944
+ end
1945
+
1946
+ #: (String, String, Array[untyped], Symbol) -> void
1947
+ def park_branch(key, marker, body, namespace)
1948
+ statics = @statics
1949
+ return unless statics
1950
+
1951
+ markup = Statics.new(@standing).markup(body)
1952
+ return unless markup
1953
+
1954
+ statics[key] = "#{marker}#{@markers.foreign(markup, namespace)}"
1955
+ end
1956
+
1957
+ #: (Herb::AST::DocumentNode) -> void
1958
+ def append_statics(document_node)
1959
+ return unless client?
1960
+
1961
+ statics = @statics
1962
+ entries = (statics || {}).sort_by { |key, _| key.split(":").map(&:to_i) } #: Array[[String, (String | Array[untyped])]]
1963
+
1964
+ withheld = deferred_entries.keys.to_set { |index| "#{index}:0" }
1965
+ entries.reject! { |key, _| withheld.include?(key) }
1966
+
1967
+ entries.concat(fallback_statics_entries)
1968
+
1969
+ return if entries.empty?
1970
+
1971
+ seen = entries.map { |key, _| "#{COVERED}[#{covered_key(key).inspect}]" }.join(" && ")
1972
+
1973
+ nodes = [
1974
+ erb_code_node("unless #{seen}"),
1975
+ text_node(@markers.statics_open(identifier, version))
1976
+ ] #: Array[Herb::AST::Node]
1977
+
1978
+ entries.each do |key, parked|
1979
+ reference = "#{COVERED}[#{covered_key(key).inspect}]"
1980
+
1981
+ nodes.push(erb_code_node("unless #{reference}"))
1982
+
1983
+ if parked.is_a?(String)
1984
+ nodes.push(text_node(parked))
1985
+ else
1986
+ nodes.concat(parked)
1987
+ end
1988
+
1989
+ nodes.push(erb_code_node("#{reference} = true"), erb_code_node("end"))
1990
+ end
1991
+
1992
+ nodes.push(text_node(@markers.statics_close), erb_code_node("end"))
1993
+
1994
+ document_node.children.unshift(erb_code_node("#{COVERED} ||= {}"))
1995
+ document_node.children.concat(nodes)
1996
+ end
1997
+
1998
+ #: (Herb::AST::DocumentNode) -> void
1999
+ def expose_fallbacks(document_node)
2000
+ return if @fragment_fallbacks.empty?
2001
+
2002
+ @exposed_fallbacks = @fragment_fallbacks.each_value.flat_map { |body| body } #: Array[untyped]
2003
+
2004
+ document_node.children.concat(@exposed_fallbacks)
2005
+
2006
+ nil
2007
+ end
2008
+
2009
+ #: (Herb::AST::DocumentNode) -> void
2010
+ def withdraw_fallbacks(document_node)
2011
+ exposed = @exposed_fallbacks
2012
+
2013
+ return if exposed.empty?
2014
+
2015
+ document_node.children.reject! { |child| exposed.any? { |node| node.equal?(child) } }
2016
+ @exposed_fallbacks = [] #: Array[untyped]
2017
+
2018
+ nil
2019
+ end
2020
+
2021
+ #: () -> Array[[String, Array[untyped]]]
2022
+ def fallback_statics_entries
2023
+ @fragment_fallbacks.filter_map { |node, body|
2024
+ index = @indices[node]
2025
+
2026
+ next unless index
2027
+
2028
+ [@markers.statics_key(index, 1), [text_node(@markers.branch(index, 1)), *marked_fallback(body)]]
2029
+ }
2030
+ end
2031
+
2032
+ #: (Array[untyped]) -> Array[untyped]
2033
+ def marked_fallback(children)
2034
+ children.flat_map do |child|
2035
+ insert_markers(child)
2036
+
2037
+ slot_index = @standing[child]&.survivor&.index
2038
+
2039
+ next [child] unless slot_index
2040
+
2041
+ [
2042
+ comment_node(@markers.slot_open(slot_index, @slots[slot_index].type)),
2043
+ child,
2044
+ comment_node(@markers.slot_close(slot_index))
2045
+ ]
2046
+ end
2047
+ end
2048
+
2049
+ #: (String) -> String
2050
+ def covered_key(key)
2051
+ "#{identifier}:#{key}"
2052
+ end
2053
+
2054
+ #: (untyped) -> Array[Array[Herb::AST::Node]]
2055
+ def branch_bodies(node)
2056
+ bodies = [] #: Array[Array[Herb::AST::Node]]
2057
+
2058
+ if node.respond_to?(:conditions)
2059
+ node.conditions.each { |arm| bodies << arm.statements if arm.respond_to?(:statements) }
2060
+ elsif node.respond_to?(:statements)
2061
+ bodies << node.statements
2062
+ end
2063
+
2064
+ current = continuation_of(node) #: untyped
2065
+
2066
+ while (branch = current)
2067
+ bodies << branch.statements if branch.respond_to?(:statements)
2068
+
2069
+ current = continuation_of(branch)
2070
+ end
2071
+
2072
+ bodies
2073
+ end
2074
+
2075
+ #: (Herb::AST::Node, Integer?, ?Symbol) -> void
2076
+ def wrap_items(node, slot_index, namespace = :html)
2077
+ return unless slot_index
2078
+
2079
+ slot = @slots[slot_index]
2080
+ return unless slot.type == :collection && slot.key_expression
2081
+
2082
+ BRANCH_BODY_PROPERTIES.each do |property|
2083
+ next unless node.respond_to?(property)
2084
+
2085
+ body = node.send(property)
2086
+ next unless body.is_a?(Array) && !body.empty?
2087
+
2088
+ key = @markers.item_statics_key(slot_index)
2089
+ parked = park_item(key, body, namespace)
2090
+
2091
+ body.unshift(
2092
+ text_node(@markers.item_open_prefix(slot_index)),
2093
+ erb_code_node("#{@bufvar} << ::Herb::Engine.raw((#{slot.key_expression}).to_s)"),
2094
+ text_node(@markers.item_open_suffix)
2095
+ )
2096
+
2097
+ body.insert(3, erb_code_node(%(#{COVERED}[#{covered_key(key).inspect}] = true))) if parked && client?
2098
+
2099
+ body.push(text_node(@markers.item_close(slot_index)))
2100
+ end
2101
+ end
2102
+
2103
+ #: (untyped) -> Array[String]?
2104
+ def attribute_segments(node)
2105
+ segments = [+""]
2106
+
2107
+ (node.value&.children || []).each do |child|
2108
+ case child
2109
+ when Herb::AST::LiteralNode
2110
+ segments.last << child.content.to_s
2111
+ when Herb::AST::ERBContentNode
2112
+ return nil unless erb_outputs?(child)
2113
+
2114
+ segments << +""
2115
+ when Herb::AST::ERBIfNode, Herb::AST::ERBUnlessNode, Herb::AST::ERBCaseNode
2116
+ segments << +""
2117
+ else
2118
+ return nil
2119
+ end
2120
+ end
2121
+
2122
+ segments.size > 1 ? segments : nil
2123
+ end
2124
+
2125
+ #: (Integer, untyped, Symbol) -> void
2126
+ def park_keyed(slot_index, element, namespace)
2127
+ statics = @statics
2128
+ return unless statics
2129
+
2130
+ markup = Statics.new(@standing).markup([element.open_tag, *element.body, element.close_tag].compact)
2131
+ return unless markup
2132
+
2133
+ statics[@markers.item_statics_key(slot_index)] = "#{@markers.branch(slot_index, Markers::ITEM_STATICS)}#{@markers.foreign(markup, namespace)}"
2134
+ end
2135
+
2136
+ #: (untyped, untyped, Symbol) -> Array[untyped]?
2137
+ def park_item(key, body, namespace)
2138
+ statics = @statics
2139
+ return unless statics
2140
+
2141
+ slot_index = key.split(":").first.to_i
2142
+ markup = Statics.new(@standing).markup(body)
2143
+ return unless markup
2144
+
2145
+ item = [
2146
+ @markers.item_open_prefix(slot_index),
2147
+ @markers.item_open_suffix,
2148
+ markup,
2149
+ @markers.item_close(slot_index)
2150
+ ].join
2151
+
2152
+ statics[key] = "#{@markers.branch(slot_index, Markers::ITEM_STATICS)}#{@markers.foreign(item, namespace)}"
2153
+ end
2154
+
2155
+ def anchor_attributes(node, content_index = nil)
2156
+ return unless node.is_a?(Herb::AST::HTMLElementNode)
2157
+
2158
+ open_tag = node.open_tag
2159
+ targets = open_tags_for(open_tag)
2160
+
2161
+ return if targets.empty?
2162
+
2163
+ anchors = (@element_anchored[open_tag] || []).map { |annotation|
2164
+ slot = @slots.fetch(annotation.survivor.index)
2165
+
2166
+ [slot.index, slot.type, anchor_name_for(slot)]
2167
+ }
2168
+
2169
+ anchors << [content_index, :child, nil] if content_index
2170
+
2171
+ return if anchors.empty?
2172
+
2173
+ value = @markers.element_anchors(anchors)
2174
+
2175
+ targets.each { |target| target.children << attribute_node("data-herb-slot", value) }
2176
+ end
2177
+
2178
+ #: (Slot) -> String?
2179
+ def anchor_name_for(slot)
2180
+ name = slot.attribute
2181
+
2182
+ return nil if name.nil? || name.include?(",")
2183
+
2184
+ name
2185
+ end
2186
+
2187
+ #: (Herb::AST::Node, ?Symbol) { (Array[untyped]) -> void } -> void
2188
+ def each_child_array(node, namespace = :html)
2189
+ BRANCH_BODY_PROPERTIES.each do |property|
2190
+ next unless node.respond_to?(property)
2191
+
2192
+ array = node.send(property)
2193
+ yield array if array.is_a?(Array)
2194
+ end
2195
+
2196
+ (BRANCH_CONTINUATION_PROPERTIES + [:end_node, :open_tag]).each do |property|
2197
+ next unless node.respond_to?(property)
2198
+
2199
+ child = node.send(property)
2200
+ insert_markers(child, namespace) if child
2201
+ end
2202
+ end
2203
+
2204
+ #: (Herb::AST::DocumentNode) -> void
2205
+ def wrap_region(document_node)
2206
+ document_node.children.unshift(
2207
+ erb_code_node("#{OCCURRENCE} = #{occurrence_expression}"),
2208
+ *region_open
2209
+ )
2210
+
2211
+ document_node.children.push(comment_node(@markers.region_close(identifier)))
2212
+ end
2213
+
2214
+ #: () -> void
2215
+ def wrap_displaced
2216
+ @displaced.each do |node|
2217
+ BRANCH_BODY_PROPERTIES.each do |property|
2218
+ next unless node.respond_to?(property)
2219
+
2220
+ body = node.send(property)
2221
+ next unless body.is_a?(Array) && !body.empty?
2222
+
2223
+ body.unshift(*region_open)
2224
+ body.push(comment_node(@markers.region_close(identifier)))
2225
+ end
2226
+ end
2227
+ end
2228
+
2229
+ def region_open
2230
+ [
2231
+ text_node(@markers.region_open_prefix(identifier, version)),
2232
+ erb_output_node(OCCURRENCE),
2233
+ text_node(@markers.region_open_suffix)
2234
+ ]
2235
+ end
2236
+
2237
+ def comment_node(text)
2238
+ Herb::AST::HTMLCommentNode.build(
2239
+ comment_start: token(:html_comment_start, text),
2240
+ comment_end: token(:html_comment_end, "")
2241
+ )
2242
+ end
2243
+
2244
+ def attribute_node(name, value)
2245
+ name_node = Herb::AST::HTMLAttributeNameNode.build(children: [literal(name)])
2246
+
2247
+ value_node = Herb::AST::HTMLAttributeValueNode.build(
2248
+ open_quote: token(:quote, '"'),
2249
+ children: [literal(value)],
2250
+ close_quote: token(:quote, '"'),
2251
+ quoted: true
2252
+ )
2253
+
2254
+ Herb::AST::HTMLAttributeNode.build(name: name_node, equals: token(:equals, "="), value: value_node)
2255
+ end
2256
+
2257
+ def literal(content)
2258
+ Herb::AST::LiteralNode.build(content: content.dup)
2259
+ end
2260
+
2261
+ def token(type, value)
2262
+ Herb::Token.new(value.dup, Herb::Range.zero, Herb::Location.zero, type.to_s)
2263
+ end
2264
+ end
2265
+ end
2266
+ end
2267
+ end