├── .dockerignore
├── .github
├── FUNDING.yml
├── CODEOWNERS
├── workflows
│ ├── validate.yml
│ ├── nightly.yml
│ ├── api-breakage.yml
│ ├── verify-documentation.yml
│ └── ci.yml
└── dependabot.yml
├── .spi.yml
├── .editorconfig
├── .gitignore
├── Dockerfile
├── documentation
├── Pragmas.md
├── Lambdas.md
├── Template Inheritance.md
├── Transforms.md
└── Mustache Syntax.md
├── Package.swift
├── Sources
└── Mustache
│ ├── NSLock+Backport.swift
│ ├── AnyOptional.swift
│ ├── Parent.swift
│ ├── Mirror.swift
│ ├── Template+FileSystem.swift
│ ├── String.swift
│ ├── CustomRenderable.swift
│ ├── Lambda.swift
│ ├── SequenceContext.swift
│ ├── Library+FileSystem.swift
│ ├── Sequence.swift
│ ├── Deprecations.swift
│ ├── ContentType.swift
│ ├── Template.swift
│ ├── Library.swift
│ ├── Context.swift
│ ├── Transform.swift
│ ├── Template+Render.swift
│ ├── Parser.swift
│ └── Template+Parser.swift
├── CONTRIBUTING.md
├── README.md
├── .swift-format
├── CODE_OF_CONDUCT.md
├── Tests
└── MustacheTests
│ ├── ErrorTests.swift
│ ├── TemplateParserTests.swift
│ ├── LibraryTests.swift
│ ├── PartialTests.swift
│ ├── TransformTests.swift
│ ├── SpecTests.swift
│ └── TemplateRendererTests.swift
├── scripts
└── validate.sh
└── LICENSE
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build
2 | .git
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: adam-fowler
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @adam-fowler @Joannis
2 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | external_links:
3 | documentation: "https://docs.hummingbird.codes/2.0/documentation/mustache"
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build/
3 | .swiftpm/
4 | .vscode/
5 | .index-build/
6 | .devcontainer/
7 | /Packages
8 | /*.xcodeproj
9 | xcuserdata/
10 | Package.resolved
11 | /public
12 | /docs
13 | .benchmarkBaselines
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: Validity Check
2 |
3 | on:
4 | pull_request:
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}-validate
7 | cancel-in-progress: true
8 |
9 | jobs:
10 | validate:
11 | runs-on: ubuntu-latest
12 | timeout-minutes: 15
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v6
16 | with:
17 | fetch-depth: 1
18 | - name: run script
19 | run: ./scripts/validate.sh
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # ================================
2 | # Build image
3 | # ================================
4 | FROM swift:6.0 as build
5 |
6 | WORKDIR /build
7 |
8 | # First just resolve dependencies.
9 | # This creates a cached layer that can be reused
10 | # as long as your Package.swift/Package.resolved
11 | # files do not change.
12 | COPY ./Package.* ./
13 | RUN swift package resolve
14 |
15 | # Copy entire repo into container
16 | COPY . .
17 |
18 | RUN swift test --sanitize=thread
19 |
--------------------------------------------------------------------------------
/.github/workflows/nightly.yml:
--------------------------------------------------------------------------------
1 | name: Swift nightly build
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | linux:
8 | runs-on: ubuntu-latest
9 | timeout-minutes: 15
10 | strategy:
11 | matrix:
12 | image: ['nightly-focal', 'nightly-jammy', 'nightly-amazonlinux2']
13 | container:
14 | image: swiftlang/swift:${{ matrix.image }}
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v6
18 | - name: Test
19 | run: |
20 | swift test
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | ignore:
8 | - dependency-name: "codecov/codecov-action"
9 | update-types: ["version-update:semver-major"]
10 | groups:
11 | dependencies:
12 | patterns:
13 | - "*"
14 | - package-ecosystem: "swift"
15 | directory: "/"
16 | schedule:
17 | interval: "daily"
18 | open-pull-requests-limit: 5
19 | allow:
20 | - dependency-type: all
21 | groups:
22 | all-dependencies:
23 | patterns:
24 | - "*"
25 |
--------------------------------------------------------------------------------
/documentation/Pragmas.md:
--------------------------------------------------------------------------------
1 | # Pragmas/Configuration variables
2 |
3 | The syntax `{{% var: value}}` can be used to set template rendering configuration variables specific to swift-mustache. The only variable you can set at the moment is `CONTENT_TYPE`. This can be set to either to `HTML` or `TEXT` and defines how variables are escaped. A content type of `TEXT` means no variables are escaped and a content type of `HTML` will do HTML escaping of the rendered text. The content type defaults to `HTML`.
4 |
5 | Given input object "<>", template `{{%CONTENT_TYPE: HTML}}{{.}}` will render as `<>` and `{{%CONTENT_TYPE: TEXT}}{{.}}` will render as `<>`.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swift-mustache",
8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
9 | products: [
10 | .library(name: "Mustache", targets: ["Mustache"])
11 | ],
12 | dependencies: [],
13 | targets: [
14 | .target(name: "Mustache", dependencies: []),
15 | .testTarget(name: "MustacheTests", dependencies: ["Mustache"]),
16 | ],
17 | swiftLanguageVersions: [.v5, .version("6")]
18 | )
19 |
--------------------------------------------------------------------------------
/.github/workflows/api-breakage.yml:
--------------------------------------------------------------------------------
1 | name: API breaking changes
2 |
3 | on:
4 | pull_request:
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}-apibreakage
7 | cancel-in-progress: true
8 |
9 | jobs:
10 | linux:
11 | runs-on: ubuntu-latest
12 | timeout-minutes: 15
13 | container:
14 | image: swift:latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v6
18 | with:
19 | fetch-depth: 0
20 | # https://github.com/actions/checkout/issues/766
21 | - name: Mark the workspace as safe
22 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
23 | - name: API breaking changes
24 | run: |
25 | swift package diagnose-api-breaking-changes origin/${GITHUB_BASE_REF}
26 |
--------------------------------------------------------------------------------
/Sources/Mustache/NSLock+Backport.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | #if compiler(<6.0)
16 | import Foundation
17 |
18 | extension NSLock {
19 | func withLock(_ operation: () throws -> Value) rethrows -> Value {
20 | self.lock()
21 | defer {
22 | self.unlock()
23 | }
24 | return try operation()
25 | }
26 | }
27 | #endif
28 |
--------------------------------------------------------------------------------
/Sources/Mustache/AnyOptional.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Internal protocol to allow testing if a variable contains a wrapped value
16 | protocol AnyOptional {
17 | var anyWrapped: Any? { get }
18 | }
19 |
20 | /// Internal extension to allow testing if a variable contains a wrapped value
21 | extension Optional: AnyOptional {
22 | var anyWrapped: Any? { self }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/verify-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Verify Documentation
2 |
3 | on:
4 | pull_request:
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}-verifydocs
7 | cancel-in-progress: true
8 |
9 | jobs:
10 | linux:
11 | runs-on: ubuntu-latest
12 | timeout-minutes: 15
13 | container:
14 | image: swift:latest
15 | steps:
16 | - name: Install rsync 📚
17 | run: |
18 | apt-get update && apt-get install -y rsync bc
19 | - name: Checkout
20 | uses: actions/checkout@v6
21 | with:
22 | fetch-depth: 0
23 | path: "package"
24 | - name: Checkout
25 | uses: actions/checkout@v6
26 | with:
27 | repository: "hummingbird-project/hummingbird-docs"
28 | fetch-depth: 0
29 | path: "documentation"
30 | - name: Verify
31 | run: |
32 | cd documentation
33 | swift package edit ${GITHUB_REPOSITORY#*/} --path ../package
34 | ./scripts/build-docc.sh -e
35 |
36 |
--------------------------------------------------------------------------------
/Sources/Mustache/Parent.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Protocol for object that has a custom method for accessing their children, instead
16 | /// of using Mirror
17 | public protocol MustacheParent {
18 | func child(named: String) -> Any?
19 | }
20 |
21 | /// Extend dictionary where the key is a string so that it uses the key values to access
22 | /// it values
23 | extension Dictionary: MustacheParent where Key == String {
24 | public func child(named: String) -> Any? { self[named] }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Mustache/Mirror.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | extension Mirror {
16 | /// Return value from Mirror given name
17 | func getValue(forKey key: String) -> Any? {
18 | guard let matched = children.filter({ $0.label == key }).first else {
19 | return nil
20 | }
21 | return unwrapOptional(matched.value)
22 | }
23 | }
24 |
25 | /// Return object and if it is an Optional return object Optional holds
26 | private func unwrapOptional(_ object: Any) -> Any? {
27 | let mirror = Mirror(reflecting: object)
28 | guard mirror.displayStyle == .optional else { return object }
29 | guard let first = mirror.children.first else { return nil }
30 | return first.value
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Mustache/Template+FileSystem.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | extension MustacheTemplate {
18 | /// Internal function to load a template from a file
19 | /// - Parameters
20 | /// - string: Template text
21 | /// - filename: File template was loaded from
22 | /// - Throws: MustacheTemplate.Error
23 | init?(filename: String) throws {
24 | let fs = FileManager()
25 | guard let data = fs.contents(atPath: filename) else { return nil }
26 | let string = String(decoding: data, as: Unicode.UTF8.self)
27 | let template = try Self.parse(string)
28 | self.tokens = template.tokens
29 | self.text = string
30 | self.filename = filename
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/documentation/Lambdas.md:
--------------------------------------------------------------------------------
1 | # Lambda Implementation
2 |
3 | The library doesn't provide a lambda implementation but it does provide something akin to the lambda feature.
4 |
5 | Add a `MustacheLambda` to the object you want to be rendered and it can be used in a similar way to lambdas are used in Mustache. When you create a section referencing the lambda the contents of the section are passed as a template along with the current object to the lamdba function. This is slightly different from the standard implementation where the unprocessed text is passed to the lambda.
6 |
7 | Given the object `person` defined below
8 | ```swift
9 | struct Person {
10 | let name: String
11 | let wrapped: MustacheLambda
12 | }
13 | let person = Person(
14 | name: "John",
15 | wrapped: MustacheLambda { object, template in
16 | return "\(template.render(object))"
17 | }
18 | )
19 |
20 | ```
21 | and the following mustache template
22 | ```swift
23 | let mustache = "{{#wrapped}}{{name}} is awesome.{{/wrapped}}"
24 | let template = try MustacheTemplate(string: mustache)
25 | ```
26 | Then `template.render(person)` will output
27 | ```
28 | John is awesome.
29 | ```
30 | In this example the template constructed from the contents of the `wrapped` section of the mustache is passed to my `wrapped` function inside the `Person` type.
31 |
--------------------------------------------------------------------------------
/Sources/Mustache/String.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | extension String {
16 | private static let htmlEscapedCharacters: [Character: String] = [
17 | "<": "<",
18 | ">": ">",
19 | "&": "&",
20 | "\"": """,
21 | "'": "'",
22 | ]
23 | /// HTML escape string. Replace '<', '>' and '&' with HTML escaped versions
24 | func htmlEscape() -> String {
25 | var newString = ""
26 | newString.reserveCapacity(count)
27 | // currently doing this by going through each character could speed
28 | // this us by treating as an array of UInt8's
29 | for c in self {
30 | if let replacement = Self.htmlEscapedCharacters[c] {
31 | newString += replacement
32 | } else {
33 | newString.append(c)
34 | }
35 | }
36 | return newString
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/Mustache/CustomRenderable.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | /// Allow object to override standard hummingbird type rendering which uses
18 | /// `String(describing)`.
19 | public protocol MustacheCustomRenderable {
20 | /// Custom rendered version of object
21 | var renderText: String { get }
22 | /// Whether the object is a null object. Used when scoping sections
23 | var isNull: Bool { get }
24 | }
25 |
26 | extension MustacheCustomRenderable {
27 | /// default version returning the standard rendering
28 | var renderText: String { String(describing: self) }
29 | /// default version returning false
30 | var isNull: Bool { false }
31 | }
32 |
33 | /// Extend NSNull to conform to `MustacheCustomRenderable` to avoid outputting `` and returning
34 | /// a valid response for `isNull`
35 | extension NSNull: MustacheCustomRenderable {
36 | public var renderText: String { "" }
37 | public var isNull: Bool { true }
38 | }
39 |
--------------------------------------------------------------------------------
/documentation/Template Inheritance.md:
--------------------------------------------------------------------------------
1 | # Template Inheritance
2 |
3 | Template inheritance is not part of the Mustache spec yet but it is a commonly implemented feature. Template inheritance allows you to override elements of an included partial. It allows you to create a base page template and override elements of it with your page content. A partial that includes overriding elements is indicated with a `{{`. This is a section tag so needs a ending tag as well. Inside the section the tagged sections to override are added using the syntax `{{$tag}}contents{{/tag}}`. If your template and partial were as follows
4 | ```
5 | {{! mypage.mustache }}
6 | {{My page title{{/head}}
8 | {{$body}}Hello world{{/body}}
9 | {{/base}}
10 | ```
11 | ```
12 | {{! base.mustache }}
13 |
14 |
15 | {{$head}}{{/head}}
16 |
17 |
18 | {{$body}}Default text{{/body}}
19 |
20 |
21 | ```
22 | You would get the following output when rendering `mypage.mustache`.
23 | ```
24 |
25 |
26 | My page title
27 |
28 |
29 | Hello world
30 |
31 | ```
32 | Note the `{{$head}}` section in `base.mustache` is replaced with the `{{$head}}` section included inside the `{{\(string)"
27 | /// }))
28 | /// let mustache = "{{#wrapped}}{{name}} is awesome.{{/wrapped}}"
29 | /// let template = try MustacheTemplate(string: mustache)
30 | /// let output = template.render(willy)
31 | /// print(output) // Willy is awesome
32 | /// ```
33 | ///
34 | public struct MustacheLambda {
35 | /// lambda callback
36 | public typealias Callback = (String) -> Any?
37 |
38 | let callback: Callback
39 |
40 | /// Initialize `MustacheLambda`
41 | /// - Parameter cb: function to be called by lambda
42 | public init(_ cb: @escaping Callback) {
43 | self.callback = cb
44 | }
45 |
46 | /// Initialize `MustacheLambda`
47 | /// - Parameter cb: function to be called by lambda
48 | public init(_ cb: @escaping () -> Any?) {
49 | self.callback = { _ in cb() }
50 | }
51 |
52 | internal func callAsFunction(_ s: String) -> Any? {
53 | self.callback(s)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/Mustache/SequenceContext.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Context that current object inside a sequence is being rendered in. Only relevant when rendering a sequence
16 | struct MustacheSequenceContext: MustacheTransformable {
17 | var first: Bool
18 | var last: Bool
19 | var index: Int
20 |
21 | init(first: Bool = false, last: Bool = false) {
22 | self.first = first
23 | self.last = last
24 | self.index = 0
25 | }
26 |
27 | /// Transform `MustacheContext`. These are available when processing elements
28 | /// of a sequence.
29 | ///
30 | /// Format your mustache as follows to accept them. They look like a function without any arguments
31 | /// ```
32 | /// {{#sequence}}{{index()}}{{/sequence}}
33 | /// ```
34 | ///
35 | /// Transforms available are `first`, `last`, `index`, `even` and `odd`
36 | /// - Parameter name: transform name
37 | /// - Returns: Result
38 | func transform(_ name: String) -> Any? {
39 | switch name {
40 | case "first":
41 | return self.first
42 | case "last":
43 | return self.last
44 | case "index":
45 | return self.index
46 | case "even":
47 | return (self.index & 1) == 0
48 | case "odd":
49 | return (self.index & 1) == 1
50 | default:
51 | return nil
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Mustache/Library+FileSystem.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | extension MustacheLibrary {
18 | /// Load templates from a folder
19 | static func loadTemplates(from directory: String, withExtension extension: String = "mustache") throws -> [String: MustacheTemplate] {
20 | var directory = directory
21 | if !directory.hasSuffix("/") {
22 | directory += "/"
23 | }
24 | let extWithDot = ".\(`extension`)"
25 | let fs = FileManager()
26 | guard let enumerator = fs.enumerator(atPath: directory) else { return [:] }
27 | var templates: [String: MustacheTemplate] = [:]
28 | for case let path as String in enumerator {
29 | guard path.hasSuffix(extWithDot) else { continue }
30 | do {
31 | guard let template = try MustacheTemplate(filename: directory + path) else { continue }
32 | // drop ".mustache" from path to get name
33 | let name = String(path.dropLast(extWithDot.count))
34 | #if os(Windows)
35 | templates[name.replacingOccurrences(of: "\\", with: "/")] = template
36 | #else
37 | templates[name] = template
38 | #endif
39 | } catch let error as MustacheTemplate.ParserError {
40 | throw ParserError(filename: path, context: error.context, error: error.error)
41 | }
42 | }
43 | return templates
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift-Mustache
2 |
3 | Package for rendering Mustache templates. Mustache is a "logic-less" templating language commonly used in web and mobile platforms. You can find out more about Mustache [here](http://mustache.github.io/mustache.5.html).
4 |
5 | ## Usage
6 |
7 | Load your templates from the filesystem
8 | ```swift
9 | import Mustache
10 | let library = MustacheLibrary("folder/my/templates/are/in")
11 | ```
12 | This will look for all the files with the extension ".mustache" in the specified folder and subfolders and attempt to load them. Each file is registed with the name of the file (with subfolder, if inside a subfolder) minus the "mustache" extension.
13 |
14 | Render an object with a template
15 | ```swift
16 | let output = library.render(object, withTemplate: "myTemplate")
17 | ```
18 | `Swift-Mustache` treats an object as a set of key/value pairs when rendering and will render both dictionaries and objects via `Mirror` reflection. Find out more on how Mustache renders objects [here](https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachesyntax).
19 |
20 | ## Support
21 |
22 | Swift-Mustache supports all standard Mustache tags and is fully compliant with the Mustache [spec](https://github.com/mustache/spec) with the exception of the Lambda support.
23 |
24 | ## Additional features
25 |
26 | Swift-Mustache includes some features that are specific to its implementation. Please follow the links below to find out more.
27 |
28 | - [Lambda Implementation](https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachefeatures#Lambdas)
29 | - [Transforms](https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachefeatures#Transforms)
30 | - [Template Inheritance](https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachefeatures#Template-inheritance-and-parents)
31 | - [Pragmas](https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachefeatures#PragmasConfiguration-variables)
32 |
33 | ## Documentation
34 |
35 | Reference documentation for swift-mustache can be found [here](https://docs.hummingbird.codes/2.0/documentation/mustache)
36 |
--------------------------------------------------------------------------------
/Sources/Mustache/Sequence.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Protocol for objects that can be rendered as a sequence in Mustache
16 | public protocol MustacheSequence: Sequence {}
17 |
18 | extension MustacheSequence {
19 | /// Render section using template
20 | func renderSection(with template: MustacheTemplate, context: MustacheContext) -> String {
21 | var string = ""
22 | var sequenceContext = MustacheSequenceContext(first: true)
23 |
24 | var iterator = makeIterator()
25 | guard var currentObject = iterator.next() else { return "" }
26 |
27 | while let object = iterator.next() {
28 | string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))
29 | currentObject = object
30 | sequenceContext.first = false
31 | sequenceContext.index += 1
32 | }
33 |
34 | sequenceContext.last = true
35 | string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))
36 |
37 | return string
38 | }
39 |
40 | /// Render inverted section using template
41 | func renderInvertedSection(with template: MustacheTemplate, context: MustacheContext) -> String {
42 | var iterator = makeIterator()
43 | if iterator.next() == nil {
44 | return template.render(context: context.withObject(self))
45 | }
46 | return ""
47 | }
48 | }
49 |
50 | extension Array: MustacheSequence {}
51 | extension Set: MustacheSequence {}
52 | extension ReversedCollection: MustacheSequence {}
53 |
--------------------------------------------------------------------------------
/Sources/Mustache/Deprecations.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | // Below is a list of unavailable symbols with the "HB" prefix. These are available
16 | // temporarily to ease transition from the old symbols that included the "HB"
17 | // prefix to the new ones.
18 |
19 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheContentType")
20 | public typealias HBMustacheContentType = MustacheContentType
21 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheContentTypes")
22 | public typealias HBMustacheContentTypes = MustacheContentTypes
23 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheCustomRenderable")
24 | public typealias HBMustacheCustomRenderable = MustacheCustomRenderable
25 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheLambda")
26 | public typealias HBMustacheLambda = MustacheLambda
27 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheLibrary")
28 | public typealias HBMustacheLibrary = MustacheLibrary
29 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheParent")
30 | public typealias HBMustacheParent = MustacheParent
31 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheParserContext")
32 | public typealias HBMustacheParserContext = MustacheParserContext
33 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheTemplate")
34 | public typealias HBMustacheTemplate = MustacheTemplate
35 | @_documentation(visibility: internal) @available(*, unavailable, renamed: "MustacheTransformable")
36 | public typealias HBMustacheTransformable = MustacheTransformable
37 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '**.swift'
9 | - '**.yml'
10 | pull_request:
11 | workflow_dispatch:
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}-ci
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | macOS:
18 | runs-on: macos-14
19 | timeout-minutes: 15
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v6
23 | - name: SPM tests
24 | run: swift test --enable-code-coverage
25 | - name: Convert coverage files
26 | run: |
27 | xcrun llvm-cov export -format "lcov" \
28 | .build/debug/swift-mustachePackageTests.xctest/Contents/MacOs/swift-mustachePackageTests \
29 | -ignore-filename-regex="\/Tests\/" \
30 | -ignore-filename-regex="\/Benchmarks\/" \
31 | -instr-profile=.build/debug/codecov/default.profdata > info.lcov
32 | - name: Upload to codecov.io
33 | uses: codecov/codecov-action@v4
34 | with:
35 | files: info.lcov
36 | token: ${{ secrets.CODECOV_TOKEN }}
37 | linux:
38 | runs-on: ubuntu-latest
39 | timeout-minutes: 15
40 | strategy:
41 | matrix:
42 | image: ["swift:6.0", "swift:6.1", "swift:6.2"]
43 | container:
44 | image: ${{ matrix.image }}
45 | steps:
46 | - name: Checkout
47 | uses: actions/checkout@v6
48 | - name: Test
49 | run: |
50 | swift test --enable-code-coverage
51 | - name: Convert coverage files
52 | run: |
53 | llvm-cov export -format="lcov" \
54 | .build/debug/swift-mustachePackageTests.xctest \
55 | -ignore-filename-regex="\/Tests\/" \
56 | -ignore-filename-regex="\/Benchmarks\/" \
57 | -instr-profile .build/debug/codecov/default.profdata > info.lcov
58 | - name: Upload to codecov.io
59 | uses: codecov/codecov-action@v4
60 | with:
61 | files: info.lcov
62 | token: ${{ secrets.CODECOV_TOKEN }}
63 | windows:
64 | runs-on: windows-latest
65 | strategy:
66 | matrix:
67 | swift-version: ["6.1"]
68 | steps:
69 | - uses: compnerd/gha-setup-swift@main
70 | with:
71 | branch: swift-${{ matrix.swift-version }}-release
72 | tag: ${{ matrix.swift-version }}-RELEASE
73 | - uses: actions/checkout@v6
74 | - run: swift test
75 |
--------------------------------------------------------------------------------
/Sources/Mustache/ContentType.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | /// Protocol for content types
18 | public protocol MustacheContentType: Sendable {
19 | /// escape text for this content type eg for HTML replace "<" with "<"
20 | func escapeText(_ text: String) -> String
21 | }
22 |
23 | /// Text content type where no character is escaped
24 | struct TextContentType: MustacheContentType {
25 | func escapeText(_ text: String) -> String {
26 | text
27 | }
28 | }
29 |
30 | /// HTML content where text is escaped for HTML output
31 | struct HTMLContentType: MustacheContentType {
32 | func escapeText(_ text: String) -> String {
33 | text.htmlEscape()
34 | }
35 | }
36 |
37 | /// Map of strings to content types.
38 | ///
39 | /// The string is read from the "CONTENT_TYPE" pragma `{{% CONTENT_TYPE: type}}`. Replace type with
40 | /// the content type required. The default available types are `TEXT` and `HTML`. You can register your own
41 | /// with `MustacheContentTypes.register`.
42 | public enum MustacheContentTypes {
43 |
44 | private static let lock = NSLock()
45 |
46 | static func get(_ name: String) -> MustacheContentType? {
47 | lock.withLock {
48 | self.types[name]
49 | }
50 | }
51 |
52 | /// Register new content type
53 | /// - Parameters:
54 | /// - contentType: Content type
55 | /// - name: String to identify it
56 | public static func register(_ contentType: MustacheContentType, named name: String) {
57 | lock.withLock {
58 | self.types[name] = contentType
59 | }
60 | }
61 |
62 | private static let _types: [String: MustacheContentType] = [
63 | "HTML": HTMLContentType(),
64 | "TEXT": TextContentType(),
65 | ]
66 |
67 | #if compiler(>=6.0)
68 | nonisolated(unsafe) static var types: [String: MustacheContentType] = _types
69 | #else
70 | static var types: [String: MustacheContentType] = _types
71 | #endif
72 | }
73 |
--------------------------------------------------------------------------------
/.swift-format:
--------------------------------------------------------------------------------
1 | {
2 | "version" : 1,
3 | "indentation" : {
4 | "spaces" : 4
5 | },
6 | "tabWidth" : 4,
7 | "fileScopedDeclarationPrivacy" : {
8 | "accessLevel" : "private"
9 | },
10 | "spacesAroundRangeFormationOperators" : false,
11 | "indentConditionalCompilationBlocks" : false,
12 | "indentSwitchCaseLabels" : false,
13 | "lineBreakAroundMultilineExpressionChainComponents" : false,
14 | "lineBreakBeforeControlFlowKeywords" : false,
15 | "lineBreakBeforeEachArgument" : true,
16 | "lineBreakBeforeEachGenericRequirement" : true,
17 | "lineLength" : 150,
18 | "maximumBlankLines" : 1,
19 | "respectsExistingLineBreaks" : true,
20 | "prioritizeKeepingFunctionOutputTogether" : true,
21 | "multiElementCollectionTrailingCommas" : true,
22 | "rules" : {
23 | "AllPublicDeclarationsHaveDocumentation" : false,
24 | "AlwaysUseLiteralForEmptyCollectionInit" : false,
25 | "AlwaysUseLowerCamelCase" : false,
26 | "AmbiguousTrailingClosureOverload" : true,
27 | "BeginDocumentationCommentWithOneLineSummary" : false,
28 | "DoNotUseSemicolons" : true,
29 | "DontRepeatTypeInStaticProperties" : true,
30 | "FileScopedDeclarationPrivacy" : true,
31 | "FullyIndirectEnum" : true,
32 | "GroupNumericLiterals" : true,
33 | "IdentifiersMustBeASCII" : true,
34 | "NeverForceUnwrap" : false,
35 | "NeverUseForceTry" : false,
36 | "NeverUseImplicitlyUnwrappedOptionals" : false,
37 | "NoAccessLevelOnExtensionDeclaration" : true,
38 | "NoAssignmentInExpressions" : true,
39 | "NoBlockComments" : true,
40 | "NoCasesWithOnlyFallthrough" : true,
41 | "NoEmptyTrailingClosureParentheses" : true,
42 | "NoLabelsInCasePatterns" : true,
43 | "NoLeadingUnderscores" : false,
44 | "NoParensAroundConditions" : true,
45 | "NoVoidReturnOnFunctionSignature" : true,
46 | "OmitExplicitReturns" : true,
47 | "OneCasePerLine" : true,
48 | "OneVariableDeclarationPerLine" : true,
49 | "OnlyOneTrailingClosureArgument" : true,
50 | "OrderedImports" : true,
51 | "ReplaceForEachWithForLoop" : true,
52 | "ReturnVoidInsteadOfEmptyTuple" : true,
53 | "UseEarlyExits" : false,
54 | "UseExplicitNilCheckInConditions" : false,
55 | "UseLetInEveryBoundCaseVariable" : false,
56 | "UseShorthandTypeNames" : true,
57 | "UseSingleLinePropertyGetter" : false,
58 | "UseSynthesizedInitializer" : false,
59 | "UseTripleSlashForDocumentationComments" : true,
60 | "UseWhereClausesInForLoops" : false,
61 | "ValidateDocumentationComments" : false
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All developers should feel welcome and encouraged to contribute to Hummingbird. Because of this we have adopted the code of conduct defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source
4 | communities, and we think it articulates our values well. The full text is copied below:
5 |
6 | ## Contributor Code of Conduct v1.3
7 |
8 | As contributors and maintainers of this project, and in the interest of
9 | fostering an open and welcoming community, we pledge to respect all people who
10 | contribute through reporting issues, posting feature requests, updating
11 | documentation, submitting pull requests or patches, and other activities.
12 |
13 | We are committed to making participation in this project a harassment-free
14 | experience for everyone, regardless of level of experience, gender, gender
15 | identity and expression, sexual orientation, disability, personal appearance,
16 | body size, race, ethnicity, age, religion, or nationality.
17 |
18 | Examples of unacceptable behavior by participants include:
19 |
20 | * The use of sexualized language or imagery
21 | * Personal attacks
22 | * Trolling or insulting/derogatory comments
23 | * Public or private harassment
24 | * Publishing other's private information, such as physical or electronic
25 | addresses, without explicit permission
26 | * Other unethical or unprofessional conduct
27 |
28 | Project maintainers have the right and responsibility to remove, edit, or
29 | reject comments, commits, code, wiki edits, issues, and other contributions
30 | that are not aligned to this Code of Conduct, or to ban temporarily or
31 | permanently any contributor for other behaviors that they deem inappropriate,
32 | threatening, offensive, or harmful.
33 |
34 | By adopting this Code of Conduct, project maintainers commit themselves to
35 | fairly and consistently applying these principles to every aspect of managing
36 | this project. Project maintainers who do not follow or enforce the Code of
37 | Conduct may be permanently removed from the project team.
38 |
39 | This Code of Conduct applies both within project spaces and in public spaces
40 | when an individual is representing the project or its community.
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
43 | reported by contacting a project maintainer at [INSERT EMAIL ADDRESS]. All
44 | complaints will be reviewed and investigated and will result in a response that
45 | is deemed necessary and appropriate to the circumstances. Maintainers are
46 | obligated to maintain confidentiality with regard to the reporter of an
47 | incident.
48 |
49 |
50 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
51 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html
52 |
53 | [homepage]: https://www.contributor-covenant.org
54 |
55 |
--------------------------------------------------------------------------------
/documentation/Transforms.md:
--------------------------------------------------------------------------------
1 | # Transforms
2 |
3 | Transforms are specific to this implementation of Mustache. They are similar to Lambdas but instead of generating rendered text they allow you to transform an object into another. Transforms are formatted as a function call inside a tag eg
4 | ```
5 | {{uppercase(string)}}
6 | ```
7 | They can be applied to variable, section and inverted section tags. If you apply them to a section or inverted section tag the transform name should be included in the end section tag as well eg
8 | ```
9 | {{#sorted(array)}}{{.}}{{/sorted(array)}}
10 | ```
11 | The library comes with a series of transforms for the Swift standard objects.
12 | - String/Substring
13 | - capitalized: Return string with first letter capitalized
14 | - lowercase: Return lowercased version of string
15 | - uppercase: Return uppercased version of string
16 | - reversed: Reverse string
17 | - Int/UInt/Int8/Int16...
18 | - equalzero: Returns if equal to zero
19 | - plusone: Add one to integer
20 | - minusone: Subtract one from integer
21 | - odd: return if integer is odd
22 | - even: return if integer is even
23 | - Array
24 | - first: Return first element of array
25 | - last: Return last element of array
26 | - count: Return number of elements in array
27 | - empty: Returns if array is empty
28 | - reversed: Reverse array
29 | - sorted: If the elements of the array are comparable sort them
30 | - Dictionary
31 | - count: Return number of elements in dictionary
32 | - empty: Returns if dictionary is empty
33 | - enumerated: Return dictionary as array of key, value pairs
34 | - sorted: If the keys are comparable return as array of key, value pairs sorted by key
35 |
36 | If a transform is applied to an object that doesn't recognise it then `nil` is returned.
37 |
38 | ## Sequence context transforms
39 |
40 | Sequence context transforms are transforms applied to the current position in the sequence. They are formatted as a function that takes no parameter eg
41 | ```
42 | {{#array}}{{.}}{{^last()}}, {{/last()}}{{/array}}
43 | ```
44 | This will render an array as a comma separated list. The inverted section of the `last()` transform ensures we don't add a comma after the last element.
45 |
46 | The following sequence context transforms are available
47 | - first: Is this the first element of the sequence
48 | - last: Is this the last element of the sequence
49 | - index: Returns the index of the element within the sequence
50 | - odd: Returns if the index of the element is odd
51 | - even: Returns if the index of the element is even
52 |
53 | ## Custom transforms
54 |
55 | You can add transforms to your own objects. Conform the object to `MustacheTransformable` and provide an implementation of the function `transform`. eg
56 | ```swift
57 | struct Object: MustacheTransformable {
58 | let either: Bool
59 | let or: Bool
60 |
61 | func transform(_ name: String) -> Any? {
62 | switch name {
63 | case "eitherOr":
64 | return either || or
65 | default:
66 | break
67 | }
68 | return nil
69 | }
70 | }
71 | ```
72 | When we render an instance of this object with `either` or `or` set to true using the following template it will render "Success".
73 | ```
74 | {{#eitherOr(object)}}Success{{/eitherOr(object)}}
75 | ```
76 | With this we have got around the fact it is not possible to do logical OR statements in Mustache.
77 |
--------------------------------------------------------------------------------
/Sources/Mustache/Template.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Class holding Mustache template
16 | public struct MustacheTemplate: Sendable, CustomStringConvertible {
17 | /// Initialize template
18 | /// - Parameter string: Template text
19 | /// - Throws: MustacheTemplate.Error
20 | public init(string: String) throws {
21 | let template = try Self.parse(string)
22 | self.tokens = template.tokens
23 | self.text = string
24 | self.filename = nil
25 | }
26 |
27 | /// Render object using this template
28 | /// - Parameters
29 | /// - object: Object to render
30 | /// - library: library template uses to access partials
31 | /// - Returns: Rendered text
32 | public func render(_ object: Any, library: MustacheLibrary? = nil) -> String {
33 | self.render(context: .init(object, library: library))
34 | }
35 |
36 | /// Render object using this template
37 | /// - Parameters
38 | /// - object: Object to render
39 | /// - library: library template uses to access partials
40 | /// - reload: Should I reload this template when rendering. This is only available in debug builds
41 | /// - Returns: Rendered text
42 | public func render(_ object: Any, library: MustacheLibrary? = nil, reload: Bool) -> String {
43 | #if DEBUG
44 | if reload {
45 | guard let filename else {
46 | preconditionFailure("Can only use reload if template was generated from a file")
47 | }
48 | do {
49 | guard let template = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
50 | return template.render(context: .init(object, library: library, reloadPartials: reload))
51 | } catch {
52 | return "\(error)"
53 | }
54 | }
55 | #endif
56 | return self.render(context: .init(object, library: library))
57 | }
58 |
59 | internal init(_ tokens: [Token], text: String) {
60 | self.tokens = tokens
61 | self.filename = nil
62 | self.text = text
63 | }
64 |
65 | public var description: String { self.text }
66 |
67 | enum Token: Sendable /* , CustomStringConvertible */ {
68 | case text(String)
69 | case variable(name: String, transforms: [String] = [])
70 | case unescapedVariable(name: String, transforms: [String] = [])
71 | case section(name: String, transforms: [String] = [], template: MustacheTemplate)
72 | case invertedSection(name: String, transforms: [String] = [], template: MustacheTemplate)
73 | case blockDefinition(name: String, template: MustacheTemplate)
74 | case blockExpansion(name: String, default: MustacheTemplate, indentation: String?)
75 | case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
76 | case dynamicNamePartial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
77 | case contentType(MustacheContentType)
78 | }
79 |
80 | var tokens: [Token]
81 | let text: String
82 | let filename: String?
83 | }
84 |
--------------------------------------------------------------------------------
/documentation/Mustache Syntax.md:
--------------------------------------------------------------------------------
1 | # Mustache Syntax
2 |
3 | Mustache is a "logic-less" templating engine. The core language has no flow control statements. Instead it has tags that can be replaced with a value, nothing or a series of values. Below we document all the standard tags
4 |
5 | ## Context
6 |
7 | Mustache renders a template with a context stack. A context is a list of key/value pairs. These can be represented by either a `Dictionary` or the reflection information from `Mirror`. For example the following two objects will render in the same way
8 | ```swift
9 | let object = ["name": "John Smith", "age": 68]
10 | ```
11 | ```swift
12 | struct Person {
13 | let name: String
14 | let age: Int
15 | }
16 | let object = Person(name: "John Smith", age: 68)
17 | ```
18 |
19 | Initially the stack will consist of the root context object you want to render. When we enter a section tag we push the associated value onto the context stack and when we leave the section we pop that value back off the stack.
20 |
21 | ## Tags
22 |
23 | All tags are surrounded by a double curly bracket `{{}}`. When a tag has a reference to a key, the key will be searched for from the context at the top of the context stack and the associated value will be output. If the key cannot be found then the next context down will be searched and so on until either a key is found or we have reached the bottom of the stack. If no key is found the output for that value is `nil`.
24 |
25 | A tag can be used to reference a child value from the associated value of a key by using dot notation in a similar manner to Swift. eg in `{{main.sub}}` the first context is searched for the `main` key. If a value is found, that value is used as a context and the key `sub` is used to search within that context and so on.
26 |
27 | If you want to only search for values in the context at the top of the stack then prefix the variable name with a "." eg `{{.key}}`
28 |
29 | ## Tag types
30 |
31 | - `{{key}}`: Render value associated with `key` as text. By default this is HTML escaped. A `nil` value is rendered as an empty string.
32 | - `{{{name}}}`: Acts the same as `{{name}}` except the resultant text is not HTML escaped. You can also use `{{&name}}` to avoid HTML escaping.
33 | - `{{#section}}`: Section render blocks either render text once or multiple times depending on the value of the key in the current context. A section begins with `{{#section}}` and end with `{{/section}}`. If the key represents a `Bool` value it will only render if it is true. If the key represents an `Optional` it will only render if the object is non-nil. If the key represents an `Array` it will then render the internals of the section multiple times, once for each element of the `Array`. Otherwise it will render with the selected value pushed onto the top of the context stack.
34 | - `{{^section}}`: An inverted section does the opposite of a section. If the key represents a `Bool` value it will render if it is false. If the key represents an `Optional` it will render if it is `nil`. If the key represents a `Array` it will render if the `Array` is empty.
35 | - `{{! comment }}`: This is a comment tag and is ignored.
36 | - `{{> partial}}`: A partial tag renders another mustache file, with the current context stack. In swift-mustache partial tags only work for templates that are a part of a library and the tag is the name of the referenced file without the ".mustache" extension.
37 | - `{{=<% %>=}}`: The set delimiter tag allows you to change from using the double curly brackets as tag delimiters. In the example the delimiters have been changed to `<% %>` but you can change them to whatever you like.
38 |
39 | You can find out more about the standard Mustache tags in the [Mustache Manual](https://mustache.github.io/mustache.5.html).
40 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/ErrorTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Mustache
16 | import XCTest
17 |
18 | final class ErrorTests: XCTestCase {
19 | func testSectionCloseNameIncorrect() {
20 | XCTAssertThrowsError(
21 | try MustacheTemplate(
22 | string: """
23 | {{#test}}
24 | {{.}}
25 | {{/test2}}
26 | """
27 | )
28 | ) { error in
29 | switch error {
30 | case let error as MustacheTemplate.ParserError:
31 | XCTAssertEqual(error.error as? MustacheTemplate.Error, .sectionCloseNameIncorrect)
32 | XCTAssertEqual(error.context.line, "{{/test2}}")
33 | XCTAssertEqual(error.context.lineNumber, 3)
34 | XCTAssertEqual(error.context.columnNumber, 4)
35 |
36 | default:
37 | XCTFail("\(error)")
38 | }
39 | }
40 | }
41 |
42 | func testUnfinishedName() {
43 | XCTAssertThrowsError(
44 | try MustacheTemplate(
45 | string: """
46 | {{#test}}
47 | {{name}
48 | {{/test2}}
49 | """
50 | )
51 | ) { error in
52 | switch error {
53 | case let error as MustacheTemplate.ParserError:
54 | XCTAssertEqual(error.error as? MustacheTemplate.Error, .unfinishedName)
55 | XCTAssertEqual(error.context.line, "{{name}")
56 | XCTAssertEqual(error.context.lineNumber, 2)
57 | XCTAssertEqual(error.context.columnNumber, 7)
58 |
59 | default:
60 | XCTFail("\(error)")
61 | }
62 | }
63 | }
64 |
65 | func testExpectedSectionEnd() {
66 | XCTAssertThrowsError(
67 | try MustacheTemplate(
68 | string: """
69 | {{#test}}
70 | {{.}}
71 | """
72 | )
73 | ) { error in
74 | switch error {
75 | case let error as MustacheTemplate.ParserError:
76 | XCTAssertEqual(error.error as? MustacheTemplate.Error, .expectedSectionEnd)
77 | XCTAssertEqual(error.context.line, "{{.}}")
78 | XCTAssertEqual(error.context.lineNumber, 2)
79 | XCTAssertEqual(error.context.columnNumber, 6)
80 |
81 | default:
82 | XCTFail("\(error)")
83 | }
84 | }
85 | }
86 |
87 | func testInvalidSetDelimiter() {
88 | XCTAssertThrowsError(
89 | try MustacheTemplate(
90 | string: """
91 | {{=<% %>=}}
92 | <%.%>
93 | <%={{}}=%>
94 | """
95 | )
96 | ) { error in
97 | switch error {
98 | case let error as MustacheTemplate.ParserError:
99 | XCTAssertEqual(error.error as? MustacheTemplate.Error, .invalidSetDelimiter)
100 | XCTAssertEqual(error.context.line, "<%={{}}=%>")
101 | XCTAssertEqual(error.context.lineNumber, 3)
102 | XCTAssertEqual(error.context.columnNumber, 4)
103 |
104 | default:
105 | XCTFail("\(error)")
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/TemplateParserTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import XCTest
16 |
17 | @testable import Mustache
18 |
19 | final class TemplateParserTests: XCTestCase {
20 | func testText() throws {
21 | let template = try MustacheTemplate(string: "test template")
22 | XCTAssertEqual(template.tokens, [.text("test template")])
23 | }
24 |
25 | func testVariable() throws {
26 | let template = try MustacheTemplate(string: "test {{variable}}")
27 | XCTAssertEqual(template.tokens, [.text("test "), .variable(name: "variable")])
28 | }
29 |
30 | func testSection() throws {
31 | let template = try MustacheTemplate(string: "test {{#section}}text{{/section}}")
32 | XCTAssertEqual(template.tokens, [.text("test "), .section(name: "section", template: .init([.text("text")], text: "text"))])
33 | }
34 |
35 | func testInvertedSection() throws {
36 | let template = try MustacheTemplate(string: "test {{^section}}text{{/section}}")
37 | XCTAssertEqual(template.tokens, [.text("test "), .invertedSection(name: "section", template: .init([.text("text")], text: "text"))])
38 | }
39 |
40 | func testComment() throws {
41 | let template = try MustacheTemplate(string: "test {{!section}}")
42 | XCTAssertEqual(template.tokens, [.text("test ")])
43 | }
44 |
45 | func testWhitespace() throws {
46 | let template = try MustacheTemplate(string: "{{ section }}")
47 | XCTAssertEqual(template.tokens, [.variable(name: "section")])
48 | }
49 |
50 | func testContentType() throws {
51 | let template = try MustacheTemplate(string: "{{% CONTENT_TYPE:TEXT}}")
52 | let template1 = try MustacheTemplate(string: "{{% CONTENT_TYPE:TEXT }}")
53 | let template2 = try MustacheTemplate(string: "{{% CONTENT_TYPE: TEXT}}")
54 | let template3 = try MustacheTemplate(string: "{{%CONTENT_TYPE:TEXT}}")
55 | XCTAssertEqual(template.tokens, [.contentType(TextContentType())])
56 | XCTAssertEqual(template1.tokens, [.contentType(TextContentType())])
57 | XCTAssertEqual(template2.tokens, [.contentType(TextContentType())])
58 | XCTAssertEqual(template3.tokens, [.contentType(TextContentType())])
59 | }
60 | }
61 |
62 | extension MustacheTemplate {
63 | public static func == (lhs: MustacheTemplate, rhs: MustacheTemplate) -> Bool {
64 | lhs.tokens == rhs.tokens
65 | }
66 | }
67 |
68 | extension MustacheTemplate: Equatable {}
69 |
70 | extension MustacheTemplate.Token {
71 | public static func == (lhs: MustacheTemplate.Token, rhs: MustacheTemplate.Token) -> Bool {
72 | switch (lhs, rhs) {
73 | case (.text(let lhs), .text(let rhs)):
74 | return lhs == rhs
75 | case (.variable(let lhs, let lhs2), .variable(let rhs, let rhs2)):
76 | return lhs == rhs && lhs2 == rhs2
77 | case (.section(let lhs1, let lhs2, let lhs3), .section(let rhs1, let rhs2, let rhs3)):
78 | return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
79 | case (.invertedSection(let lhs1, let lhs2, let lhs3), .invertedSection(let rhs1, let rhs2, let rhs3)):
80 | return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3
81 | case (.partial(let name1, let indent1, _), .partial(let name2, let indent2, _)):
82 | return name1 == name2 && indent1 == indent2
83 | case (.contentType(let contentType), .contentType(let contentType2)):
84 | return type(of: contentType) == type(of: contentType2)
85 | default:
86 | return false
87 | }
88 | }
89 | }
90 |
91 | extension MustacheTemplate.Token: Equatable {}
92 |
--------------------------------------------------------------------------------
/Sources/Mustache/Library.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Class holding a collection of mustache templates.
16 | ///
17 | /// Each template can reference the others via a partial using the name the template is registered under
18 | /// ```
19 | /// {{#sequence}}{{>entry}}{{/sequence}}
20 | /// ```
21 | public struct MustacheLibrary: Sendable {
22 | /// Initialize empty library
23 | public init() {
24 | self.templates = [:]
25 | }
26 |
27 | /// Initialize library with collection of templates.
28 | ///
29 | /// Each template is mapped to a name which can then be used as the partial name.
30 | /// - Parameter templates: Dictionary of mustache templates with a String key
31 | public init(templates: [String: MustacheTemplate]) {
32 | self.templates = templates
33 | }
34 |
35 | /// Initialize library with contents of folder.
36 | ///
37 | /// Each template is registered with the name of the file minus its extension. The search through
38 | /// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`.
39 | /// - Parameter directory: Directory to look for mustache templates
40 | /// - Parameter extension: Extension of files to look for
41 | public init(directory: String, withExtension extension: String = "mustache") async throws {
42 | self.templates = try Self.loadTemplates(from: directory, withExtension: `extension`)
43 | }
44 |
45 | /// Register template under name
46 | /// - Parameters:
47 | /// - template: Template
48 | /// - name: Name of template
49 | public mutating func register(_ template: MustacheTemplate, named name: String) {
50 | self.templates[name] = template
51 | }
52 |
53 | /// Register template under name
54 | /// - Parameters:
55 | /// - mustache: Mustache text
56 | /// - name: Name of template
57 | public mutating func register(_ mustache: String, named name: String) throws {
58 | let template = try MustacheTemplate(string: mustache)
59 | self.templates[name] = template
60 | }
61 |
62 | /// Return template registed with name
63 | /// - Parameter name: name to search for
64 | /// - Returns: Template
65 | public func getTemplate(named name: String) -> MustacheTemplate? {
66 | self.templates[name]
67 | }
68 |
69 | /// Render object using templated with name
70 | /// - Parameters:
71 | /// - object: Object to render
72 | /// - name: Name of template
73 | /// - Returns: Rendered text
74 | public func render(_ object: Any, withTemplate name: String) -> String? {
75 | guard let template = templates[name] else { return nil }
76 | return template.render(object, library: self)
77 | }
78 |
79 | /// Render object using templated with name
80 | /// - Parameters:
81 | /// - object: Object to render
82 | /// - name: Name of template
83 | /// - reload: Reload templates when rendering. This is only available in debug builds
84 | /// - Returns: Rendered text
85 | public func render(_ object: Any, withTemplate name: String, reload: Bool) -> String? {
86 | guard let template = templates[name] else { return nil }
87 | #if DEBUG
88 | return template.render(object, library: self, reload: reload)
89 | #else
90 | return template.render(object, library: self)
91 | #endif
92 | }
93 |
94 | /// Error returned by init() when parser fails
95 | public struct ParserError: Swift.Error {
96 | /// File error occurred in
97 | public let filename: String
98 | /// Context (line, linenumber and column number)
99 | public let context: MustacheParserContext
100 | /// Actual error that occurred
101 | public let error: Error
102 | }
103 |
104 | private var templates: [String: MustacheTemplate]
105 | }
106 |
--------------------------------------------------------------------------------
/scripts/validate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ##===----------------------------------------------------------------------===##
3 | ##
4 | ## This source file is part of the Hummingbird server framework project
5 | ##
6 | ## Copyright (c) 2021-2024 the Hummingbird authors
7 | ## Licensed under Apache License v2.0
8 | ##
9 | ## See LICENSE.txt for license information
10 | ## See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
11 | ##
12 | ## SPDX-License-Identifier: Apache-2.0
13 | ##
14 | ##===----------------------------------------------------------------------===##
15 | ##===----------------------------------------------------------------------===##
16 | ##
17 | ## This source file is part of the SwiftNIO open source project
18 | ##
19 | ## Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors
20 | ## Licensed under Apache License v2.0
21 | ##
22 | ## See LICENSE.txt for license information
23 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
24 | ##
25 | ## SPDX-License-Identifier: Apache-2.0
26 | ##
27 | ##===----------------------------------------------------------------------===##
28 |
29 | SWIFT_FORMAT_VERSION=0.53.10
30 |
31 | set -eu
32 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
33 |
34 | function replace_acceptable_years() {
35 | # this needs to replace all acceptable forms with 'YEARS'
36 | sed -e 's/20[12][0-9]-20[12][0-9]/YEARS/' -e 's/20[12][0-9]/YEARS/' -e '/^#!/ d'
37 | }
38 |
39 | printf "=> Checking format... "
40 | FIRST_OUT="$(git status --porcelain)"
41 | git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
42 | git diff --exit-code '*.swift'
43 |
44 | SECOND_OUT="$(git status --porcelain)"
45 | if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
46 | printf "\033[0;31mformatting issues!\033[0m\n"
47 | git --no-pager diff
48 | exit 1
49 | else
50 | printf "\033[0;32mokay.\033[0m\n"
51 | fi
52 | printf "=> Checking license headers... "
53 | tmp=$(mktemp /tmp/.soto-core-sanity_XXXXXX)
54 |
55 | exit 0
56 |
57 | for language in swift-or-c; do
58 | declare -a matching_files
59 | declare -a exceptions
60 | expections=( )
61 | matching_files=( -name '*' )
62 | case "$language" in
63 | swift-or-c)
64 | exceptions=( -path '*/Benchmarks/.build/*' -o -name Package.swift)
65 | matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' )
66 | cat > "$tmp" <<"EOF"
67 | //===----------------------------------------------------------------------===//
68 | //
69 | // This source file is part of the Hummingbird server framework project
70 | //
71 | // Copyright (c) YEARS the Hummingbird authors
72 | // Licensed under Apache License v2.0
73 | //
74 | // See LICENSE.txt for license information
75 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
76 | //
77 | // SPDX-License-Identifier: Apache-2.0
78 | //
79 | //===----------------------------------------------------------------------===//
80 | EOF
81 | ;;
82 | bash)
83 | matching_files=( -name '*.sh' )
84 | cat > "$tmp" <<"EOF"
85 | ##===----------------------------------------------------------------------===##
86 | ##
87 | ## This source file is part of the Hummingbird server framework project
88 | ##
89 | ## Copyright (c) YEARS the Hummingbird authors
90 | ## Licensed under Apache License v2.0
91 | ##
92 | ## See LICENSE.txt for license information
93 | ## See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
94 | ##
95 | ## SPDX-License-Identifier: Apache-2.0
96 | ##
97 | ##===----------------------------------------------------------------------===##
98 | EOF
99 | ;;
100 | *)
101 | echo >&2 "ERROR: unknown language '$language'"
102 | ;;
103 | esac
104 |
105 | lines_to_compare=$(cat "$tmp" | wc -l | tr -d " ")
106 | # need to read one more line as we remove the '#!' line
107 | lines_to_read=$(expr "$lines_to_compare" + 1)
108 | expected_sha=$(cat "$tmp" | shasum)
109 |
110 | (
111 | cd "$here/.."
112 | find . \
113 | \( \! -path './.build/*' -a \
114 | \( "${matching_files[@]}" \) -a \
115 | \( \! \( "${exceptions[@]}" \) \) \) | while read line; do
116 | if [[ "$(cat "$line" | head -n $lines_to_read | replace_acceptable_years | head -n $lines_to_compare | shasum)" != "$expected_sha" ]]; then
117 | printf "\033[0;31mmissing headers in file '$line'!\033[0m\n"
118 | diff -u <(cat "$line" | head -n $lines_to_read | replace_acceptable_years | head -n $lines_to_compare) "$tmp"
119 | exit 1
120 | fi
121 | done
122 | printf "\033[0;32mokay.\033[0m\n"
123 | )
124 | done
125 |
126 | rm "$tmp"
127 |
--------------------------------------------------------------------------------
/Sources/Mustache/Context.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | /// Context while rendering mustache tokens
16 | struct MustacheContext {
17 | let stack: [Any]
18 | let sequenceContext: MustacheSequenceContext?
19 | let indentation: String?
20 | let inherited: [String: MustacheTemplate]?
21 | let contentType: MustacheContentType
22 | let library: MustacheLibrary?
23 | let reloadPartials: Bool
24 |
25 | /// initialize context with a single objectt
26 | init(_ object: Any, library: MustacheLibrary? = nil, reloadPartials: Bool = false) {
27 | self.stack = [object]
28 | self.sequenceContext = nil
29 | self.indentation = nil
30 | self.inherited = nil
31 | self.contentType = HTMLContentType()
32 | self.library = library
33 | self.reloadPartials = reloadPartials
34 | }
35 |
36 | private init(
37 | stack: [Any],
38 | sequenceContext: MustacheSequenceContext?,
39 | indentation: String?,
40 | inherited: [String: MustacheTemplate]?,
41 | contentType: MustacheContentType,
42 | library: MustacheLibrary? = nil,
43 | reloadPartials: Bool
44 | ) {
45 | self.stack = stack
46 | self.sequenceContext = sequenceContext
47 | self.indentation = indentation
48 | self.inherited = inherited
49 | self.contentType = contentType
50 | self.library = library
51 | self.reloadPartials = reloadPartials
52 | }
53 |
54 | /// return context with object add to stack
55 | func withObject(_ object: Any) -> MustacheContext {
56 | var stack = self.stack
57 | stack.append(object)
58 | return .init(
59 | stack: stack,
60 | sequenceContext: nil,
61 | indentation: self.indentation,
62 | inherited: self.inherited,
63 | contentType: self.contentType,
64 | library: self.library,
65 | reloadPartials: self.reloadPartials
66 | )
67 | }
68 |
69 | /// return context with indent and parameter information for invoking a partial
70 | func withPartial(indented: String?, inheriting: [String: MustacheTemplate]?) -> MustacheContext {
71 | let indentation: String?
72 | if let indented {
73 | indentation = (self.indentation ?? "") + indented
74 | } else {
75 | indentation = self.indentation
76 | }
77 | let inherits: [String: MustacheTemplate]?
78 | if let inheriting {
79 | if let originalInherits = self.inherited {
80 | inherits = originalInherits.merging(inheriting) { value, _ in value }
81 | } else {
82 | inherits = inheriting
83 | }
84 | } else {
85 | inherits = self.inherited
86 | }
87 | return .init(
88 | stack: self.stack,
89 | sequenceContext: nil,
90 | indentation: indentation,
91 | inherited: inherits,
92 | contentType: HTMLContentType(),
93 | library: self.library,
94 | reloadPartials: self.reloadPartials
95 | )
96 | }
97 |
98 | /// return context with indent information for invoking an inheritance block
99 | func withBlockExpansion(indented: String?) -> MustacheContext {
100 | let indentation: String?
101 | if let indented {
102 | indentation = (self.indentation ?? "") + indented
103 | } else {
104 | indentation = self.indentation
105 | }
106 | return .init(
107 | stack: self.stack,
108 | sequenceContext: nil,
109 | indentation: indentation,
110 | inherited: self.inherited,
111 | contentType: self.contentType,
112 | library: self.library,
113 | reloadPartials: self.reloadPartials
114 | )
115 | }
116 |
117 | /// return context with sequence info and sequence element added to stack
118 | func withSequence(_ object: Any, sequenceContext: MustacheSequenceContext) -> MustacheContext {
119 | var stack = self.stack
120 | stack.append(object)
121 | return .init(
122 | stack: stack,
123 | sequenceContext: sequenceContext,
124 | indentation: self.indentation,
125 | inherited: self.inherited,
126 | contentType: self.contentType,
127 | library: self.library,
128 | reloadPartials: self.reloadPartials
129 | )
130 | }
131 |
132 | /// return context with sequence info and sequence element added to stack
133 | func withContentType(_ contentType: MustacheContentType) -> MustacheContext {
134 | .init(
135 | stack: self.stack,
136 | sequenceContext: self.sequenceContext,
137 | indentation: self.indentation,
138 | inherited: self.inherited,
139 | contentType: contentType,
140 | library: self.library,
141 | reloadPartials: self.reloadPartials
142 | )
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/LibraryTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import XCTest
16 |
17 | @testable import Mustache
18 |
19 | final class LibraryTests: XCTestCase {
20 | func testDirectoryLoad() async throws {
21 | let fs = FileManager()
22 | try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
23 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
24 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
25 | try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache"))
26 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) }
27 |
28 | let library = try await MustacheLibrary(directory: "./templates")
29 | let object = ["value": ["value1", "value2"]]
30 | XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2")
31 | }
32 |
33 | func testPartial() async throws {
34 | let fs = FileManager()
35 | try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
36 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
37 | try mustache.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
38 | let mustache2 = Data("{{>test-partial}}".utf8)
39 | try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
40 | defer {
41 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test-partial.mustache"))
42 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache"))
43 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
44 | }
45 |
46 | let library = try await MustacheLibrary(directory: "./templates")
47 | let object = ["value": ["value1", "value2"]]
48 | XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2")
49 | }
50 |
51 | func testPartialInSubfolder() async throws {
52 | let fs = FileManager()
53 | try? fs.createDirectory(atPath: "templates/subfolder", withIntermediateDirectories: true)
54 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
55 | try mustache.write(to: URL(fileURLWithPath: "templates/subfolder/test-partial.mustache"))
56 | let mustache2 = Data("{{>subfolder/test-partial}}".utf8)
57 | try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
58 | defer {
59 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/subfolder/test-partial.mustache"))
60 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache"))
61 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
62 | }
63 |
64 | let library = try await MustacheLibrary(directory: "./templates")
65 | let object = ["value": ["value1", "value2"]]
66 | XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2")
67 | }
68 |
69 | func testLibraryParserError() async throws {
70 | let fs = FileManager()
71 | try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
72 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
73 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
74 | try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache"))
75 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) }
76 | let mustache2 = Data(
77 | """
78 | {{#test}}
79 | {{{name}}
80 | {{/test2}}
81 | """.utf8
82 | )
83 | try mustache2.write(to: URL(fileURLWithPath: "templates/error.mustache"))
84 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/error.mustache")) }
85 |
86 | do {
87 | _ = try await MustacheLibrary(directory: "./templates")
88 | } catch let parserError as MustacheLibrary.ParserError {
89 | XCTAssertEqual(parserError.filename, "error.mustache")
90 | XCTAssertEqual(parserError.context.line, "{{{name}}")
91 | XCTAssertEqual(parserError.context.lineNumber, 2)
92 | XCTAssertEqual(parserError.context.columnNumber, 10)
93 | }
94 | }
95 |
96 | #if DEBUG
97 | func testReload() async throws {
98 | let fs = FileManager()
99 | try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
100 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
101 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
102 | try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache"))
103 | defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) }
104 |
105 | let library = try await MustacheLibrary(directory: "./templates")
106 | let object = ["value": ["value1", "value2"]]
107 | XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2")
108 | let mustache2 = Data("{{#value}}{{.}}{{/value}}".utf8)
109 | try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
110 | XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "value1value2")
111 | }
112 |
113 | func testReloadPartial() async throws {
114 | let fs = FileManager()
115 | try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
116 | let mustache = Data("{{#value}}{{.}}{{/value}}".utf8)
117 | try mustache.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
118 | let mustache2 = Data("{{>test-partial}}".utf8)
119 | try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
120 | defer {
121 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test-partial.mustache"))
122 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache"))
123 | XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
124 | }
125 |
126 | let library = try await MustacheLibrary(directory: "./templates")
127 | let object = ["value": ["value1", "value2"]]
128 | XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2")
129 | let mustache3 = Data("{{#value}}{{.}}{{/value}}".utf8)
130 | try mustache3.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
131 | XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "value1value2")
132 | }
133 | #endif
134 | }
135 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/PartialTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import XCTest
16 |
17 | @testable import Mustache
18 |
19 | final class PartialTests: XCTestCase {
20 | /// Testing partials
21 | func testMustacheManualExample9() throws {
22 | let template = try MustacheTemplate(
23 | string: """
24 | Names
25 | {{#names}}
26 | {{> us/er}}
27 | {{/names}}
28 | """
29 | )
30 | let template2 = try MustacheTemplate(
31 | string: """
32 | {{.}}
33 |
34 | """
35 | )
36 | let library = MustacheLibrary(templates: ["base": template, "us/er": template2])
37 |
38 | let object: [String: Any] = ["names": ["john", "adam", "claire"]]
39 | XCTAssertEqual(
40 | library.render(object, withTemplate: "base"),
41 | """
42 | Names
43 | john
44 | adam
45 | claire
46 |
47 | """
48 | )
49 | }
50 |
51 | /// Test where last line of partial generates no content. It should not add a
52 | /// tab either
53 | func testPartialEmptyLineTabbing() throws {
54 | let template = try MustacheTemplate(
55 | string: """
56 | Names
57 | {{#names}}
58 | {{> user}}
59 | {{/names}}
60 | Text after
61 |
62 | """
63 | )
64 | let template2 = try MustacheTemplate(
65 | string: """
66 | {{^empty(.)}}
67 | {{.}}
68 | {{/empty(.)}}
69 | {{#empty(.)}}
70 | empty
71 | {{/empty(.)}}
72 |
73 | """
74 | )
75 | var library = MustacheLibrary()
76 | library.register(template, named: "base")
77 | library.register(template2, named: "user")
78 | let object: [String: Any] = ["names": ["john", "adam", "claire"]]
79 | XCTAssertEqual(
80 | library.render(object, withTemplate: "base"),
81 | """
82 | Names
83 | john
84 | adam
85 | claire
86 | Text after
87 |
88 | """
89 | )
90 | }
91 |
92 | func testTrailingNewLines() throws {
93 | let template1 = try MustacheTemplate(
94 | string: """
95 | {{> withNewLine }}
96 | >> {{> withNewLine }}
97 | [ {{> withNewLine }} ]
98 | """
99 | )
100 | let template2 = try MustacheTemplate(
101 | string: """
102 | {{> withoutNewLine }}
103 | >> {{> withoutNewLine }}
104 | [ {{> withoutNewLine }} ]
105 | """
106 | )
107 | let withNewLine = try MustacheTemplate(
108 | string: """
109 | {{#things}}{{.}}, {{/things}}
110 |
111 | """
112 | )
113 | let withoutNewLine = try MustacheTemplate(string: "{{#things}}{{.}}, {{/things}}")
114 | let library = MustacheLibrary(templates: [
115 | "base1": template1, "base2": template2, "withNewLine": withNewLine, "withoutNewLine": withoutNewLine,
116 | ])
117 | let object = ["things": [1, 2, 3, 4, 5]]
118 | XCTAssertEqual(
119 | library.render(object, withTemplate: "base1"),
120 | """
121 | 1, 2, 3, 4, 5,
122 | >> 1, 2, 3, 4, 5,
123 |
124 | [ 1, 2, 3, 4, 5,
125 | ]
126 | """
127 | )
128 | XCTAssertEqual(
129 | library.render(object, withTemplate: "base2"),
130 | """
131 | 1, 2, 3, 4, 5, >> 1, 2, 3, 4, 5,
132 | [ 1, 2, 3, 4, 5, ]
133 | """
134 | )
135 | }
136 |
137 | /// Testing dynamic partials
138 | func testDynamicPartials() throws {
139 | let template = try MustacheTemplate(
140 | string: """
141 | Names
142 | {{partial}}
143 | """
144 | )
145 | let template2 = try MustacheTemplate(
146 | string: """
147 | {{#names}}
148 | {{.}}
149 | {{/names}}
150 | """
151 | )
152 | let library = MustacheLibrary(templates: ["base": template])
153 |
154 | let object: [String: Any] = ["names": ["john", "adam", "claire"], "partial": template2]
155 | XCTAssertEqual(
156 | library.render(object, withTemplate: "base"),
157 | """
158 | Names
159 | john
160 | adam
161 | claire
162 |
163 | """
164 | )
165 | }
166 |
167 | /// test inheritance
168 | func testInheritance() throws {
169 | var library = MustacheLibrary()
170 | try library.register(
171 | """
172 |
173 | {{$title}}Default title{{/title}}
174 |
175 | """,
176 | named: "header"
177 | )
178 | try library.register(
179 | """
180 |
181 | {{$header}}{{/header}}
182 | {{$content}}{{/content}}
183 |
184 |
185 | """,
186 | named: "base"
187 | )
188 | try library.register(
189 | """
190 | {{Hello world{{/content}}
197 | {{/base}}
198 |
199 | """,
200 | named: "mypage"
201 | )
202 | XCTAssertEqual(
203 | library.render({}, withTemplate: "mypage")!,
204 | """
205 |
206 |
207 | My page title
208 |
209 | Hello world
210 |
211 |
212 | """
213 | )
214 | }
215 |
216 | func testInheritanceIndentation() throws {
217 | var library = MustacheLibrary()
218 | try library.register(
219 | """
220 | Hi,
221 | {{$block}}{{/block}}
222 | """,
223 | named: "parent"
224 | )
225 | try library.register(
226 | """
227 | {{ Any?
32 | }
33 |
34 | extension StringProtocol {
35 | /// Transform String/Substring
36 | ///
37 | /// Transforms available are `capitalized`, `lowercased`, `uppercased` and `reversed`
38 | /// - Parameter name: transform name
39 | /// - Returns: Result
40 | public func transform(_ name: String) -> Any? {
41 | switch name {
42 | case "empty":
43 | return isEmpty
44 | case "capitalized":
45 | return capitalized
46 | case "lowercased":
47 | return lowercased()
48 | case "uppercased":
49 | return uppercased()
50 | case "reversed":
51 | return Substring(self.reversed())
52 | default:
53 | return nil
54 | }
55 | }
56 | }
57 |
58 | extension String: MustacheTransformable {}
59 | extension Substring: MustacheTransformable {}
60 |
61 | /// Protocol for sequence that can be sorted
62 | private protocol ComparableSequence {
63 | func comparableTransform(_ name: String) -> Any?
64 | }
65 |
66 | extension Array: MustacheTransformable {
67 | /// Transform Array.
68 | ///
69 | /// Transforms available are `first`, `last`, `reversed`, `count`, `empty` and for arrays
70 | /// with comparable elements `sorted`.
71 | /// - Parameter name: transform name
72 | /// - Returns: Result
73 | public func transform(_ name: String) -> Any? {
74 | switch name {
75 | case "first":
76 | return first
77 | case "last":
78 | return last
79 | case "reversed":
80 | return reversed()
81 | case "count":
82 | return count
83 | case "empty":
84 | return isEmpty
85 | default:
86 | if let comparableSeq = self as? ComparableSequence {
87 | return comparableSeq.comparableTransform(name)
88 | }
89 | return nil
90 | }
91 | }
92 | }
93 |
94 | extension Array: ComparableSequence where Element: Comparable {
95 | func comparableTransform(_ name: String) -> Any? {
96 | switch name {
97 | case "sorted":
98 | return sorted()
99 | default:
100 | return nil
101 | }
102 | }
103 | }
104 |
105 | extension Set: MustacheTransformable {
106 | /// Transform Set.
107 | ///
108 | /// Transforms available are `count`, `empty` and for sets
109 | /// with comparable elements `sorted`.
110 | /// - Parameter name: transform name
111 | /// - Returns: Result
112 | public func transform(_ name: String) -> Any? {
113 | switch name {
114 | case "count":
115 | return count
116 | case "empty":
117 | return isEmpty
118 | default:
119 | if let comparableSeq = self as? ComparableSequence {
120 | return comparableSeq.comparableTransform(name)
121 | }
122 | return nil
123 | }
124 | }
125 | }
126 |
127 | extension Set: ComparableSequence where Element: Comparable {
128 | func comparableTransform(_ name: String) -> Any? {
129 | switch name {
130 | case "sorted":
131 | return sorted()
132 | default:
133 | return nil
134 | }
135 | }
136 | }
137 |
138 | extension ReversedCollection: MustacheTransformable {
139 | /// Transform ReversedCollection.
140 | ///
141 | /// Transforms available are `first`, `last`, `reversed`, `count`, `empty` and for collections
142 | /// with comparable elements `sorted`.
143 | /// - Parameter name: transform name
144 | /// - Returns: Result
145 | public func transform(_ name: String) -> Any? {
146 | switch name {
147 | case "first":
148 | return first
149 | case "last":
150 | return last
151 | case "reversed":
152 | return reversed()
153 | case "count":
154 | return count
155 | case "empty":
156 | return isEmpty
157 | default:
158 | if let comparableSeq = self as? ComparableSequence {
159 | return comparableSeq.comparableTransform(name)
160 | }
161 | return nil
162 | }
163 | }
164 | }
165 |
166 | extension ReversedCollection: ComparableSequence where Element: Comparable {
167 | func comparableTransform(_ name: String) -> Any? {
168 | switch name {
169 | case "sorted":
170 | return sorted()
171 | default:
172 | return nil
173 | }
174 | }
175 | }
176 |
177 | extension Dictionary: MustacheTransformable {
178 | /// Transform Dictionary
179 | ///
180 | /// Transforms available are `count`, `enumerated` and for dictionaries
181 | /// with comparable keys `sorted`.
182 | /// - Parameter name: transform name
183 | /// - Returns: Result
184 | public func transform(_ name: String) -> Any? {
185 | switch name {
186 | case "count":
187 | return count
188 | case "empty":
189 | return isEmpty
190 | case "enumerated":
191 | return map { (key: $0.key, value: $0.value) }
192 | default:
193 | if let comparableSeq = self as? ComparableSequence {
194 | return comparableSeq.comparableTransform(name)
195 | }
196 | return nil
197 | }
198 | }
199 | }
200 |
201 | extension Dictionary: ComparableSequence where Key: Comparable {
202 | func comparableTransform(_ name: String) -> Any? {
203 | switch name {
204 | case "sorted":
205 | return map { (key: $0.key, value: $0.value) }.sorted { $0.key < $1.key }
206 | default:
207 | return nil
208 | }
209 | }
210 | }
211 |
212 | extension FixedWidthInteger {
213 | /// Transform FixedWidthInteger
214 | ///
215 | /// Transforms available are `plusone`, `minusone`, `odd`, `even`
216 | /// - Parameter name: transform name
217 | /// - Returns: Result
218 | public func transform(_ name: String) -> Any? {
219 | switch name {
220 | case "equalzero":
221 | return self == 0
222 | case "plusone":
223 | return self + 1
224 | case "minusone":
225 | return self - 1
226 | case "even":
227 | return (self & 1) == 0
228 | case "odd":
229 | return (self & 1) == 1
230 | default:
231 | return nil
232 | }
233 | }
234 | }
235 |
236 | extension Int: MustacheTransformable {}
237 | extension Int8: MustacheTransformable {}
238 | extension Int16: MustacheTransformable {}
239 | extension Int32: MustacheTransformable {}
240 | extension Int64: MustacheTransformable {}
241 | extension UInt: MustacheTransformable {}
242 | extension UInt8: MustacheTransformable {}
243 | extension UInt16: MustacheTransformable {}
244 | extension UInt32: MustacheTransformable {}
245 | extension UInt64: MustacheTransformable {}
246 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/TransformTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Mustache
16 | import XCTest
17 |
18 | final class TransformTests: XCTestCase {
19 | func testLowercased() throws {
20 | let template = try MustacheTemplate(
21 | string: """
22 | {{ lowercased(name) }}
23 | """
24 | )
25 | let object: [String: Any] = ["name": "Test"]
26 | XCTAssertEqual(template.render(object), "test")
27 | }
28 |
29 | func testUppercased() throws {
30 | let template = try MustacheTemplate(
31 | string: """
32 | {{ uppercased(name) }}
33 | """
34 | )
35 | let object: [String: Any] = ["name": "Test"]
36 | XCTAssertEqual(template.render(object), "TEST")
37 | }
38 |
39 | func testNewline() throws {
40 | let template = try MustacheTemplate(
41 | string: """
42 | {{#repo}}
43 | {{name}}
44 | {{/repo}}
45 |
46 | """
47 | )
48 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
49 | XCTAssertEqual(
50 | template.render(object),
51 | """
52 | resque
53 | hub
54 | rip
55 |
56 | """
57 | )
58 | }
59 |
60 | func testFirstLast() throws {
61 | let template = try MustacheTemplate(
62 | string: """
63 | {{#repo}}
64 | {{#first()}}first: {{/first()}}{{#last()}}last: {{/last()}}{{ name }}
65 | {{/repo}}
66 |
67 | """
68 | )
69 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
70 | XCTAssertEqual(
71 | template.render(object),
72 | """
73 | first: resque
74 | hub
75 | last: rip
76 |
77 | """
78 | )
79 | }
80 |
81 | func testIndex() throws {
82 | let template = try MustacheTemplate(
83 | string: """
84 | {{#repo}}
85 | {{#index()}}{{plusone(.)}}{{/index()}}) {{ name }}
86 | {{/repo}}
87 |
88 | """
89 | )
90 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
91 | XCTAssertEqual(
92 | template.render(object),
93 | """
94 | 1) resque
95 | 2) hub
96 | 3) rip
97 |
98 | """
99 | )
100 | }
101 |
102 | func testDoubleSequenceTransformWorks() throws {
103 | let template = try MustacheTemplate(
104 | string: """
105 | {{#repo}}
106 | {{count(reversed(numbers))}}
107 | {{/repo}}
108 |
109 | """
110 | )
111 | let object: [String: Any] = ["repo": ["numbers": [1, 2, 3]]]
112 | XCTAssertEqual(
113 | template.render(object),
114 | """
115 | 3
116 |
117 | """
118 | )
119 | }
120 |
121 | func testMultipleTransformWorks() throws {
122 | let template = try MustacheTemplate(
123 | string: """
124 | {{#repo}}
125 | {{minusone(plusone(last(reversed(numbers))))}}
126 | {{/repo}}
127 |
128 | """
129 | )
130 | let object: [String: Any] = ["repo": ["numbers": [5, 4, 3]]]
131 | XCTAssertEqual(
132 | template.render(object),
133 | """
134 | 5
135 |
136 | """
137 | )
138 | }
139 |
140 | func testNestedTransformWorks() throws {
141 | let template = try MustacheTemplate(
142 | string: """
143 | {{#repo}}
144 | {{#uppercased(string)}}{{reversed(.)}}{{/uppercased(string)}}
145 | {{/repo}}
146 |
147 | """
148 | )
149 | let object: [String: Any] = ["repo": ["string": "a123a"]]
150 | XCTAssertEqual(
151 | template.render(object),
152 | """
153 | A321A
154 |
155 | """
156 | )
157 | }
158 |
159 | func testEvenOdd() throws {
160 | let template = try MustacheTemplate(
161 | string: """
162 | {{#repo}}
163 | {{index()}}) {{#even()}}even {{/even()}}{{#odd()}}odd {{/odd()}}{{ name }}
164 | {{/repo}}
165 |
166 | """
167 | )
168 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
169 | XCTAssertEqual(
170 | template.render(object),
171 | """
172 | 0) even resque
173 | 1) odd hub
174 | 2) even rip
175 |
176 | """
177 | )
178 | }
179 |
180 | func testReversed() throws {
181 | let template = try MustacheTemplate(
182 | string: """
183 | {{#reversed(repo)}}
184 | {{ name }}
185 | {{/reversed(repo)}}
186 |
187 | """
188 | )
189 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
190 | XCTAssertEqual(
191 | template.render(object),
192 | """
193 | rip
194 | hub
195 | resque
196 |
197 | """
198 | )
199 | }
200 |
201 | func testArrayIndex() throws {
202 | let template = try MustacheTemplate(
203 | string: """
204 | {{#repo}}
205 | {{ index() }}) {{ name }}
206 | {{/repo}}
207 | """
208 | )
209 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
210 | XCTAssertEqual(
211 | template.render(object),
212 | """
213 | 0) resque
214 | 1) hub
215 | 2) rip
216 |
217 | """
218 | )
219 | }
220 |
221 | func testArraySorted() throws {
222 | let template = try MustacheTemplate(
223 | string: """
224 | {{#sorted(repo)}}
225 | {{ index() }}) {{ . }}
226 | {{/sorted(repo)}}
227 | """
228 | )
229 | let object: [String: Any] = ["repo": ["resque", "hub", "rip"]]
230 | XCTAssertEqual(
231 | template.render(object),
232 | """
233 | 0) hub
234 | 1) resque
235 | 2) rip
236 |
237 | """
238 | )
239 | }
240 |
241 | func testDictionaryEmpty() throws {
242 | let template = try MustacheTemplate(
243 | string: """
244 | {{#empty(array)}}Array{{/empty(array)}}{{#empty(dictionary)}}Dictionary{{/empty(dictionary)}}
245 | """
246 | )
247 | let object: [String: Any] = ["array": [], "dictionary": [:]]
248 | XCTAssertEqual(template.render(object), "ArrayDictionary")
249 | }
250 |
251 | func testListOutput() throws {
252 | let object = [1, 2, 3, 4]
253 | let template = try MustacheTemplate(string: "{{#.}}{{.}}{{^last()}}, {{/last()}}{{/.}}")
254 | XCTAssertEqual(template.render(object), "1, 2, 3, 4")
255 | }
256 |
257 | func testDictionaryEnumerated() throws {
258 | let template = try MustacheTemplate(
259 | string: """
260 | {{#enumerated(.)}}{{ key }} = {{ value }}{{/enumerated(.)}}
261 | """
262 | )
263 | let object: [String: Any] = ["one": 1, "two": 2]
264 | let result = template.render(object)
265 | XCTAssertTrue(result == "one = 1two = 2" || result == "two = 2one = 1")
266 | }
267 |
268 | func testDictionarySortedByKey() throws {
269 | let template = try MustacheTemplate(
270 | string: """
271 | {{#sorted(.)}}{{ key }} = {{ value }}{{/sorted(.)}}
272 | """
273 | )
274 | let object: [String: Any] = ["one": 1, "two": 2, "three": 3]
275 | let result = template.render(object)
276 | XCTAssertEqual(result, "one = 1three = 3two = 2")
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/SpecTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import Mustache
17 | import XCTest
18 |
19 | #if os(Linux) || os(Windows)
20 | import FoundationNetworking
21 | #endif
22 |
23 | public struct AnyDecodable: Decodable {
24 | public let value: Any
25 |
26 | public init(_ value: some Any) {
27 | self.value = value
28 | }
29 | }
30 |
31 | extension AnyDecodable {
32 | public init(from decoder: Decoder) throws {
33 | let container = try decoder.singleValueContainer()
34 |
35 | if container.decodeNil() {
36 | self.init(NSNull())
37 | } else if let bool = try? container.decode(Bool.self) {
38 | self.init(bool)
39 | } else if let int = try? container.decode(Int.self) {
40 | self.init(int)
41 | } else if let uint = try? container.decode(UInt.self) {
42 | self.init(uint)
43 | } else if let double = try? container.decode(Double.self) {
44 | self.init(double)
45 | } else if let string = try? container.decode(String.self) {
46 | self.init(string)
47 | } else if let array = try? container.decode([AnyDecodable].self) {
48 | self.init(array.map(\.value))
49 | } else if let dictionary = try? container.decode([String: AnyDecodable].self) {
50 | self.init(dictionary.mapValues { $0.value })
51 | } else {
52 | throw DecodingError.dataCorruptedError(
53 | in: container,
54 | debugDescription: "AnyDecodable value cannot be decoded"
55 | )
56 | }
57 | }
58 | }
59 |
60 | /// Verify implementation against formal standard for Mustache.
61 | /// https://github.com/mustache/spec
62 | final class MustacheSpecTests: XCTestCase {
63 | struct Spec: Decodable {
64 | struct Test: Decodable {
65 | let name: String
66 | let desc: String
67 | var data: AnyDecodable
68 | let partials: [String: String]?
69 | let template: String
70 | let expected: String
71 |
72 | func run() throws {
73 | print("Test: \(self.name)")
74 | if let partials = self.partials {
75 | let template = try MustacheTemplate(string: self.template)
76 | var templates: [String: MustacheTemplate] = ["__test__": template]
77 | for (key, value) in partials {
78 | let template = try MustacheTemplate(string: value)
79 | templates[key] = template
80 | }
81 | let library = MustacheLibrary(templates: templates)
82 | let result = library.render(self.data.value, withTemplate: "__test__")
83 | self.XCTAssertSpecEqual(result, self)
84 | } else {
85 | let template = try MustacheTemplate(string: self.template)
86 | let result = template.render(self.data.value)
87 | self.XCTAssertSpecEqual(result, self)
88 | }
89 | }
90 |
91 | func XCTAssertSpecEqual(_ result: String?, _ test: Spec.Test) {
92 | if result != test.expected {
93 | XCTFail(
94 | """
95 | \(test.name)
96 | \(test.desc)
97 | template:
98 | \(test.template)
99 | data:
100 | \(test.data.value)
101 | \(test.partials.map { "partials:\n\($0)" } ?? "")
102 | result:
103 | \(result ?? "nil")
104 | expected:
105 | \(test.expected)
106 | """
107 | )
108 | }
109 | }
110 | }
111 |
112 | let overview: String
113 | let tests: [Test]
114 | }
115 |
116 | func testSpec(name: String, ignoring: [String] = []) async throws {
117 | let url = URL(
118 | string: "https://raw.githubusercontent.com/mustache/spec/master/specs/\(name).json"
119 | )!
120 | try await testSpec(url: url, ignoring: ignoring)
121 | }
122 |
123 | func testSpec(url: URL, ignoring: [String] = []) async throws {
124 | #if compiler(>=6.0)
125 | let (data, _) = try await URLSession.shared.data(from: url)
126 | #else
127 | let data = try Data(contentsOf: url)
128 | #endif
129 | let spec = try JSONDecoder().decode(Spec.self, from: data)
130 |
131 | let date = Date()
132 | for test in spec.tests {
133 | guard !ignoring.contains(test.name) else { continue }
134 | XCTAssertNoThrow(try test.run())
135 | }
136 | print(-date.timeIntervalSinceNow)
137 | }
138 |
139 | func testSpec(name: String, only: [String]) async throws {
140 | let url = URL(
141 | string: "https://raw.githubusercontent.com/mustache/spec/master/specs/\(name).json"
142 | )!
143 | try await testSpec(url: url, only: only)
144 | }
145 |
146 | func testSpec(url: URL, only: [String]) async throws {
147 | #if compiler(>=6.0)
148 | let (data, _) = try await URLSession.shared.data(from: url)
149 | #else
150 | let data = try Data(contentsOf: url)
151 | #endif
152 | let spec = try JSONDecoder().decode(Spec.self, from: data)
153 |
154 | let date = Date()
155 | for test in spec.tests {
156 | guard only.contains(test.name) else { continue }
157 | XCTAssertNoThrow(try test.run())
158 | }
159 | print(-date.timeIntervalSinceNow)
160 | }
161 |
162 | func testLambdaSpec() async throws {
163 | var g = 0
164 | let lambdaMap = [
165 | "Interpolation": MustacheLambda { "world" },
166 | "Interpolation - Expansion": MustacheLambda { "{{planet}}" },
167 | "Interpolation - Alternate Delimiters": MustacheLambda { "|planet| => {{planet}}" },
168 | "Interpolation - Multiple Calls": MustacheLambda {
169 | MustacheLambda {
170 | g += 1
171 | return g
172 | }
173 | },
174 | "Escaping": MustacheLambda { ">" },
175 | "Section": MustacheLambda { text in text == "{{x}}" ? "yes" : "no" },
176 | "Section - Expansion": MustacheLambda { text in text + "{{planet}}" + text },
177 | // Not going to bother implementing this requires pushing alternate delimiters through the context
178 | // "Section - Alternate Delimiters": MustacheLambda { text in return text + "{{planet}} => |planet|" + text },
179 | "Section - Multiple Calls": MustacheLambda { text in "__" + text + "__" },
180 | "Inverted Section": MustacheLambda { false },
181 | ]
182 | let url = URL(string: "https://raw.githubusercontent.com/mustache/spec/master/specs/~lambdas.json")!
183 | #if compiler(>=6.0)
184 | let (data, _) = try await URLSession.shared.data(from: url)
185 | #else
186 | let data = try Data(contentsOf: url)
187 | #endif
188 | let spec = try JSONDecoder().decode(Spec.self, from: data)
189 | // edit spec and replace lambda with Swift lambda
190 | let editedSpecTests = spec.tests.compactMap { test -> Spec.Test? in
191 | var test = test
192 | var newTestData: [String: Any] = [:]
193 | guard let dictionary = test.data.value as? [String: Any] else { return nil }
194 | for values in dictionary {
195 | newTestData[values.key] = values.value
196 | }
197 | guard let lambda = lambdaMap[test.name] else { return nil }
198 | newTestData["lambda"] = lambda
199 | test.data = .init(newTestData)
200 | return test
201 | }
202 |
203 | let date = Date()
204 | for test in editedSpecTests {
205 | XCTAssertNoThrow(try test.run())
206 | }
207 | print(-date.timeIntervalSinceNow)
208 | }
209 |
210 | func testCommentsSpec() async throws {
211 | try await self.testSpec(name: "comments")
212 | }
213 |
214 | func testDelimitersSpec() async throws {
215 | try await self.testSpec(name: "delimiters")
216 | }
217 |
218 | func testInterpolationSpec() async throws {
219 | try await self.testSpec(name: "interpolation")
220 | }
221 |
222 | func testInvertedSpec() async throws {
223 | try await self.testSpec(name: "inverted")
224 | }
225 |
226 | func testPartialsSpec() async throws {
227 | try await self.testSpec(name: "partials")
228 | }
229 |
230 | func testSectionsSpec() async throws {
231 | try await self.testSpec(name: "sections")
232 | }
233 |
234 | func testInheritanceSpec() async throws {
235 | try await self.testSpec(
236 | name: "~inheritance",
237 | ignoring: [
238 | "Intrinsic indentation",
239 | "Nested block reindentation",
240 | ]
241 | )
242 | }
243 |
244 | func testDynamicNamesSpec() async throws {
245 | try await self.testSpec(name: "~dynamic-names")
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021 Adam Fowler
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Sources/Mustache/Template+Render.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2024 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | extension MustacheTemplate {
18 | /// Render template using object
19 | /// - Parameters:
20 | /// - stack: Object
21 | /// - context: Context that render is occurring in. Contains information about position in sequence
22 | /// - indentation: indentation of partial
23 | /// - Returns: Rendered text
24 | func render(context: MustacheContext) -> String {
25 | var string = ""
26 | var context = context
27 |
28 | if let indentation = context.indentation, indentation != "" {
29 | for token in tokens {
30 | let renderedString = self.renderToken(token, context: &context)
31 | // if rendered string is not empty and we are on a new line
32 | if renderedString.count > 0, string.last == "\n" {
33 | string += indentation
34 | }
35 | string += renderedString
36 | }
37 | } else {
38 | for token in tokens {
39 | let result = self.renderToken(token, context: &context)
40 | string += result
41 | }
42 | }
43 | return string
44 | }
45 |
46 | func renderToken(_ token: Token, context: inout MustacheContext) -> String {
47 | switch token {
48 | case .text(let text):
49 | return text
50 |
51 | case .variable(let variable, let transforms):
52 | if let child = getChild(named: variable, transforms: transforms, context: context) {
53 | if let template = child as? MustacheTemplate {
54 | return template.render(context: context)
55 | } else if let renderable = child as? MustacheCustomRenderable {
56 | return context.contentType.escapeText(renderable.renderText)
57 | } else if let lambda = child as? MustacheLambda {
58 | return self.renderLambda(lambda, parameter: "", context: context)
59 | } else {
60 | return context.contentType.escapeText(String(describing: unwrapIfAnyContainsOptional(child)))
61 | }
62 | }
63 |
64 | case .unescapedVariable(let variable, let transforms):
65 | if let child = getChild(named: variable, transforms: transforms, context: context) {
66 | if let renderable = child as? MustacheCustomRenderable {
67 | return renderable.renderText
68 | } else if let lambda = child as? MustacheLambda {
69 | return self.renderUnescapedLambda(lambda, parameter: "", context: context)
70 | } else {
71 | return String(describing: unwrapIfAnyContainsOptional(child))
72 | }
73 | }
74 |
75 | case .section(let variable, let transforms, let template):
76 | let child = self.getChild(named: variable, transforms: transforms, context: context)
77 | if let lambda = child as? MustacheLambda {
78 | return self.renderUnescapedLambda(lambda, parameter: template.text, context: context)
79 | }
80 | return self.renderSection(child, with: template, context: context)
81 |
82 | case .invertedSection(let variable, let transforms, let template):
83 | let child = self.getChild(named: variable, transforms: transforms, context: context)
84 | return self.renderInvertedSection(child, with: template, context: context)
85 |
86 | case .blockExpansion(let name, let defaultTemplate, let indented):
87 | if let override = context.inherited?[name] {
88 | return override.render(context: context.withBlockExpansion(indented: indented))
89 | } else {
90 | return defaultTemplate.render(context: context.withBlockExpansion(indented: indented))
91 | }
92 |
93 | case .partial(let name, let indentation, let overrides):
94 | if var template = context.library?.getTemplate(named: name) {
95 | #if DEBUG
96 | if context.reloadPartials {
97 | guard let filename = template.filename else {
98 | preconditionFailure("Can only use reload if template was generated from a file")
99 | }
100 | do {
101 | guard let partialTemplate = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
102 | template = partialTemplate
103 | } catch {
104 | return "\(error)"
105 | }
106 | }
107 | #endif
108 | return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
109 | }
110 |
111 | case .dynamicNamePartial(let name, let indentation, let overrides):
112 | let child = self.getChild(named: name, transforms: [], context: context)
113 | guard let childName = child as? String else {
114 | return ""
115 | }
116 | if var template = context.library?.getTemplate(named: childName) {
117 | #if DEBUG
118 | if context.reloadPartials {
119 | guard let filename = template.filename else {
120 | preconditionFailure("Can only use reload if template was generated from a file")
121 | }
122 | do {
123 | guard let partialTemplate = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
124 | template = partialTemplate
125 | } catch {
126 | return "\(error)"
127 | }
128 | }
129 | #endif
130 | return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
131 | }
132 |
133 | case .contentType(let contentType):
134 | context = context.withContentType(contentType)
135 |
136 | case .blockDefinition:
137 | fatalError("Should not be rendering block definitions")
138 | }
139 | return ""
140 | }
141 |
142 | /// Render a section
143 | /// - Parameters:
144 | /// - child: Object to render section for
145 | /// - parent: Current object being rendered
146 | /// - template: Template to render with
147 | /// - Returns: Rendered text
148 | func renderSection(_ child: Any?, with template: MustacheTemplate, context: MustacheContext) -> String {
149 | switch child {
150 | case let array as any MustacheSequence:
151 | return array.renderSection(with: template, context: context)
152 | case let bool as Bool:
153 | return bool ? template.render(context: context) : ""
154 | case let null as MustacheCustomRenderable where null.isNull == true:
155 | return ""
156 | case .some(let value):
157 | return template.render(context: context.withObject(value))
158 | case .none:
159 | return ""
160 | }
161 | }
162 |
163 | /// Render an inverted section
164 | /// - Parameters:
165 | /// - child: Object to render section for
166 | /// - parent: Current object being rendered
167 | /// - template: Template to render with
168 | /// - Returns: Rendered text
169 | func renderInvertedSection(_ child: Any?, with template: MustacheTemplate, context: MustacheContext) -> String {
170 | switch child {
171 | case let array as any MustacheSequence:
172 | return array.renderInvertedSection(with: template, context: context)
173 | case let bool as Bool:
174 | return bool ? "" : template.render(context: context)
175 | case let null as MustacheCustomRenderable where null.isNull == true:
176 | return template.render(context: context)
177 | case .some:
178 | return ""
179 | case .none:
180 | return template.render(context: context)
181 | }
182 | }
183 |
184 | func renderLambda(_ lambda: MustacheLambda, parameter: String, context: MustacheContext) -> String {
185 | var lambda = lambda
186 | while true {
187 | guard let result = lambda(parameter) else { return "" }
188 | if let string = result as? String {
189 | do {
190 | let newTemplate = try MustacheTemplate(string: context.contentType.escapeText(string))
191 | return self.renderSection(context.stack.last, with: newTemplate, context: context)
192 | } catch {
193 | return ""
194 | }
195 | } else if let lambda2 = result as? MustacheLambda {
196 | lambda = lambda2
197 | continue
198 | } else {
199 | return context.contentType.escapeText(String(describing: unwrapIfAnyContainsOptional(result)))
200 | }
201 | }
202 | }
203 |
204 | func renderUnescapedLambda(_ lambda: MustacheLambda, parameter: String, context: MustacheContext) -> String {
205 | var lambda = lambda
206 | while true {
207 | guard let result = lambda(parameter) else { return "" }
208 | if let string = result as? String {
209 | do {
210 | let newTemplate = try MustacheTemplate(string: string)
211 | return self.renderSection(context.stack.last, with: newTemplate, context: context)
212 | } catch {
213 | return ""
214 | }
215 | } else if let lambda2 = result as? MustacheLambda {
216 | lambda = lambda2
217 | continue
218 | } else {
219 | return String(describing: unwrapIfAnyContainsOptional(result))
220 | }
221 | }
222 | }
223 |
224 | /// Get child object from variable name
225 | func getChild(named name: String, transforms: [String], context: MustacheContext) -> Any? {
226 | func _getImmediateChild(named name: String, from object: Any) -> Any? {
227 | let object = {
228 | if let customBox = object as? MustacheParent {
229 | return customBox.child(named: name)
230 | } else {
231 | let mirror = Mirror(reflecting: object)
232 | return mirror.getValue(forKey: name)
233 | }
234 | }()
235 | return object
236 | }
237 |
238 | func _getChild(named names: ArraySlice, from object: Any) -> Any? {
239 | guard let name = names.first else { return object }
240 | var object = object
241 | if let lambda = object as? MustacheLambda {
242 | guard let result = lambda("") else { return nil }
243 | object = result
244 | }
245 | guard let childObject = _getImmediateChild(named: name, from: object) else { return nil }
246 | let names2 = names.dropFirst()
247 | return _getChild(named: names2, from: childObject)
248 | }
249 |
250 | func _getChildInStack(named names: ArraySlice, from stack: [Any]) -> Any? {
251 | guard let name = names.first else { return stack.last }
252 | for object in stack.reversed() {
253 | if let childObject = _getImmediateChild(named: name, from: object) {
254 | let names2 = names.dropFirst()
255 | return _getChild(named: names2, from: childObject)
256 | }
257 | }
258 | return nil
259 | }
260 |
261 | // work out which object to access. "." means the current object, if the variable name is ""
262 | // and we have a transform to run on the variable then we need the context object, otherwise
263 | // the name is split by "." and we use mirror to get the correct child object. If we cannot find
264 | // the root object we look up the context stack until we can find one with a matching name. The
265 | // stack climbing can be disabled by prefixing the variable name with a "."
266 | let child: Any?
267 | if name == "." {
268 | child = context.stack.last!
269 | } else if name == "", !transforms.isEmpty {
270 | child = context.sequenceContext
271 | } else if name.first == "." {
272 | let nameSplit = name.split(separator: ".").map { String($0) }
273 | child = _getChild(named: nameSplit[...], from: context.stack.last!)
274 | } else {
275 | let nameSplit = name.split(separator: ".").map { String($0) }
276 | child = _getChildInStack(named: nameSplit[...], from: context.stack)
277 | }
278 |
279 | // skip transforms if child is already nil
280 | guard var child else {
281 | return nil
282 | }
283 |
284 | // if we want to run a transform and the current child can have transforms applied to it then
285 | // run transform on the current child
286 | for transform in transforms.reversed() {
287 | if let runnable = child as? MustacheTransformable,
288 | let transformed = runnable.transform(transform)
289 | {
290 | child = transformed
291 | continue
292 | }
293 |
294 | // return nil if transform is unsuccessful or has returned nil
295 | return nil
296 | }
297 |
298 | return child
299 | }
300 |
301 | private func unwrapIfAnyContainsOptional(_ value: Any) -> Any {
302 | guard let opt = value as? AnyOptional else { return value }
303 | return opt.anyWrapped ?? value // nil? keep the original Optional.none
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/Sources/Mustache/Parser.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | /// Reader object for parsing String buffers
18 | struct Parser {
19 | enum Error: Swift.Error {
20 | case overflow
21 | }
22 |
23 | /// internal storage used to store String
24 | private class Storage {
25 | init(_ buffer: String) {
26 | self.buffer = buffer
27 | }
28 |
29 | let buffer: String
30 | }
31 |
32 | private let _storage: Storage
33 |
34 | /// Create a Reader object
35 | /// - Parameter string: String to parse
36 | init(_ string: String) {
37 | self._storage = Storage(string)
38 | self.position = string.startIndex
39 | }
40 |
41 | var buffer: String { self._storage.buffer }
42 | private(set) var position: String.Index
43 | }
44 |
45 | extension Parser {
46 | /// Return current character
47 | /// - Throws: .overflow
48 | /// - Returns: Current character
49 | mutating func character() throws -> Character {
50 | guard !self.reachedEnd() else { throw Parser.Error.overflow }
51 | let c = unsafeCurrent()
52 | unsafeAdvance()
53 | return c
54 | }
55 |
56 | /// Read the current character and return if it is as intended. If character test returns true then move forward 1
57 | /// - Parameter char: character to compare against
58 | /// - Throws: .overflow
59 | /// - Returns: If current character was the one we expected
60 | mutating func read(_ char: Character) throws -> Bool {
61 | let c = try character()
62 | guard c == char else {
63 | unsafeRetreat()
64 | return false
65 | }
66 | return true
67 | }
68 |
69 | /// Read the current character and return if it is as intended. If character test returns true then move forward 1
70 | /// - Parameter char: character to compare against
71 | /// - Throws: .overflow
72 | /// - Returns: If current character was the one we expected
73 | mutating func read(string: String) throws -> Bool {
74 | let initialPosition = self.position
75 | guard string.count > 0 else { return true }
76 | let subString = try read(count: string.count)
77 | guard subString == string else {
78 | self.position = initialPosition
79 | return false
80 | }
81 | return true
82 | }
83 |
84 | /// Read the current character and check if it is in a set of characters If character test returns true then move forward 1
85 | /// - Parameter characterSet: Set of characters to compare against
86 | /// - Throws: .overflow
87 | /// - Returns: If current character is in character set
88 | mutating func read(_ characterSet: Set) throws -> Bool {
89 | let c = try character()
90 | guard characterSet.contains(c) else {
91 | unsafeRetreat()
92 | return false
93 | }
94 | return true
95 | }
96 |
97 | /// Read next so many characters from buffer
98 | /// - Parameter count: Number of characters to read
99 | /// - Throws: .overflow
100 | /// - Returns: The string read from the buffer
101 | mutating func read(count: Int) throws -> Substring {
102 | guard self.buffer.distance(from: self.position, to: self.buffer.endIndex) >= count else { throw Parser.Error.overflow }
103 | let end = self.buffer.index(self.position, offsetBy: count)
104 | let subString = self.buffer[self.position.. Substring {
113 | let string = self.buffer[self.position.. Substring {
123 | let startIndex = self.position
124 | while !self.reachedEnd() {
125 | if unsafeCurrent() == until {
126 | return self.buffer[startIndex.. Substring {
144 | guard untilString.count > 0 else { return "" }
145 | let startIndex = self.position
146 | var foundIndex = self.position
147 | var untilIndex = untilString.startIndex
148 | while !self.reachedEnd() {
149 | if unsafeCurrent() == untilString[untilIndex] {
150 | if untilIndex == untilString.startIndex {
151 | foundIndex = self.position
152 | }
153 | untilIndex = untilString.index(after: untilIndex)
154 | if untilIndex == untilString.endIndex {
155 | unsafeAdvance()
156 | if skipToEnd == false {
157 | self.position = foundIndex
158 | }
159 | let result = self.buffer[startIndex.., throwOnOverflow: Bool = true) throws -> Substring {
179 | let startIndex = self.position
180 | while !self.reachedEnd() {
181 | if characterSet.contains(unsafeCurrent()) {
182 | return self.buffer[startIndex.., throwOnOverflow: Bool = true) throws -> Substring {
198 | let startIndex = self.position
199 | while !self.reachedEnd() {
200 | if current()[keyPath: keyPath] {
201 | return self.buffer[startIndex.. Bool, throwOnOverflow: Bool = true) throws -> Substring {
217 | let startIndex = self.position
218 | while !self.reachedEnd() {
219 | if cb(current()) {
220 | return self.buffer[startIndex.. Substring {
234 | let startIndex = self.position
235 | self.position = self.buffer.endIndex
236 | return self.buffer[startIndex.. Int {
243 | var count = 0
244 | while !self.reachedEnd(),
245 | unsafeCurrent() == `while`
246 | {
247 | unsafeAdvance()
248 | count += 1
249 | }
250 | return count
251 | }
252 |
253 | /// Read while keyPath on character at current position returns true is the one supplied
254 | /// - Parameter while: keyPath to check
255 | /// - Returns: String read from buffer
256 | @discardableResult mutating func read(while keyPath: KeyPath) -> Substring {
257 | let startIndex = self.position
258 | while !self.reachedEnd(),
259 | unsafeCurrent()[keyPath: keyPath]
260 | {
261 | unsafeAdvance()
262 | }
263 | return self.buffer[startIndex.. Bool) -> Substring {
270 | let startIndex = self.position
271 | while !self.reachedEnd(),
272 | cb(unsafeCurrent())
273 | {
274 | unsafeAdvance()
275 | }
276 | return self.buffer[startIndex..) -> Substring {
283 | let startIndex = self.position
284 | while !self.reachedEnd(),
285 | characterSet.contains(unsafeCurrent())
286 | {
287 | unsafeAdvance()
288 | }
289 | return self.buffer[startIndex.. Bool {
295 | self.position == self.buffer.endIndex
296 | }
297 |
298 | /// Return whether we are at the start of the buffer
299 | /// - Returns: Are we are the start
300 | func atStart() -> Bool {
301 | self.position == self.buffer.startIndex
302 | }
303 | }
304 |
305 | /// context used in parser error
306 | public struct MustacheParserContext: Sendable {
307 | public let line: String
308 | public let lineNumber: Int
309 | public let columnNumber: Int
310 | }
311 |
312 | extension Parser {
313 | /// Return context of current position (line, lineNumber, columnNumber)
314 | func getContext() -> MustacheParserContext {
315 | var parser = self
316 | var columnNumber = 0
317 | while !parser.atStart() {
318 | try? parser.retreat()
319 | if parser.current() == "\n" {
320 | break
321 | }
322 | columnNumber += 1
323 | }
324 | if parser.current() == "\n" {
325 | try? parser.advance()
326 | }
327 | // read line from parser
328 | let line = try! parser.read(until: Character("\n"), throwOnOverflow: false)
329 | // count new lines up to this current position
330 | let buffer = parser.buffer
331 | let textBefore = buffer[buffer.startIndex.. Character {
344 | guard !self.reachedEnd() else { return "\0" }
345 | return unsafeCurrent()
346 | }
347 |
348 | /// Move forward one character
349 | /// - Throws: .overflow
350 | mutating func advance() throws {
351 | guard !self.reachedEnd() else { throw Parser.Error.overflow }
352 | return unsafeAdvance()
353 | }
354 |
355 | /// Move back one character
356 | /// - Throws: .overflow
357 | mutating func retreat() throws {
358 | guard self.position != self.buffer.startIndex else { throw Parser.Error.overflow }
359 | return unsafeRetreat()
360 | }
361 |
362 | /// Move forward so many character
363 | /// - Parameter amount: number of characters to move forward
364 | /// - Throws: .overflow
365 | mutating func advance(by amount: Int) throws {
366 | guard self.buffer.distance(from: self.position, to: self.buffer.endIndex) >= amount else { throw Parser.Error.overflow }
367 | return unsafeAdvance(by: amount)
368 | }
369 |
370 | /// Move back so many characters
371 | /// - Parameter amount: number of characters to move back
372 | /// - Throws: .overflow
373 | mutating func retreat(by amount: Int) throws {
374 | guard self.buffer.distance(from: self.buffer.startIndex, to: self.position) >= amount else { throw Parser.Error.overflow }
375 | return unsafeRetreat(by: amount)
376 | }
377 |
378 | mutating func setPosition(_ position: String.Index) throws {
379 | guard position <= self.buffer.endIndex else { throw Parser.Error.overflow }
380 | unsafeSetPosition(position)
381 | }
382 | }
383 |
384 | // unsafe versions without checks
385 | extension Parser {
386 | func unsafeCurrent() -> Character {
387 | self.buffer[self.position]
388 | }
389 |
390 | mutating func unsafeAdvance() {
391 | self.position = self.buffer.index(after: self.position)
392 | }
393 |
394 | mutating func unsafeRetreat() {
395 | self.position = self.buffer.index(before: self.position)
396 | }
397 |
398 | mutating func unsafeAdvance(by amount: Int) {
399 | self.position = self.buffer.index(self.position, offsetBy: amount)
400 | }
401 |
402 | mutating func unsafeRetreat(by amount: Int) {
403 | self.position = self.buffer.index(self.position, offsetBy: -amount)
404 | }
405 |
406 | mutating func unsafeSetPosition(_ position: String.Index) {
407 | self.position = position
408 | }
409 | }
410 |
--------------------------------------------------------------------------------
/Tests/MustacheTests/TemplateRendererTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Mustache
16 | import XCTest
17 |
18 | final class TemplateRendererTests: XCTestCase {
19 | func testText() throws {
20 | let template = try MustacheTemplate(string: "test text")
21 | XCTAssertEqual(template.render("test"), "test text")
22 | }
23 |
24 | func testStringVariable() throws {
25 | let template = try MustacheTemplate(string: "test {{.}}")
26 | XCTAssertEqual(template.render("text"), "test text")
27 | }
28 |
29 | func testIntegerVariable() throws {
30 | let template = try MustacheTemplate(string: "test {{.}}")
31 | XCTAssertEqual(template.render(101), "test 101")
32 | }
33 |
34 | func testDictionary() throws {
35 | let template = try MustacheTemplate(string: "test {{value}} {{bool}}")
36 | XCTAssertEqual(template.render(["value": "test2", "bool": true]), "test test2 true")
37 | }
38 |
39 | func testArraySection() throws {
40 | let template = try MustacheTemplate(string: "test {{#value}}*{{.}}{{/value}}")
41 | XCTAssertEqual(template.render(["value": ["test2", "bool"]]), "test *test2*bool")
42 | XCTAssertEqual(template.render(["value": ["test2"]]), "test *test2")
43 | XCTAssertEqual(template.render(["value": []]), "test ")
44 | }
45 |
46 | func testBooleanSection() throws {
47 | let template = try MustacheTemplate(string: "test {{#.}}Yep{{/.}}")
48 | XCTAssertEqual(template.render(true), "test Yep")
49 | XCTAssertEqual(template.render(false), "test ")
50 | }
51 |
52 | func testIntegerSection() throws {
53 | let template = try MustacheTemplate(string: "test {{#.}}{{.}}{{/.}}")
54 | XCTAssertEqual(template.render(23), "test 23")
55 | }
56 |
57 | func testStringSection() throws {
58 | let template = try MustacheTemplate(string: "test {{#.}}{{.}}{{/.}}")
59 | XCTAssertEqual(template.render("Hello"), "test Hello")
60 | }
61 |
62 | func testInvertedSection() throws {
63 | let template = try MustacheTemplate(string: "test {{^.}}Inverted{{/.}}")
64 | XCTAssertEqual(template.render(true), "test ")
65 | XCTAssertEqual(template.render(false), "test Inverted")
66 | }
67 |
68 | func testMirror() throws {
69 | struct Test {
70 | let string: String
71 | }
72 | let template = try MustacheTemplate(string: "test {{string}}")
73 | XCTAssertEqual(template.render(Test(string: "string")), "test string")
74 | }
75 |
76 | func testOptionalMirror() throws {
77 | struct Test {
78 | let string: String?
79 | }
80 | let template = try MustacheTemplate(string: "test {{string}}")
81 | XCTAssertEqual(template.render(Test(string: "string")), "test string")
82 | XCTAssertEqual(template.render(Test(string: nil)), "test ")
83 | }
84 |
85 | func testOptionalSection() throws {
86 | struct Test {
87 | let string: String?
88 | }
89 | let template = try MustacheTemplate(string: "test {{#string}}*{{.}}{{/string}}")
90 | XCTAssertEqual(template.render(Test(string: "string")), "test *string")
91 | XCTAssertEqual(template.render(Test(string: nil)), "test ")
92 | let template2 = try MustacheTemplate(string: "test {{^string}}*{{/string}}")
93 | XCTAssertEqual(template2.render(Test(string: "string")), "test ")
94 | XCTAssertEqual(template2.render(Test(string: nil)), "test *")
95 | }
96 |
97 | func testOptionalSequence() throws {
98 | struct Test {
99 | let string: String?
100 | }
101 | let template = try MustacheTemplate(string: "test {{#.}}{{string}}{{/.}}")
102 | XCTAssertEqual(template.render([Test(string: "string")]), "test string")
103 | }
104 |
105 | func testOptionalSequenceSection() throws {
106 | struct Test {
107 | let string: String?
108 | }
109 | let template = try MustacheTemplate(string: "test {{#.}}{{#string}}*{{.}}{{/string}}{{/.}}")
110 | XCTAssertEqual(template.render([Test(string: "string")]), "test *string")
111 | }
112 |
113 | func testStructureInStructure() throws {
114 | struct SubTest {
115 | let string: String?
116 | }
117 | struct Test {
118 | let test: SubTest
119 | }
120 |
121 | let template = try MustacheTemplate(string: "test {{test.string}}")
122 | XCTAssertEqual(template.render(Test(test: .init(string: "sub"))), "test sub")
123 | }
124 |
125 | func testTextEscaping() throws {
126 | let template1 = try MustacheTemplate(string: "{{% CONTENT_TYPE:TEXT}}{{.}}")
127 | XCTAssertEqual(template1.render("<>"), "<>")
128 | let template2 = try MustacheTemplate(string: "{{% CONTENT_TYPE:HTML}}{{.}}")
129 | XCTAssertEqual(template2.render("<>"), "<>")
130 | }
131 |
132 | func testStopClimbingStack() throws {
133 | let template1 = try MustacheTemplate(string: "{{#test}}{{name}}{{/test}}")
134 | let template2 = try MustacheTemplate(string: "{{#test}}{{.name}}{{/test}}")
135 | let object: [String: Any] = ["test": [:], "name": "John"]
136 | let object2: [String: Any] = ["test": ["name": "Jane"], "name": "John"]
137 | XCTAssertEqual(template1.render(object), "John")
138 | XCTAssertEqual(template2.render(object), "")
139 | XCTAssertEqual(template2.render(object2), "Jane")
140 | }
141 |
142 | /// variables
143 | func testMustacheManualVariables() throws {
144 | let template = try MustacheTemplate(
145 | string: """
146 | Hello {{name}}
147 | You have just won {{value}} dollars!
148 | {{#in_ca}}
149 | Well, {{taxed_value}} dollars, after taxes.
150 | {{/in_ca}}
151 | """
152 | )
153 | let object: [String: Any] = ["name": "Chris", "value": 10000, "taxed_value": 10000 - (10000 * 0.4), "in_ca": true]
154 | XCTAssertEqual(
155 | template.render(object),
156 | """
157 | Hello Chris
158 | You have just won 10000 dollars!
159 | Well, 6000.0 dollars, after taxes.
160 |
161 | """
162 | )
163 | }
164 |
165 | /// test escaped and unescaped text
166 | func testMustacheManualEscapedText() throws {
167 | let template = try MustacheTemplate(
168 | string: """
169 | *{{name}}
170 | *{{age}}
171 | *{{company}}
172 | *{{{company}}}
173 | """
174 | )
175 | let object: [String: Any] = ["name": "Chris", "company": "GitHub"]
176 | XCTAssertEqual(
177 | template.render(object),
178 | """
179 | *Chris
180 | *
181 | *<b>GitHub</b>
182 | *GitHub
183 | """
184 | )
185 | }
186 |
187 | /// test dotted names
188 | func test_MustacheManualDottedNames() throws {
189 | let template = try MustacheTemplate(
190 | string: """
191 | * {{client.name}}
192 | * {{age}}
193 | * {{client.company.name}}
194 | * {{{company.name}}}
195 | """
196 | )
197 | let object: [String: Any] = [
198 | "client": (
199 | name: "Chris & Friends",
200 | age: 50
201 | ),
202 | "company": [
203 | "name": "GitHub"
204 | ],
205 | ]
206 | XCTAssertEqual(
207 | template.render(object),
208 | """
209 | * Chris & Friends
210 | *
211 | *
212 | * GitHub
213 | """
214 | )
215 | }
216 |
217 | /// test implicit operator
218 | func testMustacheManualImplicitOperator() throws {
219 | let template = try MustacheTemplate(
220 | string: """
221 | * {{.}}
222 | """
223 | )
224 | let object = "Hello!"
225 | XCTAssertEqual(
226 | template.render(object),
227 | """
228 | * Hello!
229 | """
230 | )
231 | }
232 |
233 | /// test lambda
234 | func test_MustacheManualLambda() throws {
235 | let template = try MustacheTemplate(
236 | string: """
237 | * {{time.hour}}
238 | * {{today}}
239 | """
240 | )
241 | let object: [String: Any] = [
242 | "year": 1970,
243 | "month": 1,
244 | "day": 1,
245 | "time": MustacheLambda { _ in
246 | (
247 | hour: 0,
248 | minute: 0,
249 | second: 0
250 | )
251 | },
252 | "today": MustacheLambda { _ in
253 | "{{year}}-{{month}}-{{day}}"
254 | },
255 | ]
256 | XCTAssertEqual(
257 | template.render(object),
258 | """
259 | * 0
260 | * 1970-1-1
261 | """
262 | )
263 | }
264 |
265 | /// test boolean
266 | func testMustacheManualSectionFalse() throws {
267 | let template = try MustacheTemplate(
268 | string: """
269 | Shown.
270 | {{#person}}
271 | Never shown!
272 | {{/person}}
273 | """
274 | )
275 | let object: [String: Any] = ["person": false]
276 | XCTAssertEqual(
277 | template.render(object),
278 | """
279 | Shown.
280 |
281 | """
282 | )
283 | }
284 |
285 | /// test non-empty lists
286 | func testMustacheManualSectionList() throws {
287 | let template = try MustacheTemplate(
288 | string: """
289 | {{#repo}}
290 | {{name}}
291 | {{/repo}}
292 | """
293 | )
294 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
295 | XCTAssertEqual(
296 | template.render(object),
297 | """
298 | resque
299 | hub
300 | rip
301 |
302 | """
303 | )
304 | }
305 |
306 | /// test non-empty lists
307 | func testMustacheManualSectionList2() throws {
308 | let template = try MustacheTemplate(
309 | string: """
310 | {{#repo}}
311 | {{.}}
312 | {{/repo}}
313 | """
314 | )
315 | let object: [String: Any] = ["repo": ["resque", "hub", "rip"]]
316 | XCTAssertEqual(
317 | template.render(object),
318 | """
319 | resque
320 | hub
321 | rip
322 |
323 | """
324 | )
325 | }
326 |
327 | /// test lambdas
328 | func testMustacheManualSectionLambda() throws {
329 | let template = try MustacheTemplate(
330 | string: """
331 | {{#wrapped}}{{name}} is awesome.{{/wrapped}}
332 | """
333 | )
334 | func wrapped(_ s: String) -> Any? {
335 | "\(s)"
336 | }
337 | let object: [String: Any] = ["name": "Willy", "wrapped": MustacheLambda(wrapped)]
338 | XCTAssertEqual(
339 | template.render(object),
340 | """
341 | Willy is awesome.
342 | """
343 | )
344 | }
345 |
346 | /// test setting context object
347 | func testMustacheManualContextObject() throws {
348 | let template = try MustacheTemplate(
349 | string: """
350 | {{#person?}}
351 | Hi {{name}}!
352 | {{/person?}}
353 | """
354 | )
355 | let object: [String: Any] = ["person?": ["name": "Jon"]]
356 | XCTAssertEqual(
357 | template.render(object),
358 | """
359 | Hi Jon!
360 |
361 | """
362 | )
363 | }
364 |
365 | /// test inverted sections
366 | func testMustacheManualInvertedSection() throws {
367 | let template = try MustacheTemplate(
368 | string: """
369 | {{#repo}}
370 | {{name}}
371 | {{/repo}}
372 | {{^repo}}
373 | No repos :(
374 | {{/repo}}
375 | """
376 | )
377 | let object: [String: Any] = ["repo": []]
378 | XCTAssertEqual(
379 | template.render(object),
380 | """
381 | No repos :(
382 |
383 | """
384 | )
385 | }
386 |
387 | /// test comments
388 | func testMustacheManualComment() throws {
389 | let template = try MustacheTemplate(
390 | string: """
391 | Today{{! ignore me }}.
392 | """
393 | )
394 | let object: [String: Any] = ["repo": []]
395 | XCTAssertEqual(
396 | template.render(object),
397 | """
398 | Today.
399 | """
400 | )
401 | }
402 |
403 | /// test dynamic names
404 | func testMustacheManualDynamicNames() throws {
405 | var library = MustacheLibrary()
406 | try library.register(
407 | "Hello {{>*dynamic}}",
408 | named: "main"
409 | )
410 | try library.register(
411 | "everyone!",
412 | named: "world"
413 | )
414 | let object = ["dynamic": "world"]
415 | XCTAssertEqual(library.render(object, withTemplate: "main"), "Hello everyone!")
416 | }
417 |
418 | /// test block with defaults
419 | func testMustacheManualBlocksWithDefaults() throws {
420 | let template = try MustacheTemplate(
421 | string: """
422 | {{$title}}The News of Today{{/title}}
423 | {{$body}}
424 | Nothing special happened.
425 | {{/body}}
426 |
427 | """
428 | )
429 | XCTAssertEqual(
430 | template.render([]),
431 | """
432 | The News of Today
433 | Nothing special happened.
434 |
435 | """
436 | )
437 | }
438 |
439 | func testMustacheManualParents() throws {
440 | var library = MustacheLibrary()
441 | try library.register(
442 | """
443 | {{{{.}}
448 | {{/headlines}}
449 | {{/body}}
450 | {{/article}}
451 |
452 | {{{{$title}}The News of Today{{/title}}
462 | {{$body}}
463 | Nothing special happened.
464 | {{/body}}
465 |
466 | """,
467 | named: "article"
468 | )
469 | let object = [
470 | "headlines": [
471 | "A pug's handler grew mustaches.",
472 | "What an exciting day!",
473 | ]
474 | ]
475 | XCTAssertEqual(
476 | library.render(object, withTemplate: "main"),
477 | """
478 | The News of Today
479 | A pug's handler grew mustaches.
480 | What an exciting day!
481 |
482 | Yesterday
483 | Nothing special happened.
484 |
485 | """
486 | )
487 | }
488 |
489 | func testMustacheManualDynamicNameParents() throws {
490 | var library = MustacheLibrary()
491 | try library.register(
492 | """
493 | {{<*dynamic}}
494 | {{$text}}Hello World!{{/text}}
495 | {{/*dynamic}}
496 |
497 | """,
498 | named: "dynamic"
499 | )
500 | try library.register(
501 | """
502 | {{$text}}Here goes nothing.{{/text}}
503 | """,
504 | named: "normal"
505 | )
506 | try library.register(
507 | """
508 | {{$text}}Here also goes nothing but it's bold.{{/text}}
509 | """,
510 | named: "bold"
511 | )
512 | let object = ["dynamic": "bold"]
513 | XCTAssertEqual(
514 | library.render(object, withTemplate: "dynamic"),
515 | """
516 | Hello World!
517 | """
518 | )
519 | }
520 |
521 | /// test MustacheCustomRenderable
522 | func testCustomRenderable() throws {
523 | let template = try MustacheTemplate(string: "{{.}}")
524 | let template1 = try MustacheTemplate(string: "{{#.}}not null{{/.}}")
525 | let template2 = try MustacheTemplate(string: "{{^.}}null{{/.}}")
526 | struct Object: MustacheCustomRenderable {
527 | let value: String
528 |
529 | var renderText: String { self.value.uppercased() }
530 | var isNull: Bool { self.value == "null" }
531 | }
532 | let testObject = Object(value: "test")
533 | let nullObject = Object(value: "null")
534 | XCTAssertEqual(template.render(testObject), "TEST")
535 | XCTAssertEqual(template1.render(testObject), "not null")
536 | XCTAssertEqual(template1.render(nullObject), "")
537 | XCTAssertEqual(template2.render(testObject), "")
538 | XCTAssertEqual(template2.render(nullObject), "null")
539 | }
540 |
541 | func testTypeErasedOptionalContext() throws {
542 | let object = ["name": "Test" as Any?]
543 |
544 | let template = try MustacheTemplate(string: "{{name}}")
545 | let result = template.render(object)
546 |
547 | XCTAssertEqual(result, "Test")
548 | }
549 |
550 | func testPerformance() throws {
551 | let template = try MustacheTemplate(
552 | string: """
553 | {{#repo}}
554 | {{name}}
555 | {{/repo}}
556 | """
557 | )
558 | let object: [String: Any] = ["repo": [["name": "resque"], ["name": "hub"], ["name": "rip"]]]
559 | let date = Date()
560 | for _ in 1...10000 {
561 | _ = template.render(object)
562 | }
563 | print(-date.timeIntervalSinceNow)
564 | }
565 | }
566 |
--------------------------------------------------------------------------------
/Sources/Mustache/Template+Parser.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Hummingbird server framework project
4 | //
5 | // Copyright (c) 2021-2021 the Hummingbird authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | extension MustacheTemplate {
16 | /// Error return by `MustacheTemplate.parse`. Includes information about where error occurred
17 | public struct ParserError: Swift.Error {
18 | public let context: MustacheParserContext
19 | public let error: Swift.Error
20 | }
21 |
22 | /// Error generated by `MustacheTemplate.parse`
23 | public enum Error: Swift.Error {
24 | /// the end section does not match the name of the start section
25 | case sectionCloseNameIncorrect
26 | /// tag was badly formatted
27 | case unfinishedName
28 | /// was expecting a section end
29 | case expectedSectionEnd
30 | /// set delimiter tag badly formatted
31 | case invalidSetDelimiter
32 | /// cannot apply transform to inherited section
33 | case transformAppliedToInheritanceSection
34 | /// illegal token inside inherit section of partial
35 | case illegalTokenInsideInheritSection
36 | /// text found inside inherit section of partial
37 | case textInsideInheritSection
38 | /// config variable syntax is wrong
39 | case invalidConfigVariableSyntax
40 | /// unrecognised config variable
41 | case unrecognisedConfigVariable
42 | }
43 |
44 | struct ParserState {
45 | struct Flags: OptionSet {
46 | let rawValue: Int
47 |
48 | init(rawValue: Int) {
49 | self.rawValue = rawValue
50 | }
51 |
52 | static var newLine: Self { .init(rawValue: 1 << 0) }
53 | static var isPartialDefinition: Self { .init(rawValue: 1 << 1) }
54 | static var isPartialDefinitionTopLevel: Self { .init(rawValue: 1 << 2) }
55 | }
56 |
57 | var sectionName: String?
58 | var sectionTransforms: [String] = []
59 | var flags: Flags
60 | var startDelimiter: String
61 | var endDelimiter: String
62 | var partialDefinitionIndent: Substring?
63 |
64 | var newLine: Bool {
65 | get { self.flags.contains(.newLine) }
66 | set {
67 | if newValue {
68 | self.flags.insert(.newLine)
69 | } else {
70 | self.flags.remove(.newLine)
71 | }
72 | }
73 | }
74 |
75 | init() {
76 | self.sectionName = nil
77 | self.flags = .newLine
78 | self.startDelimiter = "{{"
79 | self.endDelimiter = "}}"
80 | }
81 |
82 | func withSectionName(_ name: String, newLine: Bool, transforms: [String] = []) -> ParserState {
83 | var newValue = self
84 | newValue.sectionName = name
85 | newValue.sectionTransforms = transforms
86 | newValue.flags.remove(.isPartialDefinitionTopLevel)
87 | if !newLine {
88 | newValue.flags.remove(.newLine)
89 | }
90 | return newValue
91 | }
92 |
93 | func withInheritancePartial(_ name: String) -> ParserState {
94 | var newValue = self
95 | newValue.sectionName = name
96 | newValue.flags.insert([.newLine, .isPartialDefinition, .isPartialDefinitionTopLevel])
97 | return newValue
98 | }
99 |
100 | func withDelimiters(start: String, end: String) -> ParserState {
101 | var newValue = self
102 | newValue.startDelimiter = start
103 | newValue.endDelimiter = end
104 | newValue.flags.remove(.isPartialDefinitionTopLevel)
105 | return newValue
106 | }
107 | }
108 |
109 | /// parse mustache text to generate a list of tokens
110 | static func parse(_ string: String) throws -> MustacheTemplate {
111 | var parser = Parser(string)
112 | do {
113 | return try self.parse(&parser, state: .init())
114 | } catch {
115 | throw ParserError(context: parser.getContext(), error: error)
116 | }
117 | }
118 |
119 | /// parse section in mustache text
120 | static func parse(_ parser: inout Parser, state: ParserState) throws -> MustacheTemplate {
121 | var tokens: [Token] = []
122 | var state = state
123 | var whiteSpaceBefore: Substring = ""
124 | var origParser = parser
125 | while !parser.reachedEnd() {
126 | // if new line read whitespace
127 | if state.newLine {
128 | let whiteSpace = parser.read(while: Set(" \t"))
129 | // If inside a partial block definition
130 | if state.flags.contains(.isPartialDefinition), !state.flags.contains(.isPartialDefinitionTopLevel) {
131 | // if definition indent has been set then remove it from current whitespace otherwise set the
132 | // indent as this is the first line of the partial definition
133 | if let partialDefinitionIndent = state.partialDefinitionIndent {
134 | whiteSpaceBefore = whiteSpace.dropFirst(partialDefinitionIndent.count)
135 | } else {
136 | state.partialDefinitionIndent = whiteSpace
137 | }
138 | } else {
139 | whiteSpaceBefore = whiteSpace
140 | }
141 | }
142 | let text = try readUntilDelimiterOrNewline(&parser, state: state)
143 | // if we hit a newline add text
144 | if parser.current().isNewline {
145 | tokens.append(.text(whiteSpaceBefore + text + String(parser.current())))
146 | state.newLine = true
147 | parser.unsafeAdvance()
148 | continue
149 | }
150 | // we have found a tag
151 | // whatever text we found before the tag should be added as a token
152 | if text.count > 0 {
153 | tokens.append(.text(whiteSpaceBefore + text))
154 | whiteSpaceBefore = ""
155 | state.newLine = false
156 | }
157 | // have we reached the end of the text
158 | if parser.reachedEnd() {
159 | break
160 | }
161 | var setNewLine = false
162 | switch parser.current() {
163 | case "#":
164 | // section
165 | parser.unsafeAdvance()
166 | let (name, transforms) = try parseName(&parser, state: state)
167 | if self.isStandalone(&parser, state: state) {
168 | setNewLine = true
169 | } else if whiteSpaceBefore.count > 0 {
170 | tokens.append(.text(String(whiteSpaceBefore)))
171 | whiteSpaceBefore = ""
172 | }
173 | let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
174 | tokens.append(.section(name: name, transforms: transforms, template: sectionTemplate))
175 |
176 | case "^":
177 | // inverted section
178 | parser.unsafeAdvance()
179 | let (name, transforms) = try parseName(&parser, state: state)
180 | if self.isStandalone(&parser, state: state) {
181 | setNewLine = true
182 | } else if whiteSpaceBefore.count > 0 {
183 | tokens.append(.text(String(whiteSpaceBefore)))
184 | whiteSpaceBefore = ""
185 | }
186 | let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
187 | tokens.append(.invertedSection(name: name, transforms: transforms, template: sectionTemplate))
188 |
189 | case "/":
190 | // end of section
191 |
192 | // record end of section text
193 | var sectionParser = parser
194 | sectionParser.unsafeRetreat()
195 | sectionParser.unsafeRetreat()
196 |
197 | parser.unsafeAdvance()
198 | let position = parser.position
199 | let (name, transforms) = try parseName(&parser, state: state)
200 | guard name == state.sectionName, transforms == state.sectionTransforms else {
201 | parser.unsafeSetPosition(position)
202 | throw Error.sectionCloseNameIncorrect
203 | }
204 | if self.isStandalone(&parser, state: state) {
205 | setNewLine = true
206 | } else if whiteSpaceBefore.count > 0 {
207 | tokens.append(.text(String(whiteSpaceBefore)))
208 | whiteSpaceBefore = ""
209 | }
210 | return .init(tokens, text: String(origParser.read(until: sectionParser.position)))
211 |
212 | case "!":
213 | // comment
214 | parser.unsafeAdvance()
215 | _ = try self.parseComment(&parser, state: state)
216 | setNewLine = self.isStandalone(&parser, state: state)
217 |
218 | case "{":
219 | // unescaped variable
220 | if whiteSpaceBefore.count > 0 {
221 | tokens.append(.text(String(whiteSpaceBefore)))
222 | whiteSpaceBefore = ""
223 | }
224 | parser.unsafeAdvance()
225 | let (name, transforms) = try parseName(&parser, state: state)
226 | guard try parser.read("}") else { throw Error.unfinishedName }
227 | tokens.append(.unescapedVariable(name: name, transforms: transforms))
228 |
229 | case "&":
230 | // unescaped variable
231 | if whiteSpaceBefore.count > 0 {
232 | tokens.append(.text(String(whiteSpaceBefore)))
233 | whiteSpaceBefore = ""
234 | }
235 | parser.unsafeAdvance()
236 | let (name, transforms) = try parseName(&parser, state: state)
237 | tokens.append(.unescapedVariable(name: name, transforms: transforms))
238 |
239 | case ">":
240 | // partial
241 | parser.unsafeAdvance()
242 | // skip whitespace
243 | parser.read(while: \.isWhitespace)
244 | var dynamic = false
245 | if parser.current() == "*" {
246 | parser.unsafeAdvance()
247 | dynamic = true
248 | }
249 | let name = try parsePartialName(&parser, state: state)
250 | if whiteSpaceBefore.count > 0 {
251 | tokens.append(.text(String(whiteSpaceBefore)))
252 | }
253 | if self.isStandalone(&parser, state: state) {
254 | setNewLine = true
255 | if dynamic {
256 | tokens.append(.dynamicNamePartial(name, indentation: String(whiteSpaceBefore), inherits: nil))
257 | } else {
258 | tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
259 | }
260 | } else {
261 | if dynamic {
262 | tokens.append(.dynamicNamePartial(name, indentation: nil, inherits: nil))
263 | } else {
264 | tokens.append(.partial(name, indentation: nil, inherits: nil))
265 | }
266 | }
267 | whiteSpaceBefore = ""
268 |
269 | case "<":
270 | // partial with inheritance
271 | parser.unsafeAdvance()
272 | // skip whitespace
273 | parser.read(while: \.isWhitespace)
274 | let sectionName = try parsePartialName(&parser, state: state)
275 | let name: String
276 | let dynamic: Bool
277 | if sectionName.first == "*" {
278 | dynamic = true
279 | name = String(sectionName.dropFirst())
280 | } else {
281 | dynamic = false
282 | name = sectionName
283 | }
284 | if whiteSpaceBefore.count > 0 {
285 | tokens.append(.text(String(whiteSpaceBefore)))
286 | }
287 | if self.isStandalone(&parser, state: state) {
288 | setNewLine = true
289 | }
290 | let sectionTemplate = try parse(&parser, state: state.withInheritancePartial(sectionName))
291 | var inherit: [String: MustacheTemplate] = [:]
292 | // parse tokens in section to extract inherited sections
293 | for token in sectionTemplate.tokens {
294 | switch token {
295 | case .blockDefinition(let name, let template):
296 | inherit[name] = template
297 | case .text:
298 | break
299 | default:
300 | throw Error.illegalTokenInsideInheritSection
301 | }
302 | }
303 | if dynamic {
304 | tokens.append(.dynamicNamePartial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
305 | } else {
306 | tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
307 | }
308 | whiteSpaceBefore = ""
309 |
310 | case "$":
311 | // inherited section
312 | parser.unsafeAdvance()
313 | let (name, transforms) = try parseName(&parser, state: state)
314 | // ERROR: can't have transforms applied to inherited sections
315 | guard transforms.isEmpty else { throw Error.transformAppliedToInheritanceSection }
316 | if state.flags.contains(.isPartialDefinitionTopLevel) {
317 | let standAlone = self.isStandalone(&parser, state: state)
318 | if standAlone {
319 | setNewLine = true
320 | }
321 | let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
322 | tokens.append(.blockDefinition(name: name, template: sectionTemplate))
323 |
324 | } else {
325 | if whiteSpaceBefore.count > 0 {
326 | tokens.append(.text(String(whiteSpaceBefore)))
327 | }
328 | if self.isStandalone(&parser, state: state) {
329 | setNewLine = true
330 | } else if whiteSpaceBefore.count > 0 {
331 | }
332 | let sectionTemplate = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
333 | tokens.append(.blockExpansion(name: name, default: sectionTemplate, indentation: String(whiteSpaceBefore)))
334 | whiteSpaceBefore = ""
335 | }
336 |
337 | case "=":
338 | // set delimiter
339 | parser.unsafeAdvance()
340 | state = try self.parserSetDelimiter(&parser, state: state)
341 | setNewLine = self.isStandalone(&parser, state: state)
342 |
343 | case "%":
344 | // read config variable
345 | parser.unsafeAdvance()
346 | if let token = try self.readConfigVariable(&parser, state: state) {
347 | tokens.append(token)
348 | }
349 | setNewLine = self.isStandalone(&parser, state: state)
350 |
351 | default:
352 | // variable
353 | if whiteSpaceBefore.count > 0 {
354 | tokens.append(.text(String(whiteSpaceBefore)))
355 | whiteSpaceBefore = ""
356 | }
357 | let (name, transforms) = try parseName(&parser, state: state)
358 | tokens.append(.variable(name: name, transforms: transforms))
359 | }
360 | state.newLine = setNewLine
361 | }
362 | // should never get here if reading section
363 | guard state.sectionName == nil else {
364 | throw Error.expectedSectionEnd
365 | }
366 | return .init(tokens, text: String(origParser.read(until: parser.position)))
367 | }
368 |
369 | /// read until we hit either the start delimiter of a tag or a newline
370 | static func readUntilDelimiterOrNewline(_ parser: inout Parser, state: ParserState) throws -> String {
371 | var untilSet: Set = ["\n", "\r\n"]
372 | guard let delimiterFirstChar = state.startDelimiter.first else { return "" }
373 | var totalText = ""
374 | untilSet.insert(delimiterFirstChar)
375 |
376 | while !parser.reachedEnd() {
377 | // read until we hit either a newline or "{"
378 | let text = try parser.read(until: untilSet, throwOnOverflow: false)
379 | totalText += text
380 | // if new line append all text read plus newline
381 | if parser.current().isNewline {
382 | break
383 | } else if parser.current() == delimiterFirstChar {
384 | if try parser.read(string: state.startDelimiter) {
385 | break
386 | }
387 | totalText += String(delimiterFirstChar)
388 | parser.unsafeAdvance()
389 | }
390 | }
391 | return totalText
392 | }
393 |
394 | /// parse variable name
395 | static func parseName(_ parser: inout Parser, state: ParserState) throws -> (String, [String]) {
396 | parser.read(while: \.isWhitespace)
397 | let text = String(parser.read(while: self.sectionNameChars))
398 | parser.read(while: \.isWhitespace)
399 | guard try parser.read(string: state.endDelimiter) else { throw Error.unfinishedName }
400 |
401 | // does the name include brackets. If so this is a transform call
402 | var nameParser = Parser(String(text))
403 | let string = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
404 | if nameParser.reachedEnd() {
405 | return (text, [])
406 | } else {
407 | // parse function parameter, as we have just parsed a function name
408 | guard nameParser.current() == "(" else { throw Error.unfinishedName }
409 | nameParser.unsafeAdvance()
410 |
411 | func parseTransforms(existing: [Substring]) throws -> (Substring, [Substring]) {
412 | let name = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
413 | switch nameParser.current() {
414 | case ")":
415 | // Transforms are ending
416 | nameParser.unsafeAdvance()
417 | // We need to have a `)` for each transform that we've parsed
418 | guard nameParser.read(while: ")") + 1 == existing.count,
419 | nameParser.reachedEnd()
420 | else {
421 | throw Error.unfinishedName
422 | }
423 | return (name, existing)
424 | case "(":
425 | // Parse the next transform
426 | nameParser.unsafeAdvance()
427 |
428 | var transforms = existing
429 | transforms.append(name)
430 | return try parseTransforms(existing: transforms)
431 | default:
432 | throw Error.unfinishedName
433 | }
434 | }
435 | let (parameterName, transforms) = try parseTransforms(existing: [string])
436 |
437 | return (String(parameterName), transforms.map(String.init))
438 | }
439 | }
440 |
441 | /// parse partial name
442 | static func parsePartialName(_ parser: inout Parser, state: ParserState) throws -> String {
443 | parser.read(while: \.isWhitespace)
444 | let text = String(parser.read(while: self.partialNameChars))
445 | parser.read(while: \.isWhitespace)
446 | guard try parser.read(string: state.endDelimiter) else { throw Error.unfinishedName }
447 | return text
448 | }
449 |
450 | static func parseComment(_ parser: inout Parser, state: ParserState) throws -> String {
451 | let text = try parser.read(untilString: state.endDelimiter, throwOnOverflow: true, skipToEnd: true)
452 | return String(text)
453 | }
454 |
455 | static func parserSetDelimiter(_ parser: inout Parser, state: ParserState) throws -> ParserState {
456 | let startDelimiter: Substring
457 | let endDelimiter: Substring
458 |
459 | do {
460 | parser.read(while: \.isWhitespace)
461 | startDelimiter = try parser.read(until: \.isWhitespace)
462 | parser.read(while: \.isWhitespace)
463 | endDelimiter = try parser.read(until: { $0 == "=" || $0.isWhitespace })
464 | parser.read(while: \.isWhitespace)
465 | } catch {
466 | throw Error.invalidSetDelimiter
467 | }
468 | guard try parser.read("=") else { throw Error.invalidSetDelimiter }
469 | guard try parser.read(string: state.endDelimiter) else { throw Error.invalidSetDelimiter }
470 | guard startDelimiter.count > 0, endDelimiter.count > 0 else { throw Error.invalidSetDelimiter }
471 | return state.withDelimiters(start: String(startDelimiter), end: String(endDelimiter))
472 | }
473 |
474 | static func readConfigVariable(_ parser: inout Parser, state: ParserState) throws -> Token? {
475 | let variable: Substring
476 | let value: Substring
477 |
478 | do {
479 | parser.read(while: \.isWhitespace)
480 | variable = parser.read(while: self.sectionNameCharsWithoutBrackets)
481 | parser.read(while: \.isWhitespace)
482 | guard try parser.read(":") else { throw Error.invalidConfigVariableSyntax }
483 | parser.read(while: \.isWhitespace)
484 | value = parser.read(while: self.sectionNameCharsWithoutBrackets)
485 | parser.read(while: \.isWhitespace)
486 | guard try parser.read(string: state.endDelimiter) else { throw Error.invalidConfigVariableSyntax }
487 | } catch {
488 | throw Error.invalidConfigVariableSyntax
489 | }
490 |
491 | // do both variable and value have content
492 | guard variable.count > 0, value.count > 0 else { throw Error.invalidConfigVariableSyntax }
493 |
494 | switch variable {
495 | case "CONTENT_TYPE":
496 | guard let contentType = MustacheContentTypes.get(String(value)) else { throw Error.unrecognisedConfigVariable }
497 | return .contentType(contentType)
498 | default:
499 | throw Error.unrecognisedConfigVariable
500 | }
501 | }
502 |
503 | static func hasLineFinished(_ parser: inout Parser) -> Bool {
504 | var parser2 = parser
505 | if parser.reachedEnd() { return true }
506 | parser2.read(while: Set(" \t"))
507 | if parser2.current().isNewline {
508 | parser2.unsafeAdvance()
509 | try! parser.setPosition(parser2.position)
510 | return true
511 | }
512 | return false
513 | }
514 |
515 | static func isStandalone(_ parser: inout Parser, state: ParserState) -> Bool {
516 | state.newLine && self.hasLineFinished(&parser)
517 | }
518 |
519 | private static let sectionNameCharsWithoutBrackets = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?*")
520 | private static let sectionNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()*")
521 | private static let partialNameChars = Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()*/")
522 | }
523 |
--------------------------------------------------------------------------------