Last active
November 3, 2022 09:43
Build HTML with ResultBuilder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment