📄 swift-web-page (swep)

Swift Version: 5.1 Swift Package Manager Swift Package Manager

Swep 是一个 Swift DSL,用于以 SwiftUI 的方式编写类型安全的 HTML/CSS。

请查看 SwepUI,了解类似 SwiftUI 的封装!

目录

动机

目前在 Swift 中渲染 HTML/CSS 的流行选择是使用模板语言,但这会使您的应用程序暴露于运行时错误无效的 HTML/CSS。我们的库通过将 HTML/CSS 直接嵌入到 Swift 强大的类型系统中,在编译时就防止了这些运行时问题。

示例

HTML/CSS 文档可以像创建嵌套的 SwiftUI View 一样,以类似 SwiftUI 的方式创建

import Swep

let page = document {
  html {
    head(title("YOLO!"))
    body {
      h1("Welcome!")
      p("You've found our site!")
    }
  }
}

Style 标签内的 CSS!

import Swep

let page = document {
  html {
    head {
      title("YOLO!")
      style {
        selector("h1") {
          color(.tomato())
        }
      }
    }
    body {
      h1("Welcome!")
      p("You've found our site!")
    }
  }
}

更棒的是,CSS 作为标签修饰符

import Swep

let page = document {
  html {
    head(title("YOLO!"))
    body {
      h1("Welcome!")
        .color(.tomato())
      p("You've found our site!")
    }
  }
}

一旦您的文档创建完成,您可以使用 render 函数渲染它

/// when you just want to see the output..
page.render(.debug(.pretty(.spaces(2))))
// or
page.debugRender()
<!DOCTYPE html>
<html>
  <head>
    <title>
      YOLO!
    </title>
  </head>
  <body>
    <h1 style="color:rgba(255,99,71,1.0)">
      Welcome!
    </h1>
    <p>
      You've found our site!
    </p>
  </body>
</html>
/// when your document is ready for release..
page.render(.release(.inline))
// or
page.render()
<!DOCTYPE html><html><head><title>YOLO!</title></head><body><h1 style="color:rgba(255,99,71,1.0);">Welcome!</h1><p>You&apos;ve found our site!</p></body></html>

安全性

因为我们将 DSL 嵌入到 Swift 中,所以我们可以利用一些高级 Swift 功能,在构建 HTML/CSS 文档时添加额外的安全层。举一个简单的例子,我们可以加强许多 HTML/CSS API,强制它们使用真实的类型,而不是仅仅依赖字符串。

let imgTag = img()
               .src("cat.jpg")
               .width(400)
               .height(300)

imgTag.render()
// <img src="cat.jpg" width="400" height="300">

这里 src 属性接受一个字符串,但 widthheight 接受整数,因为在这些属性中放入任何其他内容都是无效的。

对于更高级的示例,<li> 标签只能放在 <ol><ul> 标签内,我们可以表示这个事实,使得构建无效的文档成为不可能

let listTag = ul {
  li("Cat")
  li("Dog")
  li("Rabbit")
} // ✅ Compiles!

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

div {
  li("Cat")
  li("Dog")
  li("Rabbit")
} // 🛑 Compile error

另一个高级示例,<thead><tbody><tfoot> 标签只能放在 <table> 标签内,同样,<th><td> 标签只能放在 <tr> 标签内。

let tableTag = table {
  tr(th("A Head"))
  tr(td("A Body"))
} // ✅ Compiles!

type(of: tableTag)
// Table<Tuple<(Tr<Th<Text>>, Tr<Td<Text>>)>>

let tableTag = table {
  thead(tr(th("A Head")))
  tbody(tr(td("A Body")))
} // ✅ Compiles!

type(of: tableTag)
// Table<Tuple<(Thead<Tr<Th<Text>>>, Tbody<Tr<Td<Text>>>)>>

let tableTag = table {
  thead(tr(th("A Head")))
  for _ in 1...3 {
    tbody(tr(td("A Body")))
  }
} // ✅ Compiles!

type(of: tableTag)
// Table<Tuple<(Thead<Tr<Th<Text>>>, Array<Tbody<Tr<Td<Text>>>>)>>

table {
  tbody(tr(td("A Body")))
  thead(tr(th("A Head")))
} // 🛑 Compile error

还有更多内容..

设计

在幕后,Swep 遵循面向协议编程 (POP) 方法,以及强大的 swift 功能 @resultBuilder。主要有两个库:Html 处理 html 方面,Css 处理 css 方面。还有一个微型库 HtmlCssSupport 处理 HtmlCss 的组合。

FAQ

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

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

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

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

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

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

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

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

什么时候更适合使用模板语言而不是 swift-web-page?

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

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

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

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

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

真实世界示例

创建一个 html 文档和 css 样式表

let titillimFont: StaticString = """
  https://fonts.googleapis.com/css2?family=\
  Titillium+Web:ital,wght@0,200;0,300;0,400;\
  0,600;0,700;0,900;1,200;1,300;1,400;1,600;\
  1,700&display=swap
  """
  
let plainStyle = style {
  `import`(titillimFont)
  selector("*, *::before, *::after") {
    margin(.px(0))
    padding(.px(0))
    boxSizing(.borderBox)
  }
  selector("body") {
    margin(.px(0), .auto)
    backgroundColor(.hex(0x111))
    fontFamily("'Titillium Web', sans-serif")
  }
}

let page = document {
  html {
    head {
      title("Hello, Swep!")
      plainStyle
    }
    body {
      h1("📄 swift-web-page (swep)")
      hr()
        .width(50%)
        .minWidth(.px(720))
        .color(.hex(0x323232))
      p {
        strong("Swep ")
        text("is a Swift DSL for writing type-safe HTML/CSS in declarative way.")
      }
      p {
        text("With ")
        strong("Swep:")
      }
      ul {
        li("You can write html documents along with css")
          .fontWeight(.bolder)
        li("Bringing all swift-language features out of the box")
        #if swift(>=5.1)
          for version in 1...4 {
            if version != 2 {
              li("supporting swift v5.\(version)")
            }
          }
        #else
          li("Unfortunately this library is built using @resultBuilder which is available in swift v5.1 and higher 😢")
        #endif
      }
      blockquote("Enjoy! ✌️😁")
    }
  }
}

检查文档的类型..

type(of: page.content)
Html<Tuple<(Head<Tuple<(Title<Text>, Style<Stylesheet>)>>, Body<Tuple<(H1<Text>, Hr, P<Tuple<(Strong<Text>, Text)>>, P<Tuple<(Text, Strong<Text>)>>, Ul<Tuple<(Li<Text>, Li<Text>, Array<Optional<Li<Text>>>)>>, Blockquote<Text>)>>)>>

渲染输出文档..

let renderMode: RenderMode = .release(.pretty(.spaces(2)))
page.render(renderMode)
<!DOCTYPE html>
<html>
  <head>
    <title>
      Hello, Swep!
    </title>
    <style>
      @import https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap;
      *, *::before, *::after {
        margin: 0px;
        padding: 0px;
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
      }
      body {
        margin: 0px auto;
        background-color: #111;
        font-family: 'Titillium Web', sans-serif;
      }
    </style>
  </head>
  <body>
    <h1>
      📄 swift-web-page (swep)
    </h1>
    <hr style="width:50%;
               min-width:720px;
               color:#323232">
    <p>
      <strong>
        Swep 
      </strong>
      is a Swift DSL for writing type-safe HTML/CSS in declarative way.
    </p>
    <p>
      With 
      <strong>
        Swep:
      </strong>
    </p>
    <ul>
      <li style="font-weight:bolder">
        You can write html documents along with css
      </li>
      <li>
        Bringing all swift-language features out of the box
      </li>
      <li>
        supporting swift v5.1
      </li>
      <li>
        supporting swift v5.3
      </li>
      <li>
        supporting swift v5.4
      </li>
    </ul>
    <blockquote>
      Enjoy! ✌️😁
    </blockquote>
  </body>
</html>

安装

Swift Package Manager (SPM)

如果您想在使用 SPM 的项目中使用 swift-web-page,只需将 dependencies 子句添加到您的 Package.swift

dependencies: [
  .package(url: "https://github.com/alja7dali/swift-web-page.git", from: "0.0.1")
]

从那里,您可以将 Swep 添加为目标依赖项。

let Swep: Target.Dependency = .product(name: "Swep", package: "swift-web-page")
...
targets: [
  .target(name: "yourProject", dependencies: [Swep]),
]

许可证

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