import Foundation extension Sequence where Element: Hashable { func uniqued() -> [Element] { var set = Set() return filter { set.insert($0).inserted } } } public extension StringProtocol { var whitespacesFiltered: String { split(whereSeparator: \.isNewline) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0 != "" } .joined(separator: "\n") } } public protocol HTMLComponent { var importedScripts: [String] { get } var importedStyles: [String] { get } var inlineStyle: String? { get } func render() -> String } public struct HTMLPage : CustomStringConvertible { private let title: String private let language: String private let charset: String private let pageStyles: [String] private let components: [HTMLComponent] public init(title: String, components: [HTMLComponent], pageStyles: [String] = ["https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"], language: String = "en", charset: String = "utf-8") { self.title = title self.pageStyles = pageStyles self.language = language self.charset = charset self.components = components } public var description: String { var scripts = [String]() var styles = [String]() components.forEach { component in component.importedScripts.forEach { script in if !scripts.contains(script) { scripts.append(script) } } component.importedStyles.forEach { style in if !styles.contains(style) { styles.append(style) } } } pageStyles.forEach { pageStyle in if !styles.contains(pageStyle) { styles.append(pageStyle) } } let htmlScripts = scripts.map { "" }.joined(separator: "\n") let htmlStyles = styles.map { "" }.joined(separator: "\n") let inlineStyles = components.compactMap { $0.inlineStyle }.uniqued().joined(separator: "\n") var htmlInlineStyles = "" if !inlineStyles.isEmpty { htmlInlineStyles = "" } return """ \(htmlStyles) \(htmlScripts) \(htmlInlineStyles) \(title)
\(components.map { $0.render() }.joined(separator: "\n"))
""".whitespacesFiltered } } public struct HTMLCode : HTMLComponent { public var importedScripts: [String] = [] public var importedStyles: [String] = [] private let html: String public let inlineStyle: String? public init(_ html: String, inlineStyle: String? = nil) { self.html = html self.inlineStyle = inlineStyle } public func render() -> String { html } } public struct HTMLTitle : HTMLComponent { public var importedScripts: [String] = [] public var importedStyles: [String] = [] private let html: String public let inlineStyle: String? = nil public init(_ title: String, heading: Int = 1) { self.html = "\(title)" } public func render() -> String { html } } public struct HTMLTile: HTMLComponent { public let importedScripts: [String] public let importedStyles: [String] public let inlineStyle: String? = nil public let components: [HTMLComponent] public init(_ components: [HTMLComponent] = []) { self.components = components self.importedStyles = components.map { $0.importedStyles }.flatMap { $0 }.uniqued() self.importedScripts = components.map { $0.importedScripts }.flatMap { $0 }.uniqued() } public func render() -> String { """
\(components.map { $0.render() }.joined())
""" } } public struct HTMLTiles: HTMLComponent { public let importedScripts: [String] public let importedStyles: [String] public let inlineStyle: String? = nil public let tiles: [HTMLTile] public init(_ tiles: [HTMLTile] = []) { self.tiles = tiles self.importedStyles = tiles.map { $0.importedStyles }.flatMap { $0 }.uniqued() self.importedScripts = tiles.map { $0.importedScripts }.flatMap { $0 }.uniqued() } public func render() -> String { """
\(tiles.map { $0.render() }.joined())
""" } } // Example usage: let page = HTMLPage( title: "My Page", components: [ HTMLTitle("Hello, world"), HTMLTiles([ HTMLTile([ HTMLCode("First tile") ]), HTMLTile([ HTMLCode("Second tile"), ]), HTMLTile([ HTMLCode("Third tile"), ]) ]), HTMLTiles([ HTMLTile([ HTMLCode("Fourth tile") ]), HTMLTile([ HTMLCode("Fifth tile"), ]), HTMLTile([ HTMLCode("Sixth tile"), ]) ]) ] ) let destination = FileManager.default .temporaryDirectory .appendingPathComponent("test") .appendingPathExtension("html") print("Saving page to \(destination.path)") try page.description.data(using: .utf8)?.write(to: destination)