CHGenerator/WebsiteGenerator/Generic/Element.swift
2022-08-19 18:05:06 +02:00

238 lines
9.1 KiB
Swift

import Foundation
struct Element {
static let overviewItemCountDefault = 6
/**
The author of the content.
If no author is set, then the author from the parent element is used.
*/
let author: String
/**
The title used in the top bar of the website, next to the logo.
This title can be HTML content, and only the root level value is used.
*/
let topBarTitle: String
/**
The url where the site will be deployed.
This value is required to build absolute links for link previews.
- Note: Only the root level value is used.
- Note: The path does not need to contain a trailing slash.
*/
let deployedBaseUrl: String
/**
The (start) date of the element.
The date is printed on content pages and may also used for sorting elements,
depending on the `useManualSorting` property of the parent.
*/
let date: Date?
/**
The end date of the element.
This property can be used to specify a date range for a content page.
*/
let endDate: Date?
/**
The deployment state of the page.
- Note: This property defaults to ``PageState.standard`
*/
let state: PageState
/**
The sort index of the page for manual sorting.
- Note: This property is only used (and must be set) if `useManualSorting` option of the parent is set.
*/
let sortIndex: Int?
/**
All files which may occur in content but is stored externally.
Missing files which would otherwise produce a warning are ignored when included here.
- Note: This property defaults to an empty set.
*/
let externalFiles: Set<String>
/**
Specifies additional files which should be copied to the destination when generating the content.
- Note: This property defaults to an empty set.
*/
let requiredFiles: Set<String>
/**
The style of thumbnail to use when generating overviews.
- Note: This property is only relevant for sections.
- Note: This property is inherited from the parent if not specified.
*/
let thumbnailStyle: ThumbnailStyle
/**
Sort the child elements by their `sortIndex` property when generating overviews, instead of using the `date`.
- Note: This property is only relevant for sections.
- Note: This property defaults to `false`
*/
let useManualSorting: Bool
/**
The number of items to show when generating overviews of this element.
- Note: This property is only relevant for sections.
- Note: This property is inherited from the parent if not specified.
*/
let overviewItemCount: Int
/**
The localized metadata for each language.
*/
let languages: [LocalizedMetadata]
/**
All elements contained within the element.
If the element is a section, then this property contains the pages or subsections within.
*/
var elements: [Element] = []
/**
The url of the element's folder in the source hierarchy.
- Note: This property is essentially the root folder of the site, appended with the value of the ``path`` property.
*/
let inputFolder: URL
/**
The path to the element's folder in the source hierarchy (without a leading slash).
*/
let path: String
/**
Create the root element of a site.
The root element will recursively move into subfolders and build the site content
by looking for metadata files in each subfolder.
- Parameter folder: The root folder of the site content.
- Parameter context: The context to create the element (validation, file access, etc.)
*/
init?(atRoot folder: URL, with context: Context) throws {
let validation = context.validation
self.inputFolder = folder
self.path = ""
let source = "root"
guard let metadata = try GenericMetadata(path: nil, with: context) else {
return nil
}
self.author = validation.required(metadata.author, name: "author", source: source) ?? "author"
self.topBarTitle = validation
.required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website"
self.deployedBaseUrl = validation
.required(metadata.deployedBaseUrl, name: "deployedBaseUrl", source: source) ?? "https://example.com"
self.date = validation.unused(metadata.date, "date", source: source)
self.endDate = validation.unused(metadata.endDate, "endDate", source: source)
self.state = validation.state(metadata.state, source: source)
self.sortIndex = validation.unused(metadata.sortIndex, "sortIndex", source: source)
self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? []
self.thumbnailStyle = validation.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large
self.useManualSorting = validation.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
self.languages = validation.required(metadata.languages, name: "languages", source: source)?
.compactMap { language in
.init(atRoot: folder, data: language, with: context)
} ?? []
try self.readElements(in: folder, source: nil, with: context)
}
mutating func readElements(in folder: URL, source: String?, with context: Context) throws {
let subFolders: [URL]
do {
subFolders = try FileSystem.folders(in: folder)
} catch {
context.validation.add(error: "Failed to read subfolders", source: source ?? "root", error: error)
return
}
self.elements = try subFolders.compactMap { subFolder in
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent
return try Element(parent: self, folder: subFolder, with: context, source: s)
}
}
init?(parent: Element, folder: URL, with: Context, source: String) throws {
let validation = context.validation
self.inputFolder = folder
self.path = source
guard let metadata = try GenericMetadata(path: source, with: context) else {
return nil
}
self.author = metadata.author ?? parent.author
self.topBarTitle = validation
.unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle
self.deployedBaseUrl = validation
.unused(metadata.deployedBaseUrl, "deployedBaseUrl", source: source) ?? parent.deployedBaseUrl
let date = validation.date(from: metadata.date, property: "date", source: source).ifNil {
if !parent.useManualSorting {
validation.add(error: "No 'date', but parent defines 'useManualSorting' = false", source: source)
}
}
self.date = date
self.endDate = validation.date(from: metadata.endDate, property: "endDate", source: source).ifNotNil {
if date == nil {
validation.add(warning: "Set 'endDate', but no 'date'", source: source)
}
}
self.state = validation.state(metadata.state, source: source)
self.sortIndex = metadata.sortIndex.ifNil {
if parent.useManualSorting {
validation.add(error: "No 'sortIndex', but parent defines 'useManualSorting' = true", source: source)
}
}
// TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? []
self.thumbnailStyle = validation.thumbnailStyle(metadata.state, source: source)
self.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
self.languages = parent.languages.compactMap { parentData in
guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else {
validation.add(info: "Language '\(parentData.language)' not found", source: source)
return nil
}
return .init(folder: folder, data: data, source: source, parent: parentData, with: context)
}
// Check that each 'language' tag is present, and that all languages appear in the parent
validation.required(metadata.languages, name: "languages", source: source)?
.compactMap { validation.required($0.language, name: "language", source: source) }
.filter { language in
!parent.languages.contains { $0.language == language }
}
.forEach {
validation.add(warning: "Language '\($0)' not found in parent, so not generated", source: source)
}
try self.readElements(in: folder, source: source, with: context)
}
}
// MARK: Debug
extension Element {
func printTree(indentation: String = "") {
print(indentation + "/" + path)
elements.forEach { $0.printTree(indentation: indentation + " ") }
}
}