diff --git a/Packages/Infrastructure/README.md b/Packages/Infrastructure/README.md index c7fdf23..5028528 100644 --- a/Packages/Infrastructure/README.md +++ b/Packages/Infrastructure/README.md @@ -8,7 +8,7 @@ The package provides, grouped by role: | Routing | `RouterController`, `RouteCollectionBuilder`, the `addController` extension on `RouterMethods` | | Middlewares | `SecurityHeadersMiddleware`, `VaryMiddleware`, `RateLimitMiddleware`, `LocalizationMiddleware`, `NotFoundMiddleware` | | Pages and assets | `Page`, `Asset`, `AssetExtension`, `FingerprintAssets` | -| Link previews | `SocialCard`, its `Image`, and the `SocialCardTag` meta tags it derives | +| Link previews | `SocialCard`, its `Image`, and the `Tag` meta tags it derives | | Responses | `CachedHTMLResponse`, `LocalizedHTMLCollectionResponse` | | Contexts | `LocalizedRequestContext` | | Constants | The `HTTPField.Name` header names, `Int.RateLimit` limits, and `String.Security` header values the middlewares default to | diff --git a/Packages/Infrastructure/Sources/Public/Types/SocialCard.swift b/Packages/Infrastructure/Sources/Public/Types/SocialCard.swift index 101ef61..0fe4a74 100644 --- a/Packages/Infrastructure/Sources/Public/Types/SocialCard.swift +++ b/Packages/Infrastructure/Sources/Public/Types/SocialCard.swift @@ -70,52 +70,16 @@ public struct SocialCard: Sendable { /// The card's meta tags, in a stable order: the Open Graph type, site name, title, description, URL, and locale, then the image group, and /// the Twitter card style last. A tag whose fact the card does not carry is left out. - public var tags: [SocialCardTag] { - let tags: [SocialCardTag?] = [ - SocialCardTag( - attribute: .property, - content: type, - name: .type - ), - siteName.map { - SocialCardTag( - attribute: .property, - content: $0, - name: .siteName - ) - }, - SocialCardTag( - attribute: .property, - content: title, - name: .title - ), - summary.map { - SocialCardTag( - attribute: .property, - content: $0, - name: .description - ) - }, - url.map { - SocialCardTag( - attribute: .property, - content: $0, - name: .url - ) - }, - locale.map { - SocialCardTag( - attribute: .property, - content: $0, - name: .locale - ) - }, + public var tags: [Tag] { + let tags: [Tag?] = [ + Tag(type, name: .type), + siteName.map { Tag($0, name: .siteName) }, + Tag(title, name: .title), + summary.map { Tag($0, name: .description) }, + url.map { Tag($0, name: .url) }, + locale.map { Tag($0, name: .locale) }, ] + (image?.tags ?? []) + [ - SocialCardTag( - attribute: .name, - content: style.rawValue, - name: .twitter - ), + Tag(style.rawValue, name: .twitter), ] return tags.compactMap { $0 } diff --git a/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardImage.swift b/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardImage.swift index d00398f..43f8575 100644 --- a/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardImage.swift +++ b/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardImage.swift @@ -39,30 +39,12 @@ extension SocialCard { // MARK: Computed /// The image's meta tags, in a stable order: its URL, width, and height, then its alt text when it carries one. - public var tags: [SocialCardTag] { - let tags: [SocialCardTag?] = [ - SocialCardTag( - attribute: .property, - content: url, - name: .image - ), - SocialCardTag( - attribute: .property, - content: String(width), - name: .imageWidth - ), - SocialCardTag( - attribute: .property, - content: String(height), - name: .imageHeight - ), - alt.map { - SocialCardTag( - attribute: .property, - content: $0, - name: .imageAlt - ) - }, + public var tags: [Tag] { + let tags: [Tag?] = [ + Tag(url, name: .image), + Tag(String(width), name: .imageWidth), + Tag(String(height), name: .imageHeight), + alt.map { Tag($0, name: .imageAlt) }, ] return tags.compactMap { $0 } diff --git a/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardTag.swift b/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardTag.swift index 414782b..ec446a5 100644 --- a/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardTag.swift +++ b/Packages/Infrastructure/Sources/Public/Types/SocialCard/SocialCardTag.swift @@ -1,24 +1,44 @@ -/// A head meta tag of a ``SocialCard``: which attribute keys it, and its name and content. -public struct SocialCardTag: Equatable, Sendable { +extension SocialCard { + /// A head meta tag of a ``SocialCard``: its name and content, keyed by the attribute its ``name`` dictates. + public struct Tag: Equatable, Sendable { - // MARK: Properties + // MARK: Properties - /// The attribute the tag is keyed by. - public let attribute: Attribute + /// The tag's value. + public let content: String - /// The tag's value. - public let content: String + /// The tag's name. + public let name: Name + + // MARK: Initializers - /// The tag's name. - public let name: Name + /// Creates a head meta tag. + /// - Parameters: + /// - content: the tag's value. + /// - name: the tag's name, dictating the attribute the tag is keyed by. + public init( + _ content: String, + name: Name + ) { + self.content = content + self.name = name + } + // MARK: Computed + + /// The attribute the tag is keyed by, dictated by its ``name``. + public var attribute: Attribute { + name.attribute + } + + } } // MARK: - Enumerations -extension SocialCardTag { - - /// The meta attribute a ``SocialCardTag`` is keyed by, named by its raw value. +extension SocialCard.Tag { + + /// The meta attribute a ``SocialCard/Tag`` is keyed by, named by its raw value. public enum Attribute: String, Sendable { /// The `name` attribute, keying the Twitter tags. case name @@ -26,7 +46,7 @@ extension SocialCardTag { case property } - /// The name of a ``SocialCardTag``, carried in its raw value. + /// The name of a ``SocialCard/Tag``, carried in its raw value. public enum Name: String, Sendable { /// The `og:description` tag, carrying the card's summary. case description = "og:description" @@ -34,10 +54,10 @@ extension SocialCardTag { case image = "og:image" /// The `og:image:alt` tag, carrying the share image's text for assistive technologies. case imageAlt = "og:image:alt" - /// The `og:image:width` tag, carrying the share image's width in pixels. - case imageWidth = "og:image:width" /// The `og:image:height` tag, carrying the share image's height in pixels. case imageHeight = "og:image:height" + /// The `og:image:width` tag, carrying the share image's width in pixels. + case imageWidth = "og:image:width" /// The `og:locale` tag, carrying the locale of the card's text. case locale = "og:locale" /// The `og:site_name` tag, carrying the name of the site the card belongs to. @@ -51,5 +71,18 @@ extension SocialCardTag { /// The `og:url` tag, carrying the absolute URL the card's page is served at. case url = "og:url" } - + +} + +// MARK: - Implementations + +public extension SocialCard.Tag.Name { + + // MARK: Computed + + /// The attribute keying a tag with this name: `property` for the Open Graph names, `name` for the Twitter ones. + var attribute: SocialCard.Tag.Attribute { + self == .twitter ? .name : .property + } + } diff --git a/Packages/Infrastructure/Tests/Cases/Public/Types/SocialCardTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Types/SocialCardTests.swift index 1282bb9..6849302 100644 --- a/Packages/Infrastructure/Tests/Cases/Public/Types/SocialCardTests.swift +++ b/Packages/Infrastructure/Tests/Cases/Public/Types/SocialCardTests.swift @@ -27,17 +27,17 @@ struct SocialCardTests { ) #expect(card.tags == [ - .init(attribute: .property, content: "website", name: .type), - .init(attribute: .property, content: "A Site", name: .siteName), - .init(attribute: .property, content: "A Title", name: .title), - .init(attribute: .property, content: "A summary.", name: .description), - .init(attribute: .property, content: "https://site.example/", name: .url), - .init(attribute: .property, content: "en", name: .locale), - .init(attribute: .property, content: "https://site.example/img/card.png", name: .image), - .init(attribute: .property, content: "2400", name: .imageWidth), - .init(attribute: .property, content: "1260", name: .imageHeight), - .init(attribute: .property, content: "An image.", name: .imageAlt), - .init(attribute: .name, content: "summary_large_image", name: .twitter), + .init("website", name: .type), + .init("A Site", name: .siteName), + .init("A Title", name: .title), + .init("A summary.", name: .description), + .init("https://site.example/", name: .url), + .init("en", name: .locale), + .init("https://site.example/img/card.png", name: .image), + .init("2400", name: .imageWidth), + .init("1260", name: .imageHeight), + .init("An image.", name: .imageAlt), + .init("summary_large_image", name: .twitter), ]) } @@ -46,9 +46,9 @@ struct SocialCardTests { let card = SocialCard(title: "A Title") #expect(card.tags == [ - .init(attribute: .property, content: "website", name: .type), - .init(attribute: .property, content: "A Title", name: .title), - .init(attribute: .name, content: "summary_large_image", name: .twitter), + .init("website", name: .type), + .init("A Title", name: .title), + .init("summary_large_image", name: .twitter), ]) } @@ -77,8 +77,15 @@ struct SocialCardTests { style: .summary ) - #expect(card.tags.contains(.init(attribute: .property, content: "article", name: .type))) - #expect(card.tags.contains(.init(attribute: .name, content: "summary", name: .twitter))) + #expect(card.tags.contains(.init("article", name: .type))) + #expect(card.tags.contains(.init("summary", name: .twitter))) + } + + @Test + func `keys a tag by the attribute its name dictates`() { + #expect(SocialCard.Tag.Name.twitter.attribute == .name) + #expect(SocialCard.Tag.Name.title.attribute == .property) + #expect(SocialCard.Tag("website", name: .type).attribute == .property) } }