Skip to content

RandomHashTags/swift-htmlkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Write HTML using Swift Macros. Supports HTMX via global attributes.

Requires at least Swift 5.9 Apache 2.0 License

Table of Contents

Why

  • Swift Macros are powerful, efficient and essentially removes any runtime overhead
  • Alternative libraries may not fit all situations and may restrict how the html is generated, manipulated, prone to human error, or cost a constant performance overhead (middleware, rendering, result builders, etc)
  • HTML macros enforce safety, can be used anywhere, and compile directly to strings
  • The output is minified at no performance cost

Usage

Basic

How do I use this library?

Syntax

#<html element>(
  attributes: [<global attribute>],
  <element specific attribute>: <element specific attribute value>?,
  _ innerHTML: ExpressibleByStringLiteral...
)
  • all parameters are optional by default

Examples

// <div class="dark"><p>Macros are beautiful</p></div>
#div(attributes: [.class(["dark"])],
    #p("Macros are beautiful")
)

// <a href="https://github.com/RandomHashTags/litleagues" target="_blank"></a>
#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">
#input(
    attributes: [.id("funny-number")],
    max: 420,
    min: 69,
    name: "funny_number",
    step: 1,
    type: .number,
    value: "69"
)

// html example
let test:String = #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 output automatically escapes html characters known only at compile time.

If you know the data at compile time (and not working with HTML macros):

  • #escapeHTML() macro

If you're working with runtime data:

  • <string>.escapeHTML(escapeAttributes:)
    • mutates self escaping HTML and, optionally, attribute characters
  • <string>.escapeHTMLAttributes()
    • mutates self escaping only attribute characters
  • <string>.escapingHTML(escapeAttributes:)
    • returns a copy of self escaping HTML and, optionally, attribute characters
  • <string>.escapingHTMLAttributes()
    • returns a copy of self escaping only attribute characters
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 replace known interpolation at compile time with a compile time equivalent 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.

Example

let string:String = "any string value", integer:Int = -69, float:Float = 3.141592

// ✅ DO
let _:String = #p("\(string); \(integer); \(float)")
let _:String = #p("\(string)", "; ", String(describing: integer), "; ", float.description)

let integer_string:String = String(describing: integer), float_string:String = String(describing: float)
let _:String = #p(string, "; ", integer_string, "; ", float_string)

// ❌ DON'T; compiler error; compile time value cannot contain interpolation
let _:StaticString = #p("\(string); \(integer); \(float)")
let _:StaticString = #p("\(string)", "; ", String(describing: integer), "; ", float.description)
let _:StaticString = #p(string, "; ", integer_string, "; ", float_string)

Advanced

I need a custom element!

Use the #custom(tag:isVoid:attributes:innerHTML:) macro.

Example

We want to show the Apple Pay button:

#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 HTMLElementAttribute.custom(id:value:)

Example

We want to show the Apple Pay button:

#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 HTMLElementAttribute.event(<type>, "<value>").

Example

#div(attributes: [.event(.click, "doThing()"), .event(.change, "doAnotherThing()")])
I need the output as a different type!

Use a different html macro. Currently supported types (more to come with new language features):

  • #html() -> String/StaticString
  • #htmlUTF8Bytes() -> [UInt8]
  • #htmlUTF16Bytes() -> [UInt16]
  • #htmlUTF8CString() -> ContiguousArray<CChar>
  • #htmlData() -> Foundation.Data
  • #htmlByteBuffer() -> NIOCore.ByteBuffer

HTMX

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).

Examples

// <div hx-boost="true"></div>
var string:StaticString = #div(attributes: [.htmx(.boost(.true))])

// <div hx-get="/test"></div>
string = #div(attributes: [.htmx(.get("/test"))])

// <div hx-on::abort="bruh()"></div>
string = #div(attributes: [.htmx(.on(.abort, "bruh()"))])

// <div hx-on::after-on-load="test()"></div>
string = #div(attributes: [.htmx(.on(.afterOnLoad, "test()"))])

// <div hx-on:click="thing()"></div>
string = #div(attributes: [.htmx(.onevent(.click, "thing()"))])

// <div hx-preserve></div>
string = #div(attributes: [.htmx(.preserve(true))])

// <div sse-connect="/connect"></div>
string = #div(attributes: [.htmx(.sse(.connect("/connect")))])

// <div ws-connect="/chatroom"></div>
string = #div(attributes: [.htmx(.ws(.connect("/chatroom")))])

// <div hx-ext="ws" ws-send></div>
string = #div(attributes: [.htmx(.ext("ws")), .htmx(.ws(.send(true)))])

Benchmarks

Test machine: iMac (i9 9900k, 72GB RAM, 2TB) running macOS 15.0 with the Swift 6 compiler.

Executed command: swift package --allow-writing-to-package-directory benchmark --target Benchmarks --metric throughput --format jmh

Static

Dynamic

Conclusion

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).

Contributing

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.