First version
This commit is contained in:
119
WebsiteGenerator/Content/LanguageContainer.swift
Normal file
119
WebsiteGenerator/Content/LanguageContainer.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
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)"
|
||||
}
|
||||
}
|
41
WebsiteGenerator/Content/LinkPreviewMetadata.swift
Normal file
41
WebsiteGenerator/Content/LinkPreviewMetadata.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
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")
|
||||
}
|
||||
}
|
23
WebsiteGenerator/Content/LinkPreviewMetadataProvider.swift
Normal file
23
WebsiteGenerator/Content/LinkPreviewMetadataProvider.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
}
|
29
WebsiteGenerator/Content/Metadata.swift
Normal file
29
WebsiteGenerator/Content/Metadata.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
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)
|
||||
}
|
||||
}
|
62
WebsiteGenerator/Content/Page+LocalizedMetadata.swift
Normal file
62
WebsiteGenerator/Content/Page+LocalizedMetadata.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
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 }
|
||||
}
|
93
WebsiteGenerator/Content/Page+Metadata.swift
Normal file
93
WebsiteGenerator/Content/Page+Metadata.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
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]
|
||||
|
||||
#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: [])
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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) ?? []
|
||||
}
|
||||
}
|
83
WebsiteGenerator/Content/Page.swift
Normal file
83
WebsiteGenerator/Content/Page.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
65
WebsiteGenerator/Content/Section+LocalizedMetadata.swift
Normal file
65
WebsiteGenerator/Content/Section+LocalizedMetadata.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
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 {
|
||||
|
||||
}
|
79
WebsiteGenerator/Content/Section+Metadata.swift
Normal file
79
WebsiteGenerator/Content/Section+Metadata.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
}
|
74
WebsiteGenerator/Content/Section.swift
Normal file
74
WebsiteGenerator/Content/Section.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
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 {
|
||||
|
||||
}
|
71
WebsiteGenerator/Content/Site+LocalizedMetadata.swift
Normal file
71
WebsiteGenerator/Content/Site+LocalizedMetadata.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
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 {
|
||||
|
||||
}
|
48
WebsiteGenerator/Content/Site+Metadata.swift
Normal file
48
WebsiteGenerator/Content/Site+Metadata.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
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])
|
||||
}
|
||||
}
|
54
WebsiteGenerator/Content/Site.swift
Normal file
54
WebsiteGenerator/Content/Site.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
}
|
||||
}
|
199
WebsiteGenerator/Content/SiteElement.swift
Normal file
199
WebsiteGenerator/Content/SiteElement.swift
Normal file
@@ -0,0 +1,199 @@
|
||||
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() }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user