Skip to content

Commit 9e0aa44

Browse files
can now create custom elements or attributes
1 parent 1aeb69d commit 9e0aa44

File tree

4 files changed

+113
-62
lines changed

4 files changed

+113
-62
lines changed

Sources/HTMLKit/HTMLKit.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public extension StaticString {
1717
@freestanding(expression)
1818
public macro html<T: ExpressibleByStringLiteral>(xmlns: T? = nil, _ innerHTML: [T]) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")
1919

20+
@freestanding(expression)
21+
public macro custom<T: ExpressibleByStringLiteral>(tag: String, isVoid: Bool, attributes: [HTMLElementAttribute] = [], _ innerHTML: [T] = []) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")
22+
2023
// MARK: A
2124
@freestanding(expression)
2225
public macro a<T: ExpressibleByStringLiteral>(

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,16 @@ import HTMLKitUtilities
1212

1313
struct HTMLElement : ExpressionMacro {
1414
static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
15-
let type:HTMLElementType = HTMLElementType(rawValue: node.macroName.text)!
16-
let data:ElementData = parse_arguments(context: context, elementType: type, arguments: node.arguments)
17-
var string:String = (type == .html ? "<!DOCTYPE html>" : "") + "<" + type.rawValue + data.attributes + ">" + data.innerHTML
18-
if !type.isVoid {
19-
string += "</" + type.rawValue + ">"
20-
}
21-
return "\"\(raw: string)\""
15+
return "\"\(raw: parse_element_macro(context: context, expression: node.as(MacroExpansionExprSyntax.self)!))\""
2216
}
2317
}
2418

2519
private extension HTMLElement {
26-
static func parse_arguments(context: some MacroExpansionContext, elementType: HTMLElementType, arguments: LabeledExprListSyntax) -> ElementData {
20+
static func parse_arguments(context: some MacroExpansionContext, elementType: HTMLElementType, children: Slice<SyntaxChildren>) -> ElementData {
2721
var attributes:[String] = [], innerHTML:[String] = []
28-
for element in arguments.children(viewMode: .all) {
22+
for element in children {
2923
if let child:LabeledExprSyntax = element.as(LabeledExprSyntax.self) {
30-
if var key:String = child.label?.text { // attributes
24+
if var key:String = child.label?.text {
3125
if key == "attributes" {
3226
attributes.append(contentsOf: parse_global_attributes(context: context, elementType: elementType, array: child.expression.as(ArrayExprSyntax.self)!))
3327
} else {
@@ -54,24 +48,36 @@ private extension HTMLElement {
5448
static func parse_global_attributes(context: some MacroExpansionContext, elementType: HTMLElementType, array: ArrayExprSyntax) -> [String] {
5549
var keys:Set<String> = [], attributes:[String] = []
5650
for element in array.elements {
57-
let function:FunctionCallExprSyntax = element.expression.as(FunctionCallExprSyntax.self)!
51+
let function:FunctionCallExprSyntax = element.expression.as(FunctionCallExprSyntax.self)!, key_element:ExprSyntax = function.arguments.first!.expression
5852
var key:String = function.calledExpression.as(MemberAccessExprSyntax.self)!.declName.baseName.text, value:String? = nil
59-
if key == "data" {
60-
var (literalValue, returnType):(String, LiteralReturnType) = parse_literal_value(elementType: elementType, key: "data", expression: function.arguments.last!.expression)!
61-
if returnType == .interpolation {
62-
literalValue = "\\(" + literalValue + ")"
63-
}
64-
value = literalValue
65-
key += "-\(function.arguments.first!.expression.as(StringLiteralExprSyntax.self)!.string)"
66-
} else if key == "event" {
67-
key = "on" + function.arguments.first!.expression.as(MemberAccessExprSyntax.self)!.declName.baseName.text
68-
value = function.arguments.last!.expression.as(StringLiteralExprSyntax.self)!.string
69-
} else if let string:String = parse_attribute(elementType: elementType, key: key, expression: function.arguments.first!.expression) {
70-
value = string
53+
switch key {
54+
case "custom", "data":
55+
var (literalValue, returnType):(String, LiteralReturnType) = parse_literal_value(elementType: elementType, key: key, expression: function.arguments.last!.expression)!
56+
if returnType == .interpolation {
57+
literalValue = "\\(" + literalValue + ")"
58+
}
59+
value = literalValue
60+
if key == "custom" {
61+
key = key_element.as(StringLiteralExprSyntax.self)!.string
62+
} else {
63+
key += "-\(key_element.as(StringLiteralExprSyntax.self)!.string)"
64+
}
65+
break
66+
case "event":
67+
key = "on" + key_element.as(MemberAccessExprSyntax.self)!.declName.baseName.text
68+
value = function.arguments.last!.expression.as(StringLiteralExprSyntax.self)!.string
69+
break
70+
default:
71+
if let string:String = parse_attribute(elementType: elementType, key: key, expression: key_element) {
72+
value = string
73+
}
74+
break
7175
}
72-
if let value:String = value {
76+
if key.contains(" ") {
77+
context.diagnose(Diagnostic(node: key_element, message: ErrorDiagnostic(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
78+
} else if let value:String = value {
7379
if keys.contains(key) {
74-
context.diagnose(Diagnostic(node: element, message: ErrorDiagnostic(id: "globalAttributeAlreadyDefined", message: "Global attribute is already defined.")))
80+
context.diagnose(Diagnostic(node: key_element, message: ErrorDiagnostic(id: "globalAttributeAlreadyDefined", message: "Global attribute is already defined.")))
7581
} else {
7682
attributes.append(key + (value.isEmpty ? "" : "=\\\"" + value + "\\\""))
7783
keys.insert(key)
@@ -82,8 +88,25 @@ private extension HTMLElement {
8288
}
8389
static func parse_element_macro(context: some MacroExpansionContext, expression: MacroExpansionExprSyntax) -> String {
8490
guard let elementType:HTMLElementType = HTMLElementType(rawValue: expression.macroName.text) else { return "\(expression)" }
85-
let data:ElementData = parse_arguments(context: context, elementType: elementType, arguments: expression.arguments)
86-
return "<" + elementType.rawValue + data.attributes + ">" + data.innerHTML + (elementType.isVoid ? "" : "</" + elementType.rawValue + ">")
91+
let childs:SyntaxChildren = expression.arguments.children(viewMode: .all)
92+
let tag:String, isVoid:Bool
93+
var children:Slice<SyntaxChildren>
94+
if elementType == .custom {
95+
tag = childs.first(where: { $0.as(LabeledExprSyntax.self)?.label?.text == "tag" })!.as(LabeledExprSyntax.self)!.expression.as(StringLiteralExprSyntax.self)!.string
96+
isVoid = childs.first(where: { $0.as(LabeledExprSyntax.self)?.label?.text == "isVoid" })!.as(LabeledExprSyntax.self)!.expression.as(BooleanLiteralExprSyntax.self)!.literal.text == "true"
97+
children = childs.dropFirst() // tag
98+
children.removeFirst() // isVoid
99+
} else {
100+
tag = elementType.rawValue
101+
isVoid = elementType.isVoid
102+
children = childs.prefix(childs.count)
103+
}
104+
let data:ElementData = parse_arguments(context: context, elementType: elementType, children: children)
105+
var string:String = (elementType == .html ? "<!DOCTYPE html>" : "") + "<" + tag + data.attributes + ">" + data.innerHTML
106+
if !isVoid {
107+
string += "</" + tag + ">"
108+
}
109+
return string
87110
}
88111

89112
struct ElementData {
@@ -195,6 +218,7 @@ enum LiteralReturnType {
195218
// MARK: HTMLElementType
196219
enum HTMLElementType : String {
197220
case html
221+
case custom
198222

199223
case a
200224
case abbr

Sources/HTMLKitUtilities/HTMLKitUtilities.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public enum HTMLElementAttribute {
8080
case virtualkeyboardpolicy(Extra.virtualkeyboardpolicy? = nil)
8181
case writingsuggestions(Extra.writingsuggestions? = nil)
8282

83+
case custom(_ id: any ExpressibleByStringLiteral, _ value: (any ExpressibleByStringLiteral)?)
84+
8385
@available(*, deprecated, message: "\nInline event handlers are an outdated way to handle events. General consensus considers this \"bad practice\" and you shouldn't mix your HTML and JavaScript.\n\nThis will never be removed and remains deprecated to encourage use of other techniques.\n\nLearn more at https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_—_dont_use_these.")
8486
case event(Extra.event, _ value: (any ExpressibleByStringLiteral)? = nil)
8587
}

Tests/HTMLKitTests/HTMLKitTests.swift

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,107 +25,129 @@ struct HTMLKitTests {
2525
}
2626

2727
extension HTMLKitTests {
28-
@Test func test_element_html() {
28+
@Test func element_html() {
2929
#expect(#html([]) == "<!DOCTYPE html><html></html>")
3030
#expect(#html(xmlns: "test", []) == "<!DOCTYPE html><html xmlns=\"test\"></html>")
3131
}
32-
@Test func test_element_area() {
32+
@Test func element_area() {
3333
#expect(#area(coords: [1, 2, 3]) == "<area coords=\"1,2,3\">")
3434
}
35-
@Test func test_element_audio() {
35+
@Test func element_audio() {
3636
#expect(#audio(controlslist: .nodownload) == "<audio controlslist=\"nodownload\"></audio>")
3737
}
38-
@Test func test_element_button() {
38+
@Test func element_button() {
3939
#expect(#button(type: .submit) == "<button type=\"submit\"></button>")
4040
}
41-
@Test func test_element_canvas() {
41+
@Test func element_canvas() {
4242
#expect(#canvas(height: .percent(4), width: .em(2.69)) == "<canvas height=\"4%\" width=\"2.69em\"></canvas>")
4343
}
44-
@Test func test_element_form() {
44+
@Test func element_form() {
4545
#expect(#form(acceptCharset: ["utf-8"], autocomplete: .on) == "<form accept-charset=\"utf-8\" autocomplete=\"on\"></form>")
4646
}
47-
@Test func test_element_iframe() {
47+
@Test func element_iframe() {
4848
#expect(#iframe(sandbox: [.allowDownloads, .allowForms]) == "<iframe sandbox=\"allow-downloads allow-forms\"></iframe>")
4949
}
50-
@Test func test_element_input() {
50+
@Test func element_input() {
5151
#expect(#input(autocomplete: ["email", "password"], type: .text) == "<input autocomplete=\"email password\" type=\"text\">")
5252
#expect(#input(type: .password) == "<input type=\"password\">")
5353
#expect(#input(type: .datetimeLocal) == "<input type=\"datetime-local\">")
5454
}
55-
@Test func test_element_img() {
55+
@Test func element_img() {
5656
#expect(#img(sizes: ["(max-height: 500px) 1000px", "(min-height: 25rem)"], srcset: ["https://paradigm-app.com", "https://litleagues.com"]) == "<img sizes=\"(max-height: 500px) 1000px,(min-height: 25rem)\" srcset=\"https://paradigm-app.com,https://litleagues.com\">")
5757
}
5858
@Test func test_link() {
5959
#expect(#link(as: .document, imagesizes: ["lmno", "p"]) == "<link as=\"document\" imagesizes=\"lmno,p\">")
6060
}
61-
@Test func test_element_ol() {
61+
@Test func element_ol() {
6262
#expect(#ol() == "<ol></ol>")
6363
#expect(#ol(type: .a) == "<ol type=\"a\"></ol>")
6464
#expect(#ol(type: .A) == "<ol type=\"A\"></ol>")
6565
#expect(#ol(type: .i) == "<ol type=\"i\"></ol>")
6666
#expect(#ol(type: .I) == "<ol type=\"I\"></ol>")
6767
#expect(#ol(type: .one) == "<ol type=\"1\"></ol>")
6868
}
69-
@Test func test_element_script() {
69+
@Test func element_script() {
7070
#expect(#script() == "<script></script>")
7171
#expect(#script(type: .importmap) == "<script type=\"importmap\"></script>")
7272
#expect(#script(type: .module) == "<script type=\"module\"></script>")
7373
#expect(#script(type: .speculationrules) == "<script type=\"speculationrules\"></script>")
7474
}
75-
@Test func test_element_text_area() {
75+
@Test func element_text_area() {
7676
#expect(#textarea(autocomplete: ["email", "password"], rows: 5) == "<textarea autocomplete=\"email password\" rows=\"5\"></textarea>")
7777
}
78-
@Test func test_element_video() {
78+
@Test func element_video() {
7979
#expect(#video(controlslist: [.nodownload, .nofullscreen, .noremoteplayback]) == "<video controlslist=\"nodownload nofullscreen noremoteplayback\"></video>")
8080
}
81+
82+
@Test func element_custom() {
83+
var bro:String = #custom(tag: "bro", isVoid: false)
84+
#expect(bro == "<bro></bro>")
85+
86+
bro = #custom(tag: "bro", isVoid: true)
87+
#expect(bro == "<bro>")
88+
}
89+
90+
@Test func element_events() {
91+
#expect(#div(attributes: [.event(.click, "doThing()"), .event(.change, "doAnotherThing()")], []) == "<div onclick=\"doThing()\" onchange=\"doAnotherThing()\"></div>")
92+
}
93+
94+
@Test func elements_void() {
95+
let string:StaticString = #area([#base(), #br(), #col(), #embed(), #hr(), #img(), #input(), #link(), #meta(), #source(), #track(), #wbr()])
96+
#expect(string == "<area><base><br><col><embed><hr><img><input><link><meta><source><track><wbr>")
97+
}
8198
}
8299

83100
extension HTMLKitTests {
84-
@Test func test_recursive() {
101+
@Test func recursive_elements() {
85102
let string:StaticString = #div([
86103
#div(),
87104
#div([#div(), #div(), #div()]),
88105
#div()
89106
])
90107
#expect(string == "<div><div></div><div><div></div><div></div><div></div></div><div></div></div>")
91108
}
92-
/*@Test func test_same_attribute_multiple_times() {
109+
/*@Test func same_attribute_multiple_times() {
93110
let string:StaticString = #div(attributes: [.id("1"), .id("2"), .id("3"), .id("4")])
94111
#expect(string == "<div id=\"1\"></div>")
95112
}*/
96-
@Test func test_attribute_hidden() {
97-
#expect(#div(attributes: [.hidden(.true)]) == "<div hidden></div>")
98-
#expect(#div(attributes: [.hidden(.untilFound)]) == "<div hidden=\"until-found\"></div>")
99-
}
100113

101-
@Test func test_void() {
102-
let string:StaticString = #area([#base(), #br(), #col(), #embed(), #hr(), #img(), #input(), #link(), #meta(), #source(), #track(), #wbr()])
103-
#expect(string == "<area><base><br><col><embed><hr><img><input><link><meta><source><track><wbr>")
114+
@Test func no_value_type() {
115+
let test1 = #html([#body([#h1(["HTMLKitTests"])])])
116+
let test2 = #html([#body([#h1([StaticString("HTMLKitTests")])])])
104117
}
105-
@Test func test_multiline() {
118+
119+
@Test func multiline_value_type() {
106120
/*#expect(#script(["""
107121
bro
108122
"""
109123
]) == "<script>bro</script>")*/
110124
}
111-
@Test func test_events() {
112-
#expect(#div(attributes: [.event(.click, "doThing()"), .event(.change, "doAnotherThing()")], []) == "<div onclick=\"doThing()\" onchange=\"doAnotherThing()\"></div>")
113-
}
114125
}
115126

116127
extension HTMLKitTests {
117-
@Test func test_attribute_data() {
128+
@Test func attribute_data() {
118129
#expect(#div(attributes: [.data("id", "5")]) == "<div data-id=\"5\"></div>")
119130
}
131+
@Test func attribute_hidden() {
132+
#expect(#div(attributes: [.hidden(.true)]) == "<div hidden></div>")
133+
#expect(#div(attributes: [.hidden(.untilFound)]) == "<div hidden=\"until-found\"></div>")
134+
}
135+
136+
@Test func attribute_custom() {
137+
#expect(#div(attributes: [.custom("potofgold", "north")]) == "<div potofgold=\"north\"></div>")
138+
#expect(#div(attributes: [.custom("potofgold", "\(1)")]) == "<div potofgold=\"1\"></div>")
139+
#expect(#div(attributes: [.custom("potofgold1", "\(1)"), .custom("potofgold2", "2")]) == "<div potofgold1=\"1\" potofgold2=\"2\"></div>")
140+
//#expect(#div(attributes: [.custom("potof gold1", "\(1)"), .custom("potof gold2", "2")]) == "<div potofgold1=\"1\" potofgold2=\"2\"></div>")
141+
}
120142
}
121143

122144
extension HTMLKitTests {
123145
enum Shrek : String {
124146
case isLove, isLife
125147
}
126-
@Test func test_third_party_enum() {
127-
#expect(#div(attributes: [.title(Shrek.isLove.rawValue)]) == "<div title=\"isLove\"></div>")
128-
#expect(#div(attributes: [.title("\(Shrek.isLife)")]) == "<div title=\"isLife\"></div>")
148+
@Test func third_party_enum() {
149+
#expect(#a(attributes: [.title(Shrek.isLove.rawValue)]) == "<a title=\"isLove\"></a>")
150+
#expect(#a(attributes: [.title("\(Shrek.isLife)")]) == "<a title=\"isLife\"></a>")
129151
}
130152
}
131153

@@ -140,7 +162,7 @@ extension HTMLKitTests {
140162
}
141163
}
142164

143-
@Test func test_third_party_literal() {
165+
@Test func third_party_literal() {
144166
var string:String = #div(attributes: [.title(HTMLKitTests.spongebob)])
145167
#expect(string == "<div title=\"Spongebob Squarepants\"></div>")
146168

@@ -151,13 +173,13 @@ extension HTMLKitTests {
151173
let static_string:StaticString = #div(attributes: [.title(mr_crabs)])
152174
#expect(static_string == "<div title=\"Mr. Crabs\"></div>")*/
153175
}
154-
@Test func test_third_party_func() {
155-
#expect(#div(attributes: [.title(HTMLKitTests.spongebobCharacter("patrick"))]) == "<div title=\"Patrick Star\"></div>")
176+
@Test func third_party_func() {
177+
//#expect(#div(attributes: [.title(HTMLKitTests.spongebobCharacter("patrick"))]) == "<div title=\"Patrick Star\"></div>")
156178
}
157179
}
158180

159181
extension HTMLKitTests {
160-
@Test func test_example_1() {
182+
@Test func example_1() {
161183
let test:StaticString = #html([
162184
#body([
163185
#div(
@@ -189,7 +211,7 @@ extension HTMLKitTests {
189211
}
190212

191213
extension HTMLKitTests {
192-
@Test func testExample2() {
214+
@Test func example2() {
193215
var test:TestStruct = TestStruct(name: "one", array: ["1", "2", "3"])
194216
#expect(test.html == "<p>one123</p>")
195217

0 commit comments

Comments
 (0)