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 margin modifier. #65

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
12 changes: 12 additions & 0 deletions Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Margin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Margin

## Topics

### Modifiers

- ``View/margin(_:_:)``

### Enumerations

- ``Edge``
- ``MarginValue``
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Slipstream implementations of Tailwind CSS's classes.

### Spacing

- <doc:Margin>
- <doc:Padding>

### Typography
Expand Down
50 changes: 49 additions & 1 deletion Sources/Slipstream/TailwindCSS/Edge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public enum Edge: Int8, CaseIterable {
///
/// ```swift
/// let edges: Edge.Set = [.top, .left]
/// ```
/// ```
public struct Set: OptionSet {
/// The element type of the option set.
///
Expand Down Expand Up @@ -66,4 +66,52 @@ public enum Edge: Int8, CaseIterable {
/// A convenience option representing all edges (top, left, bottom, right).
public static let all: Edge.Set = [.top, .left, .bottom, .right]
}

/// Maps a point length to the closest Tailwind CSS spacing class.
///
/// - Parameter size: The size in points to be mapped.
/// - Returns: The Tailwind CSS spacing class.
static func pointToTailwindSpacingClass(ptLength: Double) -> String {
// Tailwind spacing classes and their corresponding sizes in points.
let mapping: [(name: String, ptLength: Double)] = [
("0", 0), // 0pt
("0.5", 2), // 0.125rem
("1", 4), // 0.25rem
("1.5", 6), // 0.375rem
("2", 8), // 0.5rem
("2.5", 10), // 0.625rem
("3", 12), // 0.75rem
("3.5", 14), // 0.875rem
("4", 16), // 1rem
("5", 20), // 1.25rem
("6", 24), // 1.5rem
("7", 28), // 1.75rem
("8", 32), // 2rem
("9", 36), // 2.25rem
("10", 40), // 2.5rem
("11", 44), // 2.75rem
("12", 48), // 3rem
("14", 56), // 3.5rem
("16", 64), // 4rem
("20", 80), // 5rem
("24", 96), // 6rem
("28", 112), // 7rem
("32", 128), // 8rem
("36", 144), // 9rem
("40", 160), // 10rem
("44", 176), // 11rem
("48", 192), // 12rem
("52", 208), // 13rem
("56", 224), // 14rem
("60", 240), // 15rem
("64", 256), // 16rem
("72", 288), // 18rem
("80", 320), // 20rem
("96", 384) // 24rem
]

// Find the closest matching spacing class
let closestClass = mapping.min { abs($0.ptLength - ptLength) < abs($1.ptLength - ptLength) }
return closestClass?.name ?? "0"
}
}
105 changes: 105 additions & 0 deletions Sources/Slipstream/TailwindCSS/Spacing/View+margin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/// A struct representing a margin value for layout purposes, with support
/// for both numerical values and the "auto" value.
@available(iOS 17.0, macOS 14.0, *)
public struct MarginValue {
/// A static instance representing the "auto" margin value.
public static let auto = MarginValue(.auto)

fileprivate init(integerLiteral value: Int) {
self.storage = .rawValue(Double(value))
}

fileprivate init(floatLiteral value: Double) {
self.storage = .rawValue(value)
}

fileprivate init(_ value: Storage) {
self.storage = value
}

fileprivate enum Storage {
case rawValue(Double)
case auto
}
fileprivate let storage: Storage

/// Returns the margin value as the closest Tailwind CSS spacing class.
///
/// - Returns: The Tailwind CSS spacing class string.
fileprivate func toTailwindSpacingClass() -> String {
switch storage {
case .rawValue(let ptLength):
return Edge.pointToTailwindSpacingClass(ptLength: ptLength)
case .auto:
return "auto"
}
}
}

extension View {
/// Set the margin for specific edges.
///
/// - Parameters:
/// - edges: The edges to which margin should be applied.
/// - length: The size of the margin to apply, in points. If the margin is exactly between
/// two margin classes, then the smaller margin class will be used.
///
/// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func margin(_ edges: Edge.Set, _ length: MarginValue) -> some View {
let spacingClass = length.toTailwindSpacingClass()
var classes = [String]()

if Edge.Set.all.isSubset(of: edges) {
classes = ["m-" + spacingClass]
} else {
if Edge.Set.horizontal.isSubset(of: edges) {
classes.append("mx-" + spacingClass)
} else {
if edges.contains(.left) {
classes.append("ml-" + spacingClass)
}
if edges.contains(.right) {
classes.append("mr-" + spacingClass)
}
}
if Edge.Set.vertical.isSubset(of: edges) {
classes.append("my-" + spacingClass)
} else {
if edges.contains(.top) {
classes.append("mt-" + spacingClass)
}
if edges.contains(.bottom) {
classes.append("mb-" + spacingClass)
}
}
}
return modifier(ClassModifier(add: classes.joined(separator: " ")))
}

/// Set the margin for specific edges.
///
/// - Parameters:
/// - edges: The edges to which margin should be applied.
/// - length: The size of the margin to apply, in points. If the margin is exactly between
/// two margin classes, then the smaller margin class will be used.
///
/// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func margin(_ edges: Edge.Set, _ length: Double) -> some View {
margin(edges, MarginValue(floatLiteral: length))
}

/// Set the margin for specific edges.
///
/// - Parameters:
/// - edges: The edges to which margin should be applied.
/// - length: The size of the margin to apply, in points. If the margin is exactly between
/// two margin classes, then the smaller margin class will be used.
///
/// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func margin(_ edges: Edge.Set, _ length: Int) -> some View {
margin(edges, MarginValue(integerLiteral: length))
}
}
67 changes: 9 additions & 58 deletions Sources/Slipstream/TailwindCSS/Spacing/View+padding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,82 +9,33 @@ extension View {
/// - SeeAlso: Tailwind CSS' [`padding`](https://tailwindcss.com/docs/padding) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func padding(_ edges: Edge.Set, _ length: Double) -> some View {
let paddingClass = closestTailwindPadding(ptLength: length)
let spacingClass = Edge.pointToTailwindSpacingClass(ptLength: length)
var classes = [String]()

if Edge.Set.all.isSubset(of: edges) {
classes = ["p-" + paddingClass]
classes = ["p-" + spacingClass]
} else {
if Edge.Set.horizontal.isSubset(of: edges) {
classes.append("px-" + paddingClass)
classes.append("px-" + spacingClass)
} else {
if edges.contains(.left) {
classes.append("pl-" + paddingClass)
classes.append("pl-" + spacingClass)
}
if edges.contains(.right) {
classes.append("pr-" + paddingClass)
classes.append("pr-" + spacingClass)
}
}
if Edge.Set.vertical.isSubset(of: edges) {
classes.append("py-" + paddingClass)
classes.append("py-" + spacingClass)
} else {
if edges.contains(.top) {
classes.append("pt-" + paddingClass)
classes.append("pt-" + spacingClass)
}
if edges.contains(.bottom) {
classes.append("pb-" + paddingClass)
classes.append("pb-" + spacingClass)
}
}
}
return self.modifier(ClassModifier(add: classes.joined(separator: " ")))
}

/// Map a point size to the closest Tailwind CSS padding class.
///
/// - Parameter ptLength: The size, in points, to be mapped.
/// - Returns: The Tailwind CSS padding class string.
@available(iOS 17.0, macOS 14.0, *)
private func closestTailwindPadding(ptLength: Double) -> String {
// Tailwind padding classes and their corresponding sizes in points
let paddingMapping: [(paddingClass: String, ptLength: Double)] = [
("0", 0), // 0pt
("0.5", 2), // 0.125rem
("1", 4), // 0.25rem
("1.5", 6), // 0.375rem
("2", 8), // 0.5rem
("2.5", 10), // 0.625rem
("3", 12), // 0.75rem
("3.5", 14), // 0.875rem
("4", 16), // 1rem
("5", 20), // 1.25rem
("6", 24), // 1.5rem
("7", 28), // 1.75rem
("8", 32), // 2rem
("9", 36), // 2.25rem
("10", 40), // 2.5rem
("11", 44), // 2.75rem
("12", 48), // 3rem
("14", 56), // 3.5rem
("16", 64), // 4rem
("20", 80), // 5rem
("24", 96), // 6rem
("28", 112), // 7rem
("32", 128), // 8rem
("36", 144), // 9rem
("40", 160), // 10rem
("44", 176), // 11rem
("48", 192), // 12rem
("52", 208), // 13rem
("56", 224), // 14rem
("60", 240), // 15rem
("64", 256), // 16rem
("72", 288), // 18rem
("80", 320), // 20rem
("96", 384) // 24rem
]

// Find the closest matching padding size class
let closestPadding = paddingMapping.min { abs($0.ptLength - ptLength) < abs($1.ptLength - ptLength) }
return closestPadding?.paddingClass ?? "0"
return modifier(ClassModifier(add: classes.joined(separator: " ")))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension View {
/// - SeeAlso: Tailwind CSS' [`font-size`](https://tailwindcss.com/docs/font-size) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func fontSize(_ fontSize: FontSize) -> some View {
return self.modifier(ClassModifier(add: "text-" + fontSize.rawValue))
return modifier(ClassModifier(add: "text-" + fontSize.rawValue))
}

/// Set the font size to the closest equivalent Tailwind CSS font size.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension View {
/// - SeeAlso: Tailwind CSS' [`font-smoothing`](https://tailwindcss.com/docs/font-smoothing) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func fontSmoothing(_ fontSmoothing: FontSmoothing) -> some View {
return self.modifier(ClassModifier(add: fontSmoothing.rawValue))
return modifier(ClassModifier(add: fontSmoothing.rawValue))
}

/// Set the font smoothing for a view to antialiased.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension View {
/// - SeeAlso: Tailwind CSS' [`font-weight`](https://tailwindcss.com/docs/font-weight) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func fontWeight(_ fontWeight: FontWeight) -> some View {
return self.modifier(ClassModifier(add: "font-" + fontWeight.rawValue))
return modifier(ClassModifier(add: "font-" + fontWeight.rawValue))
}

/// Set the font weight to the closest equivalent Tailwind CSS font weight.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ extension View {
/// - SeeAlso: Tailwind CSS' [`text-align`](https://tailwindcss.com/docs/text-align) documentation.
@available(iOS 17.0, macOS 14.0, *)
public func textAlignment(_ alignment: TextAlignment) -> some View {
return self.modifier(ClassModifier(add: "text-" + alignment.rawValue))
return modifier(ClassModifier(add: "text-" + alignment.rawValue))
}
}
6 changes: 4 additions & 2 deletions Tests/SlipstreamTests/Sites/CatalogSiteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ private struct CatalogSite: View {
H4("Heading 4")
.antialiased()
H5("Heading 5")
.margin(.horizontal, .auto)
H6("Heading 6")
.margin(.vertical, 32)
}
.padding(.horizontal, 48)
}
Expand Down Expand Up @@ -61,8 +63,8 @@ struct CatalogSiteTests {
<h2 class="text-3xl text-center">Heading 2</h2>
<h3 class="text-end">Heading 3</h3>
<h4 class="antialiased">Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h5 class="mx-auto">Heading 5</h5>
<h6 class="my-8">Heading 6</h6>
</div>
</body>
</html>
Expand Down
54 changes: 54 additions & 0 deletions Tests/SlipstreamTests/TailwindCSS/MarginTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Testing
import Slipstream

private struct MarginView: View {
let margins: Int = 32
var body: some View {
Div {

}
.margin(.horizontal, margins)
}
}

struct MarginTests {
@Test func auto() throws {
try #expect(renderHTML(Div {}.margin(.all, .auto)) == #"<div class="m-auto"></div>"#)
try #expect(renderHTML(Div {}.margin(.horizontal, .auto)) == #"<div class="mx-auto"></div>"#)
try #expect(renderHTML(Div {}.margin(.vertical, .auto)) == #"<div class="my-auto"></div>"#)
try #expect(renderHTML(Div {}.margin([.top, .left], .auto)) == #"<div class="ml-auto mt-auto"></div>"#)

try #expect(renderHTML(Div {}.margin(.top, .auto)) == #"<div class="mt-auto"></div>"#)
try #expect(renderHTML(Div {}.margin(.left, .auto)) == #"<div class="ml-auto"></div>"#)
try #expect(renderHTML(Div {}.margin(.bottom, .auto)) == #"<div class="mb-auto"></div>"#)
try #expect(renderHTML(Div {}.margin(.right, .auto)) == #"<div class="mr-auto"></div>"#)
}

@Test func marginEdges() throws {
try #expect(renderHTML(Div {}.margin(.all, 16)) == #"<div class="m-4"></div>"#)
try #expect(renderHTML(Div {}.margin(.horizontal, 8)) == #"<div class="mx-2"></div>"#)
try #expect(renderHTML(Div {}.margin(.vertical, 12)) == #"<div class="my-3"></div>"#)
try #expect(renderHTML(Div {}.margin([.top, .left], 24)) == #"<div class="ml-6 mt-6"></div>"#)

try #expect(renderHTML(Div {}.margin(.top, 0)) == #"<div class="mt-0"></div>"#)
try #expect(renderHTML(Div {}.margin(.left, 4)) == #"<div class="ml-1"></div>"#)
try #expect(renderHTML(Div {}.margin(.bottom, 32)) == #"<div class="mb-8"></div>"#)
try #expect(renderHTML(Div {}.margin(.right, 64)) == #"<div class="mr-16"></div>"#)
}

@Test func specificMarginSizes() throws {
try #expect(renderHTML(Div {}.margin(.top, 0)) == #"<div class="mt-0"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 0.5)) == #"<div class="mt-0"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 1)) == #"<div class="mt-0"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 2)) == #"<div class="mt-0.5"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 3)) == #"<div class="mt-0.5"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 4)) == #"<div class="mt-1"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 32)) == #"<div class="mt-8"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 64)) == #"<div class="mt-16"></div>"#)
}

@Test func repeatedMarginModifications() throws {
try #expect(renderHTML(Div {}.margin(.top, 0).margin(.right, 4)) == #"<div class="mt-0 mr-1"></div>"#)
try #expect(renderHTML(Div {}.margin(.top, 0).margin(.top, 4)) == #"<div class="mt-0 mt-1"></div>"#)
}
}