Convert Xcode project to swift package

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

View File

@ -0,0 +1,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,18 @@
import Foundation
struct PageImageTemplate: Template {
enum Key: String, CaseIterable {
case image = "IMAGE"
case image2x = "IMAGE_2X"
case width = "WIDTH"
case height = "HEIGHT"
case leftText = "LEFT_TEXT"
case rightText = "RIGHT_TEXT"
}
static let templateName = "image.html"
let raw: String
}

View File

@ -0,0 +1,37 @@
import Foundation
struct PageVideoTemplate: Template {
typealias VideoSource = (url: String, type: VideoType)
enum Key: String, CaseIterable {
case options = "OPTIONS"
case sources = "SOURCES"
}
enum VideoOption: String {
case controls
case autoplay
case muted
case loop
case playsinline
case poster
case preload
}
static let templateName = "video.html"
let raw: String
func generate<T>(sources: [VideoSource], options: T) -> String where T: Sequence, T.Element == VideoOption {
let sourcesCode = sources.map(makeSource).joined(separator: "\n")
let optionCode = options.map { $0.rawValue }.joined(separator: " ")
return generate([.sources: sourcesCode, .options: optionCode])
}
private func makeSource(_ source: VideoSource) -> String {
"""
<source src="\(source.url)" type="\(source.type.htmlType)">
"""
}
}

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) -> 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,158 @@
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
private let fullDateFormatter: DateFormatter
private let month: DateFormatter
private let day: DateFormatter
var language: String {
topBar.language
}
// MARK: Pages
var page: PageTemplate {
factory.page
}
init(factory: TemplateFactory, language: String, site: Element) {
self.author = site.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.sortedItems.map {
PrefilledTopBarTemplate.SectionInfo(
name: $0.title(for: language),
url: $0.path + Element.htmlPagePathAddition(for: language))
}
self.topBar = .init(
factory: factory,
language: language,
sections: sections,
topBarWebsiteTitle: site.topBarTitle)
self.pageHead = PageHeadGenerator(
factory: factory)
self.overviewSection = OverviewSectionGenerator(
factory: factory)
}
// MARK: Content
func makePlaceholder(metadata: Element.LocalizedMetadata) -> String {
makePlaceholder(title: metadata.placeholderTitle, text: metadata.placeholderText)
}
func makePlaceholder(title: String, text: String) -> String {
factory.placeholder.generate([
.title: title,
.text: text])
}
func makeBackLink(text: String, language: String) -> String {
let content: [BackNavigationTemplate.Key : String] = [
.text: text,
.url: ".." + Element.htmlPagePathAddition(for: language)
]
return backNavigation.generate(content)
}
func makeDateString(start: Date?, end: Date?) -> String {
guard let start = start else {
return ""
}
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: ",")
}
func makeHeaderContent(page: Element, metadata: Element.LocalizedMetadata, language: String) -> [HeaderKey : String] {
let backText = page.backLinkText(for: language)
var content = [HeaderKey : String]()
if let backText = backText.nonEmpty {
content[.backLink] = makeBackLink(text: backText, language: language)
}
if let suffix = metadata.titleSuffix {
content[.title] = factory.html.make(title: metadata.title, suffix: suffix)
} else {
content[.title] = metadata.title
}
content[.subtitle] = metadata.subtitle
content[.titleText] = metadata.description
content[.date] = makeDateString(start: page.date, end: page.endDate)
return content
}
}

View File

@ -0,0 +1,41 @@
import Foundation
struct PrefilledTopBarTemplate {
let language: String
let sections: [SectionInfo]
let topBarWebsiteTitle: String
private let factory: TemplateFactory
init(factory: TemplateFactory, language: String, sections: [SectionInfo], topBarWebsiteTitle: String) {
self.factory = factory
self.language = language
self.sections = sections
self.topBarWebsiteTitle = topBarWebsiteTitle
}
func generate(sectionUrl: String?, languageButton: String?, page: Element) -> String {
var content = [TopBarTemplate.Key : String]()
content[.title] = topBarWebsiteTitle
content[.titleLink] = factory.html.topBarWebsiteTitle(language: language, from: page)
content[.elements] = elements(activeSectionUrl: sectionUrl)
content[.languageButton] = languageButton.unwrapped(factory.html.topBarLanguageButton) ?? ""
return factory.topBar.generate(content)
}
private func elements(activeSectionUrl: String?) -> String {
sections
.map { factory.html.topBarNavigationLink(url: $0.url, text: $0.name, isActive: activeSectionUrl == $0.url) }
.joined(separator: "\n")
}
struct SectionInfo {
let name: String
let url: String
}
}

View File

@ -0,0 +1,32 @@
import Foundation
protocol HeaderTemplate {
func generate(_ content: [ThumbnailKey : String], shouldIndent: Bool) throws -> String
}
enum HeaderKey: String, CaseIterable {
case backLink = "BACK_LINK"
case title = "TITLE"
case subtitle = "SUBTITLE"
case titleText = "TITLE_TEXT"
case date = "DATE"
}
struct CenteredHeaderTemplate: Template {
typealias Key = HeaderKey
let raw: String
static let templateName = "header-center.html"
}
struct LeftHeaderTemplate: Template {
typealias Key = HeaderKey
let raw: String
static let templateName = "header-left.html"
}

View File

@ -0,0 +1,21 @@
import Foundation
struct PageTemplate: Template {
enum Key: String, CaseIterable {
case head = "HEAD"
case topBar = "TOP_BAR"
case contentClass = "CONTENT_CLASS"
case header = "HEADER"
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,58 @@
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 String(contentsOf: url)
self.init(raw: raw)
}
func generate(_ content: [Key : String], to url: URL) -> Bool {
let content = generate(content)
return files.write(content, 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,78 @@
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: Headers
let leftHeader: LeftHeaderTemplate
let centeredHeader: CenteredHeaderTemplate
// MARK: Pages
let page: PageTemplate
let image: PageImageTemplate
let video: PageVideoTemplate
// MARK: HTML
let html: HTMLElementsGenerator
// 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.leftHeader = try .init(in: templateFolder)
self.centeredHeader = try .init(in: templateFolder)
self.page = try .init(in: templateFolder)
self.image = try .init(in: templateFolder)
self.video = try .init(in: templateFolder)
self.html = .init()
}
}