Skip to content

Commit

Permalink
Add Fragment Node (#23)
Browse files Browse the repository at this point in the history
* Add Fragment Node

* Update Contents.swift

* Update README, document helper, array literal

* README

* WIP

* Further explore fragment API (#24)

* Change element children from [Node] to Node

* Update

* Variadics

* Fix

* Update README.md

* Add HTML example

* Remove ... for now

* Fix tests

* Performance

* Update Sources/Html/Node.swift

Co-Authored-By: stephencelis <stephen.celis@gmail.com>

* Fix tests
  • Loading branch information
stephencelis authored Feb 9, 2019
1 parent b1b59b3 commit 4e931b2
Show file tree
Hide file tree
Showing 13 changed files with 1,460 additions and 417 deletions.
99 changes: 51 additions & 48 deletions Html.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,31 @@ code {
"""

/// A document built in the HTML DSL.
let doc = html([
head([
style(unsafe: stylesheet)
]),
body([
h1(["🗺 HTML"]),
p(["""
let doc = document(
doctype,
html(
head(
style(unsafe: stylesheet)
),
body(
h1("🗺 HTML"),
p("""
A Swift DSL for type-safe, extensible, and transformable HTML documents.
"""
]),

h2(["Motivation"]),
p(["""
),
h2("Motivation"),
p("""
When building server-side application in Swift it is important to be able to render HTML documents. The current best practice in the community is to use templating languages like Stencil, Mustache, Handlebars, Leaf and others. However, templating languages are inherently unsafe due to the API being stringly typed. The vast majority of errors that can arise in creating a template happen only at runtime, including typos and type mismatches.
"""]),
p(["""
"""),
p("""
That’s unfortunate because we are used to working in Swift, which is strongly typed, and many potential bugs are discovered at compile time rather than runtime. Our approach is to instead embed HTML documents directly into Swift types so that we immediately get all of the features and safety Swift has to offer.
"""]),

h2(["Examples"]),
p(["""
"""
),
h2("Examples"),
p("""
HTML documents can be created with this library in a tree-like fashion, much like how you might create a nested JSON document:
"""]),
pre(["""
"""),
pre("""
import Html
let document = html([
Expand All @@ -64,21 +65,20 @@ let document = html([
p(["You’ve found our site!"])
])
])
"""]),

p([
"Underneath the hood these tag functions ",
code(["html"]),
", ", code(["body"]),
", ", code(["h1"]),
"etc. are just creating and nesting instances of a ",
code(["Node"]),
" type, which is a simple Swift enum. The cool part is that because ",
code(["Node"]),
" is just a simple Swift type, we can transform it in all types of intersting ways. For a silly example, what if we wanted to remove all instances of exclamation marks from our document?"
]),

pre(["""
"""),

p(
"Underneath the hood these tag functions ",
code("html"),
", ", code("body"),
", ", code("h1"),
"etc. are just creating and nesting instances of a ",
code("Node"),
" type, which is a simple Swift enum. The cool part is that because ",
code("Node"),
" is just a simple Swift type, we can transform it in all types of intersting ways. For a silly example, what if we wanted to remove all instances of exclamation marks from our document?"
),
pre("""
func unexclaim(_ node: Node) -> Node {
switch node {
case .comment:
Expand All @@ -101,25 +101,25 @@ func unexclaim(_ node: Node) -> Node {
unexclaim(document) // Node
"""
]),
),

p([
"And of course you can first run the document through the ", code(["unexlaim"]), " transformation, and then render it:"
]),
p(
"And of course you can first run the document through the ", code("unexclaim"), " transformation, and then render it:"
),

pre(["""
pre("""
render(unexclaim(document))
// <html><body><h1>Welcome.</h1><p>You’ve found our site.</p></body></html>
"""
]),

])
])
)
)
)
)

/// A function that "redacts" an HTML document by transforming all text nodes
/// into █-sequences of characters.
func redacted(node: Node) -> Node {
func redacted(string: String) -> String {
func redact(string: String) -> String {
return string
.split(separator: " ")
.map { String(repeating: "", count: $0.count )}
Expand All @@ -135,7 +135,7 @@ func redacted(node: Node) -> Node {
return .comment("")
// Raw strings will be redacted
case let .raw(string):
return .raw(redacted(string: string))
return .raw(redact(string: string))
// Style tags will not be redacted
case .element("style", _, _):
return node
Expand All @@ -148,11 +148,14 @@ func redacted(node: Node) -> Node {
children
)
// All other elements will have their children redacted.
case let .element(tag, attrs, children):
return .element(tag, attrs, children.map(redacted(node:)))
case let .element(tag, attrs, child):
return .element(tag, attrs, redacted(node: child))
// All fragments will have their children redacted.
case let .fragment(children):
return .fragment(children.map(redacted(node:)))
// Text nodes will be redacted
case let .text(string):
return .text(redacted(string: string))
return .text(redact(string: string))
}
}

Expand Down
56 changes: 29 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ HTML documents can be created in a tree-like fashion, much like you might create
```swift
import Html

let document = html([
body([
h1(["Welcome!"]),
p(["You’ve found our site!"])
])
])
let document = html(
body(
h1("Welcome!"),
p("You’ve found our site!")
)
)
```

Underneath the hood these tag functions `html`, `body`, `h1`, etc., are just creating and nesting instances of a `Node` type, which is a simple Swift enum. Because `Node` is just a simple Swift type, we can transform it in all kinds of interesting ways. For a silly example, what if we wanted to remove all instances of exclamation marks from our document?
Expand All @@ -54,6 +54,10 @@ func unexclaim(_ node: Node) -> Node {
// Recursively transform all of the children of an element
return .element(tag, attrs, children.map(unexclaim))

case let .fragment(children):
// Recursively transform all of the children of a fragment
return .fragment(children.map(unexclaim))

case let .raw(string), .text(string):
// Transform text nodes by replacing exclamation marks with periods.
return string.replacingOccurrences(of: "!", with: ".")
Expand Down Expand Up @@ -95,31 +99,32 @@ Here the `src` attribute takes a string, but `width` and `height` take integers,
For a more advanced example, `<li>` tags can only be placed inside `<ol>` and `<ul>` tags, and we can represent this fact so that it’s impossible to construct an invalid document:

```swift
let listTag = ul([
li(["Cat"]),
li(["Dog"]),
li(["Rabbit"])
]) // ✅ Compiles!
let listTag = ul(
li("Cat"),
li("Dog"),
li("Rabbit")
) // ✅ Compiles!

render(listTag)
// <ul><li>Cat</li><li>Dog</li><li>Rabbit</li></ul>

div([
li(["Cat"]),
li(["Dog"]),
li(["Rabbit"])
]) // 🛑 Compile error
div(
li("Cat"),
li("Dog"),
li("Rabbit")
) // 🛑 Compile error
```

## Design

The core of the library is a single enum with 5 cases:
The core of the library is a single enum with 6 cases:

```swift
public enum Node {
case comment(String)
case doctype(String)
indirect case element(String, [(key: String, value: String?)], [Node])
indirect case element(String, [(key: String, value: String?)], Node)
indirect case fragment([Node])
case raw(String)
case text(String)
}
Expand All @@ -138,27 +143,24 @@ Node.element("html", [], [
// versus

// Using helper functions
html([
body([
h1(["Welcome!"]),
p(["You’ve found our site!"])
])
html(
body(
h1("Welcome!"),
p("You’ve found our site!")
)
)
```

This makes the “Swiftification” of an HTML document looks very similar to the original document.

## FAQ

<!--

### Can I use this with existing Swift web frameworks like Kitura and Vapor?

Yes! We even provide plug-in libraries that reduce the friction of using this library with Kitura and Vapor. Find out more information at the following repos:

- [swift-html-kitura](https://github.com/pointfreeco/swift-html-kitura)
- [swift-html-vapor](https://github.com/pointfreeco/swift-html-vapor)

-->

### Why would I use this over a templating language?

Expand Down
8 changes: 5 additions & 3 deletions Sources/Html/DebugRender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,16 @@ public func debugRender(_ node: Node, config: Config = .pretty) -> String {
output.append(">")
output.append(config.newline)
guard !children.isEmpty || !voidElements.contains(tag) else { return }
for node in children {
debugRenderHelp(node, into: &output, config: config, indentation: indentation + config.indentation)
}
debugRenderHelp(children, into: &output, config: config, indentation: indentation + config.indentation)
output.append(indentation)
output.append("</")
output.append(tag)
output.append(">")
output.append(config.newline)
case let .fragment(children):
for node in children {
debugRenderHelp(node, into: &output, config: config, indentation: indentation)
}
case let .raw(string), let .text(string):
guard !string.isEmpty else { return }
output.append(indentation)
Expand Down
Loading

0 comments on commit 4e931b2

Please sign in to comment.