import Foundation final class GenerationResults: ObservableObject { /// The files that could not be accessed @Published var inaccessibleFiles: Set = [] /// The files that could not be parsed, with the error message produced @Published var unparsableFiles: Set = [] @Published var missingFiles: Set = [] @Published var missingTags: Set = [] @Published var missingPages: Set = [] @Published var externalLinks: Set = [] @Published var requiredFiles: Set = [] @Published var imagesToGenerate: Set = [] @Published var invalidCommands: Set = [] @Published var invalidBlocks: Set = [] @Published var warnings: Set = [] @Published var unsavedOutputFiles: Set = [] @Published var failedImages: Set = [] @Published var emptyPages: Set = [] @Published var outputFiles: Set = [] @Published var unusedFilesInOutput: Set = [] /** The url redirects to install to prevent broken links. The key is the original url, and the value the new url of the content, relative to the output folder. The dictionary is used to create a map of redirects for Nginx. */ @Published var redirects: [String : String] = [:] /// The cache of previously used GenerationResults private var cache: [LocalizedItemId : PageGenerationResults] = [:] private(set) var general: PageGenerationResults! @Published var resultCount: Int = 0 // MARK: Life cycle init() { let id = LocalizedItemId(language: .english, itemType: .general) let general = PageGenerationResults(itemId: id, delegate: self) self.general = general cache[id] = general self.resultCount = 0 } func makeResults(_ itemId: LocalizedItemId) -> PageGenerationResults { guard let result = cache[itemId] else { let result = PageGenerationResults(itemId: itemId, delegate: self) cache[itemId] = result update { self.resultCount += 1 } return result } return result } func makeResults(for type: ItemReference, in language: ContentLanguage) -> PageGenerationResults { let itemId = LocalizedItemId(language: language, itemType: type) return makeResults(itemId) } func makeResults(for page: Page, in language: ContentLanguage) -> PageGenerationResults { let itemId = LocalizedItemId(language: language, itemType: .page(page)) return makeResults(itemId) } func makeResults(for tag: Tag, in language: ContentLanguage) -> PageGenerationResults { let itemId = LocalizedItemId(language: language, itemType: .tagPage(tag)) return makeResults(itemId) } func reset() { update { self.inaccessibleFiles = [] self.unparsableFiles = [] self.missingFiles = [] self.missingTags = [] self.missingPages = [] self.externalLinks = [] self.requiredFiles = [] self.imagesToGenerate = [] self.invalidCommands = [] self.invalidBlocks = [] self.warnings = [] self.unsavedOutputFiles = [] self.emptyPages = [] self.redirects = [:] self.outputFiles = [] self.unusedFilesInOutput = [] } for result in cache.values { result.reset() } } func recalculate() { let inaccessibleFiles = cache.values.map { $0.inaccessibleFiles }.union() update { self.inaccessibleFiles = inaccessibleFiles } let unparsableFiles = cache.values.map { $0.unparsableFiles.keys }.union() update { self.unparsableFiles = unparsableFiles } let missingFiles = cache.values.map { $0.missingFiles.keys }.union() update { self.missingFiles = missingFiles } let missingTags = cache.values.map { $0.missingLinkedTags.keys }.union() update { self.missingTags = missingTags } let missingPages = cache.values.map { $0.missingLinkedPages.keys }.union() update { self.missingPages = missingPages } let externalLinks = cache.values.map { $0.externalLinks }.union() update { self.externalLinks = externalLinks } let requiredFiles = cache.values.map { $0.requiredFiles }.union() update { self.requiredFiles = requiredFiles } let imagesToGenerate = cache.values.map { $0.imagesToGenerate }.union() update { self.imagesToGenerate = imagesToGenerate } let invalidCommands = cache.values.map { $0.invalidCommands.map { $0.markdown }}.union() update { self.invalidCommands = invalidCommands } let invalidBlocks = cache.values.map { $0.invalidBlocks.map { $0.markdown }}.union() update { self.invalidBlocks = invalidBlocks } let warnings = cache.values.map { $0.warnings }.union() update { self.warnings = warnings } let unsavedOutputFiles = cache.values.map { $0.unsavedOutputFiles.keys }.union() update { self.unsavedOutputFiles = unsavedOutputFiles } let emptyPages = cache.values.filter { $0.pageIsEmpty }.map { $0.itemId }.compactMap { id -> LocalizedPageId? in guard case .page(let page) = id.itemType else { return nil } return LocalizedPageId(language: id.language, pageId: page.id) }.asSet() update { self.emptyPages = emptyPages } let redirects = cache.values.compactMap { $0.redirect }.reduce(into: [:]) { $0[$1.originalUrl] = $1.newUrl } update { self.redirects = redirects } } private func update(_ operation: @escaping () -> Void) { DispatchQueue.main.async { operation() } } // MARK: Adding entries func inaccessibleContent(file: FileResource) { update { self.inaccessibleFiles.insert(file) } } func unparsable(file: FileResource) { update { self.unparsableFiles.insert(file) } } func missing(file: String) { update { self.missingFiles.insert(file) } } func missing(tag: String) { update { self.missingTags.insert(tag) } } func missing(page: String) { update { self.missingPages.insert(page) } } func externalLink(_ url: String) { update { self.externalLinks.insert(url) } } func require(file: FileResource) { update { self.requiredFiles.insert(file) } } func require(files: S) where S: Sequence, S.Element == FileResource { update { self.requiredFiles.formUnion(files) } } func generate(_ image: ImageVersion) { update { self.imagesToGenerate.insert(image) } } func generate(_ images: S) where S: Sequence, S.Element == ImageVersion { update { self.imagesToGenerate.formUnion(images) } } func invalidCommand(_ markdown: String) { update { self.invalidCommands.insert(markdown) } } func invalidBlock(_ markdown: String) { update { self.invalidBlocks.insert(markdown) } } func warning(_ warning: String) { update { self.warnings.insert(warning) } } func failed(image: ImageVersion) { update { self.failedImages.insert(image) } } func unsaved(_ path: String) { update { self.unsavedOutputFiles.insert(path) } } func empty(_ page: LocalizedPageId) { update { self.emptyPages.insert(page) } } func redirect(from originalUrl: String, to newUrl: String) { update { self.redirects[originalUrl] = newUrl } } func created(outputFile: String) { update { self.outputFiles.insert(outputFile.withLeadingSlashRemoved) } } func determineFiles(unusedIn existingFiles: Set) { let unused = existingFiles.subtracting(outputFiles) update { self.unusedFilesInOutput = unused } } } private extension Dictionary where Value == Set { mutating func remove(keys: S, of item: LocalizedItemId) where S: Sequence, S.Element == Key { for key in keys { guard var value = self[key] else { continue } value.remove(item) if value.isEmpty { self[key] = nil } else { self[key] = value } } } }