Improve relative paths, check missing files

This commit is contained in:
Christoph Hagen 2022-08-29 13:33:48 +02:00
parent 761845311e
commit 268c0e5f39
4 changed files with 103 additions and 15 deletions

View File

@ -198,8 +198,8 @@ struct Element {
} }
} }
// TODO: Propagate external files from the parent if subpath matches? // TODO: Propagate external files from the parent if subpath matches?
self.externalFiles = metadata.externalFiles ?? [] self.externalFiles = Element.rootPaths(for: metadata.externalFiles, path: path)
self.requiredFiles = Set((metadata.requiredFiles ?? []).map { path + "/" + $0 }) self.requiredFiles = Element.rootPaths(for: metadata.requiredFiles, path: path)
self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source) self.thumbnailStyle = log.thumbnailStyle(metadata.thumbnailStyle, source: source)
self.useManualSorting = metadata.useManualSorting ?? false self.useManualSorting = metadata.useManualSorting ?? false
self.overviewItemCount = metadata.overviewItemCount ?? parent.overviewItemCount 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 This function is used to copy required input files and to generate images
*/ */
func pathRelativeToRootForContainedInputFile(_ filePath: String) -> String { func pathRelativeToRootForContainedInputFile(_ filePath: String) -> String {
guard !filePath.hasSuffix("/") && !filePath.hasSuffix("http") else { Element.relativeToRoot(filePath: filePath, folder: path)
return filePath }
/**
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)" return "\(path)/\(filePath)"
} }
static func rootPaths(for input: Set<String>?, path: String) -> Set<String> {
input.unwrapped { Set($0.map { relativeToRoot(filePath: $0, folder: path) }) } ?? []
}
func relativePathToFileWithPath(_ filePath: String) -> String { func relativePathToFileWithPath(_ filePath: String) -> String {
guard path != "" else { guard path != "" else {
return filePath return filePath

View File

@ -21,11 +21,17 @@ extension String {
} }
func dropAfterLast(_ separator: String) -> 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 { 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 { func lastComponentAfter(_ separator: String) -> String {

View File

@ -5,7 +5,6 @@ import AppKit
typealias SourceFile = (data: Data, didChange: Bool) typealias SourceFile = (data: Data, didChange: Bool)
typealias SourceTextFile = (content: String, didChange: Bool) typealias SourceTextFile = (content: String, didChange: Bool)
#warning("Skip external files")
final class FileSystem { final class FileSystem {
private static let hashesFileName = "hashes.json" private static let hashesFileName = "hashes.json"
@ -39,6 +38,20 @@ final class FileSystem {
*/ */
private var requiredFiles: Set<String> = [] private var requiredFiles: Set<String> = []
/**
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<String> = []
/**
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. The image creation tasks.
@ -227,7 +240,6 @@ final class FileSystem {
return scaledSize return scaledSize
} }
#warning("Implement image functions")
func createImages() { func createImages() {
for (destination, image) in imageTasks.sorted(by: { $0.key < $1.key }) { for (destination, image) in imageTasks.sorted(by: { $0.key < $1.key }) {
createImageIfNeeded(image, for: destination) createImageIfNeeded(image, for: destination)
@ -322,12 +334,34 @@ final class FileSystem {
requiredFiles.insert(file) 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() { func copyRequiredFiles() {
var missingFiles = [String]()
for file in requiredFiles { 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 { guard sourceUrl.exists else {
missingFiles.append(file) if !externalFiles.contains(file) {
log.add(error: "Missing required file", source: cleanPath)
}
continue continue
} }
let data: Data let data: Data
@ -337,15 +371,40 @@ final class FileSystem {
log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error) log.add(error: "Failed to read data at \(sourceUrl.path)", source: source, error: error)
continue continue
} }
let destinationUrl = output.appendingPathComponent(file) writeIfChanged(data, to: destinationUrl)
write(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 // MARK: Writing files
@discardableResult @discardableResult
func write(_ data: Data, to url: URL) -> Bool { func writeIfChanged(_ data: Data, to url: URL) -> Bool {
// Only write changed files // Only write changed files
if url.exists, let oldContent = try? Data(contentsOf: url), data == oldContent { if url.exists, let oldContent = try? Data(contentsOf: url), data == oldContent {
return false return false
@ -362,7 +421,7 @@ final class FileSystem {
@discardableResult @discardableResult
func write(_ string: String, to url: URL) -> Bool { func write(_ string: String, to url: URL) -> Bool {
let data = string.data(using: .utf8)! let data = string.data(using: .utf8)!
return write(data, to: url) return writeIfChanged(data, to: url)
} }
} }

View File

@ -11,6 +11,7 @@ struct SiteGenerator {
func generate(site: Element) throws { func generate(site: Element) throws {
site.requiredFiles.forEach(files.require) site.requiredFiles.forEach(files.require)
site.externalFiles.forEach(files.exclude)
try site.languages.forEach { metadata in try site.languages.forEach { metadata in
let language = metadata.language let language = metadata.language
let template = try LocalizedSiteTemplate( let template = try LocalizedSiteTemplate(
@ -27,6 +28,7 @@ struct SiteGenerator {
elementsToProcess.append(contentsOf: element.elements) elementsToProcess.append(contentsOf: element.elements)
element.requiredFiles.forEach(files.require) element.requiredFiles.forEach(files.require)
element.externalFiles.forEach(files.exclude)
if !element.elements.isEmpty { if !element.elements.isEmpty {
overviewGenerator.generate( overviewGenerator.generate(