一个用于类型安全、可扩展和可转换 HTML 文档的 Swift DSL。
目前在 Swift 中渲染 HTML 的流行选择是使用模板语言,但它们使您的应用程序容易出现 运行时错误 和 无效 HTML 。我们的库通过将 HTML 直接嵌入到 Swift 强大的类型系统中,在编译时就避免了这些运行时问题。
HTML 文档可以像创建嵌套的 JSON 文档一样,以树状方式创建
import Html
let document: Node = .document(
.html(
.body(
.h1("Welcome!"),
.p("You’ve found our site!")
)
)
)
在底层,这些标签函数 html
、body
、h1
等只是创建和嵌套 Node
类型的实例,这是一个简单的 Swift 枚举。因为 Node
只是一个简单的 Swift 类型,我们可以用各种有趣的方式转换它。举一个简单的例子,如果我们想从文档中删除所有感叹号的实例,该怎么办?
func unexclaim(_ node: Node) -> Node {
switch node {
case .comment:
// Don't need to transform HTML comments
return node
case .doctype:
// Don't need to transform doctypes
return node
case let .element(tag, attrs, children):
// Recursively transform all of the children of an element
return .element(tag, attrs, unexclaim(children))
case let .fragment(children):
// Recursively transform all of the children of a fragment
return .fragment(children.map(unexclaim))
case let .raw(string):
// Transform text nodes by replacing exclamation marks with periods.
return .raw(string.replacingOccurrences(of: "!", with: "."))
case let .text(string):
// Transform text nodes by replacing exclamation marks with periods.
return .text(string.replacingOccurrences(of: "!", with: "."))
}
}
unexclaim(document)
创建文档后,您可以使用 render
函数渲染它
render(document)
// <!doctype html><html><body><h1>Welcome!</h1><p>You’ve found our site!</p></body></html>
当然,您可以先通过 unexclaim
转换运行文档,然后再渲染它
render(unexclaim(document))
// <!doctype html><html><body><h1>Welcome.</h1><p>You’ve found our site.</p></body></html>
现在文档变得非常严肃认真 😂。
因为我们将 DSL 嵌入到 Swift 中,所以我们可以利用一些高级 Swift 功能,在构建 HTML 文档时添加额外的安全层。举一个简单的例子,我们可以加强许多 HTML API,强制它们使用真实的类型,而不是仅仅依赖字符串。
let imgTag = Node.img(attributes: [.src("cat.jpg"), .width(400), .height(300)])
render(imgTag)
// <img src="cat.jpg" width="400" height="300">
这里 src
属性接受字符串,但 width
和 height
接受整数,因为在这些属性中放入其他任何内容都是无效的。
对于更高级的示例,<li>
标签只能放置在 <ol>
和 <ul>
标签内,我们可以表示这个事实,使其不可能构建无效的文档
let listTag = Node.ul(
.li("Cat"),
.li("Dog"),
.li("Rabbit")
) // ✅ Compiles!
render(listTag)
// <ul><li>Cat</li><li>Dog</li><li>Rabbit</li></ul>
Node.div(
.li("Cat"),
.li("Dog"),
.li("Rabbit")
) // 🛑 Compile error
该库的核心是一个包含 6 种情况的单个枚举
public enum Node {
case comment(String)
case doctype(String)
indirect case element(String, [(key: String, value: String?)], Node)
indirect case fragment([Node])
case raw(String)
case text(String)
}
这种类型允许您表达任何可能存在的 HTML 文档。但是,直接使用这种类型可能有点笨拙,因此我们提供了许多辅助函数,用于以类型安全的方式从整个 HTML 规范中构建每个元素和属性
// Not using helper functions
Node.element("html", [], [
.element("body", [], [
.element("p", [], [.text("You’ve found our site!")])
])
])
// versus
// Using helper functions
Node.html(
.body(
.h1("Welcome!"),
.p("You’ve found our site!")
)
)
这使得 HTML 文档的“Swift 化”看起来与原始文档非常相似。
是的!我们甚至提供了插件库,以减少在此库与 Kitura 和 Vapor 一起使用时的摩擦。在以下仓库中查找更多信息
模板语言很流行且易于上手,但它们有很多缺点
字符串式 API:模板语言始终是字符串类型的,因为您将模板作为一大段字符串提供,然后在运行时插值值并执行逻辑。这意味着我们在 Swift 中理所当然的事情,例如编译器捕获拼写错误和类型不匹配,在您运行代码之前都不会被注意到。
不完整的语言:模板语言就是编程语言。这意味着您应该期望从这些语言中获得您从其他成熟语言(如 Swift)中获得的所有优点。这包括语法高亮、IDE 自动完成、静态分析、重构工具、断点、调试器以及使 Swift 强大的大量功能,如 let 绑定、条件语句、循环等等。然而,现实情况是,没有一种模板语言支持所有这些功能。
僵化:模板语言是僵化的,因为它们不允许我们习惯在 Swift 中对数据结构执行的组合和转换类型。不可能简洁地遍历您构建的文档,并检查或转换您访问的节点。这种能力有很多应用,例如能够漂亮地打印或缩小您的 HTML 输出,或者编写一个转换,允许您将 CSS 样式表内联到 HTML 节点中。由于模板语言的工作方式,整个世界都对您关闭了。
此库中的 DSL 修复了所有这些问题,并打开了模板语言完全关闭的大门。
您可能仍然想使用模板语言的原因有几个
设计师向您交付了一个大型 HTML 文档,而您只想插入一点值插值或逻辑。在这种情况下,您可以简单地将该 HTML 复制并粘贴到您的模板中,添加一些插值标记,您就可以很好地开始从您的 Web 应用程序提供完整的页面。
您需要渲染非 HTML 文档。模板语言的优点在于它直接输出到纯文本,因此它可以建模任何类型的文档,无论是 HTML、markdown、XML、RSS、ATOM、LaTeX 等。
在单个表达式中创建非常大的文档可能会导致编译时间增加,而模板不会被 Swift 编译,因此不会影响编译时间。幸运的是,这通常不是问题,因为将文档分解成任意多个小块非常容易,从长远来看,这可能会导致更多可重用的代码。
如果您确实认为模板语言更适合您的需求,那么您应该考虑 HypertextLiteral,它为您提供类似模板的功能,但以更安全的方式。
您可以通过将 swift-html 添加为包依赖项,将其添加到 Xcode 项目中。
如果您想在 SwiftPM 项目中使用 swift-html,只需将其添加到 Package.swift
中的 dependencies
子句中即可
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-html", from: "0.4.0")
]
这些概念(以及更多)在 Point-Free 的一系列剧集中进行了深入探讨,Point-Free 是一个由 Brandon Williams 和 Stephen Celis 主持的探索函数式编程和 Swift 的视频系列。
该库的想法在以下剧集中进行了探讨
所有模块均在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。