import Foundation /// 要素 protocol Element { /// HTMLを書き出すレンダー /// - Returns: HTML func render() -> String } /// HTML要素 protocol HTMLElement: Element { /// タグ名 var tag: String { get } /// 属性 var attribute: [String: String]? { get set } /// タグ内部 var innerHTML: [Element]? { get set } /// 初期化 init() /// 初期化 /// - Parameters: /// - attribute: 属性 /// - innerHTML: タグ内部 init(_ attribute: [String: String]?, @ElementBuilder innerHTML: () -> [Element]) } extension HTMLElement { /// 初期化のデフォルト実装 init(_ attribute: [String: String]? = nil, @ElementBuilder innerHTML: () -> [Element]) { self.init() self.innerHTML = innerHTML() self.attribute = attribute } /// レンダーのデフォルト実装 func render() -> String { // 属性と内部タグが設定されていない場合はオープンタグ if attribute == nil, innerHTML == nil { return "<\(tag)>" } // 属性 let a = attribute?.reduce(into: "") { $0 += $1.value.isEmpty ? " \($1.key)" : #" \#($1.key)="\#($1.value)""# } ?? "" // 内部タグ let i = innerHTML?.reduce(into: "") { $0 += $1.render() } ?? "" return "<\(tag)\(a)>\(i)</\(tag)>" } } // MARK: - resultBuilder /// HTMLを書き出すresultBuilder @resultBuilder struct HTMLBuilder { static func buildBlock(_ elements: HTMLElement...) -> String { elements.reduce(into: "") { $0 += $1.render() } } } /// 内部タグを取得するresultBuilder /// HTMLElementをネストさせるために使用 @resultBuilder struct ElementBuilder { static func buildBlock(_ elements: Element...) -> [Element] { elements } } // MARK: - 既存のstructの拡張 extension String: Element { func render() -> String { self } } extension Int: Element { func render() -> String { "\(self)" } } // MARK: - タグ /// Doctype struct Doctype: HTMLElement { let tag = "!DOCTYPE html" var attribute: [String: String]? var innerHTML: [Element]? // 属性と内部タグを無効にするために初期化とレンダーをオーバーライド init() {} init(_ attribute: [String: String]?, @ElementBuilder innerHTML: () -> [Element]) {} func render() -> String { "<!DOCTYPE html>" } } struct Html: HTMLElement { let tag = "html" var attribute: [String: String]? var innerHTML: [Element]? } struct Head: HTMLElement { let tag = "head" var attribute: [String: String]? var innerHTML: [Element]? } struct Title: HTMLElement { let tag = "title" var attribute: [String: String]? var innerHTML: [Element]? } struct Body: HTMLElement { let tag = "body" var attribute: [String: String]? var innerHTML: [Element]? } struct P: HTMLElement { let tag = "p" var attribute: [String: String]? var innerHTML: [Element]? } struct Br: HTMLElement { let tag = "br" var attribute: [String: String]? var innerHTML: [Element]? // 内部タグを無効にするために初期化とレンダーをオーバーライド init() {} init(_ attribute: [String: String]?, @ElementBuilder innerHTML: () -> [Element]) { self.attribute = attribute } func render() -> String { // 属性 let a = attribute?.reduce(into: "") { $0 += $1.value.isEmpty ? " \($1.key)" : #" \#($1.key)="\#($1.value)""# } ?? "" return "<\(tag)\(a)>" } } // MARK: - Test // 外部で要素を作れる var p = P { "段落" } p.attribute = ["data-test1": "333"] p.attribute?["data-test2"] = "444" // HTMLの作成 @HTMLBuilder func html() -> String { Doctype() Html { Head { Title { "タイトル" } } Body { P(["style": "color:red"]) { "テキスト" // String p 1 // Int } P() // オープンタグ P {} // クローズタグ Br() Br(["data-test3": ""]) { "内部タグは無視される" } } } } print(html()) // <!DOCTYPE html><html><head><title>タイトル</title></head><body><p style="color:red">テキスト<p data-test1="333" data-test2="444">段落</p>1</p><p><p></p><br><br data-test3></body></html>