Compare commits

..

10 Commits

Author SHA1 Message Date
Christoph Hagen
a7e7bc21fc Allow whitespaces in svg size element 2022-09-04 17:48:13 +02:00
Christoph Hagen
aa701d9793 Make highlight script path relative 2022-09-04 17:47:35 +02:00
Christoph Hagen
cec60e9ff2 Make top bar link relative 2022-09-04 17:47:13 +02:00
Christoph Hagen
9a40da63d3 Allow absolute urls for link preview thumbnails 2022-09-04 17:45:44 +02:00
Christoph Hagen
9408b91741 Use overview generator for start page 2022-09-04 17:45:29 +02:00
Christoph Hagen
7f65065f72 Treat placeholder text as markdown 2022-09-02 23:19:30 +02:00
Christoph Hagen
d1c418af3e Improve overview of modified pages 2022-09-02 23:19:13 +02:00
Christoph Hagen
6a2d63462e Improve display of required files 2022-09-01 10:55:42 +02:00
Christoph Hagen
4dc56e5dfe Print draft pages 2022-08-31 09:02:40 +02:00
Christoph Hagen
1537aaab01 Hide language buttons again if page is empty 2022-08-31 08:46:23 +02:00
15 changed files with 133 additions and 95 deletions

View File

@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
E22E8763289D84C300E51191 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22E8762289D84C300E51191 /* main.swift */; };
E22E876C289D855D00E51191 /* ThumbnailStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22E876B289D855D00E51191 /* ThumbnailStyle.swift */; };
E22E8772289D8C2700E51191 /* IndexPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22E8771289D8C2700E51191 /* IndexPageGenerator.swift */; };
E22E877D289DBA0A00E51191 /* OverviewSectionGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22E877C289DBA0A00E51191 /* OverviewSectionGenerator.swift */; };
E22E878C289E4A8900E51191 /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = E22E878B289E4A8900E51191 /* Ink */; };
E22E8795289E81D700E51191 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22E8794289E81D700E51191 /* URL+Extensions.swift */; };
@ -72,7 +71,6 @@
E22E875F289D84C300E51191 /* WebsiteGenerator */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = WebsiteGenerator; sourceTree = BUILT_PRODUCTS_DIR; };
E22E8762289D84C300E51191 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
E22E876B289D855D00E51191 /* ThumbnailStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailStyle.swift; sourceTree = "<group>"; };
E22E8771289D8C2700E51191 /* IndexPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexPageGenerator.swift; sourceTree = "<group>"; };
E22E877C289DBA0A00E51191 /* OverviewSectionGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewSectionGenerator.swift; sourceTree = "<group>"; };
E22E8794289E81D700E51191 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
E22E879A289EE02F00E51191 /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
@ -180,7 +178,6 @@
E22E879F289F008200E51191 /* ThumbnailListGenerator.swift */,
E22E877C289DBA0A00E51191 /* OverviewSectionGenerator.swift */,
E22E87A3289F0C7000E51191 /* SiteGenerator.swift */,
E22E8771289D8C2700E51191 /* IndexPageGenerator.swift */,
E22E87A7289F0E7B00E51191 /* PageGenerator.swift */,
E22E879D289EFDFC00E51191 /* OverviewPageGenerator.swift */,
E2F8FA1D28A539C500632026 /* MarkdownProcessor.swift */,
@ -329,7 +326,6 @@
buildActionMask = 2147483647;
files = (
E2C5A5D728A022C500102A25 /* TemplateFactory.swift in Sources */,
E22E8772289D8C2700E51191 /* IndexPageGenerator.swift in Sources */,
E2F8FA3A28AE313A00632026 /* ValidationLog.swift in Sources */,
E253C88528BA32FB0076B6D0 /* HTMLElementsGenerator.swift in Sources */,
E2C5A5D528A0223C00102A25 /* HeaderTemplate.swift in Sources */,

View File

@ -304,15 +304,15 @@ extension Element {
}
/**
Create a relative link to another page in the tree.
- Parameter pageUrl: The full page url of the target page, including localization
- Returns: The relative url from a localized page of the element to the target page.
Create a relative link to another file in the tree.
- Parameter file: The full path of the target file, including localization
- Returns: The relative url from a localized page of the element to the target file.
*/
func relativePathToOtherSiteElement(pageUrl: String) -> String {
func relativePathToOtherSiteElement(file: String) -> String {
// Note: The element `path` is missing the last component
// i.e. travel/alps instead of travel/alps/en.html
let ownParts = path.components(separatedBy: "/")
let pageParts = pageUrl.components(separatedBy: "/")
let pageParts = file.components(separatedBy: "/")
// Find the common elements of the path, which can be discarded
var index = 0
@ -326,6 +326,17 @@ extension Element {
return allParts.joined(separator: "/")
}
/**
The relative path to the site root.
*/
var pathToRoot: String? {
guard path != "" else {
return nil
}
let downPathCount = path.components(separatedBy: "/").count
return [String](repeating: "..", count: downPathCount).joined(separator: "/")
}
/**
Create an absolute path (relative to the root directory) for a file contained in the elements folder.
@ -473,7 +484,7 @@ extension Element {
*/
private func existingContentUrl(for language: String) -> URL? {
let url = contentUrl(for: language)
guard url.exists else {
guard url.exists, let size = url.size, size > 0 else {
return nil
}
return url

View File

@ -35,4 +35,9 @@ extension URL {
try url.ensureParentFolderExistence()
try FileManager.default.copyItem(at: self, to: url)
}
var size: Int? {
let attributes = try? FileManager.default.attributesOfItem(atPath: path)
return (attributes?[.size] as? NSNumber)?.intValue
}
}

View File

@ -57,6 +57,11 @@ final class FileSystem {
*/
private var emptyPages: Set<String> = []
/**
All pages which have `status` set to ``PageState.draft``
*/
private var draftPages: Set<String> = []
/**
All paths to page element folders, indexed by their unique id.
@ -71,6 +76,11 @@ final class FileSystem {
*/
private var imageTasks: [String : ImageOutput] = [:]
/**
The paths to all pages which were changed
*/
private var generatedPages: Set<String> = []
init(in input: URL, to output: URL) {
self.input = input
self.output = output
@ -370,6 +380,7 @@ final class FileSystem {
}
func copyRequiredFiles() {
var copiedFiles = Set<String>()
for file in requiredFiles {
let cleanPath = cleanRelativeURL(file)
let sourceUrl = input.appendingPathComponent(cleanPath)
@ -387,7 +398,9 @@ final class FileSystem {
log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error)
continue
}
writeIfChanged(data, to: destinationUrl)
if writeIfChanged(data, to: destinationUrl) {
copiedFiles.insert(file)
}
}
for (file, source) in expectedFiles {
guard !externalFiles.contains(file) else {
@ -399,6 +412,14 @@ final class FileSystem {
log.add(error: "Missing \(cleanPath)", source: source)
}
}
guard !copiedFiles.isEmpty else {
print("No required files copied")
return
}
print("\(copiedFiles.count) required files copied:")
for file in copiedFiles.sorted() {
print(" " + file)
}
}
private func cleanRelativeURL(_ raw: String) -> String {
@ -427,9 +448,23 @@ final class FileSystem {
guard !emptyPages.isEmpty else {
return
}
log.add(info: "\(emptyPages.count) empty pages:", source: "Files")
print("\(emptyPages.count) empty pages:")
for page in emptyPages.sorted() {
log.add(info: "\(page) has no content", source: "Files")
print(" " + page)
}
}
func isDraft(path: String) {
draftPages.insert(path)
}
func printDraftPages() {
guard !draftPages.isEmpty else {
return
}
print("\(draftPages.count) drafts:")
for page in draftPages.sorted() {
print(" " + page)
}
}
@ -444,6 +479,21 @@ final class FileSystem {
pagePaths[id]
}
func generated(page: String) {
generatedPages.insert(page)
}
func printGeneratedPages() {
guard !generatedPages.isEmpty else {
print("No pages modified")
return
}
print("\(generatedPages.count) pages modified")
for page in generatedPages.sorted() {
print(" " + page)
}
}
// MARK: Writing files

View File

@ -106,8 +106,12 @@ final class ValidationLog {
func linkPreviewThumbnail(customFile: String?, for language: String, in folder: URL, source: String) -> String? {
if let customFile = customFile {
#warning("Allow absolute urls for link preview thumbnails")
let customFileUrl = folder.appendingPathComponent(customFile)
let customFileUrl: URL
if customFile.starts(with: "/") {
customFileUrl = URL(fileURLWithPath: customFile)
} else {
customFileUrl = folder.appendingPathComponent(customFile)
}
guard customFileUrl.exists else {
missing(customFile, requiredBy: "property 'linkPreviewImage' in metadata of \(source)")
return nil

View File

@ -10,9 +10,11 @@ struct HTMLElementsGenerator {
"\(title)<span class=\"suffix\">\(suffix)</span>"
}
// - TODO: Make link relative
func topBarWebsiteTitle(language: String) -> String {
Element.htmlPagePathAddition(for: language)
func topBarWebsiteTitle(language: String, from page: Element) -> String {
guard let pathToRoot = page.pathToRoot else {
return Element.htmlPageName(for: language)
}
return pathToRoot + Element.htmlPagePathAddition(for: language)
}
func topBarLanguageButton(_ language: String) -> String {

View File

@ -1,39 +0,0 @@
import Foundation
struct IndexPageGenerator {
private let factory: LocalizedSiteTemplate
init(factory: LocalizedSiteTemplate) {
self.factory = factory
}
func generate(site: Element, language: String) {
let localized = site.localized(for: language)
let path = site.localizedPath(for: language)
let pageUrl = files.urlInOutputFolder(path)
let languageButton = site.nextLanguage(for: language)
let sectionItemCount = site.overviewItemCount
var content = [PageTemplate.Key : String]()
content[.head] = factory.pageHead.generate(page: site, language: language)
content[.topBar] = factory.topBar.generate(sectionUrl: nil, languageButton: languageButton)
content[.contentClass] = "overview"
content[.header] = makeHeader(page: site, metadata: localized, language: language)
content[.content] = factory.overviewSection.generate(
sections: site.sortedItems,
in: site,
language: language,
sectionItemCount: sectionItemCount)
content[.footer] = site.customFooterContent()
guard factory.page.generate(content, to: pageUrl) else {
return
}
log.add(info: "Page generated", source: path)
}
private func makeHeader(page: Element, metadata: Element.LocalizedMetadata, language: String) -> String {
let content = factory.makeHeaderContent(page: page, metadata: metadata, language: language)
return factory.factory.centeredHeader.generate(content)
}
}

View File

@ -48,7 +48,7 @@ struct PageContentGenerator {
}
let fullPath = pagePath + Element.htmlPagePathAddition(for: language)
// Adjust file path to get the page url
let url = page.relativePathToOtherSiteElement(pageUrl: fullPath)
let url = page.relativePathToOtherSiteElement(file: fullPath)
return html.replacingOccurrences(of: file, with: url)
}
@ -75,8 +75,8 @@ struct PageContentGenerator {
// For images: ![left_title](file right_title)
// For videos: ![option1,option2,...](file)
// For svg with custom area: ![x,y,width,height](file.svg)
// For downloads: ![download](file1,text1;file2,text2, ...)
// For files: ?
// For downloads: ![download](file1, text1; file2, text2, ...)
// External pages: ![external](url1, text1; url2, text2, ...)
let fileAndTitle = markdown.between("(", and: ")")
let alt = markdown.between("[", and: "]").nonEmpty
if alt == "download" {
@ -151,10 +151,10 @@ struct PageContentGenerator {
}
let parts = area.components(separatedBy: ",").map { $0.trimmed }
guard parts.count == 4,
let x = Int(parts[0]),
let y = Int(parts[1]),
let width = Int(parts[2]),
let height = Int(parts[3]) else {
let x = Int(parts[0].trimmed),
let y = Int(parts[1].trimmed),
let width = Int(parts[2].trimmed),
let height = Int(parts[3].trimmed) else {
log.add(warning: "Invalid area string for svg image", source: page.path)
return factory.html.svgImage(file: file)
}

View File

@ -21,7 +21,8 @@ struct OverviewPageGenerator {
let languageButton = section.nextLanguage(for: language)
content[.topBar] = factory.topBar.generate(
sectionUrl: section.sectionUrl(for: language),
languageButton: languageButton)
languageButton: languageButton,
page: section)
content[.contentClass] = "overview"
content[.header] = makeHeader(page: section, metadata: metadata, language: language)
content[.content] = makeContent(section: section, language: language)
@ -29,7 +30,7 @@ struct OverviewPageGenerator {
guard factory.page.generate(content, to: url) else {
return
}
log.add(info: "Page generated", source: path)
files.generated(page: path)
}
private func makeContent(section: Element, language: String) -> String {

View File

@ -24,19 +24,19 @@ struct PageGenerator {
let inputContentPath = page.path + "/\(language).md"
let metadata = page.localized(for: language)
let nextLanguage = page.nextLanguage(for: language)
let pageContent = makeContent(page: page, language: language, path: inputContentPath)
let pageIncludesCode = pageContent?.includesCode ?? false
let (pageContent, pageIncludesCode, pageIsEmpty) = makeContent(
page: page, metadata: metadata, language: language, path: inputContentPath)
var content = [PageTemplate.Key : String]()
content[.head] = factory.pageHead.generate(page: page, language: language, includesCode: pageIncludesCode)
let sectionUrl = page.sectionUrl(for: language)
content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage)
content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage, page: page)
content[.contentClass] = "content"
if !page.useCustomHeader {
content[.header] = makeHeader(page: page, metadata: metadata, language: language)
}
content[.content] = pageContent?.content ?? factory.makePlaceholder(metadata: metadata)
content[.content] = pageContent
content[.previousPageLinkText] = previousPage.unwrapped { factory.factory.html.makePrevText($0.text) }
content[.previousPageUrl] = previousPage?.link
content[.nextPageLinkText] = nextPage.unwrapped { factory.factory.html.makeNextText($0.text) }
@ -49,22 +49,29 @@ struct PageGenerator {
}
let url = files.urlInOutputFolder(path)
if pageContent == nil {
if page.state == .draft {
files.isDraft(path: page.path)
} else if pageIsEmpty, page.state != .hidden {
files.isEmpty(page: path)
}
guard factory.page.generate(content, to: url) else {
return
}
log.add(info: "Page generated", source: path)
files.generated(page: path)
}
private func makeContent(page: Element, language: String, path: String) -> (content: String, includesCode: Bool)? {
guard let content = files.contentOfOptionalFile(atPath: path, source: page.path, createEmptyFileIfMissing: true),
content.trimmed != "" else {
return nil
private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, includesCode: Bool, isEmpty: Bool) {
if let raw = files.contentOfOptionalFile(atPath: path, source: page.path, createEmptyFileIfMissing: true)?
.trimmed.nonEmpty {
let (content, includesCode) = PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: raw)
return (content, includesCode, false)
} else {
let (content, includesCode) = PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: metadata.placeholderText)
let placeholder = factory.makePlaceholder(title: metadata.placeholderTitle, text: content)
return (placeholder, includesCode, true)
}
return PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: content)
}
private func makeHeader(page: Element, metadata: Element.LocalizedMetadata, language: String) -> String {

View File

@ -32,9 +32,9 @@ struct PageHeadGenerator {
}
content[.customPageContent] = page.customHeadContent()
if includesCode {
let scriptPath = "/assets/js/highlight.js"
#warning("Make highlight script path relative")
let includeText = factory.html.scriptInclude(path: scriptPath)
let scriptPath = "assets/js/highlight.js"
let relative = page.relativePathToOtherSiteElement(file: scriptPath)
let includeText = factory.html.scriptInclude(path: relative)
if let head = content[.customPageContent] {
content[.customPageContent] = head + "\n" + includeText
} else {

View File

@ -27,7 +27,8 @@ struct SiteGenerator {
// Generate sections
let overviewGenerator = OverviewPageGenerator(factory: template)
let pageGenerator = PageGenerator(factory: template)
var elementsToProcess: [Element] = site.elements
var elementsToProcess: [Element] = [site]
while let element = elementsToProcess.popLast() {
// Move recursively down to all pages
elementsToProcess.append(contentsOf: element.elements)
@ -46,10 +47,5 @@ struct SiteGenerator {
previousPage: nil)
}
}
let generator = IndexPageGenerator(factory: template)
// Generate front page
generator.generate(site: site, language: language)
}
}

View File

@ -75,9 +75,13 @@ struct LocalizedSiteTemplate {
// 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: metadata.placeholderTitle,
.text: metadata.placeholderText])
.title: title,
.text: text])
}
func makeBackLink(text: String, language: String) -> String {

View File

@ -17,10 +17,10 @@ struct PrefilledTopBarTemplate {
self.topBarWebsiteTitle = topBarWebsiteTitle
}
func generate(sectionUrl: String?, languageButton: String?) -> String {
func generate(sectionUrl: String?, languageButton: String?, page: Element) -> String {
var content = [TopBarTemplate.Key : String]()
content[.title] = topBarWebsiteTitle
content[.titleLink] = factory.html.topBarWebsiteTitle(language: language)
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)

View File

@ -22,10 +22,11 @@ do {
private let siteGenerator = try SiteGenerator()
try siteGenerator.generate(site: siteData)
print("Pages generated")
files.createImages()
files.printGeneratedPages()
files.printEmptyPages()
files.printDraftPages()
files.createImages()
print("Images generated")
files.copyRequiredFiles()
print("Required files copied")
files.writeHashes()