Skip to content

Commit ae9bc0e

Browse files
authored
Fixes duplication of classes with modifiers
1 parent 800bd67 commit ae9bc0e

File tree

3 files changed

+59
-47
lines changed

3 files changed

+59
-47
lines changed

Sources/WebUI/Styles/Responsive/ResponsiveModifier.swift

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,22 +216,12 @@ public class ResponsiveBuilder {
216216
guard let breakpoint = currentBreakpoint else { return }
217217

218218
// Apply the breakpoint prefix to all pending classes
219-
let responsiveClasses = pendingClasses.map {
220-
// Handle duplication for flex-*, justify-*, items-*
221-
if $0.starts(with: "flex-") || $0.starts(with: "justify-") || $0.starts(with: "items-")
222-
|| $0.starts(with: "grid-")
223-
{
224-
return "\(breakpoint.rawValue)\($0)"
225-
} else if $0 == "flex" || $0 == "grid" {
226-
return "\(breakpoint.rawValue)\($0)"
227-
} else {
228-
return "\(breakpoint.rawValue)\($0)"
229-
}
230-
}
219+
let responsiveClasses = pendingClasses.map { "\(breakpoint.rawValue)\($0)" }
231220

232221
// Create a concrete wrapper that preserves Element conformance
233222
let wrapped = AnyElement(self.element)
234-
let styledModifier = StyleModifier(content: wrapped, classes: responsiveClasses)
223+
224+
let styledModifier = StyleModifierWithDeduplication(content: wrapped, classes: responsiveClasses)
235225
self.element = ElementWrapper(styledModifier)
236226

237227
// Clear pending classes for the next breakpoint
@@ -245,6 +235,61 @@ public class ResponsiveBuilder {
245235
}
246236
}
247237

238+
/// A smart style modifier that deduplicates redundant classes
239+
struct StyleModifierWithDeduplication<T: HTML>: HTML {
240+
private let content: T
241+
private let classes: [String]
242+
243+
init(content: T, classes: [String]) {
244+
self.content = content
245+
self.classes = classes
246+
}
247+
248+
/// Removes redundant modifier classes when the same property exists in base
249+
private func filterRedundantModifierClasses(_ modifierClasses: [String]) -> [String] {
250+
// Get base classes by rendering the content first
251+
let baseContent = content.render()
252+
let baseClasses = extractClassesFromHTML(baseContent)
253+
254+
return modifierClasses.filter { modifierClass in
255+
guard let colonIndex = modifierClass.firstIndex(of: ":") else {
256+
return true // Not a modifier class, keep it
257+
}
258+
259+
let baseClass = String(modifierClass[modifierClass.index(after: colonIndex)...])
260+
261+
// Remove redundant transition property declarations
262+
if baseClass.hasPrefix("transition-") &&
263+
!baseClass.hasPrefix("transition-duration") &&
264+
!baseClass.hasPrefix("transition-delay") &&
265+
!baseClass.hasPrefix("transition-timing") {
266+
return !baseClasses.contains(baseClass)
267+
}
268+
269+
// Keep all other modifier classes
270+
return true
271+
}
272+
}
273+
274+
/// Extracts classes from HTML class attribute
275+
private func extractClassesFromHTML(_ html: String) -> Set<String> {
276+
let pattern = #"class="([^"]*)"#
277+
guard let regex = try? NSRegularExpression(pattern: pattern),
278+
let match = regex.firstMatch(in: html, range: NSRange(html.startIndex..., in: html)),
279+
let range = Range(match.range(at: 1), in: html) else {
280+
return Set()
281+
}
282+
283+
let classString = String(html[range])
284+
return Set(classString.split(separator: " ").map(String.init))
285+
}
286+
287+
var body: some HTML {
288+
let filteredClasses = filterRedundantModifierClasses(classes)
289+
return content.addingClasses(filteredClasses)
290+
}
291+
}
292+
248293
// Font styling methods
249294
extension ResponsiveBuilder {
250295
@discardableResult

Sources/WebUI/Styles/Responsive/ResponsiveStyleModifiers.swift

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -178,37 +178,3 @@ public func xl(@ResponsiveStyleBuilder content: () -> ResponsiveModification) ->
178178
public func xl2(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification {
179179
BreakpointModification(breakpoint: .xl2, styleModification: content())
180180
}
181-
182-
// MARK: - Style Modification Functions
183-
184-
// Font styling is now implemented in FontStyleOperation.swift
185-
186-
// Background styling is now implemented in BackgroundStyleOperation.swift
187-
188-
// Padding styling is now implemented in PaddingStyleOperation.swift
189-
190-
// Margins styling is now implemented in MarginsStyleOperation.swift
191-
192-
// Border styling is now implemented in BorderStyleOperation.swift
193-
194-
// Opacity styling is now implemented in OpacityStyleOperation.swift
195-
196-
// Size styling is now implemented in SizingStyleOperation.swift
197-
198-
// Frame styling is now implemented in SizingStyleOperation.swift
199-
200-
// Flex styling is implemented in ResponsiveBuilder.swift
201-
202-
// Grid styling is implemented in ResponsiveBuilder.swift
203-
204-
// Position styling is now implemented in PositionStyleOperation.swift
205-
206-
// Overflow styling is now implemented in OverflowStyleOperation.swift
207-
208-
// Hidden styling is implemented in ResponsiveBuilder.swift
209-
210-
// Border radius styling is now implemented in BorderRadiusStyleOperation.swift
211-
212-
// Interactive state modifiers like hover, focus, etc. are implemented in InteractionModifiers.swift
213-
214-
// ARIA state modifiers are implemented in InteractionModifiers.swift

Tests/WebUITests/Styles/StyleModifiersTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ import Testing
391391
let rendered = element.render()
392392
#expect(rendered.contains("transition-transform"))
393393
#expect(rendered.contains("duration-300"))
394+
#expect(!rendered.contains("motion-reduce:transition-transform"))
394395
#expect(rendered.contains("motion-reduce:duration-0"))
395396
}
396397

0 commit comments

Comments
 (0)