From 17b9a207663bc774827a4950b7863face35bd33c Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Fri, 9 Aug 2024 22:33:59 -0400 Subject: [PATCH] Add support for the crossorigin attribute. (#137) Closes https://github.com/jverkoey/slipstream/issues/136 --- .../Documentation.docc/Slipstream.md | 2 +- .../Documentation.docc/W3C/W3CAttributes.md | 4 ++++ .../W3C/Attributes/CrossOrigin.swift | 23 +++++++++++++++++++ .../Attributes/View+accessibilityLabel.swift | 1 + .../DocumentMetadata/Stylesheet.swift | 13 ++++++++++- .../W3C/Elements/EmbeddedContent/Image.swift | 13 ++++++++++- .../W3C/Elements/Scripting/Script.swift | 19 +++++++++++---- Tests/SlipstreamTests/W3C/ImageTests.swift | 5 ++++ Tests/SlipstreamTests/W3C/ScriptTests.swift | 5 ++++ .../SlipstreamTests/W3C/StylesheetTests.swift | 5 ++++ 10 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 Sources/Slipstream/W3C/Attributes/CrossOrigin.swift diff --git a/Sources/Slipstream/Documentation.docc/Slipstream.md b/Sources/Slipstream/Documentation.docc/Slipstream.md index cf4d5a6..9316e2a 100644 --- a/Sources/Slipstream/Documentation.docc/Slipstream.md +++ b/Sources/Slipstream/Documentation.docc/Slipstream.md @@ -82,8 +82,8 @@ print(try renderHTML(HelloWorld())) ### W3C -- - +- ### Rendering views diff --git a/Sources/Slipstream/Documentation.docc/W3C/W3CAttributes.md b/Sources/Slipstream/Documentation.docc/W3C/W3CAttributes.md index 092ea32..76c0ce6 100644 --- a/Sources/Slipstream/Documentation.docc/W3C/W3CAttributes.md +++ b/Sources/Slipstream/Documentation.docc/W3C/W3CAttributes.md @@ -13,3 +13,7 @@ ### Images - ``View/accessibilityLabel(_:)`` + +### Attribute types + +- ``CrossOrigin`` diff --git a/Sources/Slipstream/W3C/Attributes/CrossOrigin.swift b/Sources/Slipstream/W3C/Attributes/CrossOrigin.swift new file mode 100644 index 0000000..2fa6d63 --- /dev/null +++ b/Sources/Slipstream/W3C/Attributes/CrossOrigin.swift @@ -0,0 +1,23 @@ +/// Constants defining how a view handles cross-origin requests. +/// +/// - SeeAlso: W3C [crossorigin](https://html.spec.whatwg.org/#cors-settings-attributes) guidance. +/// +/// ## See Also +/// +/// - ``Image`` +/// - ``Script`` +/// - ``Stylesheet`` +@available(iOS 17.0, macOS 14.0, *) +public enum CrossOrigin: String { + /// Request uses CORS headers and credentials flag is set to 'same-origin'. + /// + /// There is no exchange of user credentials via cookies, client-side TLS + /// certificates or HTTP authentication, unless destination is the same + /// origin. + case anonymous = "" + + /// Request uses CORS headers and credentials flag is set to 'include'. + /// + /// User credentials are always included. + case useCredentials = "use-credentials" +} diff --git a/Sources/Slipstream/W3C/Attributes/View+accessibilityLabel.swift b/Sources/Slipstream/W3C/Attributes/View+accessibilityLabel.swift index e579d61..497421d 100644 --- a/Sources/Slipstream/W3C/Attributes/View+accessibilityLabel.swift +++ b/Sources/Slipstream/W3C/Attributes/View+accessibilityLabel.swift @@ -7,6 +7,7 @@ extension View { /// ## See Also /// /// - ``Image`` + @available(iOS 17.0, macOS 14.0, *) public func accessibilityLabel(_ string: String) -> some View { modifier(AttributeModifier("alt", value: string)) } diff --git a/Sources/Slipstream/W3C/Elements/DocumentMetadata/Stylesheet.swift b/Sources/Slipstream/W3C/Elements/DocumentMetadata/Stylesheet.swift index 3f1be2e..2177093 100644 --- a/Sources/Slipstream/W3C/Elements/DocumentMetadata/Stylesheet.swift +++ b/Sources/Slipstream/W3C/Elements/DocumentMetadata/Stylesheet.swift @@ -18,8 +18,15 @@ import SwiftSoup @available(iOS 17.0, macOS 14.0, *) public struct Stylesheet: View { /// Creates a Stylesheet view. - public init(_ url: URL?) { + /// + /// - Parameters: + /// - url: The stylesheet will be loaded from this URL. + /// - crossOrigin: If provided, configures the Cross-Origin Resource Sharing (CORS) + /// behavior for the request of this resource. If not provided, then the No CORS state is + /// implied. + public init(_ url: URL?, crossOrigin: CrossOrigin? = nil) { self.url = url + self.crossOrigin = crossOrigin } @_documentation(visibility: private) @@ -30,7 +37,11 @@ public struct Stylesheet: View { let element = try container.appendElement("link") try element.attr("rel", "stylesheet") try element.attr("href", url.absoluteString) + if let crossOrigin { + try element.attr("crossorigin", crossOrigin.rawValue) + } } private let url: URL? + private let crossOrigin: CrossOrigin? } diff --git a/Sources/Slipstream/W3C/Elements/EmbeddedContent/Image.swift b/Sources/Slipstream/W3C/Elements/EmbeddedContent/Image.swift index a953de6..fb08b7f 100644 --- a/Sources/Slipstream/W3C/Elements/EmbeddedContent/Image.swift +++ b/Sources/Slipstream/W3C/Elements/EmbeddedContent/Image.swift @@ -24,8 +24,15 @@ import SwiftSoup @available(iOS 17.0, macOS 14.0, *) public struct Image: View { /// Creates a Stylesheet view. - public init(_ url: URL?) { + /// + /// - Parameters: + /// - url: The image will be loaded from this URL. + /// - crossOrigin: If provided, configures the Cross-Origin Resource Sharing (CORS) + /// behavior for the request of this resource. If not provided, then the No CORS state is + /// implied. + public init(_ url: URL?, crossOrigin: CrossOrigin? = nil) { self.url = url + self.crossOrigin = crossOrigin } @_documentation(visibility: private) @@ -35,7 +42,11 @@ public struct Image: View { } let element = try container.appendElement("img") try element.attr("src", url.absoluteString) + if let crossOrigin { + try element.attr("crossorigin", crossOrigin.rawValue) + } } private let url: URL? + private let crossOrigin: CrossOrigin? } diff --git a/Sources/Slipstream/W3C/Elements/Scripting/Script.swift b/Sources/Slipstream/W3C/Elements/Scripting/Script.swift index 73861c5..7e64f5a 100644 --- a/Sources/Slipstream/W3C/Elements/Scripting/Script.swift +++ b/Sources/Slipstream/W3C/Elements/Scripting/Script.swift @@ -34,8 +34,16 @@ public enum ScriptExecutionMode: String { @available(iOS 17.0, macOS 14.0, *) public struct Script: View { /// Creates a script view pointing to a URL. - public init(_ url: URL?, executionMode: ScriptExecutionMode? = nil) { - self.storage = .url(url, executionMode: executionMode) + /// + /// - Parameters: + /// - url: The script will be loaded from this URL. + /// - crossOrigin: If provided, configures the Cross-Origin Resource Sharing (CORS) + /// behavior for the request of this resource. If not provided, then the No CORS state is + /// implied. + /// - executionMode: The execution mode for this script defines how and when the + /// script should be loaded in relation to the rest of the document. + public init(_ url: URL?, crossOrigin: CrossOrigin? = nil, executionMode: ScriptExecutionMode? = nil) { + self.storage = .url(url, crossOrigin: crossOrigin, executionMode: executionMode) } /// Creates a script view with inline source. @@ -46,12 +54,15 @@ public struct Script: View { @_documentation(visibility: private) public func render(_ container: Element, environment: EnvironmentValues) throws { switch storage { - case .url(let url, let executionMode): + case .url(let url, let crossOrigin, let executionMode): guard let url else { return } let element = try container.appendElement("script") try element.attr("src", url.absoluteString) + if let crossOrigin { + try element.attr("crossorigin", crossOrigin.rawValue) + } if let executionMode { try element.attr(executionMode.rawValue, "") } @@ -63,7 +74,7 @@ public struct Script: View { } private enum Storage { - case url(URL?, executionMode: ScriptExecutionMode?) + case url(URL?, crossOrigin: CrossOrigin?, executionMode: ScriptExecutionMode?) case inline(String) } diff --git a/Tests/SlipstreamTests/W3C/ImageTests.swift b/Tests/SlipstreamTests/W3C/ImageTests.swift index ec40a51..76c43e1 100644 --- a/Tests/SlipstreamTests/W3C/ImageTests.swift +++ b/Tests/SlipstreamTests/W3C/ImageTests.swift @@ -12,6 +12,11 @@ struct ImageTests { try #expect(renderHTML(Image(URL(string: "/logo.png"))) == #""#) } + @Test func crossOrigin() throws { + try #expect(renderHTML(Image(URL(string: "/logo.png"), crossOrigin: .anonymous)) == #""#) + try #expect(renderHTML(Image(URL(string: "/logo.png"), crossOrigin: .useCredentials)) == #""#) + } + @Test func accessibilityLabel() throws { try #expect(renderHTML(Image(URL(string: "/logo.png")).accessibilityLabel("My logo")) == #"My logo"#) } diff --git a/Tests/SlipstreamTests/W3C/ScriptTests.swift b/Tests/SlipstreamTests/W3C/ScriptTests.swift index a5c72ff..e795cb9 100644 --- a/Tests/SlipstreamTests/W3C/ScriptTests.swift +++ b/Tests/SlipstreamTests/W3C/ScriptTests.swift @@ -15,6 +15,11 @@ struct ScriptTests { try #expect(renderHTML(Script(URL(string: "/main.js"), executionMode: .defer)) == #""#) } + @Test func crossOrigin() throws { + try #expect(renderHTML(Script(URL(string: "/main.js"), crossOrigin: .anonymous)) == #""#) + try #expect(renderHTML(Script(URL(string: "/main.js"), crossOrigin: .useCredentials)) == #""#) + } + @Test func source() throws { try #expect(renderHTML(Script(""" alert("Hello, world!"); diff --git a/Tests/SlipstreamTests/W3C/StylesheetTests.swift b/Tests/SlipstreamTests/W3C/StylesheetTests.swift index e8c0ba8..818cc03 100644 --- a/Tests/SlipstreamTests/W3C/StylesheetTests.swift +++ b/Tests/SlipstreamTests/W3C/StylesheetTests.swift @@ -11,4 +11,9 @@ struct StylesheetTests { @Test func validURL() throws { try #expect(renderHTML(Stylesheet(URL(string: "/main.css"))) == #""#) } + + @Test func crossOrigin() throws { + try #expect(renderHTML(Stylesheet(URL(string: "/main.css"), crossOrigin: .anonymous)) == #""#) + try #expect(renderHTML(Stylesheet(URL(string: "/main.css"), crossOrigin: .useCredentials)) == #""#) + } }