From 268c0e5f39af8dc29febdc61d67f74238c29f76b Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Mon, 29 Aug 2022 13:33:48 +0200 Subject: [PATCH] Improve relative paths, check missing files --- WebsiteGenerator/Content/Element.swift | 29 ++++++- .../Extensions/String+Extensions.swift | 10 ++- WebsiteGenerator/Files/FileSystem.swift | 77 ++++++++++++++++--- .../Generators/SiteGenerator.swift | 2 + 4 files changed, 103 insertions(+), 15 deletions(-) diff --git a/WebsiteGenerator/Content/Element.swift b/WebsiteGenerator/Content/Element.swift index 9d30f79..9c3e980 100644 --- a/WebsiteGenerator/Content/Element.swift +++ b/WebsiteGenerator/Content/Element.swift @@ -198,8 +198,8 @@ struct Element { } } // TODO: Propagate external files from the parent if subpath matches? - self.externalFiles = metadata.externalFiles ?? [] - self.requiredFiles = Set((metadata.requiredFiles ?? []).map { path + "/" + $0 }) + self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path) + self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path) self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source) self.useManualSorting = metadata.useManualSorting ?? false self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount @@ -268,12 +268,33 @@ extension Element { This function is used to copy required input files and to generate images */ func pathRelativeToRootForContainedInputFile(_ filePath: String) -> String { - guard !filePath.hasSuffix("/") && !filePath.hasSuffix("http") else { - return filePath + Element.relativeToRoot(filePath: filePath, folder: path) + } + + /** + Create an absolute path (relative to the root directory) for a file contained in the elements folder. + + This function is used to copy required input files and to generate images + */ + func nonAbsolutePathRelativeToRootForContainedInputFile(_ filePath: String) -> String? { + Element.containedFileRelativeToRoot(filePath: filePath, folder: path) + } + + static func relativeToRoot(filePath: String, folder path: String) -> String { + containedFileRelativeToRoot(filePath: filePath, folder: path) ?? filePath + } + + static func containedFileRelativeToRoot(filePath: String, folder path: String) -> String? { + if filePath.hasPrefix("/") || filePath.hasPrefix("http") || filePath.hasPrefix("mailto:") { + return nil } return "\(path)/\(filePath)" } + static func rootPaths(for input: Set?, path: String) -> Set { + input.unwrapped { Set($0.map { relativeToRoot(filePath: $0, folder: path) }) } ?? [] + } + func relativePathToFileWithPath(_ filePath: String) -> String { guard path != "" else { return filePath diff --git a/WebsiteGenerator/Extensions/String+Extensions.swift b/WebsiteGenerator/Extensions/String+Extensions.swift index 6b5dd4e..321286b 100644 --- a/WebsiteGenerator/Extensions/String+Extensions.swift +++ b/WebsiteGenerator/Extensions/String+Extensions.swift @@ -21,11 +21,17 @@ extension String { } func dropAfterLast(_ separator: String) -> String { - components(separatedBy: separator).dropLast().joined(separator: separator) + guard contains(separator) else { + return self + } + return components(separatedBy: separator).dropLast().joined(separator: separator) } func dropBeforeFirst(_ separator: String) -> String { - components(separatedBy: separator).dropFirst().joined(separator: separator) + guard contains(separator) else { + return self + } + return components(separatedBy: separator).dropFirst().joined(separator: separator) } func lastComponentAfter(_ separator: String) -> String { diff --git a/WebsiteGenerator/Files/FileSystem.swift b/WebsiteGenerator/Files/FileSystem.swift index e4cddb5..4f9a957 100644 --- a/WebsiteGenerator/Files/FileSystem.swift +++ b/WebsiteGenerator/Files/FileSystem.swift @@ -5,7 +5,6 @@ import AppKit typealias SourceFile = (data: Data, didChange: Bool) typealias SourceTextFile = (content: String, didChange: Bool) -#warning("Skip external files") final class FileSystem { private static let hashesFileName = "hashes.json" @@ -39,6 +38,20 @@ final class FileSystem { */ private var requiredFiles: Set = [] + /** + The files marked as external in element metadata. + + Files included here are not generated, since they are assumed to be added separately. + */ + private var externalFiles: Set = [] + + /** + The files marked as expected, i.e. they exist after the generation is completed. + + The key of the dictionary is the file path, the value is the file providing the link + */ + private var expectedFiles: [String : String] = [:] + /** The image creation tasks. @@ -227,7 +240,6 @@ final class FileSystem { return scaledSize } - #warning("Implement image functions") func createImages() { for (destination, image) in imageTasks.sorted(by: { $0.key < $1.key }) { createImageIfNeeded(image, for: destination) @@ -322,12 +334,34 @@ final class FileSystem { requiredFiles.insert(file) } + /** + Mark a file as explicitly missing. + + This is done for the `externalFiles` entries in metadata, + to indicate that these files will be copied to the output folder manually. + */ + func exclude(file: String) { + externalFiles.insert(file) + } + + /** + Mark a file as expected to be present in the output folder after generation. + + This is done for all links between pages, which only exist after the pages have been generated. + */ + func expect(file: String, source: String) { + expectedFiles[file] = source + } + func copyRequiredFiles() { - var missingFiles = [String]() for file in requiredFiles { - let sourceUrl = input.appendingPathComponent(file) + let cleanPath = cleanRelativeURL(file) + let sourceUrl = input.appendingPathComponent(cleanPath) + let destinationUrl = output.appendingPathComponent(cleanPath) guard sourceUrl.exists else { - missingFiles.append(file) + if !externalFiles.contains(file) { + log.add(error: "Missing required file", source: cleanPath) + } continue } let data: Data @@ -337,15 +371,40 @@ final class FileSystem { log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error) continue } - let destinationUrl = output.appendingPathComponent(file) - write(data, to: destinationUrl) + writeIfChanged(data, to: destinationUrl) } + for (file, source) in expectedFiles { + guard !externalFiles.contains(file) else { + continue + } + let cleanPath = cleanRelativeURL(file) + let destinationUrl = output.appendingPathComponent(cleanPath) + if !destinationUrl.exists { + log.add(error: "Missing \(cleanPath)", source: source) + } + } + } + + private func cleanRelativeURL(_ raw: String) -> String { + let raw = raw.dropAfterLast("#") // Clean links to page content + guard raw.contains("..") else { + return raw + } + var result: [String] = [] + for component in raw.components(separatedBy: "/") { + if component == ".." { + _ = result.popLast() + } else { + result.append(component) + } + } + return result.joined(separator: "/") } // MARK: Writing files @discardableResult - func write(_ data: Data, to url: URL) -> Bool { + func writeIfChanged(_ data: Data, to url: URL) -> Bool { // Only write changed files if url.exists, let oldContent = try? Data(contentsOf: url), data == oldContent { return false @@ -362,7 +421,7 @@ final class FileSystem { @discardableResult func write(_ string: String, to url: URL) -> Bool { let data = string.data(using: .utf8)! - return write(data, to: url) + return writeIfChanged(data, to: url) } } diff --git a/WebsiteGenerator/Generators/SiteGenerator.swift b/WebsiteGenerator/Generators/SiteGenerator.swift index f98e2b6..6d25c81 100644 --- a/WebsiteGenerator/Generators/SiteGenerator.swift +++ b/WebsiteGenerator/Generators/SiteGenerator.swift @@ -11,6 +11,7 @@ struct SiteGenerator { func generate(site: Element) throws { site.requiredFiles.forEach(files.require) + site.externalFiles.forEach(files.exclude) try site.languages.forEach { metadata in let language = metadata.language let template = try LocalizedSiteTemplate( @@ -27,6 +28,7 @@ struct SiteGenerator { elementsToProcess.append(contentsOf: element.elements) element.requiredFiles.forEach(files.require) + element.externalFiles.forEach(files.exclude) if !element.elements.isEmpty { overviewGenerator.generate(