Skip to content

Commit cd9ebab

Browse files
committed
better class manipulation
1 parent 3ca7a14 commit cd9ebab

File tree

2 files changed

+134
-19
lines changed

2 files changed

+134
-19
lines changed

Sources/SwiftHtml/Attributes/Global.swift

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,103 @@ public enum Translate: String {
3131
case no
3232
}
3333

34+
private extension String {
3435

36+
/// turns a list of class values (separated by a space" into an array of strings
37+
var classArray: [String] {
38+
split(separator: " ").map { String($0) }
39+
}
40+
}
41+
42+
private extension Array where Element == String {
43+
44+
var classString: String {
45+
filter { !$0.isEmpty }.joined(separator: " ")
46+
}
47+
}
3548

3649
public extension Tag {
3750

38-
/// Specifies a shortcut key to activate/focus an element
39-
func accesskey(_ value: Character) -> Self {
40-
attribute("accesskey", String(value))
51+
// MARK: - class management
52+
53+
/// find an existing class attribute and return the value as an array of strings or an empty array
54+
private var classArray: [String] {
55+
node.attributes.first { $0.key == "class" }?.value?.classArray ?? []
4156
}
4257

4358
/// Specifies one classname for an element (refers to a class in a style sheet)
4459
func `class`(_ value: String?, _ condition: Bool = true) -> Self {
45-
let values = value?.split(separator: " ").map { String($0) } ?? []
46-
let existing = node.attributes.first { $0.key == "class" }?.value?.split(separator: " ").map { String($0) } ?? []
47-
let newValues = existing + values
48-
49-
var newValue: String? = nil
50-
if !newValues.isEmpty {
51-
newValue = newValues.joined(separator: " ")
52-
}
53-
return attribute("class", newValue, condition)
60+
attribute("class", value, condition)
5461
}
5562

5663
/// Specifies multiple classnames for an element (refers to a class in a style sheet)
5764
func `class`(_ values: [String], _ condition: Bool = true) -> Self {
5865
/// @NOTE: explicit true flag is needed, otherwise Swift won't know which function to call...
59-
`class`(values.filter{!$0.isEmpty}.joined(separator: " "), condition)
66+
`class`(values.classString, condition)
6067
}
6168

6269
/// Specifies multiple classnames for an element (refers to a class in a style sheet)
6370
func `class`(_ values: String...) -> Self {
6471
`class`(values)
6572
}
6673

74+
/// Adds a single value to the class list if the condition is true
75+
///
76+
/// Note: If the value is empty or nil it won't be added to the list
77+
///
78+
func `class`(add value: String?, _ condition: Bool = true) -> Self {
79+
guard let value = value else {
80+
return self
81+
}
82+
return `class`(add: [value], condition)
83+
}
84+
85+
/// Adds an array of values to the class list if the condition is true
86+
///
87+
/// Note: If the value is empty it won't be added to the list
88+
///
89+
func `class`(add values: [String], _ condition: Bool = true) -> Self {
90+
let newValues = classArray + values.filter { !$0.isEmpty }
91+
92+
var newValue: String? = nil
93+
if !newValues.isEmpty {
94+
newValue = newValues.classString
95+
}
96+
return `class`(newValue, condition)
97+
}
98+
99+
/// Removes a given class values if the condition is true
100+
func `class`(remove value: String?, _ condition: Bool = true) -> Self {
101+
guard let value = value else {
102+
return self
103+
}
104+
return `class`(remove: [value], condition)
105+
}
106+
107+
/// Removes an array of class values if the condition is true
108+
func `class`(remove values: [String], _ condition: Bool = true) -> Self {
109+
let newClasses = classArray.filter { !values.contains($0) }
110+
return `class`(newClasses, condition)
111+
}
112+
113+
/// toggles a single class value
114+
func `class`(toggle value: String?, _ condition: Bool = true) -> Self {
115+
guard let value = value else {
116+
return self
117+
}
118+
if classArray.contains(value) {
119+
return `class`(remove: value, condition)
120+
}
121+
return `class`(add: value, condition)
122+
}
123+
124+
// MARK: - other global attributes
125+
126+
/// Specifies a shortcut key to activate/focus an element
127+
func accesskey(_ value: Character) -> Self {
128+
attribute("accesskey", String(value))
129+
}
130+
67131
/// Specifies whether the content of an element is editable or not
68132
func contenteditable(_ value: Bool) -> Self {
69133
attribute("contenteditable", String(value))

Tests/SwiftHtmlTests/SwiftHtmlTests.swift

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,73 @@ final class SwiftHtmlTests: XCTestCase {
1212

1313
func testClassAttribute() {
1414
let doc = Document {
15-
Span("").class("a", "b", "c")
15+
Span("").class("a", "b", "", "b", "c")
1616
}
1717
let html = DocumentRenderer(minify: true).render(doc)
18-
XCTAssertEqual(#"<span class="a b c"></span>"#, html)
18+
XCTAssertEqual(#"<span class="a b b c"></span>"#, html)
1919
}
2020

21-
func testMultipleClassAttribute() {
21+
func testMultipleClasses() {
2222
let doc = Document {
2323
Span("")
2424
.class("a", "b", "c")
2525
.class("d", "e", "f")
26-
.class("g", true)
27-
.class(["h", "i", "j"], true)
2826
}
2927
let html = DocumentRenderer(minify: true).render(doc)
30-
XCTAssertEqual(#"<span class="a b c d e f g h i j"></span>"#, html)
28+
XCTAssertEqual(#"<span class="d e f"></span>"#, html)
29+
}
30+
31+
func testClassManipulation() {
32+
let doc = Document {
33+
Span("")
34+
.class("a", "b", "c")
35+
.class(add: ["d", "e", "f"])
36+
.class(add: "b", true)
37+
.class(remove: ["b", "c", "d"])
38+
.class(remove: "e")
39+
}
40+
let html = DocumentRenderer(minify: true).render(doc)
41+
XCTAssertEqual(#"<span class="a f"></span>"#, html)
42+
}
43+
44+
func testAddClass() {
45+
let doc = Document {
46+
Span("")
47+
.class("a", "b", "c")
48+
.class(add: "d")
49+
}
50+
let html = DocumentRenderer(minify: true).render(doc)
51+
XCTAssertEqual(#"<span class="a b c d"></span>"#, html)
52+
}
53+
54+
func testRemoveClass() {
55+
let doc = Document {
56+
Span("")
57+
.class("a", "b", "c")
58+
.class(remove: "b")
59+
}
60+
let html = DocumentRenderer(minify: true).render(doc)
61+
XCTAssertEqual(#"<span class="a c"></span>"#, html)
62+
}
63+
64+
func testToggleAddClass() {
65+
let doc = Document {
66+
Span("")
67+
.class("a", "b", "c")
68+
.class(toggle: "d")
69+
}
70+
let html = DocumentRenderer(minify: true).render(doc)
71+
XCTAssertEqual(#"<span class="a b c d"></span>"#, html)
72+
}
73+
74+
func testToggleRemoveClass() {
75+
let doc = Document {
76+
Span("")
77+
.class("a", "b", "c")
78+
.class(toggle: "b")
79+
}
80+
let html = DocumentRenderer(minify: true).render(doc)
81+
XCTAssertEqual(#"<span class="a c"></span>"#, html)
3182
}
3283

3384
func testHtmlDocument() {

0 commit comments

Comments
 (0)