🗺 swift-html

CI

一个用于类型安全、可扩展和可转换 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!")
    )
  )
)

在底层,这些标签函数 htmlbodyh1 等只是创建和嵌套 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 属性接受字符串,但 widthheight 接受整数,因为在这些属性中放入其他任何内容都是无效的。

对于更高级的示例,<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 化”看起来与原始文档非常相似。

常见问题解答

我可以在现有的 Swift Web 框架(如 Kitura 和 Vapor)中使用它吗?

是的!我们甚至提供了插件库,以减少在此库与 Kitura 和 Vapor 一起使用时的摩擦。在以下仓库中查找更多信息

为什么我要使用它而不是模板语言?

模板语言很流行且易于上手,但它们有很多缺点

  1. 字符串式 API:模板语言始终是字符串类型的,因为您将模板作为一大段字符串提供,然后在运行时插值值并执行逻辑。这意味着我们在 Swift 中理所当然的事情,例如编译器捕获拼写错误和类型不匹配,在您运行代码之前都不会被注意到。

  2. 不完整的语言:模板语言就是编程语言。这意味着您应该期望从这些语言中获得您从其他成熟语言(如 Swift)中获得的所有优点。这包括语法高亮、IDE 自动完成、静态分析、重构工具、断点、调试器以及使 Swift 强大的大量功能,如 let 绑定、条件语句、循环等等。然而,现实情况是,没有一种模板语言支持所有这些功能。

  3. 僵化:模板语言是僵化的,因为它们不允许我们习惯在 Swift 中对数据结构执行的组合和转换类型。不可能简洁地遍历您构建的文档,并检查或转换您访问的节点。这种能力有很多应用,例如能够漂亮地打印或缩小您的 HTML 输出,或者编写一个转换,允许您将 CSS 样式表内联到 HTML 节点中。由于模板语言的工作方式,整个世界都对您关闭了。

此库中的 DSL 修复了所有这些问题,并打开了模板语言完全关闭的大门。

在什么情况下更适合使用模板语言而不是 swift-html?

您可能仍然想使用模板语言的原因有几个

  1. 设计师向您交付了一个大型 HTML 文档,而您只想插入一点值插值或逻辑。在这种情况下,您可以简单地将该 HTML 复制并粘贴到您的模板中,添加一些插值标记,您就可以很好地开始从您的 Web 应用程序提供完整的页面。

  2. 您需要渲染非 HTML 文档。模板语言的优点在于它直接输出到纯文本,因此它可以建模任何类型的文档,无论是 HTML、markdown、XML、RSS、ATOM、LaTeX 等。

  3. 在单个表达式中创建非常大的文档可能会导致编译时间增加,而模板不会被 Swift 编译,因此不会影响编译时间。幸运的是,这通常不是问题,因为将文档分解成任意多个小块非常容易,从长远来看,这可能会导致更多可重用的代码。

如果您确实认为模板语言更适合您的需求,那么您应该考虑 HypertextLiteral,它为您提供类似模板的功能,但以更安全的方式。

安装

您可以通过将 swift-html 添加为包依赖项,将其添加到 Xcode 项目中。

https://github.com/pointfreeco/swift-html

如果您想在 SwiftPM 项目中使用 swift-html,只需将其添加到 Package.swift 中的 dependencies 子句中即可

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-html", from: "0.4.0")
]

有兴趣了解更多?

这些概念(以及更多)在 Point-Free 的一系列剧集中进行了深入探讨,Point-Free 是一个由 Brandon WilliamsStephen Celis 主持的探索函数式编程和 Swift 的视频系列。

该库的想法在以下剧集中进行了探讨

video poster image

许可证

所有模块均在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE