import Foundation final class WebsiteGenerator { let language: ContentLanguage let localizedSettings: LocalizedSettings private var outputDirectory: URL { URL(filePath: content.settings.outputDirectoryPath) } private var postsPerPage: Int { content.settings.posts.postsPerPage } private var postFeedTitle: String { localizedSettings.posts.title } private var postFeedDescription: String { localizedSettings.posts.description } private var postFeedUrlPrefix: String { localizedSettings.posts.feedUrlPrefix } private var navigationIconPath: String { content.settings.navigationBar.iconPath } private var mainContentMaximumWidth: CGFloat { content.settings.posts.contentWidth } private let content: Content private let imageGenerator: ImageGenerator init(content: Content, language: ContentLanguage) { self.language = language self.content = content self.localizedSettings = content.settings.localized(in: language) self.imageGenerator = ImageGenerator( storage: content.storage, inputImageFolder: content.storage.filesFolder, relativeImageOutputPath: "images") } func generateWebsite(callback: (String) -> Void) -> Bool { guard imageGenerator.prepareForGeneration() else { return false } guard createPostFeedPages() else { return false } guard imageGenerator.runJobs(callback: callback) else { return false } return imageGenerator.save() } private func createPostFeedPages() -> Bool { let totalCount = content.posts.count guard totalCount > 0 else { return true } let navBarData = createNavigationBarData( settings: content.settings.navigationBar, iconDescription: localizedSettings.navigationBarIconDescription) 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 navigationItems: [NavigationBarLink] = settings.tags.map { let localized = $0.localized(in: language) return .init(text: localized.name, url: localized.urlComponent) } return NavigationBarData( navigationIconPath: navigationIconPath, iconDescription: 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 } } } }