import Foundation extension Content { func generateWebsiteInAllLanguages() { performGenerationIfIdle { self.results.reset() self.storage.writeNotification = { [weak self] in self?.results.created(outputFile: $0) } self.addGloballyRequiredFiles() self.generatePagesInternal() self.generatePostFeedPagesInternal() self.generateTagPagesInternal() self.generateTagOverviewPagesInternal() self.copyRequiredFiles() self.generateRequiredImages() self.results.recalculate() self.generateListOfExternalFiles() self.generateListOfUrlMappings() self.updateUnusedFiles() self.status("Generation completed") } } func endCurrentGeneration() { guard isGeneratingWebsite, shouldGenerateWebsite else { return } DispatchQueue.main.async { self.set(shouldGenerate: false) } } func generatePostFeedPages() { performGenerationIfIdle { self.generatePostFeedPagesInternal() } } func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) { performGenerationIfIdle { let results = self.results.makeResults(for: page, in: language) results.reset() let generator = PageContentParser(content: page.content, language: language, results: results) _ = generator.generatePage(from: content) self.results.recalculate() DispatchQueue.main.async { onComplete(results) } } } private func addGloballyRequiredFiles() { results.general.require(files: settings.general.requiredFiles) } private func copyRequiredFiles() { let count = results.requiredFiles.count var completed = 0 for file in results.requiredFiles { guard shouldGenerateWebsite else { return } defer { completed += 1 status("Copying required files: \(completed) / \(count)") } guard !file.isExternallyStored else { continue } let path = file.absoluteUrl if !storage.copy(file: file.id, to: path) { results.general.unsavedOutput(path, source: .general) } } } private func generateRequiredImages() { let images = results.imagesToGenerate.sorted() let count = images.count var completed = 0 func didFinishOneImage() { completed += 1 status("Generating required images: \(completed) / \(count)") } // Finish existing images var newImagesToGenerate: [ImageVersion] = [] var avifImagesToGenerate: [ImageVersion] = [] for image in images { guard shouldGenerateWebsite else { return } guard imageGenerator.needsToGenerate(image) else { results.created(outputFile: image.outputPath) didFinishOneImage() continue } if image.type == .avif { avifImagesToGenerate.append(image) } else { newImagesToGenerate.append(image) } } func generate(images: [ImageVersion]) { for image in images { guard shouldGenerateWebsite else { return } defer { didFinishOneImage() } if imageGenerator.generate(version: image) { results.created(outputFile: image.outputPath) continue } results.failed(image: image) } } generate(images: newImagesToGenerate) generate(images: avifImagesToGenerate) if completed != count { print("Expected \(count) images processed, but only \(completed) were") } } func generateAllPages() { performGenerationIfIdle { self.generatePagesInternal() } } func generatePage(_ page: Page) { performGenerationIfIdle { for language in ContentLanguage.allCases { self.generateInternal(page, in: language) } self.copyRequiredFiles() self.generateRequiredImages() } } func generatePage(_ page: Page, in language: ContentLanguage) { performGenerationIfIdle { self.generateInternal(page, in: language) } } // MARK: Find items by id func page(_ pageId: String) -> Page? { pages.first { $0.id == pageId } } func image(_ imageId: String) -> FileResource? { files.first { $0.id == imageId && $0.type.isImage } } func video(_ videoId: String) -> FileResource? { files.first { $0.id == videoId && $0.type.isVideo } } func file(_ fileId: String) -> FileResource? { files.first { $0.id == fileId } } func tag(_ tagId: String) -> Tag? { tags.first { $0.id == tagId } } // MARK: Generation input func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { settings.navigation.navigationItems.map { .init(text: $0.title(in: language), url: $0.absoluteUrl(in: language)) } } private func pageHeaders(css: FileResource?) -> Set { var result: Set = [.charset, .viewport] if let css { result.insert(.css(file: css, order: HeaderElement.defaultCssFileOrder)) } if let manifest = settings.pages.manifestFile { result.insert(.manifest(manifest)) } return result } var postPageHeaders: Set { pageHeaders(css: settings.posts.defaultCssFile) } var contentPageHeaders: Set { pageHeaders(css: settings.pages.defaultCssFile) } // MARK: Generation private func performGenerationIfIdle(_ operation: @escaping () -> ()) { DispatchQueue.main.async { guard !self.isGeneratingWebsite else { return } self.set(isGenerating: true) self.set(shouldGenerate: true) DispatchQueue.global(qos: .userInitiated).async { operation() DispatchQueue.main.async { self.set(isGenerating: false) self.set(shouldGenerate: false) } } } } private func status(_ message: String) { DispatchQueue.main.async { self.generationStatus = message } } /** - Note: Run on background thread */ private func generatePagesInternal() { let count = pages.count for index in pages.indices { guard shouldGenerateWebsite else { return } let page = pages[index] status("Generating pages: \(index) / \(count)") guard !page.isExternalUrl else { continue } for language in ContentLanguage.allCases { generateInternal(page, in: language) } } } /** - Note: Run on background thread */ private func generatePostFeedPagesInternal() { status("Generating post feed") for language in ContentLanguage.allCases { guard shouldGenerateWebsite else { return } let results = results.makeResults(for: .feed, in: language) let source = FeedGeneratorSource( language: language, content: self, results: results) let generator = PostListPageGenerator(source: source) generator.createPages(for: posts) } } /** - Note: Run on background thread */ private func generateTagPagesInternal() { let count = tags.count for index in tags.indices { guard shouldGenerateWebsite else { return } let tag = tags[index] status("Generating tag pages: \(index) / \(count)") generatePagesInternal(for: tag) } } /** - Note: Run on background thread */ private func generatePagesInternal(for tag: Tag) { for language in ContentLanguage.allCases { let results = results.makeResults(for: tag, in: language) let posts = posts.filter { $0.contains(tag) } guard posts.count > 0 else { continue } let source = TagPageGeneratorSource( language: language, content: self, results: results, tag: tag) let generator = PostListPageGenerator(source: source) generator.createPages(for: posts) if let originalUrl = tag.localized(in: language).originalUrl { results.redirect(from: originalUrl, to: tag.absoluteUrl(in: language)) } } } /** - Note: Run on background thread */ private func generateTagOverviewPagesInternal() { guard let tagOverview else { print("Generator: No tag overview page to generate") return } status("Generating tag overview page") for language in ContentLanguage.allCases { guard shouldGenerateWebsite else { return } let results = results.makeResults(for: .tagOverview, in: language) let generator = TagOverviewGenerator(content: self, language: language, results: results) generator.generatePages(tags: tags, overview: tagOverview) } } /** - Note: Run on background thread */ private func generateInternal(_ page: Page, in language: ContentLanguage) { let results = results.makeResults(for: page, in: language) let pageGenerator = PageGenerator(content: self) let relativePageUrl = page.absoluteUrl(in: language) let filePath = relativePageUrl + ".html" let pageUrl = settings.general.url + relativePageUrl guard let content = pageGenerator.generate(page: page, language: language, results: results, pageUrl: pageUrl) else { print("Failed to generate page \(page.id) in language \(language)") return } guard storage.write(content, to: filePath) else { print("Failed to save page \(page.id)") return } if let originalUrl = page.localized(in: language).originalUrl { results.redirect(from: originalUrl, to: pageUrl) } } // MARK: Additional infos private var externalFileListName: String { "external-files.txt" } private func generateListOfExternalFiles() { let files = results.requiredFiles .filter { $0.isExternallyStored } guard !files.isEmpty else { if storage.hasFileInOutputFolder(externalFileListName) { storage.deleteInOutputFolder(externalFileListName) } return } let content = files .map { $0.absoluteUrl } .sorted() .joined(separator: "\n") storage.write(content, to: externalFileListName) } private var redirectsListFileName: String { "redirects.conf" } private func generateListOfUrlMappings() { let redirects = results.redirects.map { "\($0.key) \($0.value);" } guard !redirects.isEmpty else { if storage.hasFileInOutputFolder(redirectsListFileName) { storage.deleteInOutputFolder(redirectsListFileName) } return } let list = redirects.sorted().joined(separator: "\n ") let content = """ map $request_uri $redirect_uri { /en.html /feed; /de.html /blog; \(list) } """ storage.write(content, to: redirectsListFileName) } private func updateUnusedFiles() { let existing = storage.getAllOutputFiles() DispatchQueue.main.async { self.results.determineFiles(unusedIn: existing) } } }