Skip to content

Commit

Permalink
Add margin modifier. (#65)
Browse files Browse the repository at this point in the history
Part of #51.
  • Loading branch information
jverkoey authored Aug 3, 2024
1 parent 566fccf commit 1327a09
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 65 deletions.
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>"#)
}
}

0 comments on commit 1327a09

Please sign in to comment.