Write HTML using Swift Macros. Supports HTMX via global attributes.
- Swift Macros are powerful, efficient and removes any runtime overhead (dynamic HTML requires runtime)
- Alternative libraries may not fit all situations and may restrict how the html is generated, manipulated, or cost a constant performance overhead (middleware, rendering, result builders, etc)
- HTML macros enforce safety, can be used anywhere, and compile directly to strings by default
- The output is minified at no performance cost
How do I use this library?
Use the #html(encoding:attributes:innerHTML:)
macro. All parameters, for the macro and default HTML elements, are optional by default. The default HTML elements are generated by an internal macro.
#html(
encoding: HTMLEncoding = .string,
attributes: [<global attribute>] = [],
<element specific attribute>: <element specific attribute value>? = nil,
_ innerHTML: CustomStringConvertible...
)
All default HTML elements conform to the HTMLElement
protocol and contain their appropriate element attributes. They can be declared when you initialize the element or be changed after initialization by accessing the attribute variable directly.
The default initializer for creating an HTML Element follows this syntax:
<html element name>(
attributes: [<global attribute>] = [],
<element specific attribute>: <value>? = nil,
_ innerHTML: CustomStringConvertible...
)
// <div class="dark"><p>Macros are beautiful</p></div>
#html(
div(attributes: [.class(["dark"])],
p("Macros are beautiful")
)
)
// <a href="https://github.com/RandomHashTags/litleagues" target="_blank"></a>
#html(
a(href: "https://github.com/RandomHashTags/litleagues", target: ._blank)
)
// <input id="funny-number" max="420" min="69" name="funny_number" step="1" type="number" value="69">
#html(
input(
attributes: [.id("funny-number")],
max: 420,
min: 69,
name: "funny_number",
step: 1,
type: .number,
value: "69"
)
)
// html example
let test:String = #html(
html(
body(
div(
attributes: [
.class(["dark-mode", "row"]),
.draggable(.false),
.hidden(.true),
.inputmode(.email),
.title("Hey, you're pretty cool")
],
"Random text",
div(),
a(
div(
abbr()
),
address()
),
div(),
button(disabled: true),
video(autoplay: true, controls: false, preload: .auto, src: "https://github.com/RandomHashTags/litleagues", width: .centimeters(1)),
)
)
)
)
How do I escape HTML?
The compiled output automatically escapes source breaking html characters known only at compile time.
You can also use the #escapeHTML(innerHTML:)
macro to escape data known at compile time.
If you're working with runtime data:
<string>.escapeHTML(escapeAttributes:)
- mutates
self
escaping HTML and, optionally, attribute characters
- mutates
<string>.escapeHTMLAttributes()
- mutates
self
escaping only attribute characters
- mutates
<string>.escapingHTML(escapeAttributes:)
- returns a copy of
self
escaping HTML and, optionally, attribute characters
- returns a copy of
<string>.escapingHTMLAttributes()
- returns a copy of
self
escaping only attribute characters
- returns a copy of
How do I encode variables?
Using String Interpolation.
You will get a compiler warning saying interpolation may introduce raw HTML.
Its up to you whether or not to suppress this warning or escape the HTML at runtime using a method described above.
Swift HTMLKit tries to promote known interpolation at compile time with an equivalent
StaticString
for the best performance. It is currently limited due to macro expansions being sandboxed and lexical contexts/AST not being available for macro arguments. This means referencing content known at compile time in a html macro won't get replaced by its literal contents. Read more about this limitation.
let string:String = "any string value", integer:Int = -69, float:Float = 3.141592
// ✅ DO
let _:String = #html(p("\(string); \(integer); \(float)"))
let _:String = #html(p("\(string)", "; ", String(describing: integer), "; ", float.description))
let integer_string:String = String(describing: integer), float_string:String = String(describing: float)
let _:String = #html(p(string, "; ", integer_string, "; ", float_string))
// ❌ DON'T; compiler error; compile time value cannot contain interpolation
let _:StaticString = #html(p("\(string); \(integer); \(float)"))
let _:StaticString = #html(p("\(string)", "; ", String(describing: integer), "; ", float.description))
let _:StaticString = #html(p(string, "; ", integer_string, "; ", float_string))
I need a custom element!
Use the default custom(tag:isVoid:attributes:innerHTML:)
html element.
We want to show the Apple Pay button:
#html(
custom(
tag: "apple-pay-button",
isVoid: false,
attributes: [
.custom("buttonstyle", "black"),
.custom("type", "buy"),
.custom("locale", "el-GR")
]
)
)
becomes
<apple-pay-button buttonstyle="black" type="buy" locale="el-GR"></apple-pay-button>
I need a custom attribute!
Use the .custom(id:value:)
global attribute.
We want to show the Apple Pay button:
#html(
custom(
tag: "apple-pay-button",
isVoid: false,
attributes: [
.custom("buttonstyle", "black"),
.custom("type", "buy"),
.custom("locale", "el-GR")
]
)
)
becomes
<apple-pay-button buttonstyle="black" type="buy" locale="el-GR"></apple-pay-button>
I need to listen for events!
WARNING
Inline event handlers are an outdated way to handle events.
General consensus considers this "bad practice" and you shouldn't mix your HTML and JavaScript.
This 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.
Use the .event(<event type>, "<value>")
global attribute.
#html(
div(
attributes: [
.event(.click, "doThing()"),
.event(.change, "doAnotherThing()")
]
)
)
I need the output as a different type!
Declare the encoding you want in the #html
macro.
#html(
encoding: HTMLEncoding = .<type>
)
string
->String
/StaticString
utf8Bytes
->[UInt8]
utf16Bytes
->[UInt16]
utf8CString
->ContiguousArray<CChar>
foundationData
->Foundation.Data
- You need to
import Foundation
to use this!
- You need to
byteBuffer
->NIOCore.ByteBuffer
- You need to
import NIOCore
to use this! Swift HTMLKit does not depend onswift-nio
!
- You need to
custom("<encoding logic>")
-> A custom type conforming toCustomStringConvertible
- Use
$0
to reference the compiled HTML (as a String without the delimiters)
- Use
How do I use HTMX?
Use the .htmx(<htmx attribute>)
global attribute. All HTMX 2.0 attributes are supported (including Server Sent Events & Web Sockets).
// <div hx-boost="true"></div>
var string:StaticString = #html(div(attributes: [.htmx(.boost(.true))]))
// <div hx-get="/test"></div>
string = #html(div(attributes: [.htmx(.get("/test"))]))
// <div hx-on::abort="bruh()"></div>
string = #html(div(attributes: [.htmx(.on(.abort, "bruh()"))]))
// <div hx-on::after-on-load="test()"></div>
string = #html(div(attributes: [.htmx(.on(.afterOnLoad, "test()"))]))
// <div hx-on:click="thing()"></div>
string = #html(div(attributes: [.htmx(.onevent(.click, "thing()"))]))
// <div hx-preserve></div>
string = #html(div(attributes: [.htmx(.preserve(true))]))
// <div sse-connect="/connect"></div>
string = #html(div(attributes: [.htmx(.sse(.connect("/connect")))]))
// <div ws-connect="/chatroom"></div>
string = #html(div(attributes: [.htmx(.ws(.connect("/chatroom")))]))
// <div hx-ext="ws" ws-send></div>
string = #html(div(attributes: [.htmx(.ext("ws")), .htmx(.ws(.send(true)))]))
- Libraries tested
- BinaryBuilds/swift-html v1.7.0 (patched version here)
- sliemeobn/elementary v0.4.1
- JohnSundell/Plot v0.14.0
- tayloraswift/swift-dom v1.1.0 (patched version here)
- RandomHashTags/swift-htmlkit v0.10.0 (this library)
- pointfreeco/swift-html v0.4.1
- robb/Swim v0.4.0
- vapor-community/HTMLKit v2.8.1
- dokun1/Vaux v0.2.0 (patched version here; custom renderer here)
Test machine:
- CPU: 7800x3D
- RAM: 32GB DDR5
- Storage: 1TB NVME (661.2 GiB free space)
- OS: Arch Linux 6.11.9-arch1-1
- Swift: 6.0.2, Swift 6 compiler
Benchmark command: swift package --allow-writing-to-package-directory benchmark --target Benchmarks --metric throughput --format jmh
This library is the clear leader in performance & efficiency. Static webpages offer the best performance, while dynamic pages still tops the charts (I am actively researching and testing improvements for dynamic pages).
Contributions are always welcome.
Please try to use this library's syntax when creating a PR.
Changes in syntax must solve real-word problems to be accepted.