Compare commits

..

7 Commits

Author SHA1 Message Date
Christoph Hagen
6c21d8c857 Add typed shorthand markdown commands 2022-09-18 17:49:50 +02:00
Christoph Hagen
53500c31f6 Add convenience box command 2022-09-18 17:21:57 +02:00
Christoph Hagen
396e03279f Improve image generation prints 2022-09-18 16:48:15 +02:00
Christoph Hagen
3872a3e419 Add path parameter to image generation 2022-09-18 16:47:13 +02:00
Christoph Hagen
763b90f689 Extract Digest extension to separate file 2022-09-18 16:45:34 +02:00
Christoph Hagen
b47c551160 Remove unnecessary aliases 2022-09-16 15:33:14 +02:00
Christoph Hagen
c727bdf91e Scale images to integer heights 2022-09-16 15:32:55 +02:00
15 changed files with 454 additions and 257 deletions

View File

@ -0,0 +1,13 @@
import Foundation
import CryptoKit
extension Digest {
var bytes: [UInt8] { Array(makeIterator()) }
var data: Data { Data(bytes) }
var hexStr: String {
bytes.map { String(format: "%02X", $0) }.joined()
}
}

View File

@ -12,7 +12,7 @@ extension NSSize {
return self return self
} }
let height = height * desiredWidth / width let height = (height * desiredWidth / width).rounded(.down)
return NSSize(width: desiredWidth, height: height) return NSSize(width: desiredWidth, height: height)
} }
} }

View File

@ -2,43 +2,22 @@ import Foundation
import CryptoKit import CryptoKit
import AppKit import AppKit
typealias SourceFile = (data: Data, didChange: Bool)
typealias SourceTextFile = (content: String, didChange: Bool)
final class FileSystem { final class FileSystem {
private static let tempFileName = "temp.bin" private static let tempFileName = "temp.bin"
private static let hashesFileName = "hashes.json"
private let input: URL private let input: URL
private let output: URL private let output: URL
private let source = "FileChangeMonitor" private let source = "FileSystem"
private var hashesFile: URL { private let images: ImageGenerator
input.appendingPathComponent(FileSystem.hashesFileName)
}
private var tempFile: URL { private var tempFile: URL {
input.appendingPathComponent(FileSystem.tempFileName) input.appendingPathComponent(FileSystem.tempFileName)
} }
/**
The hashes of all accessed files from the previous run
The key is the relative path to the file from the source
*/
private var previousFiles: [String : Data] = [:]
/**
The paths of all files which were accessed, with their new hashes
This list is used to check if a file was modified, and to write all accessed files back to disk
*/
private var accessedFiles: [String : Data] = [:]
/** /**
All files which should be copied to the output folder All files which should be copied to the output folder
*/ */
@ -90,30 +69,8 @@ final class FileSystem {
init(in input: URL, to output: URL) { init(in input: URL, to output: URL) {
self.input = input self.input = input
self.output = output self.output = output
self.images = .init(input: input, output: output)
guard exists(hashesFile) else {
log.add(info: "No file hashes loaded, regarding all content as new", source: source)
return
}
let data: Data
do {
data = try Data(contentsOf: hashesFile)
} catch {
log.add(
warning: "File hashes could not be read, regarding all content as new",
source: source,
error: error)
return
}
do {
self.previousFiles = try JSONDecoder().decode(from: data)
} catch {
log.add(
warning: "File hashes could not be decoded, regarding all content as new",
source: source,
error: error)
return
}
} }
func urlInOutputFolder(_ path: String) -> URL { func urlInOutputFolder(_ path: String) -> URL {
@ -124,15 +81,6 @@ final class FileSystem {
input.appendingPathComponent(path) input.appendingPathComponent(path)
} }
/**
Get the current hash of file data at a path.
If the hash has been computed previously during the current run, then this function directly returns it.
*/
private func hash(_ data: Data, at path: String) -> Data {
accessedFiles[path] ?? SHA256.hash(data: data).data
}
private func exists(_ url: URL) -> Bool { private func exists(_ url: URL) -> Bool {
FileManager.default.fileExists(atPath: url.path) FileManager.default.fileExists(atPath: url.path)
} }
@ -183,178 +131,19 @@ final class FileSystem {
} }
} }
private func getData(atPath path: String) -> SourceFile? { func writeDetectedFileChangesToDisk() {
let url = input.appendingPathComponent(path) images.writeDetectedFileChangesToDisk()
guard exists(url) else {
return nil
}
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
log.add(error: "Failed to read data at \(path)", source: source, error: error)
return nil
}
let newHash = hash(data, at: path)
defer {
accessedFiles[path] = newHash
}
guard let oldHash = previousFiles[path] else {
return (data: data, didChange: true)
}
return (data: data, didChange: oldHash != newHash)
} }
func writeHashes() {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(accessedFiles)
try data.write(to: hashesFile)
} catch {
log.add(warning: "Failed to save file hashes", source: source, error: error)
}
}
// MARK: Images // MARK: Images
private func loadImage(atPath path: String) -> (image: NSImage, changed: Bool)? {
guard let (data, changed) = getData(atPath: path) else {
log.add(error: "Failed to load file", source: path)
return nil
}
guard let image = NSImage(data: data) else {
log.add(error: "Failed to read image", source: path)
return nil
}
return (image, changed)
}
@discardableResult @discardableResult
func requireImage(source: String, destination: String, width: Int, desiredHeight: Int? = nil) -> NSSize { func requireImage(source: String, destination: String, requiredBy path: String, width: Int, desiredHeight: Int? = nil) -> NSSize {
let height = desiredHeight.unwrapped(CGFloat.init) images.requireImage(at: destination, generatedFrom: source, requiredBy: path, width: width, height: desiredHeight)
let sourceUrl = input.appendingPathComponent(source)
let image = ImageOutput(source: source, width: width, desiredHeight: desiredHeight)
let standardSize = NSSize(width: CGFloat(width), height: height ?? CGFloat(width) / 16 * 9)
guard sourceUrl.exists else {
log.add(error: "Missing file with size (\(width),\(desiredHeight ?? -1))",
source: source)
return standardSize
}
guard let imageSize = loadImage(atPath: image.source)?.image.size else {
log.add(error: "Unreadable image with size (\(width),\(desiredHeight ?? -1))",
source: source)
return standardSize
}
let scaledSize = imageSize.scaledDown(to: CGFloat(width))
guard let existing = imageTasks[destination] else {
imageTasks[destination] = image
return scaledSize
}
guard existing.source == source else {
log.add(error: "Multiple sources (\(existing.source),\(source))",
source: destination)
return scaledSize
}
guard existing.hasSimilarRatio(as: image) else {
log.add(error: "Multiple ratios (\(existing.ratio!),\(image.ratio!))",
source: destination)
return scaledSize
}
if image.width > existing.width {
log.add(info: "Increasing size from \(existing.width) to \(width)",
source: destination)
imageTasks[destination] = image
}
return scaledSize
} }
func createImages() { func createImages() {
for (destination, image) in imageTasks.sorted(by: { $0.key < $1.key }) { images.createImages()
createImageIfNeeded(image, for: destination)
}
}
private func createImageIfNeeded(_ image: ImageOutput, for destination: String) {
guard let (sourceImageData, sourceImageChanged) = getData(atPath: image.source) else {
log.add(error: "Failed to open file", source: image.source)
return
}
let destinationUrl = output.appendingPathComponent(destination)
// Check if image needs to be updated
guard !destinationUrl.exists || sourceImageChanged else {
return
}
// Ensure that image file is supported
let ext = destinationUrl.pathExtension.lowercased()
guard ImageType(fileExtension: ext) != nil else {
// TODO: This should never be reached, since extensions are checked before
log.add(info: "Copying image", source: image.source)
do {
let sourceUrl = input.appendingPathComponent(image.source)
try destinationUrl.ensureParentFolderExistence()
try sourceUrl.copy(to: destinationUrl)
} catch {
log.add(error: "Failed to copy image", source: destination)
}
return
}
guard let sourceImage = NSImage(data: sourceImageData) else {
log.add(error: "Failed to read file", source: image.source)
return
}
let desiredWidth = CGFloat(image.width)
let desiredHeight = image.desiredHeight.unwrapped(CGFloat.init)
let destinationSize = sourceImage.size.scaledDown(to: desiredWidth)
let scaledImage = sourceImage.scaledDown(to: destinationSize)
let scaledSize = scaledImage.size
if abs(scaledImage.size.width - desiredWidth) > 2 {
log.add(warning: "Desired width \(desiredWidth), got \(scaledSize.width)", source: destination)
}
if abs(destinationSize.height - scaledImage.size.height) > 2 {
log.add(warning: "Desired height \(destinationSize.height), got \(scaledSize.height)", source: destination)
}
if let desiredHeight = desiredHeight {
let desiredRatio = desiredHeight / desiredWidth
let adjustedDesiredHeight = scaledSize.width * desiredRatio
if abs(adjustedDesiredHeight - scaledSize.height) > 5 {
log.add(warning: "Desired height \(desiredHeight), got \(scaledSize.height)", source: destination)
return
}
}
if scaledSize.width > desiredWidth {
log.add(warning:" Desired width \(desiredWidth), got \(scaledSize.width)", source: destination)
}
let destinationExtension = destinationUrl.pathExtension.lowercased()
guard let type = ImageType(fileExtension: destinationExtension)?.fileType else {
log.add(error: "No image type for extension \(destinationExtension)",
source: destination)
return
}
guard let tiff = scaledImage.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) else {
log.add(error: "Failed to get data", source: image.source)
return
}
guard let data = tiffData.representation(using: type, properties: [.compressionFactor: NSNumber(0.7)]) else {
log.add(error: "Failed to get data", source: image.source)
return
}
do {
try data.createFolderAndWrite(to: destinationUrl)
} catch {
log.add(error: "Failed to write image \(destination)", source: "Image Processor", error: error)
return
}
} }
// MARK: File copying // MARK: File copying
@ -635,14 +424,3 @@ final class FileSystem {
return output return output
} }
} }
private extension Digest {
var bytes: [UInt8] { Array(makeIterator()) }
var data: Data { Data(bytes) }
var hexStr: String {
bytes.map { String(format: "%02X", $0) }.joined()
}
}

View File

@ -0,0 +1,88 @@
import Foundation
import CryptoKit
final class FileUpdateChecker {
private static let hashesFileName = "hashes.json"
private let input: URL
private var hashesFile: URL {
input.appendingPathComponent(FileUpdateChecker.hashesFileName)
}
/**
The hashes of all accessed files from the previous run
The key is the relative path to the file from the source
*/
private var previousFiles: [String : Data] = [:]
/**
The paths of all files which were accessed, with their new hashes
This list is used to check if a file was modified, and to write all accessed files back to disk
*/
private var accessedFiles: [String : Data] = [:]
private var source: String {
"FileUpdateChecker"
}
init(input: URL) {
self.input = input
guard hashesFile.exists else {
log.add(info: "No file hashes loaded, regarding all content as new", source: source)
return
}
let data: Data
do {
data = try Data(contentsOf: hashesFile)
} catch {
log.add(
warning: "File hashes could not be read, regarding all content as new",
source: source,
error: error)
return
}
do {
self.previousFiles = try JSONDecoder().decode(from: data)
} catch {
log.add(
warning: "File hashes could not be decoded, regarding all content as new",
source: source,
error: error)
return
}
}
func fileHasChanged(at path: String) -> Bool {
guard let oldHash = previousFiles[path] else {
// Image wasn't used last time, so treat as new
return true
}
guard let newHash = accessedFiles[path] else {
// Each image should have been loaded once
// before using this function
fatalError()
}
return oldHash != newHash
}
func didLoad(_ data: Data, at path: String) {
accessedFiles[path] = SHA256.hash(data: data).data
}
func writeDetectedFileChangesToDisk() {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(accessedFiles)
try data.write(to: hashesFile)
} catch {
log.add(warning: "Failed to save file hashes", source: source, error: error)
}
}
}

View File

@ -0,0 +1,262 @@
import Foundation
import AppKit
import CryptoKit
private struct ImageJob {
let destination: String
let width: Int
let path: String
}
final class ImageGenerator {
/**
The path to the input folder.
*/
private let input: URL
/**
The path to the output folder
*/
private let output: URL
/**
The images to generate.
The key is the image source path relative to the input folder, and the values are the destination path (relative to the output folder) and the required image width.
*/
private var imageJobs: [String : [ImageJob]] = [:]
/**
The images which could not be found, but are required for the site.
The key is the image path, and the value is the page that requires it.
*/
private var missingImages: [String : String] = [:]
/**
All warnings produced for images during generation
*/
private var imageWarnings: Set<String> = []
/**
All images required by the site.
The values are the destination paths of the images, relative to the output folder
*/
private var requiredImages: Set<String> = []
/**
All images modified or created during this generator run.
*/
private var generatedImages: Set<String> = []
/**
A cache to get the size of source images, so that files don't have to be loaded multiple times.
The key is the absolute source path, and the value is the image size
*/
private var imageSizeCache: [String : NSSize] = [:]
private var fileUpdates: FileUpdateChecker
init(input: URL, output: URL) {
self.fileUpdates = FileUpdateChecker(input: input)
self.input = input
self.output = output
}
func writeDetectedFileChangesToDisk() {
fileUpdates.writeDetectedFileChangesToDisk()
}
private func getImageSize(atPath path: String) -> NSSize? {
if let size = imageSizeCache[path] {
return size
}
guard let image = getImage(atPath: path) else {
return nil
}
let size = image.size
imageSizeCache[path] = size
return size
}
private func getImage(atPath path: String) -> NSImage? {
guard let data = getData(atPath: path) else {
log.add(error: "Failed to load file", source: path)
return nil
}
guard let image = NSImage(data: data) else {
log.add(error: "Failed to read image", source: path)
return nil
}
return image
}
private func getData(atPath path: String) -> Data? {
let url = input.appendingPathComponent(path)
guard url.exists else {
return nil
}
do {
let data = try Data(contentsOf: url)
fileUpdates.didLoad(data, at: path)
return data
} catch {
log.add(error: "Failed to read data", source: path, error: error)
return nil
}
}
func requireImage(at destination: String, generatedFrom source: String, requiredBy path: String, width: Int, height: Int?) -> NSSize {
let height = height.unwrapped(CGFloat.init)
let sourceUrl = input.appendingPathComponent(source)
guard sourceUrl.exists else {
missingImages[source] = path
return .zero
}
guard let imageSize = getImageSize(atPath: source) else {
missingImages[source] = path
return .zero
}
let scaledSize = imageSize.scaledDown(to: CGFloat(width))
// Check desired height, then we can forget about it
if let height = height {
let expectedHeight = scaledSize.width / CGFloat(width) * height
if abs(expectedHeight - scaledSize.height) > 2 {
addWarning("Invalid height (\(scaledSize.height) instead of \(expectedHeight))", destination: destination, path: path)
}
}
let job = ImageJob(destination: destination, width: width, path: path)
guard let existingSource = imageJobs[source] else {
imageJobs[source] = [job]
return scaledSize
}
guard let existingJob = existingSource.first(where: { $0.destination == destination}) else {
imageJobs[source] = existingSource + [job]
return scaledSize
}
if existingJob.width != width {
addWarning("Multiple image widths (\(existingJob.width) and \(width))", destination: destination, path: "\(existingJob.path) and \(path)")
}
return scaledSize
}
func createImages() {
for (source, jobs) in imageJobs.sorted(by: { $0.key < $1.key }) {
create(images: jobs, from: source)
}
printMissingImages()
printImageWarnings()
}
private func printMissingImages() {
guard !missingImages.isEmpty else {
return
}
print("\(missingImages.count) missing images:")
for (source, path) in missingImages {
print(" \(source) (required by \(path))")
}
}
private func printImageWarnings() {
guard !imageWarnings.isEmpty else {
return
}
print("\(imageWarnings.count) image warnings:")
for imageWarning in imageWarnings {
print(imageWarning)
}
}
private func addWarning(_ message: String, destination: String, path: String) {
let warning = " \(destination): \(message) required by \(path)"
imageWarnings.insert(warning)
}
private func addWarning(_ message: String, job: ImageJob) {
addWarning(message, destination: job.destination, path: job.path)
}
private func isMissing(_ job: ImageJob) -> Bool {
!output.appendingPathComponent(job.destination).exists
}
private func create(images: [ImageJob], from source: String) {
// Only load image if required
let imageHasChanged = fileUpdates.fileHasChanged(at: source)
guard imageHasChanged || images.contains(where: isMissing) else {
return
}
guard let image = getImage(atPath: source) else {
missingImages[source] = images.first?.path
return
}
if imageHasChanged {
// Update all images
images.forEach { create(job: $0, from: image, source: source) }
} else {
// Update only missing images
images
.filter(isMissing)
.forEach { create(job: $0, from: image, source: source) }
}
}
private func create(job: ImageJob, from image: NSImage, source: String) {
let destinationUrl = output.appendingPathComponent(job.destination)
// Ensure that image file is supported
let ext = destinationUrl.pathExtension.lowercased()
guard ImageType(fileExtension: ext) != nil else {
fatalError()
}
let desiredWidth = CGFloat(image.size.width)
let destinationSize = image.size.scaledDown(to: desiredWidth)
let scaledImage = image.scaledDown(to: destinationSize)
let scaledSize = scaledImage.size
if abs(scaledSize.width - desiredWidth) > 2 {
addWarning("Invalid width (\(scaledSize.width) instead of \(desiredWidth))", job: job)
}
if scaledSize.width > desiredWidth {
addWarning("Invalid width (\(scaledSize.width) instead of \(desiredWidth))", job: job)
}
let destinationExtension = destinationUrl.pathExtension.lowercased()
guard let type = ImageType(fileExtension: destinationExtension)?.fileType else {
addWarning("Invalid image extension \(destinationExtension)", job: job)
return
}
guard let tiff = scaledImage.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) else {
addWarning("Failed to get data", job: job)
return
}
guard let data = tiffData.representation(using: type, properties: [.compressionFactor: NSNumber(0.7)]) else {
addWarning("Failed to get data", job: job)
return
}
do {
try data.createFolderAndWrite(to: destinationUrl)
} catch {
addWarning("Failed to write image (\(error))", job: job)
return
}
}
}

View File

@ -72,22 +72,17 @@ struct PageContentGenerator {
private func processMarkdownImage(markdown: Substring, html: String, page: Element) -> String { private func processMarkdownImage(markdown: Substring, html: String, page: Element) -> String {
// Split the markdown ![alt](file title) // Split the markdown ![alt](file title)
// There are several known shorthand commands
// For images: ![left_title](file right_title) // For images: ![left_title](file right_title)
// For videos: ![option1,option2,...](file) // For videos: ![option1,option2,...](file)
// For svg with custom area: ![x,y,width,height](file.svg) // For svg with custom area: ![x,y,width,height](file.svg)
// For downloads: ![download](file1, text1; file2, text2, ...) // For downloads: ![download](file1, text1; file2, text2, ...)
// For a simple boxes: ![box](title;body)
// External pages: ![external](url1, text1; url2, text2, ...) // External pages: ![external](url1, text1; url2, text2, ...)
let fileAndTitle = markdown.between("(", and: ")") let fileAndTitle = markdown.between("(", and: ")")
let alt = markdown.between("[", and: "]").nonEmpty let alt = markdown.between("[", and: "]").nonEmpty
switch alt { if let alt = alt, let command = ShorthandMarkdownKey(rawValue: alt) {
case "download": return handleShortHandCommand(command, page: page, content: fileAndTitle)
return handleDownloadButtons(page: page, content: fileAndTitle)
case "external":
return handleExternalButtons(page: page, content: fileAndTitle)
case "html":
return handleExternalHTML(page: page, file: fileAndTitle)
default:
break
} }
let file = fileAndTitle.dropAfterFirst(" ") let file = fileAndTitle.dropAfterFirst(" ")
@ -106,14 +101,35 @@ struct PageContentGenerator {
return handleFile(page: page, file: file, fileExtension: fileExtension) return handleFile(page: page, file: file, fileExtension: fileExtension)
} }
private func handleShortHandCommand(_ command: ShorthandMarkdownKey, page: Element, content: String) -> String {
switch command {
case .downloadButtons:
return handleDownloadButtons(page: page, content: content)
case .externalLink:
return handleExternalButtons(page: page, content: content)
case .includedHtml:
return handleExternalHTML(page: page, file: content)
case .box:
return handleSimpleBox(page: page, content: content)
}
}
private func handleImage(page: Element, file: String, rightTitle: String?, leftTitle: String?) -> String { private func handleImage(page: Element, file: String, rightTitle: String?, leftTitle: String?) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file) let imagePath = page.pathRelativeToRootForContainedInputFile(file)
let size = files.requireImage(source: imagePath, destination: imagePath, width: configuration.pageImageWidth) let size = files.requireImage(
source: imagePath,
destination: imagePath,
requiredBy: page.path,
width: configuration.pageImageWidth)
let imagePath2x = imagePath.insert("@2x", beforeLast: ".") let imagePath2x = imagePath.insert("@2x", beforeLast: ".")
let file2x = file.insert("@2x", beforeLast: ".") let file2x = file.insert("@2x", beforeLast: ".")
files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * configuration.pageImageWidth) files.requireImage(
source: imagePath,
destination: imagePath2x,
requiredBy: page.path,
width: 2 * configuration.pageImageWidth)
let content: [PageImageTemplate.Key : String] = [ let content: [PageImageTemplate.Key : String] = [
.image: file, .image: file,
@ -223,4 +239,15 @@ struct PageContentGenerator {
return "" return ""
} }
} }
private func handleSimpleBox(page: Element, content: String) -> String {
let parts = content.components(separatedBy: ";")
guard parts.count > 1 else {
log.add(error: "Invalid box specification", source: page.path)
return ""
}
let title = parts[0]
let text = parts.dropFirst().joined(separator: ";")
return factory.makePlaceholder(title: title, text: text)
}
} }

View File

@ -68,7 +68,7 @@ struct PageGenerator {
} else { } else {
let (content, includesCode) = PageContentGenerator(factory: factory.factory) let (content, includesCode) = PageContentGenerator(factory: factory.factory)
.generate(page: page, language: language, content: metadata.placeholderText) .generate(page: page, language: language, content: metadata.placeholderText)
let placeholder = factory.makePlaceholder(title: metadata.placeholderTitle, text: content) let placeholder = factory.factory.makePlaceholder(title: metadata.placeholderTitle, text: content)
return (placeholder, includesCode, true) return (placeholder, includesCode, true)
} }
} }

View File

@ -27,6 +27,7 @@ struct PageHeadGenerator {
files.requireImage( files.requireImage(
source: sourceImagePath, source: sourceImagePath,
destination: destinationImagePath, destination: destinationImagePath,
requiredBy: page.path,
width: PageHeadGenerator.linkPreviewDesiredImageWidth) width: PageHeadGenerator.linkPreviewDesiredImageWidth)
content[.image] = factory.html.linkPreviewImage(file: linkPreviewImageName) content[.image] = factory.html.linkPreviewImage(file: linkPreviewImageName)
} }

View File

@ -0,0 +1,24 @@
import Foundation
enum ShorthandMarkdownKey: String {
/**
A variable number of download buttons for file downloads
*/
case downloadButtons = "download"
/**
A large button to an external page.
*/
case externalLink = "external"
/**
Additional HTML code include verbatim into the page.
*/
case includedHtml = "html"
/**
A box with a heading and a text description
*/
case box = "box"
}

View File

@ -53,6 +53,7 @@ struct SiteGenerator {
files.requireImage( files.requireImage(
source: $0.sourcePath, source: $0.sourcePath,
destination: $0.destinationPath, destination: $0.destinationPath,
requiredBy: element.path,
width: $0.desiredWidth, width: $0.desiredWidth,
desiredHeight: $0.desiredHeight) desiredHeight: $0.desiredHeight)
} }

View File

@ -39,6 +39,7 @@ struct ThumbnailListGenerator {
files.requireImage( files.requireImage(
source: fullThumbnailPath, source: fullThumbnailPath,
destination: fullThumbnailPath, destination: fullThumbnailPath,
requiredBy: item.path,
width: style.width, width: style.width,
desiredHeight: style.height) desiredHeight: style.height)
@ -46,6 +47,7 @@ struct ThumbnailListGenerator {
files.requireImage( files.requireImage(
source: fullThumbnailPath, source: fullThumbnailPath,
destination: fullThumbnailPath.insert("@2x", beforeLast: "."), destination: fullThumbnailPath.insert("@2x", beforeLast: "."),
requiredBy: item.path,
width: style.width * 2, width: style.width * 2,
desiredHeight: style.height * 2) desiredHeight: style.height * 2)

View File

@ -1,13 +1,13 @@
import Foundation import Foundation
struct PlaceholderTemplate: Template { struct BoxTemplate: Template {
enum Key: String, CaseIterable { enum Key: String, CaseIterable {
case title = "TITLE" case title = "TITLE"
case text = "TEXT" case text = "TEXT"
} }
static let templateName = "empty.html" static let templateName = "box.html"
var raw: String var raw: String
} }

View File

@ -75,13 +75,7 @@ struct LocalizedSiteTemplate {
// MARK: Content // MARK: Content
func makePlaceholder(metadata: Element.LocalizedMetadata) -> String { func makePlaceholder(metadata: Element.LocalizedMetadata) -> String {
makePlaceholder(title: metadata.placeholderTitle, text: metadata.placeholderText) factory.makePlaceholder(title: metadata.placeholderTitle, text: metadata.placeholderText)
}
func makePlaceholder(title: String, text: String) -> String {
factory.placeholder.generate([
.title: title,
.text: text])
} }
func makeBackLink(text: String, language: String) -> String { func makeBackLink(text: String, language: String) -> String {

View File

@ -16,7 +16,7 @@ final class TemplateFactory {
let overviewSectionClean: OverviewSectionCleanTemplate let overviewSectionClean: OverviewSectionCleanTemplate
let placeholder: PlaceholderTemplate let box: BoxTemplate
// MARK: Thumbnails // MARK: Thumbnails
@ -64,7 +64,7 @@ final class TemplateFactory {
self.topBar = try .init(in: templateFolder) self.topBar = try .init(in: templateFolder)
self.overviewSection = try .init(in: templateFolder) self.overviewSection = try .init(in: templateFolder)
self.overviewSectionClean = try .init(in: templateFolder) self.overviewSectionClean = try .init(in: templateFolder)
self.placeholder = try .init(in: templateFolder) self.box = try .init(in: templateFolder)
self.largeThumbnail = try .init(in: templateFolder) self.largeThumbnail = try .init(in: templateFolder)
self.squareThumbnail = try .init(in: templateFolder) self.squareThumbnail = try .init(in: templateFolder)
self.smallThumbnail = try .init(in: templateFolder) self.smallThumbnail = try .init(in: templateFolder)
@ -75,4 +75,12 @@ final class TemplateFactory {
self.video = try .init(in: templateFolder) self.video = try .init(in: templateFolder)
self.html = .init() self.html = .init()
} }
// MARK: Convenience methods
func makePlaceholder(title: String, text: String) -> String {
box.generate([
.title: title,
.text: text])
}
} }

View File

@ -36,8 +36,7 @@ private func generate(configPath: String) throws {
files.printDraftPages() files.printDraftPages()
files.createImages() files.createImages()
print("Images generated")
files.copyRequiredFiles() files.copyRequiredFiles()
files.printExternalFiles() files.printExternalFiles()
files.writeHashes() files.writeDetectedFileChangesToDisk()
} }