Skip to content

Commit fb100fe

Browse files
committed
Add comprehensive conditional styling feature
This commit introduces a powerful conditional styling system for the Rainbow library that allows developers to apply colors and styles based on runtime conditions. Features: - Basic conditional APIs: colorIf, styleIf, backgroundColorIf, applyIf - Advanced conditional builder with fluent interface - Support for closure-based predicates - Complete backward compatibility - Comprehensive test coverage (32 new tests) - Optimized ANSI code generation Usage examples: - Basic: "Error".colorIf(isError, .red).styleIf(isError, .bold) - Advanced: "Status".conditionalStyled.when(isActive).green.bold.build() The implementation includes: - String+ConditionalStyling.swift: Core conditional styling extensions - ConditionalStyleBuilder.swift: Advanced builder pattern implementation - ConditionalStylingTests.swift: Comprehensive test suite - Updated IMPROVEMENT_ROADMAP.md with completion status All 128 tests pass with optimized ANSI escape sequence generation.
1 parent f292c3c commit fb100fe

File tree

4 files changed

+802
-27
lines changed

4 files changed

+802
-27
lines changed

IMPROVEMENT_ROADMAP.md

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,21 @@ It's better to keep the readable, maintainable version rather than introduce unn
118118

119119
### 2.1 Insufficient Test Coverage
120120

121+
**Status**: 🟡 **PARTIALLY RESOLVED**
122+
123+
**Completed**:
124+
- ✅ Performance benchmarks - Comprehensive performance testing framework added
125+
- ✅ Boundary condition tests - EdgeCaseTests.swift added with extensive edge case coverage
126+
121127
**Missing Test Scenarios**:
122128
- Windows platform-specific tests
123-
- Boundary condition tests (very long strings, special characters)
124-
- Performance benchmarks
125129
- Thread safety tests
126130
- `strikethrough` style tests (implemented but not tested)
127131

128132
**Action Items**:
129133
1. Add Windows-specific test suite
130-
2. Create performance benchmark suite
131-
3. Add stress tests for edge cases
132-
4. Implement concurrent access tests
134+
2. Add stress tests for concurrent access
135+
3. Add tests for `strikethrough` style
133136

134137
### 2.2 Feature Enhancements
135138

@@ -147,27 +150,67 @@ print("Error: File not found".errorStyle())
147150
```
148151

149152
#### 2.2.2 HSL Color Support
153+
154+
**Status**: ✅ **COMPLETED**
155+
156+
HSL color support has been implemented with:
157+
- `HSLColorConverter` class for HSL to RGB conversion
158+
- String extensions: `hsl(_ h: Int, _ s: Int, _ l: Int)` and `onHsl(_ h: Int, _ s: Int, _ l: Int)`
159+
- Comprehensive test coverage in `HSLColorTests.swift`
160+
- Updated playground demonstrations
161+
162+
**Usage**:
150163
```swift
151-
extension String {
152-
func hsl(_ h: Int, _ s: Int, _ l: Int) -> String {
153-
// Convert HSL to RGB, then apply color
154-
}
155-
}
164+
"Hello".hsl(120, 100, 50) // Green text
165+
"World".onHsl(240, 100, 50) // Blue background
156166
```
157167

158168
#### 2.2.3 Conditional Styling
169+
170+
**Status**: ✅ **COMPLETED**
171+
172+
Conditional styling has been implemented with comprehensive APIs for applying styles based on conditions:
173+
174+
**Basic Conditional APIs**:
175+
- `colorIf(_:_:)` - Apply colors conditionally
176+
- `backgroundColorIf(_:_:)` - Apply background colors conditionally
177+
- `styleIf(_:_:)` - Apply styles conditionally
178+
- `stylesIf(_:_:)` - Apply multiple styles conditionally
179+
- `applyIf(_:transform:)` - Apply custom transformations conditionally
180+
181+
**Advanced Conditional Builder**:
182+
- `ConditionalStyleBuilder` - Fluent interface for complex conditional styling
183+
- `ConditionalStyleStep` - Chain multiple conditional styles
184+
- Convenience properties: `.red`, `.bold`, `.underline`, etc.
185+
186+
**Usage Examples**:
159187
```swift
160-
extension String {
161-
func colorIf(_ condition: Bool, _ color: NamedColor) -> String {
162-
return condition ? self.applying(color) : self
163-
}
164-
165-
func styleIf(_ condition: Bool, _ style: Style) -> String {
166-
return condition ? self.applying(style) : self
167-
}
168-
}
188+
// Basic conditional styling
189+
let errorMessage = "Error occurred"
190+
.colorIf(isError, .red)
191+
.styleIf(isError, .bold)
192+
193+
// Advanced conditional styling with builder pattern
194+
let styledText = "Status: Active"
195+
.conditionalStyle()
196+
.when(isActive).green.bold
197+
.when(isWarning).yellow
198+
.when(isError).red.underline
199+
.build()
200+
201+
// Conditional styling with closures
202+
let result = message
203+
.colorWhen({ level == .error }, .red)
204+
.styleWhen({ level == .error }, .bold)
169205
```
170206

207+
**Features**:
208+
- Complete test coverage (32 tests)
209+
- Optimized ANSI code generation
210+
- Fluent API design similar to SwiftUI
211+
- Backward compatibility maintained
212+
- Support for all color types and styles
213+
171214
#### 2.2.4 Gradient Text
172215
```swift
173216
extension String {
@@ -284,9 +327,9 @@ Sources/Rainbow/
284327

285328
### Phase 2: Feature Enhancement (2-3 weeks)
286329
1. Implement style presets
287-
2. Add HSL color support
330+
2. **COMPLETED** - Add HSL color support
288331
3. Improve API naming (maintain backward compatibility)
289-
4. Add conditional styling
332+
4. **COMPLETED** - Add conditional styling
290333
5. Implement missing styles (strikethrough in extensions)
291334

292335
### Phase 3: Long-term Improvements (1 month)
@@ -336,9 +379,9 @@ public extension String {
336379

337380
1. **Performance**: ✅ **ACHIEVED** - 85% improvement in builder pattern, 70% in batch operations (exceeds 50% target)
338381
2. **Stability**: ✅ **MAINTAINED** - No critical issues found, existing stability preserved
339-
3. **Test Coverage**: 🟡 **IN PROGRESS** - Performance tests added, need general coverage improvement
340-
4. **API Satisfaction**: ✅ **DELIVERED** - New APIs maintain backward compatibility
341-
5. **Documentation**: ✅ **COMPLETED** - Performance guide and examples added
382+
3. **Test Coverage**: **IMPROVED** - Comprehensive test suite with 128 tests (32 new conditional styling tests)
383+
4. **API Satisfaction**: ✅ **DELIVERED** - New APIs maintain backward compatibility with fluent interfaces
384+
5. **Documentation**: ✅ **COMPLETED** - Performance guide, HSL support, and conditional styling examples added
342385

343386
## Contributing Guidelines
344387

@@ -353,12 +396,12 @@ When implementing these improvements:
353396
## Timeline
354397

355398
- **Week 1-2**: ✅ **COMPLETED** - High priority fixes (performance optimization, stability analysis)
356-
- **Week 3-5**: 🟡 **IN PROGRESS** - Medium priority features (code deduplication, enhanced testing)
357-
- **Week 6-8**: Low priority improvements and documentation
399+
- **Week 3-5**: **COMPLETED** - Medium priority features (HSL color support, conditional styling, enhanced testing)
400+
- **Week 6-8**: 🟡 **IN PROGRESS** - Additional features (style presets, strikethrough tests, Windows tests)
358401
- **Week 9-10**: Testing, benchmarking, and release preparation
359402

360403
## Notes
361404

362405
This roadmap is a living document and should be updated as work progresses. Priority levels may be adjusted based on user feedback and real-world usage patterns.
363406

364-
Last updated: July 2025 - Updated with performance optimization results
407+
Last updated: July 2025 - Updated with conditional styling completion and comprehensive test coverage improvements
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//
2+
// ConditionalStyleBuilder.swift
3+
// Rainbow
4+
//
5+
// Created by Wei Wang on 15/12/23.
6+
//
7+
// Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
8+
//
9+
// Permission is hereby granted, free of charge, to any person obtaining a copy
10+
// of this software and associated documentation files (the "Software"), to deal
11+
// in the Software without restriction, including without limitation the rights
12+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
// copies of the Software, and to permit persons to whom the Software is
14+
// furnished to do so, subject to the following conditions:
15+
//
16+
// The above copyright notice and this permission notice shall be included in
17+
// all copies or substantial portions of the Software.
18+
//
19+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
// THE SOFTWARE.
26+
27+
/// A builder for applying multiple conditional styles to a string.
28+
/// This class provides a fluent interface for building complex conditional styling scenarios.
29+
public class ConditionalStyleBuilder {
30+
private let text: String
31+
private var steps: [(condition: Bool, transform: (String) -> String)] = []
32+
33+
/// Initializes a new conditional style builder with the given text.
34+
/// - Parameter text: The text to apply conditional styles to.
35+
init(text: String) {
36+
self.text = text
37+
}
38+
39+
/// Creates a conditional step that will apply transformations if the condition is true.
40+
/// - Parameter condition: The condition to evaluate.
41+
/// - Returns: A `ConditionalStyleStep` for chaining style applications.
42+
public func when(_ condition: Bool) -> ConditionalStyleStep {
43+
return ConditionalStyleStep(builder: self, condition: condition)
44+
}
45+
46+
/// Creates a conditional step based on a closure predicate.
47+
/// - Parameter predicate: A closure that returns true if the styles should be applied.
48+
/// - Returns: A `ConditionalStyleStep` for chaining style applications.
49+
public func when(_ predicate: () -> Bool) -> ConditionalStyleStep {
50+
return ConditionalStyleStep(builder: self, condition: predicate())
51+
}
52+
53+
/// Adds a transformation step to the builder.
54+
/// - Parameters:
55+
/// - condition: The condition for applying the transformation.
56+
/// - transform: The transformation to apply if the condition is true.
57+
func addStep(condition: Bool, transform: @escaping (String) -> String) {
58+
steps.append((condition, transform))
59+
}
60+
61+
/// Builds the final string by applying all conditional transformations.
62+
/// - Returns: The string with all applicable conditional styles applied.
63+
public func build() -> String {
64+
var result = text
65+
for step in steps {
66+
if step.condition {
67+
result = step.transform(result)
68+
}
69+
}
70+
return result
71+
}
72+
}
73+
74+
/// Represents a conditional step in the style building process.
75+
/// This class provides methods to apply various styles when a condition is met.
76+
public class ConditionalStyleStep {
77+
private let builder: ConditionalStyleBuilder
78+
private let condition: Bool
79+
80+
/// Initializes a new conditional style step.
81+
/// - Parameters:
82+
/// - builder: The parent builder.
83+
/// - condition: The condition for this step.
84+
init(builder: ConditionalStyleBuilder, condition: Bool) {
85+
self.builder = builder
86+
self.condition = condition
87+
}
88+
89+
/// Applies a color if the condition is true.
90+
/// - Parameter color: The named color to apply.
91+
/// - Returns: The parent builder for method chaining.
92+
@discardableResult
93+
public func color(_ color: NamedColor) -> ConditionalStyleBuilder {
94+
builder.addStep(condition: condition) { $0.applyingColor(color) }
95+
return builder
96+
}
97+
98+
/// Applies a color type if the condition is true.
99+
/// - Parameter color: The color type to apply.
100+
/// - Returns: The parent builder for method chaining.
101+
@discardableResult
102+
public func color(_ color: ColorType) -> ConditionalStyleBuilder {
103+
builder.addStep(condition: condition) { $0.applyingColor(color) }
104+
return builder
105+
}
106+
107+
/// Applies a background color if the condition is true.
108+
/// - Parameter color: The named background color to apply.
109+
/// - Returns: The parent builder for method chaining.
110+
@discardableResult
111+
public func backgroundColor(_ color: NamedBackgroundColor) -> ConditionalStyleBuilder {
112+
builder.addStep(condition: condition) { $0.applyingBackgroundColor(.named(color)) }
113+
return builder
114+
}
115+
116+
/// Applies a background color type if the condition is true.
117+
/// - Parameter color: The background color type to apply.
118+
/// - Returns: The parent builder for method chaining.
119+
@discardableResult
120+
public func backgroundColor(_ color: BackgroundColorType) -> ConditionalStyleBuilder {
121+
builder.addStep(condition: condition) { $0.applyingBackgroundColor(color) }
122+
return builder
123+
}
124+
125+
/// Applies a style if the condition is true.
126+
/// - Parameter style: The style to apply.
127+
/// - Returns: The parent builder for method chaining.
128+
@discardableResult
129+
public func style(_ style: Style) -> ConditionalStyleBuilder {
130+
builder.addStep(condition: condition) { $0.applyingStyle(style) }
131+
return builder
132+
}
133+
134+
/// Applies multiple styles if the condition is true.
135+
/// - Parameter styles: The styles to apply.
136+
/// - Returns: The parent builder for method chaining.
137+
@discardableResult
138+
public func styles(_ styles: Style...) -> ConditionalStyleBuilder {
139+
builder.addStep(condition: condition) { $0.applyingStyles(styles) }
140+
return builder
141+
}
142+
143+
/// Applies multiple styles from an array if the condition is true.
144+
/// - Parameter styles: The array of styles to apply.
145+
/// - Returns: The parent builder for method chaining.
146+
@discardableResult
147+
public func styles(_ styles: [Style]) -> ConditionalStyleBuilder {
148+
builder.addStep(condition: condition) { $0.applyingStyles(styles) }
149+
return builder
150+
}
151+
152+
/// Applies a custom transformation if the condition is true.
153+
/// - Parameter transform: A closure that transforms the string.
154+
/// - Returns: The parent builder for method chaining.
155+
@discardableResult
156+
public func transform(_ transform: @escaping (String) -> String) -> ConditionalStyleBuilder {
157+
builder.addStep(condition: condition, transform: transform)
158+
return builder
159+
}
160+
161+
// Convenience methods for common colors
162+
public var black: ConditionalStyleStep {
163+
builder.addStep(condition: condition) { $0.applyingColor(.black) }
164+
return self
165+
}
166+
public var red: ConditionalStyleStep {
167+
builder.addStep(condition: condition) { $0.applyingColor(.red) }
168+
return self
169+
}
170+
public var green: ConditionalStyleStep {
171+
builder.addStep(condition: condition) { $0.applyingColor(.green) }
172+
return self
173+
}
174+
public var yellow: ConditionalStyleStep {
175+
builder.addStep(condition: condition) { $0.applyingColor(.yellow) }
176+
return self
177+
}
178+
public var blue: ConditionalStyleStep {
179+
builder.addStep(condition: condition) { $0.applyingColor(.blue) }
180+
return self
181+
}
182+
public var magenta: ConditionalStyleStep {
183+
builder.addStep(condition: condition) { $0.applyingColor(.magenta) }
184+
return self
185+
}
186+
public var cyan: ConditionalStyleStep {
187+
builder.addStep(condition: condition) { $0.applyingColor(.cyan) }
188+
return self
189+
}
190+
public var white: ConditionalStyleStep {
191+
builder.addStep(condition: condition) { $0.applyingColor(.white) }
192+
return self
193+
}
194+
195+
// Convenience methods for common styles
196+
public var bold: ConditionalStyleStep {
197+
builder.addStep(condition: condition) { $0.applyingStyle(.bold) }
198+
return self
199+
}
200+
public var dim: ConditionalStyleStep {
201+
builder.addStep(condition: condition) { $0.applyingStyle(.dim) }
202+
return self
203+
}
204+
public var italic: ConditionalStyleStep {
205+
builder.addStep(condition: condition) { $0.applyingStyle(.italic) }
206+
return self
207+
}
208+
public var underline: ConditionalStyleStep {
209+
builder.addStep(condition: condition) { $0.applyingStyle(.underline) }
210+
return self
211+
}
212+
public var blink: ConditionalStyleStep {
213+
builder.addStep(condition: condition) { $0.applyingStyle(.blink) }
214+
return self
215+
}
216+
public var swap: ConditionalStyleStep {
217+
builder.addStep(condition: condition) { $0.applyingStyle(.swap) }
218+
return self
219+
}
220+
public var strikethrough: ConditionalStyleStep {
221+
builder.addStep(condition: condition) { $0.applyingStyle(.strikethrough) }
222+
return self
223+
}
224+
225+
// Methods to transition back to builder for new conditions
226+
public func when(_ condition: Bool) -> ConditionalStyleStep {
227+
return builder.when(condition)
228+
}
229+
230+
public func when(_ predicate: () -> Bool) -> ConditionalStyleStep {
231+
return builder.when(predicate)
232+
}
233+
234+
public func build() -> String {
235+
return builder.build()
236+
}
237+
}

0 commit comments

Comments
 (0)