Skip to content

Commit 751f52c

Browse files
added initial HTMX logic
1 parent 8c60422 commit 751f52c

File tree

5 files changed

+318
-4
lines changed

5 files changed

+318
-4
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ DebugRender.*
1515
DebugXmlRender.*
1616
Elements.*
1717
Events.*
18+
HTMX.d
19+
HTMX.o
20+
HTMX.swiftdeps*
21+
HTMXTests.d
22+
HTMXTests.o
23+
HTMXTests.swiftdeps*
1824
Html4.*
1925
HtmlRender.*
2026
MediaType.*

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,15 @@ private extension HTMLElement {
180180
if key == "ariaattribute" {
181181
key = "aria-" + first_expression.functionCall!.calledExpression.memberAccess!.declName.baseName.text
182182
}
183-
if let string:String = parse_attribute(context: context, elementType: elementType, key: key, expression: first_expression, lookupFiles: lookupFiles) {
183+
if key == "htmx" {
184+
let target:String = first_expression.functionCall!.calledExpression.memberAccess!.declName.baseName.text
185+
var string:String = "\(first_expression)"
186+
string = String(string[string.index(after: string.startIndex)...])
187+
if let htmx:HTMLElementAttribute.HTMX = HTMLElementAttribute.HTMX(rawValue: string) {
188+
let htmlValue:String = htmx.htmlValue
189+
value = "hx-" + target + (htmlValue.isEmpty ? "" : "=\\\"" + htmlValue + "\\\"")
190+
}
191+
} else if let string:String = parse_attribute(context: context, elementType: elementType, key: key, expression: first_expression, lookupFiles: lookupFiles) {
184192
value = string
185193
}
186194
break

Sources/HTMLKitUtilities/HTMLKitUtilities.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,15 @@ public enum HTMLElementAttribute {
119119
case virtualkeyboardpolicy(Extra.virtualkeyboardpolicy? = nil)
120120
case writingsuggestions(Extra.writingsuggestions? = nil)
121121

122-
case custom(_ id: any ExpressibleByStringLiteral, _ value: (any ExpressibleByStringLiteral)?)
123-
124122
/// This attribute adds a space and slash (" /") character before closing a void element tag.
125123
///
126124
/// Usually only used if certain browsers need it for compatibility.
127125
case trailingSlash
128126

127+
case htmx(_ attribute: HTMLElementAttribute.HTMX)
128+
129+
case custom(_ id: any ExpressibleByStringLiteral, _ value: (any ExpressibleByStringLiteral)?)
130+
129131
@available(*, deprecated, message: "General consensus considers this \"bad practice\" and you shouldn't mix your HTML and JavaScript. This will never be removed and remains deprecated to encourage use of other techniques. Learn more at https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_—_dont_use_these.")
130132
case event(Extra.event, _ value: (any ExpressibleByStringLiteral)? = nil)
131133
}
@@ -950,4 +952,4 @@ public extension HTMLElementAttribute.Extra {
950952
enum writingsuggestions : String {
951953
case `true`, `false`
952954
}
953-
}
955+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
//
2+
// HTMX.swift
3+
//
4+
//
5+
// Created by Evan Anderson on 11/12/24.
6+
//
7+
8+
public extension HTMLElementAttribute {
9+
enum HTMX {
10+
case boost(TrueOrFalse)
11+
case confirm(String)
12+
case delete(String)
13+
case disable(Bool)
14+
case disabledElt(String)
15+
case disinherit(String)
16+
case encoding(String)
17+
case ext(String)
18+
case headers(js: Bool, [String:String])
19+
case history(TrueOrFalse)
20+
case historyElt(Bool)
21+
case include(String)
22+
case indicator(String)
23+
case inherit(String)
24+
case params(Params)
25+
case patch(String)
26+
case preserve(Bool)
27+
case prompt(String)
28+
case put(String)
29+
case replaceURL(URL)
30+
case request(js: Bool, timeout: Int = 0, credentials: Bool = false, noHeaders: Bool = false)
31+
case sync(String, strategy: SyncStrategy?)
32+
case validate(TrueOrFalse)
33+
34+
case get(String)
35+
case post(String)
36+
case on(Event, String)
37+
case pushURL(URL)
38+
case select(String)
39+
case selectOOB(String)
40+
case swap(Swap)
41+
case swapOOB(String)
42+
case target(String)
43+
case trigger(String)
44+
case vals(String)
45+
46+
public init?(rawValue: String) {
47+
guard rawValue.last == ")" else { return nil }
48+
let start:String.Index = rawValue.startIndex, end:String.Index = rawValue.index(before: rawValue.endIndex), end_minus_one:String.Index = rawValue.index(before: end)
49+
let key:Substring = rawValue.split(separator: "(")[0]
50+
func string() -> String {
51+
return String(rawValue[rawValue.index(start, offsetBy: key.count + 2)..<end_minus_one])
52+
}
53+
func boolean() -> Bool {
54+
return rawValue[rawValue.index(start, offsetBy: key.count + 1)..<end] == "true"
55+
}
56+
func enumeration<T : RawRepresentable>() -> T where T.RawValue == String {
57+
return T(rawValue: String(rawValue[rawValue.index(start, offsetBy: key.count + 2)..<end]))!
58+
}
59+
switch key {
60+
case "boost": self = .boost(enumeration())
61+
case "confirm": self = .confirm(string())
62+
case "delete": self = .delete(string())
63+
case "disable": self = .disable(boolean())
64+
case "disabledElt": self = .disabledElt(string())
65+
case "disinherit": self = .disinherit(string())
66+
case "encoding": self = .encoding(string())
67+
case "ext": self = .ext(string())
68+
//case "headers": self = .headers(js: Bool, [String : String])
69+
case "history": self = .history(enumeration())
70+
case "historyElt": self = .historyElt(boolean())
71+
case "include": self = .include(string())
72+
case "indicator": self = .indicator(string())
73+
case "inherit": self = .inherit(string())
74+
//case "params": self = .params(enumeration())
75+
case "patch": self = .patch(string())
76+
case "preserve": self = .preserve(boolean())
77+
case "prompt": self = .prompt(string())
78+
case "put": self = .put(string())
79+
//case "replaceURL": self = .replaceURL(enumeration())
80+
//case "request": self = .request(js: Bool, timeout: Int, credentials: Bool, noHeaders: Bool)
81+
//case "sync": self = .sync(String, strategy: SyncStrategy?)
82+
case "validate": self = .validate(enumeration())
83+
84+
case "get": self = .get(string())
85+
case "post": self = .post(string())
86+
//case "on": self = .on(Event, String)
87+
//case "pushURL": self = .pushURL(enumeration())
88+
case "select": self = .select(string())
89+
case "selectOOB": self = .selectOOB(string())
90+
case "swap": self = .swap(enumeration())
91+
case "swapOOB": self = .swapOOB(string())
92+
case "target": self = .target(string())
93+
case "trigger": self = .trigger(string())
94+
case "vals": self = .vals(string())
95+
default: return nil
96+
}
97+
}
98+
99+
public var htmlValue : String {
100+
switch self {
101+
case .boost(let value): return value.rawValue
102+
case .confirm(let value): return value
103+
case .delete(let value): return value
104+
case .disable(_): return ""
105+
case .disabledElt(let value): return value
106+
case .disinherit(let value): return value
107+
case .encoding(let value): return value
108+
case .ext(let value): return value
109+
case .headers(let js, let headers):
110+
return js ? "" : headers.map({ "\"" + $0.key + "\":\"" + $0.value + "\"" }).joined(separator: ",")
111+
case .history(let value): return value.rawValue
112+
case .historyElt(_): return ""
113+
case .include(let value): return value
114+
case .indicator(let value): return value
115+
case .inherit(let value): return value
116+
case .params(let params): return params.htmlValue
117+
case .patch(let value): return value
118+
case .preserve(_): return ""
119+
case .prompt(let value): return value
120+
case .put(let value): return value
121+
case .replaceURL(let url): return url.htmlValue
122+
case .request(let js, let timeout, let credentials, let noHeaders):
123+
return ""
124+
case .sync(let selector, let strategy):
125+
return selector + (strategy == nil ? "" : ":" + strategy!.htmlValue)
126+
case .validate(let value): return value.rawValue
127+
128+
case .get(let value): return value
129+
case .post(let value): return value
130+
case .on(_, let value): return value
131+
case .pushURL(let url): return url.htmlValue
132+
case .select(let value): return value
133+
case .selectOOB(let value): return value
134+
case .swap(let swap): return swap.rawValue
135+
case .swapOOB(let value): return value
136+
case .target(let value): return value
137+
case .trigger(let value): return value
138+
case .vals(let value): return value
139+
}
140+
}
141+
}
142+
}
143+
144+
// MARK: Attributes
145+
146+
147+
148+
149+
public extension HTMLElementAttribute.HTMX {
150+
// MARK: Boost
151+
enum TrueOrFalse : String {
152+
case `true`, `false`
153+
}
154+
155+
// MARK: Event
156+
enum Event : String {
157+
case abort
158+
case afterOnLoad
159+
case afterProcessNode
160+
case afterRequest
161+
case afterSettle
162+
case afterSwap
163+
case beforeCleanupElement
164+
case beforeOnLoad
165+
case beforeProcessNode
166+
case beforeRequest
167+
case beforeSwap
168+
case beforeSend
169+
case beforeTransition
170+
case configRequest
171+
case confirm
172+
case historyCacheError
173+
case historyCacheMiss
174+
case historyCacheMissError
175+
case historyCacheMissLoad
176+
case historyRestore
177+
case beforeHistorySave
178+
case load
179+
case noSSESourceError
180+
case onLoadError
181+
case oobAfterSwap
182+
case oobBeforeSwap
183+
case oobErrorNoTarget
184+
case prompt
185+
case pushedIntoHistory
186+
case responseError
187+
case sendError
188+
case sseError
189+
case sseOpen
190+
case swapError
191+
case targetError
192+
case timeout
193+
case validationValidate
194+
case validationFailed
195+
case validationHalted
196+
case xhrAbort
197+
case xhrLoadEnd
198+
case xhrLoadStart
199+
case xhrProgress
200+
201+
public var htmlValue : String {
202+
switch self {
203+
case .validationValidate: return "validation:validate"
204+
case .validationFailed: return "validation:failed"
205+
case .validationHalted: return "validation:halted"
206+
case .xhrAbort: return "xhr:abort"
207+
case .xhrLoadEnd: return "xhr:loadend"
208+
case .xhrLoadStart: return "xhr:loadstart"
209+
case .xhrProgress: return "xhr:progress"
210+
default: return rawValue
211+
}
212+
}
213+
}
214+
215+
// MARK: Modifiers
216+
enum Modifier {
217+
}
218+
219+
// MARK: Params
220+
enum Params {
221+
case all
222+
case none
223+
case not([String])
224+
case list([String])
225+
226+
public var htmlValue : String {
227+
switch self {
228+
case .all: return "*"
229+
case .none: return "none"
230+
case .not(let list): return "not " + list.joined(separator: ",")
231+
case .list(let list): return list.joined(separator: ",")
232+
}
233+
}
234+
}
235+
236+
// MARK: Swap
237+
enum Swap : String {
238+
case innerHTML, outerHTML
239+
case textContent
240+
case beforebegin, afterbegin
241+
case beforeend, afterend
242+
case delete, none
243+
}
244+
245+
// MARK: Sync
246+
enum SyncStrategy {
247+
case drop, abort, replace
248+
case queue(Queue)
249+
250+
public enum Queue : String {
251+
case first, last, all
252+
}
253+
254+
public var htmlValue : String {
255+
switch self {
256+
case .drop: return "drop"
257+
case .abort: return "abort"
258+
case .replace: return "replace"
259+
case .queue(let queue): return queue.rawValue
260+
}
261+
}
262+
}
263+
264+
// MARK: URL
265+
enum URL {
266+
case `true`, `false`
267+
case url(String)
268+
269+
public var htmlValue : String {
270+
switch self {
271+
case .true: return "true"
272+
case .false: return "false"
273+
case .url(let url): return url.hasPrefix("http://") || url.hasPrefix("https://") ? url : (url.first == "/" ? "" : "/") + url
274+
}
275+
}
276+
}
277+
}

Tests/HTMLKitTests/HTMXTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// HTMXTests.swift
3+
//
4+
//
5+
// Created by Evan Anderson on 11/12/24.
6+
//
7+
8+
import Testing
9+
import HTMLKit
10+
11+
struct HTMXTests {
12+
@Test func get() {
13+
let string:StaticString = #div(attributes: [.htmx(.get("/test"))])
14+
#expect(string == "<div hx-get=\"/test\"></div>")
15+
}
16+
17+
@Test func post() {
18+
let string:StaticString = #div(attributes: [.htmx(.post("https://github.com/RandomHashTags"))])
19+
#expect(string == "<div hx-post=\"https://github.com/RandomHashTags\"></div>")
20+
}
21+
}

0 commit comments

Comments
 (0)