394 lines
12 KiB
Swift
394 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.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<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)
|
|
}
|
|
}
|
|
}
|