Implement image comparison command

This commit is contained in:
Christoph Hagen
2025-01-05 20:16:16 +01:00
parent 29bba5e76e
commit ac7fbdd638
23 changed files with 200 additions and 40 deletions

View File

@ -12,6 +12,8 @@ extension HeaderElement {
static let defaultCssFileOrder = 42
static let audioPlayerCssOrder = 43
static let imageCompareCssOrder = 44
}
enum HeaderElement {

View File

@ -1,6 +1,6 @@
import Foundation
struct ImageSet {
struct ImageSet: HtmlProducer {
let image: FileResource
@ -12,12 +12,15 @@ struct ImageSet {
let description: String
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7) {
let extraAttributes: String
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7, extraAttributes: String? = nil) {
self.image = image
self.maxWidth = maxWidth
self.maxHeight = maxHeight
self.description = description
self.quality = quality
self.extraAttributes = extraAttributes ?? ""
}
var jobs: [ImageVersion] {
@ -36,17 +39,16 @@ struct ImageSet {
]
}
var content: String {
func populate(_ result: inout String) {
let fileExtension = image.type.fileExtension.map { "." + $0 } ?? ""
let prefix1x = "/\(image.outputImageFolder)/\(maxWidth)x\(maxHeight)"
let prefix2x = "/\(image.outputImageFolder)/\(maxWidth*2)x\(maxHeight*2)"
var result = "<picture>"
result += "<picture>"
result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>"
result += "<source type='image/webp' srcset='\(prefix1x).webp 1x, \(prefix1x).webp 2x'/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'\(extraAttributes)/>"
result += "</picture>"
return result
}
}

View File

@ -11,6 +11,10 @@ enum KnownHeaderElement: Int {
/// JavaScript file for the audio player
case audioPlayerJs = 2
case imageCompareJs = 5
case imageCompareCss = 6
func header(content: Content) -> HeaderElement? {
switch self {
case .codeHightlighting:
@ -29,6 +33,14 @@ enum KnownHeaderElement: Int {
if let file = content.settings.pages.audioPlayerJsFile {
return .js(file: file, defer: true)
}
case .imageCompareJs:
if let file = content.settings.pages.imageCompareJsFile {
return .js(file: file, defer: true)
}
case .imageCompareCss:
if let file = content.settings.pages.imageCompareCssFile {
return .css(file: file, order: HeaderElement.imageCompareCssOrder)
}
}
return nil
}
@ -53,6 +65,10 @@ extension KnownHeaderElement: CustomStringConvertible {
return "audio-player-css"
case .audioPlayerJs:
return "audio-player-js"
case .imageCompareJs:
return "image-compare-js"
case .imageCompareCss:
return "image-compare-css"
}
}
}

View File

@ -8,7 +8,7 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
}

View File

@ -5,7 +5,7 @@ struct BoxCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.results = results
}

View File

@ -7,7 +7,7 @@ struct ButtonCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
}

View File

@ -3,7 +3,7 @@ protocol CommandProcessor {
var commandType: ShorthandMarkdownKey { get }
init(content: Content, results: PageGenerationResults)
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
func process(_ arguments: [String], markdown: Substring) -> String
}

View File

@ -5,7 +5,7 @@ struct IconCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.results = results
}

View File

@ -0,0 +1,53 @@
struct ImageCompareCommandProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .imageCompare
let content: Content
let results: PageGenerationResults
let language: ContentLanguage
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
self.language = language
}
func process(_ arguments: [String], markdown: Substring) -> String {
guard arguments.count == 2 else {
results.invalid(command: .imageCompare, markdown)
return ""
}
let leftImageId = arguments[0]
let rightImageId = arguments[1]
guard let leftImage = content.image(leftImageId) else {
results.missing(file: leftImageId, source: "Image compare")
return ""
}
guard let rightImage = content.image(rightImageId) else {
results.missing(file: rightImageId, source: "Image compare")
return ""
}
let size = content.settings.pages.contentWidth
let leftImageSet = leftImage.imageSet(
width: size, height: size,
language: language,
extraAttributes: ImageCompare.extraAttributes)
let rightImageSet = rightImage.imageSet(
width: size, height: size,
language: language,
extraAttributes: ImageCompare.extraAttributes)
results.require(imageSet: leftImageSet)
results.require(imageSet: rightImageSet)
results.require(icon: ImageCompare.requiredIcon)
results.require(headers: .imageCompareJs, .imageCompareCss)
return ImageCompare(left: leftImageSet, right: rightImageSet).content
}
}

View File

@ -7,7 +7,7 @@ struct LabelsCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
}

View File

@ -11,7 +11,7 @@ struct PageHtmlProcessor: CommandProcessor {
let content: Content
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
}

View File

@ -7,7 +7,7 @@ struct VideoCommandProcessor: CommandProcessor {
let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) {
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
}

View File

@ -25,6 +25,8 @@ final class PageContentParser {
private let video: VideoCommandProcessor
private let imageCompare: ImageCompareCommandProcessor
// MARK: Other handlers
private let inlineLink: InlineLinkProcessor
@ -43,13 +45,14 @@ final class PageContentParser {
self.content = content
self.results = results
self.language = language
self.buttonHandler = .init(content: content, results: results)
self.labelHandler = .init(content: content, results: results)
self.audioPlayer = .init(content: content, results: results)
self.icons = .init(content: content, results: results)
self.box = .init(content: content, results: results)
self.html = .init(content: content, results: results)
self.video = .init(content: content, results: results)
self.buttonHandler = .init(content: content, results: results, language: language)
self.labelHandler = .init(content: content, results: results, language: language)
self.audioPlayer = .init(content: content, results: results, language: language)
self.icons = .init(content: content, results: results, language: language)
self.box = .init(content: content, results: results, language: language)
self.html = .init(content: content, results: results, language: language)
self.video = .init(content: content, results: results, language: language)
self.imageCompare = .init(content: content, results: results, language: language)
self.inlineLink = .init(content: content, results: results, language: language)
self.code = .init(results: results)
@ -136,6 +139,8 @@ final class PageContentParser {
return handleTagLink(arguments, markdown: markdown)
case .icons:
return icons.process(arguments, markdown: markdown)
case .imageCompare:
return imageCompare.process(arguments, markdown: markdown)
}
}
@ -155,12 +160,10 @@ final class PageContentParser {
}
results.used(file: image)
let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.localized(in: language)
let path = image.absoluteUrl
guard !image.type.isSvg else {
let path = image.absoluteUrl
let altText = image.localized(in: language)
return SvgImage(imagePath: path, altText: altText).content
}
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
@ -169,6 +172,7 @@ final class PageContentParser {
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
results.require(imageSet: largeImage)
let caption = arguments.count == 2 ? arguments[1] : nil
return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
thumbnail: thumbnail,

View File

@ -53,4 +53,10 @@ enum ShorthandMarkdownKey: String {
/// Format: `![icons](icon-id;...)`
case icons
/**
Create an image comparison with a slider.
Format: `![compare](image1;image2)`
*/
case imageCompare = "compare"
}