Convert Xcode project to swift package

This commit is contained in:
Christoph Hagen
2022-09-09 11:18:32 +02:00
parent 64db75fb44
commit 2a9061c1d6
54 changed files with 30 additions and 724 deletions

View File

@ -0,0 +1,229 @@
import Foundation
extension Element {
/**
Metadata localized for a specific language.
*/
struct LocalizedMetadata {
static let moreLinkDefaultText = "DefaultMoreText"
/**
The language for which the content is specified.
- Note: This field is mandatory
*/
let language: String
/**
- Note: This field is mandatory
The title used in the page header.
*/
let title: String
/**
The subtitle used in the page header.
*/
let subtitle: String?
/**
The description text used in the page header.
*/
let description: String?
/**
The title to use for the link preview.
If `nil` is specified, then the localized element `title` is used.
*/
let linkPreviewTitle: String
/**
The file name of the link preview image.
- Note: The image must be located in the element folder.
- Note: If `nil` is specified, then the (localized) thumbnail is used, if available.
*/
let linkPreviewImage: String?
/**
The description text for the link preview.
- Note: If `nil` is specified, then first the (localized) element `subtitle` is used.
If this is `nil` too, then the localized `description` of the element is used.
*/
let linkPreviewDescription: String
/**
The text on the link to show the section page when previewing multiple sections on an overview page.
- Note: If this value is inherited from the parent, if it is not defined. There must be at least one
element in the path that defines this property.
*/
let moreLinkText: String
/**
The text on the back navigation link of **contained** elements.
This text does not appear on the section page, but on the pages contained within the section.
*/
let backLinkText: String
/**
The text on the back navigation link of the **parent** element.
This text appears on the section page, but not on the pages contained within the section.
*/
let parentBackLinkText: String
/**
The text to show as a title for placeholder boxes
Placeholders are included in missing pages.
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
*/
let placeholderTitle: String
/**
The text to show as a description for placeholder boxes
Placeholders are included in missing pages.
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
*/
let placeholderText: String
/**
An optional suffix to add to the title on a page.
This can be useful to express a different author, project grouping, etc.
*/
let titleSuffix: String?
/**
An optional suffix to add to the thumbnail title of a page.
This can be useful to express a different author, project grouping, etc.
*/
let thumbnailSuffix: String?
/**
A text to place in the top right corner of a large thumbnail.
The text should be a very short string to fit into the corner, like `soon`, or `draft`
- Note: This property is ignored if `thumbnailStyle` is not `large`.
*/
let cornerText: String?
/**
The external url to use instead of automatically generating the page.
This property can be used for links to other parts of the site, like additional services.
It can also be set to manually write a page.
*/
let externalUrl: String?
}
}
extension Element.LocalizedMetadata {
init?(atRoot folder: URL, data: GenericMetadata.LocalizedMetadata) {
// Go through all elements and check them for completeness
// In the end, check that all required elements are present
var isComplete = true
func markAsIncomplete() {
isComplete = false
}
let source = "root"
self.language = log
.required(data.language, name: "language", source: source)
.ifNil(markAsIncomplete) ?? ""
self.title = log
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.subtitle = data.subtitle
self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source)
.ifNil(markAsIncomplete) ?? ""
self.moreLinkText = data.moreLinkText ?? Element.LocalizedMetadata.moreLinkDefaultText
self.backLinkText = log
.required(data.backLinkText, name: "backLinkText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.parentBackLinkText = "" // Root has no parent
self.placeholderTitle = log
.required(data.placeholderTitle, name: "placeholderTitle", source: source)
.ifNil(markAsIncomplete) ?? ""
self.placeholderText = log
.required(data.placeholderText, name: "placeholderText", source: source)
.ifNil(markAsIncomplete) ?? ""
self.titleSuffix = data.titleSuffix
self.thumbnailSuffix = log.unused(data.thumbnailSuffix, "thumbnailSuffix", source: source)
self.cornerText = log.unused(data.cornerText, "cornerText", source: source)
self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source)
guard isComplete else {
return nil
}
}
init?(folder: URL, data: GenericMetadata.LocalizedMetadata, source: String, parent: Element.LocalizedMetadata) {
// Go through all elements and check them for completeness
// In the end, check that all required elements are present
var isComplete = true
func markAsIncomplete() {
isComplete = false
}
self.language = parent.language
self.title = log
.required(data.title, name: "title", source: source)
.ifNil(markAsIncomplete) ?? ""
self.subtitle = data.subtitle
self.description = data.description
self.linkPreviewTitle = data.linkPreviewTitle ?? data.title ?? ""
self.linkPreviewImage = log
.linkPreviewThumbnail(customFile: data.linkPreviewImage, for: language, in: folder, source: source)
let linkPreviewDescription = data.linkPreviewDescription ?? data.description ?? data.subtitle
self.linkPreviewDescription = log
.required(linkPreviewDescription, name: "linkPreviewDescription", source: source)
.ifNil(markAsIncomplete) ?? ""
self.moreLinkText = log.moreLinkText(data.moreLinkText, parent: parent.moreLinkText, source: source)
self.backLinkText = data.backLinkText ?? data.title ?? ""
self.parentBackLinkText = parent.backLinkText
self.placeholderTitle = data.placeholderTitle ?? parent.placeholderTitle
self.placeholderText = data.placeholderText ?? parent.placeholderText
self.titleSuffix = data.titleSuffix
self.thumbnailSuffix = data.thumbnailSuffix
self.cornerText = data.cornerText
self.externalUrl = data.externalUrl
guard isComplete else {
return nil
}
}
}
// MARK: Thumbnails
extension Element {
static let defaultThumbnailName = "thumbnail.jpg"
static func localizedThumbnailName(for language: String) -> String {
"thumbnail-\(language).jpg"
}
static func findThumbnail(for language: String, in folder: URL) -> String? {
let localizedThumbnail = localizedThumbnailName(for: language)
let localizedThumbnailUrl = folder.appendingPathComponent(localizedThumbnail)
if localizedThumbnailUrl.exists {
return localizedThumbnail
}
let defaultThumbnailUrl = folder.appendingPathComponent(defaultThumbnailName)
if defaultThumbnailUrl.exists {
return defaultThumbnailName
}
return nil
}
}

View File

@ -0,0 +1,596 @@
import Foundation
struct Element {
static let overviewItemCountDefault = 6
/**
The default unique id for the root element
*/
static let defaultRootId = "root"
/**
The unique id of the element.
The id is used for short-hand links to pages, in the form of `![page](page_id)`
for thumbnail previews or `[text](page:page_id)` for simple links.
- Note: The default id for the root element is specified by ``defaultRootId``
The id can be manually specified using ``GenericMetadata.id``,
otherwise it is set to the name of the element folder.
*/
let id: String
/**
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 (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>
/**
Additional images required by the element.
These images are specified as: `source_name destination_name width (height)`.
*/
let images: [ManualImage]
/**
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
/**
Indicate the header type to be generated automatically.
If this option is set to `none`, then custom header code should be present in the page source files
- Note: If not specified, this property defaults to `left`.
- Note: Overview pages are always using `center`.
*/
let headerType: HeaderType
/**
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.
- Note: Uses global objects.
*/
init?(atRoot folder: URL) {
self.inputFolder = folder
self.path = ""
let source = GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else {
return nil
}
self.id = metadata.customId ?? Element.defaultRootId
self.author = log.required(metadata.author, name: "author", source: source) ?? "author"
self.topBarTitle = log
.required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website"
self.date = log.unused(metadata.date, "date", source: source)
self.endDate = log.unused(metadata.endDate, "endDate", source: source)
self.state = log.state(metadata.state, source: source)
self.sortIndex = log.unused(metadata.sortIndex, "sortIndex", source: source)
self.externalFiles = metadata.externalFiles ?? []
self.requiredFiles = metadata.requiredFiles ?? [] // Paths are already relative to root
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: "") } ?? []
self.thumbnailStyle = log.unused(metadata.thumbnailStyle, "thumbnailStyle", source: source) ?? .large
self.useManualSorting = log.unused(metadata.useManualSorting, "useManualSorting", source: source) ?? true
self.overviewItemCount = metadata.overviewItemCount ?? Element.overviewItemCountDefault
self.headerType = log.headerType(metadata.headerType, source: source)
self.languages = log.required(metadata.languages, name: "languages", source: source)?
.compactMap { language in
.init(atRoot: folder, data: language)
} ?? []
// All properties initialized
guard !languages.isEmpty else {
log.add(error: "No languages found", source: source)
return nil
}
files.add(page: path, id: id)
self.readElements(in: folder, source: nil)
}
mutating func readElements(in folder: URL, source: String?) {
let subFolders: [URL]
do {
subFolders = try FileManager.default
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
.filter { $0.isDirectory }
} catch {
log.add(error: "Failed to read subfolders", source: source ?? "root", error: error)
return
}
self.elements = subFolders.compactMap { subFolder in
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent
return Element(parent: self, folder: subFolder, path: s)
}
}
init?(parent: Element, folder: URL, path: String) {
self.inputFolder = folder
self.path = path
let source = path + "/" + GenericMetadata.metadataFileName
guard let metadata = GenericMetadata(source: source) else {
return nil
}
self.id = metadata.customId ?? folder.lastPathComponent
self.author = metadata.author ?? parent.author
self.topBarTitle = log
.unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle
let date = log.date(from: metadata.date, property: "date", source: source).ifNil {
if !parent.useManualSorting {
log.add(error: "No 'date', but parent defines 'useManualSorting' = false", source: source)
}
}
self.date = date
self.endDate = log.date(from: metadata.endDate, property: "endDate", source: source).ifNotNil {
if date == nil {
log.add(warning: "Set 'endDate', but no 'date'", source: source)
}
}
let state = log.state(metadata.state, source: source)
self.state = state
self.sortIndex = metadata.sortIndex.ifNil {
if state != .hidden, parent.useManualSorting {
log.add(error: "No 'sortIndex', but parent defines 'useManualSorting' = true", source: source)
}
}
// TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path)
self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path)
self.images = metadata.images?.compactMap { ManualImage(input: $0, path: path) } ?? []
self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source)
self.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
self.headerType = log.headerType(metadata.headerType, source: source)
self.languages = parent.languages.compactMap { parentData in
guard let data = metadata.languages?.first(where: { $0.language == parentData.language }) else {
log.add(info: "Language '\(parentData.language)' not found", source: source)
return nil
}
return .init(folder: folder, data: data, source: source, parent: parentData)
}
// Check that each 'language' tag is present, and that all languages appear in the parent
log.required(metadata.languages, name: "languages", source: source)?
.compactMap { log.required($0.language, name: "language", source: source) }
.filter { language in
!parent.languages.contains { $0.language == language }
}
.forEach {
log.add(warning: "Language '\($0)' not found in parent, so not generated", source: source)
}
// All properties initialized
files.add(page: path, id: id)
self.readElements(in: folder, source: path)
}
}
// MARK: Paths
extension Element {
/**
The localized html file name for a language, including a leading slash.
*/
static func htmlPagePathAddition(for language: String) -> String {
"/" + htmlPageName(for: language)
}
/**
The localized html file name for a language, without the leading slash.
*/
static func htmlPageName(for language: String) -> String {
"\(language).html"
}
var containsElements: Bool {
!elements.isEmpty
}
var hasNestingElements: Bool {
elements.contains { $0.containsElements }
}
func itemsForOverview(_ count: Int? = nil) -> [Element] {
if let shownItemCount = count {
return Array(sortedItems.prefix(shownItemCount))
} else {
return sortedItems
}
}
var sortedItems: [Element] {
if useManualSorting {
return shownItems.sorted { $0.sortIndex! < $1.sortIndex! }
}
return shownItems.sorted { $0.date! > $1.date! }
}
private var shownItems: [Element] {
elements.filter { $0.state.isShownInOverview }
}
/**
The url of the top-level section of the element.
*/
func sectionUrl(for language: String) -> String {
path.components(separatedBy: "/").first! + Element.htmlPagePathAddition(for: language)
}
/**
Create a relative link to another file in the tree.
- Parameter file: The full path of the target file, including localization
- Returns: The relative url from a localized page of the element to the target file.
*/
func relativePathToOtherSiteElement(file: String) -> String {
// Note: The element `path` is missing the last component
// i.e. travel/alps instead of travel/alps/en.html
let ownParts = path.components(separatedBy: "/")
let pageParts = file.components(separatedBy: "/")
// Find the common elements of the path, which can be discarded
var index = 0
while pageParts[index] == ownParts[index] {
index += 1
}
// The relative path needs to go down to the first common folder,
// before going up to the target page
let allParts = [String](repeating: "..", count: ownParts.count-index)
+ pageParts.dropFirst(index)
return allParts.joined(separator: "/")
}
/**
The relative path to the site root.
*/
var pathToRoot: String? {
guard path != "" else {
return nil
}
let downPathCount = path.components(separatedBy: "/").count
return [String](repeating: "..", count: downPathCount).joined(separator: "/")
}
/**
Create an absolute path (relative to the root directory) for a file contained in the elements folder.
This function is used to copy required input files and to generate images
*/
func pathRelativeToRootForContainedInputFile(_ filePath: String) -> String {
Element.relativeToRoot(filePath: filePath, folder: path)
}
/**
Create an absolute path (relative to the root directory) for a file contained in the elements folder.
This function is used to copy required input files and to generate images
*/
func nonAbsolutePathRelativeToRootForContainedInputFile(_ filePath: String) -> String? {
Element.containedFileRelativeToRoot(filePath: filePath, folder: path)
}
static func relativeToRoot(filePath: String, folder path: String) -> String {
containedFileRelativeToRoot(filePath: filePath, folder: path) ?? filePath
}
static func containedFileRelativeToRoot(filePath: String, folder path: String) -> String? {
if path == "" {
return filePath
}
if filePath.hasPrefix("/") || filePath.hasPrefix("http") || filePath.hasPrefix("mailto:") {
return nil
}
return "\(path)/\(filePath)"
}
/**
Convert a set of relative paths to paths that are relative to the root element.
- Parameter input: The set of paths to convert.
- Parameter path: The path to the folder where the paths are currently relative to.
*/
static func rootPaths(for input: Set<String>?, path: String) -> Set<String> {
guard let input = input else {
return []
}
return Set(input.map { relativeToRoot(filePath: $0, folder: path) })
}
func relativePathToFileWithPath(_ filePath: String) -> String {
guard path != "" else {
return filePath
}
guard filePath.hasPrefix(path) else {
return filePath
}
return filePath.replacingOccurrences(of: path + "/", with: "")
}
}
// MARK: Accessing localizations
extension Element {
/**
Get the full path of the thumbnail image for the language (relative to the root folder).
*/
func thumbnailFilePath(for language: String) -> String {
guard let thumbnailFile = Element.findThumbnail(for: language, in: inputFolder) else {
log.add(error: "Missing thumbnail", source: path)
return Element.defaultThumbnailName
}
return pathRelativeToRootForContainedInputFile(thumbnailFile)
}
func fullPageUrl(for language: String) -> String {
localized(for: language).externalUrl ?? localizedPath(for: language)
}
func localized(for language: String) -> LocalizedMetadata {
languages.first { $0.language == language }!
}
func title(for language: String) -> String {
localized(for: language).title
}
/**
Get the back link text for the element.
This text is the one printed for pages of the element, which uses the back text specified by the parent.
*/
func backLinkText(for language: String) -> String {
localized(for: language).parentBackLinkText
}
/**
The optional text to display in a thumbnail corner.
- Note: This text is only displayed for large thumbnails.
*/
func cornerText(for language: String) -> String? {
localized(for: language).cornerText
}
/**
Returns the full path (relative to the site root for a page of the element in the given language.
*/
func localizedPath(for language: String) -> String {
guard path != "" else {
return Element.htmlPageName(for: language)
}
return path + Element.htmlPagePathAddition(for: language)
}
/**
Get the next language to switch to with the language button.
*/
func nextLanguage(for language: String) -> String? {
let langs = languages.map { $0.language }
guard let index = langs.firstIndex(of: language) else {
return nil
}
for i in 1..<langs.count {
let next = langs[(index + i) % langs.count]
guard hasContent(for: next) else {
continue
}
guard next != language else {
return nil
}
return next
}
return nil
}
func linkPreviewImage(for language: String) -> String? {
localized(for: language).linkPreviewImage
}
}
// MARK: Page content
extension Element {
var isExternalPage: Bool {
languages.contains { $0.externalUrl != nil }
}
/**
Get the url of the content markdown file for a language.
To check if the file also exists, use `existingContentUrl(for:)`
*/
private func contentUrl(for language: String) -> URL {
inputFolder.appendingPathComponent("\(language).md")
}
/**
Get the url of existing markdown content for a language.
*/
private func existingContentUrl(for language: String) -> URL? {
let url = contentUrl(for: language)
guard url.exists, let size = url.size, size > 0 else {
return nil
}
return url
}
private func hasContent(for language: String) -> Bool {
if !elements.isEmpty {
return true
}
return existingContentUrl(for: language) != nil
}
}
// MARK: Header and Footer
extension Element {
private var additionalHeadContentPath: String {
path + "/head.html"
}
func customHeadContent() -> String? {
files.contentOfOptionalFile(atPath: additionalHeadContentPath, source: path)
}
private var additionalFooterContentPath: String {
path + "/footer.html"
}
func customFooterContent() -> String? {
files.contentOfOptionalFile(atPath: additionalFooterContentPath, source: path)
}
}
// MARK: Debug
extension Element {
func printTree(indentation: String = "") {
print(indentation + "/" + path)
elements.forEach { $0.printTree(indentation: indentation + " ") }
}
}
// MARK: Images
extension Element {
struct ManualImage {
let sourcePath: String
let destinationPath: String
let desiredWidth: Int
let desiredHeight: Int?
init?(input: String, path: String) {
let parts = input.components(separatedBy: " ").filter { !$0.isEmpty }
guard parts.count == 3 || parts.count == 4 else {
log.add(error: "Invalid image specification, expected 'source dest width (height)", source: path)
return nil
}
guard let width = Int(parts[2]) else {
log.add(error: "Invalid width for image \(parts[0])", source: path)
return nil
}
self.sourcePath = Element.relativeToRoot(filePath: parts[0], folder: path)
self.destinationPath = Element.relativeToRoot(filePath: parts[1], folder: path)
self.desiredWidth = width
guard parts.count == 4 else {
self.desiredHeight = nil
return
}
guard let height = Int(parts[3]) else {
log.add(error: "Invalid height for image \(parts[0])", source: path)
return nil
}
self.desiredHeight = height
}
}
}

View File

@ -0,0 +1,207 @@
import Foundation
extension GenericMetadata {
/**
Metadata localized for a specific language.
*/
struct LocalizedMetadata {
/**
The language for which the content is specified.
- Note: This field is mandatory
*/
let language: String?
/**
- Note: This field is mandatory
The title used in the page header.
*/
let title: String?
/**
The subtitle used in the page header.
*/
let subtitle: String?
/**
The description text used in the page header
*/
let description: String?
/**
The title to use for the link preview.
If `nil` is specified, then the localized element `title` is used.
*/
let linkPreviewTitle: String?
/**
The file name of the link preview image.
- Note: The image must be located in the element folder.
- Note: If `nil` is specified, then the (localized) thumbnail is used.
*/
let linkPreviewImage: String?
/**
The description text for the link preview.
- Note: If `nil` is specified, then first the (localized) element `subtitle` is used.
If this is `nil` too, then the localized `description` of the element is used.
*/
let linkPreviewDescription: String?
/**
The text on the link to show the section page when previewing multiple sections on an overview page.
- Note: If this value is inherited from the parent, if it is not defined. There must be at least one
element in the path that defines this property.
*/
let moreLinkText: String?
/**
The text on the back navigation link of **contained** elements.
This text does not appear on the section page, but on the pages contained within the section.
- Note: If this property is not specified, then the root `backLinkText` is used.
- Note: The root element must specify this property.
*/
let backLinkText: String?
/**
The text to show as a title for placeholder boxes
Placeholders are included in missing pages.
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
*/
let placeholderTitle: String?
/**
The text to show as a description for placeholder boxes
Placeholders are included in missing pages.
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
*/
let placeholderText: String?
/**
An optional suffix to add to the title on a page.
This can be useful to express a different author, project grouping, etc.
*/
let titleSuffix: String?
/**
An optional suffix to add to the thumbnail title of a page.
This can be useful to express a different author, project grouping, etc.
*/
let thumbnailSuffix: String?
/**
A text to place in the top right corner of a large thumbnail.
The text should be a very short string to fit into the corner, like `soon`, or `draft`
- Note: This property is ignored if `thumbnailStyle` is not `large`.
*/
let cornerText: String?
/**
The external url to use instead of automatically generating the page.
This property can be used for links to other parts of the site, like additional services.
It can also be set to manually write a page.
*/
let externalUrl: String?
}
}
extension GenericMetadata.LocalizedMetadata: Codable {
private static var knownKeyList: [CodingKeys] {
[
.language,
.title,
.subtitle,
.description,
.linkPreviewTitle,
.linkPreviewImage,
.linkPreviewDescription,
.moreLinkText,
.backLinkText,
.placeholderTitle,
.placeholderText,
.titleSuffix,
.thumbnailSuffix,
.cornerText,
.externalUrl,
]
}
static var knownKeys: Set<String> {
Set(knownKeyList.map { $0.stringValue })
}
}
extension GenericMetadata.LocalizedMetadata {
/**
The mandatory minimum for a site element.
*/
static var mandatory: GenericMetadata.LocalizedMetadata {
.init(
language: "",
title: "",
subtitle: nil,
description: nil,
linkPreviewTitle: nil,
linkPreviewImage: nil,
linkPreviewDescription: nil,
moreLinkText: nil,
backLinkText: nil,
placeholderTitle: nil,
placeholderText: nil,
titleSuffix: nil,
thumbnailSuffix: nil,
cornerText: nil,
externalUrl: nil)
}
/**
The mandatory minimum for the root element of a site.
*/
static var mandatoryAtRoot: GenericMetadata.LocalizedMetadata {
.init(language: "",
title: "",
subtitle: nil,
description: nil,
linkPreviewTitle: nil,
linkPreviewImage: nil,
linkPreviewDescription: nil,
moreLinkText: nil,
backLinkText: "",
placeholderTitle: "",
placeholderText: "",
titleSuffix: nil,
thumbnailSuffix: nil,
cornerText: nil,
externalUrl: nil)
}
static var full: GenericMetadata.LocalizedMetadata {
.init(language: "",
title: "",
subtitle: "",
description: "",
linkPreviewTitle: "",
linkPreviewImage: "",
linkPreviewDescription: "",
moreLinkText: "",
backLinkText: "",
placeholderTitle: "",
placeholderText: "",
titleSuffix: "",
thumbnailSuffix: "",
cornerText: "",
externalUrl: "")
}
}

View File

@ -0,0 +1,216 @@
import Foundation
/**
The metadata for all site elements.
*/
struct GenericMetadata {
/**
The name of the metadata file contained in the folder of each site element.
*/
static let metadataFileName = "metadata.json"
/**
A custom id to uniquely identify the element on the site.
The id is used for short-hand links to pages, in the form of `![page](page_id)`
for thumbnail previews or `[text](page:page_id)` for simple links.
If no custom id is set, then the name of the element folder is used.
*/
let customId: String?
/**
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 (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: String?
/**
The end date of the element.
This property can be used to specify a date range for a content page.
*/
let endDate: String?
/**
The deployment state of the page.
- Note: This property defaults to ``PageState.standard`
*/
let state: String?
/**
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>?
/**
Additional images required by the element.
These images are specified as: `source_name destination_name width (height)`.
*/
let images: 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: String?
/**
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?
/**
Indicate the header type to be generated automatically.
If this option is set to `none`, then custom header code should be present in the page source files
- Note: If not specified, this property defaults to `left`.
- Note: Overview pages are always using `center`.
*/
let headerType: String?
/**
The localized metadata for each language.
*/
let languages: [LocalizedMetadata]?
}
extension GenericMetadata: Codable {
private static var knownKeyList: [CodingKeys] {
[
.customId,
.author,
.topBarTitle,
.date,
.endDate,
.state,
.sortIndex,
.externalFiles,
.requiredFiles,
.images,
.thumbnailStyle,
.useManualSorting,
.overviewItemCount,
.headerType,
.languages,
]
}
static var knownKeys: Set<String> {
Set(knownKeyList.map { $0.stringValue })
}
}
extension GenericMetadata {
/**
Decode metadata in a folder.
- Parameter data: The binary data of the metadata file.
- Parameter source: The path to the metadata file, relative to the source root
- Note: The decoding routine also checks for unknown properties, and writes them to the output.
- Note: Uses global objects
*/
init?(source: String) {
guard let data = files.dataOfOptionalFile(atPath: source, source: source) else {
return nil
}
let decoder = JSONDecoder()
let knownKeys = GenericMetadata.knownKeys
let knownLocalizedKeys = LocalizedMetadata.knownKeys
decoder.keyDecodingStrategy = .custom { keys in
let key = keys.last!
// Only one key means we are decoding the generic metadata
guard keys.count > 1 else {
if !knownKeys.contains(key.stringValue) {
log.unknown(property: key.stringValue, source: source)
}
return key
}
// Two levels means we're decoding the localized metadata
if !knownLocalizedKeys.contains(key.stringValue) {
log.unknown(property: key.stringValue, source: source)
}
return key
}
do {
self = try decoder.decode(from: data)
} catch {
print("Here \(data)")
log.failedToOpen(GenericMetadata.metadataFileName, requiredBy: source, error: error)
return nil
}
}
}
extension GenericMetadata {
static var full: GenericMetadata {
.init(
customId: "",
author: "",
topBarTitle: "",
date: "",
endDate: "",
state: "",
sortIndex: 1,
externalFiles: [],
requiredFiles: [],
images: [],
thumbnailStyle: "",
useManualSorting: false,
overviewItemCount: 6,
headerType: "left",
languages: [.full])
}
}

View File

@ -0,0 +1,19 @@
import Foundation
enum HeaderType: String {
/**
The standard page header, left-aligned
*/
case left
/**
The standard overview header, centered
*/
case center
/**
The element provides it's own header, so don't generate any.
*/
case none
}

View File

@ -0,0 +1,41 @@
import Foundation
enum PageState: String {
/**
Generate the page, and show it in overviews of the parent.
*/
case standard
/**
Generate the page, but don't provide links in overviews.
*/
case draft
/**
Generate the page, but don't include it in overviews of the parent.
*/
case hidden
}
extension PageState {
var isShownInOverview: Bool {
switch self {
case .standard, .draft:
return true
case .hidden:
return false
}
}
var hasThumbnailLink: Bool {
switch self {
case .standard:
return true
case .draft:
return false
case .hidden:
return false
}
}
}

View File

@ -0,0 +1,35 @@
import Foundation
enum ThumbnailStyle: String, CaseIterable {
case large
case square
case small
var height: Int {
switch self {
case .large:
return 210
case .square:
return 178
case .small:
return 78
}
}
var width: Int {
switch self {
case .large:
return 374
case .square:
return height
case .small:
return height
}
}
}
extension ThumbnailStyle: Codable {
}