diff --git a/Sources/HTMLKit/Abstraction/Attributes/BasicAttributes.swift b/Sources/HTMLKit/Abstraction/Attributes/BasicAttributes.swift index bf4b3a9e..8a7b2c83 100644 --- a/Sources/HTMLKit/Abstraction/Attributes/BasicAttributes.swift +++ b/Sources/HTMLKit/Abstraction/Attributes/BasicAttributes.swift @@ -47,13 +47,52 @@ public protocol AcceptAttribute: Attribute { /// ```swift /// Input() /// .type(.file) - /// .accept("image/png, image/jpeg") + /// .accept(["image/png", "image/jpeg"]) /// ``` /// - /// - Parameter value: The file types to pick from. + /// - Parameter specifiers: The file types to pick from. /// /// - Returns: The element - func accept(_ value: String) -> Self + func accept(_ specifiers: [String]) -> Self + + /// Filter accepted file types for upload. + /// + /// ```swift + /// Input() + /// .type(.file) + /// .accept("image/png", "image/jpeg") + /// ``` + /// + /// - Parameter specifiers: The file types to pick from. + /// + /// - Returns: The element + func accept(_ specifiers: String...) -> Self + + /// Filter accepted file types for upload. + /// + /// ```swift + /// Input() + /// .type(.file) + /// .accept([.ogg, .mpeg]) + /// ``` + /// + /// - Parameter specifiers: The file types to pick from. + /// + /// - Returns: The element + func accept(_ specifiers: [Values.Media]) -> Self + + /// Filter accepted file types for upload. + /// + /// ```swift + /// Input() + /// .type(.file) + /// .accept(.ogg, .mpeg) + /// ``` + /// + /// - Parameter specifiers: The file types to pick from. + /// + /// - Returns: The element + func accept(_ specifiers: Values.Media...) -> Self } extension AcceptAttribute where Self: ContentNode { @@ -2113,14 +2152,34 @@ public protocol MediaAttribute: Attribute { /// /// ```swift /// Link() - /// .reference("https://...") - /// .media("print") + /// .reference("...css") + /// .media([ + /// MediaQuery(target: .screen, features: .orientation(.portrait)), + /// MediaQuery(target: .print, features: .resolution("300dpi")) + /// ]) /// ``` /// - /// - Parameter value: The media to be considered. + /// - Parameter queries: The media to be considered. /// /// - Returns: The element - func media(_ value: String) -> Self + func media(_ queries: [MediaQuery]) -> Self + + + /// Specify the media the ressource is optimized for. + /// + /// ```swift + /// Link() + /// .reference("...css") + /// .media( + /// MediaQuery(target: .screen, features: .orientation(.portrait)), + /// MediaQuery(target: .print, features: .resolution("300dpi")) + /// ) + /// ``` + /// + /// - Parameter queries: The media to be considered. + /// + /// - Returns: The element + func media(_ queries: MediaQuery...) -> Self } extension MediaAttribute where Self: ContentNode { @@ -3139,29 +3198,31 @@ extension SizeAttribute where Self: EmptyNode { @_documentation(visibility: internal) public protocol SizesAttribute: Attribute { + associatedtype SizesValueType + /// Describe different sizes for different viewport sizes. /// /// ```swift /// Link() - /// .sizes(16x16) + /// .sizes("16x16", "32x32") /// ``` /// - /// - Parameter size: The sizes to take into consideration. + /// - Parameter candidates: The sizes to take into consideration. /// /// - Returns: The element - func sizes(_ size: Int) -> Self + func sizes(_ candidates: [SizesValueType]) -> Self } extension SizesAttribute where Self: ContentNode { - internal func mutate(sizes value: Int) -> Self { + internal func mutate(sizes value: String) -> Self { return self.mutate(key: "sizes", value: value) } } extension SizesAttribute where Self: EmptyNode { - internal func mutate(sizes value: Int) -> Self { + internal func mutate(sizes value: String) -> Self { return self.mutate(key: "sizes", value: value) } } @@ -4062,21 +4123,33 @@ extension LoadingAttribute where Self: EmptyNode { @_documentation(visibility: internal) public protocol SourceSetAttribute: Attribute { - /// Set a source set for a picture element. + /// Define a set of sources for a picture element. /// /// ```swift /// Picture { /// Source() - /// .sourceSet("https://...") + /// .sourceSet([SourceCandidate("...webp", width: 1024), SourceCandidate("...webp", width: 1680)]) + /// } + /// ``` + /// + /// - Parameter candidates: The candidates to choose from. + /// + /// - Returns: The element. + func sourceSet(_ candidates: [SourceCandidate]) -> Self + + /// Define a set of sources for a picture element. + /// + /// ```swift + /// Picture { /// Source() - /// .sourceSet("https://...") + /// .sourceSet(SourceCandidate("...webp", width: 1024), SourceCandidate("...webp", width: 1680)) /// } /// ``` /// - /// - Parameter url: The url path to load from. + /// - Parameter candidates: The candidates to choose from. /// /// - Returns: The element. - func sourceSet(_ url: String) -> Self + func sourceSet(_ candidates: SourceCandidate...) -> Self } extension SourceSetAttribute where Self: ContentNode { diff --git a/Sources/HTMLKit/Abstraction/Attributes/VectorAttributes.swift b/Sources/HTMLKit/Abstraction/Attributes/VectorAttributes.swift index 5b3739bb..68661a90 100644 --- a/Sources/HTMLKit/Abstraction/Attributes/VectorAttributes.swift +++ b/Sources/HTMLKit/Abstraction/Attributes/VectorAttributes.swift @@ -256,32 +256,57 @@ public protocol PositionPointAttribute: Attribute { /// Vector { /// Rectangle { /// } - /// .positionPoint((50, 50)) + /// .position(x: 50, y: 50) /// } /// ``` - /// - Parameter point: The coodinates to position the shape. + /// + /// - Parameters: + /// - x: The horizontal coordinate to position the shape. + /// - y: The vertical coordinate to position the shape /// /// - Returns: The element - func positionPoint(_ point: (Int, Int)) -> Self + func position(x: Int, y: Int) -> Self + + /// Set the position of the shape. + /// + /// ```Swift + /// Vector { + /// Rectangle { + /// } + /// .position(x: 50.0, y: 50.0) + /// } + /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to position the shape. + /// - y: The vertical coordinate to position the shape + /// + /// - Returns: The element + func position(x: Double, y: Double) -> Self + + /// Set the position of the shape. + /// + /// ```Swift + /// Vector { + /// Rectangle { + /// } + /// .position(UnitPoint(x: 50, y: 50)) + /// } + /// ``` + /// - Parameter point: The coordinates to position the shape. + /// + /// - Returns: The element + func position(_ point: UnitPoint) -> Self } extension PositionPointAttribute where Self: ContentNode { - internal func mutate(positionpoint: (Int, Int)) -> Self { - - guard var attributes = self.attributes else { - - var attributes = OrderedDictionary() - attributes["x"] = positionpoint.0 - attributes["y"] = positionpoint.1 - - return .init(attributes: attributes, content: content) - } - - attributes["x"] = positionpoint.0 - attributes["y"] = positionpoint.1 - - return .init(attributes: attributes, content: content) + internal func mutate(x value: String) -> Self { + return self.mutate(key: "x", value: value) + } + + internal func mutate(y value: String) -> Self { + return self.mutate(key: "y", value: value) } } @@ -295,32 +320,57 @@ public protocol RadiusPointAttribute: Attribute { /// Vector { /// Rectangle { /// } - /// .radiusPoint((10, 10)) + /// .radius(x: 50, y: 50) + /// } + /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to round off corner. + /// - y: The vertical coordinate to round off corner. + /// + /// - Returns: The element + func radius(x: Int, y: Int) -> Self + + /// Apply a corner radius to the shape. + /// + /// ```swift + /// Vector { + /// Rectangle { + /// } + /// .radius(x: 50, y: 50) /// } /// ``` - /// - Parameter point: The radius to apply to all corners. + /// + /// - Parameters: + /// - x: The horizontal coordinate to round off corner. + /// - y: The vertical coordinate to round off corner. /// /// - Returns: The element - func radiusPoint(_ point: (Int, Int)) -> Self + func radius(x: Double, y: Double) -> Self + + /// Apply a corner radius to the shape. + /// + /// ```swift + /// Vector { + /// Rectangle { + /// } + /// .radius(UnitPoint(x: 50, y: 50)) + /// } + /// ``` + /// - Parameter point: The coordinates to round off corners. + /// + /// - Returns: The element + func radius(_ point: UnitPoint) -> Self } extension RadiusPointAttribute where Self: ContentNode { - internal func mutate(radiuspoint: (Int, Int)) -> Self { - - guard var attributes = self.attributes else { - - var attributes = OrderedDictionary() - attributes["rx"] = radiuspoint.0 - attributes["ry"] = radiuspoint.1 - - return .init(attributes: attributes, content: content) - } - - attributes["rx"] = radiuspoint.0 - attributes["ry"] = radiuspoint.1 - - return .init(attributes: attributes, content: content) + internal func mutate(rx value: String) -> Self { + return self.mutate(key: "rx", value: value) + } + + internal func mutate(ry value: String) -> Self { + return self.mutate(key: "ry", value: value) } } @@ -334,32 +384,59 @@ public protocol CenterPointAttribute: Attribute { /// Vector { /// Circle { /// } - /// .centerPoint((50, 50)) + /// .center(x: 50, y: 50) + /// } + /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to use as the center. + /// - y: The vertical coordinate to use as the center. + /// + /// - Returns: The element + + func center(x: Int, y: Int) -> Self + + /// Set the center point of the shape. + /// + /// ```swift + /// Vector { + /// Circle { + /// } + /// .center(x: 50.0, y: 50.0) + /// } + /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to use as the center. + /// - y: The vertical coordinate to use as the center. + /// + /// - Returns: The element + func center(x: Double, y: Double) -> Self + + + /// Set the center point of the shape. + /// + /// ```swift + /// Vector { + /// Circle { + /// } + /// .center(UnitPoint(x: 50, y: 50)) /// } /// ``` /// - Parameter point: The coordinates to use as the center. /// /// - Returns: The element - func centerPoint(_ point: (Int, Int)) -> Self + func center(_ point: UnitPoint) -> Self } extension CenterPointAttribute where Self: ContentNode { - internal func mutate(centerpoint: (Int, Int)) -> Self { - - guard var attributes = self.attributes else { - - var attributes = OrderedDictionary() - attributes["cx"] = centerpoint.0 - attributes["cy"] = centerpoint.1 - - return .init(attributes: attributes, content: content) - } - - attributes["cx"] = centerpoint.0 - attributes["cy"] = centerpoint.1 - - return .init(attributes: attributes, content: content) + internal func mutate(cx value: String) -> Self { + return self.mutate(key: "cx", value: value) + } + + internal func mutate(cy value: String) -> Self { + return self.mutate(key: "cy", value: value) } } @@ -368,17 +445,38 @@ extension CenterPointAttribute where Self: ContentNode { public protocol ViewBoxAttribute: Attribute { /// Set the view box for the vector. - /// + /// /// ```swift /// Vector { /// } - /// .viewBox("0 0 400 200") + /// .viewBox(x: 0, y: 0, width: 400, height: 200") /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to use for the origin. + /// - y: The vertical coordinate to use for the origin. + /// - width: The width of the viewport + /// - height: The height of the viewport + /// + /// - Returns: The element + func viewBox(x: Int, y: Int, width: Int, height: Int) -> Self + + /// Set the view box for the vector. /// - /// - Parameter value: The bounds used to define the viewport. - /// + /// ```swift + /// Vector { + /// } + /// .viewBox(x: 0.0, y: 0.0, width: 400.0, height: 200.0") + /// ``` + /// + /// - Parameters: + /// - x: The horizontal coordinate to use for the origin. + /// - y: The vertical coordinate to use for the origin. + /// - width: The width of the viewport + /// - height: The height of the viewport + /// /// - Returns: The element - func viewBox(_ value: String) -> Self + func viewBox(x: Double, y: Double, width: Double, height: Double) -> Self } extension ViewBoxAttribute where Self: ContentNode { diff --git a/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift b/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift index f040a31b..5e764d6c 100644 --- a/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/BodyElements.swift @@ -7181,6 +7181,14 @@ extension Anchor: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes, return mutate(media: value) } + public func media(_ queries: [MediaQuery]) -> Anchor { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + + public func media(_ queries: MediaQuery...) -> Anchor { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + public func ping(_ value: String) -> Anchor { return mutate(ping: value) } @@ -16870,12 +16878,25 @@ extension Image: GlobalAttributes, GlobalEventAttributes, GlobalAriaAttributes, return mutate(source: value) } + @available(*, deprecated, message: "Use the sourceSet(_:) modifier instead.") public func sourceSet(_ value: String) -> Image { return mutate(sourceset: value) } - public func sizes(_ size: Int) -> Image { - return mutate(sizes: size) + public func sourceSet(_ candidates: [SourceCandidate]) -> Image { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sourceSet(_ candidates: SourceCandidate...) -> Image { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sizes(_ candidates: [SizeCandidate]) -> Image { + return mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sizes(_ candidates: SizeCandidate...) -> Image { + return mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) } public func width(_ size: Int) -> Image { @@ -22520,10 +22541,19 @@ extension Vector: GlobalVectorAttributes, WidthAttribute, HeightAttribute, ViewB return self.mutate(style: TaintedString(value, as: .css(.attribute))) } + @available(*, deprecated, message: "Use the viewBox(x:y:width:height:) modifier instead.") public func viewBox(_ value: String) -> Vector { return self.mutate(viewbox: value) } + public func viewBox(x: Int, y: Int, width: Int, height: Int) -> Vector { + return self.mutate(viewbox: "\(x) \(y) \(width) \(height)") + } + + public func viewBox(x: Double, y: Double, width: Double, height: Double) -> Vector { + return self.mutate(viewbox: "\(x) \(y) \(width) \(height)") + } + public func fill(_ value: String) -> Vector { return self.mutate(fill: value) } diff --git a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift index 867f6b69..3e88f3f8 100644 --- a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift @@ -187,9 +187,21 @@ extension Input: GlobalAttributes, GlobalEventAttributes, AcceptAttribute, Alter return self } + + public func accept(_ specifiers: [String]) -> Input { + return mutate(accept: specifiers.joined(separator: ", ")) + } + + public func accept(_ specifiers: String...) -> Input { + return mutate(accept: specifiers.joined(separator: ", ")) + } + + public func accept(_ specifiers: [Values.Media]) -> Input { + return mutate(accept: specifiers.map { $0.rawValue }.joined(separator: ", ")) + } - public func accept(_ value: String) -> Input { - return mutate(accept: value) + public func accept(_ specifiers: Values.Media...) -> Input { + return mutate(accept: specifiers.map { $0.rawValue }.joined(separator: ", ")) } @_disfavoredOverload diff --git a/Sources/HTMLKit/Abstraction/Elements/HeadElements.swift b/Sources/HTMLKit/Abstraction/Elements/HeadElements.swift index 49c495f5..8be51e31 100644 --- a/Sources/HTMLKit/Abstraction/Elements/HeadElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/HeadElements.swift @@ -899,6 +899,14 @@ extension Style: GlobalAttributes, GlobalEventAttributes, TypeAttribute, MediaAt return mutate(media: value) } + public func media(_ queries: [MediaQuery]) -> Style { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + + public func media(_ queries: MediaQuery...) -> Style { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + public func blocking(_ value: Values.Blocking) -> Style { return mutate(blocking: value.rawValue) } @@ -1143,6 +1151,14 @@ extension Link: GlobalAttributes, GlobalEventAttributes, ReferenceAttribute, Ref return mutate(media: value) } + public func media(_ queries: [MediaQuery]) -> Link { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + + public func media(_ queries: MediaQuery...) -> Link { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + public func referrerPolicy(_ value: Values.Policy) -> Link { return mutate(referrerpolicy: value.rawValue) } @@ -1151,8 +1167,12 @@ extension Link: GlobalAttributes, GlobalEventAttributes, ReferenceAttribute, Ref return mutate(rel: value.rawValue) } - public func sizes(_ size: Int) -> Link { - return mutate(sizes: size) + public func sizes(_ candidates: [String]) -> Link { + return mutate(sizes: candidates.map { $0 }.joined(separator: " ")) + } + + public func sizes(_ candidates: String...) -> Link { + return mutate(sizes: candidates.map { $0 }.joined(separator: " ")) } public func type(_ value: Values.Media) -> Link { diff --git a/Sources/HTMLKit/Abstraction/Elements/MediaElements.swift b/Sources/HTMLKit/Abstraction/Elements/MediaElements.swift index 7ad995b1..99b0b7db 100644 --- a/Sources/HTMLKit/Abstraction/Elements/MediaElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/MediaElements.swift @@ -199,18 +199,39 @@ extension Source: GlobalAttributes, GlobalEventAttributes, TypeAttribute, Source return mutate(source: value) } + @available(*, deprecated, message: "Use the sourceSet(_:) modifier instead.") public func sourceSet(_ value: String) -> Source { return mutate(sourceset: value) } - public func sizes(_ size: Int) -> Source { - return mutate(sizes: size) + public func sourceSet(_ candidates: [SourceCandidate]) -> Source { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sourceSet(_ candidates: SourceCandidate...) -> Source { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sizes(_ candidates: [SizeCandidate]) -> Source { + return mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + public func sizes(_ candidates: SizeCandidate...) -> Source { + return mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) } public func media(_ value: String) -> Source { return mutate(media: value) } + public func media(_ queries: [MediaQuery]) -> Source { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + + public func media(_ queries: MediaQuery...) -> Source { + return mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + public func width(_ size: Int) -> Source { return mutate(width: size) } diff --git a/Sources/HTMLKit/Abstraction/Elements/VectorElements.swift b/Sources/HTMLKit/Abstraction/Elements/VectorElements.swift index 60069f67..fcd58049 100644 --- a/Sources/HTMLKit/Abstraction/Elements/VectorElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/VectorElements.swift @@ -87,8 +87,21 @@ extension Circle: GlobalVectorAttributes, CenterPointAttribute, RadiusAttribute return self.mutate(strokewidth: size) } + @available(*, deprecated, message: "Use the center(x:y:) modifier instead.") public func centerPoint(_ point: (Int, Int)) -> Circle { - return self.mutate(centerpoint: point) + return self.mutate(cx: "\(point.0)").mutate(cy: "\(point.1)") + } + + public func center(x: Int, y: Int) -> Circle { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") + } + + public func center(x: Double, y: Double) -> Circle { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") + } + + public func center(_ point: UnitPoint) -> Circle { + return self.mutate(cx: point.x).mutate(cy: point.y) } public func radius(_ size: Int) -> Circle { @@ -169,8 +182,8 @@ public struct Rectangle: ContentNode, VectorElement { } } -extension Rectangle: GlobalVectorAttributes, WidthAttribute, HeightAttribute, RadiusPointAttribute { - +extension Rectangle: GlobalVectorAttributes, WidthAttribute, HeightAttribute, RadiusPointAttribute, PositionPointAttribute { + public func id(_ value: String) -> Rectangle { return self.mutate(id: value) } @@ -199,8 +212,38 @@ extension Rectangle: GlobalVectorAttributes, WidthAttribute, HeightAttribute, Ra return self.mutate(strokewidth: size) } + @available(*, deprecated, message: "Use the radius(x:y:) modifier instead.") public func radiusPoint(_ point: (Int, Int)) -> Rectangle { - return self.mutate(radiuspoint: point) + return self.mutate(rx: "\(point.0)").mutate(ry: "\(point.1)") + } + + public func radius(x: Int, y: Int) -> Rectangle { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + public func radius(x: Double, y: Double) -> Rectangle { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + public func radius(_ point: UnitPoint) -> Rectangle { + return self.mutate(rx: point.x).mutate(ry: point.y) + } + + @available(*, deprecated, message: "Use the position(x:y:) modifier instead.") + public func positionPoint(_ point: (Int, Int)) -> Rectangle { + return self.mutate(x: "\(point.0)").mutate(y: "\(point.1)") + } + + public func position(x: Int, y: Int) -> Rectangle { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + public func position(x: Double, y: Double) -> Rectangle { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + public func position(_ point: UnitPoint) -> Rectangle { + return self.mutate(x: point.x).mutate(y: point.y) } public func width(_ size: Int) -> Rectangle { @@ -315,12 +358,38 @@ extension Ellipse: GlobalVectorAttributes, CenterPointAttribute, RadiusPointAttr return self.mutate(strokewidth: size) } + @available(*, deprecated, message: "Use the center(x:y:) modifier instead.") public func centerPoint(_ point: (Int, Int)) -> Ellipse { - return self.mutate(centerpoint: point) + return self.mutate(cx: "\(point.0)").mutate(cy: "\(point.1)") + } + + public func center(x: Int, y: Int) -> Ellipse { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") } + public func center(x: Double, y: Double) -> Ellipse { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") + } + + public func center(_ point: UnitPoint) -> Ellipse { + return self.mutate(cx: point.x).mutate(cy: point.y) + } + + @available(*, deprecated, message: "Use the radius(x:y:) modifier instead.") public func radiusPoint(_ point: (Int, Int)) -> Ellipse { - return self.mutate(radiuspoint: point) + return self.mutate(rx: "\(point.0)").mutate(ry: "\(point.1)") + } + + public func radius(x: Int, y: Int) -> Ellipse { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + public func radius(x: Double, y: Double) -> Ellipse { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + public func radius(_ point: UnitPoint) -> Ellipse { + return self.mutate(rx: point.x).mutate(ry: point.y) } public func fillOpacity(_ value: Double) -> Ellipse { @@ -926,7 +995,7 @@ public struct Use: ContentNode, VectorElement { } } -extension Use: GlobalVectorAttributes, ReferenceAttribute, WidthAttribute, HeightAttribute { +extension Use: GlobalVectorAttributes, ReferenceAttribute, WidthAttribute, HeightAttribute, PositionPointAttribute { public func id(_ value: String) -> Use { return self.mutate(id: value) @@ -940,6 +1009,23 @@ extension Use: GlobalVectorAttributes, ReferenceAttribute, WidthAttribute, Heigh return self.mutate(href: value) } + @available(*, deprecated, message: "Use the position(x:y:) modifier instead.") + public func positionPoint(_ point: (Int, Int)) -> Use { + return self.mutate(x: "\(point.0)").mutate(y: "\(point.1)") + } + + public func position(x: Int, y: Int) -> Use { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + public func position(x: Double, y: Double) -> Use { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + public func position(_ point: UnitPoint) -> Use { + return self.mutate(x: point.x).mutate(y: point.y) + } + public func width(_ size: Int) -> Use { return self.mutate(width: size) } diff --git a/Sources/HTMLKit/Abstraction/Types/MediaQuery.swift b/Sources/HTMLKit/Abstraction/Types/MediaQuery.swift new file mode 100644 index 00000000..6b7b5b64 --- /dev/null +++ b/Sources/HTMLKit/Abstraction/Types/MediaQuery.swift @@ -0,0 +1,172 @@ +/// A type that represents a media query. +/// +/// The query is used to define the conditions under which the resource should then be applied. +/// +/// ```swift +/// Link() +/// .reference("https://...") +/// .media( +/// MediaQuery(.screen, features: .orientation(.landscape)), +/// MediaQuery(.print, features: .resolution("300dpi")) +/// ) +/// ``` +public struct MediaQuery { + + /// An enumeration of potential interface orientations. + public enum InterfaceOrientation { + + /// Indicates a landscape orientation. + case landscape + + /// Indicates a portrait orientation. + case portrait + + internal var rawValue: String { + + switch self { + case .landscape: + return "landscape" + + case .portrait: + return "portrait" + } + } + } + + /// An enumeration of potential media features. + public enum MediaFeature { + + /// Specifies the minimum target width. + case minWidth(String) + + /// Specifies the target width. + case width(String) + + /// Specifies the maximum target width. + case maxWidth(String) + + /// Specifies the minimum target height. + case minHeight(String) + + /// Specifies the target height. + case height(String) + + /// Specifies the maximum target height. + case maxHeight(String) + + /// Specifies the aspect ratio. + case aspectRatio(String) + + /// Specifies the interface orientation. + case orientation(InterfaceOrientation) + + /// Specifies the minimum display resolution. + case minResolution(String) + + /// Specifies the display resolution. + case resolution(String) + + /// Specifies the maximum display resolution. + case maxResolution(String) + + /// Specifies the color depth. + case color(Int?) + + internal var rawValue: String { + + switch self { + case .minWidth(let length): + return "(min-width: \(length))" + + case .width(let length): + return "(width: \(length))" + + case .maxWidth(let length): + return "(max-width: \(length))" + + case .minHeight(let length): + return "(min-height: \(length))" + + case .height(let length): + return "(height: \(length))" + + case .maxHeight(let length): + return "(max-height: \(length))" + + case .aspectRatio(let ratio): + return "(aspect-ratio: \(ratio))" + + case .orientation(let orientation): + return "(orientation: \(orientation.rawValue))" + + case .minResolution(let pixel): + return "(min-resolution: \(pixel))" + + case .resolution(let pixel): + return "(resolution: \(pixel))" + + case .maxResolution(let pixel): + return "(max-resolution: \(pixel))" + + case .color(let depth): + + if let depth { + return "(color: \(depth))" + } + + return "(color)" + } + } + } + + /// An enumeration of potential media devices. + public enum MediaTarget: String { + + /// Matches all devices. + case all + + /// Matches screen devices. + case screen + + /// Matches printer devices. + case print + } + + /// The target of the query. + internal let target: MediaTarget + + /// The potential features of the query. + internal let features: [MediaFeature]? + + /// The raw representation of the type. + internal var rawValue: String { + + if let features = self.features { + return "\(target.rawValue) and \(features.map { $0.rawValue }.joined(separator: " and "))" + } + + return "\(target.rawValue)" + } + + /// Create a media query. + /// + /// - Parameters: + /// - target: The media to target. + /// - features: The features to match to the target. + public init(_ target: MediaTarget, features: [MediaFeature]? = nil) { + + self.target = target + self.features = features + } + + /// Create a media query. + /// + /// - Parameters: + /// - target: The media to target. + /// - features: The features to match to the target. + public init(_ target: MediaTarget, features: MediaFeature...) { + + self.target = target + self.features = features + } +} diff --git a/Sources/HTMLKit/Abstraction/Types/SizeCandidate.swift b/Sources/HTMLKit/Abstraction/Types/SizeCandidate.swift new file mode 100644 index 00000000..85e73242 --- /dev/null +++ b/Sources/HTMLKit/Abstraction/Types/SizeCandidate.swift @@ -0,0 +1,109 @@ +/// A type that represents a size candidate. +/// +/// The candidate is used to define the conditions under which the size should then be applied. +/// +/// ```swift +/// Image() +/// .source("...png") +/// .sourceSet(..., ...) +/// .sizes( +/// SizeCandidate("100vw", conditions: .maxWidth("1680px")), +/// SizeCandidate("80vw") +/// ) +/// ``` +public struct SizeCandidate { + + /// An enumeration of potential interface orientations. + public enum InterfaceOrientation { + + /// Indicates a landscape orientation. + case landscape + + /// Indicates a portrait orientation. + case portrait + + /// The raw representation of the type. + internal var rawValue: String { + + switch self { + case .landscape: + return "landscape" + + case .portrait: + return "portrait" + } + } + } + + /// An enumeration of potential width conditions. + public enum SizeCondition { + + /// Specifies the maximum width. + case maxWidth(String) + + /// Specifies the target width. + case width(String) + + /// Specifies the minimum width. + case minWidth(String) + + /// Specifies a interface orientation. + case orientation(InterfaceOrientation) + + /// The raw representation of the type. + internal var rawValue: String { + + switch self { + case .maxWidth(let width): + return "(max-width: \(width))" + + case .width(let width): + return "(width: \(width))" + + case .minWidth(let width): + return "(min-width: \(width))" + + case .orientation(let orientation): + return "(orientation: \(orientation.rawValue))" + } + } + } + + /// The size of the candidate. + internal let size: String + + /// The potential conditions of the candidate. + internal let conditions: [SizeCondition]? + + /// The raw representation of the type. + internal var rawValue: String { + + if let conditions = self.conditions { + return "\(conditions.map { $0.rawValue }.joined(separator: " and ")) \(size)" + } + + return size + } + + /// Create a size candidate. + /// + /// - Parameters: + /// - size: The width to apply. + /// - conditions: The conditions under which the size should be applied. + public init(_ size: String, conditions: [SizeCondition]? = nil) { + + self.size = size + self.conditions = conditions + } + + /// Create a size candidate. + /// + /// - Parameters: + /// - size: The width to apply. + /// - conditions: The conditions under which the size should be applied. + public init(_ size: String, conditions: SizeCondition...) { + + self.size = size + self.conditions = conditions + } +} diff --git a/Sources/HTMLKit/Abstraction/Types/SourceCandidate.swift b/Sources/HTMLKit/Abstraction/Types/SourceCandidate.swift new file mode 100644 index 00000000..6a529959 --- /dev/null +++ b/Sources/HTMLKit/Abstraction/Types/SourceCandidate.swift @@ -0,0 +1,104 @@ +import Foundation + +/// A type that represents a source candidate. +/// +/// The candidate is used to define an alternative source with an additional condition. +/// +/// ```swift +/// Image() +/// .source("...png") +/// .sourceSet( +/// SourceCandidate("...png", width: 1024), +/// SourceCandiate("...png", width: 1680) +/// ) +/// ``` +public struct SourceCandidate { + + /// An enumeration of potential pixel densities. + public enum PixelDensity { + + /// Specifies a 1:1 density. + case regular + + /// Specifies a 2:1 density. + case high + + /// Specifies a 3:1 density. + case ultra + + /// The raw representation of the type. + internal var rawValue: String { + + switch self { + case .regular: + return "1x" + + case .high: + return "2x" + + case .ultra: + return "3x" + } + } + } + + /// The source path of the candidate. + internal let source: String + + /// The potential condition of the candidate. + internal let condition: String? + + /// The raw representation of the type. + internal var rawValue: String { + + if let condition = self.condition { + return "\(source) \(condition)" + } + + return source + } + + /// Create a source candidate. + /// + /// - Parameters: + /// - source: The url path to load from. + /// - width: The width to apply. + public init(_ source: String) { + + self.source = source + self.condition = nil + } + + /// Create a source candidate. + /// + /// - Parameters: + /// - source: The url path to load from. + /// - width: The width to apply. + public init(_ source: String, width: Int) { + + self.source = source + self.condition = "\(width)w" + } + + /// Create a source candidate. + /// + /// - Parameters: + /// - source: The url path to load from. + /// - density: The density to apply. + public init(_ source: String, density: Int) { + + self.source = source + self.condition = "\(density)x" + } + + /// Create a source candidate. + /// + /// - Parameters: + /// - source: The url path to load from. + /// - density: The density to apply. + public init(_ source: String, density: PixelDensity) { + + self.source = source + self.condition = density.rawValue + } +} diff --git a/Sources/HTMLKit/Abstraction/Types/UnitPoint.swift b/Sources/HTMLKit/Abstraction/Types/UnitPoint.swift new file mode 100644 index 00000000..a62e5ccc --- /dev/null +++ b/Sources/HTMLKit/Abstraction/Types/UnitPoint.swift @@ -0,0 +1,77 @@ +/// A type represents a unit point. +/// +/// The point is used to define a position by two coordinates. +/// +/// ```swift +/// Vector { +/// Circle { +/// } +/// .center(UnitPoint(x: 50, y: 50)) +/// } +/// ``` +public struct UnitPoint { + + /// An enumeration of potential point formats. + public enum PointFormat { + + /// Indicates an absolute value. + case absolute + + /// Indicates an relative value. + case relative + + /// Returns the string representation based on the format. + func string(from value: Int) -> String { + + switch self { + case .absolute: + return "\(value)" + + case .relative: + return "\(value)%" + } + } + + /// Returns the string representation based on the format. + func string(from value: Double) -> String { + + switch self { + case .absolute: + return "\(value)" + + case .relative: + return "\(value)%" + } + } + } + + /// The horizontal coordinate of the point. + internal var x: String + + /// The vertical coordinate of the point. + internal var y: String + + /// Create a point. + /// + /// - Parameters: + /// - x: The horizontal coordinate to place the point. + /// - y: The vertical coordinate to place the point. + /// - format: Whether the coordinates should be relative. + public init(x: Double, y: Double, format: PointFormat = .absolute) { + + self.x = format.string(from: x) + self.y = format.string(from: y) + } + + /// Create a point. + /// + /// - Parameters: + /// - x: The horizontal coordinate to place the point. + /// - y: The vertical coordinate to place the point. + /// - format: Whether the coordinates should be relative. + public init(x: Int, y: Int, format: PointFormat = .absolute) { + + self.x = format.string(from: x) + self.y = format.string(from: y) + } +} diff --git a/Tests/HTMLKitTests/AttributesTests.swift b/Tests/HTMLKitTests/AttributesTests.swift index f671e29b..2036c5a5 100644 --- a/Tests/HTMLKitTests/AttributesTests.swift +++ b/Tests/HTMLKitTests/AttributesTests.swift @@ -160,8 +160,20 @@ final class AttributesTests: XCTestCase { return self.mutate(translate: value.rawValue) } - func accept(_ value: String) -> Tag { - return self.mutate(accept: value) + func accept(_ specifiers: [String]) -> Tag { + return self.mutate(accept: specifiers.joined(separator: ", ")) + } + + func accept(_ specifiers: String...) -> Tag { + return self.mutate(accept: specifiers.joined(separator: ", ")) + } + + func accept(_ specifiers: [Values.Media]) -> Tag { + return self.mutate(accept: specifiers.map { $0.rawValue }.joined(separator: ", ")) + } + + func accept(_ specifiers: Values.Media...) -> Tag { + return self.mutate(accept: specifiers.map { $0.rawValue }.joined(separator: ", ")) } func action(_ value: String) -> Tag { @@ -357,6 +369,14 @@ final class AttributesTests: XCTestCase { return self.mutate(media: value) } + func media(_ queries: [MediaQuery]) -> Tag { + return self.mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + + func media(_ queries: MediaQuery...) -> Tag { + return self.mutate(media: queries.map { $0.rawValue }.joined(separator: ", ")) + } + func method(_ value: HTMLKit.Values.Method) -> Tag { return self.mutate(method: value.rawValue) } @@ -497,8 +517,12 @@ final class AttributesTests: XCTestCase { return self.mutate(size: size) } - func sizes(_ size: Int) -> Tag { - return self.mutate(sizes: size) + func sizes(_ candidates: [SizeCandidate]) -> Tag { + return self.mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + func sizes(_ candidates: SizeCandidate...) -> Tag { + return self.mutate(sizes: candidates.map { $0.rawValue }.joined(separator: ", ")) } func slot(_ value: String) -> Tag { @@ -525,8 +549,12 @@ final class AttributesTests: XCTestCase { return mutate(sourcelanguage: value.rawValue) } - func sourceSet(_ value: String) -> Tag { - return mutate(sourceset: value) + func sourceSet(_ candidates: [SourceCandidate]) -> Tag { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) + } + + func sourceSet(_ candidates: SourceCandidate...) -> Tag { + return mutate(sourceset: candidates.map { $0.rawValue }.joined(separator: ", ")) } func start(_ size: Int) -> Tag { @@ -616,21 +644,65 @@ final class AttributesTests: XCTestCase { } func positionPoint(_ point: (Int, Int)) -> Tag { - return self.mutate(positionpoint: point) + return self.mutate(x: "\(point.0)").mutate(y: "\(point.1)") + } + + func position(x: Int, y: Int) -> Tag { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + func position(x: Double, y: Double) -> Tag { + return self.mutate(x: "\(x)").mutate(y: "\(y)") + } + + func position(_ point: UnitPoint) -> Tag { + return self.mutate(x: point.x).mutate(y: point.y) } func radiusPoint(_ point: (Int, Int)) -> Tag { - return self.mutate(radiuspoint: point) + return self.mutate(rx: "\(point.0)").mutate(ry: "\(point.1)") + } + + func radius(x: Int, y: Int) -> Tag { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + func radius(x: Double, y: Double) -> Tag { + return self.mutate(rx: "\(x)").mutate(ry: "\(y)") + } + + func radius(_ point: HTMLKit.UnitPoint) -> Tag { + return self.mutate(rx: point.x).mutate(ry: point.y) } func centerPoint(_ point: (Int, Int)) -> Tag { - return self.mutate(centerpoint: point) + return self.mutate(cx: "\(point.0)").mutate(cy: "\(point.1)") + } + + func center(x: Int, y: Int) -> Tag { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") + } + + func center(x: Double, y: Double) -> Tag { + return self.mutate(cx: "\(x)").mutate(cy: "\(y)") + } + + func center(_ point: UnitPoint) -> Tag { + return self.mutate(cx: point.x).mutate(cy: point.y) } func viewBox(_ value: String) -> Tag { return self.mutate(viewbox: value) } + func viewBox(x: Int, y: Int, width: Int, height: Int) -> Tag { + return self.mutate(viewbox: "\(x) \(y) \(width) \(height)") + } + + func viewBox(x: Double, y: Double, width: Double, height: Double) -> Tag { + return self.mutate(viewbox: "\(x) \(y) \(width) \(height)") + } + func namespace(_ value: String) -> Tag { return self.mutate(namespace: value) } @@ -1075,12 +1147,20 @@ final class AttributesTests: XCTestCase { func testAcceptAttribute() throws { let view = TestView { - Tag {}.accept("accept") + Tag {}.accept("image/*") + Tag {}.accept([".jpg", ".png", ".svg"]) + Tag {}.accept(".jpg", ".png", ".svg") + Tag {}.accept([.ogg, .mpeg]) + Tag {}.accept(.ogg, .mpeg) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + \ + """ ) } @@ -1627,12 +1707,16 @@ final class AttributesTests: XCTestCase { func testMediaAttribute() throws { let view = TestView { - Tag {}.media("print and (resolution:300dpi)") + Tag {}.media(MediaQuery(.all, features: .orientation(.landscape), .resolution("300dpi"))) + Tag {}.media(MediaQuery(.all), MediaQuery(.print)) + Tag {}.media(MediaQuery(.all), MediaQuery(.print, features: [.maxHeight("20vh")])) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + """ ) } @@ -2023,12 +2107,26 @@ final class AttributesTests: XCTestCase { func testSizesAttribute() throws { let view = TestView { - Tag {}.sizes(2) + Tag {}.sizes(SizeCandidate("auto")) + Tag {}.sizes(SizeCandidate("100vw", conditions: .orientation(.landscape))) + Tag {}.sizes(SizeCandidate("100vw", conditions: .orientation(.portrait))) + Tag {}.sizes(SizeCandidate("100vw", conditions: .orientation(.landscape), .width("50em"))) + Tag {}.sizes(SizeCandidate("calc(100vw - 100px)", conditions: .minWidth("50em"))) + Tag {}.sizes(SizeCandidate("100vw", conditions: .maxWidth("50em"))) + Tag {}.sizes([SizeCandidate("100vw"), SizeCandidate("100vw", conditions: .maxWidth("50em"))]) + Tag {}.sizes(SizeCandidate("100vw"), SizeCandidate("100vw", conditions: .maxWidth("50em"))) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + \ + \ + \ + \ + """ ) } @@ -2101,12 +2199,20 @@ final class AttributesTests: XCTestCase { func testSourceSetAttribute() throws { let view = TestView { - Tag {}.sourceSet("img2.png 100w, img3.png 500w") + Tag {}.sourceSet(SourceCandidate("img.webp")) + Tag {}.sourceSet(SourceCandidate("img.png", density: 4)) + Tag {}.sourceSet(SourceCandidate("img.png", density: .ultra)) + Tag {}.sourceSet(SourceCandidate("img.png", width: 1024)) + Tag {}.sourceSet(SourceCandidate("img.png", width: 1024), SourceCandidate("img.png", density: .ultra)) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + \ + """ ) } @@ -2891,15 +2997,21 @@ final class AttributesTests: XCTestCase { ) } - func testPositionPointAttribute() throws { + func testPositionAttribute() throws { let view = TestView { - Tag {}.positionPoint((10,10)) + Tag {}.position(x: 50, y: 50) + Tag {}.position(x: 50.0, y: 50.0) + Tag {}.position(UnitPoint(x: 50.0, y: 50.0)) + Tag {}.position(UnitPoint(x: 50, y: 50, format: .relative)) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + """ ) } @@ -2907,12 +3019,18 @@ final class AttributesTests: XCTestCase { func testRadiusPointAttribute() throws { let view = TestView { - Tag {}.radiusPoint((10,10)) + Tag {}.radius(x: 10, y: 10) + Tag {}.radius(x: 10.0, y: 10.0) + Tag {}.radius(UnitPoint(x: 10.0, y: 10.0)) + Tag {}.radius(UnitPoint(x: 10, y: 10, format: .relative)) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + """ ) } @@ -2920,12 +3038,18 @@ final class AttributesTests: XCTestCase { func testCenterPointAttribute() throws { let view = TestView { - Tag {}.centerPoint((10,10)) + Tag {}.center(x: 10, y: 10) + Tag {}.center(x: 10.0, y: 10.0) + Tag {}.center(UnitPoint(x: 10.0, y: 10.0)) + Tag {}.center(UnitPoint(x: 10, y: 10, format: .relative)) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + \ + \ + """ ) } @@ -2933,12 +3057,14 @@ final class AttributesTests: XCTestCase { func testViewBoxAttribute() throws { let view = TestView { - Tag {}.viewBox("0 0 100 100") + Tag {}.viewBox(x: 0, y: 0, width: 100, height: 100) + Tag {}.viewBox(x: 0, y: 0, width: 100.0, height: 100.0) } XCTAssertEqual(try renderer.render(view: view), """ - + \ + """ ) }