Compare commits
10 Commits
268ab205b5
...
a7e7bc21fc
Author | SHA1 | Date | |
---|---|---|---|
|
a7e7bc21fc | ||
|
aa701d9793 | ||
|
cec60e9ff2 | ||
|
9a40da63d3 | ||
|
9408b91741 | ||
|
7f65065f72 | ||
|
d1c418af3e | ||
|
6a2d63462e | ||
|
4dc56e5dfe | ||
|
1537aaab01 |
@ -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 */,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ struct PageContentGenerator {
|
||||
// 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: ?
|
||||
// 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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user