import Foundation struct ImageToGenerate { let size: Int let image: FileResource } extension ImageToGenerate: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(size) hasher.combine(image.id) } } final class PageGenerationResults: ObservableObject { let itemId: LocalizedItemId private unowned let delegate: GenerationResults /// The files that could not be accessed @Published private(set) var inaccessibleFiles: Set /// The files that could not be parsed, with the error message produced @Published private(set) var unparsableFiles: [FileResource : Set] /// The missing files directly used by this page, and the source of the file @Published private(set) var missingFiles: [String: Set] /// The missing files linked to from other files. @Published private(set) var missingLinkedFiles: [String : Set] /// The missing tags linked to by this page, and the source of the link @Published private(set) var missingLinkedTags: [String : Set] /// The missing pages linked to by this page, and the source of the link @Published private(set) var missingLinkedPages: [String : Set] /// The footer scripts or html to add to the end of the body @Published private(set) var requiredFooters: Set /// The known header elements to include in the page @Published private(set) var requiredHeaders: Set /// The known icons that need to be included as hidden SVGs @Published private(set) var requiredIcons: Set /// The pages linked to by the page @Published private(set) var linkedPages: Set /// The tags linked to by this page @Published private(set) var linkedTags: Set /// The links to external content in this page @Published private(set) var externalLinks: Set /// The files used by this page, but not necessarily required in the output folder @Published private(set) var usedFiles: Set /// The files that need to be copied @Published private(set) var requiredFiles: Set /// The image versions required for this page @Published private(set) var imagesToGenerate: Set @Published private(set) var invalidCommands: [(command: CommandType?, markdown: String)] @Published private(set) var invalidBlocks: [(block: ContentBlock?, markdown: String)] @Published private(set) var warnings: Set /// The files that could not be saved to the output folder @Published private(set) var unsavedOutputFiles: [String: Set] = [:] private(set) var pageIsEmpty: Bool private(set) var redirect: (originalUrl: String, newUrl: String)? init(itemId: LocalizedItemId, delegate: GenerationResults) { self.itemId = itemId self.delegate = delegate inaccessibleFiles = [] unparsableFiles = [:] missingFiles = [:] missingLinkedFiles = [:] missingLinkedTags = [:] missingLinkedPages = [:] requiredHeaders = [] requiredFooters = [] requiredIcons = [] linkedPages = [] linkedTags = [] externalLinks = [] usedFiles = [] requiredFiles = [] imagesToGenerate = [] invalidCommands = [] invalidBlocks = [] warnings = [] unsavedOutputFiles = [:] pageIsEmpty = false redirect = nil } func onMain(_ operation: @escaping () -> Void) { DispatchQueue.main.async { operation() } } func reset() { onMain { self.inaccessibleFiles = [] self.unparsableFiles = [:] self.missingFiles = [:] self.missingLinkedFiles = [:] self.missingLinkedTags = [:] self.missingLinkedPages = [:] self.requiredHeaders = [] self.requiredFooters = [] self.requiredIcons = [] self.linkedPages = [] self.linkedTags = [] self.externalLinks = [] self.usedFiles = [] self.requiredFiles = [] self.imagesToGenerate = [] self.invalidCommands = [] self.invalidBlocks = [] self.warnings = [] self.unsavedOutputFiles = [:] self.pageIsEmpty = false self.redirect = nil } } // MARK: Adding entries func inaccessibleContent(file: FileResource) { onMain { self.inaccessibleFiles.insert(file) } delegate.inaccessibleContent(file: file) } func invalid(command: CommandType?, _ markdown: Substring) { let markdown = String(markdown) onMain { self.invalidCommands.append((command, markdown)) } delegate.invalidCommand(markdown) } func invalid(block: ContentBlock?, _ markdown: Substring) { let markdown = String(markdown) onMain { self.invalidBlocks.append((block, markdown)) } delegate.invalidBlock(markdown) } func missing(page: String, source: String) { onMain { self.missingLinkedPages[page, default: []].insert(source) } delegate.missing(page: page) } func missing(tag: String, source: String) { onMain { self.missingLinkedTags[tag, default: []].insert(source) } delegate.missing(tag: tag) } func missing(file: String, source: String) { onMain { self.missingFiles[file, default: []].insert(source) } delegate.missing(file: file) } func require(image: ImageVersion) { onMain { self.imagesToGenerate.insert(image) } used(file: image.image) delegate.generate(image) } func require(imageSet: ImageSet) { let jobs = imageSet.jobs onMain { self.imagesToGenerate.formUnion(jobs) } used(file: imageSet.image) delegate.generate(jobs) } func invalidFormat(file: FileResource, error: String) { onMain { self.unparsableFiles[file, default: []].insert(error) } delegate.unparsable(file: file) } func missing(file: String, containedIn sourceFile: FileResource) { onMain { self.missingLinkedFiles[file, default: []].insert(sourceFile) } delegate.missing(file: file) } func used(file: FileResource) { onMain { self.usedFiles.insert(file) } // TODO: Notify delegate } func require(file: FileResource) { onMain { self.requiredFiles.insert(file) } used(file: file) delegate.require(file: file) } func require(files: [FileResource]) { onMain { self.requiredFiles.formUnion(files) } onMain { self.usedFiles.formUnion(files) } delegate.require(files: files) } func require(footer: String) { onMain { self.requiredFooters.insert(footer) } } func require(header: KnownHeaderElement) { onMain { self.requiredHeaders.insert(header) } } func require(headers: KnownHeaderElement...) { onMain { self.requiredHeaders.formUnion(headers) } } func require(icon: PageIcon) { onMain { self.requiredIcons.insert(icon) } } func require(icons: PageIcon...) { onMain { self.requiredIcons.formUnion(icons) } } func require(icons: [PageIcon]) { onMain { self.requiredIcons.formUnion(icons) } } func linked(to page: Page) { onMain { self.linkedPages.insert(page) } } func linked(to tag: Tag) { onMain { self.linkedTags.insert(tag) } } func externalLink(to url: String) { onMain { self.externalLinks.insert(url) } delegate.externalLink(url) } func warning(_ warning: String) { onMain { self.warnings.insert(warning) } delegate.warning(warning) } func unsavedOutput(_ path: String, source: ItemReference) { onMain { self.unsavedOutputFiles[path, default: []].insert(source) } delegate.unsaved(path) } func markPageAsEmpty() { guard case .page(let page) = itemId.itemType else { return } onMain { self.pageIsEmpty = true } delegate.empty(.init(language: itemId.language, pageId: page.id)) } func redirect(from originalUrl: String, to newUrl: String) { onMain { self.redirect = (originalUrl, newUrl) } delegate.redirect(from: originalUrl, to: newUrl) } }