diff --git a/Sources/Publish/API/HTMLFactory.swift b/Sources/Publish/API/HTMLFactory.swift index a03888af..81e4bf94 100644 --- a/Sources/Publish/API/HTMLFactory.swift +++ b/Sources/Publish/API/HTMLFactory.swift @@ -35,7 +35,7 @@ public protocol HTMLFactory { /// Create the HTML to use for a page. /// - parameter page: The page to generate HTML for. /// - parameter context: The current publishing context. - func makePageHTML(for page: Page, + func makePageHTML(for page: Page, context: PublishingContext) throws -> HTML /// Create the HTML to use for the website's list of tags, if supported. diff --git a/Sources/Publish/API/Page.swift b/Sources/Publish/API/Page.swift index 59d6533b..6a95d4ab 100644 --- a/Sources/Publish/API/Page.swift +++ b/Sources/Publish/API/Page.swift @@ -11,16 +11,18 @@ import Foundation /// or lists of pages, that should be organized within sections, use `Section` /// and `Item` instead. Pages can either be added programmatically, or through /// Markdown files placed within the root of the website's content folder. -public struct Page: Location, Equatable { +public struct Page: Location, Equatable { public var path: Path public var content: Content + public var metadata: Site.PageMetadata /// Initialize a new page programmatically. You can also create pages from /// Markdown using the `addMarkdownFiles` step. /// - Parameter path: The absolute path of the page. /// - Parameter content: The page's content. - public init(path: Path, content: Content) { + public init(path: Path, metadata: Site.PageMetadata, content: Content) { self.path = path + self.metadata = metadata self.content = content } } diff --git a/Sources/Publish/API/PublishedWebsite.swift b/Sources/Publish/API/PublishedWebsite.swift index 8baa54b7..fe4daf70 100644 --- a/Sources/Publish/API/PublishedWebsite.swift +++ b/Sources/Publish/API/PublishedWebsite.swift @@ -15,5 +15,5 @@ public struct PublishedWebsite { /// The sections that were published. public let sections: SectionMap /// The free-form pages that were published. - public let pages: [Path : Page] + public let pages: [Path : Page] } diff --git a/Sources/Publish/API/PublishingContext.swift b/Sources/Publish/API/PublishingContext.swift index 4dd14caa..c261a86e 100644 --- a/Sources/Publish/API/PublishingContext.swift +++ b/Sources/Publish/API/PublishingContext.swift @@ -28,7 +28,7 @@ public struct PublishingContext { /// The sections that the website contains. public var sections = SectionMap() { didSet { tagCache.tags = nil } } /// The free-form pages that the website contains. - public private(set) var pages = [Path : Page]() + public private(set) var pages = [Path : Page]() /// A set containing all tags that are currently being used website-wide. public var allTags: Set { tagCache.tags ?? gatherAllTags() } /// Any date when the website was last generated. @@ -230,7 +230,7 @@ public extension PublishingContext { /// Add a page to the website programmatically. /// - parameter page: The page to add. - mutating func addPage(_ page: Page) { + mutating func addPage(_ page: Page) { pages[page.path] = page } @@ -249,8 +249,8 @@ public extension PublishingContext { /// - throws: An error in case the page couldn't be found, or /// if the mutation close itself threw an error. mutating func mutatePage(at path: Path, - matching predicate: Predicate = .any, - using mutations: Mutations) throws { + matching predicate: Predicate> = .any, + using mutations: Mutations>) throws { guard var page = pages[path] else { throw ContentError(path: path, reason: .pageNotFound) } diff --git a/Sources/Publish/API/PublishingStep.swift b/Sources/Publish/API/PublishingStep.swift index 63e8a7b0..fd6983c9 100644 --- a/Sources/Publish/API/PublishingStep.swift +++ b/Sources/Publish/API/PublishingStep.swift @@ -108,7 +108,7 @@ public extension PublishingStep { /// Add a page to website programmatically. /// - parameter page: The page to add. - static func addPage(_ page: Page) -> Self { + static func addPage(_ page: Page) -> Self { step(named: "Add page '\(page.path)'") { context in context.addPage(page) } @@ -118,7 +118,7 @@ public extension PublishingStep { /// - parameter sequence: The pages to add. static func addPages( in sequence: S - ) -> Self where S.Element == Page { + ) -> Self where S.Element == Page { step(named: "Add pages in sequence") { context in sequence.forEach { context.addPage($0) } } @@ -233,7 +233,7 @@ public extension PublishingStep { /// - parameter mutations: The mutations to apply to the page. static func mutatePage( at path: Path, - using mutations: @escaping Mutations + using mutations: @escaping Mutations> ) -> Self { step(named: "Mutate page at '\(path)'") { context in try context.mutatePage(at: path, using: mutations) @@ -244,8 +244,8 @@ public extension PublishingStep { /// - parameter predicate: Any predicate to filter the items using. /// - parameter mutations: The mutations to apply to the page. static func mutateAllPages( - matching predicate: Predicate = .any, - using mutations: @escaping Mutations + matching predicate: Predicate> = .any, + using mutations: @escaping Mutations> ) -> Self { step(named: "Mutate all pages") { context in for path in context.pages.keys { diff --git a/Sources/Publish/API/Theme+Foundation.swift b/Sources/Publish/API/Theme+Foundation.swift index 39cdcbb4..da686c2e 100644 --- a/Sources/Publish/API/Theme+Foundation.swift +++ b/Sources/Publish/API/Theme+Foundation.swift @@ -81,7 +81,7 @@ private struct FoundationHTMLFactory: HTMLFactory { ) } - func makePageHTML(for page: Page, + func makePageHTML(for page: Page, context: PublishingContext) throws -> HTML { HTML( .lang(context.site.language), diff --git a/Sources/Publish/API/Theme.swift b/Sources/Publish/API/Theme.swift index f083976f..162dd43b 100644 --- a/Sources/Publish/API/Theme.swift +++ b/Sources/Publish/API/Theme.swift @@ -14,7 +14,7 @@ public struct Theme { internal let makeIndexHTML: (Index, PublishingContext) throws -> HTML internal let makeSectionHTML: (Section, PublishingContext) throws -> HTML internal let makeItemHTML: (Item, PublishingContext) throws -> HTML - internal let makePageHTML: (Page, PublishingContext) throws -> HTML + internal let makePageHTML: (Page, PublishingContext) throws -> HTML internal let makeTagListHTML: (TagListPage, PublishingContext) throws -> HTML? internal let makeTagDetailsHTML: (TagDetailsPage, PublishingContext) throws -> HTML? internal let resourcePaths: Set diff --git a/Sources/Publish/API/Website.swift b/Sources/Publish/API/Website.swift index fd4c5b8c..45515cd7 100644 --- a/Sources/Publish/API/Website.swift +++ b/Sources/Publish/API/Website.swift @@ -12,6 +12,10 @@ import Dispatch public protocol WebsiteSectionID: Decodable, Hashable, CaseIterable, RawRepresentable where RawValue == String {} /// Protocol that all `Website.ItemMetadata` implementations must conform to. public typealias WebsiteItemMetadata = Decodable & Hashable +/// Protocol that all `Website.PageMetadata` implementations must conform to. +public typealias WebsitePageMetadata = Decodable & Hashable +/// Default type for `Website.PageMetadata` that includes no metadata. +public struct NoPageMetadata: WebsitePageMetadata {} /// Protocol used to define a Publish-based website. /// You conform to this protocol using a custom type, which is then used to @@ -23,8 +27,10 @@ public typealias WebsiteItemMetadata = Decodable & Hashable public protocol Website { /// The enum type used to represent the website's section IDs. associatedtype SectionID: WebsiteSectionID - /// The type that defines any custom metadata for the website. + /// The type that defines any custom metadata for the website's items. associatedtype ItemMetadata: WebsiteItemMetadata + /// The type that defines any custom metadata for the website's pages. + associatedtype PageMetadata: WebsitePageMetadata = NoPageMetadata /// The absolute URL that the website will be hosted at. var url: URL { get } diff --git a/Sources/Publish/Internal/MarkdownContentFactory.swift b/Sources/Publish/Internal/MarkdownContentFactory.swift index 74dfd60e..5462a4c4 100644 --- a/Sources/Publish/Internal/MarkdownContentFactory.swift +++ b/Sources/Publish/Internal/MarkdownContentFactory.swift @@ -41,11 +41,12 @@ internal struct MarkdownContentFactory { ) } - func makePage(fromFile file: File, at path: Path) throws -> Page { + func makePage(fromFile file: File, at path: Path) throws -> Page { let markdown = try parser.parse(file.readAsString()) let decoder = makeMetadataDecoder(for: markdown) + let metadata = try Site.PageMetadata(from: decoder) let content = try makeContent(fromMarkdown: markdown, file: file, decoder: decoder) - return Page(path: path, content: content) + return Page(path: path, metadata: metadata, content: content) } } diff --git a/Sources/Publish/Internal/MarkdownFileHandler.swift b/Sources/Publish/Internal/MarkdownFileHandler.swift index ffeaaaa3..e7d24b73 100644 --- a/Sources/Publish/Internal/MarkdownFileHandler.swift +++ b/Sources/Publish/Internal/MarkdownFileHandler.swift @@ -98,7 +98,7 @@ internal struct MarkdownFileHandler { private extension MarkdownFileHandler { enum FolderResult { - case pages([Page]) + case pages([Page]) case section(id: Site.SectionID, content: Content?, items: [Item]) } @@ -107,8 +107,8 @@ private extension MarkdownFileHandler { recursively: Bool, parentPath: Path, factory: MarkdownContentFactory - ) async throws -> [Page] { - let pages: [Page] = try await folder.files.concurrentCompactMap { file in + ) async throws -> [Page] { + let pages: [Page] = try await folder.files.concurrentCompactMap { file in guard file.isMarkdown else { return nil } if file.nameExcludingExtension == "index", !recursively { diff --git a/Sources/Publish/Internal/SiteMapGenerator.swift b/Sources/Publish/Internal/SiteMapGenerator.swift index 02463eac..f09a1aee 100644 --- a/Sources/Publish/Internal/SiteMapGenerator.swift +++ b/Sources/Publish/Internal/SiteMapGenerator.swift @@ -34,7 +34,7 @@ private extension SiteMapGenerator { }) } - func makeSiteMap(for sections: [Section], pages: [Page], site: Site) -> SiteMap { + func makeSiteMap(for sections: [Section], pages: [Page], site: Site) -> SiteMap { SiteMap( .forEach(sections) { section in guard shouldIncludePath(section.path) else { diff --git a/Tests/PublishTests/Infrastructure/HTMLFactoryMock.swift b/Tests/PublishTests/Infrastructure/HTMLFactoryMock.swift index 5f63fe92..7ac04996 100644 --- a/Tests/PublishTests/Infrastructure/HTMLFactoryMock.swift +++ b/Tests/PublishTests/Infrastructure/HTMLFactoryMock.swift @@ -13,7 +13,7 @@ final class HTMLFactoryMock: HTMLFactory { var makeIndexHTML: Closure = { _, _ in HTML(.body()) } var makeSectionHTML: Closure> = { _, _ in HTML(.body()) } var makeItemHTML: Closure> = { _, _ in HTML(.body()) } - var makePageHTML: Closure = { _, _ in HTML(.body()) } + var makePageHTML: Closure> = { _, _ in HTML(.body()) } var makeTagListHTML: Closure? = { _, _ in HTML(.body()) } var makeTagDetailsHTML: Closure? = { _, _ in HTML(.body()) } @@ -32,7 +32,7 @@ final class HTMLFactoryMock: HTMLFactory { try makeItemHTML(item, context) } - func makePageHTML(for page: Page, + func makePageHTML(for page: Page, context: PublishingContext) throws -> HTML { try makePageHTML(page, context) } diff --git a/Tests/PublishTests/Infrastructure/Item+Stubbable.swift b/Tests/PublishTests/Infrastructure/Item+Stubbable.swift index 0a80ba56..0e38da76 100644 --- a/Tests/PublishTests/Infrastructure/Item+Stubbable.swift +++ b/Tests/PublishTests/Infrastructure/Item+Stubbable.swift @@ -7,7 +7,7 @@ import Foundation import Publish -extension Item: Stubbable where Site == WebsiteStub.WithoutItemMetadata { +extension Item: Stubbable where Site == WebsiteStub.WithoutMetadata { private static let defaultDate = Date() static func stub(withPath path: Path) -> Self { diff --git a/Tests/PublishTests/Infrastructure/Page+Stubbable.swift b/Tests/PublishTests/Infrastructure/Page+Stubbable.swift index cdcede20..549cac20 100644 --- a/Tests/PublishTests/Infrastructure/Page+Stubbable.swift +++ b/Tests/PublishTests/Infrastructure/Page+Stubbable.swift @@ -7,12 +7,13 @@ import Foundation import Publish -extension Page: Stubbable { +extension Page: Stubbable where Site == WebsiteStub.WithoutMetadata { private static let defaultDate = Date() static func stub(withPath path: Path) -> Self { Page( path: path, + metadata: Site.PageMetadata(), content: Content( date: defaultDate, lastModified: defaultDate diff --git a/Tests/PublishTests/Infrastructure/PublishTestCase.swift b/Tests/PublishTests/Infrastructure/PublishTestCase.swift index c9f12ef5..9cf98c83 100644 --- a/Tests/PublishTests/Infrastructure/PublishTestCase.swift +++ b/Tests/PublishTests/Infrastructure/PublishTestCase.swift @@ -13,9 +13,9 @@ class PublishTestCase: XCTestCase { @discardableResult func publishWebsite( in folder: Folder? = nil, - using steps: [PublishingStep], + using steps: [PublishingStep], content: [Path : String] = [:] - ) throws -> PublishedWebsite { + ) throws -> PublishedWebsite { try performWebsitePublishing( in: folder, using: steps, @@ -25,12 +25,12 @@ class PublishTestCase: XCTestCase { } func publishWebsite( - _ site: WebsiteStub.WithoutItemMetadata = .init(), + _ site: WebsiteStub.WithoutMetadata = .init(), in folder: Folder? = nil, - using theme: Theme, + using theme: Theme, content: [Path : String] = [:], - additionalSteps: [PublishingStep] = [], - plugins: [Plugin] = [], + additionalSteps: [PublishingStep] = [], + plugins: [Plugin] = [], expectedHTML: [Path : String], allowWhitelistedOutputFiles: Bool = true, file: StaticString = #file, @@ -133,11 +133,12 @@ class PublishTestCase: XCTestCase { } @discardableResult - func publishWebsite( - withItemMetadataType itemMetadataType: T.Type, - using steps: [PublishingStep>], + func publishWebsite( + withItemMetadataType itemMetadataType: IT.Type, + pageMetadataType: PT.Type, + using steps: [PublishingStep>], content: [Path : String] = [:] - ) throws -> PublishedWebsite> { + ) throws -> PublishedWebsite> { try performWebsitePublishing( using: steps, files: content, @@ -149,7 +150,7 @@ class PublishTestCase: XCTestCase { in section: WebsiteStub.SectionID = .one, fromMarkdown markdown: String, fileName: String = "markdown.md" - ) throws -> Item { + ) throws -> Item { let site = try publishWebsite( using: [ .addMarkdownFiles() @@ -161,15 +162,17 @@ class PublishTestCase: XCTestCase { return try require(site.sections[section].items.first) } - - func generateItem( - withMetadataType metadataType: T.Type, + + struct EmptyPageMetadata: WebsitePageMetadata {} + func generateItem( + withMetadataType metadataType: IT.Type, in section: WebsiteStub.SectionID = .one, fromMarkdown markdown: String, fileName: String = "markdown.md" - ) throws -> Item> { + ) throws -> Item> { let site = try publishWebsite( - withItemMetadataType: T.self, + withItemMetadataType: IT.self, + pageMetadataType: EmptyPageMetadata.self, using: [ .addMarkdownFiles() ], diff --git a/Tests/PublishTests/Infrastructure/WebsiteStub.swift b/Tests/PublishTests/Infrastructure/WebsiteStub.swift index 09c3638f..64717dcd 100644 --- a/Tests/PublishTests/Infrastructure/WebsiteStub.swift +++ b/Tests/PublishTests/Infrastructure/WebsiteStub.swift @@ -29,15 +29,17 @@ class WebsiteStub { } extension WebsiteStub { - final class WithItemMetadata: WebsiteStub, Website {} + final class WithItemMetadata: WebsiteStub, Website {} final class WithPodcastMetadata: WebsiteStub, Website { struct ItemMetadata: PodcastCompatibleWebsiteItemMetadata { var podcast: PodcastEpisodeMetadata? } + struct PageMetadata: WebsitePageMetadata {} } - final class WithoutItemMetadata: WebsiteStub, Website { + final class WithoutMetadata: WebsiteStub, Website { struct ItemMetadata: WebsiteItemMetadata {} + struct PageMetadata: WebsitePageMetadata {} } } diff --git a/Tests/PublishTests/Tests/ErrorTests.swift b/Tests/PublishTests/Tests/ErrorTests.swift index bda3c915..d14b1978 100644 --- a/Tests/PublishTests/Tests/ErrorTests.swift +++ b/Tests/PublishTests/Tests/ErrorTests.swift @@ -10,7 +10,7 @@ import Publish final class ErrorTests: PublishTestCase { func testErrorForInvalidRootPath() throws { assertErrorThrown( - try WebsiteStub.WithoutItemMetadata().publish( + try WebsiteStub.WithoutMetadata().publish( at: "🤷‍♂️", using: [] ), diff --git a/Tests/PublishTests/Tests/HTMLGenerationTests.swift b/Tests/PublishTests/Tests/HTMLGenerationTests.swift index 2afca997..3e4c90cc 100644 --- a/Tests/PublishTests/Tests/HTMLGenerationTests.swift +++ b/Tests/PublishTests/Tests/HTMLGenerationTests.swift @@ -10,7 +10,7 @@ import Plot import Files final class HTMLGenerationTests: PublishTestCase { - private var htmlFactory: HTMLFactoryMock! + private var htmlFactory: HTMLFactoryMock! override func setUp() { super.setUp() @@ -115,6 +115,7 @@ final class HTMLGenerationTests: PublishTestCase { additionalSteps: [ .addPage(Page( path: "path/to/page3", + metadata: WebsiteStub.WithoutMetadata.PageMetadata(), content: Content(title: "Page 3") )) ], @@ -235,7 +236,7 @@ final class HTMLGenerationTests: PublishTestCase { } func testNotGeneratingTagHTMLWhenDisabled() throws { - let site = WebsiteStub.WithoutItemMetadata() + let site = WebsiteStub.WithoutMetadata() site.tagHTMLConfig = nil try publishWebsite(site, diff --git a/Tests/PublishTests/Tests/MarkdownTests.swift b/Tests/PublishTests/Tests/MarkdownTests.swift index 28df7927..ea571084 100644 --- a/Tests/PublishTests/Tests/MarkdownTests.swift +++ b/Tests/PublishTests/Tests/MarkdownTests.swift @@ -78,7 +78,7 @@ final class MarkdownTests: PublishTestCase { } func testParsingFileWithCustomMetadata() throws { - struct Metadata: WebsiteItemMetadata { + struct ItemMetadata: WebsiteItemMetadata { struct Nested: WebsiteItemMetadata { var string: String var url: URL @@ -95,7 +95,7 @@ final class MarkdownTests: PublishTestCase { } let item = try generateItem( - withMetadataType: Metadata.self, + withMetadataType: ItemMetadata.self, fromMarkdown: """ --- string: Hello, world! diff --git a/Tests/PublishTests/Tests/PlotComponentTests.swift b/Tests/PublishTests/Tests/PlotComponentTests.swift index 5269da15..85383b06 100644 --- a/Tests/PublishTests/Tests/PlotComponentTests.swift +++ b/Tests/PublishTests/Tests/PlotComponentTests.swift @@ -12,8 +12,8 @@ import Ink final class PlotComponentTests: PublishTestCase { func testStylesheetPaths() { let html = Node.head( - for: Page(path: "path", content: Content()), - on: WebsiteStub.WithoutItemMetadata(), + for: Page(path: "path", metadata: WebsiteStub.WithoutMetadata.PageMetadata(), content: Content()), + on: WebsiteStub.WithoutMetadata(), stylesheetPaths: [ "local-1.css", "/local-2.css", diff --git a/Tests/PublishTests/Tests/PluginTests.swift b/Tests/PublishTests/Tests/PluginTests.swift index 14ead1eb..52df479b 100644 --- a/Tests/PublishTests/Tests/PluginTests.swift +++ b/Tests/PublishTests/Tests/PluginTests.swift @@ -42,7 +42,7 @@ final class PluginTests: PublishTestCase { } func testAddingPluginToDefaultPipeline() throws { - let htmlFactory = HTMLFactoryMock() + let htmlFactory = HTMLFactoryMock() htmlFactory.makeIndexHTML = { content, _ in HTML(.body(content.body.node)) } diff --git a/Tests/PublishTests/Tests/PodcastFeedGenerationTests.swift b/Tests/PublishTests/Tests/PodcastFeedGenerationTests.swift index e836f52d..21e0b689 100644 --- a/Tests/PublishTests/Tests/PodcastFeedGenerationTests.swift +++ b/Tests/PublishTests/Tests/PodcastFeedGenerationTests.swift @@ -197,7 +197,7 @@ private extension PodcastFeedGenerationTests { func generateFeed( in folder: Folder, config: Configuration? = nil, - itemPredicate: Predicate>? = nil, + itemPredicate: Publish.Predicate>? = nil, generationSteps: [PublishingStep] = [ .addMarkdownFiles() ], diff --git a/Tests/PublishTests/Tests/RSSFeedGenerationTests.swift b/Tests/PublishTests/Tests/RSSFeedGenerationTests.swift index 52cd192d..3d472e2a 100644 --- a/Tests/PublishTests/Tests/RSSFeedGenerationTests.swift +++ b/Tests/PublishTests/Tests/RSSFeedGenerationTests.swift @@ -174,12 +174,12 @@ final class RSSFeedGenerationTests: PublishTestCase { } private extension RSSFeedGenerationTests { - typealias Site = WebsiteStub.WithoutItemMetadata + typealias Site = WebsiteStub.WithoutMetadata func generateFeed( in folder: Folder, config: RSSFeedConfiguration = .default, - itemPredicate: Predicate>? = nil, + itemPredicate: Publish.Predicate>? = nil, generationSteps: [PublishingStep] = [ .addMarkdownFiles() ], diff --git a/Tests/PublishTests/Tests/WebsiteTests.swift b/Tests/PublishTests/Tests/WebsiteTests.swift index 1ab15744..face4566 100644 --- a/Tests/PublishTests/Tests/WebsiteTests.swift +++ b/Tests/PublishTests/Tests/WebsiteTests.swift @@ -8,7 +8,7 @@ import XCTest import Publish final class WebsiteTests: PublishTestCase { - private var website: WebsiteStub.WithoutItemMetadata! + private var website: WebsiteStub.WithoutMetadata! override func setUp() { super.setUp() @@ -74,7 +74,7 @@ final class WebsiteTests: PublishTestCase { } func testURLForLocation() { - let page = Page(path: "mypage", content: Content()) + let page = Page(path: "mypage", metadata: WebsiteStub.WithoutMetadata.PageMetadata(), content: Content()) XCTAssertEqual( website.url(for: page),