vident 1.0.1 → 1.0.2
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 +11 -0
- data/README.md +4 -1
- data/lib/vident/component_attribute_resolver.rb +27 -8
- data/lib/vident/component_class_lists.rb +3 -0
- data/lib/vident/stimulus_builder.rb +28 -11
- data/lib/vident/stimulus_helper.rb +4 -4
- data/lib/vident/version.rb +1 -1
- data/lib/vident2/caching.rb +93 -0
- data/lib/vident2/component.rb +538 -0
- data/lib/vident2/engine.rb +18 -0
- data/lib/vident2/error.rb +30 -0
- data/lib/vident2/internals/action_builder.rb +101 -0
- data/lib/vident2/internals/attribute_writer.rb +22 -0
- data/lib/vident2/internals/class_list_builder.rb +79 -0
- data/lib/vident2/internals/declaration.rb +17 -0
- data/lib/vident2/internals/declarations.rb +76 -0
- data/lib/vident2/internals/draft.rb +60 -0
- data/lib/vident2/internals/dsl.rb +198 -0
- data/lib/vident2/internals/plan.rb +12 -0
- data/lib/vident2/internals/registry.rb +41 -0
- data/lib/vident2/internals/resolver.rb +306 -0
- data/lib/vident2/internals/target_builder.rb +29 -0
- data/lib/vident2/phlex/html.rb +84 -0
- data/lib/vident2/phlex.rb +9 -0
- data/lib/vident2/stimulus/action.rb +140 -0
- data/lib/vident2/stimulus/class_map.rb +69 -0
- data/lib/vident2/stimulus/collection.rb +42 -0
- data/lib/vident2/stimulus/controller.rb +59 -0
- data/lib/vident2/stimulus/naming.rb +26 -0
- data/lib/vident2/stimulus/null.rb +16 -0
- data/lib/vident2/stimulus/outlet.rb +113 -0
- data/lib/vident2/stimulus/param.rb +62 -0
- data/lib/vident2/stimulus/target.rb +57 -0
- data/lib/vident2/stimulus/value.rb +77 -0
- data/lib/vident2/tailwind.rb +19 -0
- data/lib/vident2/version.rb +5 -0
- data/lib/vident2/view_component/base.rb +124 -0
- data/lib/vident2/view_component.rb +9 -0
- data/lib/vident2.rb +50 -0
- data/skills/vident/SKILL.md +11 -2
- data/skills/vident/api-reference.md +518 -0
- data/skills/vident/examples.md +492 -0
- metadata +35 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registry"
|
|
4
|
+
|
|
5
|
+
module Vident2
|
|
6
|
+
module Internals
|
|
7
|
+
# @api private
|
|
8
|
+
# Pure: `Plan -> Hash{Symbol => String}` of `data-*` fragments.
|
|
9
|
+
# Delegates per-kind combining (space-join, grouped-by-controller,
|
|
10
|
+
# one-per-key) to each value class's `.to_data_hash`.
|
|
11
|
+
module AttributeWriter
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(plan)
|
|
15
|
+
Registry::KINDS.each_with_object({}) do |kind, acc|
|
|
16
|
+
fragment = kind.value_class.to_data_hash(plan.public_send(kind.name))
|
|
17
|
+
acc.merge!(fragment)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Vident2
|
|
6
|
+
module Internals
|
|
7
|
+
# @api private
|
|
8
|
+
# Root-element class-list builder. Implements the 6-tier precedence
|
|
9
|
+
# cascade plus optional TailwindMerger dedup.
|
|
10
|
+
#
|
|
11
|
+
# Tiers (render order, left -> right):
|
|
12
|
+
# 1. component_name — always first.
|
|
13
|
+
# 2-5. Priority cascade (only the highest non-nil wins):
|
|
14
|
+
# root_element_classes (instance override) <
|
|
15
|
+
# root_element_attributes[:classes] <
|
|
16
|
+
# root_element(class:) from template <
|
|
17
|
+
# html_options[:class] from prop
|
|
18
|
+
# 6. classes: prop — ALWAYS appended, even when tier 5 is present.
|
|
19
|
+
#
|
|
20
|
+
# Plus: per-kind StimulusClassMap entries whose name is in
|
|
21
|
+
# `stimulus_class_names` are appended as CSS. Tailwind merge runs last
|
|
22
|
+
# if the merger is passed.
|
|
23
|
+
module ClassListBuilder
|
|
24
|
+
CLASSNAME_SEPARATOR = " "
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def call(
|
|
29
|
+
component_name: nil,
|
|
30
|
+
root_element_classes: nil,
|
|
31
|
+
root_element_attributes_classes: nil,
|
|
32
|
+
root_element_html_class: nil,
|
|
33
|
+
html_options_class: nil,
|
|
34
|
+
classes_prop: nil,
|
|
35
|
+
stimulus_classes: nil,
|
|
36
|
+
stimulus_class_names: nil,
|
|
37
|
+
tailwind_merger: nil
|
|
38
|
+
)
|
|
39
|
+
parts = []
|
|
40
|
+
parts << component_name if component_name
|
|
41
|
+
|
|
42
|
+
# Priority cascade: top-down, first non-nil wins.
|
|
43
|
+
if html_options_class
|
|
44
|
+
parts.concat(Array.wrap(html_options_class))
|
|
45
|
+
elsif root_element_html_class
|
|
46
|
+
parts.concat(Array.wrap(root_element_html_class))
|
|
47
|
+
elsif root_element_attributes_classes
|
|
48
|
+
parts.concat(Array.wrap(root_element_attributes_classes))
|
|
49
|
+
elsif root_element_classes
|
|
50
|
+
parts.concat(Array.wrap(root_element_classes))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `classes:` prop: always appended, even when something in the
|
|
54
|
+
# cascade already contributed.
|
|
55
|
+
parts.concat(Array.wrap(classes_prop)) if classes_prop
|
|
56
|
+
|
|
57
|
+
parts.compact!
|
|
58
|
+
|
|
59
|
+
if stimulus_classes && stimulus_class_names && !stimulus_class_names.empty?
|
|
60
|
+
parts.concat(stimulus_class_css(stimulus_classes, stimulus_class_names))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
flattened = parts.flat_map { |s| s.to_s.split(/\s+/) }.reject(&:empty?)
|
|
64
|
+
deduped = flattened.uniq
|
|
65
|
+
return nil if deduped.empty?
|
|
66
|
+
|
|
67
|
+
joined = deduped.join(CLASSNAME_SEPARATOR)
|
|
68
|
+
tailwind_merger ? tailwind_merger.merge(joined) : joined
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Pick ClassMap entries whose `name` matches any of the requested
|
|
72
|
+
# Symbols/Strings (dasherized to match the ClassMap's canonical form).
|
|
73
|
+
def stimulus_class_css(class_maps, names)
|
|
74
|
+
names_set = names.map { |n| n.to_s.dasherize }.to_set
|
|
75
|
+
class_maps.select { |cm| names_set.include?(cm.name) }.map(&:css)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vident2
|
|
4
|
+
module Internals
|
|
5
|
+
# @api private
|
|
6
|
+
# One unresolved DSL entry. `args` is the raw argument tuple passed
|
|
7
|
+
# to the DSL primitive; the Resolver parses it into a Stimulus value
|
|
8
|
+
# object at instance init. `when_proc` (optional) is a `-> { ... }`
|
|
9
|
+
# filter evaluated in the component binding; `meta` is a free-form
|
|
10
|
+
# Hash for options like `from_prop: true` the parser needs to see.
|
|
11
|
+
Declaration = Data.define(:args, :when_proc, :meta) do
|
|
12
|
+
def self.of(*args, when_proc: nil, **meta)
|
|
13
|
+
new(args: args.freeze, when_proc: when_proc, meta: meta.freeze)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "declaration"
|
|
4
|
+
|
|
5
|
+
module Vident2
|
|
6
|
+
module Internals
|
|
7
|
+
# @api private
|
|
8
|
+
# Frozen per-class aggregate of what `stimulus do ... end` declared.
|
|
9
|
+
# One field per kind (plural Registry name) plus `values_from_props`.
|
|
10
|
+
# Entries stay as raw `Declaration` records — the Resolver parses
|
|
11
|
+
# them into `Stimulus::*` value objects at instance init, not here.
|
|
12
|
+
#
|
|
13
|
+
# Keyed kinds (values, params, class_maps, outlets) use `(key, entry)`
|
|
14
|
+
# pairs to let a later block's same-key entry replace an earlier one.
|
|
15
|
+
# Positional kinds (controllers, actions, targets) are flat arrays;
|
|
16
|
+
# later blocks append.
|
|
17
|
+
Declarations = Data.define(
|
|
18
|
+
:controllers,
|
|
19
|
+
:actions,
|
|
20
|
+
:targets,
|
|
21
|
+
:outlets,
|
|
22
|
+
:values,
|
|
23
|
+
:params,
|
|
24
|
+
:class_maps,
|
|
25
|
+
:values_from_props
|
|
26
|
+
) do
|
|
27
|
+
EMPTY_ARRAY = [].freeze
|
|
28
|
+
|
|
29
|
+
def self.empty = @empty ||= new(
|
|
30
|
+
controllers: EMPTY_ARRAY,
|
|
31
|
+
actions: EMPTY_ARRAY,
|
|
32
|
+
targets: EMPTY_ARRAY,
|
|
33
|
+
outlets: EMPTY_ARRAY,
|
|
34
|
+
values: EMPTY_ARRAY,
|
|
35
|
+
params: EMPTY_ARRAY,
|
|
36
|
+
class_maps: EMPTY_ARRAY,
|
|
37
|
+
values_from_props: EMPTY_ARRAY
|
|
38
|
+
).freeze
|
|
39
|
+
|
|
40
|
+
def any?
|
|
41
|
+
!controllers.empty? || !actions.empty? || !targets.empty? ||
|
|
42
|
+
!outlets.empty? || !values.empty? || !params.empty? ||
|
|
43
|
+
!class_maps.empty? || !values_from_props.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Merge two Declarations, treating `self` as parent and `other` as
|
|
47
|
+
# child. Positional kinds concat (parent first, then child).
|
|
48
|
+
# Keyed kinds last-wins on matching key.
|
|
49
|
+
def merge(other)
|
|
50
|
+
self.class.new(
|
|
51
|
+
controllers: concat_positional(controllers, other.controllers),
|
|
52
|
+
actions: concat_positional(actions, other.actions),
|
|
53
|
+
targets: concat_positional(targets, other.targets),
|
|
54
|
+
outlets: merge_keyed(outlets, other.outlets),
|
|
55
|
+
values: merge_keyed(values, other.values),
|
|
56
|
+
params: merge_keyed(params, other.params),
|
|
57
|
+
class_maps: merge_keyed(class_maps, other.class_maps),
|
|
58
|
+
values_from_props: (values_from_props + other.values_from_props).uniq.freeze
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def concat_positional(a, b) = (a + b).freeze
|
|
65
|
+
|
|
66
|
+
# Keyed entries are `[key, Declaration]` tuples; last write on a
|
|
67
|
+
# given key wins, insertion order otherwise preserved.
|
|
68
|
+
def merge_keyed(a, b)
|
|
69
|
+
merged = {}
|
|
70
|
+
a.each { |(k, d)| merged[k] = d }
|
|
71
|
+
b.each { |(k, d)| merged[k] = d }
|
|
72
|
+
merged.to_a.freeze
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registry"
|
|
4
|
+
require_relative "plan"
|
|
5
|
+
|
|
6
|
+
module Vident2
|
|
7
|
+
module Internals
|
|
8
|
+
# @api private
|
|
9
|
+
# Per-instance mutable working copy. Seven Arrays, one per Registry
|
|
10
|
+
# kind. `add_<kind>(value_or_values)` mutators are the single sanctioned
|
|
11
|
+
# seam for cross-instance mutation (outlet-host pattern) and for
|
|
12
|
+
# `add_stimulus_*` calls from `after_component_initialize`.
|
|
13
|
+
#
|
|
14
|
+
# After `seal!` the Draft is closed — any further `add_*` raises
|
|
15
|
+
# `Vident2::StateError`. The sealed Plan is a frozen Data.define snapshot.
|
|
16
|
+
class Draft
|
|
17
|
+
def initialize(**collections)
|
|
18
|
+
@collections = Registry.names.to_h { |name| [name, []] }
|
|
19
|
+
collections.each { |k, v| @collections[k] = v.dup if @collections.key?(k) }
|
|
20
|
+
@sealed = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Registry.each do |kind|
|
|
24
|
+
# reader
|
|
25
|
+
define_method(kind.name) { @collections[kind.name] }
|
|
26
|
+
|
|
27
|
+
# mutator: one call = one logical add. Array input concats all
|
|
28
|
+
# elements as pre-parsed values; a single non-Array value appends
|
|
29
|
+
# as one entry.
|
|
30
|
+
define_method(:"add_#{kind.name}") do |value_or_values|
|
|
31
|
+
raise_if_sealed!
|
|
32
|
+
Array(value_or_values).each { |v| @collections[kind.name] << v }
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sealed? = @sealed
|
|
38
|
+
|
|
39
|
+
# Freeze the working copy and snapshot as a frozen Plan. Idempotent:
|
|
40
|
+
# subsequent calls return the memoised Plan.
|
|
41
|
+
def seal!
|
|
42
|
+
return @plan if @sealed
|
|
43
|
+
@sealed = true
|
|
44
|
+
@collections.each_value(&:freeze)
|
|
45
|
+
@collections.freeze
|
|
46
|
+
@plan = Plan.new(**@collections)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def plan = seal!
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def raise_if_sealed!
|
|
54
|
+
return unless @sealed
|
|
55
|
+
raise ::Vident2::StateError,
|
|
56
|
+
"cannot modify stimulus attributes after rendering has begun"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "declaration"
|
|
4
|
+
require_relative "declarations"
|
|
5
|
+
require_relative "action_builder"
|
|
6
|
+
require_relative "target_builder"
|
|
7
|
+
|
|
8
|
+
module Vident2
|
|
9
|
+
module Internals
|
|
10
|
+
# @api private
|
|
11
|
+
# Block receiver for `stimulus do ... end`. Records each primitive
|
|
12
|
+
# call as one or more `Declaration` raw entries; `finalize` folds
|
|
13
|
+
# them into a frozen `Declarations` aggregate.
|
|
14
|
+
#
|
|
15
|
+
# Parsing into `Stimulus::*` value objects is deferred to the
|
|
16
|
+
# Resolver — this class stores only raw argument tuples.
|
|
17
|
+
class DSL
|
|
18
|
+
attr_reader :caller_location
|
|
19
|
+
|
|
20
|
+
def initialize(caller_location: nil)
|
|
21
|
+
@caller_location = caller_location
|
|
22
|
+
@controllers = []
|
|
23
|
+
@actions = []
|
|
24
|
+
@targets = []
|
|
25
|
+
@outlets = []
|
|
26
|
+
@values = []
|
|
27
|
+
@params = []
|
|
28
|
+
@class_maps = []
|
|
29
|
+
@values_from_props = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ---- plural (kwargs) forms --------------------------------------
|
|
33
|
+
|
|
34
|
+
# Each arg becomes one controller entry. An Array arg is splatted
|
|
35
|
+
# into positional args for a single controller (e.g. a tuple
|
|
36
|
+
# `[path, {as: :alias}]`); anything else is treated as a path.
|
|
37
|
+
def controllers(*args)
|
|
38
|
+
args.each do |arg|
|
|
39
|
+
case arg
|
|
40
|
+
in Array
|
|
41
|
+
controller(*arg)
|
|
42
|
+
else
|
|
43
|
+
controller(arg)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Array in the plural form splats into the singular parser (matching
|
|
50
|
+
# V1's plural→singular forwarding) so `actions [:click, :handle]`
|
|
51
|
+
# records a single Action entry with event+method rather than two
|
|
52
|
+
# separate Actions.
|
|
53
|
+
def actions(*names)
|
|
54
|
+
names.each do |name|
|
|
55
|
+
case name
|
|
56
|
+
in Array
|
|
57
|
+
action(*name)
|
|
58
|
+
else
|
|
59
|
+
action(name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def targets(*names)
|
|
66
|
+
names.each do |name|
|
|
67
|
+
case name
|
|
68
|
+
in Array
|
|
69
|
+
target(*name)
|
|
70
|
+
else
|
|
71
|
+
target(name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def values(**hash)
|
|
78
|
+
hash.each { |k, v| record_keyed(@values, k, v) }
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def params(**hash)
|
|
83
|
+
hash.each { |k, v| record_keyed(@params, k, v) }
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def classes(**hash)
|
|
88
|
+
hash.each { |k, v| record_keyed(@class_maps, k, v) }
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Outlets accept a positional Hash (for keys like `"admin--users"`
|
|
93
|
+
# that can't be a Ruby kwarg) plus kwargs. Order: positional first,
|
|
94
|
+
# kwargs after — last-write wins on duplicates per the keyed merge
|
|
95
|
+
# rule.
|
|
96
|
+
def outlets(positional = nil, **hash)
|
|
97
|
+
if positional.is_a?(Hash)
|
|
98
|
+
positional.each { |k, v| record_keyed(@outlets, k, v) }
|
|
99
|
+
elsif !positional.nil?
|
|
100
|
+
raise ArgumentError, "outlets: positional arg must be a Hash, got #{positional.class}"
|
|
101
|
+
end
|
|
102
|
+
hash.each { |k, v| record_keyed(@outlets, k, v) }
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def values_from_props(*names)
|
|
107
|
+
@values_from_props.concat(names.map(&:to_sym))
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ---- singular forms --------------------------------------------
|
|
112
|
+
|
|
113
|
+
# Optional `as: :alias` captured in meta for the Resolver.
|
|
114
|
+
def controller(*args, **meta)
|
|
115
|
+
@controllers << Declaration.of(*args, **meta)
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns an `ActionBuilder` so users can fluent-chain
|
|
120
|
+
# `.on(:event).modifier(:prevent).keyboard("ctrl+s").window.when { ... }`.
|
|
121
|
+
# If no chain methods are called, the raw args pass through unchanged.
|
|
122
|
+
def action(*args)
|
|
123
|
+
builder = ActionBuilder.new(*args)
|
|
124
|
+
@actions << builder
|
|
125
|
+
builder
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns a `TargetBuilder` so users can chain `.when { ... }` for
|
|
129
|
+
# conditional inclusion.
|
|
130
|
+
def target(*args)
|
|
131
|
+
builder = TargetBuilder.new(*args)
|
|
132
|
+
@targets << builder
|
|
133
|
+
builder
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `value(:url, "x")`, `value(:url, -> { ... })`,
|
|
137
|
+
# `value(:count, static: 0)`, `value(:clicked_count, from_prop: true)`.
|
|
138
|
+
def value(name, *args, **meta)
|
|
139
|
+
entry = [name, Declaration.of(*args, **meta)]
|
|
140
|
+
replace_or_append(@values, entry)
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def param(name, *args, **meta)
|
|
145
|
+
entry = [name, Declaration.of(*args, **meta)]
|
|
146
|
+
replace_or_append(@params, entry)
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def outlet(name, *args, **meta)
|
|
151
|
+
entry = [name, Declaration.of(*args, **meta)]
|
|
152
|
+
replace_or_append(@outlets, entry)
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def class_map(name, *args, **meta)
|
|
157
|
+
entry = [name, Declaration.of(*args, **meta)]
|
|
158
|
+
replace_or_append(@class_maps, entry)
|
|
159
|
+
self
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# ---- folding ----------------------------------------------------
|
|
163
|
+
|
|
164
|
+
# Returns a frozen Declarations snapshot of what this block
|
|
165
|
+
# received. Called once the block finishes executing.
|
|
166
|
+
def to_declarations
|
|
167
|
+
Declarations.new(
|
|
168
|
+
controllers: @controllers.dup.freeze,
|
|
169
|
+
actions: @actions.map(&:to_declaration).freeze,
|
|
170
|
+
targets: @targets.map(&:to_declaration).freeze,
|
|
171
|
+
outlets: @outlets.dup.freeze,
|
|
172
|
+
values: @values.dup.freeze,
|
|
173
|
+
params: @params.dup.freeze,
|
|
174
|
+
class_maps: @class_maps.dup.freeze,
|
|
175
|
+
values_from_props: @values_from_props.dup.freeze
|
|
176
|
+
).freeze
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def record_keyed(bucket, key, value)
|
|
182
|
+
entry = [key, Declaration.of(value)]
|
|
183
|
+
replace_or_append(bucket, entry)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Last-write wins on matching key, insertion order otherwise.
|
|
187
|
+
def replace_or_append(bucket, entry)
|
|
188
|
+
key = entry.first
|
|
189
|
+
idx = bucket.index { |(k, _)| k == key }
|
|
190
|
+
if idx
|
|
191
|
+
bucket[idx] = entry
|
|
192
|
+
else
|
|
193
|
+
bucket << entry
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registry"
|
|
4
|
+
|
|
5
|
+
module Vident2
|
|
6
|
+
module Internals
|
|
7
|
+
# @api private
|
|
8
|
+
# Frozen snapshot produced by `Draft#seal!`. One field per Registry
|
|
9
|
+
# kind, each an Array<Stimulus::*>.
|
|
10
|
+
Plan = Data.define(*Registry.names)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../stimulus/controller"
|
|
4
|
+
require_relative "../stimulus/action"
|
|
5
|
+
require_relative "../stimulus/target"
|
|
6
|
+
require_relative "../stimulus/outlet"
|
|
7
|
+
require_relative "../stimulus/value"
|
|
8
|
+
require_relative "../stimulus/param"
|
|
9
|
+
require_relative "../stimulus/class_map"
|
|
10
|
+
|
|
11
|
+
module Vident2
|
|
12
|
+
# @api private — consumed by the DSL, Resolver, Draft, Plan,
|
|
13
|
+
# AttributeWriter, and Capabilities::StimulusMutation. Not a public
|
|
14
|
+
# extension surface; extensions monkeypatch at their own risk.
|
|
15
|
+
module Internals
|
|
16
|
+
module Registry
|
|
17
|
+
# Per-kind metadata. `name` is the canonical internal key;
|
|
18
|
+
# `plural_name` / `singular_name` drive DSL method names (e.g.
|
|
19
|
+
# `stimulus_classes` / `stimulus_class` for `class_maps`); `keyed`
|
|
20
|
+
# distinguishes hash-shaped kinds (values, params, class_maps,
|
|
21
|
+
# outlets) from positional ones (controllers, actions, targets).
|
|
22
|
+
Kind = Data.define(:name, :plural_name, :singular_name, :value_class, :keyed)
|
|
23
|
+
|
|
24
|
+
KINDS = [
|
|
25
|
+
Kind.new(:controllers, :controllers, :controller, Vident2::Stimulus::Controller, false),
|
|
26
|
+
Kind.new(:actions, :actions, :action, Vident2::Stimulus::Action, false),
|
|
27
|
+
Kind.new(:targets, :targets, :target, Vident2::Stimulus::Target, false),
|
|
28
|
+
Kind.new(:outlets, :outlets, :outlet, Vident2::Stimulus::Outlet, true),
|
|
29
|
+
Kind.new(:values, :values, :value, Vident2::Stimulus::Value, true),
|
|
30
|
+
Kind.new(:params, :params, :param, Vident2::Stimulus::Param, true),
|
|
31
|
+
Kind.new(:class_maps, :classes, :class, Vident2::Stimulus::ClassMap, true)
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
BY_NAME = KINDS.to_h { |k| [k.name, k] }.freeze
|
|
35
|
+
|
|
36
|
+
def self.fetch(name) = BY_NAME.fetch(name)
|
|
37
|
+
def self.each(&block) = KINDS.each(&block)
|
|
38
|
+
def self.names = BY_NAME.keys
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|