Finish most recent and featured section

This commit is contained in:
Christoph Hagen 2022-12-07 01:01:13 +01:00
parent f185191b7f
commit 3bd75a63ab
10 changed files with 209 additions and 27 deletions

View File

@ -140,6 +140,18 @@ extension Element {
This property is mandatory at root level, and is propagated to child elements. This property is mandatory at root level, and is propagated to child elements.
*/ */
let navigationTextAsPreviousPage: String 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.relatedContentText = log.required(data.relatedContentText, name: "relatedContentText", source: source, &isValid)
self.navigationTextAsNextPage = log.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", 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.navigationTextAsPreviousPage = log.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source, &isValid)
self.mostRecentTitle = data.mostRecentTitle
self.featuredTitle = data.featuredTitle
guard isValid else { guard isValid else {
return nil return nil
@ -202,6 +216,8 @@ extension Element.LocalizedMetadata {
self.relatedContentText = data.relatedContentText ?? parent.relatedContentText self.relatedContentText = data.relatedContentText ?? parent.relatedContentText
self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage
self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage
self.mostRecentTitle = data.mostRecentTitle ?? parent.mostRecentTitle
self.featuredTitle = data.featuredTitle ?? parent.featuredTitle
guard isValid else { guard isValid else {
return nil return nil

View File

@ -132,6 +132,13 @@ struct Element {
*/ */
let showMostRecentSection: Bool 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. The localized metadata for each language.
*/ */
@ -190,6 +197,7 @@ struct Element {
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
self.headerType = log.cast(metadata.headerType, "headerType", source: source) self.headerType = log.cast(metadata.headerType, "headerType", source: source)
self.showMostRecentSection = metadata.showMostRecentSection ?? false self.showMostRecentSection = metadata.showMostRecentSection ?? false
self.featuredPages = metadata.featuredPages ?? []
self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid) self.languages = log.required(metadata.languages, name: "languages", source: source, &isValid)
.compactMap { language in .compactMap { language in
.init(atRoot: folder, data: language, log: log) .init(atRoot: folder, data: language, log: log)
@ -252,6 +260,7 @@ struct Element {
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
self.headerType = log.cast(metadata.headerType, "headerType", source: source) self.headerType = log.cast(metadata.headerType, "headerType", source: source)
self.showMostRecentSection = metadata.showMostRecentSection ?? false self.showMostRecentSection = metadata.showMostRecentSection ?? false
self.featuredPages = metadata.featuredPages ?? []
self.languages = parent.languages.compactMap { parentData in self.languages = parent.languages.compactMap { parentData in
guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else { guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else {
log.warning("Language '\(parentData.language)' not found", source: source) log.warning("Language '\(parentData.language)' not found", source: source)
@ -285,8 +294,25 @@ struct Element {
return nil return nil
} }
//files.add(page: path, id: id)
self.readElements(in: folder, source: path, log: log) 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] { func getExternalPageMap(language: String) -> [String : String] {
@ -307,6 +333,10 @@ struct Element {
private func getExternalLink(for language: String) -> String? { private func getExternalLink(for language: String) -> String? {
languages.first { $0.language == language }?.externalUrl languages.first { $0.language == language }?.externalUrl
} }
var needsFirstSection: Bool {
showMostRecentSection || !featuredPages.isEmpty
}
} }
// MARK: Paths // MARK: Paths
@ -349,6 +379,7 @@ extension Element {
} }
let all = shownItems let all = shownItems
.reduce(into: [Element]()) { $0 += $1.mostRecentElements(count) } .reduce(into: [Element]()) { $0 += $1.mostRecentElements(count) }
.filter { $0.thumbnailStyle == .large }
.filter { $0.date != nil } .filter { $0.date != nil }
.sorted { $0.date! > $1.date! } .sorted { $0.date! > $1.date! }
return Array(all.prefix(count)) return Array(all.prefix(count))

View File

@ -133,6 +133,18 @@ extension GenericMetadata {
This property is mandatory at root level, and is propagated to child elements. This property is mandatory at root level, and is propagated to child elements.
*/ */
let navigationTextAsNextPage: String? 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, .relatedContentText,
.navigationTextAsPreviousPage, .navigationTextAsPreviousPage,
.navigationTextAsNextPage, .navigationTextAsNextPage,
.mostRecentTitle,
.featuredTitle,
] ]
} }
@ -190,7 +204,9 @@ extension GenericMetadata.LocalizedMetadata {
externalUrl: nil, externalUrl: nil,
relatedContentText: nil, relatedContentText: nil,
navigationTextAsPreviousPage: nil, navigationTextAsPreviousPage: nil,
navigationTextAsNextPage: nil) navigationTextAsNextPage: nil,
mostRecentTitle: nil,
featuredTitle: nil)
} }
/** /**
@ -214,7 +230,9 @@ extension GenericMetadata.LocalizedMetadata {
externalUrl: nil, externalUrl: nil,
relatedContentText: "", relatedContentText: "",
navigationTextAsPreviousPage: "", navigationTextAsPreviousPage: "",
navigationTextAsNextPage: "") navigationTextAsNextPage: "",
mostRecentTitle: nil,
featuredTitle: nil)
} }
static var full: GenericMetadata.LocalizedMetadata { static var full: GenericMetadata.LocalizedMetadata {
@ -235,6 +253,8 @@ extension GenericMetadata.LocalizedMetadata {
externalUrl: "", externalUrl: "",
relatedContentText: "", relatedContentText: "",
navigationTextAsPreviousPage: "", navigationTextAsPreviousPage: "",
navigationTextAsNextPage: "") navigationTextAsNextPage: "",
mostRecentTitle: "",
featuredTitle: "")
} }
} }

View File

@ -131,6 +131,13 @@ struct GenericMetadata {
*/ */
let showMostRecentSection: Bool? 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. The localized metadata for each language.
*/ */
@ -157,6 +164,7 @@ extension GenericMetadata: Codable {
.overviewItemCount, .overviewItemCount,
.headerType, .headerType,
.showMostRecentSection, .showMostRecentSection,
.featuredPages,
.languages, .languages,
] ]
} }
@ -229,6 +237,7 @@ extension GenericMetadata {
overviewItemCount: 6, overviewItemCount: 6,
headerType: "left", headerType: "left",
showMostRecentSection: false, showMostRecentSection: false,
featuredPages: [],
languages: [.full]) languages: [.full])
} }
} }

View File

@ -2,36 +2,80 @@ import Foundation
struct OverviewSectionGenerator { struct OverviewSectionGenerator {
private let multipleSectionsTemplate: OverviewSectionTemplate private let factory: TemplateFactory
private let singleSectionsTemplate: OverviewSectionCleanTemplate
private let generator: ThumbnailListGenerator private let generator: ThumbnailListGenerator
init(factory: TemplateFactory, results: GenerationResultsHandler) { private let siteRoot: Element
self.multipleSectionsTemplate = factory.overviewSection
self.singleSectionsTemplate = factory.overviewSectionClean 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) self.generator = ThumbnailListGenerator(factory: factory, results: results)
} }
func generate(sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String { func generate(sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String {
let content = sectionsContent(sections, in: parent, language: language, sectionItemCount: sectionItemCount) let content = sectionsContent(sections, in: parent, language: language, sectionItemCount: sectionItemCount)
if parent.showMostRecentSection { let firstSection = firstSectionContent(for: parent, language: language, sectionItemCount: sectionItemCount)
let news = newsSectionContent(for: parent, language: language, sectionItemCount: sectionItemCount) return firstSection + content
return news + "\n" + content
} else {
return content
}
} }
private func newsSectionContent(for element: Element, language: String, sectionItemCount: Int) -> String { private func firstSectionContent(for element: Element, language: String, sectionItemCount: Int) -> String {
// let shownElements = element.mostRecentElements(sectionItemCount) guard element.needsFirstSection else {
return "" return ""
// return generator.generateContent( }
// items: shownElements, let metadata = element.localized(for: language)
// parent: element, var result = ""
// language: language, if element.showMostRecentSection {
// style: element.thumbnailStyle) 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 { private func sectionsContent(_ sections: [Element], in parent: Element, language: String, sectionItemCount: Int) -> String {
@ -50,7 +94,7 @@ struct OverviewSectionGenerator {
style: section.thumbnailStyle) style: section.thumbnailStyle)
content[.more] = metadata.moreLinkText content[.more] = metadata.moreLinkText
return multipleSectionsTemplate.generate(content) return factory.overviewSection.generate(content)
} }
.joined(separator: "\n") .joined(separator: "\n")
} }
@ -62,6 +106,6 @@ struct OverviewSectionGenerator {
parent: section, parent: section,
language: language, language: language,
style: section.thumbnailStyle) style: section.thumbnailStyle)
return singleSectionsTemplate.generate(content) return factory.overviewSectionClean.generate(content)
} }
} }

View File

@ -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: " » ") //  » ")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -67,7 +67,7 @@ struct LocalizedSiteTemplate {
sections: sections, sections: sections,
topBarWebsiteTitle: site.topBarTitle) topBarWebsiteTitle: site.topBarTitle)
self.pageHead = PageHeadGenerator(factory: factory, results: results) 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 // MARK: Content

View File

@ -53,6 +53,14 @@ final class TemplateFactory {
let video: PageVideoTemplate let video: PageVideoTemplate
// MARK: Slideshow
let slideshows: SlideshowsTemplate
let slideshow: SlideshowTemplate
let slideshowImage: SlideshowImageTemplate
// MARK: HTML // MARK: HTML
let html: HTMLElementsGenerator let html: HTMLElementsGenerator
@ -76,6 +84,9 @@ final class TemplateFactory {
self.page = try .init(in: templateFolder, results: results) self.page = try .init(in: templateFolder, results: results)
self.image = try .init(in: templateFolder, results: results) self.image = try .init(in: templateFolder, results: results)
self.video = 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() self.html = .init()
} }