Skip to content

Commit ec825d2

Browse files
committed
new tests & readme updates
1 parent 31d5454 commit ec825d2

File tree

4 files changed

+317
-2
lines changed

4 files changed

+317
-2
lines changed

README.md

+182
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,188 @@ import SwiftHtml
7171

7272
That's it.
7373

74+
75+
## Creating custom tags
76+
77+
You can define your own custom tags by subclassing the `Tag` or `EmptyTag` class.
78+
79+
You can follow the same pattern if you take a look at the core tags.
80+
81+
```swift
82+
open class Div: Tag {
83+
84+
}
85+
86+
// <div></div> - standard tag
87+
88+
open class Br: EmptyTag {
89+
90+
}
91+
// <br> - no closing tag
92+
93+
```
94+
95+
By default the name of the tag is automatically derived from the class name (lowercased), but you can also create your own tag type & name by overriding the `createNode()` class function.
96+
97+
```swift
98+
open class LastBuildDate: Tag {
99+
100+
open override class func createNode() -> Node {
101+
Node(type: .standard, name: "lastBuildDate")
102+
}
103+
}
104+
105+
// <lastBuildDate></lastBuildDate> - standard tag with custom name
106+
```
107+
108+
It is also possible to create tags with altered content or default attributes.
109+
110+
```swift
111+
open class Description: Tag {
112+
113+
public init(_ contents: String) {
114+
super.init()
115+
setContents("<![CDATA[" + contents + "]]>")
116+
}
117+
}
118+
// <description><![CDATA[lorem ipsum]]></description> - content wrapped in CDATA
119+
120+
open class Rss: Tag {
121+
122+
public init(@TagBuilder _ builder: () -> [Tag]) {
123+
super.init(builder())
124+
setAttributes([
125+
.init(key: "version", value: "2.0"),
126+
])
127+
}
128+
}
129+
// <rss version="2.0">...</rss> - tag with a default attribute
130+
```
131+
132+
## Attribute management
133+
134+
You can set, add or delete the attributes of a given tag.
135+
136+
```swift
137+
Leaf("example")
138+
// set (override) the current attributes
139+
.setAttributes([
140+
.init(key: "a", value: "foo"),
141+
.init(key: "b", value: "bar"),
142+
.init(key: "c", value: "baz"),
143+
])
144+
// add a new attribute using a key & value
145+
.attribute("foo", "example")
146+
// add a new flag attribute (without a value)
147+
.flagAttribute("bar")
148+
// delete an attribute by using a key
149+
.deleteAttribute("b")
150+
151+
// <leaf a="foo" c="baz" foo="example" bar></leaf>
152+
```
153+
154+
You can also manage the class atrribute through helper methods.
155+
156+
```swift
157+
Span("foo")
158+
// set (override) class values
159+
.class("a", "b", "c")
160+
// add new class values
161+
.class(add: ["d", "e", "f"])
162+
// add new class value if the condition is true
163+
.class(add: "b", true)
164+
/// remove multiple class values
165+
.class(remove: ["b", "c", "d"])
166+
/// remove a class value if the condition is true
167+
.class(remove: "e", true)
168+
169+
// <span class="a f"></span>
170+
```
171+
172+
You can create your own attribute modifier via an extension.
173+
174+
```swift
175+
public extension Guid {
176+
177+
func isPermalink(_ value: Bool = true) -> Self {
178+
attribute("isPermalink", String(value))
179+
}
180+
}
181+
```
182+
183+
There are other built-in type-safe attribute modifiers available on tags.
184+
185+
186+
## Composing tags
187+
188+
You can come up with your own `Tag` composition system by introducing a new protocol.
189+
190+
```swift
191+
protocol TagRepresentable {
192+
193+
func build() -> Tag
194+
}
195+
196+
struct ListComponent: TagRepresentable {
197+
198+
let items: [String]
199+
200+
init(_ items: [String]) {
201+
self.items = items
202+
}
203+
204+
@TagBuilder
205+
func build() -> Tag {
206+
Ul {
207+
items.map { Li($0) }
208+
}
209+
}
210+
}
211+
212+
let tag = ListComponent(["a", "b", "c"]).build()
213+
```
214+
215+
This way it is also possible to extend the `TagBuilder` to support the new protocol.
216+
217+
```swift
218+
extension TagBuilder {
219+
220+
static func buildExpression(_ expression: TagRepresentable) -> Tag {
221+
expression.build()
222+
}
223+
224+
static func buildExpression(_ expression: TagRepresentable) -> [Tag] {
225+
[expression.build()]
226+
}
227+
228+
static func buildExpression(_ expression: [TagRepresentable]) -> [Tag] {
229+
expression.map { $0.build() }
230+
}
231+
232+
static func buildExpression(_ expression: [TagRepresentable]) -> Tag {
233+
GroupTag {
234+
expression.map { $0.build() }
235+
}
236+
}
237+
}
238+
```
239+
240+
Sometimes you'll need extra parameters for the build function, so you have to call the build method by hand.
241+
242+
In those cases it is recommended to introduce a `render` function instead of using build.
243+
244+
```swift
245+
246+
let tag = WebIndexTemplate(ctx) {
247+
ListComponent(["a", "b", "c"])
248+
.render(req)
249+
}
250+
.render(req)
251+
```
252+
253+
If you want to create a lightweight template engine for the [Vapor](https://vapor.codes/) web framework using SwiftHtml, you can see a working example inside the [Feather CMS core](https://github.com/FeatherCMS/feather-core) repository.
254+
255+
74256
## Credits & references
75257

76258
- [HTML Reference](https://www.w3schools.com/tags/default.asp)

Sources/SwiftSgml/Document.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct Document {
1717
public let type: `Type`
1818
public let root: Tag
1919

20-
public init(_ type: `Type` = .unspecified, _ builder: () -> Tag) {
20+
public init(_ type: `Type` = .unspecified, @TagBuilder _ builder: () -> Tag) {
2121
self.type = type
2222
self.root = builder()
2323
}

Tests/SwiftHtmlTests/SwiftHtmlTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class SwiftHtmlTests: XCTestCase {
5656
.class(add: ["d", "e", "f"])
5757
.class(add: "b", true)
5858
.class(remove: ["b", "c", "d"])
59-
.class(remove: "e")
59+
.class(remove: "e", true)
6060
}
6161
let html = DocumentRenderer(minify: true).render(doc)
6262
XCTAssertEqual(#"<span class="a f"></span>"#, html)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Tibor Bodecs on 2022. 02. 16..
6+
//
7+
8+
import XCTest
9+
@testable import SwiftHtml
10+
11+
protocol TagRepresentable {
12+
13+
func build() -> Tag
14+
}
15+
16+
extension TagBuilder {
17+
18+
static func buildExpression(_ expression: TagRepresentable) -> Tag {
19+
expression.build()
20+
}
21+
22+
static func buildExpression(_ expression: TagRepresentable) -> [Tag] {
23+
[expression.build()]
24+
}
25+
26+
static func buildExpression(_ expression: [TagRepresentable]) -> [Tag] {
27+
expression.map { $0.build() }
28+
}
29+
30+
static func buildExpression(_ expression: [TagRepresentable]) -> Tag {
31+
GroupTag {
32+
expression.map { $0.build() }
33+
}
34+
}
35+
}
36+
37+
struct ListComponent: TagRepresentable {
38+
39+
let items: [String]
40+
41+
init(_ items: [String]) {
42+
self.items = items
43+
}
44+
45+
@TagBuilder
46+
func build() -> Tag {
47+
Ul {
48+
items.map { Li($0) }
49+
}
50+
}
51+
}
52+
53+
54+
final class TagCompositionTests: XCTestCase {
55+
56+
func testListComponentBuild() {
57+
let doc = Document {
58+
ListComponent(["a", "b", "c"]).build()
59+
}
60+
61+
XCTAssertEqual(DocumentRenderer().render(doc), """
62+
<ul>
63+
<li>a</li>
64+
<li>b</li>
65+
<li>c</li>
66+
</ul>
67+
""")
68+
}
69+
70+
func testListComponent() {
71+
let doc = Document {
72+
ListComponent(["a", "b", "c"])
73+
}
74+
75+
XCTAssertEqual(DocumentRenderer().render(doc), """
76+
<ul>
77+
<li>a</li>
78+
<li>b</li>
79+
<li>c</li>
80+
</ul>
81+
""")
82+
}
83+
84+
func testListComponentAndTags() {
85+
let doc = Document {
86+
H1("foo")
87+
ListComponent(["a", "b", "c"])
88+
}
89+
90+
XCTAssertEqual(DocumentRenderer().render(doc), """
91+
<h1>foo</h1>
92+
<ul>
93+
<li>a</li>
94+
<li>b</li>
95+
<li>c</li>
96+
</ul>
97+
""")
98+
}
99+
100+
func testListComponentAndGroupTag() {
101+
let doc = Document {
102+
H1("foo")
103+
["1", "2", "3"].map { value -> Tag in
104+
GroupTag {
105+
H2(value)
106+
ListComponent(["a", "b", "c"])
107+
}
108+
}
109+
}
110+
111+
XCTAssertEqual(DocumentRenderer().render(doc), """
112+
<h1>foo</h1>
113+
<h2>1</h2>
114+
<ul>
115+
<li>a</li>
116+
<li>b</li>
117+
<li>c</li>
118+
</ul>
119+
<h2>2</h2>
120+
<ul>
121+
<li>a</li>
122+
<li>b</li>
123+
<li>c</li>
124+
</ul>
125+
<h2>3</h2>
126+
<ul>
127+
<li>a</li>
128+
<li>b</li>
129+
<li>c</li>
130+
</ul>
131+
""")
132+
}
133+
}

0 commit comments

Comments
 (0)