Skip to content

Commit 2095b96

Browse files
now parses element attributes correctly (with 1 exception), and...
- macro expansion now escapes html where applicable - fixed two unit tests in `HTMLKitTests.swift` - macro compilation should be slightly faster (than v0.9.0) due to how parsing of extra attributes works
1 parent ebbb515 commit 2095b96

File tree

9 files changed

+203
-81
lines changed

9 files changed

+203
-81
lines changed

Sources/HTMLKitUtilities/HTMLElementAttribute.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public enum HTMLElementAttribute : Hashable {
7575
guard let s:String = expression.integerLiteral?.literal.text else { return nil }
7676
return Int(s)
7777
}
78-
func array_string() -> [String] { expression.array!.elements.map({ $0.expression.stringLiteral!.string }) }
78+
func array_string() -> [String] { expression.array?.elements.compactMap({ $0.expression.stringLiteral?.string }) ?? [] }
7979
func float() -> Float? {
8080
guard let s:String = expression.floatLiteral?.literal.text else { return nil }
8181
return Float(s)

Sources/HTMLKitUtilities/HTMLElementAttributeExtra.swift

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,71 @@ public extension HTMLInitializable where Self: RawRepresentable, RawValue == Str
2727
// MARK: HTMLElementAttribute.Extra
2828
extension HTMLElementAttribute {
2929
public enum Extra {
30+
31+
public static func parse(key: String, expr: ExprSyntax) -> (any HTMLInitializable)? {
32+
func get<T : HTMLInitializable>(_ type: T.Type) -> T? {
33+
let inner_key:String, arguments:LabeledExprListSyntax
34+
if let function:FunctionCallExprSyntax = expr.functionCall {
35+
inner_key = function.calledExpression.memberAccess!.declName.baseName.text
36+
arguments = function.arguments
37+
} else if let member:MemberAccessExprSyntax = expr.memberAccess {
38+
inner_key = member.declName.baseName.text
39+
arguments = LabeledExprListSyntax()
40+
} else {
41+
return nil
42+
}
43+
return T(key: inner_key, arguments: arguments)
44+
}
45+
switch key {
46+
case "as": return get(`as`.self)
47+
case "autocapitalize": return get(autocapitalize.self)
48+
case "autocomplete": return get(autocomplete.self)
49+
case "autocorrect": return get(autocorrect.self)
50+
case "blocking": return get(blocking.self)
51+
case "buttontype": return get(buttontype.self)
52+
case "capture": return get(capture.self)
53+
case "command": return get(command.self)
54+
case "contenteditable": return get(contenteditable.self)
55+
case "controlslist": return get(controlslist.self)
56+
case "crossorigin": return get(crossorigin.self)
57+
case "decoding": return get(decoding.self)
58+
case "dir": return get(dir.self)
59+
case "dirname": return get(dirname.self)
60+
case "draggable": return get(draggable.self)
61+
case "download": return get(download.self)
62+
case "enterkeyhint": return get(enterkeyhint.self)
63+
case "event": return get(event.self)
64+
case "fetchpriority": return get(fetchpriority.self)
65+
case "formenctype": return get(formenctype.self)
66+
case "formmethod": return get(formmethod.self)
67+
case "formtarget": return get(formtarget.self)
68+
case "hidden": return get(hidden.self)
69+
case "httpEquiv": return get(httpequiv.self)
70+
case "inputmode": return get(inputmode.self)
71+
case "inputtype": return get(inputtype.self)
72+
case "kind": return get(kind.self)
73+
case "loading": return get(loading.self)
74+
case "numberingtype": return get(numberingtype.self)
75+
case "popover": return get(popover.self)
76+
case "popovertargetaction": return get(popovertargetaction.self)
77+
case "preload": return get(preload.self)
78+
case "referrerpolicy": return get(referrerpolicy.self)
79+
case "rel": return get(rel.self)
80+
case "sandbox": return get(sandbox.self)
81+
case "scripttype": return get(scripttype.self)
82+
case "scope": return get(scope.self)
83+
case "shadowrootmode": return get(shadowrootmode.self)
84+
case "shadowrootclonable": return get(shadowrootclonable.self)
85+
case "shape": return get(shape.self)
86+
case "spellcheck": return get(spellcheck.self)
87+
case "target": return get(target.self)
88+
case "translate": return get(translate.self)
89+
case "virtualkeyboardpolicy": return get(virtualkeyboardpolicy.self)
90+
case "wrap": return get(wrap.self)
91+
case "writingsuggestions": return get(writingsuggestions.self)
92+
default: return nil
93+
}
94+
}
3095
}
3196
}
3297
public extension HTMLElementAttribute.Extra {
@@ -124,7 +189,7 @@ public extension HTMLElementAttribute.Extra {
124189
guard let s:String = expression.integerLiteral?.literal.text else { return nil }
125190
return Int(s)
126191
}
127-
func array_string() -> [String] { expression.array!.elements.map({ $0.expression.stringLiteral!.string }) }
192+
func array_string() -> [String] { expression.array?.elements.compactMap({ $0.expression.stringLiteral?.string }) ?? [] }
128193
func float() -> Float? {
129194
guard let s:String = expression.floatLiteral?.literal.text else { return nil }
130195
return Float(s)

Sources/HTMLKitUtilities/HTMLEncoding.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public enum HTMLEncoding {
5555
/// Use `$0` for the compiled HTML.
5656
/// - Parameters:
5757
/// - logic: The encoding logic, represented as a string.
58+
/// - Warning: The custom type needs to conform to `CustomStringConvertible` to be returned by the html macro!
5859
/// ### Example Usage
5960
/// ```swift
6061
/// let _:String = #html(encoding: .custom(#"String("$0")"#), p(5)) // String("<p>5</p>")

Sources/HTMLKitUtilities/HTMLKitUtilities.swift

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Created by Evan Anderson on 9/19/24.
66
//
77

8+
import SwiftSyntax
9+
810
// MARK: HTMLKitUtilities
911
public enum HTMLKitUtilities {
1012
public struct ElementData {
@@ -68,40 +70,87 @@ public extension String {
6870

6971
// MARK: CSSUnit
7072
public extension HTMLElementAttribute {
71-
enum CSSUnit { // https://www.w3schools.com/cssref/css_units.php
73+
enum CSSUnit : HTMLInitializable { // https://www.w3schools.com/cssref/css_units.php
7274
// absolute
73-
case centimeters(_ value: Float)
74-
case millimeters(_ value: Float)
75+
case centimeters(_ value: Float?)
76+
case millimeters(_ value: Float?)
7577
/// 1 inch = 96px = 2.54cm
76-
case inches(_ value: Float)
78+
case inches(_ value: Float?)
7779
/// 1 pixel = 1/96th of 1inch
78-
case pixels(_ value: Float)
80+
case pixels(_ value: Float?)
7981
/// 1 point = 1/72 of 1inch
80-
case points(_ value: Float)
82+
case points(_ value: Float?)
8183
/// 1 pica = 12 points
82-
case picas(_ value: Float)
84+
case picas(_ value: Float?)
8385

8486
// relative
8587
/// Relative to the font-size of the element (2em means 2 times the size of the current font)
86-
case em(_ value: Float)
88+
case em(_ value: Float?)
8789
/// Relative to the x-height of the current font (rarely used)
88-
case ex(_ value: Float)
90+
case ex(_ value: Float?)
8991
/// Relative to the width of the "0" (zero)
90-
case ch(_ value: Float)
92+
case ch(_ value: Float?)
9193
/// Relative to font-size of the root element
92-
case rem(_ value: Float)
94+
case rem(_ value: Float?)
9395
/// Relative to 1% of the width of the viewport
94-
case viewportWidth(_ value: Float)
96+
case viewportWidth(_ value: Float?)
9597
/// Relative to 1% of the height of the viewport
96-
case viewportHeight(_ value: Float)
98+
case viewportHeight(_ value: Float?)
9799
/// Relative to 1% of viewport's smaller dimension
98-
case viewportMin(_ value: Float)
100+
case viewportMin(_ value: Float?)
99101
/// Relative to 1% of viewport's larger dimension
100-
case viewportMax(_ value: Float)
102+
case viewportMax(_ value: Float?)
101103
/// Relative to the parent element
102-
case percent(_ value: Float)
104+
case percent(_ value: Float?)
105+
106+
public init?(key: String, arguments: LabeledExprListSyntax) {
107+
func float() -> Float? {
108+
guard let s:String = arguments.first!.expression.floatLiteral?.literal.text else { return nil }
109+
return Float(s)
110+
}
111+
switch key {
112+
case "centimeters": self = .centimeters(float())
113+
case "millimeters": self = .millimeters(float())
114+
case "inches": self = .inches(float())
115+
case "pixels": self = .pixels(float())
116+
case "points": self = .points(float())
117+
case "picas": self = .picas(float())
118+
119+
case "em": self = .em(float())
120+
case "ex": self = .ex(float())
121+
case "ch": self = .ch(float())
122+
case "rem": self = .rem(float())
123+
case "viewportWidth": self = .viewportWidth(float())
124+
case "viewportHeight": self = .viewportHeight(float())
125+
case "viewportMin": self = .viewportMin(float())
126+
case "viewportMax": self = .viewportMax(float())
127+
case "percent": self = .percent(float())
128+
default: return nil
129+
}
130+
}
103131

104-
public var htmlValue : String {
132+
public var key : String {
133+
switch self {
134+
case .centimeters(_): return "centimeters"
135+
case .millimeters(_): return "millimeters"
136+
case .inches(_): return "inches"
137+
case .pixels(_): return "pixels"
138+
case .points(_): return "points"
139+
case .picas(_): return "picas"
140+
141+
case .em(_): return "em"
142+
case .ex(_): return "ex"
143+
case .ch(_): return "ch"
144+
case .rem(_): return "rem"
145+
case .viewportWidth(_): return "viewportWidth"
146+
case .viewportHeight(_): return "viewportHeight"
147+
case .viewportMin(_): return "viewportMin"
148+
case .viewportMax(_): return "viewportMax"
149+
case .percent(_): return "percent"
150+
}
151+
}
152+
153+
public var htmlValue : String? {
105154
switch self {
106155
case .centimeters(let v),
107156
.millimeters(let v),
@@ -147,12 +196,13 @@ public extension HTMLElementAttribute {
147196
}
148197

149198
// MARK: LiteralReturnType
150-
public indirect enum LiteralReturnType {
199+
public enum LiteralReturnType {
151200
case boolean(Bool)
152201
case string(String)
153-
case enumCase(String)
202+
case int(Int)
203+
case float(Float)
154204
case interpolation(String)
155-
case array(of: LiteralReturnType)
205+
case array([Any])
156206

157207
public var isInterpolation : Bool {
158208
switch self {
@@ -166,26 +216,24 @@ public indirect enum LiteralReturnType {
166216
default: return false
167217
}
168218
}
169-
public var isEnumCase : Bool {
170-
switch self {
171-
case .enumCase(_): return true
172-
default: return false
173-
}
174-
}
175219

176220
public func value(key: String) -> String? {
177221
switch self {
178222
case .boolean(let b): return b ? key : nil
179-
case .string(var string), .enumCase(var string):
180-
if string.isEmpty && (isEnumCase || isString && key == "attributionsrc") {
223+
case .string(var string):
224+
if string.isEmpty && key == "attributionsrc" {
181225
return ""
182226
}
183227
string.escapeHTML(escapeAttributes: true)
184228
return string
229+
case .int(let int):
230+
return String(describing: int)
231+
case .float(let float):
232+
return String(describing: float)
185233
case .interpolation(let string):
186234
return string
187-
case .array(let value):
188-
return ""
235+
case .array(_):
236+
return nil
189237
}
190238
}
191239
}

Sources/HTMLKitUtilities/HTMX.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public extension HTMLElementAttribute {
6767
guard let s:String = expression.integerLiteral?.literal.text else { return nil }
6868
return Int(s)
6969
}
70-
func array_string() -> [String] { expression.array!.elements.map({ $0.expression.stringLiteral!.string }) }
70+
func array_string() -> [String] { expression.array?.elements.compactMap({ $0.expression.stringLiteral?.string }) ?? [] }
7171
func float() -> Float? {
7272
guard let s:String = expression.floatLiteral?.literal.text else { return nil }
7373
return Float(s)

Sources/HTMLKitUtilities/HTMXAttributes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public extension HTMLElementAttribute.HTMX {
123123

124124
public init?(key: String, arguments: LabeledExprListSyntax) {
125125
let expression:ExprSyntax = arguments.first!.expression
126-
func array_string() -> [String] { expression.array!.elements.map({ $0.expression.stringLiteral!.string }) }
126+
func array_string() -> [String] { expression.array?.elements.compactMap({ $0.expression.stringLiteral?.string }) ?? [] }
127127
switch key {
128128
case "all": self = .all
129129
case "none": self = .none

Sources/HTMLKitUtilities/ParseData.swift

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@ public extension HTMLKitUtilities {
4444
} else {
4545
if key == "acceptCharset" {
4646
key = "accept-charset"
47-
} else if key == "httpEquiv" {
48-
key = "http-equiv"
4947
}
50-
if let string:String = parse_literal_value(context: context, key: key, expression: child.expression, lookupFiles: lookupFiles)?.value(key: key) {
51-
attributes[key] = string
48+
if let test:any HTMLInitializable = HTMLElementAttribute.Extra.parse(key: key, expr: child.expression) {
49+
attributes[key] = test
50+
} else if let string:LiteralReturnType = parse_literal_value(context: context, key: key, expression: child.expression, lookupFiles: lookupFiles) {
51+
switch string {
52+
case .boolean(let b): attributes[key] = b
53+
case .string(let s), .interpolation(let s): attributes[key] = s
54+
case .int(let i): attributes[key] = i
55+
case .float(let f): attributes[key] = f
56+
case .array(let values): attributes[key] = values
57+
}
5258
}
5359
}
5460
// inner html
@@ -140,8 +146,11 @@ public extension HTMLKitUtilities {
140146
if let boolean:String = expression.booleanLiteral?.literal.text {
141147
return .boolean(boolean == "true")
142148
}
143-
if let string:String = expression.integerLiteral?.literal.text ?? expression.floatLiteral?.literal.text {
144-
return .string(string)
149+
if let string:String = expression.integerLiteral?.literal.text {
150+
return .int(Int(string)!)
151+
}
152+
if let string:String = expression.floatLiteral?.literal.text {
153+
return .float(Float(string)!)
145154
}
146155
guard var returnType:LiteralReturnType = extract_literal(context: context, key: key, expression: expression, lookupFiles: lookupFiles) else {
147156
//context.diagnose(Diagnostic(node: expression, message: DiagnosticMsg(id: "somethingWentWrong", message: "Something went wrong. (" + expression.debugDescription + ")", severity: .warning)))
@@ -183,22 +192,17 @@ public extension HTMLKitUtilities {
183192
return stringLiteral.segments.count(where: { $0.is(ExpressionSegmentSyntax.self) }) == 0 ? .string(string) : .interpolation(string)
184193
}
185194
if let function:FunctionCallExprSyntax = expression.functionCall {
186-
let enums:Set<String> = ["command", "download", "height", "width"]
187-
if enums.contains(key) || key.hasPrefix("aria-") {
188-
return .enumCase("")
189-
} else {
190-
if let decl:String = function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text {
191-
switch decl {
192-
case "StaticString":
193-
var string:String = function.arguments.first!.expression.stringLiteral!.string
194-
return .string(string)
195-
default:
196-
if let element:HTMLElement = HTMLElementValueType.parse_element(context: context, function) {
197-
let string:String = element.description
198-
return string.contains("\\(") ? .interpolation(string) : .string(string)
199-
}
200-
break
201-
}
195+
if let decl:String = function.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text {
196+
switch decl {
197+
case "StaticString":
198+
let string:String = function.arguments.first!.expression.stringLiteral!.string
199+
return .string(string)
200+
default:
201+
if let element:HTMLElement = HTMLElementValueType.parse_element(context: context, function) {
202+
let string:String = element.description
203+
return string.contains("\\(") ? .interpolation(string) : .string(string)
204+
}
205+
break
202206
}
203207
return .interpolation("\(function)")
204208
}
@@ -219,21 +223,23 @@ public extension HTMLKitUtilities {
219223
separator = " "
220224
break
221225
}
222-
var results:[String] = []
226+
var results:[Any] = []
223227
for element in array.elements {
224228
if let string:String = element.expression.stringLiteral?.string {
225229
if string.contains(separator) {
226230
context.diagnose(Diagnostic(node: element.expression, message: DiagnosticMsg(id: "characterNotAllowedInDeclaration", message: "Character \"\(separator)\" is not allowed when declaring values for \"" + key + "\".")))
227231
return nil
228232
}
229233
results.append(string)
230-
}
231-
if let string:String = element.expression.integerLiteral?.literal.text ?? element.expression.floatLiteral?.literal.text {
232-
results.append(string)
234+
} else if let string:String = element.expression.integerLiteral?.literal.text {
235+
results.append(Int(string)!)
236+
} else if let string:String = element.expression.floatLiteral?.literal.text {
237+
results.append(Float(string)!)
238+
} else if let attribute:any HTMLInitializable = HTMLElementAttribute.Extra.parse(key: key, expr: element.expression) {
239+
results.append(attribute)
233240
}
234241
}
235-
let result:String = results.joined(separator: separator)
236-
return .array(of: .string(result))
242+
return .array(results)
237243
}
238244
if let _:DeclReferenceExprSyntax = expression.as(DeclReferenceExprSyntax.self) {
239245
var string:String = "\(expression)", remaining_interpolation:Int = 1

0 commit comments

Comments
 (0)