From 3358946e3dd13b90a1cdb55b8c15354ed699e4b6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 25 Aug 2026 17:23:20 +0200 Subject: [PATCH] Added the ImageWidth enumeration to the Website library target. --- .../Internal/Enumerations/ImageWidth.swift | 29 +++++++++++++ .../Library/Internal/Types/StaticFile.swift | 39 +++++++++++++++++ .../Enumerations/ImageWidthTests.swift | 24 +++++++++++ .../Internal/Types/StaticFileTests.swift | 43 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 Services/Website/Sources/Library/Internal/Enumerations/ImageWidth.swift create mode 100644 Services/Website/Tests/Library/Cases/Internal/Enumerations/ImageWidthTests.swift diff --git a/Services/Website/Sources/Library/Internal/Enumerations/ImageWidth.swift b/Services/Website/Sources/Library/Internal/Enumerations/ImageWidth.swift new file mode 100644 index 0000000..3aa8d1a --- /dev/null +++ b/Services/Website/Sources/Library/Internal/Enumerations/ImageWidth.swift @@ -0,0 +1,29 @@ +/// A rendition an image asset is available at, from the narrowest up to the full-size file. +/// +/// ``StaticFile`` names one file per rendition, and a page's `srcset` offers them all so the browser picks by rendered width. +/// The declaration order is the `srcset` order: narrowest first. +enum ImageWidth: CaseIterable { + /// The small rendition, 480 pixels wide. + case small + /// The medium rendition, 800 pixels wide. + case medium + /// The large rendition: the full-size file, 1200 pixels wide. + case large +} + +// MARK: - Extensions + +extension ImageWidth { + + // MARK: Computed + + /// The rendition's width in pixels: what the file is resampled to, and what its `srcset` descriptor states. + var width: Int { + switch self { + case .small: 480 + case .medium: 800 + case .large: 1200 + } + } + +} diff --git a/Services/Website/Sources/Library/Internal/Types/StaticFile.swift b/Services/Website/Sources/Library/Internal/Types/StaticFile.swift index 8a8a857..d28c4b1 100644 --- a/Services/Website/Sources/Library/Internal/Types/StaticFile.swift +++ b/Services/Website/Sources/Library/Internal/Types/StaticFile.swift @@ -14,18 +14,26 @@ struct StaticFile: Asset { /// The file's name, without extension. let fileName: String + /// The folder holding the file, or `nil` when it sits in its extensions' own folders. + /// + /// Imagery sits in a folder per page — `img/index`, `img/about` — rather than in its extension's own. + let folder: String? + // MARK: Initializers /// Declares a static file. /// - Parameters: /// - fileName: the file's name, without extension. + /// - folder: the folder holding the file, or `nil` (the default) to use each extension's own folder. /// - fileExtensions: the extensions the file is available with, one file each. init( _ fileName: String, + in folder: String? = nil, as fileExtensions: AssetExtension... ) { self.fileExtensions = fileExtensions self.fileName = fileName + self.folder = folder } } @@ -55,6 +63,37 @@ extension StaticFile { } +// MARK: - Methods + +extension StaticFile { + + /// The `srcset` value offering every rendition of a responsive image, narrowest first, ending on the full-size file. + /// + /// The renditions are named by the given closure, which conventionally gives the `large` one the bare file name and the narrower ones their + /// width — as `srcset` files are usually named: + /// + /// ```swift + /// static func portrait(_ width: ImageWidth) -> Self { + /// Self(width == .large ? "portrait" : "portrait-\(width.width)", in: "img/about", as: .jpg, .webp) + /// } + /// ``` + /// - Parameters: + /// - fileExtension: the format the renditions are named in. + /// - version: the version token appended to each URL, or `nil` (the default) to leave them unversioned. + /// - rendition: the file naming a given width. + /// - Returns: the `srcset` value for that format. + static func srcSet( + for fileExtension: AssetExtension, + version: String? = nil, + rendition: (ImageWidth) -> Self + ) -> String { + ImageWidth.allCases + .map { "\(rendition($0).urlPath(for: fileExtension, version: version)) \($0.width)w" } + .joined(separator: ", ") + } + +} + // MARK: - Constants extension StaticFile { diff --git a/Services/Website/Tests/Library/Cases/Internal/Enumerations/ImageWidthTests.swift b/Services/Website/Tests/Library/Cases/Internal/Enumerations/ImageWidthTests.swift new file mode 100644 index 0000000..a635990 --- /dev/null +++ b/Services/Website/Tests/Library/Cases/Internal/Enumerations/ImageWidthTests.swift @@ -0,0 +1,24 @@ +import Testing + +@testable import WebsiteLibrary + +@Suite( + "ImageWidth enumeration", + .tags(.enumeration) +) +struct ImageWidthTests { + + // MARK: Computed tests + + @Test(arguments: zip( + ImageWidth.allCases, + [480, 800, 1200] + )) + func `width`( + for imageWidth: ImageWidth, + expects width: Int + ) { + #expect(imageWidth.width == width) + } + +} diff --git a/Services/Website/Tests/Library/Cases/Internal/Types/StaticFileTests.swift b/Services/Website/Tests/Library/Cases/Internal/Types/StaticFileTests.swift index 9c61e8f..18d1163 100644 --- a/Services/Website/Tests/Library/Cases/Internal/Types/StaticFileTests.swift +++ b/Services/Website/Tests/Library/Cases/Internal/Types/StaticFileTests.swift @@ -33,6 +33,49 @@ struct StaticFileTests { #expect(font.urlPath(for: .woff2) == "/font/a-font-400.woff2") } + /// Imagery sits in a folder per page, so a declared folder replaces the extension's own for every extension the file has. + @Test + func `declared folder paths`() { + let portrait = File("portrait", in: "img/about", as: .jpg, .webp) + + #expect(portrait.relativePath(for: .jpg) == "img/about/portrait.jpg") + #expect(portrait.relativePath(for: .webp) == "img/about/portrait.webp") + } + + // MARK: Methods tests + + @Test(arguments: zip( + [AssetExtension.jpg, .webp], + ["jpg", "webp"] + )) + func `srcset offers every rendition narrowest first`( + for fileExtension: AssetExtension, + expects suffix: String + ) { + let srcSet = File.srcSet(for: fileExtension) { width in + File( + width == .large ? "portrait" : "portrait-\(width.width)", + in: "img/about", + as: .jpg, .webp + ) + } + + #expect(srcSet == [ + "/img/about/portrait-480.\(suffix) 480w", + "/img/about/portrait-800.\(suffix) 800w", + "/img/about/portrait.\(suffix) 1200w" + ].joined(separator: ", ")) + } + + @Test + func `srcset versions every rendition when given a version`() { + let srcSet = File.srcSet(for: .jpg, version: "0123456789abcdef") { _ in + File("portrait", in: "img/about", as: .jpg) + } + + #expect(srcSet.components(separatedBy: "?v=0123456789abcdef").count - 1 == ImageWidth.allCases.count) + } + // MARK: Constants tests @Test