Update generation
- Move to global objects for files and validation - Only write changed files - Check images for changes before scaling - Simplify code
This commit is contained in:
229
WebsiteGenerator/Content/Element+LocalizedMetadata.swift
Normal file
229
WebsiteGenerator/Content/Element+LocalizedMetadata.swift
Normal 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.subtitle ?? data.description
|
||||
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.subtitle ?? data.description
|
||||
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 ?? parent.backLinkText
|
||||
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
|
||||
}
|
||||
}
|
429
WebsiteGenerator/Content/Element.swift
Normal file
429
WebsiteGenerator/Content/Element.swift
Normal file
@ -0,0 +1,429 @@
|
||||
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
|
||||
|
||||
/**
|
||||
Indicate that no header should be generated automatically.
|
||||
|
||||
This option assumes that custom header code is present in the page source files
|
||||
- Note: If not specified, this property defaults to `false`.
|
||||
*/
|
||||
let useCustomHeader: Bool
|
||||
|
||||
/**
|
||||
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) throws {
|
||||
self.inputFolder = folder
|
||||
self.path = ""
|
||||
|
||||
let source = GenericMetadata.metadataFileName
|
||||
guard let metadata = GenericMetadata(source: source) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.author = log.required(metadata.author, name: "author", source: source) ?? "author"
|
||||
self.topBarTitle = log
|
||||
.required(metadata.topBarTitle, name: "topBarTitle", source: source) ?? "My Website"
|
||||
self.deployedBaseUrl = log
|
||||
.required(metadata.deployedBaseUrl, name: "deployedBaseUrl", source: source) ?? "https://example.com"
|
||||
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.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.useCustomHeader = metadata.useCustomHeader ?? false
|
||||
self.languages = log.required(metadata.languages, name: "languages", source: source)?
|
||||
.compactMap { language in
|
||||
.init(atRoot: folder, data: language)
|
||||
} ?? []
|
||||
try self.readElements(in: folder, source: nil)
|
||||
}
|
||||
|
||||
mutating func readElements(in folder: URL, source: String?) throws {
|
||||
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 = try subFolders.compactMap { subFolder in
|
||||
let s = (source.unwrapped { $0 + "/" } ?? "") + subFolder.lastPathComponent
|
||||
return try Element(parent: self, folder: subFolder, path: s)
|
||||
}
|
||||
}
|
||||
|
||||
init?(parent: Element, folder: URL, path: String) throws {
|
||||
self.inputFolder = folder
|
||||
self.path = path
|
||||
|
||||
let source = path + "/" + GenericMetadata.metadataFileName
|
||||
guard let metadata = GenericMetadata(source: source) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.author = metadata.author ?? parent.author
|
||||
self.topBarTitle = log
|
||||
.unused(metadata.topBarTitle, "topBarTitle", source: source) ?? parent.topBarTitle
|
||||
self.deployedBaseUrl = log
|
||||
.unused(metadata.deployedBaseUrl, "deployedBaseUrl", source: source) ?? parent.deployedBaseUrl
|
||||
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)
|
||||
}
|
||||
}
|
||||
self.state = log.state(metadata.state, source: source)
|
||||
self.sortIndex = metadata.sortIndex.ifNil {
|
||||
if 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 = metadata.externalFiles ?? []
|
||||
self.requiredFiles = Set((metadata.requiredFiles ?? []).map { path + "/" + $0 })
|
||||
self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source)
|
||||
self.useManualSorting = metadata.useManualSorting ?? false
|
||||
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount
|
||||
self.useCustomHeader = metadata.useCustomHeader ?? false
|
||||
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)
|
||||
}
|
||||
try self.readElements(in: folder, source: path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
extension Element {
|
||||
|
||||
var containsElements: Bool {
|
||||
!elements.isEmpty
|
||||
}
|
||||
|
||||
var hasNestingElements: Bool {
|
||||
elements.contains { $0.containsElements }
|
||||
}
|
||||
|
||||
var sortedItems: [Element] {
|
||||
if useManualSorting {
|
||||
return elements.sorted { $0.sortIndex! < $1.sortIndex! }
|
||||
}
|
||||
return elements.sorted { $0.date! > $1.date! }
|
||||
}
|
||||
|
||||
/**
|
||||
The url of the top-level section of the element.
|
||||
*/
|
||||
func sectionUrl(for language: String) -> String {
|
||||
path.components(separatedBy: "/").first! + "/\(language).html"
|
||||
}
|
||||
|
||||
/**
|
||||
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 {
|
||||
guard !filePath.hasSuffix("/") && !filePath.hasSuffix("http") else {
|
||||
return filePath
|
||||
}
|
||||
return "\(path)/\(filePath)"
|
||||
}
|
||||
|
||||
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 {
|
||||
fatalError()
|
||||
}
|
||||
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 {
|
||||
path != "" ? "\(path)/\(language).html" : "\(language).html"
|
||||
}
|
||||
|
||||
/**
|
||||
Get the next language to switch to with the language button.
|
||||
*/
|
||||
func nextLanguage(for languageIdentifier: String) -> String? {
|
||||
let langs = languages.map { $0.language }
|
||||
guard let index = langs.firstIndex(of: languageIdentifier) else {
|
||||
return nil
|
||||
}
|
||||
for i in 1..<langs.count {
|
||||
let next = langs[(index + i) % langs.count]
|
||||
guard hasContent(for: next) else {
|
||||
continue
|
||||
}
|
||||
guard next != languageIdentifier 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 else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private func hasContent(for language: String) -> Bool {
|
||||
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 + " ") }
|
||||
}
|
||||
}
|
207
WebsiteGenerator/Content/GenericMetadata+Localized.swift
Normal file
207
WebsiteGenerator/Content/GenericMetadata+Localized.swift
Normal 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: "")
|
||||
}
|
||||
}
|
185
WebsiteGenerator/Content/GenericMetadata.swift
Normal file
185
WebsiteGenerator/Content/GenericMetadata.swift
Normal file
@ -0,0 +1,185 @@
|
||||
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"
|
||||
|
||||
/**
|
||||
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: 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>?
|
||||
|
||||
/**
|
||||
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 that no header should be generated automatically.
|
||||
|
||||
This option assumes that custom header code is present in the page source files
|
||||
- Note: If not specified, this property defaults to `false`.
|
||||
*/
|
||||
let useCustomHeader: Bool?
|
||||
|
||||
/**
|
||||
The localized metadata for each language.
|
||||
*/
|
||||
let languages: [LocalizedMetadata]?
|
||||
}
|
||||
|
||||
extension GenericMetadata: Codable {
|
||||
|
||||
private static var knownKeyList: [CodingKeys] {
|
||||
[
|
||||
.author,
|
||||
.topBarTitle,
|
||||
.deployedBaseUrl,
|
||||
.date,
|
||||
.endDate,
|
||||
.state,
|
||||
.sortIndex,
|
||||
.externalFiles,
|
||||
.requiredFiles,
|
||||
.thumbnailStyle,
|
||||
.useManualSorting,
|
||||
.overviewItemCount,
|
||||
.useCustomHeader,
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
protocol LanguageIdentifiable {
|
||||
|
||||
var languageIdentifier: String { get }
|
||||
|
||||
var title: String { get }
|
||||
}
|
||||
|
||||
protocol LanguageContainer {
|
||||
|
||||
associatedtype LocalizedContainer: LanguageIdentifiable
|
||||
|
||||
var languages: [LocalizedContainer] { get }
|
||||
|
||||
}
|
||||
|
||||
protocol LocalizedMetadataContainer {
|
||||
|
||||
associatedtype MetadataType: LanguageContainer
|
||||
|
||||
var metadata: MetadataType { get }
|
||||
|
||||
func hasContent(for language: String) -> Bool
|
||||
}
|
||||
|
||||
// MARK: Default implementations
|
||||
|
||||
extension LocalizedMetadataContainer {
|
||||
|
||||
func hasContent(for language: String) -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Extensions
|
||||
|
||||
extension LocalizedMetadataContainer {
|
||||
|
||||
func localized(for language: String) -> MetadataType.LocalizedContainer {
|
||||
metadata.localized(for: language)
|
||||
}
|
||||
|
||||
/**
|
||||
The localized title of the element.
|
||||
|
||||
This title is used as large text in overview pages, or as the `<h1>` title on pages. If no separate link preview title is specified using a localized `linkPreview.title`, then this value is also used for link previews.
|
||||
*/
|
||||
func title(for language: String) -> String {
|
||||
localized(for: language).title
|
||||
}
|
||||
|
||||
|
||||
func nextLanguage(for languageIdentifier: String) -> String? {
|
||||
let langs = metadata.languages.map { $0.languageIdentifier }
|
||||
guard let index = langs.firstIndex(of: languageIdentifier) else {
|
||||
return nil
|
||||
}
|
||||
for i in 1..<langs.count {
|
||||
let next = langs[(index + i) % langs.count]
|
||||
guard hasContent(for: next) else {
|
||||
continue
|
||||
}
|
||||
guard next != languageIdentifier else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension LanguageContainer {
|
||||
|
||||
var languageIdentifiers: [String] {
|
||||
languages.map { $0.languageIdentifier }
|
||||
}
|
||||
|
||||
#warning("Throw better error for missing language")
|
||||
func localized(for language: String) -> LocalizedContainer {
|
||||
languages.first { $0.languageIdentifier == language }!
|
||||
}
|
||||
|
||||
/**
|
||||
The localized title of the element.
|
||||
|
||||
This title is used as large text in overview pages, or as the `<h1>` title on pages. If no separate link preview title is specified using a localized `linkPreview.title`, then this value is also used for link previews.
|
||||
*/
|
||||
func title(for language: String) -> String {
|
||||
localized(for: language).title
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedMetadataContainer where Self: SiteElement, Self.MetadataType.LocalizedContainer: LinkPreviewMetadataProvider {
|
||||
|
||||
private func linkPreviewImageFileName(for language: String) -> String? {
|
||||
if let fileName = localized(for: language).linkPreview?.image {
|
||||
return fileName
|
||||
}
|
||||
// Check for the existence of a localized thumbnail
|
||||
let fileName = Self.thumbnailFileNameLocalized(for: language)
|
||||
if inputFolder.appendingPathComponent(fileName).exists {
|
||||
return fileName
|
||||
}
|
||||
let defaultThumbnail = Self.defaultThumbnailFileName
|
||||
if inputFolder.appendingPathComponent(defaultThumbnail).exists {
|
||||
return defaultThumbnail
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func linkPreviewImage(for language: String) -> String? {
|
||||
guard let fileName = linkPreviewImageFileName(for: language) else {
|
||||
return nil
|
||||
}
|
||||
return "/\(path)/\(fileName)"
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Localized configuration data for link previews of site elements.
|
||||
|
||||
This struct is embedded in localized metadata and intended to be filled in the JSON source.
|
||||
*/
|
||||
struct LinkPreviewMetadata {
|
||||
|
||||
/**
|
||||
The title to use for the link preview.
|
||||
|
||||
If `nil` is specified, then the localized element `title` is used.
|
||||
*/
|
||||
let title: 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 image: 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 description: String?
|
||||
}
|
||||
|
||||
extension LinkPreviewMetadata: Codable { }
|
||||
|
||||
extension LinkPreviewMetadata {
|
||||
|
||||
static var initial: LinkPreviewMetadata {
|
||||
.init(title: nil,
|
||||
image: nil,
|
||||
description: "The page description for link previews")
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
protocol LinkPreviewMetadataProvider {
|
||||
|
||||
var linkPreview: LinkPreviewMetadata? { get }
|
||||
|
||||
var title: String { get }
|
||||
|
||||
var subtitle: String? { get }
|
||||
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension LinkPreviewMetadataProvider {
|
||||
|
||||
var linkPreviewTitle: String {
|
||||
linkPreview?.title ?? title
|
||||
}
|
||||
|
||||
var linkPreviewDescription: String {
|
||||
linkPreview?.description ?? subtitle ?? description
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
protocol Metadata: Codable {
|
||||
|
||||
static var fileName: String { get }
|
||||
|
||||
static var initial: Self { get }
|
||||
}
|
||||
|
||||
extension Metadata {
|
||||
|
||||
static func url(in folder: URL) -> URL {
|
||||
folder.appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
static func exists(in folder: URL) -> Bool {
|
||||
url(in: folder).exists
|
||||
}
|
||||
|
||||
init?(in folder: URL) throws {
|
||||
let metadataUrl = Self.url(in: folder)
|
||||
guard metadataUrl.exists else {
|
||||
try Self.initial.writeJSON(to: metadataUrl)
|
||||
print("Created metadata in \(folder)")
|
||||
return nil
|
||||
}
|
||||
try self.init(decodeFrom: metadataUrl)
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Page {
|
||||
|
||||
struct LocalizedMetadata {
|
||||
|
||||
let id: String
|
||||
|
||||
let title: String
|
||||
|
||||
#warning("Generate title suffix")
|
||||
let titleSuffix: String?
|
||||
|
||||
let linkPreview: LinkPreviewMetadata?
|
||||
|
||||
let subtitle: String?
|
||||
|
||||
#warning("Generate thumbnail suffix")
|
||||
let thumbnailSuffix: String?
|
||||
|
||||
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 Page.LocalizedMetadata: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension Page.LocalizedMetadata: LanguageIdentifiable {
|
||||
|
||||
var languageIdentifier: String {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
extension Page.LocalizedMetadata {
|
||||
|
||||
static var initial: Page.LocalizedMetadata {
|
||||
.init(id: "en",
|
||||
title: "Page title",
|
||||
titleSuffix: nil,
|
||||
linkPreview: .initial,
|
||||
subtitle: "Some text below the title",
|
||||
thumbnailSuffix: "Project",
|
||||
cornerText: nil,
|
||||
externalUrl: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page.LocalizedMetadata: LinkPreviewMetadataProvider {
|
||||
|
||||
var description: String { subtitle ?? title }
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Page {
|
||||
|
||||
struct Metadata {
|
||||
|
||||
let date: Date
|
||||
|
||||
let endDate: Date?
|
||||
|
||||
let author: String?
|
||||
|
||||
let isDraft: Bool
|
||||
|
||||
let sortIndex: Int?
|
||||
|
||||
let languages: [LocalizedMetadata]
|
||||
|
||||
#warning("Add hideFromOverview property")
|
||||
|
||||
let requiredFiles: [String]
|
||||
|
||||
/**
|
||||
Indicate that no header should be generated automatically.
|
||||
|
||||
This option assumes that custom header code is present in the page source files
|
||||
- Note: If not specified, this property defaults to `false`.
|
||||
*/
|
||||
let useCustomHeader: Bool
|
||||
|
||||
#warning("Add files for which errors are ignored when missing")
|
||||
}
|
||||
}
|
||||
|
||||
extension Page.Metadata: Metadata {
|
||||
|
||||
static let fileName = "page.json"
|
||||
|
||||
static var initial: Page.Metadata {
|
||||
.init(
|
||||
date: .now,
|
||||
endDate: .now,
|
||||
author: nil,
|
||||
isDraft: true,
|
||||
sortIndex: 0,
|
||||
languages: [.initial],
|
||||
requiredFiles: [],
|
||||
useCustomHeader: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page.Metadata: LanguageContainer {
|
||||
|
||||
}
|
||||
|
||||
extension Page.Metadata: Codable {
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case date
|
||||
case endDate
|
||||
case author
|
||||
case isDraft
|
||||
case sortIndex
|
||||
case languages
|
||||
case requiredFiles
|
||||
case useCustomHeader
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
let dateString = Page.metadataDateFormatter.string(from: date)
|
||||
try container.encode(dateString, forKey: .date)
|
||||
if let date = endDate {
|
||||
let endDateString = Page.metadataDateFormatter.string(from: date)
|
||||
try container.encode(endDateString, forKey: .endDate)
|
||||
}
|
||||
try container.encodeIfPresent(author, forKey: .author)
|
||||
try container.encode(isDraft, forKey: .isDraft)
|
||||
try container.encodeIfPresent(sortIndex, forKey: .sortIndex)
|
||||
try container.encode(languages, forKey: .languages)
|
||||
if !requiredFiles.isEmpty {
|
||||
try container.encode(requiredFiles, forKey: .requiredFiles)
|
||||
}
|
||||
if useCustomHeader {
|
||||
try container.encode(true, forKey: .useCustomHeader)
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let dateString = try container.decode(String.self, forKey: .date)
|
||||
self.date = try Page.metadataDateFormatter.date(from: dateString)
|
||||
.unwrap(or: .invalidDateInPageMetadata(dateString))
|
||||
self.author = try container.decodeIfPresent(String.self, forKey: .author)
|
||||
self.languages = try container.decode([Page.LocalizedMetadata].self, forKey: .languages)
|
||||
self.isDraft = try container.decodeIfPresent(Bool.self, forKey: .isDraft) ?? false
|
||||
self.sortIndex = try container.decodeIfPresent(Int.self, forKey: .sortIndex)
|
||||
if let endDateString = try container.decodeIfPresent(String.self, forKey: .endDate) {
|
||||
self.endDate = try Page.metadataDateFormatter.date(from: endDateString)
|
||||
.unwrap(or: .invalidDateInPageMetadata(endDateString))
|
||||
} else {
|
||||
self.endDate = nil
|
||||
}
|
||||
self.requiredFiles = try container.decodeIfPresent([String].self, forKey: .requiredFiles) ?? []
|
||||
self.useCustomHeader = try container.decodeIfPresent(Bool.self, forKey: .useCustomHeader) ?? false
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct Page {
|
||||
|
||||
let metadata: Metadata
|
||||
|
||||
/// The input folder where the page data is stored
|
||||
let inputFolder: URL
|
||||
|
||||
let path: String
|
||||
|
||||
init?(folder: URL, path: String) throws {
|
||||
self.path = path
|
||||
guard let metadata = try Metadata(in: folder) else {
|
||||
return nil
|
||||
}
|
||||
self.inputFolder = folder
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
extension Page {
|
||||
|
||||
static let metadataDateFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "dd.MM.yy"
|
||||
return df
|
||||
}()
|
||||
}
|
||||
|
||||
extension Page: SiteElement {
|
||||
|
||||
var sortIndex: Int? {
|
||||
metadata.sortIndex
|
||||
}
|
||||
|
||||
var sortDate: Date? {
|
||||
metadata.date
|
||||
}
|
||||
|
||||
var elements: [SiteElement] { [] }
|
||||
|
||||
func cornerText(for language: String) -> String? {
|
||||
localized(for: language).cornerText
|
||||
}
|
||||
|
||||
var isExternalPage: Bool {
|
||||
metadata.languages.contains { $0.externalUrl != nil }
|
||||
}
|
||||
|
||||
func fullPageUrl(for language: String) -> String {
|
||||
localized(for: language).externalUrl ?? "\(path)/\(language).html"
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: LocalizedMetadataContainer {
|
||||
|
||||
/**
|
||||
Get the url of the content markdown file for a language.
|
||||
|
||||
To check if the file also exists, use `existingContentUrl(for:)`
|
||||
*/
|
||||
func contentUrl(for language: String) -> URL {
|
||||
inputFolder.appendingPathComponent("\(language).md")
|
||||
}
|
||||
|
||||
/**
|
||||
Get the url of existing markdown content for a language.
|
||||
*/
|
||||
func existingContentUrl(for language: String) -> URL? {
|
||||
let url = contentUrl(for: language)
|
||||
guard url.exists else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func hasContent(for language: String) -> Bool {
|
||||
existingContentUrl(for: language) != nil
|
||||
}
|
||||
|
||||
|
||||
}
|
18
WebsiteGenerator/Content/PageState.swift
Normal file
18
WebsiteGenerator/Content/PageState.swift
Normal file
@ -0,0 +1,18 @@
|
||||
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 hide
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Section {
|
||||
|
||||
struct LocalizedMetadata {
|
||||
|
||||
let id: String
|
||||
|
||||
let title: String
|
||||
|
||||
let subtitle: String?
|
||||
|
||||
let description: String
|
||||
|
||||
/**
|
||||
The text on the link to show the section page when previewing multiple sections on an overview page.
|
||||
*/
|
||||
let moreLinkTitle: String
|
||||
|
||||
/**
|
||||
An optional text to display in the corner of the section thumbnail.
|
||||
|
||||
Can be used to show things like "new", "draft", etc.
|
||||
*/
|
||||
let cornerText: String?
|
||||
|
||||
let linkPreview: LinkPreviewMetadata?
|
||||
|
||||
/**
|
||||
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?
|
||||
}
|
||||
|
||||
}
|
||||
extension Section.LocalizedMetadata: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension Section.LocalizedMetadata: LanguageIdentifiable {
|
||||
|
||||
var languageIdentifier: String {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
extension Section.LocalizedMetadata {
|
||||
|
||||
static var initial: Section.LocalizedMetadata {
|
||||
.init(id: "en",
|
||||
title: "Section title",
|
||||
subtitle: "Tag line below the title",
|
||||
description: "The short text below the tagline on the section overview page",
|
||||
moreLinkTitle: "More section items",
|
||||
cornerText: nil,
|
||||
linkPreview: .initial,
|
||||
backLinkText: "Back to section")
|
||||
}
|
||||
}
|
||||
|
||||
extension Section.LocalizedMetadata: LinkPreviewMetadataProvider {
|
||||
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Section {
|
||||
|
||||
static let defaultSectionOverviewItemCount = 6
|
||||
|
||||
struct Metadata {
|
||||
|
||||
let thumbnailStyle: ThumbnailStyle
|
||||
|
||||
let sortByMostRecent: Bool
|
||||
|
||||
let sortIndex: Int?
|
||||
|
||||
let date: Date?
|
||||
|
||||
let languages: [LocalizedMetadata]
|
||||
|
||||
let sectionOverviewItemCount: Int
|
||||
}
|
||||
}
|
||||
|
||||
extension Section.Metadata: Metadata {
|
||||
|
||||
static let fileName = "section.json"
|
||||
|
||||
static var initial: Section.Metadata {
|
||||
.init(thumbnailStyle: .large,
|
||||
sortByMostRecent: true,
|
||||
sortIndex: nil,
|
||||
date: nil,
|
||||
languages: [.initial],
|
||||
sectionOverviewItemCount: 6)
|
||||
}
|
||||
}
|
||||
|
||||
extension Section.Metadata: LanguageContainer {
|
||||
|
||||
}
|
||||
|
||||
extension Section.Metadata: Codable {
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case thumbnailStyle
|
||||
case sortByMostRecent
|
||||
case sortIndex
|
||||
case date
|
||||
case languages
|
||||
case sectionOverviewItemCount
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(thumbnailStyle, forKey: .thumbnailStyle)
|
||||
try container.encode(sortByMostRecent, forKey: .sortByMostRecent)
|
||||
try container.encodeIfPresent(sortIndex, forKey: .sortIndex)
|
||||
try container.encode(languages, forKey: .languages)
|
||||
if let date = date {
|
||||
let dateString = Page.metadataDateFormatter.string(from: date)
|
||||
try container.encode(dateString, forKey: .date)
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.thumbnailStyle = try container.decode(ThumbnailStyle.self, forKey: .thumbnailStyle)
|
||||
self.sortByMostRecent = try container.decode(Bool.self, forKey: .sortByMostRecent)
|
||||
self.sortIndex = try container.decodeIfPresent(Int.self, forKey: .sortIndex)
|
||||
self.languages = try container.decode([Section.LocalizedMetadata].self, forKey: .languages)
|
||||
if let dateString = try container.decodeIfPresent(String.self, forKey: .date) {
|
||||
self.date = try Page.metadataDateFormatter.date(from: dateString)
|
||||
.unwrap(or: .invalidDateInPageMetadata(dateString))
|
||||
} else {
|
||||
self.date = nil
|
||||
}
|
||||
self.sectionOverviewItemCount = try container
|
||||
.decodeIfPresent(Int.self, forKey: .sectionOverviewItemCount) ?? Section.defaultSectionOverviewItemCount
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct Section {
|
||||
|
||||
let metadata: Metadata
|
||||
|
||||
let inputFolder: URL
|
||||
|
||||
let elements: [SiteElement]
|
||||
|
||||
/// The path to get to the section from the root folder (no leading slash)
|
||||
let path: String
|
||||
|
||||
var folderName: String {
|
||||
inputFolder.lastPathComponent
|
||||
}
|
||||
|
||||
var sortedItems: [SiteElement] {
|
||||
guard metadata.sortByMostRecent else {
|
||||
return elements.sorted { $0.sortIndex! < $1.sortIndex! }
|
||||
}
|
||||
return elements.sorted { $0.sortDate! > $1.sortDate! }
|
||||
}
|
||||
|
||||
init?(folder: URL, path: String) throws {
|
||||
self.path = path
|
||||
guard let metadata = try Metadata(in: folder) else {
|
||||
return nil
|
||||
}
|
||||
self.metadata = metadata
|
||||
self.inputFolder = folder
|
||||
let elements: [SiteElement] = try FileSystem.folders(in: folder)
|
||||
.compactMap {
|
||||
let sectionPath = "\(path)/\($0.lastPathComponent)"
|
||||
if Page.Metadata.exists(in: $0) {
|
||||
return try Page(folder: $0, path: sectionPath)
|
||||
}
|
||||
if Section.Metadata.exists(in: $0) {
|
||||
return try Section(folder: $0, path: sectionPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if metadata.sortByMostRecent {
|
||||
self.elements = elements.sorted { $0.sortDate! > $1.sortDate! }
|
||||
} else {
|
||||
self.elements = elements.sorted { $0.sortIndex! < $1.sortIndex! }
|
||||
}
|
||||
#warning("Verify that all sort indices or sort dates are present")
|
||||
print("Section \(folderName): \(elements.count) pages")
|
||||
}
|
||||
}
|
||||
|
||||
extension Section: SiteElement {
|
||||
|
||||
var sortIndex: Int? {
|
||||
metadata.sortIndex
|
||||
}
|
||||
|
||||
var sortDate: Date? {
|
||||
metadata.date
|
||||
}
|
||||
|
||||
func cornerText(for language: String) -> String? {
|
||||
localized(for: language).cornerText
|
||||
}
|
||||
|
||||
func backLinkText(for language: String) -> String? {
|
||||
localized(for: language).backLinkText
|
||||
}
|
||||
}
|
||||
|
||||
extension Section: LocalizedMetadataContainer {
|
||||
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Site {
|
||||
|
||||
struct LocalizedMetadata {
|
||||
|
||||
let languageIdentifier: String
|
||||
|
||||
let linkPreview: LinkPreviewMetadata?
|
||||
|
||||
let title: String
|
||||
|
||||
let subtitle: String?
|
||||
|
||||
let description: 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 back text to use for element which don't specify a `backLinkText` themselves.
|
||||
*/
|
||||
let defaultBackLinkText: String
|
||||
|
||||
/**
|
||||
The text to show as a title for placeholder boxes
|
||||
|
||||
Placeholders are included in missing pages.
|
||||
*/
|
||||
let placeholderTitle: String
|
||||
|
||||
/**
|
||||
The text to show as a description for placeholder boxes
|
||||
|
||||
Placeholders are included in missing pages.
|
||||
*/
|
||||
let placeholderText: String
|
||||
}
|
||||
}
|
||||
|
||||
extension Site.LocalizedMetadata: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension Site.LocalizedMetadata: LanguageIdentifiable {
|
||||
|
||||
}
|
||||
|
||||
extension Site.LocalizedMetadata {
|
||||
|
||||
static var initial: Site.LocalizedMetadata {
|
||||
.init(
|
||||
languageIdentifier: "en",
|
||||
linkPreview: .initial,
|
||||
title: "Website name on front page",
|
||||
subtitle: "Tag line on front page",
|
||||
description: "Some text below the tag line on the title page",
|
||||
backLinkText: "Back to start",
|
||||
defaultBackLinkText: "Back",
|
||||
placeholderTitle: "Content missing",
|
||||
placeholderText: "This page is incomplete. Content will be added in the coming days.")
|
||||
}
|
||||
}
|
||||
|
||||
extension Site.LocalizedMetadata: LinkPreviewMetadataProvider {
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Site {
|
||||
|
||||
struct Metadata {
|
||||
|
||||
let author: String
|
||||
|
||||
let ignoredSubFolders: Set<String>
|
||||
|
||||
let topBarTitle: String?
|
||||
|
||||
/**
|
||||
The url where the site will be deployed.
|
||||
|
||||
This value is required to build absolute links for link previews.
|
||||
- Note: The path does not need to contain a trailing slash.
|
||||
*/
|
||||
let deployedBaseUrl: String
|
||||
|
||||
let languages: [LocalizedMetadata]
|
||||
|
||||
static func write(to url: URL) throws {
|
||||
try Metadata.initial.writeJSON(to: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Site.Metadata: LanguageContainer {
|
||||
|
||||
}
|
||||
|
||||
extension Site.Metadata: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension Site.Metadata: Metadata {
|
||||
|
||||
static let fileName = "site.json"
|
||||
|
||||
static var initial: Self {
|
||||
.init(author: "Author",
|
||||
ignoredSubFolders: ["templates"],
|
||||
topBarTitle: "<b>Title</b>",
|
||||
deployedBaseUrl: "http://example.com",
|
||||
languages: [.initial])
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct Site {
|
||||
|
||||
static let linkPreviewDesiredImageWidth = 1600
|
||||
|
||||
let elements: [SiteElement]
|
||||
|
||||
let metadata: Metadata
|
||||
|
||||
let inputFolder: URL
|
||||
|
||||
init?(folder: URL) throws {
|
||||
self.inputFolder = folder
|
||||
|
||||
guard let metadata = try Metadata(in: folder) else {
|
||||
return nil
|
||||
}
|
||||
guard !metadata.languages.isEmpty else {
|
||||
throw GenerationError.invalidLanguageSpecification("No languages specified in site.json")
|
||||
}
|
||||
self.metadata = metadata
|
||||
self.elements = try FileSystem.folders(in: folder)
|
||||
.filter { !metadata.ignoredSubFolders.contains($0.lastPathComponent) }
|
||||
.compactMap { sectionUrl in
|
||||
return try Section(
|
||||
folder: sectionUrl, path: sectionUrl.lastPathComponent)
|
||||
}
|
||||
print("Loaded site with \(elements.count) sections and \(metadata.languages.count) languages")
|
||||
|
||||
// Create example metadata
|
||||
//_ = try? Page.Metadata(in: folder)
|
||||
//_ = try? Section.Metadata(in: folder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Site: LocalizedMetadataContainer {
|
||||
|
||||
}
|
||||
|
||||
extension Site: SiteElement {
|
||||
|
||||
var sortIndex: Int? { 0 }
|
||||
|
||||
var sortDate: Date? { nil }
|
||||
|
||||
var path: String { "" }
|
||||
|
||||
func cornerText(for language: String) -> String? { nil }
|
||||
|
||||
func backLinkText(for language: String) throws -> String? {
|
||||
localized(for: language).backLinkText
|
||||
}
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
protocol SiteElement {
|
||||
|
||||
/**
|
||||
The sort index for the element when manual sorting is specified for the parent.
|
||||
- Note: Elements are sorted in ascending order.
|
||||
*/
|
||||
var sortIndex: Int? { get }
|
||||
|
||||
/**
|
||||
The date used for sorting of the element, if automatic sorting is specified by the parent.
|
||||
- Note: Elements are sorted by newest first.
|
||||
*/
|
||||
var sortDate: Date? { get }
|
||||
|
||||
/**
|
||||
The path to the element's folder in the source hierarchy (without a leading slash).
|
||||
*/
|
||||
var path: String { get }
|
||||
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
var inputFolder: URL { get }
|
||||
|
||||
/**
|
||||
The localized title of the element.
|
||||
|
||||
This title is used as large text in overview pages, or as the `<h1>` title on pages. If no separate link preview title is specified using a localized `linkPreview.title`, then this value is also used for link previews.
|
||||
*/
|
||||
func title(for language: String) -> String
|
||||
|
||||
/**
|
||||
The optional text to display in a thumbnail corner.
|
||||
- Note: This text is only displayed for large thumbnails.
|
||||
*/
|
||||
func cornerText(for language: String) -> String?
|
||||
|
||||
/**
|
||||
The url to the element in the given language.
|
||||
|
||||
If the `externalUrl` property is not set for the page metadata in the given language, then the standard path is returned.
|
||||
- If this value starts with a slash, it is considered an absolute url to the same domain
|
||||
- If the value starts with `http://` or `https://` it is considered an external url
|
||||
- Otherwise the value is treated as a path from the root of the site.
|
||||
*/
|
||||
func fullPageUrl(for language: String) -> String
|
||||
|
||||
/**
|
||||
All elements contained within the element.
|
||||
|
||||
If the element is a section, then this property contains the pages within.
|
||||
*/
|
||||
var elements: [SiteElement] { get }
|
||||
|
||||
func backLinkText(for language: String) throws -> String?
|
||||
|
||||
}
|
||||
|
||||
extension SiteElement {
|
||||
|
||||
func fullPageUrl(for language: String) -> String {
|
||||
localizedPath(for: language)
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteElement {
|
||||
|
||||
/**
|
||||
The id of the section to which this element contains.
|
||||
|
||||
This property is used to highlight the active section in the top bar.
|
||||
|
||||
The section id is the folder name of the top-level section
|
||||
*/
|
||||
var sectionId: String {
|
||||
path.components(separatedBy: "/").first!
|
||||
}
|
||||
|
||||
static var defaultThumbnailFileName: String { "thumbnail.jpg" }
|
||||
|
||||
static func thumbnailFileNameLocalized(for language: String) -> String {
|
||||
defaultThumbnailFileName.insert("-\(language)", beforeLast: ".")
|
||||
}
|
||||
|
||||
var containedFolder: String {
|
||||
inputFolder.lastPathComponent
|
||||
}
|
||||
|
||||
var containsElements: Bool {
|
||||
!elements.isEmpty
|
||||
}
|
||||
|
||||
var hasNestingElements: Bool {
|
||||
elements.contains { $0.containsElements }
|
||||
}
|
||||
|
||||
/**
|
||||
Get the full path of the thumbnail image for the language (relative to the root folder).
|
||||
*/
|
||||
func thumbnailFilePath(for language: String) -> String {
|
||||
let specificImageName = Self.thumbnailFileNameLocalized(for: language)
|
||||
let specificImageUrl = inputFolder.appendingPathComponent(specificImageName)
|
||||
guard specificImageUrl.exists else {
|
||||
return "\(path)/\(Self.defaultThumbnailFileName)"
|
||||
}
|
||||
return "\(path)/\(specificImageName)"
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the thumbnail image for the element.
|
||||
|
||||
If a localized thumbnail exists, then this image name is returned.
|
||||
*/
|
||||
func thumbnailName(for language: String) -> String {
|
||||
let specificImageName = "thumbnail-\(language).jpg"
|
||||
let specificImageUrl = inputFolder.appendingPathComponent(specificImageName)
|
||||
guard specificImageUrl.exists else {
|
||||
return "\(inputFolder.lastPathComponent)/thumbnail.jpg"
|
||||
}
|
||||
return "\(inputFolder.lastPathComponent)/\(specificImageName)"
|
||||
}
|
||||
/**
|
||||
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 {
|
||||
guard !filePath.hasSuffix("/") && !filePath.hasSuffix("http") else {
|
||||
return filePath
|
||||
}
|
||||
return "\(path)/\(filePath)"
|
||||
}
|
||||
|
||||
func backLinkText(for language: String) throws -> String? { nil }
|
||||
|
||||
/**
|
||||
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 {
|
||||
path != "" ? "\(path)/\(language).html" : "\(language).html"
|
||||
}
|
||||
|
||||
func relativePathToFileWithPath(_ filePath: String) -> String {
|
||||
guard path != "" else {
|
||||
return filePath
|
||||
}
|
||||
guard filePath.hasPrefix(path) else {
|
||||
return filePath
|
||||
}
|
||||
return filePath.replacingOccurrences(of: path + "/", with: "")
|
||||
}
|
||||
|
||||
private var additionalHeadContentUrl: URL {
|
||||
inputFolder.appendingPathComponent("head.html")
|
||||
}
|
||||
|
||||
var hasAdditionalHeadContent: Bool {
|
||||
additionalHeadContentUrl.exists
|
||||
}
|
||||
|
||||
func customHeadContent() throws -> String? {
|
||||
let url = additionalHeadContentUrl
|
||||
guard url.exists else {
|
||||
return nil
|
||||
}
|
||||
return try wrap(.failedToOpenFile(url.path)) {
|
||||
try String(contentsOf: url)
|
||||
}
|
||||
}
|
||||
|
||||
private var additionalFooterContentUrl: URL {
|
||||
inputFolder.appendingPathComponent("footer.html")
|
||||
}
|
||||
|
||||
var hasAdditionalFooterContent: Bool {
|
||||
additionalFooterContentUrl.exists
|
||||
}
|
||||
|
||||
func customFooterContent() throws -> String? {
|
||||
let url = additionalFooterContentUrl
|
||||
guard url.exists else {
|
||||
return nil
|
||||
}
|
||||
return try wrap(.failedToOpenFile(url.path)) {
|
||||
try String(contentsOf: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteElement {
|
||||
|
||||
func printContents() {
|
||||
print(path)
|
||||
elements.forEach { $0.printContents() }
|
||||
}
|
||||
}
|
35
WebsiteGenerator/Content/ThumbnailStyle.swift
Normal file
35
WebsiteGenerator/Content/ThumbnailStyle.swift
Normal 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 {
|
||||
|
||||
}
|
Reference in New Issue
Block a user