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,60 @@
import Foundation
private struct AboveRootDummy: SiteElement {
var sortIndex: Int? { nil }
var sortDate: Date? { nil }
var path: String { "" }
let inputFolder: URL
func title(for language: String) -> String { "" }
func cornerText(for language: String) -> String? { nil }
var elements: [SiteElement] { [] }
}
struct IndexPageGenerator {
private let factory: LocalizedSiteTemplate
init(factory: LocalizedSiteTemplate, imageProcessor: ImageProcessor) {
self.factory = factory
}
func generate(
site: Site,
language: String,
languageButton: String?,
sectionItemCount: Int,
to url: URL) throws {
let localized = site.localized(for: language)
var content = [OverviewPageTemplate.Key : String]()
content[.head] = try makeHead(site: site, language: language)
content[.topBar] = factory.topBar.generate(section: nil, languageButton: languageButton)
content[.title] = localized.title
content[.subtitle] = localized.subtitle
content[.titleText] = localized.description
let sections = site.elements.compactMap { $0 as? Section }
content[.sections] = try factory.overviewSection.generate(
sections: sections,
in: site,
language: language,
sectionItemCount: sectionItemCount)
content[.footer] = SiteGenerator.pageFooter
try factory.overviewPage.generate(content, to: url)
}
private func makeHead(site: Site, language: String) throws -> String {
let localized = site.localized(for: language)
return try factory.pageHead.generate(page: PageHeadInfo(
author: site.metadata.author,
linkPreviewTitle: localized.linkPreviewTitle,
linkPreviewDescription: localized.linkPreviewDescription,
linkPreviewImage: site.linkPreviewImage(for: language),
customHeadContent: try site.customHeadContent()))
}
}

View File

@@ -0,0 +1,130 @@
import Foundation
import Ink
struct PageContentGenerator {
private let imageProcessor: ImageProcessor
init(imageProcessor: ImageProcessor) {
self.imageProcessor = imageProcessor
}
func generate(page: Page, language: String, at url: URL) throws -> String {
var errorToThrow: Error? = nil
let content = try wrap(.missingPage(page: url.path, language: language)) {
try String(contentsOf: url)
}
var hasCodeContent = false
let imageModifier = Modifier(target: .images) { html, markdown in
let result = processMarkdownImage(markdown: markdown, html: html, page: page)
switch result {
case .success(let content):
return content
case .failure(let error):
errorToThrow = error
return ""
}
}
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
if markdown.starts(with: "```swift") {
#warning("Syntax highlight swift code")
return html
}
hasCodeContent = true
return html
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier])
#warning("Check links in markdown for (missing) files to copy")
if hasCodeContent {
#warning("Automatically add hljs hightlighting if code samples are found")
}
let result = parser.html(from: content)
if let error = errorToThrow {
throw error
}
return result
}
private func processMarkdownImage(markdown: Substring, html: String, page: Page) -> Result<String, Error> {
let fileAndTitle = markdown
.components(separatedBy: "(").last!
.components(separatedBy: ")").first!
let file = fileAndTitle.components(separatedBy: " \"").first! // Remove title
let rightSubtitle: String?
if fileAndTitle.contains(" \"") {
rightSubtitle = fileAndTitle.dropBeforeFirst("\"").dropAfterLast("\"")
} else {
rightSubtitle = nil
}
let leftSubtitle = markdown
.components(separatedBy: "]").first!
.components(separatedBy: "[").last!.nonEmpty
#warning("Specify page image width in configuration")
let pageImageWidth = 748
let size: NSSize
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
do {
size = try imageProcessor.requireImage(
source: imagePath,
destination: imagePath,
width: pageImageWidth,
desiredHeight: nil,
createDoubleVersion: true)
} catch {
return .failure(error)
}
let file2x = file.insert("@2x", beforeLast: ".")
#warning("Move HTML code to single location")
let result = articelImage(
image: file,
image2x: file2x,
width: size.width,
height: size.height,
rightSubtitle: rightSubtitle,
leftSubtitle: leftSubtitle)
return .success(result)
}
private func articelImage(image: String, image2x: String, width: CGFloat, height: CGFloat, rightSubtitle: String?, leftSubtitle: String?) -> String {
let subtitleCode = subtitle(left: leftSubtitle, right: rightSubtitle)
return fullImageCode(image: image, image2x: image2x, width: width, height: height, subtitle: subtitleCode)
}
private func articleImageWithoutSubtitle(image: String, image2x: String, width: CGFloat, height: CGFloat) -> String {
"""
<span class="image">
<img src="\(image)" srcset="\(image2x) 2x" width="\(Int(width))" height="\(Int(height))" loading="lazy"/>
</span>
"""
}
private func subtitle(left: String?, right: String?) -> String {
guard left != nil || right != nil else {
return ""
}
let leftCode = left.unwrapped { "<span class=\"left\">\($0)</span>" } ?? ""
let rightCode = right.unwrapped { "<span class=\"right\">\($0)</span>" } ?? ""
return """
<div class="subtitle">
\(leftCode)
\(rightCode)
</div>
"""
}
private func fullImageCode(image: String, image2x: String, width: CGFloat, height: CGFloat, subtitle: String) -> String {
"""
<span class="image">
<img src="\(image)" srcset="\(image2x) 2x" width="\(Int(width))" height="\(Int(height))" loading="lazy"/>
\(subtitle)
</span>
"""
}
}

View File

@@ -0,0 +1,62 @@
import Foundation
struct OverviewPageGenerator {
private let factory: LocalizedSiteTemplate
let outputFolder: URL
init(factory: LocalizedSiteTemplate, imageProcessor: ImageProcessor) {
self.factory = factory
self.outputFolder = imageProcessor.outputFolder
}
func generate(
section: Section,
language: String,
backText: String?) throws {
let url = outputFolder.appendingPathComponent(section.localizedPath(for: language))
let metadata = section.localized(for: language)
var content = [OverviewPageTemplate.Key : String]()
content[.head] = try makeHead(section: section, language: language)
let languageButton = section.nextLanguage(for: language)
content[.topBar] = factory.topBar.generate(
section: section.sectionId,
languageButton: languageButton)
content[.sections] = try makeContent(section: section, language: language)
content[.title] = metadata.title
content[.subtitle] = metadata.subtitle
content[.titleText] = metadata.description
content[.footer] = SiteGenerator.pageFooter
content[.backLink] = backText.unwrapped { factory.makeBackLink(text: $0, language: language) }
try factory.overviewPage.generate(content, to: url)
}
private func makeContent(section: Section, language: String) throws -> String {
if section.hasNestingElements {
let sections = section.elements.compactMap { $0 as? Section }
return try factory.overviewSection.generate(
sections: sections,
in: section,
language: language,
sectionItemCount: section.metadata.sectionOverviewItemCount)
} else {
return try factory.overviewSection.generate(section: section, language: language)
}
}
private func makeHead(section: Section, language: String) throws -> String {
let localized = section.localized(for: language)
let image = section.linkPreviewImage(for: language)
let info = PageHeadInfo(
author: factory.author,
linkPreviewTitle: localized.title,
linkPreviewDescription: localized.linkPreviewDescription,
linkPreviewImage: image,
customHeadContent: try section.customHeadContent())
return try factory.pageHead.generate(page: info)
}
}

View File

@@ -0,0 +1,69 @@
import Foundation
struct OverviewSectionGenerator {
private let multipleSectionsTemplate: OverviewSectionTemplate
private let singleSectionsTemplate: OverviewSectionCleanTemplate
let imageProcessor: ImageProcessor
private let generator: ThumbnailListGenerator
init(factory: TemplateFactory, imageProcessor: ImageProcessor) {
self.multipleSectionsTemplate = factory.overviewSection
self.singleSectionsTemplate = factory.overviewSectionClean
self.imageProcessor = imageProcessor
self.generator = ThumbnailListGenerator(factory: factory, imageProcessor: imageProcessor)
}
func generate(sections: [Section], in parent: SiteElement, language: String, sectionItemCount: Int) throws -> String {
try sections.map { section in
let metadata = section.localized(for: language)
let fullUrl = section.fullPageUrl(for: language)
let relativeUrl = parent.relativePathToFileWithPath(fullUrl)
var content = [OverviewSectionTemplate.Key : String]()
content[.url] = relativeUrl
content[.title] = metadata.title
content[.items] = try sectionContent(section: section, in: parent, language: language, shownItemCount: sectionItemCount)
content[.more] = metadata.moreLinkTitle
return multipleSectionsTemplate.generate(content)
}
.joined(separator: "\n")
}
func generate(section: Section, language: String) throws -> String {
var content = [OverviewSectionCleanTemplate.Key : String]()
content[.items] = try sectionContent(section: section, in: section, language: language, shownItemCount: nil)
return singleSectionsTemplate.generate(content)
}
private func sectionContent(section: Section, in parent: SiteElement, language: String, shownItemCount: Int?) throws -> String {
let sectionItems: [SiteElement]
if let shownItemCount = shownItemCount {
sectionItems = Array(section.sortedItems.prefix(shownItemCount))
} else {
sectionItems = section.sortedItems
}
let items: [ThumbnailInfo] = sectionItems.map { item in
#warning("Check if page exists for the language")
let fullPageUrl = item.fullPageUrl(for: language)
let relativePageUrl = parent.relativePathToFileWithPath(fullPageUrl)
let fullThumbnailPath = item.thumbnailFilePath(for: language)
let relativeImageUrl = parent.relativePathToFileWithPath(fullThumbnailPath)
return ThumbnailInfo(
url: relativePageUrl,
imageFilePath: fullThumbnailPath,
imageHtmlUrl: relativeImageUrl,
title: item.title(for: language),
cornerText: item.cornerText(for: language))
}
return try generator.generateContent(
items: items,
style: section.metadata.thumbnailStyle)
}
}

View File

@@ -0,0 +1,71 @@
import Foundation
import Ink
struct PageGenerator {
struct NavigationLink {
let link: String
let text: String
}
private let factory: LocalizedSiteTemplate
private let imageProcessor: ImageProcessor
init(factory: LocalizedSiteTemplate, imageProcessor: ImageProcessor) {
self.factory = factory
self.imageProcessor = imageProcessor
}
func generate(page: Page, language: String, backText: String, nextPage: NavigationLink?, previousPage: NavigationLink?) throws {
guard !page.isExternalPage else {
return
}
guard !page.metadata.isDraft else {
return
}
let path = page.fullPageUrl(for: language)
let inputContentUrl = page.inputFolder.appendingPathComponent("\(language).md")
#warning("Make prev and next navigation relative")
let metadata = page.localized(for: language)
let nextLanguage = page.nextLanguage(for: language)
var content = [ContentPageTemplate.Key : String]()
content[.head] = try makeHead(page: page, language: language)
content[.topBar] = factory.topBar.generate(section: page.sectionId, languageButton: nextLanguage)
content[.backLink] = factory.makeBackLink(text: backText, language: language)
content[.title] = metadata.title
content[.subtitle] = metadata.subtitle
content[.date] = factory.makeDateString(start: page.metadata.date, end: page.metadata.endDate)
content[.content] = try makeContent(page: page, language: language, url: inputContentUrl)
content[.previousPageLinkText] = previousPage.unwrapped { factory.makePrevText($0.text) }
content[.previousPageUrl] = previousPage?.link
content[.nextPageLinkText] = nextPage.unwrapped { factory.makeNextText($0.text) }
content[.nextPageUrl] = nextPage?.link
content[.footer] = try page.customFooterContent()
let url = imageProcessor.outputFolder.appendingPathComponent(path)
try factory.contentPage.generate(content, to: url)
}
private func makeContent(page: Page, language: String, url: URL) throws -> String {
guard url.exists else {
print("Generated empty page \(page.path)")
return factory.placeholder
}
print("Generated page \(page.path)")
return try PageContentGenerator(imageProcessor: imageProcessor).generate(page: page, language: language, at: url)
}
private func makeHead(page: Page, language: String) throws -> String {
let metadata = page.localized(for: language)
let info = PageHeadInfo(
author: page.metadata.author ?? factory.author,
linkPreviewTitle: metadata.linkPreviewTitle,
linkPreviewDescription: metadata.linkPreviewDescription,
linkPreviewImage: page.linkPreviewImage(for: language),
customHeadContent: try page.customHeadContent())
return try factory.pageHead.generate(page: info)
}
}

View File

@@ -0,0 +1,61 @@
import Foundation
protocol PageHeadInfoProvider {
var author: String { get }
var linkPreviewTitle: String { get }
var linkPreviewDescription: String { get }
var linkPreviewImage: String? { get }
var customHeadContent: String? { get }
}
struct PageHeadInfo: PageHeadInfoProvider {
let author: String
let linkPreviewTitle: String
let linkPreviewDescription: String
let linkPreviewImage: String?
let customHeadContent: String?
}
struct PageHeadGenerator {
let template: PageHeadTemplate
let imageProcessor: ImageProcessor
init(factory: TemplateFactory, imageProcessor: ImageProcessor) {
self.template = factory.pageHead
self.imageProcessor = imageProcessor
}
func generate(page: PageHeadInfoProvider) throws -> String {
var content = [PageHeadTemplate.Key : String]()
content[.author] = page.author
content[.title] = page.linkPreviewTitle
content[.description] = page.linkPreviewDescription
if let image = page.linkPreviewImage {
// Note: Generate separate destination link for the image,
// since we don't want a single large image for thumbnails.
// Warning: Link preview source path must be relative to root
let linkPreviewImagePath = image.insert("-link", beforeLast: ".")
try imageProcessor.requireImage(
source: image,
destination: linkPreviewImagePath,
width: Site.linkPreviewDesiredImageWidth)
#warning("Make link preview image path absolute")
content[.image] = "<meta property=\"og:image\" content=\"\(linkPreviewImagePath)\" />"
}
content[.customPageContent] = page.customHeadContent
return template.generate(content)
}
}

View File

@@ -0,0 +1,80 @@
import Foundation
struct SiteGenerator {
let site: Site
let templates: TemplateFactory
private let imageProcessor: ImageProcessor
private var outputFolder: URL {
imageProcessor.outputFolder
}
init(site: Site, imageProcessor: ImageProcessor) throws {
self.site = site
let templatesFolder = site.inputFolder.appendingPathComponent("templates")
self.templates = try TemplateFactory(templateFolder: templatesFolder)
self.imageProcessor = imageProcessor
}
func generate() throws {
try site.metadata.languages.forEach { metadata in
let language = metadata.languageIdentifier
let template = try LocalizedSiteTemplate(
factory: templates,
language: language,
site: site,
imageProcessor: imageProcessor)
// Generate sections
let overviewGenerator = OverviewPageGenerator(factory: template, imageProcessor: imageProcessor)
let pageGenerator = PageGenerator(factory: template, imageProcessor: imageProcessor)
let backLinkText = try site.backLinkText(for: language)
var elementsToProcess: [(element: SiteElement, backText: String?)] = site.elements.map { ($0, backLinkText) }
while let (element, backText) = elementsToProcess.popLast() {
if let section = element as? Section {
try overviewGenerator.generate(
section: section,
language: language,
backText: backText)
let elementBackText = try element.backLinkText(for: language)
let nestedElements = section.elements.map { ($0, elementBackText) }
elementsToProcess.append(contentsOf: nestedElements)
}
if let page = element as? Page {
#warning("Determine previous and next pages")
try pageGenerator.generate(
page: page,
language: language,
backText: backText ?? metadata.defaultBackLinkText,
nextPage: nil,
previousPage: nil)
}
}
let generator = IndexPageGenerator(
factory: template,
imageProcessor: imageProcessor)
// Generate front page
let relativeUrl = site.localizedPath(for: language)
let indexPageUrl = outputFolder.appendingPathComponent(relativeUrl)
let button = site.nextLanguage(for: language)
try generator.generate(
site: site,
language: language,
languageButton: button,
sectionItemCount: 6,
to: indexPageUrl)
}
}
static let pageFooter: String =
"""
<script src="/assets/js/jquery.js"></script>
<script src="/assets/js/global.min.js"></script>
"""
}

View File

@@ -0,0 +1,14 @@
import Foundation
struct ThumbnailInfo {
let url: String?
let imageFilePath: String
let imageHtmlUrl: String
let title: String
let cornerText: String?
}

View File

@@ -0,0 +1,38 @@
import Foundation
struct ThumbnailListGenerator {
private let factory: TemplateFactory
let imageProcessor: ImageProcessor
init(factory: TemplateFactory, imageProcessor: ImageProcessor) {
self.factory = factory
self.imageProcessor = imageProcessor
}
func generateContent(items: [ThumbnailInfo], style: ThumbnailStyle) throws -> String {
try items.map { try itemContent($0, style: style) }
.joined(separator: "\n")
}
private func itemContent(_ thumbnail: ThumbnailInfo, style: ThumbnailStyle) throws -> String {
var content = [ThumbnailKey : String]()
content[.url] = thumbnail.url.unwrapped { "href=\"\($0)\"" }
content[.image] = thumbnail.imageHtmlUrl
content[.title] = thumbnail.title
content[.image2x] = thumbnail.imageHtmlUrl.insert("@2x", beforeLast: ".")
content[.corner] = thumbnail.cornerText.unwrapped {
factory.largeThumbnail.makeCorner(text: $0)
}
try imageProcessor.requireImage(
source: thumbnail.imageFilePath,
destination: thumbnail.imageFilePath,
width: style.width,
desiredHeight: style.height,
createDoubleVersion: true)
return try factory.thumbnail(style: style).generate(content, shouldIndent: false)
}
}