Reparse - Swift 中超强的 HTML 服务器端模板引擎 (实验性)

三大核心概念

  1. 超强且灵活的语法
  2. HTML 模板 (而非文本模板)
  3. 模板被编译成 Swift 代码

示例

以下是在捆绑的 Example 项目中使用的 Reparse 语法示例 (为了简洁,删除了一些行)

<r-require name="context" type="[String]" label="superheroes" />
<r-extend name="base" />
<r-extend name="body" />

<r-eval line='req.logger.info("Index Debug Message")' />

<main>
    <h1>
        <r-set name="class" value=" red" append r-if="context.isEmpty" />
        Hello
        <r-include name="components.world" r-if="!context.isEmpty">
            Ultra Heroes!
        </r-include>
        <r-block r-else> World?</r-block>
    </h1>
    <ol>
        <li r-for-every="context" r-with-item r-with-index>
            <p>
                <r-include name="components.hello-me">\(item)</r-include>
            </p>
            <p>Index: \(index)</p>
        </li>
        <li r-else>No more heroes...</li>
    </ol>

    <p><r-value of="req.url.string" /> or \(req.url.string)</p>
</main>

<title r-add-to-slot="head">Hero List</title>

以下是手动构建示例页面的默认命令:swift run reparse ./Resources/Pages ./Sources/Example --parameters req:Request --imports Vapor

然后使用以下命令运行 Example 项目:swift run ReparseExample

如何使用

安装

如果您想在自己的项目中使用它,首先,像这样将其添加到您的 package 依赖项中

.package(url: "https://github.com/RussBaz/experimental-reparse-html.git", from: "0.0.19"),

然后像这样将 ReparseRuntime 作为依赖项添加到您的 target 中

.product(name: "ReparseRuntime", package: "experimental-reparse-html"),

作为独立工具

要将其用作独立工具,您需要 git clone 此仓库,然后像这样编译该工具:swift build -c release。然后将构建的工具从 build 文件夹复制到您想要的任何位置并运行它。否则,您可以只需从该项目文件夹内部调用 swift run reparse,并提供有效的参数和选项。

这是该工具的帮助页面

USAGE: reparse <location> <destination> [--file-name <file-name>] [--file-extension <file-extension>] [--enum-name <enum-name>] [--imports <imports> ...] [--parameters <parameters> ...] [--protocols <protocols> ...] [--dry-run]

ARGUMENTS:
  <location>              The target data folder location.
  <destination>           The destination folder for the output file.

OPTIONS:
  --file-name <file-name> Output file name (default: pages.swift)
  --file-extension <file-extension>
                          The file extension to be searched for (default: html)
  --enum-name <enum-name> The name of the generated enum (default: Pages)
  --imports <imports>     List of global imports
  --parameters <parameters>
                          List of shared parameters (parameters to be added to
                          every 'include' function) in a form of
                          '[?][label:]name:type[=default]' where the optional
                          parts are in square brackets. The question mark at the
                          beginning indicates that the parameter will be
                          overriden by a local requirement if it is present.
  --protocols <protocols> A list of protocols to apply to enums with render
                          functions in them in a form of
                          'name[:associatedName:associatedType]' where the
                          optional parts are in square brackets. Optional part
                          can be repeated any number of times.
  --dry-run               Write the output to the console instead of file
  -h, --help              Show help information.

命令插件

您还可以从终端将其作为命令插件运行。您只需按照安装说明进行操作即可使其自动可用。

列出可用插件

swift package plugin --list

要使用自动 Vapor 设置,请使用

swift package plugin reparse --preset vapor

注意:每个基于 Vapor 的预设都会将 'req: Request' 作为第一个参数添加到生成的函数中。

或者如果您正在使用 VaporHX

swift package plugin reparse --preset vaporhx

注意:如果要将其与 VaporHX 一起使用,那么您只能 'require' context 变量,因为否则生成的代码将不再符合 HXTemplateable 协议,这是自动使用生成的模板所必需的。

除了来自 Vapor 预设的 'req: Request' 之外,它还添加了 'isPage: Bool' 和 'context: Context' 参数。当模板预期返回整个页面时,'isPage' 为 true;当预期返回页面片段(就地更新)时,'isPage' 为 false。然后 'Context' 类型默认为 EmptyContext 结构体,由 VaporHX 提供。但是,如果您使用 'r-require' 标签指定 'context',那么它将被替换为您自己的类型,而不会破坏与 VaporHX 的兼容性。

如果您不使用任何预设,那么所有相对路由都将指向项目根文件夹。

您可以传递的所有选项都是相同的,除了 location 和 destination 参数被以下选项替换

--source 用于指定与项目根文件夹不同的根文件夹

--target 用于从 package targets 中选择一个 target 作为生成文件的目标

--destination 作为路径组件列表,用于为 target 提供相对于源根文件夹(或如果未选择则为项目根)的不同输出文件夹

语法

所有特殊属性和标签都将在编译时删除,并且不得出现在 render 函数的输出中。如果它们出现,那就是一个 bug。

字符串插值

您可以使用标准的 swift 语法进行字符串插值 \(expression),将任何 swift 表达式插入到 html 模板的文本部分 - 属性值内部和标签之间的文本。

控制属性

有几种类型的控制属性,可以分为 3 组

条件语句

大多数 html 标签都可以配备条件控制属性之一

如果条件(必须是有效的 Swift 表达式)满足,则渲染 html 标签及其内容。

如果条件满足,则一个名为 previousUnnamedIfTaken 的特殊变量将被设置为 true。否则,它将被设置为 false。

可选地,您可以使用附加属性将条件的结果保存到不同的变量

循环

插槽

控制标签

它是如何工作的?

它分两个阶段运行。编译阶段和运行时阶段。

编译阶段

  1. 所有模板都被发现,它们的名称和完整路径被记录下来。名称从根文件夹的相对路径和文件名派生而来。
  2. 对于每个模板,分词器将字符流转换为 token 流。
  3. 现在将 token 解析为抽象语法树。
  4. 语法树被扁平化并转译成一系列文件构建器的命令。文件之间和与其他元数据的引用被记录下来。
  5. 为每个模板解析函数签名
  6. 每个模板的所有单独命令序列都组合成一个,并被执行。
  7. 最终字符串被保存到指定位置。

运行时阶段

渲染模板

  1. 调用者将参数传递给 render 函数。
  2. render 函数将它们传递给指定模板的 include 函数。
  3. include 函数返回带有 render 方法的行存储对象。
  4. 然后调用 render 方法,它将使用空的外部插槽调用其 resolve 方法。
  5. 返回的文本常量序列然后被合并成一个字符串并返回给调用者。

解析模板

  1. 调用模板将调用一个自动生成的函数,该函数通过调用非常有限数量的命令为每个模板构建一个简单的内部模型。(在调用 include 方法时完成)
  2. 插槽被传递到模板中,默认插槽被解析。传递到模板中的插槽称为外部插槽。(在调用 resolve 方法时完成)
  3. 所有插槽声明站点(包括 include 命令内部)都会递归地替换为外部插槽的内容(如果找到)并标记为删除,或者替换为默认值。如果两者都未找到,则不执行任何操作。允许嵌套声明,但如果父级“消费”了插槽,则它将不再出现在嵌套声明中。同级声明没有此限制。
  4. 内部插槽被计算 - 要传递给包含的模板的插槽。所有影响插槽的命令都被解析。
  5. 为每个 include 命令计算默认内部插槽,并将其传递到指定的模板中。允许嵌套包含。
  6. 解析后的内部模板被合并到当前模板中
  7. 现在仅由文本常量命令组成的内容被返回给调用者。