Skip to content

Commit 21ca726

Browse files
micro-optimizations and...
- throw compiler error if runtime values are detected - fixed empty array still populating attribute value
1 parent 4e7b80e commit 21ca726

File tree

2 files changed

+76
-72
lines changed

2 files changed

+76
-72
lines changed

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 68 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -88,80 +88,82 @@ private extension HTMLElement {
8888
isVoid = elementType.isVoid
8989
children = childs.prefix(childs.count)
9090
}
91-
let data:ElementData = parse_arguments(context: context, elementType: elementType, children: children)
92-
var string:String = (tag == "html" ? "<!DOCTYPE html>" : "") + "<" + tag + data.attributes + ">" + data.innerHTML
91+
let (attributes, innerHTML):(String, String) = parse_arguments(context: context, elementType: elementType, children: children)
92+
var string:String = (tag == "html" ? "<!DOCTYPE html>" : "") + "<" + tag + attributes + ">" + innerHTML
9393
if !isVoid {
9494
string += "</" + tag + ">"
9595
}
9696
return string
9797
}
9898
// MARK: Parse Arguments
99-
static func parse_arguments(context: some MacroExpansionContext, elementType: HTMLElementType, children: Slice<SyntaxChildren>) -> ElementData {
100-
var attributes:[String] = [], innerHTML:[String] = []
99+
static func parse_arguments(context: some MacroExpansionContext, elementType: HTMLElementType, children: Slice<SyntaxChildren>) -> (attributes: String, innerHTML: String) {
100+
var attributes:String = " ", innerHTML:String = ""
101101
for element in children {
102102
if let child:LabeledExprSyntax = element.labeled {
103103
if var key:String = child.label?.text {
104-
if key == "dataType" { // HTMLDataRepresentation; we don't care
105-
} else if key == "attributes" {
106-
attributes.append(contentsOf: parse_global_attributes(context: context, elementType: elementType, array: child.expression.array!))
104+
if key == "attributes" {
105+
for attribute in parse_global_attributes(context: context, elementType: elementType, array: child.expression.array!.elements) {
106+
attributes += attribute + " "
107+
}
107108
} else {
108109
if key == "acceptCharset" {
109110
key = "accept-charset"
110111
}
111-
if let string:String = parse_attribute(context: context, elementType: elementType, key: key, argument: child) {
112-
attributes.append(key + (string.isEmpty ? "" : "=\\\"" + string + "\\\""))
112+
if let string:String = parse_attribute(context: context, elementType: elementType, key: key, expression: child.expression) {
113+
attributes += key + (string.isEmpty ? "" : "=\\\"" + string + "\\\"") + " "
113114
}
114115
}
115116
// inner html
116117
} else if let inner_html:String = parse_inner_html(context: context, elementType: elementType, child: child) {
117-
innerHTML.append(inner_html)
118+
innerHTML += inner_html
118119
}
119120
}
120121
}
121-
return ElementData(attributes: attributes, innerHTML: innerHTML)
122+
attributes.removeLast()
123+
return (attributes, innerHTML)
122124
}
123125
// MARK: Parse Global Attributes
124-
static func parse_global_attributes(context: some MacroExpansionContext, elementType: HTMLElementType, array: ArrayExprSyntax) -> [String] {
126+
static func parse_global_attributes(context: some MacroExpansionContext, elementType: HTMLElementType, array: ArrayElementListSyntax) -> [String] {
125127
var keys:Set<String> = [], attributes:[String] = []
126-
for element in array.elements {
127-
let function:FunctionCallExprSyntax = element.expression.as(FunctionCallExprSyntax.self)!, key_argument:LabeledExprSyntax = function.arguments.first!, key_element:ExprSyntax = key_argument.expression
128-
var key:String = function.calledExpression.memberAccess!.declName.baseName.text, value:String? = nil
128+
for element in array {
129+
let function:FunctionCallExprSyntax = element.expression.functionCall!, first_expression:ExprSyntax = function.arguments.first!.expression
130+
var key:String = function.calledExpression.memberAccess!.declName.baseName.text, value:String! = nil
129131
switch key {
130132
case "custom", "data":
131-
var (literalValue, returnType):(String, LiteralReturnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: function.arguments.last!.expression)!
133+
var returnType:LiteralReturnType = .string
134+
(value, returnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: function.arguments.last!.expression)!
132135
if returnType == .string {
133-
literalValue.escapeHTML(escapeAttributes: true)
136+
value.escapeHTML(escapeAttributes: true)
134137
}
135-
value = literalValue
136138
if key == "custom" {
137-
key = key_element.stringLiteral!.string
139+
key = first_expression.stringLiteral!.string
138140
} else {
139-
key += "-\(key_element.stringLiteral!.string)"
141+
key += "-\(first_expression.stringLiteral!.string)"
140142
}
141143
break
142144
case "event":
143-
key = "on" + key_element.memberAccess!.declName.baseName.text
144-
if var result:(String, LiteralReturnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: function.arguments.last!.expression) {
145-
if result.1 == .string {
146-
result.0.escapeHTML(escapeAttributes: true)
145+
key = "on" + first_expression.memberAccess!.declName.baseName.text
146+
if var (string, returnType):(String, LiteralReturnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: function.arguments.last!.expression) {
147+
if returnType == .string {
148+
string.escapeHTML(escapeAttributes: true)
147149
}
148-
value = result.0
150+
value = string
149151
} else {
150152
unallowed_expression(context: context, node: function.arguments.last!)
151153
return []
152154
}
153155
break
154156
default:
155-
if let string:String = parse_attribute(context: context, elementType: elementType, key: key, argument: key_argument) {
157+
if let string:String = parse_attribute(context: context, elementType: elementType, key: key, expression: first_expression) {
156158
value = string
157159
}
158160
break
159161
}
160162
if key.contains(" ") {
161-
context.diagnose(Diagnostic(node: key_element, message: DiagnosticMsg(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
163+
context.diagnose(Diagnostic(node: first_expression, message: DiagnosticMsg(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
162164
} else if let value:String = value {
163165
if keys.contains(key) {
164-
context.diagnose(Diagnostic(node: key_element, message: DiagnosticMsg(id: "globalAttributeAlreadyDefined", message: "Global attribute \"" + key + "\" is already defined.")))
166+
context.diagnose(Diagnostic(node: first_expression, message: DiagnosticMsg(id: "globalAttributeAlreadyDefined", message: "Global attribute \"" + key + "\" is already defined.")))
165167
} else {
166168
attributes.append(key + (value.isEmpty ? "" : "=\\\"" + value + "\\\""))
167169
keys.insert(key)
@@ -196,15 +198,6 @@ private extension HTMLElement {
196198
])
197199
]))
198200
}
199-
200-
struct ElementData {
201-
let attributes:String, innerHTML:String
202-
203-
init(attributes: [String], innerHTML: [String]) {
204-
self.attributes = attributes.isEmpty ? "" : " " + attributes.joined(separator: " ")
205-
self.innerHTML = innerHTML.joined()
206-
}
207-
}
208201

209202
static func enumName(elementType: HTMLElementType, key: String) -> String {
210203
switch elementType.rawValue + key {
@@ -217,24 +210,22 @@ private extension HTMLElement {
217210
}
218211

219212
// MARK: Parse Attribute
220-
static func parse_attribute(context: some MacroExpansionContext, elementType: HTMLElementType, key: String, argument: LabeledExprSyntax) -> String? {
221-
let expression:ExprSyntax = argument.expression
222-
if var result:(String, LiteralReturnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: expression) {
223-
switch result.1 {
224-
case .boolean: return result.0.elementsEqual("true") ? "" : nil
225-
case .string:
226-
result.0.escapeHTML(escapeAttributes: true)
227-
return result.0
228-
case .interpolation: return result.0
213+
static func parse_attribute(context: some MacroExpansionContext, elementType: HTMLElementType, key: String, expression: ExprSyntax) -> String? {
214+
if var (string, returnType):(String, LiteralReturnType) = parse_literal_value(context: context, elementType: elementType, key: key, expression: expression) {
215+
switch returnType {
216+
case .boolean: return string.elementsEqual("true") ? "" : nil
217+
case .string, .enumCase:
218+
if returnType == .string && string.isEmpty {
219+
return nil
220+
}
221+
string.escapeHTML(escapeAttributes: true)
222+
return string
223+
case .interpolation: return string
229224
}
230225
}
231-
func member(_ value: String) -> String {
232-
var string:String = String(value[value.index(after: value.startIndex)...])
233-
string = HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: string)
234-
return string
235-
}
236-
if let function:FunctionCallExprSyntax = expression.as(FunctionCallExprSyntax.self) {
237-
return member("\(function)")
226+
if let function:FunctionCallExprSyntax = expression.functionCall {
227+
let string:String = "\(function)"
228+
return HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: String(string[string.index(after: string.startIndex)...]))
238229
}
239230
return nil
240231
}
@@ -247,13 +238,13 @@ private extension HTMLElement {
247238
if let string:String = expression.stringLiteral?.string ?? expression.integerLiteral?.literal.text ?? expression.floatLiteral?.literal.text {
248239
return (string, .string)
249240
}
250-
if let function:FunctionCallExprSyntax = expression.as(FunctionCallExprSyntax.self) {
241+
if let function:FunctionCallExprSyntax = expression.functionCall {
251242
switch key {
252243
case "height", "width":
253244
var value:String = "\(function)"
254245
value = String(value[value.index(after: value.startIndex)...])
255246
value = HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: value)
256-
return (value, .string)
247+
return (value, .enumCase)
257248
default:
258249
if function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text == "StaticString" {
259250
return (function.arguments.first!.expression.stringLiteral!.string, .string)
@@ -262,15 +253,13 @@ private extension HTMLElement {
262253
}
263254
}
264255
if let member:MemberAccessExprSyntax = expression.memberAccess {
265-
let decl:String = member.declName.baseName.text
266256
if let _:ExprSyntax = member.base {
267257
return ("\(member)", .interpolation)
268-
} else {
269-
return (HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: decl), .string)
270258
}
259+
return (HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: member.declName.baseName.text), .enumCase)
271260
}
272261
if let array:ArrayExprSyntax = expression.array {
273-
let separator:String
262+
let separator:Character, separator_string:String
274263
switch key {
275264
case "accept", "coords", "exportparts", "imagesizes", "imagesrcset", "sizes", "srcset":
276265
separator = ","
@@ -279,22 +268,30 @@ private extension HTMLElement {
279268
separator = " "
280269
break
281270
}
282-
return (array.elements.compactMap({
283-
if let string:String = $0.expression.stringLiteral?.string {
271+
separator_string = String(separator)
272+
var result:String = ""
273+
for element in array.elements {
274+
if let string:String = element.expression.stringLiteral?.string {
284275
if string.contains(separator) {
285-
context.diagnose(Diagnostic(node: $0.expression, message: DiagnosticMsg(id: "characterNotAllowedInDeclaration", message: "Character \"" + separator + "\" is not allowed when declaring values for \"" + key + "\".")))
276+
context.diagnose(Diagnostic(node: element.expression, message: DiagnosticMsg(id: "characterNotAllowedInDeclaration", message: "Character \"\(separator)\" is not allowed when declaring values for \"" + key + "\".")))
286277
return nil
287278
}
288-
return string
279+
result += string + separator_string
289280
}
290-
if let string:String = $0.expression.integerLiteral?.literal.text ?? $0.expression.floatLiteral?.literal.text {
291-
return string
281+
if let string:String = element.expression.integerLiteral?.literal.text ?? element.expression.floatLiteral?.literal.text {
282+
result += string + separator_string
292283
}
293-
if let string:String = $0.expression.memberAccess?.declName.baseName.text {
294-
return HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: string)
284+
if let string:String = element.expression.memberAccess?.declName.baseName.text {
285+
result += HTMLElementAttribute.Extra.htmlValue(enumName: enumName(elementType: elementType, key: key), for: string) + separator_string
295286
}
296-
return nil
297-
}).joined(separator: separator), .string)
287+
}
288+
if !result.isEmpty {
289+
result.removeLast()
290+
}
291+
return (result, .string)
292+
}
293+
if let _:DeclReferenceExprSyntax = expression.as(DeclReferenceExprSyntax.self) {
294+
context.diagnose(Diagnostic(node: expression, message: DiagnosticMsg(id: "runtimeValueNotAllowed", message: "Runtime value not allowed here.")))
298295
}
299296
return nil
300297
}
@@ -362,7 +359,7 @@ private extension HTMLElement {
362359
}
363360

364361
enum LiteralReturnType {
365-
case boolean, string, interpolation
362+
case boolean, string, enumCase, interpolation
366363
}
367364

368365
// MARK: HTMLElementType
@@ -530,6 +527,7 @@ extension SyntaxProtocol {
530527
var array : ArrayExprSyntax? { self.as(ArrayExprSyntax.self) }
531528
var memberAccess : MemberAccessExprSyntax? { self.as(MemberAccessExprSyntax.self) }
532529
var macroExpansion : MacroExpansionExprSyntax? { self.as(MacroExpansionExprSyntax.self) }
530+
var functionCall : FunctionCallExprSyntax? { self.as(FunctionCallExprSyntax.self) }
533531
}
534532
extension SyntaxChildren.Element {
535533
var labeled : LabeledExprSyntax? { self.as(LabeledExprSyntax.self) }

Tests/HTMLKitTests/HTMLKitTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ extension HTMLKitTests {
9999
#expect(string == "<!DOCTYPE html><html xmlns=\"test\"></html>")
100100
}
101101
@Test func element_area() {
102-
let string:StaticString = #area(coords: [1, 2, 3])
102+
var string:StaticString = #area(coords: [1, 2, 3])
103103
#expect(string == "<area coords=\"1,2,3\">")
104+
105+
string = #area(coords: [])
106+
#expect(string == "<area>")
104107
}
105108
@Test func element_audio() {
106109
let string:StaticString = #audio(controlslist: .nodownload)
@@ -291,9 +294,12 @@ extension HTMLKitTests {
291294
rel: ["lets go"],
292295
sizes: ["lets,go"]
293296
)
294-
let bro:String = "yup"
297+
var bro:String = "yup"
295298
let _:String = #a(bro)
296299
let _:String = #div(attributes: [.custom("potof gold1", "\(1)"), .custom("potof gold2", "2")])
300+
301+
let test:[Int] = [1]
302+
bro = #area(coords: test)
297303
}*/
298304
}
299305

0 commit comments

Comments
 (0)