ChWebsiteApp/CHDataManagement/Storage/WebsiteGenerator.swift
2024-12-04 08:10:45 +01:00

184 lines
6.3 KiB
Swift

import Foundation
struct WebsiteGeneratorConfiguration {
let language: ContentLanguage
let outputDirectory: URL
let postsPerPage: Int
let postFeedTitle: String
let postFeedDescription: String
let postFeedUrlPrefix: String
let navigationIconPath: String
let mainContentMaximumWidth: CGFloat
}
final class WebsiteGenerator {
let language: ContentLanguage
let outputDirectory: URL
let postsPerPage: Int
let postFeedTitle: String
let postFeedDescription: String
let postFeedUrlPrefix: String
let navigationIconPath: String
let mainContentMaximumWidth: CGFloat
private let content: Content
private let imageGenerator: ImageGenerator
init(content: Content, configuration: WebsiteGeneratorConfiguration) {
self.language = configuration.language
self.outputDirectory = configuration.outputDirectory
self.postsPerPage = configuration.postsPerPage
self.postFeedTitle = configuration.postFeedTitle
self.postFeedDescription = configuration.postFeedDescription
self.postFeedUrlPrefix = configuration.postFeedUrlPrefix
self.navigationIconPath = configuration.navigationIconPath
self.mainContentMaximumWidth = configuration.mainContentMaximumWidth
self.content = content
self.imageGenerator = ImageGenerator(
storage: content.storage,
inputImageFolder: content.storage.filesFolder,
relativeImageOutputPath: "images")
}
func generateWebsite() -> Bool {
guard imageGenerator.prepareForGeneration() else {
return false
}
guard createPostFeedPages() else {
return false
}
guard imageGenerator.runJobs() else {
return false
}
return imageGenerator.save()
}
private func createPostFeedPages() -> Bool {
let totalCount = content.posts.count
guard totalCount > 0 else {
return true
}
let navBarData = createNavigationBarData()
let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up
for pageIndex in 1...numberOfPages {
let startIndex = (pageIndex - 1) * postsPerPage
let endIndex = min(pageIndex * postsPerPage, totalCount)
let postsOnPage = content.posts[startIndex..<endIndex]
guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navBarData) else {
return false
}
}
return true
}
private func createNavigationBarData() -> NavigationBarData {
let data = content.websiteData.localized(in: language)
let navigationItems: [NavigationBarLink] = content.websiteData.navigationTags.map {
let localized = $0.localized(in: language)
return .init(text: localized.name, url: localized.urlComponent)
}
return NavigationBarData(
navigationIconPath: navigationIconPath,
iconDescription: data.iconDescription,
navigationItems: navigationItems)
}
private func createImageSet(for image: ImageResource) -> FeedEntryData.Image {
let size1x = mainContentMaximumWidth
let size2x = mainContentMaximumWidth * 2
let avif1x = imageGenerator.generateVersion(for: image.id, type: .avif, maximumWidth: size1x, maximumHeight: size1x)
let avif2x = imageGenerator.generateVersion(for: image.id, type: .avif, maximumWidth: size2x, maximumHeight: size2x)
let webp1x = imageGenerator.generateVersion(for: image.id, type: .webp, maximumWidth: size1x, maximumHeight: size1x)
let webp2x = imageGenerator.generateVersion(for: image.id, type: .webp, maximumWidth: size2x, maximumHeight: size2x)
let jpg1x = imageGenerator.generateVersion(for: image.id, type: .jpg, maximumWidth: size1x, maximumHeight: size1x)
let jpg2x = imageGenerator.generateVersion(for: image.id, type: .jpg, maximumWidth: size2x, maximumHeight: size2x)
return FeedEntryData.Image(
altText: image.altText.getText(for: language),
avif1x: avif1x,
avif2x: avif2x,
webp1x: webp1x,
webp2x: webp2x,
jpg1x: jpg1x,
jpg2x: jpg2x)
}
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: NavigationBarData) -> Bool {
let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language)
let linkUrl = post.linkedPage.map {
FeedEntryData.Link(url: $0.localized(in: language).relativeUrl, text: "View")
}
return FeedEntryData(
entryId: "\(post.id)",
title: localized.title,
textAboveTitle: post.dateText(in: language),
link: linkUrl,
tags: post.tags.map { $0.data(in: language) },
text: [localized.content], // TODO: Convert from markdown to html
images: localized.images.map(createImageSet))
}
let feed = PageInFeed(
language: language,
title: postFeedTitle,
description: postFeedDescription,
navigationBarData: bar,
pageNumber: pageIndex,
totalPages: pageCount,
posts: posts)
let fileContent = feed.content
if pageIndex == 1 {
return save(fileContent, to: "\(postFeedUrlPrefix).html")
} else {
return save(fileContent, to: "\(postFeedUrlPrefix)-\(pageIndex).html")
}
}
private func save(_ content: String, to relativePath: String) -> Bool {
guard let data = content.data(using: .utf8) else {
print("Failed to create data for \(relativePath)")
return false
}
return save(data, to: relativePath)
}
private func save(_ data: Data, to relativePath: String) -> Bool {
self.content.storage.write(in: .outputPath) { folder in
let outputFile = folder.appendingPathComponent(relativePath, isDirectory: false)
do {
try data.write(to: outputFile)
return true
} catch {
print("Failed to save \(outputFile.path()): \(error)")
return false
}
}
}
}