vident 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +49 -18
- data/lib/vident/caching.rb +4 -110
- data/lib/vident/capabilities/caching.rb +98 -0
- data/lib/vident/capabilities/child_element_rendering.rb +92 -0
- data/lib/vident/capabilities/class_list_building.rb +23 -0
- data/lib/vident/capabilities/declarable.rb +39 -0
- data/lib/vident/capabilities/identifiable.rb +54 -0
- data/lib/vident/capabilities/inspectable.rb +17 -0
- data/lib/vident/capabilities/root_element_rendering.rb +31 -0
- data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
- data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
- data/lib/vident/capabilities/stimulus_draft.rb +51 -0
- data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
- data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
- data/lib/vident/capabilities/tailwind.rb +18 -0
- data/lib/vident/component.rb +14 -76
- data/lib/vident/engine.rb +6 -5
- data/lib/vident/error.rb +16 -0
- data/lib/vident/internals/action_builder.rb +97 -0
- data/lib/vident/internals/attribute_writer.rb +17 -0
- data/lib/vident/internals/class_list_builder.rb +62 -0
- data/lib/vident/internals/declaration.rb +13 -0
- data/lib/vident/internals/declarations.rb +64 -0
- data/lib/vident/internals/draft.rb +47 -0
- data/lib/vident/internals/dsl.rb +172 -0
- data/lib/vident/internals/plan.rb +9 -0
- data/lib/vident/internals/registry.rb +37 -0
- data/lib/vident/internals/resolver.rb +316 -0
- data/lib/vident/internals/target_builder.rb +23 -0
- data/lib/vident/stable_id.rb +3 -3
- data/lib/vident/stimulus/action.rb +127 -0
- data/lib/vident/stimulus/base.rb +26 -0
- data/lib/vident/stimulus/class_map.rb +57 -0
- data/lib/vident/stimulus/collection.rb +40 -0
- data/lib/vident/stimulus/combinable.rb +30 -0
- data/lib/vident/stimulus/controller.rb +45 -0
- data/lib/vident/stimulus/naming.rb +9 -9
- data/lib/vident/stimulus/null.rb +7 -0
- data/lib/vident/stimulus/outlet.rb +93 -0
- data/lib/vident/stimulus/param.rb +56 -0
- data/lib/vident/stimulus/target.rb +48 -0
- data/lib/vident/stimulus/value.rb +57 -0
- data/lib/vident/stimulus_null.rb +4 -8
- data/lib/vident/tailwind.rb +4 -17
- data/lib/vident/types.rb +28 -0
- data/lib/vident/version.rb +1 -6
- data/lib/vident.rb +44 -36
- data/skills/vident/SKILL.md +133 -21
- data/skills/vident/api-reference.md +662 -0
- data/skills/vident/examples.md +505 -0
- metadata +40 -28
- data/lib/vident/child_element_helper.rb +0 -64
- data/lib/vident/class_list_builder.rb +0 -112
- data/lib/vident/component_attribute_resolver.rb +0 -87
- data/lib/vident/component_class_lists.rb +0 -34
- data/lib/vident/stimulus/primitive.rb +0 -38
- data/lib/vident/stimulus.rb +0 -31
- data/lib/vident/stimulus_action.rb +0 -133
- data/lib/vident/stimulus_action_collection.rb +0 -11
- data/lib/vident/stimulus_attribute_base.rb +0 -67
- data/lib/vident/stimulus_attributes.rb +0 -129
- data/lib/vident/stimulus_builder.rb +0 -119
- data/lib/vident/stimulus_class.rb +0 -59
- data/lib/vident/stimulus_class_collection.rb +0 -11
- data/lib/vident/stimulus_collection_base.rb +0 -51
- data/lib/vident/stimulus_component.rb +0 -75
- data/lib/vident/stimulus_controller.rb +0 -41
- data/lib/vident/stimulus_controller_collection.rb +0 -14
- data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
- data/lib/vident/stimulus_helper.rb +0 -66
- data/lib/vident/stimulus_outlet.rb +0 -90
- data/lib/vident/stimulus_outlet_collection.rb +0 -11
- data/lib/vident/stimulus_param.rb +0 -42
- data/lib/vident/stimulus_param_collection.rb +0 -11
- data/lib/vident/stimulus_target.rb +0 -47
- data/lib/vident/stimulus_target_collection.rb +0 -18
- data/lib/vident/stimulus_value.rb +0 -39
- data/lib/vident/stimulus_value_collection.rb +0 -11
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registry"
|
|
4
|
+
require_relative "draft"
|
|
5
|
+
|
|
6
|
+
module Vident
|
|
7
|
+
module Internals
|
|
8
|
+
# Resolves raw Declarations and instance state into a Draft of typed Stimulus values.
|
|
9
|
+
# Phase `:static` skips proc-bearing entries; `:procs` processes only those; `:all` does both.
|
|
10
|
+
# Procs nested inside a Hash descriptor escape the phase gate — use the fluent builder instead.
|
|
11
|
+
module Resolver
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(declarations, instance, phase: :all)
|
|
15
|
+
raise ArgumentError, "use resolve_procs_into for phase: :procs" if phase == :procs
|
|
16
|
+
|
|
17
|
+
draft = Draft.new
|
|
18
|
+
implied = build_implied_controller(instance)
|
|
19
|
+
alias_map = build_alias_map(declarations)
|
|
20
|
+
|
|
21
|
+
seed_implied_controller(draft, instance)
|
|
22
|
+
resolve_declarations(draft, declarations, instance, implied, phase:, alias_map:)
|
|
23
|
+
absorb_stimulus_props(draft, instance, implied, phase:, alias_map:)
|
|
24
|
+
absorb_root_element_attributes(draft, instance, implied, phase:, alias_map:)
|
|
25
|
+
|
|
26
|
+
draft
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_procs_into(draft, declarations, instance)
|
|
30
|
+
implied = build_implied_controller(instance)
|
|
31
|
+
alias_map = build_alias_map(declarations)
|
|
32
|
+
resolve_declarations(draft, declarations, instance, implied, phase: :procs, alias_map:)
|
|
33
|
+
absorb_stimulus_props(draft, instance, implied, phase: :procs, alias_map:)
|
|
34
|
+
absorb_root_element_attributes(draft, instance, implied, phase: :procs, alias_map:)
|
|
35
|
+
draft
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_alias_map(declarations)
|
|
39
|
+
map = {}
|
|
40
|
+
declarations.controllers.each do |decl|
|
|
41
|
+
alias_name = decl.meta[:as]
|
|
42
|
+
next unless alias_name
|
|
43
|
+
raw_path = decl.args.first
|
|
44
|
+
next if raw_path.nil?
|
|
45
|
+
map[alias_name] = raw_path.to_s
|
|
46
|
+
end
|
|
47
|
+
map
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_implied_controller(instance)
|
|
51
|
+
path = instance.class.stimulus_identifier_path
|
|
52
|
+
name = instance.class.stimulus_identifier
|
|
53
|
+
::Vident::Stimulus::Controller.new(path: path, name: name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def seed_implied_controller(draft, instance)
|
|
57
|
+
return unless instance.class.stimulus_controller?
|
|
58
|
+
draft.add_controllers(build_implied_controller(instance))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_declarations(draft, declarations, instance, implied, phase:, alias_map: {})
|
|
62
|
+
resolve_positional(draft, :controllers, declarations.controllers, instance, phase:) do |args, meta, _inst|
|
|
63
|
+
::Vident::Stimulus::Controller.parse(*args, implied: implied, **meta_for_controller(meta))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
resolve_positional(draft, :actions, declarations.actions, instance, phase:) do |args, _meta, _inst|
|
|
67
|
+
parse_single(::Vident::Stimulus::Action, resolve_action_aliases(args, alias_map), implied: implied, component_id: instance_id(instance))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
resolve_positional(draft, :targets, declarations.targets, instance, phase:) do |args, _meta, _inst|
|
|
71
|
+
parse_single(::Vident::Stimulus::Target, args, implied: implied, component_id: instance_id(instance))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
resolve_keyed(draft, :outlets, declarations.outlets, instance, phase:) do |key, args, _meta|
|
|
75
|
+
parsed_args = [key_for_parse(key), *args]
|
|
76
|
+
parse_single(::Vident::Stimulus::Outlet, parsed_args, implied: implied, component_id: instance_id(instance))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
resolve_keyed_values(draft, declarations, instance, implied, phase:)
|
|
80
|
+
resolve_keyed_scalars(draft, :params, declarations.params, instance, implied, ::Vident::Stimulus::Param, phase:)
|
|
81
|
+
resolve_keyed_scalars(draft, :class_maps, declarations.class_maps, instance, implied, ::Vident::Stimulus::ClassMap, phase:)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Unknown aliases raise — a symbolic controller ref is declared intent, not a guess.
|
|
85
|
+
def resolve_action_aliases(args, alias_map)
|
|
86
|
+
return args if alias_map.empty?
|
|
87
|
+
args.map do |arg|
|
|
88
|
+
next arg unless arg.is_a?(Hash) && arg[:controller].is_a?(Symbol)
|
|
89
|
+
sym = arg[:controller]
|
|
90
|
+
unless alias_map.key?(sym)
|
|
91
|
+
raise ::Vident::DeclarationError, "Unknown controller alias :#{sym} in action. Declared aliases: #{alias_map.keys.inspect}"
|
|
92
|
+
end
|
|
93
|
+
arg.merge(controller: alias_map[sym])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_positional(draft, kind, entries, instance, phase:)
|
|
98
|
+
entries.each do |decl|
|
|
99
|
+
next unless phase_matches?(decl, phase)
|
|
100
|
+
next if gated_out?(decl.when_proc, instance)
|
|
101
|
+
args = resolve_args(decl.args, instance)
|
|
102
|
+
next if args.nil?
|
|
103
|
+
args = splat_single_array(args)
|
|
104
|
+
parsed = yield(args, decl.meta, instance)
|
|
105
|
+
draft.public_send(:"add_#{kind}", parsed) if parsed
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def splat_single_array(args)
|
|
110
|
+
(args.size == 1 && args[0].is_a?(Array)) ? args[0] : args
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resolve_keyed(draft, kind, entries, instance, phase:)
|
|
114
|
+
entries.each do |(key, decl)|
|
|
115
|
+
next unless phase_matches?(decl, phase)
|
|
116
|
+
next if gated_out?(decl.when_proc, instance)
|
|
117
|
+
args = resolve_args(decl.args, instance)
|
|
118
|
+
next if args.nil?
|
|
119
|
+
parsed = yield(key, args, decl.meta)
|
|
120
|
+
draft.public_send(:"add_#{kind}", parsed) if parsed
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve_keyed_values(draft, declarations, instance, implied, phase:)
|
|
125
|
+
declarations.values.each do |(key, decl)|
|
|
126
|
+
next unless phase_matches?(decl, phase)
|
|
127
|
+
next if gated_out?(decl.when_proc, instance)
|
|
128
|
+
|
|
129
|
+
if decl.meta[:from_prop]
|
|
130
|
+
raw = read_prop(instance, key)
|
|
131
|
+
next if raw.nil?
|
|
132
|
+
draft.add_values(::Vident::Stimulus::Value.parse(key, raw, implied: implied))
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
raw = resolve_value_meta(decl, instance)
|
|
137
|
+
next if raw.nil?
|
|
138
|
+
draft.add_values(::Vident::Stimulus::Value.parse(key, raw, implied: implied))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# values_from_props has no when_proc, so phase_matches? doesn't apply; skip on :procs pass.
|
|
142
|
+
return if phase == :procs
|
|
143
|
+
declarations.values_from_props.each do |name|
|
|
144
|
+
raw = read_prop(instance, name)
|
|
145
|
+
next if raw.nil?
|
|
146
|
+
draft.add_values(::Vident::Stimulus::Value.parse(name, raw, implied: implied))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def resolve_keyed_scalars(draft, kind, entries, instance, implied, value_class, phase:)
|
|
151
|
+
entries.each do |(key, decl)|
|
|
152
|
+
next unless phase_matches?(decl, phase)
|
|
153
|
+
next if gated_out?(decl.when_proc, instance)
|
|
154
|
+
raw = resolve_value_meta(decl, instance)
|
|
155
|
+
next if raw.nil?
|
|
156
|
+
draft.public_send(:"add_#{kind}", value_class.parse(key, raw, implied: implied))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def phase_matches?(decl, phase)
|
|
161
|
+
return true if phase == :all
|
|
162
|
+
has_proc = decl.when_proc || decl.args.any? { |a| a.is_a?(Proc) }
|
|
163
|
+
(phase == :procs) ? has_proc : !has_proc
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def absorb_stimulus_props(draft, instance, implied, phase:, alias_map: {})
|
|
167
|
+
absorb_input(draft, :controllers, instance_ivar(instance, :@stimulus_controllers), instance, implied, phase:, alias_map:)
|
|
168
|
+
absorb_input(draft, :actions, instance_ivar(instance, :@stimulus_actions), instance, implied, phase:, alias_map:)
|
|
169
|
+
absorb_input(draft, :targets, instance_ivar(instance, :@stimulus_targets), instance, implied, phase:, alias_map:)
|
|
170
|
+
absorb_input(draft, :outlets, instance_ivar(instance, :@stimulus_outlets), instance, implied, phase:, alias_map:)
|
|
171
|
+
absorb_input(draft, :values, instance_ivar(instance, :@stimulus_values), instance, implied, phase:, alias_map:)
|
|
172
|
+
absorb_input(draft, :params, instance_ivar(instance, :@stimulus_params), instance, implied, phase:, alias_map:)
|
|
173
|
+
absorb_input(draft, :class_maps, instance_ivar(instance, :@stimulus_classes), instance, implied, phase:, alias_map:)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def absorb_root_element_attributes(draft, instance, implied, phase:, alias_map: {})
|
|
177
|
+
return unless instance.respond_to?(:resolved_root_element_attributes, true)
|
|
178
|
+
attrs = instance.send(:resolved_root_element_attributes)
|
|
179
|
+
return unless attrs.is_a?(Hash) && !attrs.empty?
|
|
180
|
+
|
|
181
|
+
absorb_input(draft, :controllers, attrs[:stimulus_controllers], instance, implied, phase:, alias_map:)
|
|
182
|
+
absorb_input(draft, :actions, attrs[:stimulus_actions], instance, implied, phase:, alias_map:)
|
|
183
|
+
absorb_input(draft, :targets, attrs[:stimulus_targets], instance, implied, phase:, alias_map:)
|
|
184
|
+
absorb_input(draft, :outlets, attrs[:stimulus_outlets], instance, implied, phase:, alias_map:)
|
|
185
|
+
absorb_input(draft, :values, attrs[:stimulus_values], instance, implied, phase:, alias_map:)
|
|
186
|
+
absorb_input(draft, :params, attrs[:stimulus_params], instance, implied, phase:, alias_map:)
|
|
187
|
+
absorb_input(draft, :class_maps, attrs[:stimulus_classes], instance, implied, phase:, alias_map:)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def absorb_input(draft, kind, input, instance, implied, phase:, alias_map: {})
|
|
191
|
+
return if input.nil?
|
|
192
|
+
|
|
193
|
+
kind_meta = Registry.fetch(kind)
|
|
194
|
+
case input
|
|
195
|
+
in Hash => h
|
|
196
|
+
h.each do |key, raw|
|
|
197
|
+
is_proc = raw.is_a?(Proc)
|
|
198
|
+
next unless phase_allows?(is_proc, phase)
|
|
199
|
+
absorbed = is_proc ? instance.instance_exec(&raw) : raw
|
|
200
|
+
next if absorbed.nil?
|
|
201
|
+
if kind_meta.keyed?
|
|
202
|
+
parsed = kind_meta.value_class.parse(key, absorbed, implied: implied, component_id: instance_id(instance))
|
|
203
|
+
draft.public_send(:"add_#{kind}", parsed)
|
|
204
|
+
else
|
|
205
|
+
entry = resolve_absorb_alias(kind, [key, absorbed], alias_map)
|
|
206
|
+
parsed = parse_entry(kind_meta, entry, implied: implied, component_id: instance_id(instance))
|
|
207
|
+
draft.public_send(:"add_#{kind}", parsed) if parsed
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
in Array => a
|
|
211
|
+
a.each do |entry|
|
|
212
|
+
is_proc = entry.is_a?(Proc)
|
|
213
|
+
next unless phase_allows?(is_proc, phase)
|
|
214
|
+
parsed = absorb_one(kind_meta, entry, instance, implied, kind:, alias_map:)
|
|
215
|
+
draft.public_send(:"add_#{kind}", parsed) if parsed
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
is_proc = input.is_a?(Proc)
|
|
219
|
+
return unless phase_allows?(is_proc, phase)
|
|
220
|
+
parsed = absorb_one(kind_meta, input, instance, implied, kind:, alias_map:)
|
|
221
|
+
draft.public_send(:"add_#{kind}", parsed) if parsed
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def resolve_absorb_alias(kind, entry, alias_map)
|
|
226
|
+
return entry unless kind == :actions && alias_map.any?
|
|
227
|
+
return entry unless entry.is_a?(Hash) && entry[:controller].is_a?(Symbol)
|
|
228
|
+
sym = entry[:controller]
|
|
229
|
+
unless alias_map.key?(sym)
|
|
230
|
+
raise ::Vident::DeclarationError, "Unknown controller alias :#{sym} in stimulus_actions input. Declared aliases: #{alias_map.keys.inspect}"
|
|
231
|
+
end
|
|
232
|
+
entry.merge(controller: alias_map[sym])
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Uses pattern matching so an unknown phase raises NoMatchingPatternError early.
|
|
236
|
+
def phase_allows?(is_proc, phase)
|
|
237
|
+
case phase
|
|
238
|
+
in :all then true
|
|
239
|
+
in :static then !is_proc
|
|
240
|
+
in :procs then is_proc
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def absorb_one(kind_meta, entry, instance, implied, kind: nil, alias_map: {})
|
|
245
|
+
entry = instance.instance_exec(&entry) if entry.is_a?(Proc)
|
|
246
|
+
return nil if entry.nil?
|
|
247
|
+
return entry if entry.is_a?(kind_meta.value_class)
|
|
248
|
+
|
|
249
|
+
entry = resolve_absorb_alias(kind, entry, alias_map) if kind
|
|
250
|
+
parse_entry(kind_meta, entry, implied: implied, component_id: instance_id(instance))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def parse_entry(kind_meta, entry, implied:, component_id:)
|
|
254
|
+
case entry
|
|
255
|
+
when Hash
|
|
256
|
+
if kind_meta.keyed?
|
|
257
|
+
first_key, first_val = entry.first
|
|
258
|
+
kind_meta.value_class.parse(first_key, first_val, implied: implied, component_id: component_id)
|
|
259
|
+
else
|
|
260
|
+
kind_meta.value_class.parse(entry, implied: implied, component_id: component_id)
|
|
261
|
+
end
|
|
262
|
+
when Array
|
|
263
|
+
kind_meta.value_class.parse(*entry, implied: implied, component_id: component_id)
|
|
264
|
+
else
|
|
265
|
+
kind_meta.value_class.parse(entry, implied: implied, component_id: component_id)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Only nil drops — false, blank strings, and empty collections reach the parser.
|
|
270
|
+
def resolve_args(args, instance)
|
|
271
|
+
resolved = args.map { |arg| arg.is_a?(Proc) ? instance.instance_exec(&arg) : arg }
|
|
272
|
+
return nil if resolved.any?(&:nil?)
|
|
273
|
+
resolved
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def resolve_value_meta(decl, instance)
|
|
277
|
+
return decl.meta[:static] if decl.meta.key?(:static)
|
|
278
|
+
return nil if decl.args.empty?
|
|
279
|
+
|
|
280
|
+
raw = decl.args.first
|
|
281
|
+
raw = instance.instance_exec(&raw) if raw.is_a?(Proc)
|
|
282
|
+
raw
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def gated_out?(when_proc, instance)
|
|
286
|
+
return false unless when_proc
|
|
287
|
+
!instance.instance_exec(&when_proc)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def parse_single(value_class, args, implied:, component_id:)
|
|
291
|
+
value_class.parse(*args, implied: implied, component_id: component_id)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def key_for_parse(key) = key
|
|
295
|
+
|
|
296
|
+
def meta_for_controller(meta) = meta.slice(:as)
|
|
297
|
+
|
|
298
|
+
def instance_ivar(instance, name)
|
|
299
|
+
return nil unless instance.instance_variable_defined?(name)
|
|
300
|
+
instance.instance_variable_get(name)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Must match the mutation-API path (#id, memoised) so DSL outlet
|
|
304
|
+
# auto-selectors and runtime-added outlets scope identically.
|
|
305
|
+
def instance_id(instance)
|
|
306
|
+
instance.id
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def read_prop(instance, name)
|
|
310
|
+
ivar = :"@#{name}"
|
|
311
|
+
return nil unless instance.instance_variable_defined?(ivar)
|
|
312
|
+
instance.instance_variable_get(ivar)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "declaration"
|
|
4
|
+
|
|
5
|
+
module Vident
|
|
6
|
+
module Internals
|
|
7
|
+
class TargetBuilder
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
@args = args
|
|
10
|
+
@when_proc = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def when(callable = nil, &block)
|
|
14
|
+
@when_proc = block || callable
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_declaration
|
|
19
|
+
Declaration.new(args: @args.freeze, when_proc: @when_proc, meta: {}.freeze)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/vident/stable_id.rb
CHANGED
|
@@ -5,9 +5,9 @@ require "digest/md5"
|
|
|
5
5
|
|
|
6
6
|
module Vident
|
|
7
7
|
class StableId
|
|
8
|
-
class GeneratorNotSetError <
|
|
8
|
+
class GeneratorNotSetError < ::Vident::ConfigurationError; end
|
|
9
9
|
|
|
10
|
-
class StrategyNotConfiguredError <
|
|
10
|
+
class StrategyNotConfiguredError < ::Vident::ConfigurationError; end
|
|
11
11
|
|
|
12
12
|
RANDOM_FALLBACK = ->(generator) do
|
|
13
13
|
return Random.hex(16) unless generator
|
|
@@ -25,7 +25,7 @@ module Vident
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
class << self
|
|
28
|
-
# Callable(generator_or_nil) -> String.
|
|
28
|
+
# Callable(generator_or_nil) -> String. Host app must configure before first render.
|
|
29
29
|
attr_accessor :strategy
|
|
30
30
|
|
|
31
31
|
def set_current_sequence_generator(seed:)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "literal"
|
|
4
|
+
require_relative "naming"
|
|
5
|
+
require_relative "controller"
|
|
6
|
+
require_relative "base"
|
|
7
|
+
|
|
8
|
+
module Vident
|
|
9
|
+
module Stimulus
|
|
10
|
+
# `data-action` fragment: single action descriptor like
|
|
11
|
+
# `"click->admin--users#handleClick"`.
|
|
12
|
+
class Action < Base
|
|
13
|
+
# Keep in sync with https://stimulus.hotwired.dev/reference/actions#options.
|
|
14
|
+
VALID_OPTIONS = %i[once prevent stop passive !passive capture self].freeze
|
|
15
|
+
|
|
16
|
+
prop :controller, Controller
|
|
17
|
+
prop :method_name, String
|
|
18
|
+
prop :event, _Nilable(String), default: nil
|
|
19
|
+
prop :modifiers, _Array(Symbol), default: -> { [] }
|
|
20
|
+
prop :keyboard, _Nilable(String), default: nil
|
|
21
|
+
prop :window, _Boolean, default: false
|
|
22
|
+
|
|
23
|
+
def self.parse(*args, implied:, component_id: nil)
|
|
24
|
+
case args
|
|
25
|
+
in [Action => a]
|
|
26
|
+
a
|
|
27
|
+
in [Symbol => event, Action => a]
|
|
28
|
+
a.with(event: event.to_s)
|
|
29
|
+
in [Hash => h]
|
|
30
|
+
from_descriptor(h, implied: implied)
|
|
31
|
+
in [Symbol => method_sym]
|
|
32
|
+
new(
|
|
33
|
+
controller: implied,
|
|
34
|
+
method_name: Naming.js_name(method_sym),
|
|
35
|
+
event: nil
|
|
36
|
+
)
|
|
37
|
+
in [String => s]
|
|
38
|
+
parse_qualified_string(s)
|
|
39
|
+
in [Symbol => event, Symbol => method_sym]
|
|
40
|
+
new(
|
|
41
|
+
controller: implied,
|
|
42
|
+
method_name: Naming.js_name(method_sym),
|
|
43
|
+
event: event.to_s
|
|
44
|
+
)
|
|
45
|
+
in [String => ctrl_path, Symbol => method_sym]
|
|
46
|
+
new(
|
|
47
|
+
controller: Controller.parse(ctrl_path, implied: implied),
|
|
48
|
+
method_name: Naming.js_name(method_sym),
|
|
49
|
+
event: nil
|
|
50
|
+
)
|
|
51
|
+
in [Symbol => event, String => ctrl_path, Symbol => method_sym]
|
|
52
|
+
new(
|
|
53
|
+
controller: Controller.parse(ctrl_path, implied: implied),
|
|
54
|
+
method_name: Naming.js_name(method_sym),
|
|
55
|
+
event: event.to_s
|
|
56
|
+
)
|
|
57
|
+
else
|
|
58
|
+
raise ::Vident::ParseError, "Action.parse: invalid arguments #{args.inspect}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
head =
|
|
64
|
+
if event
|
|
65
|
+
ev = event.to_s
|
|
66
|
+
ev = "#{ev}.#{keyboard}" if keyboard
|
|
67
|
+
ev = "#{ev}#{modifiers.map { |o| ":#{o}" }.join}" if modifiers.any?
|
|
68
|
+
ev = "#{ev}@window" if window
|
|
69
|
+
"#{ev}->"
|
|
70
|
+
else
|
|
71
|
+
""
|
|
72
|
+
end
|
|
73
|
+
"#{head}#{controller.name}##{method_name}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_data_pair = [:action, to_s]
|
|
77
|
+
|
|
78
|
+
def to_h = {action: to_s}
|
|
79
|
+
alias_method :to_hash, :to_h
|
|
80
|
+
|
|
81
|
+
def self.to_data_hash(actions)
|
|
82
|
+
return {} if actions.empty?
|
|
83
|
+
{action: actions.map(&:to_s).join(" ")}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.from_descriptor(h, implied:)
|
|
87
|
+
invalid_options = Array(h[:options]) - VALID_OPTIONS
|
|
88
|
+
unless invalid_options.empty?
|
|
89
|
+
raise ::Vident::ParseError,
|
|
90
|
+
"Action.parse: invalid option(s) #{invalid_options.inspect}. Valid: #{VALID_OPTIONS.inspect}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
method_raw = h.fetch(:method)
|
|
94
|
+
method_name = method_raw.is_a?(Symbol) ? Naming.js_name(method_raw) : method_raw.to_s
|
|
95
|
+
controller = h[:controller] ? Controller.parse(h[:controller], implied: implied) : implied
|
|
96
|
+
new(
|
|
97
|
+
controller: controller,
|
|
98
|
+
method_name: method_name,
|
|
99
|
+
event: h[:event]&.to_s,
|
|
100
|
+
modifiers: Array(h[:options]),
|
|
101
|
+
keyboard: h[:keyboard],
|
|
102
|
+
window: h.fetch(:window, false)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Pass-through: the controller segment is NOT re-stimulized.
|
|
107
|
+
def self.parse_qualified_string(s)
|
|
108
|
+
if s.include?("->")
|
|
109
|
+
event_part, ctrl_method = s.split("->", 2)
|
|
110
|
+
ctrl, method = ctrl_method.split("#", 2)
|
|
111
|
+
new(
|
|
112
|
+
controller: Controller.new(path: ctrl, name: ctrl),
|
|
113
|
+
method_name: method,
|
|
114
|
+
event: event_part
|
|
115
|
+
)
|
|
116
|
+
else
|
|
117
|
+
ctrl, method = s.split("#", 2)
|
|
118
|
+
new(
|
|
119
|
+
controller: Controller.new(path: ctrl, name: ctrl),
|
|
120
|
+
method_name: method,
|
|
121
|
+
event: nil
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "literal"
|
|
4
|
+
require_relative "combinable"
|
|
5
|
+
|
|
6
|
+
module Vident
|
|
7
|
+
module Stimulus
|
|
8
|
+
# Shared frozen-value base for the Stimulus primitive value classes
|
|
9
|
+
# (Action, Target, Controller, Outlet, Value, Param, ClassMap).
|
|
10
|
+
# Provides `Combinable` (`with`, pattern-matching `deconstruct_keys`)
|
|
11
|
+
# and a default `to_data_hash(items)` that subclasses override when
|
|
12
|
+
# they need non-trivial collection semantics (space-join etc.).
|
|
13
|
+
#
|
|
14
|
+
# Subclasses still override `to_h` / `to_hash` per class — Literal
|
|
15
|
+
# auto-generates a prop-hash `to_h` from the prop DSL that would
|
|
16
|
+
# shadow any default here, so the data-attribute-shape override
|
|
17
|
+
# must live on each concrete class.
|
|
18
|
+
class Base < ::Literal::Data
|
|
19
|
+
include Combinable
|
|
20
|
+
|
|
21
|
+
def self.to_data_hash(items)
|
|
22
|
+
items.to_h(&:to_data_pair)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "literal"
|
|
4
|
+
require_relative "naming"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "controller"
|
|
7
|
+
|
|
8
|
+
module Vident
|
|
9
|
+
module Stimulus
|
|
10
|
+
# `data-<ctrl>-<name>-class` fragment — a named CSS class set readable
|
|
11
|
+
# on the JS side as `this.<name>Class`.
|
|
12
|
+
class ClassMap < Base
|
|
13
|
+
prop :controller, Controller
|
|
14
|
+
prop :name, String
|
|
15
|
+
prop :css, String
|
|
16
|
+
|
|
17
|
+
def self.parse(*args, implied:, component_id: nil)
|
|
18
|
+
case args
|
|
19
|
+
in [Symbol => name_sym, css_input]
|
|
20
|
+
new(
|
|
21
|
+
controller: implied,
|
|
22
|
+
name: name_sym.to_s.dasherize,
|
|
23
|
+
css: normalize_css(css_input)
|
|
24
|
+
)
|
|
25
|
+
in [String => ctrl_path, Symbol => name_sym, css_input]
|
|
26
|
+
new(
|
|
27
|
+
controller: Controller.parse(ctrl_path, implied: implied),
|
|
28
|
+
name: name_sym.to_s.dasherize,
|
|
29
|
+
css: normalize_css(css_input)
|
|
30
|
+
)
|
|
31
|
+
else
|
|
32
|
+
raise ::Vident::ParseError, "ClassMap.parse: invalid arguments #{args.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.normalize_css(input)
|
|
37
|
+
case input
|
|
38
|
+
when String
|
|
39
|
+
input.split(/\s+/).reject(&:empty?).join(" ")
|
|
40
|
+
when Array
|
|
41
|
+
input.map(&:to_s).reject(&:empty?).join(" ")
|
|
42
|
+
else
|
|
43
|
+
raise ::Vident::ParseError, "ClassMap.parse: css must be a String or Array, got #{input.class}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s = css
|
|
48
|
+
|
|
49
|
+
def data_attribute_key = :"#{controller.name}-#{name}-class"
|
|
50
|
+
|
|
51
|
+
def to_data_pair = [data_attribute_key, css]
|
|
52
|
+
|
|
53
|
+
def to_h = {data_attribute_key => css}
|
|
54
|
+
alias_method :to_hash, :to_h
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Stimulus
|
|
5
|
+
class Collection
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :kind, :items
|
|
9
|
+
|
|
10
|
+
def initialize(kind:, items:)
|
|
11
|
+
@kind = kind
|
|
12
|
+
@items = items.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each(&block) = @items.each(&block)
|
|
16
|
+
|
|
17
|
+
def to_a = @items.dup
|
|
18
|
+
|
|
19
|
+
def size = @items.size
|
|
20
|
+
|
|
21
|
+
def length = @items.size
|
|
22
|
+
|
|
23
|
+
def empty? = @items.empty?
|
|
24
|
+
|
|
25
|
+
def any? = @items.any?
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
@kind.value_class.to_data_hash(@items)
|
|
29
|
+
end
|
|
30
|
+
alias_method :to_hash, :to_h
|
|
31
|
+
|
|
32
|
+
def merge(other)
|
|
33
|
+
unless other.is_a?(self.class) && other.kind == @kind
|
|
34
|
+
raise ArgumentError, "Collection#merge: can only merge with same-kind Collection"
|
|
35
|
+
end
|
|
36
|
+
self.class.new(kind: @kind, items: @items + other.items)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident
|
|
4
|
+
module Stimulus
|
|
5
|
+
# Shared `with(**overrides)` combinator for the frozen value classes.
|
|
6
|
+
# Mirrors Ruby's `Data.define#with` convention (which Literal::Data
|
|
7
|
+
# doesn't ship) so callers can decorate a value object without
|
|
8
|
+
# mutating it.
|
|
9
|
+
module Combinable
|
|
10
|
+
# Canonical Ruby Data-object hooks. The value classes override `to_h`
|
|
11
|
+
# to serialise to their data-attribute shape; without this module,
|
|
12
|
+
# `deconstruct_keys` would inherit that override and pattern-matching
|
|
13
|
+
# (`case a; in {event:}`) would silently fail.
|
|
14
|
+
def deconstruct_keys(keys)
|
|
15
|
+
h = self.class.literal_properties.properties_index.each_with_object({}) do |(name, _), acc|
|
|
16
|
+
acc[name.to_sym] = public_send(name)
|
|
17
|
+
end
|
|
18
|
+
keys ? h.slice(*keys) : h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def deconstruct
|
|
22
|
+
deconstruct_keys(nil).values
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with(**overrides)
|
|
26
|
+
self.class.new(**deconstruct_keys(nil).merge(overrides))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "literal"
|
|
4
|
+
require_relative "naming"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Vident
|
|
8
|
+
module Stimulus
|
|
9
|
+
# `data-controller` fragment.
|
|
10
|
+
class Controller < Base
|
|
11
|
+
prop :path, String
|
|
12
|
+
prop :name, String
|
|
13
|
+
prop :alias_name, _Nilable(Symbol), default: nil
|
|
14
|
+
|
|
15
|
+
def self.parse(*args, implied:, as: nil, component_id: nil)
|
|
16
|
+
case args.size
|
|
17
|
+
when 0
|
|
18
|
+
new(path: implied.path, name: implied.name, alias_name: as)
|
|
19
|
+
when 1
|
|
20
|
+
raw = args[0]
|
|
21
|
+
path = raw.to_s
|
|
22
|
+
new(path: path, name: Naming.stimulize_path(path), alias_name: as)
|
|
23
|
+
else
|
|
24
|
+
raise ::Vident::ParseError, "Controller.parse: expected 0 or 1 positional args, got #{args.size}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def identifier = name
|
|
29
|
+
|
|
30
|
+
def to_s = name
|
|
31
|
+
|
|
32
|
+
def to_data_pair = [:controller, name]
|
|
33
|
+
|
|
34
|
+
def to_h = {controller: name}
|
|
35
|
+
alias_method :to_hash, :to_h
|
|
36
|
+
|
|
37
|
+
def self.to_data_hash(controllers)
|
|
38
|
+
return {} if controllers.empty?
|
|
39
|
+
joined = controllers.map(&:name).reject(&:empty?).join(" ")
|
|
40
|
+
return {} if joined.empty?
|
|
41
|
+
{controller: joined}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|