Module: Wabi::ClassMerge

Defined in:
lib/wabi/class_merge.rb

Overview

Minimal Tailwind class deduplication. Later tokens win for the same group key.

The group key is variant-aware: any variants stacked in front of a utility (e.g. “focus-visible:”, “data-:”, “md:dark:”) become part of the key, so variant-scoped utilities never collide with their plain counterparts. Inside a given variant scope, the key is the utility’s first hyphen-separated segment (“h-4” → “h”, “bg-red-500” → “bg”).

NOTE: Still intentionally minimal for v0.1 — utilities that share a prefix but represent different CSS properties (e.g. ‘ring-2` width vs `ring-ring` color, or `border` width vs `border-primary` color) collide and the later one wins. Full Tailwind conflict resolution (à la tailwind-merge) is post-v0.1.

Constant Summary collapse

ATOM_UTILITIES =

Single-word “atom” utilities that name a CSS property whose compound siblings (e.g. ‘flex` vs `flex-col`, `border` vs `border-input`) target an entirely different property and must NOT share a dedup bucket. Without this distinction, `flex flex-col` collapses to just `flex-col` – the display:flex rule is lost and the children don’t lay out as a flex row.

%w[
  flex grid block inline hidden visible
  border ring rounded outline
  transition truncate
  absolute relative fixed static sticky
].to_set.freeze
AXIS_FAMILIES =

Directional / axial suffix segments that distinguish utilities in the same family (e.g. ‘-translate-x-1/2` vs `-translate-y-1/2` are different axes, `border-l` vs `border-r` are different sides). When the first segment after the family root matches one of these, keep the family root PLUS the direction segment as the group key.

%w[translate -translate scale skew rotate space border].to_set.freeze
AXIS_SUFFIXES =
%w[x y z t b l r s e].to_set.freeze
WIDTH_COLOR_FAMILIES =

Families whose first non-family segment can be EITHER a width/size value OR a color name. Tailwind treats these as different CSS properties so they must dedup independently. Without this, ‘border-2 border-input` collapses to just `border-input` (loses the width), or `ring-2 ring-ring ring-offset-2` collapses to one of them (the focus ring vanishes).

%w[border ring text outline divide].to_set.freeze
SIZE_TOKENS =

Tail tokens that unambiguously identify a size/length utility. Anything that doesn’t match these AND doesn’t look numeric is assumed to be a color (or other named theme token).

%w[
  xs sm base md lg xl 2xl 3xl 4xl 5xl 6xl 7xl 8xl 9xl
  auto full screen min max fit none px
].to_set.freeze

Class Method Summary collapse

Class Method Details

.call(*inputs) ⇒ Object



22
23
24
25
26
27
28
29
# File 'lib/wabi/class_merge.rb', line 22

def call(*inputs)
  tokens = inputs.compact.flat_map { |s| s.to_s.split(/\s+/) }.reject(&:empty?)
  seen = {}
  tokens.each do |token|
    seen[group_key(token)] = token
  end
  seen.values.join(" ")
end

.group_key(token) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/wabi/class_merge.rb', line 31

def group_key(token)
  idx = last_colon_outside_brackets(token)
  if idx
    prefix  = token[0..idx]
    utility = token[(idx + 1)..]
  else
    prefix  = ""
    utility = token
  end
  "#{prefix}#{utility_group(utility)}"
end

.last_colon_outside_brackets(token) ⇒ Object

The last ‘:` that lives outside any `[…]` block. Arbitrary-value variants like `data-:` may contain `:` inside the brackets, which must not be treated as the variant→utility separator.



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/wabi/class_merge.rb', line 113

def last_colon_outside_brackets(token)
  depth = 0
  idx   = nil
  token.each_char.with_index do |c, i|
    case c
    when "[" then depth += 1
    when "]" then depth -= 1
    when ":" then idx = i if depth.zero?
    end
  end
  idx
end

.size_or_numeric?(tail) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
# File 'lib/wabi/class_merge.rb', line 104

def size_or_numeric?(tail)
  return true if SIZE_TOKENS.include?(tail)
  return true if tail.match?(/\A-?\d+(\/\d+)?\z/)
  false
end

.utility_group(utility) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/wabi/class_merge.rb', line 78

def utility_group(utility)
  return "atom:#{utility}" if ATOM_UTILITIES.include?(utility)

  segments = utility.split("-").reject(&:empty?)
  head = utility.start_with?("-") ? "-#{segments.first}" : segments.first
  tail = utility.start_with?("-") ? segments[1] : segments[1]

  # `ring-offset-*` is its own family: `ring-offset-2` (width) and
  # `ring-offset-input` (color) must NOT collide with bare `ring-*`.
  if head == "ring" && tail == "offset"
    sub_tail = utility.start_with?("-") ? segments[2] : segments[2]
    return "ring-offset:size"  if sub_tail && size_or_numeric?(sub_tail)
    return "ring-offset:color" if sub_tail
    return "ring-offset"
  end

  return "#{head}-#{tail}" if AXIS_FAMILIES.include?(head) && AXIS_SUFFIXES.include?(tail)

  if WIDTH_COLOR_FAMILIES.include?(head) && tail
    return "#{head}:size"  if size_or_numeric?(tail)
    return "#{head}:color"
  end

  head
end