diff --git a/Example/Package.resolved b/Example/Package.resolved index 056a3df..2d02558 100644 --- a/Example/Package.resolved +++ b/Example/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/loopwerk/Parsley", "state": { "branch": null, - "revision": "10da1efa3ea278a4828b8c87fd430bdfa326ffbc", - "version": "0.8.0" + "revision": "3240bdfee97f3bbde5c4ec150a29b0abcb0d3d21", + "version": "0.9.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/loopwerk/SagaParsleyMarkdownReader", "state": { "branch": null, - "revision": "dff2d3fa3f4eb83b74ec026cdf9b2f62df0383af", - "version": "0.5.0" + "revision": "d4fe9fca9829c1fb294af0160c23d5fc4bcf5fe4", + "version": "0.6.0" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/loopwerk/SagaSwimRenderer", "state": { "branch": null, - "revision": "b4b833d20846c19581f4a0328cfdefa8d4e564ed", - "version": "0.6.1" + "revision": "f53481e5c9972b83ca2d16ca2b962c63d460e23f", + "version": "0.7.0" } }, { diff --git a/Example/Package.swift b/Example/Package.swift index 83ccba0..3266986 100644 --- a/Example/Package.swift +++ b/Example/Package.swift @@ -10,7 +10,7 @@ let package = Package( dependencies: [ .package(path: "../"), .package(url: "https://github.com/loopwerk/SagaParsleyMarkdownReader", from: "0.5.0"), - .package(url: "https://github.com/loopwerk/SagaSwimRenderer", from: "0.6.1"), + .package(url: "https://github.com/loopwerk/SagaSwimRenderer", from: "0.7.0"), ], targets: [ .executableTarget( diff --git a/Example/Sources/Example/run.swift b/Example/Sources/Example/run.swift index a43e1fb..6980641 100644 --- a/Example/Sources/Example/run.swift +++ b/Example/Sources/Example/run.swift @@ -4,6 +4,12 @@ import PathKit import SagaParsleyMarkdownReader import SagaSwimRenderer +enum SiteMetadata { + static let url = URL(string: "http://www.example.com")! + static let name = "Example website" + static let author = "Kevin Renskers" +} + struct ArticleMetadata: Metadata { let tags: [String] var summary: String? @@ -15,18 +21,6 @@ struct AppMetadata: Metadata { let images: [String]? } -// SiteMetadata is given to every template. -// You can put whatever you want in here, as long as it's Decodable. -struct SiteMetadata: Metadata { - let url: URL - let name: String -} - -let siteMetadata = SiteMetadata( - url: URL(string: "http://www.example.com")!, - name: "Example website" -) - // An easy way to only get public articles, since ArticleMetadata.public is optional extension Item where M == ArticleMetadata { var `public`: Bool { @@ -63,7 +57,7 @@ struct Run { }() static func main() async throws { - try await Saga(input: "content", output: "deploy", siteMetadata: siteMetadata) + try await Saga(input: "content", output: "deploy") // All markdown files within the "articles" subfolder will be parsed to html, // using ArticleMetadata as the Item's metadata type. // Furthermore we are only interested in public articles. diff --git a/Example/Sources/Example/templates.swift b/Example/Sources/Example/templates.swift index a9c8419..7802a5a 100644 --- a/Example/Sources/Example/templates.swift +++ b/Example/Sources/Example/templates.swift @@ -3,10 +3,10 @@ import Saga import SagaSwimRenderer import Foundation -func baseHtml(siteMetadata: SiteMetadata, title pageTitle: String, @NodeBuilder children: () -> NodeConvertible) -> Node { +func baseHtml(title pageTitle: String, @NodeBuilder children: () -> NodeConvertible) -> Node { html(lang: "en-US") { head { - title { siteMetadata.name+": "+pageTitle } + title { SiteMetadata.name+": "+pageTitle } link(href: "/static/style.css", rel: "stylesheet") link(href: "/static/prism.css", rel: "stylesheet") } @@ -34,8 +34,8 @@ extension Date { } } -func renderArticle(context: ItemRenderingContext) -> Node { - return baseHtml(siteMetadata: context.siteMetadata, title: context.item.title) { +func renderArticle(context: ItemRenderingContext) -> Node { + return baseHtml(title: context.item.title) { div(id: "article") { h1 { context.item.title } h2 { @@ -85,24 +85,24 @@ func renderPagination(_ paginator: Paginator?) -> Node { } } -func renderArticles(context: ItemsRenderingContext) -> Node { - baseHtml(siteMetadata: context.siteMetadata, title: "Articles") { +func renderArticles(context: ItemsRenderingContext) -> Node { + baseHtml(title: "Articles") { h1 { "Articles" } context.items.map(articleInList) renderPagination(context.paginator) } } -func renderPartition(context: PartitionedRenderingContext) -> Node { - baseHtml(siteMetadata: context.siteMetadata, title: "Articles in \(context.key)") { +func renderPartition(context: PartitionedRenderingContext) -> Node { + baseHtml(title: "Articles in \(context.key)") { h1 { "Articles in \(context.key)" } context.items.map(articleInList) renderPagination(context.paginator) } } -func renderPage(context: ItemRenderingContext) -> Node { - baseHtml(siteMetadata: context.siteMetadata, title: context.item.title) { +func renderPage(context: ItemRenderingContext) -> Node { + baseHtml(title: context.item.title) { div(id: "page") { h1 { context.item.title } Node.raw(context.item.body) @@ -117,8 +117,8 @@ func renderPage(context: ItemRenderingContext) -> N } } -func renderApps(context: ItemsRenderingContext) -> Node { - baseHtml(siteMetadata: context.siteMetadata, title: "Apps") { +func renderApps(context: ItemsRenderingContext) -> Node { + baseHtml(title: "Apps") { h1 { "Apps" } context.items.map { app in div(class: "app") { @@ -151,36 +151,28 @@ extension Item where M == ArticleMetadata { } } -func renderFeed(context: ItemsRenderingContext) -> Node { +func renderFeed(context: ItemsRenderingContext) -> Node { AtomFeed( - title: context.siteMetadata.name, - author: "Kevin Renskers", - baseURL: context.siteMetadata.url, - pagePath: "articles/", - feedPath: "articles/feed.xml", + title: SiteMetadata.name, + author: SiteMetadata.author, + baseURL: SiteMetadata.url, + feedPath: context.outputPath.string, items: Array(context.items.prefix(20)), summary: { item in - if let article = item as? Item { - return article.summary - } - return nil + return item.summary } ).node() } -func renderTagFeed(context: PartitionedRenderingContext) -> Node { +func renderTagFeed(context: PartitionedRenderingContext) -> Node { AtomFeed( - title: context.siteMetadata.name, - author: "Kevin Renskers", - baseURL: context.siteMetadata.url, - pagePath: "articles/tag/\(context.key)/", - feedPath: "articles/tag/\(context.key)/feed.xml", + title: SiteMetadata.name, + author: SiteMetadata.author, + baseURL: SiteMetadata.url, + feedPath: context.outputPath.string, items: Array(context.items.prefix(20)), summary: { item in - if let article = item as? Item { - return article.summary - } - return nil + return item.summary } ).node() } diff --git a/Sources/Saga/ProcessingStep.swift b/Sources/Saga/ProcessingStep.swift index 4ec21d2..a7969d3 100644 --- a/Sources/Saga/ProcessingStep.swift +++ b/Sources/Saga/ProcessingStep.swift @@ -1,14 +1,14 @@ import Foundation import PathKit -internal class ProcessStep { +internal class ProcessStep { let folder: Path? let readers: [Reader] let filter: (Item) -> Bool - let writers: [Writer] + let writers: [Writer] var items: [Item] - init(folder: Path?, readers: [Reader], filter: @escaping (Item) -> Bool, writers: [Writer]) { + init(folder: Path?, readers: [Reader], filter: @escaping (Item) -> Bool, writers: [Writer]) { self.folder = folder self.readers = readers self.filter = filter @@ -21,7 +21,7 @@ internal class AnyProcessStep { let runReaders: () async throws -> () let runWriters: () throws -> () - init(step: ProcessStep, fileStorage: [FileContainer], inputPath: Path, outputPath: Path, itemWriteMode: ItemWriteMode, siteMetadata: SiteMetadata, fileIO: FileIO) { + init(step: ProcessStep, fileStorage: [FileContainer], inputPath: Path, outputPath: Path, itemWriteMode: ItemWriteMode, fileIO: FileIO) { runReaders = { var items = [Item]() @@ -71,7 +71,7 @@ internal class AnyProcessStep { .sorted(by: { left, right in left.date > right.date }) for writer in step.writers { - try writer.run(step.items, allItems, siteMetadata, outputPath, step.folder ?? "", fileIO) + try writer.run(step.items, allItems, outputPath, step.folder ?? "", fileIO) } } } diff --git a/Sources/Saga/RenderingContexts.swift b/Sources/Saga/RenderingContexts.swift index 0dc937a..c16accf 100644 --- a/Sources/Saga/RenderingContexts.swift +++ b/Sources/Saga/RenderingContexts.swift @@ -1,26 +1,25 @@ import PathKit -public struct ItemRenderingContext { +public struct ItemRenderingContext { public let item: Item public let items: [Item] public let allItems: [AnyItem] - public let siteMetadata: SiteMetadata } -public struct ItemsRenderingContext { +public struct ItemsRenderingContext { public let items: [Item] public let allItems: [AnyItem] - public let siteMetadata: SiteMetadata public let paginator: Paginator? + public let outputPath: Path } public typealias ContextKey = CustomStringConvertible & Comparable -public struct PartitionedRenderingContext { +public struct PartitionedRenderingContext { public let key: T public let items: [Item] public let allItems: [AnyItem] - public let siteMetadata: SiteMetadata public let paginator: Paginator? + public let outputPath: Path } /// A model representing a paginator. diff --git a/Sources/Saga/Saga.swift b/Sources/Saga/Saga.swift index 22ab772..557f99d 100644 --- a/Sources/Saga/Saga.swift +++ b/Sources/Saga/Saga.swift @@ -7,7 +7,7 @@ import PathKit /// @main /// struct Run { /// static func main() async throws { -/// try await Saga(input: "content", output: "deploy", siteMetadata: EmptyMetadata()) +/// try await Saga(input: "content", output: "deploy") /// // All files in the input folder will be parsed to html, and written to the output folder. /// .register( /// metadata: EmptyMetadata.self, @@ -26,7 +26,7 @@ import PathKit /// } /// } /// ``` -public class Saga { +public class Saga { /// The root working path. This is automatically set to the same folder that holds `Package.swift`. public let rootPath: Path @@ -36,21 +36,17 @@ public class Saga { /// The path that Saga will write the rendered website to, relative to the `rootPath`. For example "deploy". public let outputPath: Path - /// The metadata used to hold site-wide information, such as the website name or URL. This will be included in all rendering contexts. - public let siteMetadata: SiteMetadata - /// An array of all file containters. public let fileStorage: [FileContainer] internal var processSteps = [AnyProcessStep]() internal let fileIO: FileIO - public init(input: Path, output: Path = "deploy", siteMetadata: SiteMetadata, fileIO: FileIO = .diskAccess, originFilePath: StaticString = #file) throws { + public init(input: Path, output: Path = "deploy", fileIO: FileIO = .diskAccess, originFilePath: StaticString = #file) throws { let originFile = Path("\(originFilePath)") rootPath = try fileIO.resolveSwiftPackageFolder(originFile) inputPath = rootPath + input outputPath = rootPath + output - self.siteMetadata = siteMetadata self.fileIO = fileIO // 1. Find all files in the source folder @@ -75,7 +71,7 @@ public class Saga { /// - writers: The writers that will be used by this step. /// - Returns: The Saga instance itself, so you can chain further calls onto it. @discardableResult - public func register(folder: Path? = nil, metadata: M.Type, readers: [Reader], itemWriteMode: ItemWriteMode = .moveToSubfolder, filter: @escaping ((Item) -> Bool) = { _ in true }, writers: [Writer]) throws -> Self { + public func register(folder: Path? = nil, metadata: M.Type, readers: [Reader], itemWriteMode: ItemWriteMode = .moveToSubfolder, filter: @escaping ((Item) -> Bool) = { _ in true }, writers: [Writer]) throws -> Self { let step = ProcessStep(folder: folder, readers: readers, filter: filter, writers: writers) self.processSteps.append( .init( @@ -84,7 +80,6 @@ public class Saga { inputPath: inputPath, outputPath: outputPath, itemWriteMode: itemWriteMode, - siteMetadata: siteMetadata, fileIO: fileIO )) return self diff --git a/Sources/Saga/Writer.swift b/Sources/Saga/Writer.swift index 2a56e3e..e01a3b1 100644 --- a/Sources/Saga/Writer.swift +++ b/Sources/Saga/Writer.swift @@ -6,8 +6,8 @@ import Foundation /// To turn an ``Item`` into a `String`, a `Writer` uses a "renderer"; a function that knows how to turn a rendering context such as ``ItemRenderingContext`` into a `String`. /// /// > Note: Saga does not come bundled with any renderers out of the box, instead you should install one such as [SagaSwimRenderer](https://github.com/loopwerk/SagaSwimRenderer) or [SagaStencilRenderer](https://github.com/loopwerk/SagaStencilRenderer). -public struct Writer { - let run: (_ items: [Item], _ allItems: [AnyItem], _ siteMetadata: SiteMetadata, _ outputRoot: Path, _ outputPrefix: Path, _ fileIO: FileIO) throws -> Void +public struct Writer { + let run: (_ items: [Item], _ allItems: [AnyItem], _ outputRoot: Path, _ outputPrefix: Path, _ fileIO: FileIO) throws -> Void } private extension Array { @@ -20,10 +20,10 @@ private extension Array { public extension Writer { /// Writes a single ``Item`` to a single output file, using `Item.destination` as the destination path. - static func itemWriter(_ renderer: @escaping (ItemRenderingContext) throws -> String) -> Self { - Writer { items, allItems, siteMetadata, outputRoot, outputPrefix, fileIO in + static func itemWriter(_ renderer: @escaping (ItemRenderingContext) throws -> String) -> Self { + Writer { items, allItems, outputRoot, outputPrefix, fileIO in for item in items { - let context = ItemRenderingContext(item: item, items: items, allItems: allItems, siteMetadata: siteMetadata) + let context = ItemRenderingContext(item: item, items: items, allItems: allItems) let stringToWrite = try renderer(context) try fileIO.write(outputRoot + item.relativeDestination, stringToWrite) } @@ -31,10 +31,10 @@ public extension Writer { } /// Writes an array of items into a single output file. - static func listWriter(_ renderer: @escaping (ItemsRenderingContext) throws -> String, output: Path = "index.html", paginate: Int? = nil, paginatedOutput: Path = "page/[page]/index.html") -> Self { - return Self { items, allItems, siteMetadata, outputRoot, outputPrefix, fileIO in - try writePages(renderer: renderer, items: items, allItems: allItems, siteMetadata: siteMetadata, outputRoot: outputRoot, outputPrefix: outputPrefix, output: output, paginate: paginate, paginatedOutput: paginatedOutput, fileIO: fileIO) { - return ItemsRenderingContext(items: $0, allItems: $1, siteMetadata: $2, paginator: $3) + static func listWriter(_ renderer: @escaping (ItemsRenderingContext) throws -> String, output: Path = "index.html", paginate: Int? = nil, paginatedOutput: Path = "page/[page]/index.html") -> Self { + return Self { items, allItems, outputRoot, outputPrefix, fileIO in + try writePages(renderer: renderer, items: items, allItems: allItems, outputRoot: outputRoot, outputPrefix: outputPrefix, output: output, paginate: paginate, paginatedOutput: paginatedOutput, fileIO: fileIO) { + return ItemsRenderingContext(items: $0, allItems: $1, paginator: $2, outputPath: $3) } } } @@ -45,22 +45,22 @@ public extension Writer { /// /// The `output` path is a template where `[key]` will be replaced with the key used for the partition. /// Example: `articles/[key]/index.html` - static func partitionedWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "[key]/page/[page]/index.html", partitioner: @escaping ([Item]) -> [T: [Item]]) -> Self { - return Self { items, allItems, siteMetadata, outputRoot, outputPrefix, fileIO in + static func partitionedWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "[key]/page/[page]/index.html", partitioner: @escaping ([Item]) -> [T: [Item]]) -> Self { + return Self { items, allItems, outputRoot, outputPrefix, fileIO in let partitions = partitioner(items) for (key, itemsInPartition) in Array(partitions).sorted(by: {$0.0 < $1.0}) { let finishedOutputPath = Path(output.string.replacingOccurrences(of: "[key]", with: "\(key.slugified)")) let finishedPaginatedOutputPath = Path(paginatedOutput.string.replacingOccurrences(of: "[key]", with: "\(key.slugified)")) - try writePages(renderer: renderer, items: itemsInPartition, allItems: allItems, siteMetadata: siteMetadata, outputRoot: outputRoot, outputPrefix: outputPrefix, output: finishedOutputPath, paginate: paginate, paginatedOutput: finishedPaginatedOutputPath, fileIO: fileIO) { - return PartitionedRenderingContext(key: key, items: $0, allItems: $1, siteMetadata: $2, paginator: $3) + try writePages(renderer: renderer, items: itemsInPartition, allItems: allItems, outputRoot: outputRoot, outputPrefix: outputPrefix, output: finishedOutputPath, paginate: paginate, paginatedOutput: finishedPaginatedOutputPath, fileIO: fileIO) { + return PartitionedRenderingContext(key: key, items: $0, allItems: $1, paginator: $2, outputPath: $3) } } } } /// A convenience version of `partitionedWriter` that splits items based on year. - static func yearWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "[key]/page/[page]/index.html") -> Self { + static func yearWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "[key]/page/[page]/index.html") -> Self { let partitioner: ([Item]) -> [Int: [Item]] = { items in var itemsPerYear = [Int: [Item]]() @@ -83,7 +83,7 @@ public extension Writer { /// A convenience version of `partitionedWriter` that splits items based on tags. /// /// Tags can be any `[String]` array. - static func tagWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "tag/[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "tag/[key]/page/[page]/index.html", tags: @escaping (Item) -> [String]) -> Self { + static func tagWriter(_ renderer: @escaping (PartitionedRenderingContext) throws -> String, output: Path = "tag/[key]/index.html", paginate: Int? = nil, paginatedOutput: Path = "tag/[key]/page/[page]/index.html", tags: @escaping (Item) -> [String]) -> Self { let partitioner: ([Item]) -> [String: [Item]] = { items in var itemsPerTag = [String: [Item]]() @@ -106,7 +106,7 @@ public extension Writer { } private extension Writer { - static func writePages(renderer: @escaping (Context) throws -> String, items: [Item], allItems: [AnyItem], siteMetadata: SiteMetadata, outputRoot: Path, outputPrefix: Path, output: Path, paginate: Int?, paginatedOutput: Path, fileIO: FileIO, getContext: ([Item], [AnyItem], SiteMetadata, Paginator?) -> Context) throws { + static func writePages(renderer: @escaping (Context) throws -> String, items: [Item], allItems: [AnyItem], outputRoot: Path, outputPrefix: Path, output: Path, paginate: Int?, paginatedOutput: Path, fileIO: FileIO, getContext: ([Item], [AnyItem], Paginator?, Path) -> Context) throws { if let perPage = paginate { let ranges = items.chunked(into: perPage) let numberOfPages = ranges.count @@ -123,7 +123,7 @@ private extension Writer { next: numberOfPages > 1 ? (outputPrefix + nextPage) : nil ) - let context = getContext(firstItems, allItems, siteMetadata, paginator) + let context = getContext(firstItems, allItems, paginator, outputPrefix + output) let stringToWrite = try renderer(context) try fileIO.write(outputRoot + outputPrefix + output, stringToWrite) } @@ -143,12 +143,12 @@ private extension Writer { ) let finishedOutputPath = Path(paginatedOutput.string.replacingOccurrences(of: "[page]", with: "\(currentPage)")) - let context = getContext(items, allItems, siteMetadata, paginator) + let context = getContext(items, allItems, paginator, outputPrefix + finishedOutputPath) let stringToWrite = try renderer(context) try fileIO.write(outputRoot + outputPrefix + finishedOutputPath, stringToWrite) } } else { - let context = getContext(items, allItems, siteMetadata, nil) + let context = getContext(items, allItems, nil, outputPrefix + output) let stringToWrite = try renderer(context) try fileIO.write(outputRoot + outputPrefix + output, stringToWrite) }