herb 0.9.4-arm-linux-gnu → 0.9.6-arm-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/config.yml +57 -21
  3. data/ext/herb/extension.c +8 -0
  4. data/ext/herb/extension_helpers.c +1 -0
  5. data/ext/herb/nodes.c +93 -55
  6. data/lib/herb/3.0/herb.so +0 -0
  7. data/lib/herb/3.1/herb.so +0 -0
  8. data/lib/herb/3.2/herb.so +0 -0
  9. data/lib/herb/3.3/herb.so +0 -0
  10. data/lib/herb/3.4/herb.so +0 -0
  11. data/lib/herb/4.0/herb.so +0 -0
  12. data/lib/herb/action_view/helper_registry.rb +8107 -0
  13. data/lib/herb/ast/nodes.rb +212 -78
  14. data/lib/herb/engine/compiler.rb +4 -6
  15. data/lib/herb/parser_options.rb +7 -2
  16. data/lib/herb/project.rb +2 -5
  17. data/lib/herb/version.rb +1 -1
  18. data/lib/herb/visitor.rb +8 -2
  19. data/sig/herb/action_view/helper_registry.rbs +1144 -0
  20. data/sig/herb/ast/nodes.rbs +85 -34
  21. data/sig/herb/parser_options.rbs +6 -2
  22. data/sig/herb/visitor.rbs +5 -2
  23. data/sig/serialized_ast_nodes.rbs +20 -9
  24. data/src/analyze/action_view/generated_handlers.c +355 -0
  25. data/src/analyze/action_view/generated_handlers.h +16 -0
  26. data/src/analyze/action_view/helper_registry.c +7244 -0
  27. data/src/analyze/action_view/image_tag.c +4 -31
  28. data/src/analyze/action_view/javascript_include_tag.c +1 -42
  29. data/src/analyze/action_view/javascript_tag.c +26 -40
  30. data/src/analyze/action_view/registry.c +2 -2
  31. data/src/analyze/action_view/tag_helper_node_builders.c +23 -2
  32. data/src/analyze/action_view/tag_helpers.c +61 -134
  33. data/src/analyze/action_view/turbo_frame_tag.c +1 -36
  34. data/src/analyze/analyze.c +28 -0
  35. data/src/analyze/analyze_helpers.c +406 -0
  36. data/src/analyze/builders.c +1 -0
  37. data/src/analyze/missing_end.c +16 -0
  38. data/src/analyze/parse_errors.c +3 -2
  39. data/src/analyze/postfix_conditionals.c +326 -0
  40. data/src/analyze/render_nodes.c +231 -35
  41. data/src/analyze/strict_locals.c +22 -338
  42. data/src/analyze/ternary_conditionals.c +265 -0
  43. data/src/analyze/transform.c +23 -2
  44. data/src/ast/ast_nodes.c +114 -57
  45. data/src/ast/ast_pretty_print.c +109 -25
  46. data/src/include/analyze/action_view/helper_registry.h +325 -0
  47. data/src/include/analyze/action_view/tag_helper_handler.h +3 -0
  48. data/src/include/analyze/action_view/tag_helper_node_builders.h +7 -0
  49. data/src/include/analyze/action_view/tag_helpers.h +0 -1
  50. data/src/include/analyze/helpers.h +18 -0
  51. data/src/include/analyze/postfix_conditionals.h +9 -0
  52. data/src/include/analyze/ternary_conditionals.h +15 -0
  53. data/src/include/ast/ast_nodes.h +27 -13
  54. data/src/include/parser/parser.h +1 -0
  55. data/src/include/version.h +1 -1
  56. data/src/parser/match_tags.c +37 -6
  57. data/src/parser.c +9 -0
  58. data/src/visitor.c +50 -7
  59. data/templates/java/org/herb/ast/HelperRegistry.java.erb +258 -0
  60. data/templates/javascript/packages/core/src/action-view-helpers.ts.erb +171 -0
  61. data/templates/javascript/packages/core/src/nodes.ts.erb +5 -1
  62. data/templates/lib/herb/action_view/helper_registry.rb.erb +288 -0
  63. data/templates/rust/src/action_view_helpers.rs.erb +154 -0
  64. data/templates/src/analyze/action_view/generated_handlers.c.erb +230 -0
  65. data/templates/src/analyze/action_view/generated_handlers.h.erb +12 -0
  66. data/templates/src/analyze/action_view/helper_registry.c.erb +114 -0
  67. data/templates/src/include/analyze/action_view/helper_registry.h.erb +82 -0
  68. data/templates/template.rb +338 -1
  69. metadata +19 -3
  70. data/src/analyze/action_view/content_tag.c +0 -78
  71. data/src/analyze/action_view/tag.c +0 -87
data/src/visitor.c CHANGED
@@ -237,6 +237,12 @@ void herb_visit_child_nodes(const AST_NODE_T *node, bool (*visitor)(const AST_NO
237
237
  }
238
238
  }
239
239
 
240
+ if (erb_block_node->block_arguments != NULL) {
241
+ for (size_t index = 0; index < hb_array_size(erb_block_node->block_arguments); index++) {
242
+ herb_visit_node(hb_array_get(erb_block_node->block_arguments, index), visitor, data);
243
+ }
244
+ }
245
+
240
246
  if (erb_block_node->rescue_clause != NULL) {
241
247
  herb_visit_node((AST_NODE_T *) erb_block_node->rescue_clause, visitor, data);
242
248
  }
@@ -442,22 +448,59 @@ void herb_visit_child_nodes(const AST_NODE_T *node, bool (*visitor)(const AST_NO
442
448
 
443
449
  } break;
444
450
 
451
+ case AST_RUBY_RENDER_KEYWORDS_NODE: {
452
+ const AST_RUBY_RENDER_KEYWORDS_NODE_T* ruby_render_keywords_node = ((const AST_RUBY_RENDER_KEYWORDS_NODE_T *) node);
453
+
454
+ if (ruby_render_keywords_node->locals != NULL) {
455
+ for (size_t index = 0; index < hb_array_size(ruby_render_keywords_node->locals); index++) {
456
+ herb_visit_node(hb_array_get(ruby_render_keywords_node->locals, index), visitor, data);
457
+ }
458
+ }
459
+
460
+ } break;
461
+
445
462
  case AST_ERB_RENDER_NODE: {
446
463
  const AST_ERB_RENDER_NODE_T* erb_render_node = ((const AST_ERB_RENDER_NODE_T *) node);
447
464
 
448
- if (erb_render_node->locals != NULL) {
449
- for (size_t index = 0; index < hb_array_size(erb_render_node->locals); index++) {
450
- herb_visit_node(hb_array_get(erb_render_node->locals, index), visitor, data);
465
+ if (erb_render_node->keywords != NULL) {
466
+ herb_visit_node((AST_NODE_T *) erb_render_node->keywords, visitor, data);
467
+ }
468
+
469
+ if (erb_render_node->body != NULL) {
470
+ for (size_t index = 0; index < hb_array_size(erb_render_node->body); index++) {
471
+ herb_visit_node(hb_array_get(erb_render_node->body, index), visitor, data);
451
472
  }
452
473
  }
453
474
 
475
+ if (erb_render_node->block_arguments != NULL) {
476
+ for (size_t index = 0; index < hb_array_size(erb_render_node->block_arguments); index++) {
477
+ herb_visit_node(hb_array_get(erb_render_node->block_arguments, index), visitor, data);
478
+ }
479
+ }
480
+
481
+ if (erb_render_node->rescue_clause != NULL) {
482
+ herb_visit_node((AST_NODE_T *) erb_render_node->rescue_clause, visitor, data);
483
+ }
484
+
485
+ if (erb_render_node->else_clause != NULL) {
486
+ herb_visit_node((AST_NODE_T *) erb_render_node->else_clause, visitor, data);
487
+ }
488
+
489
+ if (erb_render_node->ensure_clause != NULL) {
490
+ herb_visit_node((AST_NODE_T *) erb_render_node->ensure_clause, visitor, data);
491
+ }
492
+
493
+ if (erb_render_node->end_node != NULL) {
494
+ herb_visit_node((AST_NODE_T *) erb_render_node->end_node, visitor, data);
495
+ }
496
+
454
497
  } break;
455
498
 
456
- case AST_RUBY_STRICT_LOCAL_NODE: {
457
- const AST_RUBY_STRICT_LOCAL_NODE_T* ruby_strict_local_node = ((const AST_RUBY_STRICT_LOCAL_NODE_T *) node);
499
+ case AST_RUBY_PARAMETER_NODE: {
500
+ const AST_RUBY_PARAMETER_NODE_T* ruby_parameter_node = ((const AST_RUBY_PARAMETER_NODE_T *) node);
458
501
 
459
- if (ruby_strict_local_node->default_value != NULL) {
460
- herb_visit_node((AST_NODE_T *) ruby_strict_local_node->default_value, visitor, data);
502
+ if (ruby_parameter_node->default_value != NULL) {
503
+ herb_visit_node((AST_NODE_T *) ruby_parameter_node->default_value, visitor, data);
461
504
  }
462
505
 
463
506
  } break;
@@ -0,0 +1,258 @@
1
+ package org.herb.ast;
2
+
3
+ import java.util.Collections;
4
+ import java.util.LinkedHashMap;
5
+ import java.util.List;
6
+ import java.util.Map;
7
+
8
+ /**
9
+ * Type enum for all supported Action View tag helpers.
10
+ */
11
+ public enum HelperType {
12
+ <%- helpers.each_with_index do |helper, index| -%>
13
+ <%= helper.constant_name %>("<%= helper.name %>")<%= index < helpers.size - 1 ? "," : ";" %>
14
+ <%- end -%>
15
+
16
+ private final String name;
17
+
18
+ HelperType(String name) {
19
+ this.name = name;
20
+ }
21
+
22
+ public String getName() {
23
+ return name;
24
+ }
25
+
26
+ public static HelperType fromName(String name) {
27
+ for (HelperType type : values()) {
28
+ if (type.name.equals(name)) {
29
+ return type;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Argument metadata for a tag helper.
38
+ */
39
+ class HelperArgument {
40
+ private final String name;
41
+ private final int position;
42
+ private final String type;
43
+ private final boolean optional;
44
+ private final String defaultValue;
45
+ private final boolean splat;
46
+ private final String description;
47
+
48
+ HelperArgument(String name, int position, String type, boolean optional, String defaultValue, boolean splat, String description) {
49
+ this.name = name;
50
+ this.position = position;
51
+ this.type = type;
52
+ this.optional = optional;
53
+ this.defaultValue = defaultValue;
54
+ this.splat = splat;
55
+ this.description = description;
56
+ }
57
+
58
+ public String getName() { return name; }
59
+ public int getPosition() { return position; }
60
+ public String getType() { return type; }
61
+ public boolean isOptional() { return optional; }
62
+ public String getDefaultValue() { return defaultValue; }
63
+ public boolean isSplat() { return splat; }
64
+ public String getDescription() { return description; }
65
+ }
66
+
67
+ /**
68
+ * Option metadata for a tag helper.
69
+ */
70
+ class HelperOption {
71
+ private final String name;
72
+ private final String type;
73
+ private final String mapsTo;
74
+ private final String description;
75
+
76
+ HelperOption(String name, String type, String mapsTo, String description) {
77
+ this.name = name;
78
+ this.type = type;
79
+ this.mapsTo = mapsTo;
80
+ this.description = description;
81
+ }
82
+
83
+ public String getName() { return name; }
84
+ public String getType() { return type; }
85
+ public String getMapsTo() { return mapsTo; }
86
+ public String getDescription() { return description; }
87
+ }
88
+
89
+ /**
90
+ * Implicit attribute metadata for a tag helper.
91
+ */
92
+ class HelperImplicitAttribute {
93
+ private final String name;
94
+ private final String source;
95
+ private final String sourceWithBlock;
96
+ private final String wrapper;
97
+
98
+ HelperImplicitAttribute(String name, String source, String sourceWithBlock, String wrapper) {
99
+ this.name = name;
100
+ this.source = source;
101
+ this.sourceWithBlock = sourceWithBlock;
102
+ this.wrapper = wrapper;
103
+ }
104
+
105
+ public String getName() { return name; }
106
+ public String getSource() { return source; }
107
+ public String getSourceWithBlock() { return sourceWithBlock; }
108
+ public String getWrapper() { return wrapper; }
109
+ }
110
+
111
+ /**
112
+ * Registry entry containing all metadata for a single tag helper.
113
+ */
114
+ class HelperEntry {
115
+ private final String name;
116
+ private final HelperType type;
117
+ private final String source;
118
+ private final String gem;
119
+ private final String output;
120
+ private final String visibility;
121
+ private final String tagName;
122
+ private final boolean isVoid;
123
+ private final boolean supportsBlock;
124
+ private final boolean preferredForTag;
125
+ private final boolean supported;
126
+ private final String detectStyle;
127
+ private final String description;
128
+ private final String signature;
129
+ private final String documentationUrl;
130
+ private final HelperImplicitAttribute implicitAttribute;
131
+ private final List<HelperArgument> arguments;
132
+ private final List<HelperOption> options;
133
+ private final List<String> specialBehaviors;
134
+ private final List<String> aliases;
135
+
136
+ HelperEntry(
137
+ String name, HelperType type, String source, String gem, String output, String visibility, String tagName,
138
+ boolean isVoid, boolean supportsBlock, boolean preferredForTag, boolean supported, String detectStyle, String description,
139
+ String signature, String documentationUrl, HelperImplicitAttribute implicitAttribute,
140
+ List<HelperArgument> arguments, List<HelperOption> options, List<String> specialBehaviors, List<String> aliases
141
+ ) {
142
+ this.name = name;
143
+ this.type = type;
144
+ this.source = source;
145
+ this.gem = gem;
146
+ this.output = output;
147
+ this.visibility = visibility;
148
+ this.tagName = tagName;
149
+ this.isVoid = isVoid;
150
+ this.supportsBlock = supportsBlock;
151
+ this.preferredForTag = preferredForTag;
152
+ this.supported = supported;
153
+ this.detectStyle = detectStyle;
154
+ this.description = description;
155
+ this.signature = signature;
156
+ this.documentationUrl = documentationUrl;
157
+ this.implicitAttribute = implicitAttribute;
158
+ this.arguments = arguments;
159
+ this.options = options;
160
+ this.specialBehaviors = specialBehaviors;
161
+ this.aliases = aliases;
162
+ }
163
+
164
+ public String getName() { return name; }
165
+ public HelperType getType() { return type; }
166
+ public String getSource() { return source; }
167
+ public String getGem() { return gem; }
168
+ public String getOutput() { return output; }
169
+ public String getVisibility() { return visibility; }
170
+ public String getTagName() { return tagName; }
171
+ public boolean isVoid() { return isVoid; }
172
+ public boolean supportsBlock() { return supportsBlock; }
173
+ public boolean isPreferredForTag() { return preferredForTag; }
174
+ public boolean isSupported() { return supported; }
175
+ public String getDetectStyle() { return detectStyle; }
176
+ public String getDescription() { return description; }
177
+ public String getSignature() { return signature; }
178
+ public String getDocumentationUrl() { return documentationUrl; }
179
+ public HelperImplicitAttribute getImplicitAttribute() { return implicitAttribute; }
180
+ public List<HelperArgument> getArguments() { return arguments; }
181
+ public List<HelperOption> getOptions() { return options; }
182
+ public List<String> getSpecialBehaviors() { return specialBehaviors; }
183
+ public List<String> getAliases() { return aliases; }
184
+ }
185
+
186
+ /**
187
+ * Global registry of all supported Action View tag helpers.
188
+ *
189
+ * <p>Usage:
190
+ * <pre>
191
+ * HelperEntry entry = HelperRegistry.get("link_to");
192
+ * entry.supportsBlock(); // true
193
+ * entry.getTagName(); // "a"
194
+ * </pre>
195
+ */
196
+ public final class HelperRegistry {
197
+ private static final Map<String, HelperEntry> BY_NAME;
198
+ private static final Map<String, HelperEntry> BY_SOURCE;
199
+
200
+ static {
201
+ Map<String, HelperEntry> byName = new LinkedHashMap<>();
202
+ Map<String, HelperEntry> bySource = new LinkedHashMap<>();
203
+
204
+ <%- helpers.each do |helper| -%>
205
+ {
206
+ HelperEntry entry = new HelperEntry(
207
+ "<%= helper.name %>",
208
+ HelperType.<%= helper.constant_name %>,
209
+ "<%= helper.source %>",
210
+ "<%= helper.gem %>",
211
+ "<%= helper.output %>",
212
+ "<%= helper.visibility %>",
213
+ <%= helper.tag_name ? "\"#{helper.tag_name}\"" : "null" %>,
214
+ <%= helper.void? %>,
215
+ <%= helper.supports_block %>,
216
+ <%= helper.preferred_for_tag %>,
217
+ <%= helper.supported %>,
218
+ "<%= helper.detect_style || "call_name" %>",
219
+ "<%= helper.escaped_description %>",
220
+ "<%= helper.escaped_signature %>",
221
+ "<%= helper.documentation_url %>",
222
+ <%- if helper.implicit_attribute? -%>
223
+ new HelperImplicitAttribute("<%= helper.implicit_attribute.name %>", "<%= helper.implicit_attribute.source %>", <%= helper.implicit_attribute.source_with_block ? "\"#{helper.implicit_attribute.source_with_block}\"" : "null" %>, "<%= helper.implicit_attribute.wrapper %>"),
224
+ <%- else -%>
225
+ null,
226
+ <%- end -%>
227
+ List.of(<%= helper.arguments.map { |arg| "new HelperArgument(\"#{arg.name}\", #{arg.position}, \"#{arg.type_display}\", #{arg.optional}, #{arg.default ? "\"#{arg.escaped_default}\"" : "null"}, #{arg.splat}, \"#{arg.escaped_description}\")" }.join(", ") %>),
228
+ List.of(<%= helper.options.map { |opt| "new HelperOption(\"#{opt.name}\", \"#{opt.type_display}\", #{opt.maps_to ? "\"#{opt.maps_to}\"" : "null"}, \"#{opt.escaped_description}\")" }.join(", ") %>),
229
+ List.of(<%= helper.special_behaviors.map { |b| "\"#{b}\"" }.join(", ") %>),
230
+ List.of(<%= helper.aliases.map { |a| "\"#{a}\"" }.join(", ") %>)
231
+ );
232
+ byName.put("<%= helper.name %>", entry);
233
+ bySource.put("<%= helper.source %>", entry);
234
+ }
235
+
236
+ <%- end -%>
237
+ BY_NAME = Collections.unmodifiableMap(byName);
238
+ BY_SOURCE = Collections.unmodifiableMap(bySource);
239
+ }
240
+
241
+ private HelperRegistry() {}
242
+
243
+ public static HelperEntry get(String name) {
244
+ return BY_NAME.get(name);
245
+ }
246
+
247
+ public static HelperEntry findBySource(String source) {
248
+ return BY_SOURCE.get(source);
249
+ }
250
+
251
+ public static List<HelperEntry> entries() {
252
+ return List.copyOf(BY_NAME.values());
253
+ }
254
+
255
+ public static int count() {
256
+ return BY_NAME.size();
257
+ }
258
+ }
@@ -0,0 +1,171 @@
1
+ export enum HelperType {
2
+ <%- helpers.each do |helper| -%>
3
+ <%= helper.camel_case_name %> = "<%= helper.name %>",
4
+ <%- end -%>
5
+ }
6
+
7
+ export type HelperDetectStyle = "call_name" | "receiver_call"
8
+ export type HelperOutput = "html" | "text" | "url" | "boolean" | "void" | "object"
9
+
10
+ export interface HelperArgument {
11
+ readonly name: string
12
+ readonly position: number
13
+ readonly type: string
14
+ readonly optional: boolean
15
+ readonly default: string | null
16
+ readonly splat: boolean
17
+ readonly description: string
18
+ }
19
+
20
+ export interface HelperOption {
21
+ readonly name: string
22
+ readonly type: string
23
+ readonly mapsTo: string | null
24
+ readonly description: string
25
+ }
26
+
27
+ export interface HelperImplicitAttribute {
28
+ readonly name: string
29
+ readonly source: string
30
+ readonly sourceWithBlock: string | null
31
+ readonly wrapper: string
32
+ readonly skipWrappingFor: string[]
33
+ }
34
+
35
+ export interface HelperEntry {
36
+ readonly name: string
37
+ readonly type: HelperType
38
+ readonly source: string
39
+ readonly gem: string
40
+ readonly output: HelperOutput
41
+ readonly visibility: "public" | "internal"
42
+ readonly tagName: string | null
43
+ readonly isVoid: boolean
44
+ readonly supportsBlock: boolean
45
+ readonly preferredForTag: boolean
46
+ readonly supported: boolean
47
+ readonly detectStyle: HelperDetectStyle
48
+ readonly description: string
49
+ readonly signature: string
50
+ readonly documentationURL: string
51
+ readonly implicitAttribute: HelperImplicitAttribute | null
52
+ readonly arguments: HelperArgument[]
53
+ readonly options: HelperOption[]
54
+ readonly specialBehaviors: string[]
55
+ readonly aliases: string[]
56
+ }
57
+
58
+ <%- helpers.each do |helper| -%>
59
+ const <%= helper.lower_camel_case_name %>Entry: HelperEntry = {
60
+ name: "<%= helper.name %>",
61
+ type: HelperType.<%= helper.camel_case_name %>,
62
+ source: "<%= helper.source %>",
63
+ gem: "<%= helper.gem %>",
64
+ output: "<%= helper.output %>",
65
+ visibility: "<%= helper.visibility %>",
66
+ tagName: <%= helper.tag_name ? "\"#{helper.tag_name}\"" : "null" %>,
67
+ isVoid: <%= helper.void? %>,
68
+ supportsBlock: <%= helper.supports_block %>,
69
+ preferredForTag: <%= helper.preferred_for_tag %>,
70
+ supported: <%= helper.supported %>,
71
+ detectStyle: "<%= helper.detect_style || "call_name" %>",
72
+ description: "<%= helper.escaped_description %>",
73
+ signature: "<%= helper.escaped_signature %>",
74
+ documentationURL: "<%= helper.documentation_url %>",
75
+ <%- if helper.implicit_attribute? -%>
76
+ implicitAttribute: {
77
+ name: "<%= helper.implicit_attribute.name %>",
78
+ source: "<%= helper.implicit_attribute.source %>",
79
+ sourceWithBlock: <%= helper.implicit_attribute.source_with_block ? "\"#{helper.implicit_attribute.source_with_block}\"" : "null" %>,
80
+ wrapper: "<%= helper.implicit_attribute.wrapper %>",
81
+ skipWrappingFor: [<%= helper.implicit_attribute.skip_wrapping_for.map { |s| "\"#{s}\"" }.join(", ") %>],
82
+ },
83
+ <%- else -%>
84
+ implicitAttribute: null,
85
+ <%- end -%>
86
+ arguments: [
87
+ <%- helper.arguments.each do |arg| -%>
88
+ { name: "<%= arg.name %>", position: <%= arg.position %>, type: "<%= arg.type_display %>", optional: <%= arg.optional %>, default: <%= arg.default ? "\"#{arg.escaped_default}\"" : "null" %>, splat: <%= arg.splat %>, description: "<%= arg.escaped_description %>" },
89
+ <%- end -%>
90
+ ],
91
+ options: [
92
+ <%- helper.options.each do |opt| -%>
93
+ { name: "<%= opt.name %>", type: "<%= opt.type_display %>", mapsTo: <%= opt.maps_to ? "\"#{opt.maps_to}\"" : "null" %>, description: "<%= opt.escaped_description %>" },
94
+ <%- end -%>
95
+ ],
96
+ specialBehaviors: [<%= helper.special_behaviors.map { |b| "\"#{b}\"" }.join(", ") %>],
97
+ aliases: [<%= helper.aliases.map { |a| "\"#{a}\"" }.join(", ") %>],
98
+ }
99
+
100
+ <%- end -%>
101
+ export const HELPER_REGISTRY: Record<string, HelperEntry> = {
102
+ <%- helpers.each do |helper| -%>
103
+ "<%= helper.name %>": <%= helper.lower_camel_case_name %>Entry,
104
+ <%- helper.aliases.each do |alias_name| -%>
105
+ "<%= alias_name %>": <%= helper.lower_camel_case_name %>Entry,
106
+ <%- end -%>
107
+ <%- end -%>
108
+ }
109
+
110
+ export const HELPER_BY_SOURCE: Record<string, HelperEntry> = {
111
+ <%- helpers.each do |helper| -%>
112
+ "<%= helper.source %>": <%= helper.lower_camel_case_name %>Entry,
113
+ <%- end -%>
114
+ }
115
+
116
+ <%- helpers_by_tag = helpers.select(&:static_tag_name?).group_by(&:tag_name) -%>
117
+ export const HELPERS_BY_TAG_NAME: Record<string, HelperEntry[]> = {
118
+ <%- helpers_by_tag.each do |tag_name, helpers| -%>
119
+ "<%= tag_name %>": [<%= helpers.map { |h| "#{h.lower_camel_case_name}Entry" }.join(", ") %>],
120
+ <%- end -%>
121
+ }
122
+
123
+ export function getHelper(name: string): HelperEntry | undefined {
124
+ return HELPER_REGISTRY[name]
125
+ }
126
+
127
+ export function getHelperBySource(source: string): HelperEntry | undefined {
128
+ return HELPER_BY_SOURCE[source]
129
+ }
130
+
131
+ export function getHelperEntries(): HelperEntry[] {
132
+ return Object.values(HELPER_REGISTRY)
133
+ }
134
+
135
+ export function isHelperSupported(name: string): boolean {
136
+ return HELPER_REGISTRY[name]?.supported ?? false
137
+ }
138
+
139
+ export function helperExists(name: string): boolean {
140
+ return name in HELPER_REGISTRY
141
+ }
142
+
143
+ export function getHelpersForTag(tagName: string): HelperEntry[] {
144
+ return HELPERS_BY_TAG_NAME[tagName] ?? []
145
+ }
146
+
147
+ export function findPreferredHelperForTag(tagName: string): HelperEntry | undefined {
148
+ const helpers = HELPERS_BY_TAG_NAME[tagName]
149
+ if (!helpers) return undefined
150
+
151
+ return helpers.find(entry => entry.preferredForTag) ?? helpers[0]
152
+ }
153
+
154
+ export function getSupportedHelpers(): HelperEntry[] {
155
+ return Object.values(HELPER_REGISTRY).filter(entry => entry.supported)
156
+ }
157
+
158
+ export function getHelpersByGem(gem: string): HelperEntry[] {
159
+ return Object.values(HELPER_REGISTRY).filter(entry => entry.gem === gem)
160
+ }
161
+
162
+ export function getHelpersByOutput(output: HelperOutput): HelperEntry[] {
163
+ return Object.values(HELPER_REGISTRY).filter(entry => entry.output === output)
164
+ }
165
+
166
+ export function getHelpersByModule(moduleName: string): HelperEntry[] {
167
+ return Object.values(HELPER_REGISTRY).filter(entry => {
168
+ const parts = entry.source.split("#")[0].split("::")
169
+ return parts[parts.length - 1] === moduleName
170
+ })
171
+ }
@@ -318,7 +318,11 @@ export class <%= node.name %> extends Node {
318
318
  }
319
319
 
320
320
  accept(visitor: Visitor): void {
321
- visitor.visit<%= node.name %>(this)
321
+ const visitMethod = visitor.visit<%= node.name %> as ((node: <%= node.name %>) => void) | undefined
322
+
323
+ if (typeof visitMethod === "function") {
324
+ visitMethod.call(visitor, this)
325
+ }
322
326
  }
323
327
 
324
328
  childNodes(): (Node | null | undefined)[] {