diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..0bbaa52
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,17 @@
+name: Run Tests
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ tests:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checking out code
+ uses: actions/checkout@v2
+
+ - name: Run tests
+ run: swift test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..305a0f7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.DS_Store
+
+# vim swap files
+*.sw[nop]
+
+# Swift Package Manager
+/.build
+.swiftpm/xcode/xcuserdata
+*.xcuserstate
+*.xcworkspacedata
+*.orig
+_site/index.html
diff --git a/.swift-version b/.swift-version
new file mode 100644
index 0000000..95ee81a
--- /dev/null
+++ b/.swift-version
@@ -0,0 +1 @@
+5.9
diff --git a/.swiftformat b/.swiftformat
new file mode 100644
index 0000000..2c90c98
--- /dev/null
+++ b/.swiftformat
@@ -0,0 +1,5 @@
+--disable wrapMultilineStatementBraces
+--enable isEmpty
+--header strip
+--commas always
+--indent 4
diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/jasonzurita.xcuserdatad/IDEFindNavigatorScopes.plist b/.swiftpm/xcode/package.xcworkspace/xcuserdata/jasonzurita.xcuserdatad/IDEFindNavigatorScopes.plist
new file mode 100644
index 0000000..5dd5da8
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/xcuserdata/jasonzurita.xcuserdatad/IDEFindNavigatorScopes.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftWebsiteDSL.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftWebsiteDSL.xcscheme
new file mode 100644
index 0000000..85666b4
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftWebsiteDSL.xcscheme
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Modules/ExampleSwiftWebsite/src/Site/Header.swift b/Modules/ExampleSwiftWebsite/src/Site/Header.swift
new file mode 100644
index 0000000..c52b8e5
--- /dev/null
+++ b/Modules/ExampleSwiftWebsite/src/Site/Header.swift
@@ -0,0 +1,27 @@
+import SwiftWebsiteDSL
+
+struct SiteHeader: HtmlProvider {
+ public let html: HtmlNode = {
+ Header {
+ Img(src: "images/logo.svg", alt: "let hello = \"world\"")
+ .padding([.top], 50)
+ .width(500)
+ P("One minute Swift reads to get Swifty-er")
+ .color(.white)
+ Div {
+ // TODO: pull the swift value in from .swift-version
+ P("Updated for Swift 5.8")
+ }
+ .textAlign(.right)
+ .color(.lightGray)
+ .padding([.bottom], 1)
+ .padding([.top], 25)
+ .padding([.trailing], 18)
+ }
+ .background(
+ .linearGradient(.init(degree: 180, first: (.headerTopBlue, 0), second: (.headerBottomBlue, 100)))
+ )
+ // TODO: work on this quirk where we shouldn't need to call `.html` after
+ .html
+ }()
+}
diff --git a/Modules/ExampleSwiftWebsite/src/Site/RootSite.swift b/Modules/ExampleSwiftWebsite/src/Site/RootSite.swift
new file mode 100644
index 0000000..c08895a
--- /dev/null
+++ b/Modules/ExampleSwiftWebsite/src/Site/RootSite.swift
@@ -0,0 +1,30 @@
+import SwiftWebsiteDSL
+
+// TODO: custom style guide for code
+
+func renderHtml() -> String {
+ let site =
+ Html {
+ Head(title: "ExampleSwiftWebsite", cssStyleFileName: "")
+ Body {
+ H1("Hello World!")
+ .padding([.top], 48)
+ .color(.white)
+ Footer {
+ P("Built using the Swift Language and is ") {
+ A(copy: "open source.", url: "https://github.com/jasonzurita/swift-website-dsl")
+ }
+ }
+ .textAlign(.center)
+ .color(.lightGray)
+ }
+ .font(.apple)
+ .textAlign(.center)
+ .margin(0)
+ .padding(0)
+ .background(
+ .linearGradient(.init(degree: 180, first: (.headerTopBlue, 0), second: (.headerBottomBlue, 100)))
+ )
+ }
+ return site.html.render
+}
diff --git a/Modules/ExampleSwiftWebsite/src/Support/Colors.swift b/Modules/ExampleSwiftWebsite/src/Support/Colors.swift
new file mode 100644
index 0000000..c39ae23
--- /dev/null
+++ b/Modules/ExampleSwiftWebsite/src/Support/Colors.swift
@@ -0,0 +1,11 @@
+import SwiftWebsiteDSL
+
+// https://rgbacolorpicker.com/rgba-to-hex
+extension Color {
+ static let white = Color(hex: "FFFFFF")
+ static let lightGray = Color(hex: "F2F2F2")
+ static let mediumGray = Color(hex: "8a8a8a")
+ static let darkGray = Color(hex: "333333")
+ static let headerTopBlue = Color(hex: "459bd0a8")
+ static let headerBottomBlue = Color(hex: "2F80ED")
+}
diff --git a/Modules/ExampleSwiftWebsite/src/main.swift b/Modules/ExampleSwiftWebsite/src/main.swift
new file mode 100644
index 0000000..7f5483f
--- /dev/null
+++ b/Modules/ExampleSwiftWebsite/src/main.swift
@@ -0,0 +1,22 @@
+// Usage: Go to root directory then `swift run`
+import Foundation
+
+let siteDirectory = FileManager.default.currentDirectoryPath + "/_site"
+let outputFilePath = siteDirectory + "/index.html"
+
+print("🛠️ Starting to generate the example website...")
+
+do {
+ print("🖍️ Generating HTML...")
+ let html = renderHtml() // This is a free function from this module
+ if let data = html.data(using: .utf8) {
+ try data.write(to: URL(fileURLWithPath: outputFilePath), options: [])
+ print("🚀 Finished generating site! Output is in: \(siteDirectory)")
+ } else {
+ print("❌ Failed to write generated site out to: \(outputFilePath)")
+ exit(1)
+ }
+} catch {
+ print("❌ Failed to build and generate site. Error: \(error)")
+ exit(1)
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/AnyElement.test.swift b/Modules/SwiftWebsiteDSL/Tests/AnyElement.test.swift
new file mode 100644
index 0000000..89fcb64
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/AnyElement.test.swift
@@ -0,0 +1,130 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class AnyElementTests: XCTestCase {
+ // MARK: - Styles
+
+ func testWidthStyle() {
+ // given
+ let element = AnyElement(element: "fake-element", attrs: [:], copy: "", nodes: [])
+ .width(789)
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testColorStyle() {
+ // given
+ let element = AnyElement(element: "fake-element", attrs: [:], copy: "", nodes: [])
+ .color(.init(hex: "123456"))
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testMarginAutoStyle() {
+ // given
+ let element = AnyElement(element: "fake-margin-auto-element", attrs: [:], copy: "", nodes: [])
+ .margin([.leading, .top], .auto)
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBackgroundColor() {
+ // given
+ let element = AnyElement(element: "fake-background-color-element", attrs: [:], copy: "", nodes: [])
+ .background(.color(.init(hex: "123456")))
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBackgroundLinearGradient() {
+ // given
+ let gradient: BackgroundType.LinearGradient = .init(
+ degree: 77,
+ first: (.init(hex: "ASDFGH"), 4),
+ second: (.init(hex: "QWERTY"), 99)
+ )
+
+ let element = AnyElement(element: "fake-background-linear-gradient-element", attrs: [:], copy: "", nodes: [])
+ .background(.linearGradient(gradient))
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBorderRadius() {
+ // given
+ let element = AnyElement(element: "fake-border-radius-element", attrs: [:], copy: "", nodes: [])
+ .borderRadius(px: 16)
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testLineHeight() {
+ // given
+ let element = AnyElement(element: "fake-line-height-element", attrs: [:], copy: "", nodes: [])
+ .lineHeight(2.3)
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testMostGeneralStylesWithPElement() {
+ // given
+ let element = P("Super cool copy here")
+ .color(.init(hex: "1234567"))
+ .margin(0)
+ .padding([.top], 7)
+// .width(13) // TODO: this fails because width for a p element should be in the style section
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testCopyIsNotLostWithModifiers() {
+ // given
+ let copy = "mic test"
+ let element = P(copy)
+ // when
+ .color(.init(hex: "1234567"))
+ .margin(1)
+ .padding([.top], 0)
+
+ switch element.html {
+ case let .element(tag, attrs: _, copy, _):
+ // then
+ XCTAssertEqual(tag, "p")
+ XCTAssertFalse(copy.isEmpty)
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/BodyUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/BodyUnit.test.swift
new file mode 100644
index 0000000..30ed9b9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/BodyUnit.test.swift
@@ -0,0 +1,104 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class BodyUnitTests: XCTestCase {
+ func testEmptyBody() {
+ // given
+ let body = Body(attrs: [:], nodes: [])
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBodyWithEachMargin() {
+ Side.allCases.forEach {
+ // given
+ let body = Body(attrs: [:], nodes: []).margin([$0], 7.1)
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+ }
+
+ func testBodyWithAllMargins() {
+ // given
+ let body = Body(attrs: [:], nodes: []).margin(Side.allCases, 7.1)
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBodyWithEachPadding() {
+ Side.allCases.forEach {
+ // given
+ let body = Body(attrs: [:], nodes: []).padding([$0], 7.1)
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+ }
+
+ func testBodyWithAllPaddings() {
+ // given
+ let body = Body(attrs: [:], nodes: []).padding(7.1)
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBodyWithBackground() {
+ // given
+ let body = Body(attrs: [:], nodes: [])
+ .background(.color(.init(hex: "ASDFGH")))
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBodyWithFont() {
+ // given
+ let body = Body(attrs: [:], nodes: []).font(.apple)
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testBodyWithMultipleStyles() {
+ // given
+ let body = Body(attrs: [:], nodes: [])
+ .font(.apple)
+ .textAlign(.left)
+ .background(.color(.init(hex: "ASDFGH")))
+ .margin(11)
+ .color(.init(hex: "asdfgh"))
+
+ // when
+ let rendered = body.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/CodeUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/CodeUnit.test.swift
new file mode 100644
index 0000000..2750313
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/CodeUnit.test.swift
@@ -0,0 +1,30 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class CodeUnitTests: XCTestCase {
+ func testCodeElement() {
+ // given
+ let code = Code {}
+
+ // when
+ let rendered = code.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testCodeWithNestedElementToEnsureSingleLineHtml() {
+ // given
+ let code = Code {
+ P("let hello = world!")
+ }
+
+ // when
+ let rendered = code.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/DivUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/DivUnit.test.swift
new file mode 100644
index 0000000..52d3c01
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/DivUnit.test.swift
@@ -0,0 +1,56 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class DivUnitTests: XCTestCase {
+ func testDivElement() {
+ // given
+ let div = Div {}
+
+ // when
+ let rendered = div.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testDivWithNestedElement() {
+ // given
+ let div = Div {
+ P("I am nested :)")
+ }
+
+ // when
+ let rendered = div.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testDivWithMaxWidth() {
+ // given
+ let div = Div {}
+ .maxWidth(percent: 82)
+
+ // when
+ let rendered = div.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testTextAlignment() {
+ // given
+ TextAlignment.allCases.forEach {
+ let element = Div {}
+ .textAlign($0)
+
+ // when
+ let rendered = element.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/FooterUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/FooterUnit.test.swift
new file mode 100644
index 0000000..24908b8
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/FooterUnit.test.swift
@@ -0,0 +1,44 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class FooterUnitTests: XCTestCase {
+ func testFooterElement() {
+ // given
+ let footer = Footer {}
+
+ // when
+ let rendered = footer.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testFooterEnclosingPElement() {
+ // given
+ let footer = Footer {
+ P("Hi footer")
+ }
+
+ // when
+ let rendered = footer.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testFooterEnclosingPWithStyleElement() {
+ // given
+ let footer = Footer {
+ P("Hi footer")
+ }
+ .color(.init(hex: "123456"))
+
+ // when
+ let rendered = footer.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/HeadUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/HeadUnit.test.swift
new file mode 100644
index 0000000..2c452f2
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/HeadUnit.test.swift
@@ -0,0 +1,17 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class HeadUnitTests: XCTestCase {
+ func testHeadElement() {
+ // given
+ let head = Head(title: "test-title", cssStyleFileName: "fake.css")
+
+ // when
+ let rendered = head.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/HeaderUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/HeaderUnit.test.swift
new file mode 100644
index 0000000..2334f65
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/HeaderUnit.test.swift
@@ -0,0 +1,41 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class HeaderUnitTests: XCTestCase {
+ func testEmptyHeader() {
+ // given
+ let header = Header(attrs: [:], nodes: [])
+
+ // when
+ let rendered = header.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testHeaderWithEachPadding() {
+ Side.allCases.forEach {
+ // given
+ let header = Header(attrs: [:], nodes: []).padding([$0], 7.6)
+
+ // when
+ let rendered = header.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+ }
+
+ func testHeaderWithAllPaddings() {
+ // given
+ let header = Header(attrs: [:], nodes: []).padding(Side.allCases, 7.1)
+
+ // when
+ let rendered = header.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/ImageUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/ImageUnit.test.swift
new file mode 100644
index 0000000..8d90f55
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/ImageUnit.test.swift
@@ -0,0 +1,29 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class ImageUnitTests: XCTestCase {
+ func testBasicImg() {
+ // given
+ let img = Img(src: "images/logo.png", alt: "alt required string")
+
+ // when
+ let rendered = img.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testImageWithMargin() {
+ // given
+ let img = Img(src: "images/logo.png", alt: "alt required string")
+ .margin(10)
+
+ // when
+ let rendered = img.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/PreUnit.test.swift b/Modules/SwiftWebsiteDSL/Tests/PreUnit.test.swift
new file mode 100644
index 0000000..ddc7dec
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/PreUnit.test.swift
@@ -0,0 +1,32 @@
+import Foundation
+import SnapshotTesting
+@testable import SwiftWebsiteDSL
+import XCTest
+
+final class PreUnitTests: XCTestCase {
+ func testPreElement() {
+ // given
+ let pre = Pre {}
+
+ // when
+ let rendered = pre.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+
+ func testPreWithNestedElementToEnsureSingleLineHtml() {
+ // given
+ let pre = Pre {
+ Div {
+ P("ooo, nested twice")
+ }
+ }
+
+ // when
+ let rendered = pre.html.render
+
+ // then
+ assertSnapshot(matching: rendered, as: .lines)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testAllGeneralStylesWithPElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testAllGeneralStylesWithPElement.1.txt
new file mode 100644
index 0000000..a29bcd0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testAllGeneralStylesWithPElement.1.txt
@@ -0,0 +1 @@
+
Super cool copy here
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundColor.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundColor.1.txt
new file mode 100644
index 0000000..62b5d65
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundColor.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundLinearGradient.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundLinearGradient.1.txt
new file mode 100644
index 0000000..f8caac9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBackgroundLinearGradient.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBorderRadius.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBorderRadius.1.txt
new file mode 100644
index 0000000..a4e172d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testBorderRadius.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testColorStyle.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testColorStyle.1.txt
new file mode 100644
index 0000000..8cf973c
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testColorStyle.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testLineHeight.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testLineHeight.1.txt
new file mode 100644
index 0000000..3a5afc2
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testLineHeight.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMarginAutoStyle.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMarginAutoStyle.1.txt
new file mode 100644
index 0000000..0ad9aac
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMarginAutoStyle.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMostGeneralStylesWithPElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMostGeneralStylesWithPElement.1.txt
new file mode 100644
index 0000000..bf61cdd
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testMostGeneralStylesWithPElement.1.txt
@@ -0,0 +1 @@
+
Super cool copy here
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testWidthStyle.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testWidthStyle.1.txt
new file mode 100644
index 0000000..e8dd6d1
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/AnyElement.test/testWidthStyle.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllMargins.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllMargins.1.txt
new file mode 100644
index 0000000..73246e0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllMargins.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllPaddings.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllPaddings.1.txt
new file mode 100644
index 0000000..5fa2088
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithAllPaddings.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithBackground.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithBackground.1.txt
new file mode 100644
index 0000000..780138b
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithBackground.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.1.txt
new file mode 100644
index 0000000..968eb66
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.2.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.2.txt
new file mode 100644
index 0000000..203b583
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.2.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.3.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.3.txt
new file mode 100644
index 0000000..0442551
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.3.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.4.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.4.txt
new file mode 100644
index 0000000..feeb0e0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachMargin.4.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.1.txt
new file mode 100644
index 0000000..5c4e2bb
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.2.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.2.txt
new file mode 100644
index 0000000..ae70ac5
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.2.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.3.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.3.txt
new file mode 100644
index 0000000..b85d627
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.3.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.4.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.4.txt
new file mode 100644
index 0000000..581df57
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithEachPadding.4.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithFont.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithFont.1.txt
new file mode 100644
index 0000000..ff29fc2
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithFont.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithMultipleStyles.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithMultipleStyles.1.txt
new file mode 100644
index 0000000..96b4248
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testBodyWithMultipleStyles.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testEmptyBody.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testEmptyBody.1.txt
new file mode 100644
index 0000000..5db7bc1
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/BodyUnit.test/testEmptyBody.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeElement.1.txt
new file mode 100644
index 0000000..5c8eea5
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeElement.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeWithNestedElementToEnsureSingleLineHtml.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeWithNestedElementToEnsureSingleLineHtml.1.txt
new file mode 100644
index 0000000..f8a752b
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/CodeUnit.test/testCodeWithNestedElementToEnsureSingleLineHtml.1.txt
@@ -0,0 +1 @@
+
let hello = world!
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivElement.1.txt
new file mode 100644
index 0000000..281c686
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivElement.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithMaxWidth.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithMaxWidth.1.txt
new file mode 100644
index 0000000..07c879d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithMaxWidth.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithNestedElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithNestedElement.1.txt
new file mode 100644
index 0000000..e9a2078
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testDivWithNestedElement.1.txt
@@ -0,0 +1,3 @@
+
+
I am nested :)
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.1.txt
new file mode 100644
index 0000000..ddfa3f9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.2.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.2.txt
new file mode 100644
index 0000000..0d40aa0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.2.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.3.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.3.txt
new file mode 100644
index 0000000..d039bf4
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.3.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.4.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.4.txt
new file mode 100644
index 0000000..81d4287
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/DivUnit.test/testTextAlignment.4.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterElement.1.txt
new file mode 100644
index 0000000..4a8bf4d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterElement.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPElement.1.txt
new file mode 100644
index 0000000..7f40b7b
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPElement.1.txt
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPWithStyleElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPWithStyleElement.1.txt
new file mode 100644
index 0000000..e0b35bf
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/FooterUnit.test/testFooterEnclosingPWithStyleElement.1.txt
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeadUnit.test/testHeadElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeadUnit.test/testHeadElement.1.txt
new file mode 100644
index 0000000..12684cb
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeadUnit.test/testHeadElement.1.txt
@@ -0,0 +1,4 @@
+
+ test-title
+
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testEmptyHeader.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testEmptyHeader.1.txt
new file mode 100644
index 0000000..68adf2e
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testEmptyHeader.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithAllPaddings.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithAllPaddings.1.txt
new file mode 100644
index 0000000..78e18fb
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithAllPaddings.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.1.txt
new file mode 100644
index 0000000..6c44441
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.2.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.2.txt
new file mode 100644
index 0000000..bc00076
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.2.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.3.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.3.txt
new file mode 100644
index 0000000..4703f36
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.3.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.4.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.4.txt
new file mode 100644
index 0000000..30a18db
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/HeaderUnit.test/testHeaderWithEachPadding.4.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testBasicImg.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testBasicImg.1.txt
new file mode 100644
index 0000000..07e3e2d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testBasicImg.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testImageWithMargin.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testImageWithMargin.1.txt
new file mode 100644
index 0000000..654cab9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/ImageUnit.test/testImageWithMargin.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreElement.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreElement.1.txt
new file mode 100644
index 0000000..a435706
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreElement.1.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreWithNestedElementToEnsureSingleLineHtml.1.txt b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreWithNestedElementToEnsureSingleLineHtml.1.txt
new file mode 100644
index 0000000..03f4c09
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/Tests/__Snapshots__/PreUnit.test/testPreWithNestedElementToEnsureSingleLineHtml.1.txt
@@ -0,0 +1,3 @@
+
+
ooo, nested twice
+
\ No newline at end of file
diff --git a/Modules/SwiftWebsiteDSL/src/AttrType.swift b/Modules/SwiftWebsiteDSL/src/AttrType.swift
new file mode 100644
index 0000000..b397e2a
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/AttrType.swift
@@ -0,0 +1,3 @@
+public enum AttrType: String {
+ case style, src, alt, href, width, rel
+}
diff --git a/Modules/SwiftWebsiteDSL/src/BlockHtmlProvider.swift b/Modules/SwiftWebsiteDSL/src/BlockHtmlProvider.swift
new file mode 100644
index 0000000..f7315f0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/BlockHtmlProvider.swift
@@ -0,0 +1,2 @@
+// This is used to enforce some styles that should only apply to block level html elements
+public protocol BlockHtmlProvider: HtmlProvider {}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlBuilder.swift b/Modules/SwiftWebsiteDSL/src/HtmlBuilder.swift
new file mode 100644
index 0000000..3122b21
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlBuilder.swift
@@ -0,0 +1,87 @@
+@resultBuilder
+public enum HtmlBuilder {
+ // this is the highest level builder. All other builders like "buildArray" will
+ // be given the output of this since this will be applied to that subscope before
+ // the result is bubbled up to the higher level scope.
+ public static func buildBlock(_ components: [HtmlProvider]...) -> [HtmlNode] {
+ components.flatMap { $0 }.map(\.html)
+ }
+
+ // See note above. The output of this needs to match the input of that build block.
+ // There may be a limitation becuase of this where we can't have an array or
+ // for loop at the top level, but that should be an okay edge case.
+ //
+ // Also, `AnyElement` is used below because the type doesn't make much of a
+ // difference at this point.
+ public static func buildArray(_ components: [[HtmlNode]]) -> [HtmlProvider] {
+ components.flatMap { $0 }.map {
+ switch $0 {
+ case let .element(element, attrs: attrs, copy, nodes):
+ return AnyElement(
+ element: element,
+ attrs: attrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ }
+ }
+
+ // This is used so that we promote all single HtmlProviders to the main currency
+ // for this result builders, which is an array of HtmlProviders. See the note
+ // in `buildBlock` above for its input.
+ public static func buildExpression(_ expression: HtmlProvider) -> [HtmlProvider] {
+ [expression]
+ }
+
+ // This makes it so we can use an array literal
+ public static func buildExpression(_ expression: [HtmlProvider]) -> [HtmlProvider] {
+ expression
+ }
+
+ // if/else statement support
+ public static func buildEither(first component: [HtmlNode]) -> [HtmlProvider] {
+ component.map {
+ switch $0 {
+ case let .element(element, attrs: attrs, copy, nodes):
+ return AnyElement(
+ element: element,
+ attrs: attrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ }
+ }
+
+ // if/else statement support
+ public static func buildEither(second component: [HtmlNode]) -> [HtmlProvider] {
+ component.map {
+ switch $0 {
+ case let .element(element, attrs: attrs, copy, nodes):
+ return AnyElement(
+ element: element,
+ attrs: attrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ }
+ }
+
+ // single if statement support
+ public static func buildOptional(_ component: [HtmlNode]?) -> [HtmlProvider] {
+ guard let c = component else { return [] }
+ return c.map {
+ switch $0 {
+ case let .element(element, attrs: attrs, copy, nodes):
+ return AnyElement(
+ element: element,
+ attrs: attrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/AnyElement.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/AnyElement.swift
new file mode 100644
index 0000000..d8d682e
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/AnyElement.swift
@@ -0,0 +1,18 @@
+import Foundation
+
+public struct AnyElement: HtmlProvider {
+ public let html: HtmlNode
+
+ public init(
+ element: String,
+ attrs: [AttrType: String] = [:],
+ copy: String = "",
+ @HtmlBuilder content: () -> [HtmlNode]
+ ) {
+ html = .element(element, attrs: attrs, copy: copy, content())
+ }
+
+ public init(element: String, attrs: [AttrType: String], copy: String, nodes: [HtmlNode]) {
+ html = .element(element, attrs: attrs, copy: copy, nodes)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Body.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Body.swift
new file mode 100644
index 0000000..c463fb6
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Body.swift
@@ -0,0 +1,27 @@
+public struct Body: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(attrs: [AttrType: String] = [:], @HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("body", attrs: attrs, content())
+ }
+
+ public init(attrs: [AttrType: String], nodes: [HtmlNode]) {
+ html = .element("body", attrs: attrs, nodes)
+ }
+}
+
+public extension Body {
+ func font(_ font: Font) -> Body {
+ let result: Body
+ switch html {
+ case let .element(_, attrs: attrs, _, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "\(font.style);"
+ result = Body(
+ attrs: newAttrs,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Code.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Code.swift
new file mode 100644
index 0000000..7577db6
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Code.swift
@@ -0,0 +1,7 @@
+public struct Code: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(@HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("code", attrs: [:], content())
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Footer.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Footer.swift
new file mode 100644
index 0000000..f71ae8d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Footer.swift
@@ -0,0 +1,13 @@
+import Swift
+
+public struct Footer: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(attrs: [AttrType: String] = [:], @HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("footer", attrs: attrs, content())
+ }
+
+ public init(attrs: [AttrType: String], nodes: [HtmlNode]) {
+ html = .element("footer", attrs: attrs, nodes)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Head.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Head.swift
new file mode 100644
index 0000000..911dfb6
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Head.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+public struct Head: HtmlProvider {
+ public let html: HtmlNode
+
+ public init(title: String, cssStyleFileName: String) {
+ let titleNode: HtmlNode = .element("title", copy: title)
+ // TODO: fine for now, but remove trailing "link" tag ()
+ let styleLinkNode: HtmlNode = .element("link", attrs: [.rel: "stylesheet", .href: cssStyleFileName], [])
+ html = .element("head", attrs: [:], [titleNode, styleLinkNode])
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Header.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Header.swift
new file mode 100644
index 0000000..f1e1259
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Header.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+public struct Header: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(attrs: [AttrType: String] = [:], @HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("header", attrs: attrs, content())
+ }
+
+ public init(attrs: [AttrType: String], nodes: [HtmlNode]) {
+ html = .element("header", attrs: attrs, nodes)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/HtmlElements.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/HtmlElements.swift
new file mode 100644
index 0000000..7bf8ca1
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/HtmlElements.swift
@@ -0,0 +1,50 @@
+public struct Html: HtmlProvider {
+ public let html: HtmlNode
+
+ public init(@HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("html", attrs: [:], content())
+ }
+}
+
+public struct Div: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(@HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("div", attrs: [:], content())
+ }
+}
+
+// TODO: inline links
+public struct P: BlockHtmlProvider {
+ public var html: HtmlNode
+
+ // Can we limit the elements that go in here?
+ public init(_ copy: String, @HtmlBuilder content: () -> [HtmlNode] = { [] }) {
+ html = HtmlNode.element("p", attrs: [:], copy: copy, content())
+ }
+}
+
+public struct H1: BlockHtmlProvider {
+ public var html: HtmlNode
+
+ public init(_ copy: String) {
+ html = HtmlNode.element("h1", attrs: [:], copy: copy, [])
+ }
+}
+
+public struct H2: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(_ copy: String) {
+ html = HtmlNode.element("h2", attrs: [:], copy: copy, [])
+ }
+}
+
+public struct A: HtmlProvider {
+ public var html: HtmlNode
+
+ // TODO: make url type?
+ public init(copy: String, url: String) {
+ html = HtmlNode.element("a", attrs: [.href: url], copy: copy, [])
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Img.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Img.swift
new file mode 100644
index 0000000..6f7ad6b
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Img.swift
@@ -0,0 +1,13 @@
+public struct Img: HtmlProvider {
+ public let html: HtmlNode
+
+ // TODO: make src this a url?
+ /// src - Specifies the path to the image
+ /// alt - Specifies an alternate text for the image, if the image for some reason cannot be displayed
+ public init(src: String, alt: String, attrs: [AttrType: String] = [:]) {
+ var fullAttrs = attrs
+ fullAttrs[.src] = src
+ fullAttrs[.alt] = alt
+ html = .element("img", attrs: fullAttrs, [])
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlElements/Pre.swift b/Modules/SwiftWebsiteDSL/src/HtmlElements/Pre.swift
new file mode 100644
index 0000000..1943572
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlElements/Pre.swift
@@ -0,0 +1,7 @@
+public struct Pre: BlockHtmlProvider {
+ public let html: HtmlNode
+
+ public init(@HtmlBuilder content: () -> [HtmlNode]) {
+ html = .element("pre", attrs: [:], content())
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlNode.swift b/Modules/SwiftWebsiteDSL/src/HtmlNode.swift
new file mode 100644
index 0000000..cfe5ba5
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlNode.swift
@@ -0,0 +1,32 @@
+public enum HtmlNode {
+ indirect case element(
+ String,
+ attrs: [AttrType: String] = [:],
+ copy: String = "",
+ [HtmlNode] = []
+ )
+}
+
+public extension HtmlNode {
+ var render: String {
+ switch self {
+ case let .element(el, attrs, copy, nested) where nested.isEmpty:
+ let attributes = attrs.isEmpty ? "" : " \(attrs.map { "\($0.0)=\"\($0.1)\"" }.sorted().joined(separator: " "))"
+ return """
+ <\(el)\(attributes)>\(copy)\(el)>
+ """
+ case let .element(el, attrs: attrs, _, nested) where ["pre", "code"].contains(el):
+ let attributes = attrs.isEmpty ? "" : " \(attrs.map { "\($0.0)=\"\($0.1)\"" }.sorted().joined(separator: " "))"
+ return """
+ <\(el)\(attributes)>\(nested.map(\.render).joined(separator: "\n"))\(el)>
+ """
+ case let .element(el, attrs: attrs, copy, nested):
+ let attributes = attrs.isEmpty ? "" : " \(attrs.map { "\($0.0)=\"\($0.1)\"" }.sorted().joined(separator: " "))"
+ return """
+ <\(el)\(attributes)>\(copy)
+ \(nested.map(\.render).joined(separator: "\n"))
+ \(el)>
+ """
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/HtmlProvider.swift b/Modules/SwiftWebsiteDSL/src/HtmlProvider.swift
new file mode 100644
index 0000000..f5b567b
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/HtmlProvider.swift
@@ -0,0 +1,3 @@
+public protocol HtmlProvider {
+ var html: HtmlNode { get }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/PreserveIndentationExtension.swift b/Modules/SwiftWebsiteDSL/src/PreserveIndentationExtension.swift
new file mode 100644
index 0000000..e3253d9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/PreserveIndentationExtension.swift
@@ -0,0 +1,11 @@
+// https://forums.swift.org/t/multi-line-string-nested-indentation-with-interpolation/36933/2
+extension DefaultStringInterpolation {
+ mutating func appendInterpolation(indented string: String) {
+ let indent = String(stringInterpolation: self).reversed().prefix { " \t".contains($0) }
+ if indent.isEmpty {
+ appendInterpolation(string)
+ } else {
+ appendLiteral(string.split(separator: "\n", omittingEmptySubsequences: false).joined(separator: "\n" + indent))
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/Auto.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Auto.swift
new file mode 100644
index 0000000..71f299d
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Auto.swift
@@ -0,0 +1,6 @@
+// This is just to provide a nice type to pass into the overloaded margin
+// style. This should probably be revisited later, and possibly combined with
+// the other margin functions as noted in that file.
+public enum Auto {
+ case auto
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/BackgroundType.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/BackgroundType.swift
new file mode 100644
index 0000000..59a5217
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/BackgroundType.swift
@@ -0,0 +1,21 @@
+// TODO: should this be able to configure the style string itself?
+public enum BackgroundType {
+ public struct LinearGradient {
+ public typealias Percent = Int
+ /// Informs the direction for the gradient
+ let degree: Int
+ /// The first color for the gradient and where to start by percentage
+ let first: (Color, Percent)
+ /// The second color for the gradient and where to end by percentage
+ let second: (Color, Percent)
+
+ public init(degree: Int, first: (Color, Percent), second: (Color, Percent)) {
+ self.degree = degree
+ self.first = first
+ self.second = second
+ }
+ }
+
+ case color(Color)
+ case linearGradient(LinearGradient)
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/ColorAttribute.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/ColorAttribute.swift
new file mode 100644
index 0000000..f735650
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/ColorAttribute.swift
@@ -0,0 +1,7 @@
+// TODO: consider a property that adds the `#`
+public struct Color {
+ let hex: String // TODO: raw hex code and separate property with hash?
+ public init(hex: String) {
+ self.hex = hex
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/Font.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Font.swift
new file mode 100644
index 0000000..8f5fc88
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Font.swift
@@ -0,0 +1,11 @@
+public enum Font {
+ case apple
+ case custom(String)
+
+ var style: String {
+ switch self {
+ case .apple: return "font-family: -apple-system"
+ case let .custom(custom): return custom
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/Side.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Side.swift
new file mode 100644
index 0000000..2dad8c9
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/Side.swift
@@ -0,0 +1,23 @@
+import Foundation
+
+public enum Side: CaseIterable {
+ case top, bottom, leading, trailing
+
+ var padding: String {
+ switch self {
+ case .top: return "padding-top"
+ case .bottom: return "padding-bottom"
+ case .leading: return "padding-left"
+ case .trailing: return "padding-right"
+ }
+ }
+
+ var margin: String {
+ switch self {
+ case .top: return "margin-top"
+ case .bottom: return "margin-bottom"
+ case .leading: return "margin-left"
+ case .trailing: return "margin-right"
+ }
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Attributes/TextAlignment.swift b/Modules/SwiftWebsiteDSL/src/Style/Attributes/TextAlignment.swift
new file mode 100644
index 0000000..d5d4817
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Attributes/TextAlignment.swift
@@ -0,0 +1,3 @@
+public enum TextAlignment: String, CaseIterable {
+ case center, left, right, justify
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Background.swift b/Modules/SwiftWebsiteDSL/src/Style/Background.swift
new file mode 100644
index 0000000..803aaa4
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Background.swift
@@ -0,0 +1,32 @@
+// https://www.w3.org/TR/CSS22/colors.html#propdef-background
+
+public extension HtmlProvider {
+ func background(_ background: BackgroundType) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+
+ switch background {
+ case let .linearGradient(lg):
+ newAttrs[.style, default: ""] +=
+ """
+ background: linear-gradient(\
+ \(lg.degree)deg,\
+ #\(lg.first.0.hex) \(lg.first.1)%,\
+ #\(lg.second.0.hex) \(lg.second.1)%\
+ );
+ """
+ case let .color(color):
+ newAttrs[.style, default: ""] += "background: #\(color.hex);"
+ }
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/BorderRadius.swift b/Modules/SwiftWebsiteDSL/src/Style/BorderRadius.swift
new file mode 100644
index 0000000..fabf4f0
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/BorderRadius.swift
@@ -0,0 +1,18 @@
+public extension HtmlProvider {
+ /// Adds a border radius in px to all corners
+ func borderRadius(px: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "border-radius: \(px)px;"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Color.swift b/Modules/SwiftWebsiteDSL/src/Style/Color.swift
new file mode 100644
index 0000000..4ca1c83
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Color.swift
@@ -0,0 +1,19 @@
+// https://www.w3.org/TR/CSS2/colors.html#q14.0
+public extension HtmlProvider {
+ /// This property describes the foreground color of an element's text content.
+ func color(_ color: Color) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "color: #\(color.hex);"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/LineHeight.swift b/Modules/SwiftWebsiteDSL/src/Style/LineHeight.swift
new file mode 100644
index 0000000..db2ac92
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/LineHeight.swift
@@ -0,0 +1,23 @@
+// https://www.w3.org/TR/CSS22/visudet.html#x17
+
+public extension HtmlProvider {
+ /** specifies the minimal height of line boxes within the element.
+ The minimum height consists of a minimum height above the baseline and a minimum depth below it,
+ exactly as if each line box starts with a zero-width inline box with the element's font and line height properties.
+ **/
+ func lineHeight(_ height: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "line-height: \(height);"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Margin.swift b/Modules/SwiftWebsiteDSL/src/Style/Margin.swift
new file mode 100644
index 0000000..4260459
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Margin.swift
@@ -0,0 +1,53 @@
+public extension HtmlProvider {
+ func margin(_ sides: [Side], _ value: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ if Set(Side.allCases).isSubset(of: sides) {
+ newAttrs[.style, default: ""] += "margin: \(value)px;"
+ } else {
+ for side in sides {
+ newAttrs[.style, default: ""] += "\(side.margin): \(value)px;"
+ }
+ }
+
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+
+ // Not sure why a default argument didn't work in the above function 🤷♂️
+ func margin(_ value: Double) -> AnyElement {
+ margin(Side.allCases, value)
+ }
+
+ // TODO: consider combining this with the px function and making auto into a general enum with associated value
+ func margin(_ sides: [Side], _: Auto) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ if Set(Side.allCases).isSubset(of: sides) {
+ newAttrs[.style, default: ""] += "margin: auto;"
+ } else {
+ for side in sides {
+ newAttrs[.style, default: ""] += "\(side.margin): auto;"
+ }
+ }
+
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/MaxWidth.swift b/Modules/SwiftWebsiteDSL/src/Style/MaxWidth.swift
new file mode 100644
index 0000000..b7c58d4
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/MaxWidth.swift
@@ -0,0 +1,18 @@
+public extension HtmlProvider {
+ /// Specifies the max width of the content area using a length unit.
+ func maxWidth(percent: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "max-width: \(percent)%;"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Padding.swift b/Modules/SwiftWebsiteDSL/src/Style/Padding.swift
new file mode 100644
index 0000000..39cb7ac
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Padding.swift
@@ -0,0 +1,29 @@
+public extension HtmlProvider {
+ func padding(_ sides: [Side], _ value: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ if Set(Side.allCases).isSubset(of: sides) {
+ newAttrs[.style, default: ""] += "padding: \(value)px;"
+ } else {
+ for side in sides {
+ newAttrs[.style, default: ""] += "\(side.padding): \(value)px;"
+ }
+ }
+
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+
+ // Not sure why a default argument didn't work in the above function 🤷♂️
+ func padding(_ value: Double) -> AnyElement {
+ padding(Side.allCases, value)
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/TextAlign.swift b/Modules/SwiftWebsiteDSL/src/Style/TextAlign.swift
new file mode 100644
index 0000000..700e1ec
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/TextAlign.swift
@@ -0,0 +1,20 @@
+// https://www.w3.org/TR/CSS22/text.html#x3
+
+public extension BlockHtmlProvider {
+ /// This property describes how inline-level content of a block container is aligned.
+ func textAlign(_ alignment: TextAlignment) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.style, default: ""] += "text-align: \(alignment.rawValue);"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Modules/SwiftWebsiteDSL/src/Style/Width.swift b/Modules/SwiftWebsiteDSL/src/Style/Width.swift
new file mode 100644
index 0000000..7a65e34
--- /dev/null
+++ b/Modules/SwiftWebsiteDSL/src/Style/Width.swift
@@ -0,0 +1,19 @@
+// TODO: figure out putting the width separately or in the 'style' list
+public extension HtmlProvider {
+ /// Specifies the width of the content area using a length unit.
+ func width(_ length: Double) -> AnyElement {
+ let result: AnyElement
+ switch html {
+ case let .element(element, attrs: attrs, copy, nodes):
+ var newAttrs = attrs
+ newAttrs[.width] = "\(length)px;"
+ result = AnyElement(
+ element: element,
+ attrs: newAttrs,
+ copy: copy,
+ nodes: nodes
+ )
+ }
+ return result
+ }
+}
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..81f2977
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,23 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-snapshot-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
+ "state" : {
+ "revision" : "59b663f68e69f27a87b45de48cb63264b8194605",
+ "version" : "1.15.1"
+ }
+ },
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-syntax.git",
+ "state" : {
+ "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
+ "version" : "509.0.2"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..b5e9e0f
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,46 @@
+// swift-tools-version:5.9
+
+import PackageDescription
+
+let package = Package(
+ name: "swift-website-dsl",
+ products: [
+ .library(
+ name: "SwiftWebsiteDSL",
+ targets: ["SwiftWebsiteDSL"]
+ ),
+ .executable(
+ name: "ExampleSwiftWebsite",
+ targets: ["ExampleSwiftWebsite"]
+ ),
+ ],
+ dependencies: [
+ .package(
+ url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
+ from: "1.10.0"
+ ),
+ ],
+ targets: [
+ .target(
+ name: "SwiftWebsiteDSL",
+ dependencies: [],
+ path: "Modules/SwiftWebsiteDSL/src"
+ ),
+ .testTarget(
+ name: "SwiftWebsiteDSLTest",
+ dependencies: [
+ "SwiftWebsiteDSL",
+ .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
+ ],
+ path: "Modules/SwiftWebsiteDSL/Tests",
+ exclude: ["__Snapshots__"]
+ ),
+ .executableTarget(
+ name: "ExampleSwiftWebsite",
+ dependencies: [
+ "SwiftWebsiteDSL",
+ ],
+ path: "Modules/ExampleSwiftWebsite/src"
+ ),
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..64cfb30
--- /dev/null
+++ b/README.md
@@ -0,0 +1,95 @@
+
+
+