ChWebsiteApp/CHDataManagement/Model/Content+Generation.swift
2025-02-24 19:12:15 +01:00

389 lines
12 KiB
Swift

import Foundation
extension Content {
func generateWebsiteInAllLanguages() {
performGenerationIfIdle {
self.results.reset()
self.storage.writeNotification = { [weak self] in
self?.results.created(outputFile: $0)
}
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 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<HeaderElement> {
var result: Set<HeaderElement> = [.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<HeaderElement> {
pageHeaders(css: settings.posts.defaultCssFile)
}
var contentPageHeaders: Set<HeaderElement> {
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)
}
}
}