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 navigationIconPath: String { content.settings.navigationBar.iconPath } private var mainContentMaximumWidth: CGFloat { CGFloat(content.settings.posts.contentWidth) } private let content: Content private let imageGenerator: ImageGenerator private var navigationBarData: NavigationBarData { createNavigationBarData( settings: content.settings.navigationBar, iconDescription: localizedSettings.navigationBarIconDescription) } init(content: Content, language: ContentLanguage) { self.language = language self.content = content self.localizedSettings = content.settings.localized(in: language) self.imageGenerator = ImageGenerator( storage: content.storage, relativeImageOutputPath: "images") } func generateWebsite(callback: (String) -> Void) -> Bool { guard imageGenerator.prepareForGeneration() else { return false } guard createMainPostFeedPages() else { return false } guard generateTagPages() else { return false } guard imageGenerator.runJobs(callback: callback) else { return false } return imageGenerator.save() } private func createMainPostFeedPages() -> Bool { let generator = PostListPageGenerator( language: language, content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData, showTitle: false, pageTitle: localizedSettings.posts.title, pageDescription: localizedSettings.posts.description, pageUrlPrefix: localizedSettings.posts.feedUrlPrefix) return generator.createPages(for: content.posts) } private func generateTagPages() -> Bool { for tag in content.tags { let posts = content.posts.filter { $0.tags.contains(tag) } guard posts.count > 0 else { continue } let localized = tag.localized(in: language) #warning("Get tag url prefix from settings") let generator = PostListPageGenerator( language: language, content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData, showTitle: true, pageTitle: localized.name, pageDescription: localized.description ?? "", pageUrlPrefix: "tags/\(localized.urlComponent)") guard generator.createPages(for: posts) else { return false } } return true } private func createNavigationBarData(settings: NavigationBarSettings, iconDescription: String) -> 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 generatePagesFolderIfNeeded() -> Bool { let relativePath = content.settings.pages.pageUrlPrefix return content.storage.write(in: .outputPath) { folder in let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true) do { try outputFile.ensureFolderExistence() return true } catch { return false } } } func generate(page: Page) -> Bool { guard generatePagesFolderIfNeeded() else { print("Failed to generate output folder") return false } let pageGenerator = PageGenerator(content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData) let content: String do { content = try pageGenerator.generate(page: page, language: language) } catch { print("Failed to generate page \(page.id) in language \(language): \(error)") return false } let path = self.content.pageLink(page, language: language) + ".html" guard save(content, to: path) else { print("Failed to save page") return false } guard imageGenerator.runJobs(callback: { _ in }) else { return false } guard copy(requiredVideoFiles: pageGenerator.results.requiredVideoFiles) else { return false } return true } private func copy(requiredVideoFiles: Set) -> Bool { print("Copying \(requiredVideoFiles.count) videos...") for fileId in requiredVideoFiles { guard let outputPath = content.pathToFile(fileId) else { return false } do { try content.storage.copy(file: fileId, to: outputPath) } catch { print("Failed to copy video file: \(error)") return false } } return true } private func save(_ content: String, to relativePath: String) -> Bool { do { try self.content.storage.write(content: content, to: relativePath) return true } catch { print("Failed to write page \(relativePath)") return false } } }