Skip to content

Commit 747351e

Browse files
two bug fixes (one hazardous)
- fixed: expansion with an encoding other than `string` produces wrong result (it didn't account for how the string delimiter is represented when encoded via a byte representation) - fixed: attribute values, for attributes other than global, not being HTML escaped - also began a QOL feature: translating an HTML document into a macro representation
1 parent c9f9335 commit 747351e

17 files changed

+375
-205
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ DebugRender.*
1515
DebugXmlRender.*
1616
Elements.*
1717
Events.*
18+
EncodingTests.d
19+
EncodingTests.o
20+
EncodingTests.swiftdeps*
1821
HTMX.d
1922
HTMX.o
2023
HTMX.swiftdeps*

Sources/HTMLKitUtilities/HTMLElementValueType.swift

Lines changed: 116 additions & 116 deletions
Large diffs are not rendered by default.

Sources/HTMLKitUtilities/HTMLEncoding.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public enum HTMLEncoding {
6161
/// let _:String = #html(encoding: .custom(#"String("$0")"#), p(5)) // String("<p>5</p>")
6262
/// ```
6363
///
64-
case custom(_ logic: String)
64+
case custom(_ logic: String, stringDelimiter: String = "\\\"")
6565

6666
public init?(rawValue: String) {
6767
switch rawValue {
@@ -74,4 +74,15 @@ public enum HTMLEncoding {
7474
default: return nil
7575
}
7676
}
77+
78+
public var stringDelimiter : String {
79+
switch self {
80+
case .string:
81+
return "\\\""
82+
case .utf8Bytes, .utf16Bytes, .utf8CString, .foundationData, .byteBuffer:
83+
return "\""
84+
case .custom(_, let delimiter):
85+
return delimiter
86+
}
87+
}
7788
}

Sources/HTMLKitUtilities/LiteralElements.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,13 @@ public struct custom : HTMLElement {
154154
public var trailingSlash:Bool
155155
public var escaped:Bool = false
156156
public let tag:String
157+
private var encoding:HTMLEncoding = .string
157158
public var attributes:[HTMLElementAttribute]
158159
public var innerHTML:[CustomStringConvertible]
159160

160-
public init(context: some MacroExpansionContext, _ children: SyntaxChildren) {
161-
let data:HTMLKitUtilities.ElementData = HTMLKitUtilities.parseArguments(context: context, children: children)
161+
public init(_ context: some MacroExpansionContext, _ encoding: HTMLEncoding, _ children: SyntaxChildren) {
162+
self.encoding = encoding
163+
let data:HTMLKitUtilities.ElementData = HTMLKitUtilities.parseArguments(context: context, encoding: encoding, children: children)
162164
tag = data.attributes["tag"] as? String ?? ""
163165
isVoid = data.attributes["isVoid"] as? Bool ?? false
164166
trailingSlash = data.trailingSlash
@@ -180,8 +182,8 @@ public struct custom : HTMLElement {
180182

181183
public var description : String {
182184
let attributes_string:String = self.attributes.compactMap({
183-
guard let v:String = $0.htmlValue else { return nil }
184-
let delimiter:String = $0.htmlValueDelimiter
185+
guard let v:String = $0.htmlValue(encoding) else { return nil }
186+
let delimiter:String = $0.htmlValueDelimiter(encoding)
185187
return $0.key + ($0.htmlValueIsVoidable && v.isEmpty ? "" : "=\(delimiter)\(v)\(delimiter)")
186188
}).joined(separator: " ")
187189
let l:String, g:String

Sources/HTMLKitUtilities/ParseData.swift

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public extension HTMLKitUtilities {
1414
static func escapeHTML(expansion: MacroExpansionExprSyntax, context: some MacroExpansionContext) -> String {
1515
return expansion.arguments.children(viewMode: .all).compactMap({
1616
guard let child:LabeledExprSyntax = $0.labeled,
17-
var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, child: child, lookupFiles: []) else {
17+
// TODO: fix the below encoding?
18+
var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, encoding: .string, child: child, lookupFiles: []) else {
1819
return nil
1920
}
2021
if var element:HTMLElement = c as? HTMLElement {
@@ -59,18 +60,19 @@ public extension HTMLKitUtilities {
5960

6061
case .string:
6162
return "\"\(raw: string)\""
62-
case .custom(let encoded):
63+
case .custom(let encoded, _):
6364
return "\(raw: encoded.replacingOccurrences(of: "$0", with: string))"
6465
}
6566
}
6667

6768
// MARK: Parse Arguments
6869
static func parseArguments(
6970
context: some MacroExpansionContext,
71+
encoding: HTMLEncoding,
7072
children: SyntaxChildren,
7173
otherAttributes: [String:String] = [:]
7274
) -> ElementData {
73-
var encoding:HTMLEncoding = HTMLEncoding.string
75+
var encoding:HTMLEncoding = encoding
7476
var global_attributes:[HTMLElementAttribute] = []
7577
var attributes:[String:Any] = [:]
7678
var innerHTML:[CustomStringConvertible] = []
@@ -83,7 +85,12 @@ public extension HTMLKitUtilities {
8385
if let key:String = child.expression.memberAccess?.declName.baseName.text {
8486
encoding = HTMLEncoding(rawValue: key) ?? .string
8587
} else if let custom:FunctionCallExprSyntax = child.expression.functionCall {
86-
encoding = .custom(custom.arguments.first!.expression.stringLiteral!.string)
88+
let logic:String = custom.arguments.first!.expression.stringLiteral!.string
89+
if custom.arguments.count == 1 {
90+
encoding = .custom(logic)
91+
} else {
92+
encoding = .custom(logic, stringDelimiter: custom.arguments.last!.expression.stringLiteral!.string)
93+
}
8794
}
8895
} else if key == "lookupFiles" {
8996
lookupFiles = Set(child.expression.array!.elements.compactMap({ $0.expression.stringLiteral?.string }))
@@ -99,15 +106,20 @@ public extension HTMLKitUtilities {
99106
} else if let string:LiteralReturnType = parse_literal_value(context: context, key: key, expression: child.expression, lookupFiles: lookupFiles) {
100107
switch string {
101108
case .boolean(let b): attributes[key] = b
102-
case .string(let s), .interpolation(let s): attributes[key] = s
109+
case .string(_), .interpolation(_): attributes[key] = string.value(key: key)
103110
case .int(let i): attributes[key] = i
104111
case .float(let f): attributes[key] = f
105-
case .array(let a): attributes[key] = a
112+
case .array(_):
113+
let escaped:LiteralReturnType = string.escapeArray()
114+
switch escaped {
115+
case .array(let a): attributes[key] = a
116+
default: break
117+
}
106118
}
107119
}
108120
}
109121
// inner html
110-
} else if let inner_html:CustomStringConvertible = parseInnerHTML(context: context, child: child, lookupFiles: lookupFiles) {
122+
} else if let inner_html:CustomStringConvertible = parseInnerHTML(context: context, encoding: encoding, child: child, lookupFiles: lookupFiles) {
111123
innerHTML.append(inner_html)
112124
}
113125
}
@@ -132,7 +144,7 @@ public extension HTMLKitUtilities {
132144
context.diagnose(Diagnostic(node: first_expression, message: DiagnosticMsg(id: "spacesNotAllowedInAttributeDeclaration", message: "Spaces are not allowed in attribute declaration.")))
133145
} else if keys.contains(key) {
134146
global_attribute_already_defined(context: context, attribute: key, node: first_expression)
135-
} else if let attr:HTMLElementAttribute = HTMLElementAttribute.init(context: context, key: key, function) {
147+
} else if let attr:HTMLElementAttribute = HTMLElementAttribute(context: context, key: key, function) {
136148
attributes.append(attr)
137149
key = attr.key
138150
keys.insert(key)
@@ -152,6 +164,7 @@ public extension HTMLKitUtilities {
152164
// MARK: Parse Inner HTML
153165
static func parseInnerHTML(
154166
context: some MacroExpansionContext,
167+
encoding: HTMLEncoding,
155168
child: LabeledExprSyntax,
156169
lookupFiles: Set<String>
157170
) -> CustomStringConvertible? {
@@ -160,7 +173,7 @@ public extension HTMLKitUtilities {
160173
return escapeHTML(expansion: expansion, context: context)
161174
}
162175
return "" // TODO: fix?
163-
} else if let element:HTMLElement = parse_element(context: context, expr: child.expression) {
176+
} else if let element:HTMLElement = parse_element(context: context, encoding: encoding, expr: child.expression) {
164177
return element
165178
} else if let string:String = parse_literal_value(context: context, key: "", expression: child.expression, lookupFiles: lookupFiles)?.value(key: "") {
166179
return string
@@ -171,9 +184,9 @@ public extension HTMLKitUtilities {
171184
}
172185

173186
// MARK: Parse element
174-
static func parse_element(context: some MacroExpansionContext, expr: ExprSyntax) -> HTMLElement? {
187+
static func parse_element(context: some MacroExpansionContext, encoding: HTMLEncoding, expr: ExprSyntax) -> HTMLElement? {
175188
guard let function:FunctionCallExprSyntax = expr.functionCall else { return nil }
176-
return HTMLElementValueType.parse_element(context: context, function)
189+
return HTMLElementValueType.parse_element(context: context, encoding: encoding, function)
177190
}
178191
}
179192
extension HTMLKitUtilities {
@@ -217,7 +230,7 @@ extension HTMLKitUtilities {
217230
guard macro.macroName.text == "html" else {
218231
return ("\(macro)", .string)
219232
}
220-
let data:HTMLKitUtilities.ElementData = HTMLKitUtilities.parseArguments(context: context, children: macro.arguments.children(viewMode: .all))
233+
let data:HTMLKitUtilities.ElementData = HTMLKitUtilities.parseArguments(context: context, encoding: .string, children: macro.arguments.children(viewMode: .all))
221234
return (data.innerHTML.map({ String(describing: $0) }).joined(), data.encoding)
222235
}
223236
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// TranslateHTML.swift
3+
//
4+
//
5+
// Created by Evan Anderson on 11/27/24.
6+
//
7+
8+
#if canImport(Foundation)
9+
import Foundation
10+
11+
public enum TranslateHTML { // TODO: finish
12+
public static func translate(string: String) -> String {
13+
var result:String = ""
14+
result.reserveCapacity(string.count)
15+
let end_index:String.Index = string.endIndex
16+
var index:String.Index = string.startIndex
17+
while index < end_index {
18+
if string[index].isWhitespace { // skip
19+
index = string.index(after: index)
20+
} else if string[index] == "<" {
21+
var i:Int = 0, depth:Int = 1
22+
loop: while true {
23+
i += 1
24+
let char:Character = string[string.index(index, offsetBy: i)]
25+
switch char {
26+
case "<": depth += 1
27+
case ">":
28+
depth -= 1
29+
if depth == 0 {
30+
break loop
31+
}
32+
default:
33+
break
34+
}
35+
}
36+
37+
let end_index:String.Index = string.firstIndex(of: ">")!
38+
let input:String = String(string[index...])
39+
if let element:String = parse_element(input: input) {
40+
result += element
41+
index = string.index(index, offsetBy: element.count)
42+
}
43+
}
44+
}
45+
return "#html(\n" + result + "\n)"
46+
}
47+
}
48+
49+
extension TranslateHTML {
50+
/// input: "<[HTML ELEMENT TAG NAME] [attributes]>[innerHTML]"
51+
static func parse_element(input: String) -> String? {
52+
let tag_name_ends:String.Index = input.firstIndex(of: " ") ?? input.firstIndex(of: ">")!
53+
let tag_name:String = String(input[input.index(after: input.startIndex)..<tag_name_ends])
54+
if let type:HTMLElementType = HTMLElementType(rawValue: tag_name) {
55+
return type.rawValue + "(\n)"
56+
} else {
57+
let is_void:Bool = input.range(of: "</" + tag_name + ">") == nil
58+
return "custom(tag: \"\(tag_name)\", isVoid: \(is_void))"
59+
}
60+
}
61+
}
62+
#endif

Sources/HTMLKitUtilities/attributes/HTMLElementAttribute.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,21 +178,21 @@ public enum HTMLElementAttribute : Hashable {
178178
}
179179

180180
// MARK: htmlValue
181-
public var htmlValue : String? {
181+
public func htmlValue(_ encoding: HTMLEncoding) -> String? {
182182
switch self {
183183
case .accesskey(let value): return value
184-
case .ariaattribute(let value): return value?.htmlValue
184+
case .ariaattribute(let value): return value?.htmlValue(encoding)
185185
case .role(let value): return value?.rawValue
186186
case .autocapitalize(let value): return value?.rawValue
187187
case .autofocus(let value): return value == true ? "" : nil
188188
case .class(let value): return value?.joined(separator: " ")
189-
case .contenteditable(let value): return value?.htmlValue
190-
case .data(_, let value): return value
189+
case .contenteditable(let value): return value?.htmlValue(encoding)
190+
case .data(_, let value): return value
191191
case .dir(let value): return value?.rawValue
192192
case .draggable(let value): return value?.rawValue
193193
case .enterkeyhint(let value): return value?.rawValue
194194
case .exportparts(let value): return value?.joined(separator: ",")
195-
case .hidden(let value): return value?.htmlValue
195+
case .hidden(let value): return value?.htmlValue(encoding)
196196
case .id(let value): return value
197197
case .inert(let value): return value == true ? "" : nil
198198
case .inputmode(let value): return value?.rawValue
@@ -215,11 +215,11 @@ public enum HTMLElementAttribute : Hashable {
215215
case .virtualkeyboardpolicy(let value): return value?.rawValue
216216
case .writingsuggestions(let value): return value?.rawValue
217217

218-
case .trailingSlash: return nil
218+
case .trailingSlash: return nil
219219

220-
case .htmx(let htmx): return htmx?.htmlValue
221-
case .custom(_, let value): return value
222-
case .event(_, let value): return value
220+
case .htmx(let htmx): return htmx?.htmlValue(encoding)
221+
case .custom(_, let value): return value
222+
case .event(_, let value): return value
223223
}
224224
}
225225

@@ -236,14 +236,14 @@ public enum HTMLElementAttribute : Hashable {
236236
}
237237

238238
// MARK: htmlValueDelimiter
239-
public var htmlValueDelimiter : String {
239+
public func htmlValueDelimiter(_ encoding: HTMLEncoding) -> String {
240240
switch self {
241241
case .htmx(let v):
242242
switch v {
243243
case .request(_, _, _, _), .headers(_, _): return "'"
244-
default: return "\\\""
244+
default: return encoding.stringDelimiter
245245
}
246-
default: return "\\\""
246+
default: return encoding.stringDelimiter
247247
}
248248
}
249249
}
@@ -331,7 +331,7 @@ public extension HTMLElementAttribute {
331331
}
332332
}
333333

334-
public var htmlValue : String? {
334+
public func htmlValue(_ encoding: HTMLEncoding) -> String? {
335335
switch self {
336336
case .centimeters(let v),
337337
.millimeters(let v),

0 commit comments

Comments
 (0)