Module: LocoMotion::Lint::ComponentMap
- Defined in:
- lib/loco_motion/lint/component_map.rb
Overview
Derives the "DaisyUI class -> LocoMotion helper" map that the component usage linter enforces.
The map is DERIVED, never hand-maintained. Every entry comes from
COMPONENTS paired with the component's own
add_css(:component, "...") declaration, so a component and the rule
covering it cannot drift apart: adding a component adds its coverage in
the same commit, and ComponentMap.unmapped makes a component that declares no class
a visible, testable fact rather than a silent gap.
This replaces a hand-written list that had reached 25 of 70 components.
Constant Summary collapse
- LAYOUT_UTILITIES =
A component root is sometimes a plain Tailwind utility — Countdown declares only "flex", putting its DaisyUI class on child spans. Claiming those would flag every .flex in every view, so they are refused and the component is reported as unmapped instead. A gap is recoverable; a false positive teaches people to ignore the linter.
%w[ flex grid block inline inline-block hidden contents flow-root relative absolute fixed sticky static isolate table ].freeze
Class Method Summary collapse
- .add_component(map, class_name, meta, names_only:) ⇒ Object
-
.add_parts(map, decls, base, helper) ⇒ Object
Anything extending the base ("card-body", "drawer-side") belongs to this component.
- .base_for(decls, names) ⇒ Object
-
.build ⇒ Object
{ "card" => "daisy_card", "card-body" => "daisy_card (body part)" }.
-
.component_class?(token) ⇒ Boolean
Tailwind variants (
sm:,where:) and utilities are not component classes — a view is free to use them, so they must never be flagged. - .component_tokens(decls) ⇒ Object
- .declarations(source) ⇒ Object
- .gem_root ⇒ Object
-
.helper_name(class_name, meta) ⇒ Object
Helper names follow "#framework_#name" (see LocoMotion.define_helpers), and a component may register several — the first is the canonical suggestion.
-
.source_path(class_name) ⇒ Object
"Daisy::DataDisplay::CardComponent" -> app/components/daisy/data_display/card_component.rb.
- .underscore(string) ⇒ Object
-
.unmapped ⇒ Object
Components that declare no usable base class — they build it dynamically, or style the root with utilities.
Class Method Details
.add_component(map, class_name, meta, names_only:) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/loco_motion/lint/component_map.rb', line 95 def add_component(map, class_name, , names_only:) path = source_path(class_name) return unless File.exist?(path) names = Array([:names]).map(&:to_s) decls = declarations(File.read(path)) return if names_only && (names & component_tokens(decls)).empty? base = base_for(decls, names) return unless base helper = helper_name(class_name, ) map[base] ||= helper add_parts(map, decls, base, helper) end |
.add_parts(map, decls, base, helper) ⇒ Object
Anything extending the base ("card-body", "drawer-side") belongs to this component. A part reusing a shared class like "label" belongs to no single component and is left alone.
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/loco_motion/lint/component_map.rb', line 114 def add_parts(map, decls, base, helper) decls.each do |part, tokens| tokens.each do |token| next if token == base || !token.start_with?("#{base}-") label = part == "component" ? "nested" : part map[token] ||= "#{helper} (#{label} part)" end end end |
.base_for(decls, names) ⇒ Object
73 74 75 76 77 78 79 80 |
# File 'lib/loco_motion/lint/component_map.rb', line 73 def base_for(decls, names) tokens = component_tokens(decls) # A file may declare several component roots — a nested sub-component # (drawer-side, carousel-item) often appears BEFORE the real root — so # prefer the token that matches a registered helper name. tokens.find { |token| names.include?(token) } || tokens.reject { |token| LAYOUT_UTILITIES.include?(token) }.first end |
.build ⇒ Object
{ "card" => "daisy_card", "card-body" => "daisy_card (body part)" }
83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/loco_motion/lint/component_map.rb', line 83 def build map = {} # Two passes so an exact name match always wins the class: Accordion and # Collapse both style "collapse", and it should read as daisy_collapse. [true, false].each do |names_only| LocoMotion::COMPONENTS.each do |class_name, | add_component(map, class_name, , names_only: names_only) end end map end |
.component_class?(token) ⇒ Boolean
Tailwind variants (sm:, where:) and utilities are not component
classes — a view is free to use them, so they must never be flagged.
29 30 31 |
# File 'lib/loco_motion/lint/component_map.rb', line 29 def component_class?(token) !token.include?(":") && token.match?(/\A[a-z][a-z0-9-]*\z/) end |
.component_tokens(decls) ⇒ Object
69 70 71 |
# File 'lib/loco_motion/lint/component_map.rb', line 69 def component_tokens(decls) decls.select { |part, _| part == "component" }.flat_map(&:last) end |
.declarations(source) ⇒ Object
54 55 56 57 |
# File 'lib/loco_motion/lint/component_map.rb', line 54 def declarations(source) source.scan(/add_css\(\s*:([a-z_]+)\s*,\s*"([^"]+)"/) .map { |part, css| [part, css.split(/\s+/).select { |t| component_class?(t) }] } end |
.gem_root ⇒ Object
37 38 39 |
# File 'lib/loco_motion/lint/component_map.rb', line 37 def gem_root File.("../../..", __dir__) end |
.helper_name(class_name, meta) ⇒ Object
Helper names follow "#framework_#name" (see LocoMotion.define_helpers), and a component may register several — the first is the canonical suggestion.
49 50 51 52 |
# File 'lib/loco_motion/lint/component_map.rb', line 49 def helper_name(class_name, ) framework = underscore(class_name.split("::").first) "#{framework}_#{Array([:names]).first}" end |
.source_path(class_name) ⇒ Object
"Daisy::DataDisplay::CardComponent" -> app/components/daisy/data_display/card_component.rb
42 43 44 45 |
# File 'lib/loco_motion/lint/component_map.rb', line 42 def source_path(class_name) segments = class_name.split("::").map { |segment| underscore(segment) } "#{File.join(gem_root, 'app', 'components', *segments)}.rb" end |
.underscore(string) ⇒ Object
33 34 35 |
# File 'lib/loco_motion/lint/component_map.rb', line 33 def underscore(string) string.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase end |
.unmapped ⇒ Object
Components that declare no usable base class — they build it dynamically, or style the root with utilities. Surfaced so the spec can pin the list: a NEW component landing here fails the suite rather than quietly going unenforced.
129 130 131 132 133 134 135 136 |
# File 'lib/loco_motion/lint/component_map.rb', line 129 def unmapped LocoMotion::COMPONENTS.reject do |class_name, | path = source_path(class_name) next false unless File.exist?(path) !base_for(declarations(File.read(path)), Array([:names]).map(&:to_s)).nil? end.keys end |