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 @@ +alt required string \ 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 @@ +alt required string \ 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) + """ + 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")) + """ + 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")) + + """ + } + } +} 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 @@ +![example workflow](https://github.com/jasonzurita/swift-website-dsl/actions/workflows/tests.yml/badge.svg) + +

+ Swift Website DSL Logo + Swift Website DSL Logo +

+ +--- + +`swift-website-dsl` is a domain specific language (DSL) library for building websites using the [Swift programming language](https://www.swift.org). + +While some website DSLs built in Swift abstract away HTML/CSS, this DSL doesn't. This library keeps the elements of HTML/CSS while providing the elegance of writing code in a style like SwiftUI and with the type safety of Swift. The result is a DSL that encodes the HTML spec and keeps is rooted in the familiarity of website development but leverages the power the Swift language to make it more enjoyable and less error prone. + +The [Swifty Notes](https://swiftynotes.com) website is an example of a site written using this library. Here is a code snippet to see what using this library is like +```swift +Html { + Head(title: "SwiftyNotes", cssStyleFileName: "CodeColors.css") + Body { + SiteHeader() + SiteNotes() + Footer { + P("Jason Zurita © 2024 | Built in Swift and ") { + A(copy: "open source.", url: "https://github.com/jasonzurita/swiftynotes") + } + } + .textAlign(.center) + .color(.mediumGray) + } + .font(.apple) + .textAlign(.center) + .background(.color(.lightGray)) + .margin(0) + .padding(0) +} +``` + +# Quick Start (building the example website) +To test out the DSL using the provided example website +- Open terminal +- Clone this repo + + `git clone https://github.com/jasonzurita/swift-website-dsl` +- Change directories to the cloned project + + `cd /swift-website-dsl` +- Generate the example website + + `swift run` +- Open the generated website in your browser + + `open _site/index.html` + + Congrats 🥳. Try and make changes to the example site, `swift run` again, and refresh your browser! + +# Getting Started (using this library) +_Note: The [example website](./Modules/ExampleSwiftWebsite/src) and [swiftynotes project](https://github.com/jasonzurita/swiftynotes) setup are good examples to follow along with the below steps_ +- Add the swift-website-dsl to your package.swift manifest + ```swift + dependencies: [ + .package(url: "https://github.com/jasonzurita/swift-website-dsl.git", from: "1.0.0"), + ] + ``` +- Make use of the library in the desired _target_ + ```swift + .product(name: "SwiftWebsiteDSL", package: "swift-website-dsl"), + ``` +- Import the library to get access to all the HTML elements and styling modifiers + + `import SwiftWebsiteDSL` +- To generate your website + + Create a `main.swift` file like [this](./Modules/ExampleSwiftWebsite/src/main.swift) + + This step will define the output folder for the generated site. For example, the example website uses `_site` +- Make your website module an executable in your package.swift + ```swift + products: [ + .executable(name: "", targets: [""]), + ], + ``` +- To build and generate the site + + `swift run` from the root directory + +# Tech Stack + +## Modules +There are two [Swift modules](./Package.swift) in this repo: the example website, and the HTML eDSL. + +### [The HTML eDSL](./Modules/SwiftWebsiteDSL) +The HTML embedded domain specific language (eDSL) was build using Swift's [Result Builders](https://developer.apple.com/videos/play/wwdc2021/10253/) to be _SwiftUI_ like in its syntax. Knowing [about HTML](https://www.w3schools.com/html/) will help in knowing the provided elements like _body_ and what styles can be used. + +### [The Example Website Module](https://github.com/jasonzurita/swift-website-dsl/tree/main/Modules/ExampleSwiftWebsite/src) +This module consumes the above HTML eDSL library. The result is a fully Swift defined site along with styling. When `swift run` is invoked, this module runs as an executable and [generates HTML](./Modules/ExampleSwiftWebsite/src/main.swift) that is ready to be statically hosted and consumed by browsers. + +## Tests +The building block elements (e.g., H and Body tags) are individually generated in tests (including styles) and then text snapshotted using the [Point-Free Snapshot Testing library](https://github.com/pointfreeco/swift-snapshot-testing). The result is that there is a high level of code coverage and that each element is _locked_ in with the generated output HTML and given style. This further ensures the HTML spec is codified and won't break when making changes to the library. Check out the [snapshots](./Modules/SwiftWebsiteDSL/Tests/__Snapshots__) and [tests](./Modules/SwiftWebsiteDSL/Tests/). + +Tests can be run locally and also automatically run in CI via the [test workflow](./.github/workflows/). + +## Deployment +The output of the `_site` directory is a ready to go static website! For example, the [SwiftyNotes](https://swiftynotes.com) site is hosted using [CloudFlare pages](https://pages.cloudflare.com) as a static website. When a new commit is _pushed_ to the main branch, [GitHub Actions](https://github.com/jasonzurita/swiftynotes/blob/main/.github/workflows/publish.yml) runs the tests, builds the website, and then uploads the generated site (the `_site` directory) for CloudFlare to host it 🚀! +