Add navigation settings, fix page generation
This commit is contained in:
@ -2,28 +2,34 @@ import Foundation
|
||||
|
||||
final class FeedPageGenerator {
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let content: Content
|
||||
|
||||
init(content: Content) {
|
||||
init(content: Content, results: PageGenerationResults) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
}
|
||||
|
||||
private func includeSwiper(in headers: inout Set<HeaderElement>) {
|
||||
if let swiperCss = content.settings.posts.swiperCssFile {
|
||||
headers.insert(.css(file: swiperCss, order: HeaderElement.swiperCssFileOrder))
|
||||
results.require(file: swiperCss)
|
||||
}
|
||||
if let swiperJs = content.settings.posts.swiperJsFile {
|
||||
headers.insert(.js(file: swiperJs, defer: true))
|
||||
results.require(file: swiperJs)
|
||||
}
|
||||
}
|
||||
|
||||
func generatePage(language: ContentLanguage,
|
||||
posts: [FeedEntryData],
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
showTitle: Bool,
|
||||
pageNumber: Int,
|
||||
totalPages: Int) -> String {
|
||||
totalPages: Int,
|
||||
languageButtonUrl: String) -> String {
|
||||
var headers = content.defaultPageHeaders
|
||||
var footer = ""
|
||||
if posts.contains(where: { $0.images.count > 1 }) {
|
||||
@ -32,12 +38,23 @@ final class FeedPageGenerator {
|
||||
footer = swiperInitScript(posts: posts)
|
||||
}
|
||||
|
||||
let page = GenericPage(
|
||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
||||
let languageButton = NavigationBar.Link(
|
||||
text: language.next.rawValue,
|
||||
url: languageButtonUrl)
|
||||
|
||||
let pageHeader = PageHeader(
|
||||
language: language,
|
||||
title: title,
|
||||
description: description,
|
||||
iconUrl: iconUrl,
|
||||
languageButton: languageButton,
|
||||
links: content.navigationBar(in: language),
|
||||
headers: headers,
|
||||
icons: [])
|
||||
|
||||
let page = GenericPage(
|
||||
header: pageHeader,
|
||||
additionalFooter: footer) { content in
|
||||
if showTitle {
|
||||
content += "<h1>\(title)</h1>"
|
||||
@ -48,7 +65,6 @@ final class FeedPageGenerator {
|
||||
if totalPages > 1 {
|
||||
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
|
||||
}
|
||||
|
||||
}
|
||||
return page.content
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
struct LocalizedPageId: Hashable {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let pageId: String
|
||||
}
|
||||
|
||||
final class GenerationResults: ObservableObject {
|
||||
|
||||
/// The files that could not be accessed
|
||||
@ -181,6 +174,10 @@ final class GenerationResults: ObservableObject {
|
||||
func unsaved(_ path: String) {
|
||||
update { self.unsavedOutputFiles.insert(path) }
|
||||
}
|
||||
|
||||
func empty(_ page: LocalizedPageId) {
|
||||
update {self.emptyPages.insert(page) }
|
||||
}
|
||||
}
|
||||
|
||||
private extension Dictionary where Value == Set<ItemId> {
|
||||
|
@ -0,0 +1,24 @@
|
||||
|
||||
struct BoxCommandProcessor: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .box
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
init(content: Content, results: PageGenerationResults) {
|
||||
self.results = results
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count > 1 else {
|
||||
results.invalid(command: .box, markdown)
|
||||
return ""
|
||||
}
|
||||
let title = arguments[0]
|
||||
let text = arguments.dropFirst().joined(separator: ";")
|
||||
return ContentBox(title: title, text: text).content
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ struct IconCommandProcessor: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .icons
|
||||
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
init(content: Content, results: PageGenerationResults) {
|
||||
|
@ -13,7 +13,7 @@ struct InlineLinkProcessor {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
func handleLink(html: String, markdown: Substring) -> String {
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let url = markdown.between("(", and: ")")
|
||||
if url.hasPrefix(pageLinkMarker) {
|
||||
return handleInlinePageLink(url: url, html: html, markdown: markdown)
|
||||
@ -56,7 +56,7 @@ struct InlineLinkProcessor {
|
||||
return markdown.between("[", and: "]")
|
||||
}
|
||||
results.linked(to: tag)
|
||||
let tagPath = content.absoluteUrlToTag(tag, language: language)
|
||||
let tagPath = tag.absoluteUrl(in: language)
|
||||
return html.replacingOccurrences(of: textToChange, with: tagPath)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
import Splash
|
||||
|
||||
struct PageCodeProcessor {
|
||||
|
||||
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
|
||||
|
||||
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
init(results: PageGenerationResults) {
|
||||
self.results = results
|
||||
}
|
||||
|
||||
func process(_ html: String, markdown: Substring) -> String {
|
||||
guard markdown.starts(with: "```swift") else {
|
||||
results.require(header: .codeHightlighting)
|
||||
results.require(footer: codeHighlightFooter)
|
||||
return html // Just use normal code highlighting
|
||||
}
|
||||
// Highlight swift code using Splash
|
||||
let code = markdown.between("```swift", and: "```").trimmed
|
||||
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
|
||||
}
|
||||
}
|
153
CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift
Normal file
153
CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift
Normal file
@ -0,0 +1,153 @@
|
||||
import SwiftSoup
|
||||
|
||||
/**
|
||||
Handles both inline HTML and the external HTML command
|
||||
*/
|
||||
struct PageHtmlProcessor: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .includedHtml
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let content: Content
|
||||
|
||||
init(content: Content, results: PageGenerationResults) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
}
|
||||
|
||||
/**
|
||||
Handle the HTML command
|
||||
|
||||
Format: ``
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .includedHtml, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "External HTML command")
|
||||
return ""
|
||||
}
|
||||
let content = file.textContent()
|
||||
checkResources(in: content)
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
Handle inline HTML
|
||||
*/
|
||||
func process(_ html: String, markdown: Substring) -> String {
|
||||
checkResources(in: html)
|
||||
return html
|
||||
}
|
||||
|
||||
private func checkResources(in html: String) {
|
||||
let document: Document
|
||||
do {
|
||||
document = try SwiftSoup.parse(html)
|
||||
} catch {
|
||||
results.warning("Failed to parse inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
checkImages(in: document)
|
||||
checkLinks(in: document)
|
||||
checkSourceSets(in: document)
|
||||
}
|
||||
|
||||
private func checkImages(in document: Document) {
|
||||
let srcAttributes: [String]
|
||||
do {
|
||||
let imgElements = try document.select("img")
|
||||
|
||||
srcAttributes = try imgElements.array()
|
||||
.compactMap { try $0.attr("src") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
} catch {
|
||||
results.warning("Failed to check 'src' attributes of <img> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
for src in srcAttributes {
|
||||
results.warning("Found image in html: \(src)")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkLinks(in document: Document) {
|
||||
let hrefs: [String]
|
||||
do {
|
||||
let linkElements = try document.select("a")
|
||||
|
||||
hrefs = try linkElements.array()
|
||||
.compactMap { try $0.attr("href").trimmed }
|
||||
.filter { !$0.isEmpty }
|
||||
} catch {
|
||||
results.warning("Failed to check 'href' attributes of <a> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
for url in hrefs {
|
||||
if url.hasPrefix("http://") || url.hasPrefix("https://") {
|
||||
results.externalLink(to: url)
|
||||
} else {
|
||||
results.warning("Relative link in HTML: \(url)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkSourceSets(in document: Document) {
|
||||
let sources: [Element]
|
||||
do {
|
||||
sources = try document.select("source").array()
|
||||
} catch {
|
||||
results.warning("Failed to find <source> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func checkSourceSetAttributes(sources: [Element]) {
|
||||
let srcSets: [String]
|
||||
do {
|
||||
srcSets = try sources
|
||||
.compactMap { try $0.attr("srcset") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
} catch {
|
||||
results.warning("Failed to check 'srcset' attributes of <source> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
for src in srcSets {
|
||||
results.warning("Found source set in html: \(src)")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkSourceAttributes(sources: [Element]) {
|
||||
let srcAttributes: [String]
|
||||
do {
|
||||
srcAttributes = try sources
|
||||
.compactMap { try $0.attr("src") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
} catch {
|
||||
results.warning("Failed to check 'src' attributes of <source> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
for src in srcAttributes {
|
||||
guard content.isValidIdForFile(src) else {
|
||||
results.warning("Found source in html: \(src)")
|
||||
continue
|
||||
}
|
||||
guard let file = content.file(src) else {
|
||||
results.warning("Found source in html: \(src)")
|
||||
continue
|
||||
}
|
||||
#warning("Either find files by their full path, or replace file id with full path")
|
||||
results.require(file: file)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +1,33 @@
|
||||
import Foundation
|
||||
import Ink
|
||||
import Splash
|
||||
import SwiftSoup
|
||||
|
||||
final class PageContentParser {
|
||||
|
||||
private static let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
|
||||
|
||||
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
|
||||
|
||||
private let language: ContentLanguage
|
||||
|
||||
private let content: Content
|
||||
|
||||
private let results: PageGenerationResults
|
||||
|
||||
// MARK: Command handlers
|
||||
|
||||
private let buttonHandler: ButtonCommandProcessor
|
||||
|
||||
private let labelHandler: LabelsCommandProcessor
|
||||
|
||||
private let audioPlayer: AudioPlayerCommandProcessor
|
||||
|
||||
private let icons: IconCommandProcessor
|
||||
|
||||
private let box: BoxCommandProcessor
|
||||
|
||||
private let html: PageHtmlProcessor
|
||||
|
||||
// MARK: Other handlers
|
||||
|
||||
private let inlineLink: InlineLinkProcessor
|
||||
|
||||
private let icons: IconCommandProcessor
|
||||
private let code: PageCodeProcessor
|
||||
|
||||
var largeImageWidth: Int {
|
||||
content.settings.pages.largeImageWidth
|
||||
@ -40,127 +44,25 @@ final class PageContentParser {
|
||||
self.buttonHandler = .init(content: content, results: results)
|
||||
self.labelHandler = .init(content: content, results: results)
|
||||
self.audioPlayer = .init(content: content, results: results)
|
||||
self.inlineLink = .init(content: content, results: results, language: language)
|
||||
self.icons = .init(content: content, results: results)
|
||||
self.box = .init(content: content, results: results)
|
||||
self.html = .init(content: content, results: results)
|
||||
|
||||
self.inlineLink = .init(content: content, results: results, language: language)
|
||||
self.code = .init(results: results)
|
||||
}
|
||||
|
||||
func generatePage(from content: String) -> String {
|
||||
let parser = MarkdownParser(modifiers: [
|
||||
Modifier(target: .images, closure: processMarkdownImage),
|
||||
Modifier(target: .codeBlocks, closure: handleCode),
|
||||
Modifier(target: .links, closure: inlineLink.handleLink),
|
||||
Modifier(target: .html, closure: handleHTML),
|
||||
Modifier(target: .codeBlocks, closure: code.process),
|
||||
Modifier(target: .links, closure: inlineLink.process),
|
||||
Modifier(target: .html, closure: html.process),
|
||||
Modifier(target: .headings, closure: handleHeadlines)
|
||||
])
|
||||
return parser.html(from: content)
|
||||
}
|
||||
|
||||
private func handleCode(html: String, markdown: Substring) -> String {
|
||||
guard markdown.starts(with: "```swift") else {
|
||||
results.require(header: .codeHightlighting)
|
||||
results.require(footer: PageContentParser.codeHighlightFooter)
|
||||
return html // Just use normal code highlighting
|
||||
}
|
||||
// Highlight swift code using Splash
|
||||
let code = markdown.between("```swift", and: "```").trimmed
|
||||
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
|
||||
}
|
||||
|
||||
private func handleHTML(html: String, _: Substring) -> String {
|
||||
findResourcesInHtml(html: html)
|
||||
return html
|
||||
}
|
||||
|
||||
private func findResourcesInHtml(html: String) {
|
||||
findImages(in: html)
|
||||
findLinks(in: html)
|
||||
findSourceSets(in: html)
|
||||
}
|
||||
|
||||
private func findImages(in markdown: String) {
|
||||
do {
|
||||
// Parse the HTML string
|
||||
let document = try SwiftSoup.parse(markdown)
|
||||
|
||||
// Select all 'img' elements
|
||||
let imgElements = try document.select("img")
|
||||
|
||||
// Extract the 'src' attributes from each 'img' element
|
||||
let srcAttributes = try imgElements.array()
|
||||
.compactMap { try $0.attr("src") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
|
||||
for src in srcAttributes {
|
||||
results.warning("Found image in html: \(src)")
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func findLinks(in markdown: String) {
|
||||
do {
|
||||
// Parse the HTML string
|
||||
let document = try SwiftSoup.parse(markdown)
|
||||
|
||||
// Select all 'img' elements
|
||||
let linkElements = try document.select("a")
|
||||
|
||||
// Extract the 'src' attributes from each 'img' element
|
||||
let srcAttributes = try linkElements.array()
|
||||
.compactMap { try $0.attr("href").trimmed }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
for url in srcAttributes {
|
||||
if url.hasPrefix("http://") || url.hasPrefix("https://") {
|
||||
results.externalLink(to: url)
|
||||
} else {
|
||||
results.warning("Relative link in HTML: \(url)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func findSourceSets(in markdown: String) {
|
||||
do {
|
||||
// Parse the HTML string
|
||||
let document = try SwiftSoup.parse(markdown)
|
||||
|
||||
// Select all 'img' elements
|
||||
let linkElements = try document.select("source")
|
||||
|
||||
// Extract the 'src' attributes from each 'img' element
|
||||
let srcsetAttributes = try linkElements.array()
|
||||
.compactMap { try $0.attr("srcset") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
|
||||
for src in srcsetAttributes {
|
||||
results.warning("Found source set in html: \(src)")
|
||||
}
|
||||
|
||||
let srcAttributes = try linkElements.array()
|
||||
.compactMap { try $0.attr("src") }
|
||||
.filter { !$0.trimmed.isEmpty }
|
||||
|
||||
for src in srcAttributes {
|
||||
guard content.isValidIdForFile(src) else {
|
||||
results.warning("Found source in html: \(src)")
|
||||
continue
|
||||
}
|
||||
guard let file = content.file(src) else {
|
||||
results.warning("Found source in html: \(src)")
|
||||
continue
|
||||
}
|
||||
#warning("Either find files by their full path, or replace file id with full path")
|
||||
results.require(file: file)
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Modify headlines by extracting an id from the headline and adding it into the html element
|
||||
|
||||
@ -218,9 +120,9 @@ final class PageContentParser {
|
||||
case .pageLink:
|
||||
return handlePageLink(arguments, markdown: markdown)
|
||||
case .includedHtml:
|
||||
return handleExternalHtml(arguments, markdown: markdown)
|
||||
return self.html.process(arguments, markdown: markdown)
|
||||
case .box:
|
||||
return handleSimpleBox(arguments, markdown: markdown)
|
||||
return box.process(arguments, markdown: markdown)
|
||||
case .model:
|
||||
return handleModel(arguments, markdown: markdown)
|
||||
case .svg:
|
||||
@ -343,37 +245,6 @@ final class PageContentParser {
|
||||
return option
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleExternalHtml(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .includedHtml, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "External HTML command")
|
||||
return ""
|
||||
}
|
||||
let content = file.textContent()
|
||||
findResourcesInHtml(html: content)
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleSimpleBox(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count > 1 else {
|
||||
results.invalid(command: .box, markdown)
|
||||
return ""
|
||||
}
|
||||
let title = arguments[0]
|
||||
let text = arguments.dropFirst().joined(separator: ";")
|
||||
return ContentBox(title: title, text: text).content
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
|
@ -73,6 +73,8 @@ final class PageGenerationResults: ObservableObject {
|
||||
/// The files that could not be saved to the output folder
|
||||
private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:]
|
||||
|
||||
private(set) var pageIsEmpty: Bool
|
||||
|
||||
init(itemId: ItemId, delegate: GenerationResults) {
|
||||
self.itemId = itemId
|
||||
self.delegate = delegate
|
||||
@ -94,33 +96,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
invalidCommands = []
|
||||
warnings = []
|
||||
unsavedOutputFiles = [:]
|
||||
}
|
||||
|
||||
private init(other: PageGenerationResults) {
|
||||
self.itemId = other.itemId
|
||||
self.delegate = other.delegate
|
||||
inaccessibleFiles = other.inaccessibleFiles
|
||||
unparsableFiles = other.unparsableFiles
|
||||
missingFiles = other.missingFiles
|
||||
missingLinkedFiles = other.missingLinkedFiles
|
||||
missingLinkedTags = other.missingLinkedTags
|
||||
missingLinkedPages = other.missingLinkedPages
|
||||
requiredHeaders = other.requiredHeaders
|
||||
requiredFooters = other.requiredFooters
|
||||
requiredIcons = other.requiredIcons
|
||||
linkedPages = other.linkedPages
|
||||
linkedTags = other.linkedTags
|
||||
externalLinks = other.externalLinks
|
||||
usedFiles = other.usedFiles
|
||||
requiredFiles = other.requiredFiles
|
||||
imagesToGenerate = other.imagesToGenerate
|
||||
invalidCommands = other.invalidCommands
|
||||
warnings = other.warnings
|
||||
unsavedOutputFiles = other.unsavedOutputFiles
|
||||
}
|
||||
|
||||
func copy() -> PageGenerationResults {
|
||||
.init(other: self)
|
||||
pageIsEmpty = false
|
||||
}
|
||||
|
||||
// MARK: Adding entries
|
||||
@ -231,5 +207,11 @@ final class PageGenerationResults: ObservableObject {
|
||||
unsavedOutputFiles[path, default: []].insert(source)
|
||||
delegate.unsaved(path)
|
||||
}
|
||||
|
||||
func markPageAsEmpty() {
|
||||
guard case .page(let page) = itemId.itemType else { return }
|
||||
pageIsEmpty = true
|
||||
delegate.empty(.init(language: itemId.language, pageId: page.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,13 +19,32 @@ final class PageGenerator {
|
||||
return result
|
||||
}
|
||||
|
||||
private func makeEmptyPageContent(in language: ContentLanguage) -> String {
|
||||
switch language {
|
||||
case .english:
|
||||
return ContentBox(
|
||||
title: "Content not available",
|
||||
text: "This page is not available yet. Try the German version or check back later.")
|
||||
.content
|
||||
case .german:
|
||||
return ContentBox(
|
||||
title: "Inhalt nicht verfügbar",
|
||||
text: "Diese Seite ist noch nicht verfügbar. Versuche die englische Version oder komm später hierher zurück.")
|
||||
.content
|
||||
}
|
||||
}
|
||||
|
||||
func generate(page: Page, language: ContentLanguage, results: PageGenerationResults) -> String? {
|
||||
let contentGenerator = PageContentParser(
|
||||
content: content,
|
||||
language: language, results: results)
|
||||
|
||||
guard let rawPageContent = content.storage.pageContent(for: page.id, language: language) else {
|
||||
return nil
|
||||
let rawPageContent: String
|
||||
if let existing = content.storage.pageContent(for: page.id, language: language) {
|
||||
rawPageContent = existing
|
||||
} else {
|
||||
rawPageContent = makeEmptyPageContent(in: language)
|
||||
results.markPageAsEmpty()
|
||||
}
|
||||
|
||||
let pageContent = contentGenerator.generatePage(from: rawPageContent)
|
||||
@ -34,27 +53,41 @@ final class PageGenerator {
|
||||
|
||||
let tags: [FeedEntryData.Tag] = page.tags.map { tag in
|
||||
.init(name: tag.localized(in: language).name,
|
||||
url: content.absoluteUrlToTag(tag, language: language))
|
||||
url: tag.absoluteUrl(in: language))
|
||||
}
|
||||
|
||||
let headers = makeHeaders(requiredItems: results.requiredHeaders)
|
||||
results.require(files: headers.compactMap { $0.file })
|
||||
|
||||
let fullPage = ContentPage(
|
||||
language: language,
|
||||
dateString: page.dateText(in: language),
|
||||
title: localized.title,
|
||||
showTitle: !localized.hideTitle,
|
||||
tags: tags,
|
||||
linkTitle: localized.linkPreviewTitle ?? localized.title,
|
||||
description: localized.linkPreviewDescription ?? "",
|
||||
navigationBarLinks: content.navigationBar(in: language),
|
||||
pageContent: pageContent,
|
||||
headers: headers,
|
||||
footers: results.requiredFooters.sorted(),
|
||||
icons: results.requiredIcons)
|
||||
.content
|
||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
||||
let languageUrl = page.absoluteUrl(in: language.next)
|
||||
let languageButton = NavigationBar.Link(
|
||||
text: language.next.rawValue,
|
||||
url: languageUrl)
|
||||
|
||||
return fullPage
|
||||
let pageHeader = PageHeader(
|
||||
language: language,
|
||||
title: localized.linkPreviewTitle ?? localized.title,
|
||||
description: localized.linkPreviewDescription,
|
||||
iconUrl: iconUrl,
|
||||
languageButton: languageButton,
|
||||
links: content.navigationBar(in: language),
|
||||
headers: headers,
|
||||
icons: results.requiredIcons)
|
||||
|
||||
let fullPage = GenericPage(
|
||||
header: pageHeader,
|
||||
additionalFooter: results.requiredFooters.sorted().joined()) { content in
|
||||
content += "<article>"
|
||||
if !localized.hideTitle {
|
||||
content += "<h3>\(page.dateText(in: language))</h3>"
|
||||
content += "<h1>\(localized.title)</h1>"
|
||||
content += TagList(tags: tags).content
|
||||
}
|
||||
content += pageContent
|
||||
content += "</article>"
|
||||
}
|
||||
|
||||
return fullPage.content
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
|
||||
struct FeedGeneratorSource: PostListPageGeneratorSource {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
var showTitle: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
content.settings.localized(in: language).title
|
||||
}
|
||||
|
||||
var pageDescription: String {
|
||||
content.settings.localized(in: language).description
|
||||
}
|
||||
|
||||
func pageUrlPrefix(for language: ContentLanguage) -> String {
|
||||
content.settings.localized(in: language).feedUrlPrefix
|
||||
}
|
||||
}
|
@ -2,42 +2,26 @@ import Foundation
|
||||
|
||||
final class PostListPageGenerator {
|
||||
|
||||
private let language: ContentLanguage
|
||||
let source: PostListPageGeneratorSource
|
||||
|
||||
private let content: Content
|
||||
init(source: PostListPageGeneratorSource) {
|
||||
self.source = source
|
||||
}
|
||||
|
||||
private let results: PageGenerationResults
|
||||
|
||||
private let showTitle: Bool
|
||||
|
||||
private let pageTitle: String
|
||||
|
||||
private let pageDescription: String
|
||||
|
||||
/// The url of the page, excluding the extension
|
||||
private let pageUrlPrefix: String
|
||||
|
||||
init(language: ContentLanguage,
|
||||
content: Content,
|
||||
results: PageGenerationResults,
|
||||
showTitle: Bool, pageTitle: String,
|
||||
pageDescription: String,
|
||||
pageUrlPrefix: String) {
|
||||
self.language = language
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.showTitle = showTitle
|
||||
self.pageTitle = pageTitle
|
||||
self.pageDescription = pageDescription
|
||||
self.pageUrlPrefix = pageUrlPrefix
|
||||
private var language: ContentLanguage {
|
||||
source.language
|
||||
}
|
||||
|
||||
private var mainContentMaximumWidth: Int {
|
||||
content.settings.posts.contentWidth
|
||||
source.content.settings.posts.contentWidth
|
||||
}
|
||||
|
||||
private var postsPerPage: Int {
|
||||
content.settings.posts.postsPerPage
|
||||
source.content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
private func pageUrl(in language: ContentLanguage, pageNumber: Int) -> String {
|
||||
"\(source.pageUrlPrefix(for: language))/\(pageNumber).html"
|
||||
}
|
||||
|
||||
func createPages(for posts: [Post]) {
|
||||
@ -67,7 +51,7 @@ final class PostListPageGenerator {
|
||||
|
||||
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
|
||||
.init(name: tag.localized(in: language).name,
|
||||
url: content.absoluteUrlToTag(tag, language: language))
|
||||
url: tag.absoluteUrl(in: language))
|
||||
}
|
||||
|
||||
let images = localized.images.map(createFeedImage)
|
||||
@ -82,25 +66,28 @@ final class PostListPageGenerator {
|
||||
images: images)
|
||||
}
|
||||
|
||||
let feedPageGenerator = FeedPageGenerator(content: content)
|
||||
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
|
||||
|
||||
let languageButtonUrl = pageUrl(in: language.next, pageNumber: pageIndex)
|
||||
|
||||
let fileContent = feedPageGenerator.generatePage(
|
||||
language: language,
|
||||
posts: posts,
|
||||
title: pageTitle,
|
||||
description: pageDescription,
|
||||
showTitle: showTitle,
|
||||
title: source.pageTitle,
|
||||
description: source.pageDescription,
|
||||
showTitle: source.showTitle,
|
||||
pageNumber: pageIndex,
|
||||
totalPages: pageCount)
|
||||
let filePath = "\(pageUrlPrefix)/\(pageIndex).html"
|
||||
totalPages: pageCount,
|
||||
languageButtonUrl: languageButtonUrl)
|
||||
let filePath = pageUrl(in: language, pageNumber: pageIndex)
|
||||
guard save(fileContent, to: filePath) else {
|
||||
results.unsavedOutput(filePath, source: .feed)
|
||||
source.results.unsavedOutput(filePath, source: .feed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func createFeedImage(for image: FileResource) -> FeedEntryData.Image {
|
||||
results.requireImageSet(for: image, size: mainContentMaximumWidth)
|
||||
source.results.requireImageSet(for: image, size: mainContentMaximumWidth)
|
||||
return .init(
|
||||
rawImagePath: image.absoluteUrl,
|
||||
width: mainContentMaximumWidth,
|
||||
@ -109,6 +96,6 @@ final class PostListPageGenerator {
|
||||
}
|
||||
|
||||
private func save(_ content: String, to relativePath: String) -> Bool {
|
||||
self.content.storage.write(content, to: relativePath)
|
||||
source.content.storage.write(content, to: relativePath)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
|
||||
protocol PostListPageGeneratorSource {
|
||||
|
||||
var language: ContentLanguage { get }
|
||||
|
||||
var content: Content { get }
|
||||
|
||||
var results: PageGenerationResults { get }
|
||||
|
||||
var showTitle: Bool { get }
|
||||
|
||||
var pageTitle: String { get }
|
||||
|
||||
var pageDescription: String { get }
|
||||
|
||||
func pageUrlPrefix(for language: ContentLanguage) -> String
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
|
||||
struct TagPageGeneratorSource: PostListPageGeneratorSource {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let tag: Tag
|
||||
|
||||
var showTitle: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
tag.localized(in: language).name
|
||||
}
|
||||
|
||||
var pageDescription: String {
|
||||
tag.localized(in: language).description ?? ""
|
||||
}
|
||||
|
||||
func pageUrlPrefix(for language: ContentLanguage) -> String {
|
||||
tag.absoluteUrl(in: language)
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ enum ShorthandMarkdownKey: String {
|
||||
/// Format: ``
|
||||
case tagLink = "tag"
|
||||
|
||||
/// Additional HTML code include verbatim into the page.
|
||||
/// Additional HTML code included verbatim into the page.
|
||||
/// Format: ``
|
||||
case includedHtml = "html"
|
||||
|
||||
|
Reference in New Issue
Block a user