Refactor page content generators
This commit is contained in:
85
CHDataManagement/Generator/Results/GenerationAnomaly.swift
Normal file
85
CHDataManagement/Generator/Results/GenerationAnomaly.swift
Normal file
@ -0,0 +1,85 @@
|
||||
|
||||
enum GenerationAnomaly {
|
||||
case failedToLoadContent
|
||||
case failedToParseContent
|
||||
case missingFile(file: String, markdown: String)
|
||||
case missingPage(page: String, markdown: String)
|
||||
case missingTag(tag: String, markdown: String)
|
||||
case invalidCommand(command: CommandType?, markdown: String)
|
||||
case warning(String)
|
||||
}
|
||||
|
||||
extension GenerationAnomaly: Identifiable {
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .failedToLoadContent:
|
||||
return "load-failed"
|
||||
case .failedToParseContent:
|
||||
return "parse-failed"
|
||||
case .missingFile(let string, _):
|
||||
return "missing-file-\(string)"
|
||||
case .missingPage(let string, _):
|
||||
return "missing-page-\(string)"
|
||||
case .missingTag(let string, _):
|
||||
return "missing-tag-\(string)"
|
||||
case .invalidCommand(_, let markdown):
|
||||
return "invalid-command-\(markdown)"
|
||||
case .warning(let string):
|
||||
return "warning-\(string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerationAnomaly: Equatable {
|
||||
|
||||
static func == (lhs: GenerationAnomaly, rhs: GenerationAnomaly) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerationAnomaly: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerationAnomaly {
|
||||
|
||||
enum Severity: String, CaseIterable {
|
||||
case warning
|
||||
case error
|
||||
}
|
||||
|
||||
var severity: Severity {
|
||||
switch self {
|
||||
case .failedToLoadContent, .failedToParseContent:
|
||||
return .error
|
||||
case .missingFile, .missingPage, .missingTag, .invalidCommand, .warning:
|
||||
return .warning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerationAnomaly: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .failedToLoadContent:
|
||||
return "Failed to load content"
|
||||
case .failedToParseContent:
|
||||
return "Failed to parse content"
|
||||
case .missingFile(let string, _):
|
||||
return "Missing file: \(string)"
|
||||
case .missingPage(let string, _):
|
||||
return "Missing page: \(string)"
|
||||
case .missingTag(let string, _):
|
||||
return "Missing tag: \(string)"
|
||||
case .invalidCommand(_, let markdown):
|
||||
return "Invalid command: \(markdown)"
|
||||
case .warning(let string):
|
||||
return "Warning: \(string)"
|
||||
}
|
||||
}
|
||||
}
|
205
CHDataManagement/Generator/Results/GenerationResults.swift
Normal file
205
CHDataManagement/Generator/Results/GenerationResults.swift
Normal file
@ -0,0 +1,205 @@
|
||||
import Foundation
|
||||
|
||||
final class GenerationResults: ObservableObject {
|
||||
|
||||
/// The files that could not be accessed
|
||||
@Published
|
||||
var inaccessibleFiles: Set<FileResource> = []
|
||||
|
||||
/// The files that could not be parsed, with the error message produced
|
||||
@Published
|
||||
var unparsableFiles: Set<FileResource> = []
|
||||
|
||||
@Published
|
||||
var missingFiles: Set<String> = []
|
||||
|
||||
@Published
|
||||
var missingTags: Set<String> = []
|
||||
|
||||
@Published
|
||||
var missingPages: Set<String> = []
|
||||
|
||||
@Published
|
||||
var externalLinks: Set<String> = []
|
||||
|
||||
@Published
|
||||
var requiredFiles: Set<FileResource> = []
|
||||
|
||||
@Published
|
||||
var imagesToGenerate: Set<ImageVersion> = []
|
||||
|
||||
@Published
|
||||
var invalidCommands: Set<String> = []
|
||||
|
||||
@Published
|
||||
var invalidBlocks: Set<String> = []
|
||||
|
||||
@Published
|
||||
var warnings: Set<String> = []
|
||||
|
||||
@Published
|
||||
var unsavedOutputFiles: Set<String> = []
|
||||
|
||||
@Published
|
||||
var failedImages: Set<ImageVersion> = []
|
||||
|
||||
@Published
|
||||
var emptyPages: Set<LocalizedPageId> = []
|
||||
|
||||
/// The cache of previously used GenerationResults
|
||||
private var cache: [ItemId : PageGenerationResults] = [:]
|
||||
|
||||
private(set) var general: PageGenerationResults!
|
||||
|
||||
@Published
|
||||
var resultCount: Int = 0
|
||||
|
||||
// MARK: Life cycle
|
||||
|
||||
init() {
|
||||
let id = ItemId(language: .english, itemType: .general)
|
||||
let general = PageGenerationResults(itemId: id, delegate: self)
|
||||
self.general = general
|
||||
cache[id] = general
|
||||
self.resultCount = 1
|
||||
}
|
||||
|
||||
func makeResults(_ itemId: ItemId) -> 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: ItemType, in language: ContentLanguage) -> PageGenerationResults {
|
||||
let itemId = ItemId(language: language, itemType: type)
|
||||
return makeResults(itemId)
|
||||
}
|
||||
|
||||
func makeResults(for page: Page, in language: ContentLanguage) -> PageGenerationResults {
|
||||
let itemId = ItemId(language: language, itemType: .page(page))
|
||||
return makeResults(itemId)
|
||||
}
|
||||
|
||||
func makeResults(for tag: Tag, in language: ContentLanguage) -> PageGenerationResults {
|
||||
let itemId = ItemId(language: language, itemType: .tagPage(tag))
|
||||
return makeResults(itemId)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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<S>(files: S) where S: Sequence, S.Element == FileResource {
|
||||
update { self.requiredFiles.formUnion(files) }
|
||||
}
|
||||
|
||||
func generate(_ image: ImageVersion) {
|
||||
update { self.imagesToGenerate.insert(image) }
|
||||
}
|
||||
|
||||
func generate<S>(_ 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) }
|
||||
}
|
||||
}
|
||||
|
||||
private extension Dictionary where Value == Set<ItemId> {
|
||||
|
||||
mutating func remove<S>(keys: S, of item: ItemId) 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
255
CHDataManagement/Generator/Results/PageGenerationResults.swift
Normal file
255
CHDataManagement/Generator/Results/PageGenerationResults.swift
Normal file
@ -0,0 +1,255 @@
|
||||
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: ItemId
|
||||
|
||||
private unowned let delegate: GenerationResults
|
||||
|
||||
/// The files that could not be accessed
|
||||
private(set) var inaccessibleFiles: Set<FileResource>
|
||||
|
||||
/// The files that could not be parsed, with the error message produced
|
||||
private(set) var unparsableFiles: [FileResource : Set<String>]
|
||||
|
||||
/// The missing files directly used by this page, and the source of the file
|
||||
private(set) var missingFiles: [String: Set<String>]
|
||||
|
||||
/// The missing files linked to from other files.
|
||||
private(set) var missingLinkedFiles: [String : Set<FileResource>]
|
||||
|
||||
/// The missing tags linked to by this page, and the source of the link
|
||||
private(set) var missingLinkedTags: [String : Set<String>]
|
||||
|
||||
/// The missing pages linked to by this page, and the source of the link
|
||||
private(set) var missingLinkedPages: [String : Set<String>]
|
||||
|
||||
/// The footer scripts or html to add to the end of the body
|
||||
private(set) var requiredFooters: Set<String>
|
||||
|
||||
/// The known header elements to include in the page
|
||||
private(set) var requiredHeaders: Set<KnownHeaderElement>
|
||||
|
||||
/// The known icons that need to be included as hidden SVGs
|
||||
private(set) var requiredIcons: Set<PageIcon>
|
||||
|
||||
/// The pages linked to by the page
|
||||
private(set) var linkedPages: Set<Page>
|
||||
|
||||
/// The tags linked to by this page
|
||||
private(set) var linkedTags: Set<Tag>
|
||||
|
||||
/// The links to external content in this page
|
||||
private(set) var externalLinks: Set<String>
|
||||
|
||||
/// The files used by this page, but not necessarily required in the output folder
|
||||
private(set) var usedFiles: Set<FileResource>
|
||||
|
||||
/// The files that need to be copied
|
||||
private(set) var requiredFiles: Set<FileResource>
|
||||
|
||||
/// The image versions required for this page
|
||||
private(set) var imagesToGenerate: Set<ImageVersion>
|
||||
|
||||
private(set) var invalidCommands: [(command: CommandType?, markdown: String)]
|
||||
|
||||
private(set) var invalidBlocks: [(block: ContentBlock?, markdown: String)]
|
||||
|
||||
private(set) var warnings: Set<String>
|
||||
|
||||
/// The files that could not be saved to the output folder
|
||||
private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:]
|
||||
|
||||
private(set) var pageIsEmpty: Bool
|
||||
|
||||
init(itemId: ItemId, 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
|
||||
}
|
||||
|
||||
func reset() {
|
||||
inaccessibleFiles = []
|
||||
unparsableFiles = [:]
|
||||
missingFiles = [:]
|
||||
missingLinkedFiles = [:]
|
||||
missingLinkedTags = [:]
|
||||
missingLinkedPages = [:]
|
||||
requiredHeaders = []
|
||||
requiredFooters = []
|
||||
requiredIcons = []
|
||||
linkedPages = []
|
||||
linkedTags = []
|
||||
externalLinks = []
|
||||
usedFiles = []
|
||||
requiredFiles = []
|
||||
imagesToGenerate = []
|
||||
invalidCommands = []
|
||||
invalidBlocks = []
|
||||
warnings = []
|
||||
unsavedOutputFiles = [:]
|
||||
pageIsEmpty = false
|
||||
}
|
||||
|
||||
// MARK: Adding entries
|
||||
|
||||
func inaccessibleContent(file: FileResource) {
|
||||
inaccessibleFiles.insert(file)
|
||||
delegate.inaccessibleContent(file: file)
|
||||
}
|
||||
|
||||
func invalid(command: CommandType?, _ markdown: Substring) {
|
||||
let markdown = String(markdown)
|
||||
invalidCommands.append((command, markdown))
|
||||
delegate.invalidCommand(markdown)
|
||||
}
|
||||
|
||||
func invalid(block: ContentBlock?, _ markdown: Substring) {
|
||||
let markdown = String(markdown)
|
||||
invalidBlocks.append((block, markdown))
|
||||
delegate.invalidBlock(markdown)
|
||||
}
|
||||
|
||||
func missing(page: String, source: String) {
|
||||
missingLinkedPages[page, default: []].insert(source)
|
||||
delegate.missing(page: page)
|
||||
}
|
||||
|
||||
func missing(tag: String, source: String) {
|
||||
missingLinkedTags[tag, default: []].insert(source)
|
||||
delegate.missing(tag: tag)
|
||||
}
|
||||
|
||||
func missing(file: String, source: String) {
|
||||
missingFiles[file, default: []].insert(source)
|
||||
delegate.missing(file: file)
|
||||
}
|
||||
|
||||
func require(image: ImageVersion) {
|
||||
imagesToGenerate.insert(image)
|
||||
used(file: image.image)
|
||||
delegate.generate(image)
|
||||
}
|
||||
|
||||
func require(imageSet: ImageSet) {
|
||||
let jobs = imageSet.jobs
|
||||
imagesToGenerate.formUnion(jobs)
|
||||
used(file: imageSet.image)
|
||||
delegate.generate(jobs)
|
||||
}
|
||||
|
||||
func invalidFormat(file: FileResource, error: String) {
|
||||
unparsableFiles[file, default: []].insert(error)
|
||||
delegate.unparsable(file: file)
|
||||
}
|
||||
|
||||
func missing(file: String, containedIn sourceFile: FileResource) {
|
||||
missingLinkedFiles[file, default: []].insert(sourceFile)
|
||||
delegate.missing(file: file)
|
||||
}
|
||||
|
||||
func used(file: FileResource) {
|
||||
usedFiles.insert(file)
|
||||
// TODO: Notify delegate
|
||||
}
|
||||
|
||||
func require(file: FileResource) {
|
||||
requiredFiles.insert(file)
|
||||
usedFiles.insert(file)
|
||||
delegate.require(file: file)
|
||||
}
|
||||
|
||||
func require(files: [FileResource]) {
|
||||
requiredFiles.formUnion(files)
|
||||
usedFiles.formUnion(files)
|
||||
delegate.require(files: files)
|
||||
}
|
||||
|
||||
func require(footer: String) {
|
||||
requiredFooters.insert(footer)
|
||||
}
|
||||
|
||||
func require(header: KnownHeaderElement) {
|
||||
requiredHeaders.insert(header)
|
||||
}
|
||||
|
||||
func require(headers: KnownHeaderElement...) {
|
||||
requiredHeaders.formUnion(headers)
|
||||
}
|
||||
|
||||
func require(icon: PageIcon) {
|
||||
requiredIcons.insert(icon)
|
||||
}
|
||||
|
||||
func require(icons: PageIcon...) {
|
||||
requiredIcons.formUnion(icons)
|
||||
}
|
||||
|
||||
func require(icons: [PageIcon]) {
|
||||
requiredIcons.formUnion(icons)
|
||||
}
|
||||
|
||||
func linked(to page: Page) {
|
||||
linkedPages.insert(page)
|
||||
}
|
||||
|
||||
func linked(to tag: Tag) {
|
||||
linkedTags.insert(tag)
|
||||
}
|
||||
|
||||
func externalLink(to url: String) {
|
||||
externalLinks.insert(url)
|
||||
delegate.externalLink(url)
|
||||
}
|
||||
|
||||
func warning(_ warning: String) {
|
||||
warnings.insert(warning)
|
||||
delegate.warning(warning)
|
||||
}
|
||||
|
||||
func unsavedOutput(_ path: String, source: ItemType) {
|
||||
unsavedOutputFiles[path, default: []].insert(source)
|
||||
delegate.unsaved(path)
|
||||
}
|
||||
|
||||
func markPageAsEmpty() {
|
||||
guard case .page(let page) = itemId.itemType else { return }
|
||||
pageIsEmpty = true
|
||||
delegate.empty(.init(language: itemId.language, pageId: page.id))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user