diff --git a/Sources/SwiftCss/CSS/CSSRepresentable.swift b/Sources/SwiftCss/CSS/CSSRepresentable.swift deleted file mode 100644 index fa104bf..0000000 --- a/Sources/SwiftCss/CSS/CSSRepresentable.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// CSSRepresentable.swift -// SwiftCss -// -// Created by Tibor Bodecs on 2021. 07. 09.. -// - -public protocol CSSRepresentable { - var css: String { get } -} diff --git a/Sources/SwiftCss/CSS/Stylesheet.swift b/Sources/SwiftCss/CSS/Stylesheet.swift deleted file mode 100644 index d344575..0000000 --- a/Sources/SwiftCss/CSS/Stylesheet.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Stylesheet.swift -// SwiftCss -// -// Created by Tibor Bodecs on 2021. 07. 09.. -// - -//struct Stylesheet { -// -// let color1 = "red" -// let color2 = "red" -// -// -// func general() -> [Selector] { -// -// } -// -// func xs() -> [Selector] { //600< -// -// } -// -// func s() -> [Selector] { //600+899 -// -// } -// -// func normal() -> [Selector] { //900-12 -// -// } -// -// func l() -> [Selector] {//1800+ -// -// } -// -// func xl() -> [Selector] {//1800+ -// -// } -// -// func dark() -> [Selector] { -// -// } -// -// func standalone() -> [Selector] { -// -// } -//} - -///* general style comes here (mobile first) */ -// -//@media screen and (max-width: 599px) { -// /* extra small device screens only < 600 */ -//} -//@media screen and (min-width: 600px) { -// /* small device screens: 600-899 */ -//} -//@media screen and (min-width: 900px) { -// /* "normal" device screens: 900-1199 */ -//} -//@media screen and (min-width: 1200px) { -// /* large device screens: 1200-1799 */ -//} -//@media screen and (min-width: 1800px) { -// /* extra large device screens: 1800+ */ -//} -// -///* light mode */ -//@media (prefers-color-scheme: dark) { -// /* dark mode */ -//} diff --git a/Sources/SwiftCss/Components/CSSColor.swift b/Sources/SwiftCss/Components/CSSColor.swift index 8ebb24b..4a1f14c 100644 --- a/Sources/SwiftCss/Components/CSSColor.swift +++ b/Sources/SwiftCss/Components/CSSColor.swift @@ -7,69 +7,69 @@ public struct CSSColor: ExpressibleByStringLiteral { - private var css: String + private var colorValue: String init(raw: String) { - css = raw + colorValue = raw } // init(hex: String) { // css = hex // } - + public init(stringLiteral value: StringLiteralType) { - css = value + colorValue = value /// check if length is valid (000, #000, cafe00, #cafe00) assert([3,4,6,7].contains(value.count), "Invalid hex string") /// add # prefix if missing - if !css.hasPrefix("#") { - css = "#" + css + if !colorValue.hasPrefix("#") { + colorValue = "#" + colorValue } } public init(r: Int, g: Int, b: Int, a: Double? = nil) { - css = "\(r),\(g),\(b)" + colorValue = "\(r),\(g),\(b)" if let a = a { - css = "rgba(" + css + ", \(a))" + colorValue = "rgba(" + colorValue + ", \(a))" } else { - css = "rgb(" + css + ")" + colorValue = "rgb(" + colorValue + ")" } } public init(r: Double, g: Double, b: Double, a: Double? = nil) { - css = "\(r)%,\(g)%,\(b)%" + colorValue = "\(r)%,\(g)%,\(b)%" if let a = a { - css = "rgba(" + css + ", \(a))" + colorValue = "rgba(" + colorValue + ", \(a))" } else { - css = "rgb(" + css + ")" + colorValue = "rgb(" + colorValue + ")" } } public init(h: Int, s: Int, l: Int, a: Double? = nil) { - css = "\(h),\(s),\(l)" + colorValue = "\(h),\(s),\(l)" if let a = a { - css = "hsla(" + css + ", \(a))" + colorValue = "hsla(" + colorValue + ", \(a))" } else { - css = "hsl(" + css + ")" + colorValue = "hsl(" + colorValue + ")" } } public init(h: Double, s: Double, l: Double, a: Double? = nil) { - css = "\(h)%,\(s)%,\(l)%" + colorValue = "\(h)%,\(s)%,\(l)%" if let a = a { - css = "hsla(" + css + ", \(a))" + colorValue = "hsla(" + colorValue + ", \(a))" } else { - css = "hsl(" + css + ")" + colorValue = "hsl(" + colorValue + ")" } } var rawValue: String { - css + colorValue } } diff --git a/Sources/SwiftCss/Components/Property.swift b/Sources/SwiftCss/Components/Property.swift index 28e2db1..bf3649b 100644 --- a/Sources/SwiftCss/Components/Property.swift +++ b/Sources/SwiftCss/Components/Property.swift @@ -6,7 +6,7 @@ // /// https://www.w3schools.com/cssref/ -public struct Property: CSSRepresentable { +public struct Property { var name: String var value: String var isImportant: Bool = false @@ -16,11 +16,7 @@ public struct Property: CSSRepresentable { self.value = value self.isImportant = isImportant } - - public var css: String { - "\t" + name + ": " + value + (isImportant ? " !important" : "") + ";\n" - } - + public func important() -> Property { guard !isImportant else { return self diff --git a/Sources/SwiftCss/Components/Rule.swift b/Sources/SwiftCss/Components/Rule.swift index de3d4f0..bd24188 100644 --- a/Sources/SwiftCss/Components/Rule.swift +++ b/Sources/SwiftCss/Components/Rule.swift @@ -5,6 +5,6 @@ // Created by Tibor Bodecs on 2021. 07. 10.. // -public protocol Rule: CSSRepresentable { +public protocol Rule { } diff --git a/Sources/SwiftCss/Components/Selector.swift b/Sources/SwiftCss/Components/Selector.swift index c462e43..b105803 100644 --- a/Sources/SwiftCss/Components/Selector.swift +++ b/Sources/SwiftCss/Components/Selector.swift @@ -6,7 +6,7 @@ // /// https://www.w3schools.com/cssref/css_selectors.asp -public struct Selector: CSSRepresentable { +public struct Selector { var name: String var properties: [Property] var pseudo: String? = nil @@ -21,12 +21,4 @@ public struct Selector: CSSRepresentable { self.name = name self.properties = builder() } - - public var css: String { - var suffix = "" - if let pseudo = pseudo { - suffix = pseudo - } - return name + suffix + " {\n" + properties.map(\.css).joined() + "}\n" - } } diff --git a/Sources/SwiftCss/Components/Unit.swift b/Sources/SwiftCss/Components/Unit.swift index 586d564..07568d5 100644 --- a/Sources/SwiftCss/Components/Unit.swift +++ b/Sources/SwiftCss/Components/Unit.swift @@ -5,6 +5,17 @@ // Created by Tibor Bodecs on 2021. 07. 10.. // +fileprivate extension Double { + + /// converts a double value into an integer if it has no fraction digits, otherwise it uses the double value + var asIntOrDouble: String { + if truncatingRemainder(dividingBy: 1) == 0 { + return String(Int(self)) + } + return String(self) + } +} + public enum Unit { case zero @@ -45,35 +56,35 @@ public enum Unit { case .zero: return "0" case .cm(let value): - return "\(value)cm" + return "\(value.asIntOrDouble)cm" case .mm(let value): - return "\(value)mm" + return "\(value.asIntOrDouble)mm" case .in(let value): - return "\(value)in" + return "\(value.asIntOrDouble)in" case .px(let value): - return "\(value)px" + return "\(value.asIntOrDouble)px" case .pt(let value): - return "\(value)pt" + return "\(value.asIntOrDouble)pt" case .pc(let value): - return "\(value)pc" + return "\(value.asIntOrDouble)pc" case .em(let value): - return "\(value)em" + return "\(value.asIntOrDouble)em" case .ex(let value): - return "\(value)ex" + return "\(value.asIntOrDouble)ex" case .ch(let value): - return "\(value)ch" + return "\(value.asIntOrDouble)ch" case .rem(let value): - return "\(value)rem" + return "\(value.asIntOrDouble)rem" case .vw(let value): - return "\(value)vw" + return "\(value.asIntOrDouble)vw" case .vh(let value): - return "\(value)vh" + return "\(value.asIntOrDouble)vh" case .vmin(let value): - return "\(value)vmin" + return "\(value.asIntOrDouble)vmin" case .vmax(let value): - return "\(value)vmax" + return "\(value.asIntOrDouble)vmax" case .percent(let value): - return "\(value)%" + return "\(value.asIntOrDouble)%" } } } diff --git a/Sources/SwiftCss/Rules/Charset.swift b/Sources/SwiftCss/Rules/Charset.swift index 9e7959e..34d1d7e 100644 --- a/Sources/SwiftCss/Rules/Charset.swift +++ b/Sources/SwiftCss/Rules/Charset.swift @@ -12,9 +12,4 @@ public struct Charset: Rule { public init(_ name: String) { self.name = name } - - /// @charset "UTF-8"; - public var css: String { - #"@charset ""# + name + #"";"# - } } diff --git a/Sources/SwiftCss/Rules/FontFace.swift b/Sources/SwiftCss/Rules/FontFace.swift index 4376e8d..e002930 100644 --- a/Sources/SwiftCss/Rules/FontFace.swift +++ b/Sources/SwiftCss/Rules/FontFace.swift @@ -13,8 +13,4 @@ public struct FontFace: Rule { self.properties = builder() } - public var css: String { - let value = properties.map(\.css).joined() - return "@font-face {\n\t" + value.split(separator: "\n").joined(separator: "\n\t") + "\n}\n" - } } diff --git a/Sources/SwiftCss/Rules/Import.swift b/Sources/SwiftCss/Rules/Import.swift index 9d740f2..d607f72 100644 --- a/Sources/SwiftCss/Rules/Import.swift +++ b/Sources/SwiftCss/Rules/Import.swift @@ -16,9 +16,4 @@ public struct Import: Rule { public init(_ name: String) { self.name = name } - - /// @import "mobstyle.css" screen and (max-width: 768px); - public var css: String { - "@import " + name + ";" - } } diff --git a/Sources/SwiftCss/Rules/Keyframes.swift b/Sources/SwiftCss/Rules/Keyframes.swift index 01abb69..31a475e 100644 --- a/Sources/SwiftCss/Rules/Keyframes.swift +++ b/Sources/SwiftCss/Rules/Keyframes.swift @@ -14,9 +14,4 @@ public struct Keyframes: Rule { self.name = name self.selectors = builder() } - - public var css: String { - let value = selectors.map(\.css).joined() - return "@keyframes " + name + " {\n\t" + value.split(separator: "\n").joined(separator: "\n\t") + "\n}\n" - } } diff --git a/Sources/SwiftCss/Rules/Media.swift b/Sources/SwiftCss/Rules/Media.swift index cb634ec..cae9cf7 100644 --- a/Sources/SwiftCss/Rules/Media.swift +++ b/Sources/SwiftCss/Rules/Media.swift @@ -35,12 +35,4 @@ public struct Media: Rule { public init(screen: Screen, @SelectorBuilder _ builder: () -> [Selector]) { self.init(screen.rawValue, builder) } - - public var css: String { - let css = selectors.map(\.css).joined() - guard let query = query else { - return css - } - return "@media " + query + " {\n\t" + css.split(separator: "\n").joined(separator: "\n\t") + "\n}\n" - } } diff --git a/Sources/SwiftCss/Stylesheet/Stylesheet.swift b/Sources/SwiftCss/Stylesheet/Stylesheet.swift new file mode 100644 index 0000000..99e2dcd --- /dev/null +++ b/Sources/SwiftCss/Stylesheet/Stylesheet.swift @@ -0,0 +1,14 @@ +// +// Stylesheet.swift +// SwiftCss +// +// Created by Tibor Bodecs on 2021. 07. 09.. +// + +public struct Stylesheet { + let rules: [Rule] + + public init(@RuleBuilder _ builder: () -> [Rule]) { + self.rules = builder() + } +} diff --git a/Sources/SwiftCss/Stylesheet/StylesheetRenderer.swift b/Sources/SwiftCss/Stylesheet/StylesheetRenderer.swift new file mode 100644 index 0000000..d23e68d --- /dev/null +++ b/Sources/SwiftCss/Stylesheet/StylesheetRenderer.swift @@ -0,0 +1,64 @@ +// +// StylesheetRenderer.swift +// SwiftCss +// +// Created by Tibor Bodecs on 2021. 11. 21.. +// + +public struct StylesheetRenderer { + + private let newline: String + private let singleSpace: String + public let minify: Bool + public let indent: Int + + public init(minify: Bool = false, indent: Int = 4) { + self.minify = minify + self.indent = minify ? 0 : indent + self.newline = minify ? "" : "\n" + self.singleSpace = minify ? "" : " " + } + + public func render(_ stylesheet: Stylesheet) -> String { + stylesheet.rules.map { rule in + switch rule { + case let charset as Charset: + return #"@charset ""# + charset.name + #"";"# + case let value as FontFace: + let properties = value.properties.map { renderProperty($0) }.joined(separator: newline) + return "@font-face {" + newline + properties + newline + "}" + case let value as Import: + return "@import " + value.name + ";" + case let keyframes as Keyframes: + let selectors = keyframes.selectors.map { renderSelector($0) }.joined(separator: newline) + return "@keyframes " + keyframes.name + singleSpace + "{" + newline + selectors + newline + "}" + newline + case let media as Media: + let level = media.query != nil ? 1 : 0 + var selectors = media.selectors.map { renderSelector($0, level: level) }.joined(separator: newline) + if let query = media.query { + selectors = "@media " + query + singleSpace + "{" + newline + selectors + newline + "}" + } + return selectors + default: + fatalError("unknown rule object") + } + }.joined(separator: newline) + } + + // MARK: - helpers + + private func renderProperty(_ property: Property, level: Int = 0) -> String { + let spaces = String(repeating: " ", count: level * indent) + return spaces + property.name + ":" + singleSpace + property.value + (property.isImportant ? " !important" : "") + } + + private func renderSelector(_ selector: Selector, level: Int = 0) -> String { + let spaces = String(repeating: " ", count: level * indent) + var suffix = "" + if let pseudo = selector.pseudo { + suffix = pseudo + } + let properties = selector.properties.map { renderProperty($0, level: level + 1) }.joined(separator: ";" + newline) + (!minify ? ";" : "") + return spaces + selector.name + suffix + singleSpace + "{" + newline + properties + newline + spaces + "}" + } +} diff --git a/Tests/SwiftCssTests/SelectorTests.swift b/Tests/SwiftCssTests/SelectorTests.swift index 382aa47..244b0c5 100644 --- a/Tests/SwiftCssTests/SelectorTests.swift +++ b/Tests/SwiftCssTests/SelectorTests.swift @@ -11,24 +11,47 @@ import XCTest final class SelectorTests: XCTestCase { // MARK: - margin - - func testMargin() { - XCTAssertEqual(Margin(horizontal: .px(8)).css, "\tmargin: 8.0px 0;\n") - XCTAssertEqual(Margin(horizontal: .length(.zero), vertical: .auto).css, "\tmargin: 0 auto;\n") - } func testMarginBottom() { - XCTAssertEqual(MarginBottom(.length(.px(8))).css, "\tmargin-bottom: 8.0px;\n") - XCTAssertEqual(MarginBottom(.length(.percent(25))).css, "\tmargin-bottom: 25.0%;\n") + let css = Stylesheet { + Media { + All { + MarginTop(.length(.px(8))) + MarginBottom(.length(.percent(25))) + } + } + } + + XCTAssertEqual(StylesheetRenderer().render(css), #""" + * { + margin-top: 8px; + margin-bottom: 25%; + } + """#) } // MARK: - padding func testPadding() { - XCTAssertEqual(Padding(.zero).css, "\tpadding: 0;\n") - XCTAssertEqual(Padding(.rem(8)).css, "\tpadding: 8.0rem;\n") - XCTAssertEqual(Padding(horizontal: .px(8)).css, "\tpadding: 8.0px 0;\n") - XCTAssertEqual(Padding(horizontal: .length(.zero), vertical: .inherit).css, "\tpadding: 0 inherit;\n") + let css = Stylesheet { + Media { + All { + Padding(.zero) + Padding(.rem(8)) + Padding(horizontal: .px(8)) + Padding(horizontal: .length(.zero), vertical: .inherit) + } + } + } + + XCTAssertEqual(StylesheetRenderer().render(css), #""" + * { + padding: 0; + padding: 8rem; + padding: 8px 0; + padding: 0 inherit; + } + """#) } } diff --git a/Tests/SwiftCssTests/SwiftCssTests.swift b/Tests/SwiftCssTests/SwiftCssTests.swift index b8bb46d..c28c190 100644 --- a/Tests/SwiftCssTests/SwiftCssTests.swift +++ b/Tests/SwiftCssTests/SwiftCssTests.swift @@ -10,103 +10,62 @@ import XCTest final class SwiftCssTests: XCTestCase { - func testCss001() { - @RuleBuilder func buildCSS() -> [Rule] { - Media { - All { - Margin(.zero) - Padding(.zero) - } - Element(.body) { - Margin(horizontal: .auto, vertical: .length(.rem(2))) - Background(.color("cafe00"), image: .url("./test.png"), position: .leftTop) - Color(.white) - } - Element(.a) { - Color(.red) - } - Element(.a) { - Color(.orange) - BoxShadow(.px(6), .px(4), blur: .px(2), color: "cafe00") - } - .pseudo(.hover) - } - } + func testStylesheet() { + let css = Stylesheet { + Charset("UTF-8") - let sel = buildCSS() - print(sel.map(\.css).joined(separator: "\n")) - XCTAssertTrue(true) - } - - func testExample() { - @RuleBuilder func buildCSS() -> [Rule] { Media { - Element(.div) { - BackgroundColor(.red) - Color(.white) - TextAlign("left") + Root { + Margin(horizontal: .px(8.5), vertical: .px(8)) + Padding(horizontal: .px(8), vertical: .px(8)) } - .pseudo(.nthChild(2)) + } - Id("custom-identifier") { - Background("#222") - Color("cafe00") + Media(screen: .s) { + Class("button") { Color("#cafe00") } - Class("custom-class") { - Background("#333") - Color(.red) - } - Selector("ul > li > a") { - Background("black") - Color(.blue) - .important() - } } - Media("only screen and (max-width: 600px)") { - Element(.div) { - BackgroundColor("#000") - Color(.white) - } - Id("custom-identifier") { - Background("#222") - Color(.cyan) - } - Class("custom-class") { - Background("#333") - Color(.aliceBlue) - } - Selector("ul > li > a") { - Background("black") - Color(.red) - .important() - } - .pseudo(.hover) + Media(screen: .dark, { All { - Background("red") - Padding(.zero) - Margin(.zero) - } - Element(.p) { - Margin(bottom: .px(20)) - MarginBottom(.px(20)) + Margin(horizontal: .px(8), vertical: .px(8)) } - Root { - Color(.blue) - BackgroundColor(.transparent) - AnimationDelay(.seconds(45)) - BorderBottomWidth(.length(.px(4))) + }) + Media(screen: .standalone) { + Id("lead") { + Background(.color(.red)) } } } - - let sel = buildCSS() - print(sel.map(\.css).joined(separator: "\n")) - XCTAssertTrue(true) + + XCTAssertEqual(StylesheetRenderer().render(css), #""" + @charset "UTF-8"; + :root { + margin: 8.5px 8px; + padding: 8px 8px; + } + @media screen and (min-width: 600px) { + .button { + color: #cafe00; + } + } + @media screen and (prefers-color-scheme: dark) { + * { + margin: 8px 8px; + } + } + @media screen and (display-mode: standalone) { + #lead { + background: red; + } + } + """#) } + + func testVariable() { - @RuleBuilder func buildCSS() -> [Rule] { + let css = Stylesheet { Media { Root { Variable("size", "400px") @@ -133,13 +92,33 @@ final class SwiftCssTests: XCTestCase { } } - let sel = buildCSS() - print(sel.map(\.css).joined(separator: "\n")) - XCTAssertTrue(true) + XCTAssertEqual(StylesheetRenderer().render(css), #""" + :root { + --size: 400px; + } + .container { + width: var(--size); + } + @media screen and (max-width: 599px) { + :root { + --size: 200px; + } + } + @media screen and (prefers-color-scheme: dark) { + :root { + --size: 500px; + } + } + @media screen and (display-mode: standalone) { + :root { + --size: 460px; + } + } + """#) } func testMediaQueries() { - @RuleBuilder func buildCSS() -> [Rule] { + let css = Stylesheet { Media { Root { Background(.color(.red)) @@ -162,9 +141,26 @@ final class SwiftCssTests: XCTestCase { } } - let sel = buildCSS() - print(sel.map(\.css).joined(separator: "\n")) - XCTAssertTrue(true) + XCTAssertEqual(StylesheetRenderer().render(css), #""" + :root { + background: red; + } + @media screen and (max-width: 599px) { + :root { + background: blue; + } + } + @media screen and (prefers-color-scheme: dark) { + :root { + background: green; + } + } + @media screen and (display-mode: standalone) { + body { + background: yellow; + } + } + """#) } }