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.. 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, 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 } } } }