A renderer for Saga that uses Stencil to turn a RenderingContext into a String.
It comes with a free function named stencil which takes an HTML template and a Stencil Environment, and returns a function that goes from RenderingContext to String, which can then be plugged into Saga's writers.
Package.swift
// swift-tools-version:5.5
import PackageDescription
let package = Package(
  name: "Example",
  platforms: [
    .macOS(.v12)
  ],
  dependencies: [
    .package(url: "https://github.com/loopwerk/Saga", from: "1.0.0"),
    .package(url: "https://github.com/loopwerk/SagaParsleyMarkdownReader", from: "0.5.0"),
    .package(url: "https://github.com/loopwerk/SagaStencilRenderer", from: "0.4.0")
  ],
  targets: [
    .target(
      name: "Example",
      dependencies: [
        "Saga",
        "SagaParsleyMarkdownReader",
        "SagaStencilRenderer"
      ]
    ),
  ]
)main.swift:
import Foundation
import Saga
import PathKit
import SagaParsleyMarkdownReader
import SagaStencilRenderer
import Stencil
@main
struct Run {
  static func main() async throws {
    let saga = try Saga(input: "content", output: "deploy")
    try await saga
      // All the Markdown files will be parsed to html.
      .register(
        readers: [.parsleyMarkdownReader()],
        itemWriteMode: .keepAsFile,
        writers: [
          .itemWriter(stencil("page.html", environment: getEnvironment(root: saga.rootPath)))
        ]
      )
      // Run the steps we registered above
      .run()
      // All the remaining files that were not parsed to markdown, so for example images, raw html files and css,
      // are copied as-is to the output folder.
      .staticFiles()
  }
}
func getEnvironment(root: Path) -> Environment {
  Environment(loader: FileSystemLoader(paths: [root + "templates"]))
}Please check out the Example app to play around.
You can extend the Environment with your own tags and filters, see the official Stencil docs.
For example:
func getEnvironment(root: Path) -> Environment {
  let ext = Extension()
  
  ext.registerFilter("url") { (value: Any?) in
    guard let item = value as? AnyItem else {
      return ""
    }
    var url = "/" + item.relativeDestination.string
    if url.hasSuffix("/index.html") {
      url.removeLast(10)
    }
    return url
  }
  return Environment(loader: FileSystemLoader(paths: [root + "templates"]), extensions: [ext])
}