Skip to content

Commit 8521f17

Browse files
refactor and organize
add readme
1 parent 661627f commit 8521f17

11 files changed

Lines changed: 592 additions & 284 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1230"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "HTMLBuilder"
18+
BuildableName = "HTMLBuilder"
19+
BlueprintName = "HTMLBuilder"
20+
ReferencedContainer = "container:">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
<BuildActionEntry
24+
buildForTesting = "YES"
25+
buildForRunning = "YES"
26+
buildForProfiling = "NO"
27+
buildForArchiving = "NO"
28+
buildForAnalyzing = "YES">
29+
<BuildableReference
30+
BuildableIdentifier = "primary"
31+
BlueprintIdentifier = "HTMLBuilderTests"
32+
BuildableName = "HTMLBuilderTests"
33+
BlueprintName = "HTMLBuilderTests"
34+
ReferencedContainer = "container:">
35+
</BuildableReference>
36+
</BuildActionEntry>
37+
</BuildActionEntries>
38+
</BuildAction>
39+
<TestAction
40+
buildConfiguration = "Debug"
41+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
42+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
43+
shouldUseLaunchSchemeArgsEnv = "YES"
44+
codeCoverageEnabled = "YES">
45+
<CodeCoverageTargets>
46+
<BuildableReference
47+
BuildableIdentifier = "primary"
48+
BlueprintIdentifier = "HTMLBuilder"
49+
BuildableName = "HTMLBuilder"
50+
BlueprintName = "HTMLBuilder"
51+
ReferencedContainer = "container:">
52+
</BuildableReference>
53+
</CodeCoverageTargets>
54+
<Testables>
55+
<TestableReference
56+
skipped = "NO">
57+
<BuildableReference
58+
BuildableIdentifier = "primary"
59+
BlueprintIdentifier = "HTMLBuilderTests"
60+
BuildableName = "HTMLBuilderTests"
61+
BlueprintName = "HTMLBuilderTests"
62+
ReferencedContainer = "container:">
63+
</BuildableReference>
64+
</TestableReference>
65+
</Testables>
66+
</TestAction>
67+
<LaunchAction
68+
buildConfiguration = "Debug"
69+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
70+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
71+
launchStyle = "0"
72+
useCustomWorkingDirectory = "NO"
73+
ignoresPersistentStateOnLaunch = "NO"
74+
debugDocumentVersioning = "YES"
75+
debugServiceExtension = "internal"
76+
allowLocationSimulation = "YES">
77+
</LaunchAction>
78+
<ProfileAction
79+
buildConfiguration = "Release"
80+
shouldUseLaunchSchemeArgsEnv = "YES"
81+
savedToolIdentifier = ""
82+
useCustomWorkingDirectory = "NO"
83+
debugDocumentVersioning = "YES">
84+
<MacroExpansion>
85+
<BuildableReference
86+
BuildableIdentifier = "primary"
87+
BlueprintIdentifier = "HTMLBuilder"
88+
BuildableName = "HTMLBuilder"
89+
BlueprintName = "HTMLBuilder"
90+
ReferencedContainer = "container:">
91+
</BuildableReference>
92+
</MacroExpansion>
93+
</ProfileAction>
94+
<AnalyzeAction
95+
buildConfiguration = "Debug">
96+
</AnalyzeAction>
97+
<ArchiveAction
98+
buildConfiguration = "Release"
99+
revealArchiveInOrganizer = "YES">
100+
</ArchiveAction>
101+
</Scheme>

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,99 @@
11
# HTMLBuilder
22

3-
A description of this package.
3+
Swift library for generating HTML using [Result Builder](https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md)
4+
5+
# Features
6+
7+
- Clean & clear syntax
8+
- Composable & extensible
9+
- Conditional and iteration support
10+
- Underlying generation with libxml2 for always valid results
11+
- Built-in convenient builders and modifiers for common cases
12+
- Raw HTML integration
13+
14+
# Usage
15+
16+
## Tree builder
17+
18+
An HTML tree is built by combining `Node` which is a protocol implemented on `Element` and `String`
19+
20+
```swift
21+
let tree = Element.html(head: {
22+
Element.metadata(charset: "UTF-8")
23+
Element(name: "title") { "Hello world" }
24+
}, body: {
25+
Element.division {
26+
Element(name: "h1") { "Hello" }
27+
Element.paragraph { "Lorem ipsum dolor sit amet, <consectetur> adipiscing elit, sed & eiusmod." }
28+
}
29+
})
30+
print(tree.renderHTML())
31+
```
32+
Will output:
33+
```html
34+
<html><head><meta charset="UTF-8"><title>Hello world</title></head><body><div><h1>Hello</h1><p>Lorem ipsum dolor sit amet, &lt;consectetur&gt; adipiscing&nbsp;elit, sed &amp; eiusmod.</p></div></body></html>
35+
```
36+
37+
## Condition and Iteration
38+
Conditions (`if`, `if else`, `switch`) and iterations (`for ... in`) will work the same as imperative control-flow.
39+
40+
```swift
41+
let cond1 = true
42+
let cond2 = false
43+
let elements = ["Lorem", "ipsum"]
44+
let treeControlFlow = Element.html(head: {
45+
Element.metadata(charset: "UTF-8")
46+
Element(name: "title") { "Hello world" }
47+
}, body: {
48+
Element.division {
49+
if cond1 {
50+
Element(name: "h1") { "Hello" }
51+
}
52+
if cond2 {
53+
Element(name: "h1") { "Hello" }
54+
} else {
55+
Element(name: "h1") { "world" }
56+
}
57+
ForEach(elements) { el in
58+
Element.paragraph { el }
59+
}
60+
}
61+
})
62+
print(treeControlFlow.renderHTML())
63+
```
64+
Will output:
65+
```html
66+
<html><head><meta charset="UTF-8"><title>Hello world</title></head><body><div><h1>Hello</h1><h1>world</h1><p>Lorem</p><p>ipsum</p></div></body></html>
67+
```
68+
69+
## Modifiers
70+
71+
`Element` is modifiable while building the tree.
72+
There is 2 built in modifiers, `identifier` and `class`
73+
74+
```swift
75+
let modifierTree = Element.division {
76+
Element.paragraph { "Hello world" }.identifier("title")
77+
}.class("container")
78+
print(modifierTree.renderHTML())
79+
```
80+
81+
Will output:
82+
```html
83+
<div class="container"><p id="title">Hello world</p></div>
84+
```
85+
86+
## Raw HTML
87+
You can include raw html in the tree with `RawHTML`
88+
```swift
89+
let rawHTMLTree = try Element.division {
90+
try RawHTML("""
91+
<h1>hello world</h1>
92+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit</p>
93+
""")
94+
}
95+
```
96+
Will output:
97+
```html
98+
<div><h1>hello world</h1><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit</p></div>
99+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// ConvenientElementBuilders.swift
3+
//
4+
//
5+
// Created by Antoine Palazzolo on 18/12/2020.
6+
//
7+
8+
import Foundation
9+
10+
extension Element {
11+
12+
public static func division(@NodeBuilder content: () throws -> [Node]) rethrows -> Self {
13+
try Self(name: "div", children: content)
14+
}
15+
public static func image(_ url: URL) -> Self {
16+
Self(name: "img", attributes: [.source: url.absoluteString])
17+
}
18+
public static func paragraph(@NodeBuilder content: () throws -> [Node]) rethrows -> Self {
19+
try Self(name: "p", children: content)
20+
}
21+
public static func button(_ title: String) -> Self {
22+
Self(name: "button", attributes: [.type: "button"]) {
23+
title
24+
}
25+
}
26+
public static func html(@NodeBuilder head: () throws -> [Node], @NodeBuilder body: () throws -> [Node]) rethrows -> Self {
27+
try Self(name: "html") {
28+
try Self(name: "head", children: head)
29+
try Self(name: "body", children: body)
30+
}
31+
}
32+
public static func cssLink(_ url: URL) -> Self {
33+
Self(name: "link", attributes: [.relationship: "stylesheet", .type: "text/css", .hypertextReference: url.absoluteString])
34+
}
35+
public static func javascript(_ script: String) -> Self {
36+
Self(name: "script", attributes: [.type: "application/javascript"]) {
37+
script
38+
}
39+
}
40+
public static func metadata(name: String, content: String) -> Self {
41+
Self.init(name: "meta", attributes: ["name": name, "content": content])
42+
}
43+
public static func metadata(httpEquivalent: String, content: String) -> Self {
44+
Self.init(name: "meta", attributes: ["http-equiv": httpEquivalent, "content": content])
45+
}
46+
public static func metadata(charset: String) -> Self {
47+
Self.init(name: "meta", attributes: ["charset": charset])
48+
}
49+
}
50+
51+
extension Element.AttributeName {
52+
public static let identifier = Self(rawValue: "id")
53+
public static let `class` = Self(rawValue: "class")
54+
public static let hypertextReference = Self(rawValue: "href")
55+
public static let type = Self(rawValue: "type")
56+
public static let source = Self(rawValue: "src")
57+
public static let relationship = Self(rawValue: "rel")
58+
}

Sources/HTMLBuilder/Element.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Element.swift
3+
//
4+
//
5+
// Created by Antoine Palazzolo on 18/12/2020.
6+
//
7+
8+
import Foundation
9+
import libxml2
10+
11+
public struct Element: Node {
12+
public var name: String
13+
public var attributes: [AttributeName: AttributeValue]
14+
public var children: [Node]
15+
16+
public func asXMLNode() -> xmlNodePtr {
17+
guard let node = xmlNewNode(nil, self.name) else { fatalError("node allocation failed") }
18+
for (attrName, attrValue) in self.attributes.sorted(by: { $0.key.rawValue < $1.key.rawValue }) {
19+
xmlNewProp(node, attrName.rawValue, attrValue)
20+
}
21+
for child in self.children {
22+
xmlAddChild(node, child.asXMLNode())
23+
}
24+
return node
25+
}
26+
public func isEqual(to other: Node) -> Bool {
27+
if let other = other as? Element {
28+
if self.name != other.name { return false }
29+
if self.attributes != other.attributes { return false }
30+
if self.children.count != other.children.count { return false }
31+
if zip(self.children, other.children).allSatisfy({ $0.0.isEqual(to: $0.1) }) == false { return false }
32+
return true
33+
}
34+
return false
35+
}
36+
37+
public init(name: String, attributes: [AttributeName: AttributeValue], children: [Node]) {
38+
self.name = name
39+
self.attributes = attributes
40+
self.children = children
41+
}
42+
public init(name: String, attributes: [AttributeName: AttributeValue] = [:], @NodeBuilder children: () throws -> [Node]) rethrows {
43+
self.init(name: name, attributes: attributes, children: try children())
44+
}
45+
public init(name: String, attributes: [AttributeName: AttributeValue] = [:]) {
46+
self.init(name: name, attributes: attributes, children: [])
47+
}
48+
}
49+
extension Element {
50+
public struct AttributeName: RawRepresentable, Hashable, ExpressibleByStringLiteral {
51+
public var rawValue: String
52+
public init(rawValue: String) {
53+
self.rawValue = rawValue
54+
}
55+
public init(stringLiteral value: String) {
56+
self.init(rawValue: value)
57+
}
58+
}
59+
public typealias AttributeValue = String?
60+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// ElementModifiers.swift
3+
//
4+
//
5+
// Created by Antoine Palazzolo on 18/12/2020.
6+
//
7+
8+
import Foundation
9+
10+
extension Element {
11+
public func identifier(_ identifier: String?) -> Self {
12+
var result = self
13+
result.attributes[.identifier] = identifier
14+
return result
15+
}
16+
17+
public func `class`(_ class: String?) -> Self {
18+
var result = self
19+
result.attributes[.class] = `class`
20+
return result
21+
}
22+
}

0 commit comments

Comments
 (0)