diff --git a/.spi.yml b/.spi.yml deleted file mode 100644 index 6d56aebb..00000000 --- a/.spi.yml +++ /dev/null @@ -1,4 +0,0 @@ -version: 1 -builder: - configs: - - documentation_targets: [WebUI] diff --git a/Package.resolved b/Package.resolved index eb6dd308..cc146997 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1ccf4329cf20c417c5f7a71949a5e37c07f5d9d2b8bc5845976fcb430278ab5a", + "originHash" : "27fc44db5d602a3eb6052ee08ebaf473001ed8db7fa1b54b79f7f4682568491d", "pins" : [ { "identity" : "swift-cmark", diff --git a/Package.swift b/Package.swift index c512754d..b736b4a1 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,7 @@ import PackageDescription let package = Package( name: "web-ui", + platforms: [.macOS(.v15), .iOS(.v13), .tvOS(.v13)], products: [ .library(name: "WebUI", targets: ["WebUI"]) ], diff --git a/README.md b/README.md index 8dd52712..97c86044 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# WebUI -

Logo diff --git a/Sources/WebUI/Core/Application.swift b/Sources/WebUI/Core/Application.swift deleted file mode 100644 index e30f8827..00000000 --- a/Sources/WebUI/Core/Application.swift +++ /dev/null @@ -1,243 +0,0 @@ -import Foundation -import Logging - -/// A structure that manages and builds a collection of routes into HTML files. -public struct Application { - private let logger = Logger(label: "com.webui.application") - public let routes: [Document] - - /// Initializes an application with a collection of routes. - /// - Parameter routes: The routes to be built into HTML files. - public init(routes: [Document]) { - self.routes = routes - logger.info("Application initialized with \(routes.count) routes") - } - - /// Builds the application by generating HTML files for each route in the specified directory. - /// - Parameters: - /// - outputDirectory: The directory where HTML files will be generated. Defaults to `.output`. - /// - assetsPath: The path to the public assets directory. Defaults to `Sources/Public`. - /// - Throws: `BuildError` if directory creation, file creation, or asset copying fails. - /// - Complexity: O(n) where n is the number of routes. - public func build( - to outputDirectory: URL = URL(fileURLWithPath: ".output"), - assetsPath: String = "Sources/Public" - ) throws { - logger.info("Starting build process to \(outputDirectory.path)") - logger.debug("Using assets path: \(assetsPath)") - - let fileManager = FileManager.default - - // Remove existing output directory if it exists - logger.debug("Checking for existing output directory") - try removeExistingDirectory(at: outputDirectory, using: fileManager) - - // Create output directory - logger.debug("Creating output directory") - try createDirectory(at: outputDirectory, using: fileManager) - - // Build each route - logger.info("Building \(routes.count) routes") - var failedRoutes: [String] = [] - try buildRoutes( - routes, - to: outputDirectory, - trackingFailuresIn: &failedRoutes, - using: fileManager - ) - - // Copy assets - logger.info("Copying assets from \(assetsPath) to output") - try copyAssets( - from: URL(fileURLWithPath: assetsPath), - to: outputDirectory.appendingPathComponent("public"), - using: fileManager - ) - - // Throw error if any routes failed - if !failedRoutes.isEmpty { - logger.error("Build completed with \(failedRoutes.count) failed routes") - throw BuildError.failedRoutes(failedRoutes) - } - - logger.notice("Build completed successfully.") - } - - /// Removes an existing directory if it exists. - /// - Parameters: - /// - url: The URL of the directory to remove. - /// - fileManager: The file manager to use for operations. - /// - Throws: `BuildError.directoryCreationFailed` if removal fails. - private func removeExistingDirectory( - at url: URL, - using fileManager: FileManager - ) throws { - if fileManager.fileExists(atPath: url.path) { - logger.debug("Removing existing directory at \(url.path)") - do { - try fileManager.removeItem(at: url) - logger.debug("Successfully removed existing directory") - } catch { - logger.error("Failed to remove existing directory: \(error.localizedDescription)") - throw BuildError.directoryCreationFailed(error) - } - } else { - logger.debug("No existing directory found at \(url.path)") - } - } - - /// Creates a directory at the specified URL. - /// - Parameters: - /// - url: The URL where the directory should be created. - /// - fileManager: The file manager to use for operations. - /// - Throws: `BuildError.directoryCreationFailed` if creation fails. - private func createDirectory( - at url: URL, - using fileManager: FileManager - ) throws { - logger.debug("Creating directory at \(url.path)") - do { - try fileManager.createDirectory( - at: url, - withIntermediateDirectories: true - ) - logger.debug("Successfully created directory") - } catch { - logger.error("Failed to create directory: \(error.localizedDescription)") - throw BuildError.directoryCreationFailed(error) - } - } - - /// Builds routes by creating HTML files in the output directory. - /// - Parameters: - /// - routes: The routes to build. - /// - outputDirectory: The directory where HTML files will be created. - /// - failedRoutes: An array to track routes that fail to build. - /// - fileManager: The file manager to use for operations. - /// - Throws: `BuildError.fileCreationFailed` if a file cannot be created. - private func buildRoutes( - _ routes: [Document], - to outputDirectory: URL, - trackingFailuresIn failedRoutes: inout [String], - using fileManager: FileManager - ) throws { - for (index, route) in routes.enumerated() { - let routePath = route.path ?? "unnamed" - logger.debug("Building route [\(index+1)/\(routes.count)]: \(routePath)") - - do { - let pathComponents = route.path?.split(separator: "/") ?? [""] - let fileName = pathComponents.last.map(String.init) ?? "index" - - logger.trace( - "Creating path for route with components: \(pathComponents), filename: \(fileName)") - let filePath = try createPath( - for: pathComponents, - in: outputDirectory, - with: fileName, - using: fileManager - ) - - logger.debug("Rendering HTML for route: \(routePath)") - let htmlContent = route.render().data(using: .utf8) - - logger.debug("Creating file at: \(filePath.path)") - guard - fileManager.createFile( - atPath: filePath.path, - contents: htmlContent - ) - else { - logger.error("Failed to create file for route: \(routePath)") - throw BuildError.fileCreationFailed( - routePath, - nil - ) - } - - logger.debug("Successfully built route: \(routePath)") - } catch { - logger.error("Failed to build route '\(routePath)': \(error.localizedDescription)") - failedRoutes.append(routePath) - print( - "Failed to build route '\(routePath)': \(error.localizedDescription)") - } - } - - if failedRoutes.isEmpty { - logger.info("All routes built successfully") - } else { - logger.warning("\(failedRoutes.count) routes failed to build") - } - } - - /// Creates the file path for a route, including any necessary directories. - /// - Parameters: - /// - components: The path components of the route. - /// - outputDirectory: The base output directory. - /// - fileName: The name of the HTML file. - /// - fileManager: The file manager to use for operations. - /// - Returns: The URL of the created file path. - /// - Throws: `BuildError.directoryCreationFailed` if directory creation fails. - private func createPath( - for components: [Substring], - in outputDirectory: URL, - with fileName: String, - using fileManager: FileManager - ) throws -> URL { - var currentPath = outputDirectory - - if components.count > 1 { - for component in components.dropLast() { - currentPath = currentPath.appendingPathComponent(String(component)) - logger.trace("Creating directory for path component: \(component)") - try createDirectory(at: currentPath, using: fileManager) - } - } - - let finalPath = currentPath.appendingPathComponent("\(fileName).html") - logger.trace("Final file path created: \(finalPath.path)") - return finalPath - } - - /// Copies assets from the source to the destination directory. - /// - Parameters: - /// - sourceURL: The source URL of the assets. - /// - destinationURL: The destination URL for the assets. - /// - fileManager: The file manager to use for operations. - /// - Throws: `BuildError.publicCopyFailed` if copying fails. - private func copyAssets( - from sourceURL: URL, - to destinationURL: URL, - using fileManager: FileManager - ) throws { - if fileManager.fileExists(atPath: sourceURL.path) { - logger.debug("Assets directory exists at: \(sourceURL.path)") - do { - logger.debug("Copying assets to: \(destinationURL.path)") - try fileManager.copyItem(at: sourceURL, to: destinationURL) - logger.debug("Successfully copied assets") - } catch { - logger.error("Failed to copy assets: \(error.localizedDescription)") - throw BuildError.publicCopyFailed(error) - } - } else { - logger.warning("Assets directory not found at: \(sourceURL.path)") - } - } - - /// Errors that can occur during the build process. - public enum BuildError: Error { - /// Indicates failure to create a directory. - case directoryCreationFailed(Error) - - /// Indicates failure to create a file for a route. - case fileCreationFailed(String, Error?) - - /// Indicates that some routes failed to build. - case failedRoutes([String]) - - /// Indicates failure to copy public assets. - case publicCopyFailed(Error) - } -} diff --git a/Sources/WebUI/Core/Document.swift b/Sources/WebUI/Core/Document.swift index acfd6023..74fdd98a 100644 --- a/Sources/WebUI/Core/Document.swift +++ b/Sources/WebUI/Core/Document.swift @@ -8,6 +8,11 @@ public enum ScriptAttribute: String { case async } +public struct Script { + let src: String + let attribute: ScriptAttribute? +} + /// Represents an immutable HTML document with metadata and content. public struct Document { private let logger = Logger(label: "com.webui.document") @@ -17,7 +22,7 @@ public struct Document { public var stylesheets: [String]? public var theme: Theme? public let head: String? - private let contentBuilder: () -> [any HTML] + public let contentBuilder: () -> [any HTML] /// Computed HTML content from the content builder. var content: [any HTML] { @@ -69,42 +74,11 @@ public struct Document { logger.debug("Rendering document: \(path ?? "index")") logger.trace("Starting metadata rendering") - var optionalMetaTags: [String] = [] - if let image = metadata.image, !image.isEmpty { - logger.trace("Adding og:image meta tag: \(image)") - optionalMetaTags.append( - "" - ) - } - if let author = metadata.author, !author.isEmpty { - logger.trace("Adding author meta tag: \(author)") - optionalMetaTags.append("") - } - if let twitter = metadata.twitter, !twitter.isEmpty { - logger.trace("Adding twitter meta tag: \(twitter)") - optionalMetaTags.append( - "" - ) - } - if let keywords = metadata.keywords, !keywords.isEmpty { - logger.trace("Adding keywords meta tag with \(keywords.count) keywords") - optionalMetaTags.append( - "" - ) - } - if let themeColor = metadata.themeColor { - logger.trace("Adding theme-color meta tags") - optionalMetaTags.append( - "" - ) - optionalMetaTags.append( - "" - ) - } + var optionalTags: [String] = metadata.tags + [] if let scripts = scripts { logger.trace("Adding \(scripts.count) script tags") for script in scripts { - optionalMetaTags.append( + optionalTags.append( "" ) } @@ -112,48 +86,27 @@ public struct Document { if let stylesheets = stylesheets { logger.trace("Adding \(stylesheets.count) stylesheet links") for stylesheet in stylesheets { - optionalMetaTags.append( + optionalTags.append( "" ) } } - logger.trace("Building head section") - let headSection = """ + logger.debug("Document rendered successfully: \(metadata.pageTitle)") + + return """ + + \(metadata.pageTitle) - - - - "" - - \(optionalMetaTags.joined(separator: "\n")) + \(optionalTags.joined(separator: "\n")) - \(head ?? "") - - """ - - logger.trace("Rendering content elements") - let contentElements = content.map { $0.render() }.joined() - logger.debug("Document rendered successfully: \(metadata.pageTitle)") - - return """ - - - \(headSection) + - \(contentElements) + \(content.map { $0.render() }.joined()) """ diff --git a/Sources/WebUI/Core/Metadata.swift b/Sources/WebUI/Core/Metadata.swift index 5ae97f98..5c08051f 100644 --- a/Sources/WebUI/Core/Metadata.swift +++ b/Sources/WebUI/Core/Metadata.swift @@ -1,37 +1,28 @@ +// Metadata.swift import Foundation -/// Defines supported Open Graph content types for a document. public enum ContentType: String { case website, article, video, profile } -/// Defines supported language locales for a document. public enum Locale: String { case en, sp, fr, de, ja, ru } -/// Represents theme colors for light and dark modes. public struct ThemeColor { public let light: String - public let dark: String + public let dark: String? - /// Creates a theme color with light and dark mode values. - /// - /// - Parameters: - /// - light: Hex color code for light mode (e.g., "#FFFFFF"). - /// - dark: Hex color code for dark mode (e.g., "#000000"). - public init(light: String, dark: String) { + public init(_ light: String, dark: String? = nil) { self.light = light self.dark = dark } } -/// Stores metadata configuration for a document’s head section. public struct Metadata { public var site: String? public var title: String? - public var titleSeperator: String - public var pageTitle: String + public var titleSeperator: String? public var description: String public var date: Date? public var image: String? @@ -42,24 +33,14 @@ public struct Metadata { public var type: ContentType public var themeColor: ThemeColor? - /// Creates metadata for a document’s head section. - /// - /// - Parameters: - /// - site: Website name used in title and branding, optional. - /// - title: Base title template for the page. - /// - titleSeperator: Separator between title and site, defaults to "|". - /// - description: Brief summary of page content for meta tags. - /// - image: Displayed when the url is shared on social media. - /// - author: Author name, optional. - /// - keywords: SEO keywords, optional. - /// - twitter: Twitter handle without "@", optional. - /// - locale: Language setting, defaults to `.en`. - /// - type: Open Graph content type, defaults to `.website`. - /// - themeColor: Theme colors for light and dark modes, optional. + public var pageTitle: String { + "\(title ?? "")\(titleSeperator ?? "")\(site ?? "")" + } + public init( site: String? = nil, title: String? = nil, - titleSeperator: String = "|", + titleSeperator: String? = " ", description: String, date: Date? = nil, image: String? = nil, @@ -73,8 +54,6 @@ public struct Metadata { self.site = site self.title = title self.titleSeperator = titleSeperator - self.pageTitle = - "\(title ?? "")\(site.map { " \(titleSeperator) \($0)" } ?? "")" self.description = description self.date = date self.image = image @@ -85,25 +64,7 @@ public struct Metadata { self.type = type self.themeColor = themeColor } -} -extension Metadata { - /// Initializes a new `Metadata` instance based on an existing one, with optional modifications to any property. - /// - /// - Parameters: - /// - base: The original `Metadata` instance. - /// - site: Optional site value, default is `nil`. - /// - title: Optional title value, default is `nil`. - /// - titleSeperator: Optional title separator, default is the same as `base`. - /// - description: Optional description value, default is the same as `base`. - /// - date: Optional date value, default is the same as `base`. - /// - image: Optional image value, default is the same as `base`. - /// - author: Optional author value, default is the same as `base`. - /// - keywords: Optional keywords value, default is the same as `base`. - /// - twitter: Optional twitter handle, default is the same as `base`. - /// - locale: Optional locale value, default is the same as `base`. - /// - type: Optional content type, default is the same as `base`. - /// - themeColor: Optional theme color, default is the same as `base`. public init( from base: Metadata, site: String? = nil, @@ -122,8 +83,6 @@ extension Metadata { self.site = site ?? base.site self.title = title ?? base.title self.titleSeperator = titleSeperator ?? base.titleSeperator - self.pageTitle = - "\(title ?? "")\(site.map { " \(titleSeperator ?? "|") \($0)" } ?? "")" self.description = description ?? base.description self.date = date ?? base.date self.image = image ?? base.image @@ -134,4 +93,41 @@ extension Metadata { self.type = type ?? base.type self.themeColor = themeColor ?? base.themeColor } + + var tags: [String] { + var baseTags: [String] = [ + "", + "", + "", + "", + "", + ] + + if let image, !image.isEmpty { + baseTags.append("") + } + if let author, !author.isEmpty { + baseTags.append("") + } + if let twitter, !twitter.isEmpty { + baseTags.append("") + } + if let keywords, !keywords.isEmpty { + baseTags.append( + "" + ) + } + if let themeColor { + baseTags.append( + "" + ) + if let themeDark = themeColor.dark { + baseTags.append( + "" + ) + } + } + + return baseTags + } } diff --git a/Sources/WebUI/Core/Theme.swift b/Sources/WebUI/Core/Theme.swift index 51a8e32e..df796bc9 100644 --- a/Sources/WebUI/Core/Theme.swift +++ b/Sources/WebUI/Core/Theme.swift @@ -208,4 +208,16 @@ public struct Theme { logger.debug("CSS generation completed with \(propertyCount) properties") return css } + + public func generateFile() -> String { + """ + @theme { + --breakpoint-xs: 30rem; + --breakpoint-3xl: 120rem; + --breakpoint-4xl: 160rem; + \(self.generateCSS()) + @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); + } + """ + } } diff --git a/Sources/WebUI/Core/Website.swift b/Sources/WebUI/Core/Website.swift new file mode 100644 index 00000000..cf3083be --- /dev/null +++ b/Sources/WebUI/Core/Website.swift @@ -0,0 +1,175 @@ +import Foundation +import Logging + +/// Manages and builds a collection of routes into HTML files. +public struct Website { + private let logger = Logger(label: "com.webui.application") + + public let metadata: Metadata? + public let theme: Theme? + public let stylesheets: [String]? + public let scripts: [String: ScriptAttribute?]? + public let head: String? + public let routes: [Document] + + /// Initializes a static site. + /// - Parameters: + /// - metadata: Optional default metadata for all routes. + /// - theme: Optional default theme for all routes. + /// - stylesheets: Optional stylesheet URLs for all routes. + /// - scripts: Optional JavaScript sources for all routes. + /// - head: Optional custom head tags for all routes. + /// - routes: Routes to build into HTML files. + public init( + metadata: Metadata? = nil, + theme: Theme? = nil, + stylesheets: [String]? = nil, + scripts: [String: ScriptAttribute?]? = nil, + head: String? = nil, + routes: [Document] + ) { + self.metadata = metadata + self.theme = theme + self.stylesheets = stylesheets + self.scripts = scripts + self.head = head + self.routes = routes.map { document in + // Merge scripts: Website scripts + Document scripts (Document scripts override duplicates) + var mergedScripts = scripts ?? [:] + if let documentScripts = document.scripts { + mergedScripts.merge(documentScripts) { _, new in new } + } + + // Merge stylesheets: Website stylesheets + Document stylesheets (combine, remove duplicates) + let mergedStylesheets = Array( + Set((stylesheets ?? []) + (document.stylesheets ?? [])) + ) + + // Merge head: Use Document head if provided, otherwise use Website head + let mergedHead = document.head ?? head + + // Create new Document with merged properties + return Document( + path: document.path, + metadata: document.metadata, + scripts: mergedScripts.isEmpty ? nil : mergedScripts, + stylesheets: mergedStylesheets.isEmpty ? nil : mergedStylesheets, + theme: document.theme ?? theme, + head: mergedHead, + content: document.contentBuilder + ) + } + + logger.info( + "Initialized '\(metadata?.site ?? "Untitled")' with \(routes.count) routes, theme: \(theme != nil ? "set" : "none")" + ) + } + + /// Builds HTML files for each route in the specified directory. + /// - Parameters: + /// - outputDirectory: Directory for generated HTML files. Defaults to `.output`. + /// - assetsPath: Path to public assets. Defaults to `Sources/Public`. + /// - Throws: `BuildError` if file operations fail. + public func build( + to outputDirectory: URL = URL(filePath: ".output"), + assetsPath: String = "Sources/Public" + ) throws { + logger.info( + "Starting build to '\(outputDirectory.path)' with assets from '\(assetsPath)'" + ) + let fileManager = FileManager.default + + // Clear and create output directory + logger.debug( + "Checking for existing output directory at '\(outputDirectory.path)'" + ) + if fileManager.fileExists(atPath: outputDirectory.path) { + logger.trace("Removing existing output directory") + try fileManager.removeItem(at: outputDirectory) + logger.debug("Cleared existing output directory") + } else { + logger.trace("No existing output directory found") + } + + logger.trace("Creating output directory at '\(outputDirectory.path)'") + try fileManager.createDirectory( + at: outputDirectory, + withIntermediateDirectories: true + ) + logger.debug("Created output directory") + + // Build routes + logger.info("Building \(routes.count) routes") + var failedRoutes: [String] = [] + for (index, route) in routes.enumerated() { + let routePath = route.path ?? "unnamed" + logger.debug( + "Processing route [\(index+1)/\(routes.count)]: '\(routePath)'" + ) + + do { + let pathComponents = route.path?.split(separator: "/") ?? [""] + let fileName = pathComponents.last.map(String.init) ?? "index" + var currentPath = outputDirectory + + // Create intermediate directories + if pathComponents.count > 1 { + logger.trace( + "Creating directories for path components: \(pathComponents.dropLast())" + ) + for component in pathComponents.dropLast() { + currentPath.appendPathComponent(String(component)) + try fileManager.createDirectory( + at: currentPath, + withIntermediateDirectories: true + ) + } + logger.debug("Created intermediate directories for '\(routePath)'") + } + + // Write HTML file + let filePath = currentPath.appendingPathComponent("\(fileName).html") + logger.trace("Rendering HTML for '\(routePath)' to '\(filePath.path)'") + let htmlContent = route.render().data(using: .utf8) + guard + fileManager.createFile(atPath: filePath.path, contents: htmlContent) + else { + throw BuildError.fileCreationFailed(routePath) + } + logger.debug("Successfully wrote HTML file for '\(routePath)'") + } catch { + logger.error("Failed to build route '\(routePath)': \(error)") + failedRoutes.append(routePath) + } + } + + // Copy assets + let sourceURL = URL(fileURLWithPath: assetsPath) + let destinationURL = outputDirectory.appendingPathComponent("public") + logger.info( + "Copying assets from '\(sourceURL.path)' to '\(destinationURL.path)'" + ) + if fileManager.fileExists(atPath: sourceURL.path) { + logger.trace("Copying assets directory") + try fileManager.copyItem(at: sourceURL, to: destinationURL) + logger.debug("Successfully copied assets") + } else { + logger.warning("Assets directory not found at '\(sourceURL.path)'") + } + + // Report failures + if !failedRoutes.isEmpty { + logger.error( + "Build completed with \(failedRoutes.count) failed routes: \(failedRoutes.joined(separator: ", "))" + ) + throw BuildError.failedRoutes(failedRoutes) + } + logger.info("Build completed successfully with \(routes.count) routes") + } + + /// Errors during the build process. + public enum BuildError: Error { + case fileCreationFailed(String) + case failedRoutes([String]) + } +} diff --git a/Sources/WebUI/Elements/Base/Text.swift b/Sources/WebUI/Elements/Base/Text.swift index 94a8fa5f..9bdff69b 100644 --- a/Sources/WebUI/Elements/Base/Text.swift +++ b/Sources/WebUI/Elements/Base/Text.swift @@ -68,7 +68,7 @@ public final class Heading: Element { /// - data: Dictionary of `data-*` attributes for element relevant storing data. /// - content: Closure providing heading content. public init( - level: HeadingLevel, + _ level: HeadingLevel, id: String? = nil, classes: [String]? = nil, role: AriaRole? = nil, diff --git a/Tests/WebUITests/Core/ApplicationTests.swift b/Tests/WebUITests/Core/ApplicationTests.swift index ddb4bd58..47bbf059 100644 --- a/Tests/WebUITests/Core/ApplicationTests.swift +++ b/Tests/WebUITests/Core/ApplicationTests.swift @@ -6,7 +6,7 @@ import Testing @Suite("Application Tests") struct ApplicationTests { @Test("Creates the build directory and populates correctly") func createsAndPopulatesBuildDirectory() throws { - let app = Application( + let app = Website( routes: [ Document( path: "index", @@ -22,7 +22,7 @@ import Testing } Main { Stack { - Heading(level: .one) { "Tagline" } + Heading(.one) { "Tagline" } Text { "Lorem ipsum dolor sit amet." } } } @@ -35,7 +35,7 @@ import Testing metadata: .init(title: "About", description: "Learn more here") ) { Article { - Heading(level: .two) { "Article Heading" } + Heading(.two) { "Article Heading" } Text { "Lorem ipsum dolor sit amet." } } }, diff --git a/Tests/WebUITests/Core/DocumentTests.swift b/Tests/WebUITests/Core/DocumentTests.swift index 093e1c36..19262a91 100644 --- a/Tests/WebUITests/Core/DocumentTests.swift +++ b/Tests/WebUITests/Core/DocumentTests.swift @@ -11,6 +11,7 @@ import Testing metadata: Metadata( site: "Test Site", title: "Hello World", + titleSeperator: " | ", description: "A test description" ) ) { @@ -40,7 +41,7 @@ import Testing metadata: Metadata( site: "Test Site", title: "Full Test", - titleSeperator: "-", + titleSeperator: " - ", description: "A complete metadata test", date: Date(), image: "https://example.com/image.png", @@ -49,7 +50,7 @@ import Testing twitter: "testhandle", locale: .ru, type: .article, - themeColor: .init(light: "#0099ff", dark: "#1c1c1c"), + themeColor: .init("#0099ff", dark: "#1c1c1c"), ) ) { "Content" diff --git a/Tests/WebUITests/Core/MetadataTests.swift b/Tests/WebUITests/Core/MetadataTests.swift index 9c757ac3..02dd8aa9 100644 --- a/Tests/WebUITests/Core/MetadataTests.swift +++ b/Tests/WebUITests/Core/MetadataTests.swift @@ -11,15 +11,16 @@ struct MetadataTests { let metadata = Metadata( site: "Test Site", title: "Test Title", + titleSeperator: " | ", description: "Test description", - themeColor: .init(light: "#0099ff", dark: "#1c1c1c") + themeColor: .init("#0099ff", dark: "#1c1c1c") ) // Assert #expect(metadata.site == "Test Site") #expect(metadata.title == "Test Title") #expect(metadata.description == "Test description") - #expect(metadata.titleSeperator == "|") + #expect(metadata.titleSeperator == " | ") #expect(metadata.pageTitle == "Test Title | Test Site") #expect(metadata.locale == .en) #expect(metadata.themeColor?.light == "#0099ff") @@ -30,6 +31,7 @@ struct MetadataTests { @Test func testNoSiteMetadata() throws { let metadata = Metadata( title: "Just Title", + titleSeperator: nil, description: "No site metadata" ) @@ -42,11 +44,11 @@ struct MetadataTests { let metadata = Metadata( site: "My Site", title: "My Title", - titleSeperator: "-", + titleSeperator: " - ", description: "Custom separator test" ) - #expect(metadata.titleSeperator == "-") + #expect(metadata.titleSeperator == " - ") #expect(metadata.pageTitle == "My Title - My Site") } @@ -58,7 +60,7 @@ struct MetadataTests { let metadata = Metadata( site: "Full Site", title: "Full Title", - titleSeperator: ":", + titleSeperator: " : ", description: "Full metadata description", date: testDate, image: "/images/test.jpg", diff --git a/Tests/WebUITests/ElementTests.swift b/Tests/WebUITests/ElementTests.swift index e1e90120..88983a4d 100644 --- a/Tests/WebUITests/ElementTests.swift +++ b/Tests/WebUITests/ElementTests.swift @@ -292,9 +292,9 @@ import Testing @Test("Heading element") func testHeadingElement() async throws { - let heading1 = Heading(level: .one) { "Main Title" } - let heading2 = Heading(level: .two) { "Subtitle" } - let heading6 = Heading(level: .six) { "Small Heading" } + let heading1 = Heading(.one) { "Main Title" } + let heading2 = Heading(.two) { "Subtitle" } + let heading6 = Heading(.six) { "Small Heading" } #expect(heading1.render() == "

Main Title

") #expect(heading2.render() == "

Subtitle

") @@ -484,7 +484,7 @@ import Testing @Test("Header element") func testHeaderElement() async throws { let header = Header(id: "page-header") { - Heading(level: .one) { "Site Title" } + Heading(.one) { "Site Title" } } let rendered = header.render() @@ -551,7 +551,7 @@ import Testing @Test("Article element") func testArticleElement() async throws { let article = Article(id: "blog-post") { - Heading(level: .two) { "Article Title" } + Heading(.two) { "Article Title" } Text { "Article content" } } @@ -566,7 +566,7 @@ import Testing @Test("Section element") func testSectionElement() async throws { let section = Section(id: "features") { - Heading(level: .three) { "Features" } + Heading(.three) { "Features" } Text { "Feature list" } } @@ -649,7 +649,7 @@ import Testing func testPageLayout() async throws { let page = Fragment { Header(id: "main-header") { - Heading(level: .one) { "My Website" } + Heading(.one) { "My Website" } Navigation { List(type: .unordered, classes: ["nav-links"]) { Item { Link(to: "/") { "Home" } } @@ -660,12 +660,12 @@ import Testing } Main { Article { - Heading(level: .two) { "Welcome" } + Heading(.two) { "Welcome" } Text { "This is the main content of the page." } } } Aside(id: "sidebar") { - Heading(level: .three) { "Related Links" } + Heading(.three) { "Related Links" } List(type: .unordered) { Item { Link(to: "/link1") { "Link 1" } } Item { Link(to: "/link2") { "Link 2" } } diff --git a/Tests/WebUITests/Styles/AppearanceTests.swift b/Tests/WebUITests/Styles/AppearanceTests.swift index ae792dda..f14b61da 100644 --- a/Tests/WebUITests/Styles/AppearanceTests.swift +++ b/Tests/WebUITests/Styles/AppearanceTests.swift @@ -71,7 +71,7 @@ import Testing let rendered = element.render() #expect(rendered.contains("class=\"rounded-md\"")) } - + @Test("Border with radius on just one side") func testBorderWithOneSidedRadius() async throws { let element = Element(tag: "div").rounded(.full, .topLeft)