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 /** 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 /** 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 = GenericMetadata.metadataFileName guard let metadata = try GenericMetadata(source: source, 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, path: s) } } init?(parent: Element, folder: URL, with: Context, path: String) throws { let validation = context.validation self.inputFolder = folder self.path = path let source = path + "/" + GenericMetadata.metadataFileName guard let metadata = try GenericMetadata(source: 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.thumbnailStyle, 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: path, with: context) } } // MARK: Debug extension Element { func printTree(indentation: String = "") { print(indentation + "/" + path) elements.forEach { $0.printTree(indentation: indentation + " ") } } }