diff --git a/Sources/Generator/Content/Element+LocalizedMetadata.swift b/Sources/Generator/Content/Element+LocalizedMetadata.swift index 2de7a18..afc7d06 100644 --- a/Sources/Generator/Content/Element+LocalizedMetadata.swift +++ b/Sources/Generator/Content/Element+LocalizedMetadata.swift @@ -140,6 +140,18 @@ extension Element { This property is mandatory at root level, and is propagated to child elements. */ let navigationTextAsPreviousPage: String + + /** + The text to display above a slideshow for most recent items. + Only used for elements that define `showMostRecentSection = true` + */ + let mostRecentTitle: String? + + /** + The text to display above a slideshow for featured items. + Only used for elements that define `showFeaturedSection = true` + */ + let featuredTitle: String? } } @@ -171,6 +183,8 @@ extension Element.LocalizedMetadata { self.relatedContentText = log.required(data.relatedContentText, name: "relatedContentText", source: source, &isValid) self.navigationTextAsNextPage = log.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source, &isValid) self.navigationTextAsPreviousPage = log.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source, &isValid) + self.mostRecentTitle = data.mostRecentTitle + self.featuredTitle = data.featuredTitle guard isValid else { return nil @@ -202,6 +216,8 @@ extension Element.LocalizedMetadata { self.relatedContentText = data.relatedContentText ?? parent.relatedContentText self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage + self.mostRecentTitle = data.mostRecentTitle ?? parent.mostRecentTitle + self.featuredTitle = data.featuredTitle ?? parent.featuredTitle guard isValid else { return nil diff --git a/Sources/Generator/Content/Element.swift b/Sources/Generator/Content/Element.swift index a60db5c..0778a16 100644 --- a/Sources/Generator/Content/Element.swift +++ b/Sources/Generator/Content/Element.swift @@ -132,6 +132,13 @@ struct Element { */ let showMostRecentSection: Bool + /** + Indicate that the overview section should contain a `Featured Content` section before the other sections. + The elements are the page ids of the elements contained in the feature. + - Note: If not specified, this property defaults to `false` + */ + let featuredPages: [String] + /** The localized metadata for each language. */ @@ -190,6 +197,7 @@ struct Element { self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault self.headerType = log.cast(metadata.headerType, "headerType", source: source) self.showMostRecentSection = metadata.showMostRecentSection ?? false + self.featuredPages = metadata.featuredPages ?? [] self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid) .compactMap { language in .init(atRoot: folder, data: language, log: log) @@ -252,6 +260,7 @@ struct Element { self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount self.headerType = log.cast(metadata.headerType, "headerType", source: source) self.showMostRecentSection = metadata.showMostRecentSection ?? false + self.featuredPages = metadata.featuredPages ?? [] self.languages = parent.languages.compactMap { parentData in guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else { log.warning("Language '\(parentData.language)' not found", source: source) @@ -285,8 +294,25 @@ struct Element { return nil } - //files.add(page: path, id: id) self.readElements(in: folder, source: path, log: log) + + if showMostRecentSection { + if elements.isEmpty { + log.error("Page has no children", source: source) + } + languages.filter { $0.mostRecentTitle == nil }.forEach { + log.error("'showMostRecentSection' = true, but 'mostRecentTitle' not set for language '\($0.language)'", source: source) + } + } + + if !featuredPages.isEmpty { + if elements.isEmpty { + log.error("'featuredPages' contains elements, but page has no children", source: source) + } + languages.filter { $0.featuredTitle == nil }.forEach { + log.error("'featuredPages' contains elements, but 'featuredTitle' not set for language '\($0.language)'", source: source) + } + } } func getExternalPageMap(language: String) -> [String : String] { @@ -307,6 +333,10 @@ struct Element { private func getExternalLink(for language: String) -> String? { languages.first { $0.language == language }?.externalUrl } + + var needsFirstSection: Bool { + showMostRecentSection || !featuredPages.isEmpty + } } // MARK: Paths @@ -349,6 +379,7 @@ extension Element { } let all = shownItems .reduce(into: [Element]()) { $0 += $1.mostRecentElements(count) } + .filter { $0.thumbnailStyle == .large } .filter { $0.date != nil } .sorted { $0.date! > $1.date! } return Array(all.prefix(count)) diff --git a/Sources/Generator/Content/GenericMetadata+Localized.swift b/Sources/Generator/Content/GenericMetadata+Localized.swift index 010699d..5e73737 100644 --- a/Sources/Generator/Content/GenericMetadata+Localized.swift +++ b/Sources/Generator/Content/GenericMetadata+Localized.swift @@ -133,6 +133,18 @@ extension GenericMetadata { This property is mandatory at root level, and is propagated to child elements. */ let navigationTextAsNextPage: String? + + /** + The text to display above a slideshow for most recent items. + Only used for elements that define `showMostRecentSection = true` + */ + let mostRecentTitle: String? + + /** + The text to display above a slideshow for featured items. + Only used for elements that define `showFeaturedSection = true` + */ + let featuredTitle: String? } } @@ -158,6 +170,8 @@ extension GenericMetadata.LocalizedMetadata: Codable { .relatedContentText, .navigationTextAsPreviousPage, .navigationTextAsNextPage, + .mostRecentTitle, + .featuredTitle, ] } @@ -190,7 +204,9 @@ extension GenericMetadata.LocalizedMetadata { externalUrl: nil, relatedContentText: nil, navigationTextAsPreviousPage: nil, - navigationTextAsNextPage: nil) + navigationTextAsNextPage: nil, + mostRecentTitle: nil, + featuredTitle: nil) } /** @@ -214,7 +230,9 @@ extension GenericMetadata.LocalizedMetadata { externalUrl: nil, relatedContentText: "", navigationTextAsPreviousPage: "", - navigationTextAsNextPage: "") + navigationTextAsNextPage: "", + mostRecentTitle: nil, + featuredTitle: nil) } static var full: GenericMetadata.LocalizedMetadata { @@ -235,6 +253,8 @@ extension GenericMetadata.LocalizedMetadata { externalUrl: "", relatedContentText: "", navigationTextAsPreviousPage: "", - navigationTextAsNextPage: "") + navigationTextAsNextPage: "", + mostRecentTitle: "", + featuredTitle: "") } } diff --git a/Sources/Generator/Content/GenericMetadata.swift b/Sources/Generator/Content/GenericMetadata.swift index 186fd4b..1263bbe 100644 --- a/Sources/Generator/Content/GenericMetadata.swift +++ b/Sources/Generator/Content/GenericMetadata.swift @@ -131,6 +131,13 @@ struct GenericMetadata { */ let showMostRecentSection: Bool? + /** + Indicate that the overview section should contain a `Featured Content` section before the other sections. + The elements are the page ids of the elements contained in the feature. + - Note: If not specified, this property defaults to `false` + */ + let featuredPages: [String]? + /** The localized metadata for each language. */ @@ -157,6 +164,7 @@ extension GenericMetadata: Codable { .overviewItemCount, .headerType, .showMostRecentSection, + .featuredPages, .languages, ] } @@ -229,6 +237,7 @@ extension GenericMetadata { overviewItemCount: 6, headerType: "left", showMostRecentSection: false, + featuredPages: [], languages: [.full]) } } diff --git a/Sources/Generator/Generators/OverviewSectionGenerator.swift b/Sources/Generator/Generators/OverviewSectionGenerator.swift index 07630b6..220b497 100644 --- a/Sources/Generator/Generators/OverviewSectionGenerator.swift +++ b/Sources/Generator/Generators/OverviewSectionGenerator.swift @@ -2,36 +2,80 @@ import Foundation struct OverviewSectionGenerator { - private let multipleSectionsTemplate: OverviewSectionTemplate - - private let singleSectionsTemplate: OverviewSectionCleanTemplate + private let factory: TemplateFactory private let generator: ThumbnailListGenerator - init(factory: TemplateFactory, results: GenerationResultsHandler) { - self.multipleSectionsTemplate = factory.overviewSection - self.singleSectionsTemplate = factory.overviewSectionClean + private let siteRoot: Element + + private let results: GenerationResultsHandler + + init(factory: TemplateFactory, siteRoot: Element, results: GenerationResultsHandler) { + self.factory = factory + self.siteRoot = siteRoot + self.results = results self.generator = ThumbnailListGenerator(factory: factory, results: results) } func generate(sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String { let content = sectionsContent(sections, in: parent, language: language, sectionItemCount: sectionItemCount) - if parent.showMostRecentSection { - let news = newsSectionContent(for: parent, language: language, sectionItemCount: sectionItemCount) - return news + "\n" + content - } else { - return content - } + let firstSection = firstSectionContent(for: parent, language: language, sectionItemCount: sectionItemCount) + return firstSection + content } - private func newsSectionContent(for element: Element, language: String, sectionItemCount: Int) -> String { - // let shownElements = element.mostRecentElements(sectionItemCount) - return "" -// return generator.generateContent( -// items: shownElements, -// parent: element, -// language: language, -// style: element.thumbnailStyle) + private func firstSectionContent(for element: Element, language: String, sectionItemCount: Int) -> String { + guard element.needsFirstSection else { + return "" + } + let metadata = element.localized(for: language) + var result = "" + if element.showMostRecentSection { + let shownElements = element.mostRecentElements(4).enumerated().map { (number, child) in + makeSlideshowItem(child, parent: element, language: language, number: number) + } + let title = metadata.mostRecentTitle ?? "Recent" + result = factory.slideshow.generate([.content: shownElements.joined(separator: "\n"), .title: title]) + } + if !element.featuredPages.isEmpty { + let elements = element.featuredPages.compactMap { id -> Element? in + guard let linked = siteRoot.find(id) else { + results.warning("Unknown id '\(id)' in 'featuredPages'", source: element.path) + return nil + } + return linked + }.prefix(4).enumerated().map { number, page in + makeSlideshowItem(page, parent: element, language: language, number: number) + } + let title = metadata.featuredTitle ?? "Featured" + result += factory.slideshow.generate([.content: elements.joined(separator: "\n"), .title: title]) + } + return factory.slideshows.generate([.content : result]) + } + + private func makeSlideshowItem(_ item: Element, parent: Element, language: String, number: Int) -> String { + let metadata = item.localized(for: language) + var content = [SlideshowImageTemplate.Key : String]() + content[.number] = "\(number + 1)" + + if item.state.hasThumbnailLink { + let fullPageUrl = item.fullPageUrl(for: language) + let relativePageUrl = parent.relativePathToFileWithPath(fullPageUrl) + content[.url] = "href=\"\(relativePageUrl)\"" + } + + // Image already assumed to be generated + let (_, thumbnailDestPath) = item.thumbnailFilePath(for: language) + let thumbnailDestNoExtension = thumbnailDestPath.dropAfterLast(".") + content[.image] = parent.relativePathToFileWithPath(thumbnailDestNoExtension) + + if let suffix = metadata.thumbnailSuffix { + content[.title] = factory.html.make(title: metadata.title, suffix: suffix) + } else { + content[.title] = metadata.title + } + let path = item.makePath(language: language, from: siteRoot) + content[.subtitle] = factory.pageLink.makePath(components: path) + return factory.slideshowImage.generate(content) } private func sectionsContent(_ sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String { @@ -50,7 +94,7 @@ struct OverviewSectionGenerator { style: section.thumbnailStyle) content[.more] = metadata.moreLinkText - return multipleSectionsTemplate.generate(content) + return factory.overviewSection.generate(content) } .joined(separator: "\n") } @@ -62,6 +106,6 @@ struct OverviewSectionGenerator { parent: section, language: language, style: section.thumbnailStyle) - return singleSectionsTemplate.generate(content) + return factory.overviewSectionClean.generate(content) } } diff --git a/Sources/Generator/Templates/Elements/SlideshowImageTemplate.swift b/Sources/Generator/Templates/Elements/SlideshowImageTemplate.swift new file mode 100644 index 0000000..3138fc2 --- /dev/null +++ b/Sources/Generator/Templates/Elements/SlideshowImageTemplate.swift @@ -0,0 +1,22 @@ +import Foundation + +struct SlideshowImageTemplate: Template { + + enum Key: String, CaseIterable { + case url = "URL" + case image = "IMAGE" + case title = "TITLE" + case subtitle = "SUBTITLE" + case number = "NUMBER" + } + + static let templateName = "slideshow-image.html" + + let raw: String + + let results: GenerationResultsHandler + + func makePath(components: [String]) -> String { + components.joined(separator: " » ") //  » ") + } +} diff --git a/Sources/Generator/Templates/Elements/SlideshowTemplate.swift b/Sources/Generator/Templates/Elements/SlideshowTemplate.swift new file mode 100644 index 0000000..b8324c4 --- /dev/null +++ b/Sources/Generator/Templates/Elements/SlideshowTemplate.swift @@ -0,0 +1,15 @@ +import Foundation + +struct SlideshowTemplate: Template { + + enum Key: String, CaseIterable { + case title = "TITLE" + case content = "CONTENT" + } + + static let templateName = "slideshow.html" + + let raw: String + + let results: GenerationResultsHandler +} diff --git a/Sources/Generator/Templates/Elements/SlideshowsTemplate.swift b/Sources/Generator/Templates/Elements/SlideshowsTemplate.swift new file mode 100644 index 0000000..4580881 --- /dev/null +++ b/Sources/Generator/Templates/Elements/SlideshowsTemplate.swift @@ -0,0 +1,14 @@ +import Foundation + +struct SlideshowsTemplate: Template { + + enum Key: String, CaseIterable { + case content = "CONTENT" + } + + static let templateName = "slideshows.html" + + let raw: String + + let results: GenerationResultsHandler +} diff --git a/Sources/Generator/Templates/Filled/LocalizedSiteTemplate.swift b/Sources/Generator/Templates/Filled/LocalizedSiteTemplate.swift index baaf68c..6b98904 100644 --- a/Sources/Generator/Templates/Filled/LocalizedSiteTemplate.swift +++ b/Sources/Generator/Templates/Filled/LocalizedSiteTemplate.swift @@ -67,7 +67,7 @@ struct LocalizedSiteTemplate { sections: sections, topBarWebsiteTitle: site.topBarTitle) self.pageHead = PageHeadGenerator(factory: factory, results: results) - self.overviewSection = OverviewSectionGenerator(factory: factory, results: results) + self.overviewSection = OverviewSectionGenerator(factory: factory, siteRoot: site, results: results) } // MARK: Content diff --git a/Sources/Generator/Templates/TemplateFactory.swift b/Sources/Generator/Templates/TemplateFactory.swift index d9f2753..0cc006d 100644 --- a/Sources/Generator/Templates/TemplateFactory.swift +++ b/Sources/Generator/Templates/TemplateFactory.swift @@ -53,6 +53,14 @@ final class TemplateFactory { let video: PageVideoTemplate + // MARK: Slideshow + + let slideshows: SlideshowsTemplate + + let slideshow: SlideshowTemplate + + let slideshowImage: SlideshowImageTemplate + // MARK: HTML let html: HTMLElementsGenerator @@ -76,6 +84,9 @@ final class TemplateFactory { self.page = try .init(in: templateFolder, results: results) self.image = try .init(in: templateFolder, results: results) self.video = try .init(in: templateFolder, results: results) + self.slideshow = try .init(in: templateFolder, results: results) + self.slideshows = try .init(in: templateFolder, results: results) + self.slideshowImage = try .init(in: templateFolder, results: results) self.html = .init() }