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?
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<String>?, path: String) -> Set<String> {
input.unwrapped { Set($0.map { relativeToRoot(filePath: $0, folder: path) }) } ?? []
}
func relativePathToFileWithPath(_ filePath: String) -> String {
guard path != "" else {
return filePath

View File

@ -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 {

View File

@ -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<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.
@ -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)
}
}

View File

@ -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(