First version

This commit is contained in:
Christoph Hagen
2022-08-16 10:39:05 +02:00
parent 104c5151b4
commit 14b935249f
44 changed files with 2891 additions and 8 deletions

View File

@ -0,0 +1,13 @@
import Foundation
struct BackNavigationTemplate: Template {
enum Key: String, CaseIterable {
case url = "URL"
case text = "TEXT"
}
static let templateName = "back.html"
let raw: String
}

View File

@ -0,0 +1,12 @@
import Foundation
struct OverviewSectionCleanTemplate: Template {
enum Key: String, CaseIterable {
case items = "ITEMS"
}
static let templateName = "overview-section-clean.html"
let raw: String
}

View File

@ -0,0 +1,15 @@
import Foundation
struct OverviewSectionTemplate: Template {
enum Key: String, CaseIterable {
case url = "URL"
case title = "TITLE"
case items = "ITEMS"
case more = "MORE"
}
static let templateName = "overview-section.html"
let raw: String
}

View File

@ -0,0 +1,16 @@
import Foundation
struct PageHeadTemplate: Template {
enum Key: String, CaseIterable {
case author = "AUTHOR"
case title = "TITLE"
case description = "DESCRIPTION"
case image = "IMAGE"
case customPageContent = "CUSTOM"
}
let raw: String
static let templateName = "head.html"
}

View File

@ -0,0 +1,13 @@
import Foundation
struct PlaceholderTemplate: Template {
enum Key: String, CaseIterable {
case title = "TITLE"
case text = "TEXT"
}
static let templateName = "empty.html"
var raw: String
}

View File

@ -0,0 +1,50 @@
import Foundation
protocol ThumbnailTemplate {
func generate(_ content: [ThumbnailKey : String], shouldIndent: Bool) throws -> String
}
enum ThumbnailKey: String, CaseIterable {
case url = "URL"
case image = "IMAGE"
case image2x = "IMAGE_2X"
case title = "TITLE"
case corner = "CORNER"
}
struct LargeThumbnailTemplate: Template, ThumbnailTemplate {
typealias Key = ThumbnailKey
static let templateName = "thumbnail-large.html"
let raw: String
func makeCorner(text: String) -> String {
"<span class=\"corner\"><span>\(text)</span></span>"
}
func makeTitleSuffix(_ suffix: String) -> String {
"<span class=\"suffix\">\(suffix)</span>"
}
}
struct SquareThumbnailTemplate: Template, ThumbnailTemplate {
typealias Key = ThumbnailKey
static let templateName = "thumbnail-square.html"
let raw: String
}
struct SmallThumbnailTemplate: Template, ThumbnailTemplate {
typealias Key = ThumbnailKey
static let templateName = "thumbnail-small.html"
let raw: String
}

View File

@ -0,0 +1,15 @@
import Foundation
struct TopBarTemplate: Template {
enum Key: String, CaseIterable {
case title = "TITLE"
case titleLink = "TITLE_URL"
case elements = "ELEMENTS"
case languageButton = "LANG_BUTTON"
}
static let templateName = "bar.html"
var raw: String
}

View File

@ -0,0 +1,167 @@
import Foundation
import Ink
struct LocalizedSiteTemplate {
let author: String
let factory: TemplateFactory
let topBar: PrefilledTopBarTemplate
// MARK: Site Elements
var backNavigation: BackNavigationTemplate {
factory.backNavigation
}
let pageHead: PageHeadGenerator
let overviewSection: OverviewSectionGenerator
let placeholder: String
private let fullDateFormatter: DateFormatter
private let month: DateFormatter
private let day: DateFormatter
var language: String {
topBar.language
}
// MARK: Thumbnails
func thumbnail(style: ThumbnailStyle) -> ThumbnailTemplate {
switch style {
case .large:
return factory.largeThumbnail
case .square:
return factory.squareThumbnail
case .small:
return factory.smallThumbnail
}
}
// MARK: Pages
var overviewPage: OverviewPageTemplate {
factory.overviewPage
}
var contentPage: ContentPageTemplate {
factory.contentPage
}
init(factory: TemplateFactory, language: String, site: Site, imageProcessor: ImageProcessor) throws {
self.author = site.metadata.author
self.factory = factory
let df = DateFormatter()
df.dateStyle = .long
df.timeStyle = .none
df.locale = Locale(identifier: language)
self.fullDateFormatter = df
let df2 = DateFormatter()
df2.dateFormat = "MMMM"
df2.locale = Locale(identifier: language)
self.month = df2
let df3 = DateFormatter()
df3.dateFormat = "dd"
df3.locale = Locale(identifier: language)
self.day = df3
let sections = site.elements.map {
PrefilledTopBarTemplate.SectionInfo(
id: $0.sectionId,
name: $0.title(for: language),
url: "\($0.path)/\(language).html")
}
let metadata = site.localized(for: language)
let title = site.metadata.topBarTitle ?? metadata.linkPreviewTitle
self.topBar = try .init(
template: factory.topBar,
language: language,
sections: sections,
topBarWebsiteTitle: title)
self.pageHead = PageHeadGenerator(
factory: factory,
imageProcessor: imageProcessor)
self.overviewSection = OverviewSectionGenerator(
factory: factory,
imageProcessor: imageProcessor)
self.placeholder = factory.placeholder.generate([
.title: metadata.placeholderTitle,
.text: metadata.placeholderText])
}
// MARK: Content
func makeBackLink(text: String, language: String) -> String {
let content: [BackNavigationTemplate.Key : String] = [
.text: text,
.url: "../\(language).html"
]
return backNavigation.generate(content)
}
#warning("Move HTML code to single location")
func makePrevText(_ text: String) -> String {
"<span class=\"icon-back\"></span>\(text)"
}
func makeNextText(_ text: String) -> String {
"\(text)<span class=\"icon-next\"></span>"
}
func makeDateString(start: Date, end: Date?) -> String {
guard let end = end else {
return fullDateFormatter.string(from: start)
}
switch language {
case "de":
return makeGermanDateString(start: start, end: end)
case "en":
fallthrough
default:
return makeEnglishDateString(start: start, end: end)
}
}
private func makeGermanDateString(start: Date, end: Date) -> String {
guard Calendar.current.isDate(start, equalTo: end, toGranularity: .year) else {
return "\(fullDateFormatter.string(from: start)) - \(fullDateFormatter.string(from: end))"
}
let startDay = day.string(from: start)
guard Calendar.current.isDate(start, equalTo: end, toGranularity: .month) else {
let startMonth = month.string(from: start)
return "\(startDay). \(startMonth) - \(fullDateFormatter.string(from: end))"
}
return "\(startDay). - \(fullDateFormatter.string(from: end))"
}
private func makeEnglishDateString(start: Date, end: Date) -> String {
guard Calendar.current.isDate(start, equalTo: end, toGranularity: .year) else {
return "\(fullDateFormatter.string(from: start)) - \(fullDateFormatter.string(from: end))"
}
guard Calendar.current.isDate(start, equalTo: end, toGranularity: .month) else {
let startDay = day.string(from: start)
let startMonth = month.string(from: start)
return "\(startMonth) \(startDay) - \(fullDateFormatter.string(from: end))"
}
return fullDateFormatter.string(from: start)
.insert(" - \(day.string(from: end))", beforeLast: ",")
}
}

View File

@ -0,0 +1,58 @@
import Foundation
struct PrefilledTopBarTemplate {
let language: String
let sections: [SectionInfo]
let topBarWebsiteTitle: String
private let topBar: TopBarTemplate
init(template: TopBarTemplate, language: String, sections: [SectionInfo], topBarWebsiteTitle: String) throws {
self.topBar = template
self.language = language
self.sections = sections
self.topBarWebsiteTitle = topBarWebsiteTitle
}
func generate(section: String?, languageButton: String?) -> String {
var content = [TopBarTemplate.Key : String]()
content[.title] = topBarWebsiteTitle
content[.titleLink] = topBarWebsiteTitle(language: language)
content[.elements] = elements(activeSection: section)
content[.languageButton] = languageButton.unwrapped(topBarLanguageButton) ?? ""
return topBar.generate(content)
}
private func elements(activeSection: String?) -> String {
sections
.map {
topBarNavigationLink(url: $0.url, text: $0.name, isActive: activeSection == $0.id)
}
.joined(separator: "\n")
}
#warning("Move HTML code to single location")
private func topBarWebsiteTitle(language: String) -> String {
"/\(language).html"
}
private func topBarLanguageButton(_ language: String) -> String {
"<a href=\"\(language).html\">\(language)</a>"
}
private func topBarNavigationLink(url: String, text: String, isActive: Bool) -> String {
"<a\(isActive ? " class=\"active\"" : "") href=\"/\(url)\">\(text)</a>"
}
struct SectionInfo {
let id: String
let name: String
let url: String
}
}

View File

@ -0,0 +1,23 @@
import Foundation
struct ContentPageTemplate: Template {
enum Key: String, CaseIterable {
case head = "HEAD"
case topBar = "TOP_BAR"
case backLink = "BACK_LINK"
case title = "TITLE"
case subtitle = "SUBTITLE"
case date = "DATE"
case content = "CONTENT"
case previousPageLinkText = "PREV_TEXT"
case previousPageUrl = "PREV_LINK"
case nextPageLinkText = "NEXT_TEXT"
case nextPageUrl = "NEXT_LINK"
case footer = "FOOTER"
}
static let templateName = "page.html"
let raw: String
}

View File

@ -0,0 +1,19 @@
import Foundation
struct OverviewPageTemplate: Template {
enum Key: String, CaseIterable {
case head = "HEAD"
case topBar = "TOP_BAR"
case backLink = "BACK_LINK"
case title = "TITLE"
case subtitle = "SUBTITLE"
case titleText = "TITLE_TEXT"
case sections = "SECTIONS"
case footer = "FOOTER"
}
let raw: String
static let templateName = "overview-page.html"
}

View File

@ -0,0 +1,62 @@
import Foundation
protocol Template {
associatedtype Key where Key: RawRepresentable, Key.RawValue == String, Key: CaseIterable, Key: Hashable
static var templateName: String { get }
var raw: String { get }
init(raw: String)
}
extension Template {
init(in folder: URL) throws {
let url = folder.appendingPathComponent(Self.templateName)
try self.init(from: url)
}
init(from url: URL) throws {
let raw = try wrap(.failedToLoadTemplate(url.lastPathComponent)) {
try String(contentsOf: url)
}
self.init(raw: raw)
}
func generate(_ content: [Key : String], to url: URL) throws {
let content = generate(content)
try wrap(.failedToWriteFile(url.path)) {
try content.createFolderAndWrite(to: url)
}
}
func generate(_ content: [Key : String], shouldIndent: Bool = false) -> String {
var result = raw.components(separatedBy: "\n")
Key.allCases.forEach { key in
let newContent = content[key]?.withoutEmptyLines ?? ""
let stringMarker = "<!--\(key.rawValue)-->"
let indices = result.enumerated().filter { $0.element.contains(stringMarker) }
.map { $0.offset }
guard !indices.isEmpty else {
return
}
for index in indices {
let old = result[index].components(separatedBy: stringMarker)
// Add indentation to all added lines
let indentation = old.first!
guard shouldIndent, indentation.trimmingCharacters(in: .whitespaces).isEmpty else {
// Prefix is not indentation, so just insert new content
result[index] = old.joined(separator: newContent)
continue
}
let indentedReplacements = newContent.indented(by: indentation)
result[index] = old.joined(separator: indentedReplacements)
}
}
return result.joined(separator: "\n").withoutEmptyLines
}
}

View File

@ -0,0 +1,62 @@
import Foundation
final class TemplateFactory {
let templateFolder: URL
// MARK: Site Elements
let backNavigation: BackNavigationTemplate
let pageHead: PageHeadTemplate
let topBar: TopBarTemplate
let overviewSection: OverviewSectionTemplate
let overviewSectionClean: OverviewSectionCleanTemplate
let placeholder: PlaceholderTemplate
// MARK: Thumbnails
let largeThumbnail: LargeThumbnailTemplate
let squareThumbnail: SquareThumbnailTemplate
let smallThumbnail: SmallThumbnailTemplate
func thumbnail(style: ThumbnailStyle) -> ThumbnailTemplate {
switch style {
case .large:
return largeThumbnail
case .square:
return squareThumbnail
case .small:
return smallThumbnail
}
}
// MARK: Pages
let overviewPage: OverviewPageTemplate
let contentPage: ContentPageTemplate
// MARK: Init
init(templateFolder: URL) throws {
self.templateFolder = templateFolder
self.backNavigation = try .init(in: templateFolder)
self.pageHead = try .init(in: templateFolder)
self.topBar = try .init(in: templateFolder)
self.overviewSection = try .init(in: templateFolder)
self.overviewSectionClean = try .init(in: templateFolder)
self.placeholder = try .init(in: templateFolder)
self.largeThumbnail = try .init(in: templateFolder)
self.squareThumbnail = try .init(in: templateFolder)
self.smallThumbnail = try .init(in: templateFolder)
self.overviewPage = try .init(in: templateFolder)
self.contentPage = try .init(in: templateFolder)
}
}