Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add meta element viewport implementation. #39

Merged
merged 1 commit into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The complete W3C HTML elements standard can be found [here](https://html.spec.wh
- <doc:Head>
- <doc:Title>
- <doc:Charset>
- <doc:Viewport>

### Sections

Expand Down
120 changes: 120 additions & 0 deletions Sources/Slipstream/Views/W3C/Elements/DocumentMetadata/Viewport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Foundation

import SwiftSoup

/// A view that defines how the document should render its content in relation to the
/// browser's viewport.
///
/// ```swift
/// struct MySiteMetadata: View {
/// var body: some View {
/// Head {
/// Viewport(width: .deviceWidth, initialScale: 1)
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C public working draft for [`viewport`](https://drafts.csswg.org/css-viewport) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct Viewport: View {
/// A value that can be used for a viewport dimension.
public enum Dimension {
/// The viewport should be rendered using the device's width.
case deviceWidth
/// The viewport should be rendered using the device's height.
case deviceHeight
/// The viewport should be rendered at an exact pixel dimension.
case pixels(Int)

fileprivate var asString: String {
switch self {
case .deviceWidth: return "device-width"
case .deviceHeight: return "device-height"
case .pixels(let pixels): return "\(pixels)"
}
}
}

/// The standard viewport used for mobile-friendly websites.
public static let mobileFriendly = Self.init(width: .deviceWidth, initialScale: 1)

/// Creates a Viewport view.
///
/// If no parameters are provided, then this view will be treated like an ``EmptyView``.
///
/// - Parameters:
/// - width: The width at which the document should be rendered.
/// - height: The height at which the document should be rendered.
/// - initialScale: The initial scale at which the document should be rendered.
/// - minimumScale: The minimum scale allowed on the document. This affects the user's ability
/// to scale the web page and should rarely be used.
/// - maximumScale: The maximum scale allowed on the document. This affects the user's ability
/// to scale the web page and should rarely be used.
/// - userScalable: Whether or not the user is allowed to scale the document. Should rarely be used.
public init(
width: Dimension? = nil,
height: Dimension? = nil,
initialScale: Double? = nil,
minimumScale: Double? = nil,
maximumScale: Double? = nil,
userScalable: Bool? = nil
) {
self.width = width
self.height = height
self.initialScale = initialScale
self.minimumScale = minimumScale
self.maximumScale = maximumScale
self.userScalable = userScalable
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let content = [
("width", width?.asString),
("height", height?.asString),
("initial-scale", initialScale?.asString),
("minimum-scale", minimumScale?.asString),
("maximum-scale", maximumScale?.asString),
("user-scalable", userScalable?.asString),
].compactMap { (item: (String, String?)) -> (String, String)? in
guard let value = item.1 else {
return nil
}
return (item.0, value)
}.map { "\($0.0)=\($0.1)"}.joined(separator: ", ")

guard !content.isEmpty else {
return
}

let element = try container.appendElement("meta")
try element.attr("name", "viewport")
try element.attr("content", content)
}

private let width: Dimension?
private let height: Dimension?
private let initialScale: Double?
private let minimumScale: Double?
private let maximumScale: Double?
private let userScalable: Bool?
}

extension Double {
fileprivate var asString: String? {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
return formatter.string(from: self as NSNumber)
}
}

extension Bool {
fileprivate var asString: String? {
if self {
return "yes"
} else {
return "no"
}
}
}
2 changes: 2 additions & 0 deletions Tests/SlipstreamTests/Sites/CatalogSiteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ private struct CatalogSite: View {
Head {
Charset(.utf8)
Title("Build websites with Swift and Tailwind CSS — Slipstream")
Viewport.mobileFriendly
}
Body {
Text("Hello, world!")
Expand All @@ -24,6 +25,7 @@ struct CatalogSiteTests {
<head>
<meta charset="UTF-8" />
<title>Build websites with Swift and Tailwind CSS — Slipstream</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
Hello, world!
Expand Down
28 changes: 28 additions & 0 deletions Tests/SlipstreamTests/Views/W3C/ViewportTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Testing

import Slipstream

struct ViewportTests {
@Test func defaults() throws {
try #expect(renderHTML(Viewport()) == #""#)
}

@Test func mobileStandard() throws {
try #expect(renderHTML(Viewport.mobileFriendly) == #"<meta name="viewport" content="width=device-width, initial-scale=1" />"#)
}

@Test func fullyConfigured() throws {
try #expect(
renderHTML(
Viewport(
width: .pixels(500),
height: .deviceHeight,
initialScale: 2.0,
minimumScale: 0.5,
maximumScale: 5.0,
userScalable: false
)
) == #"<meta name="viewport" content="width=500, height=device-height, initial-scale=2, minimum-scale=0.5, maximum-scale=5, user-scalable=no" />"#
)
}
}