/// A static file shipped with the website service. /// /// Each case identifies a file name stored under the static files root (the `Resources/Static` /// directory) and served by Hummingbird's `FileMiddleware` middleware. A name can be available /// with more than one extension (see ``fileExtensions``), each resolving to its own file. enum StaticFile: CaseIterable, Sendable { /// The `apple-touch-icon.png` icon. case appleTouchIcon /// The `css/error.css` stylesheet and `js/error.js` script for the not-found page. case error /// The `favicon.ico` icon. case favicon /// The `icon.svg` icon. case icon /// The `icon-192.png` icon for the web manifest. case icon192 /// The `icon-512.png` icon for the web manifest. case icon512 /// The `css/index.css` stylesheet and `js/index.js` script for the landing page. case index /// The `robots.txt` crawler directives. case robots /// The `css/shared.css` stylesheet and `js/shared.js` script shared across pages. case shared /// The `site.webmanifest` web application manifest. case site /// The `sitemap.xml` crawler sitemap. case sitemap } // MARK: - Enumerations extension StaticFile { /// A file extension used by a ``StaticFile``. enum Extension: String, Sendable { /// A Cascading Style Sheets file. case css /// A JavaScript file. case js /// A Portable Network Graphics image. case png /// A Windows icon image. case ico /// A Scalable Vector Graphics image. case svg /// A plain text file. case txt /// A web application manifest file. case webmanifest /// An Extensible Markup Language file. case xml } } // MARK: - Extensions extension StaticFile { // MARK: Computed /// The file extensions the file is available with. var fileExtensions: [Extension] { switch self { case .appleTouchIcon, .icon192, .icon512: [.png] case .error, .index, .shared: [.css, .js] case .favicon: [.ico] case .icon: [.svg] case .robots: [.txt] case .site: [.webmanifest] case .sitemap: [.xml] } } /// The file's name, without extension. var fileName: String { switch self { case .appleTouchIcon: "apple-touch-icon" case .error: "error" case .favicon: "favicon" case .icon: "icon" case .icon192: "icon-192" case .icon512: "icon-512" case .index: "index" case .robots: "robots" case .shared: "shared" case .site: "site" case .sitemap: "sitemap" } } // MARK: Methods /// Resolves the file's path against the given base directory. /// /// - Parameters: /// - basePath: the directory the static files are served from. /// - fileExtension: the extension of the file to resolve. /// - Returns: the path to the file, relative to the `basePath` path. func path( relativeTo basePath: String, for fileExtension: Extension ) -> String { let relativePath = relativePath(for: fileExtension) guard !basePath.isEmpty else { return relativePath } return "\(basePath)/\(relativePath)" } /// Resolves the file's path relative to the static files root (e.g. `"css/shared.css"`). /// /// This also matches the URL path the file is served at by `FileMiddleware`. /// /// - Parameter fileExtension: the extension of the file to resolve. /// - Returns: the path to the file, relative to the static files root. func relativePath( for fileExtension: Extension ) -> String { let file = "\(fileName).\(fileExtension.rawValue)" return fileExtension.subdirectory .map { "\($0)/\(file)" } ?? file } /// Resolves the absolute URL path the file is served at (e.g. `"/css/shared.css"`). /// /// - Parameter fileExtension: the extension of the file to resolve. /// - Returns: the path to use in `href` and `src` attributes. func urlPath( for fileExtension: Extension ) -> String { "/\(relativePath(for: fileExtension))" } } extension StaticFile.Extension { // MARK: Computed /// The file's content type. var contentType: String { switch self { case .css: "text/css" case .js: "text/javascript" case .png: "image/png" case .ico: "image/vnd.microsoft.icon" case .svg: "image/svg+xml" case .txt: "text/plain" case .webmanifest: "application/manifest+json" case .xml: "application/xml" } } /// The sub-directory within the static root that holds files with this extension, if any. var subdirectory: String? { switch self { case .css: "css" case .js: "js" default: nil } } }