Skip to content

Instantly share code, notes, and snippets.

@HonmaMasaru
Last active November 3, 2022 09:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HonmaMasaru/0a558a87732a3148953e3df40c188172 to your computer and use it in GitHub Desktop.
Save HonmaMasaru/0a558a87732a3148953e3df40c188172 to your computer and use it in GitHub Desktop.
Build HTML with ResultBuilder
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