HTML Templating

Nostos provides a functional HTML templating system with the Html(...) syntax. Inside Html(...), bare tag names like div, h1, and p are automatically resolved to their stdlib.html equivalents, giving you a clean, JSX-like syntax without the noise of fully qualified names.

Basic Usage

You only need to import Html and render. Inside Html(...), all tag functions are automatically available.

use stdlib.html.{Html, render}

main() = {
    # Build an HTML tree - no need to import div, h1, p!
    page = Html(
        div([
            h1("Welcome to Nostos!"),
            p("This is a paragraph.")
        ])
    )

    # Render to string
    html = render(page)
    println(html)
    # Output: <div><h1>Welcome to Nostos!</h1><p>This is a paragraph.</p></div>
}

Note: The Html(...) wrapper enables scoped name resolution. Without it, you'd need to write stdlib.html.div everywhere. The magic of Html(...) is that you get access to 40+ tag functions with just a 2-item import!

Tag Functions

Most tag functions are overloaded - they accept either a list of children or a string:

Overloaded Tags

# Most tags are overloaded - same function name, different argument types
div([...])      # <div>...</div>  - with child elements
div("text")     # <div>text</div> - with text content

span([...])     # <span>...</span>
span("text")    # <span>text</span>

p([...])        # <p>...</p>
p("text")       # <p>text</p>

h1([...])       # <h1>...</h1>
h1("title")     # <h1>title</h1>

li([...])       # <li>...</li>
li("item")      # <li>item</li>

# ... and many more: h2, h3, h4, h5, h6, th, td, button, label

Container-Only Tags

# Some tags only accept children (no text overload)
ul([...])       # <ul>...</ul>
ol([...])       # <ol>...</ol>
table([...])    # <table>...</table>
thead([...])    # <thead>...</thead>
tbody([...])    # <tbody>...</tbody>
tr([...])       # <tr>...</tr>
nav([...])      # <nav>...</nav>
header([...])   # <header>...</header>
footer([...])   # <footer>...</footer>
section([...])  # <section>...</section>
article([...])  # <article>...</article>
form([...])     # <form>...</form>

Text-Only Tags

# Some tags only accept text
title("text")   # <title>text</title>
strong("text")  # <strong>text</strong>
em("text")      # <em>text</em>
code("text")    # <code>text</code>
pre("text")     # <pre>text</pre>
small("text")   # <small>text</small>

Self-Closing Tags

# Self-closing tags with named parameters
br()                                        # <br />
hr(class: "divider")                        # <hr class="divider" />
img(src: "/img.png", alt: "Photo")          # <img src="/img.png" alt="Photo" />
input(inputType: "text", name: "email")     # <input type="text" name="email" />
meta(charset: "UTF-8")                      # <meta charset="UTF-8" />
linkTag(rel: "stylesheet", href: "/s.css")  # <link rel="stylesheet" href="/s.css" />

Attributes with Named Parameters

Tag functions support named parameters for common attributes like class, id, href, etc. This gives you clean, readable HTML building:

use stdlib.html.{Html, render}

main() = {
    page = Html(
        div([
            # Named parameters for attributes - clean and readable!
            a("Click here", href: "https://example.com", class: "link"),

            # Image with named attributes
            img(src: "/logo.png", alt: "Logo", width: "100"),

            # Button with type and click handler
            button("Submit", btnType: "submit", class: "btn-primary", onclick: "handleSubmit()")
        ], class: "container", id: "main")
    )

    println(render(page))
    # <div class="container" id="main"><a href="..." class="link">Click here</a>...</div>
}

Common Named Parameters

Most tags support these common attributes as named parameters:

# Container elements
div("content", class: "my-class", id: "my-id", style: "color: red")

# Links
a("Link text", href: "/path", target: "_blank", class: "link")

# Images
img(src: "/image.png", alt: "Description", width: "200", height: "100")

# Form inputs
input(inputType: "text", name: "username", placeholder: "Enter name")
input(inputType: "checkbox", name: "agree", checked: "checked")

# Buttons
button("Click", btnType: "submit", onclick: "handleClick()", disabled: "")

Custom Attributes with attrs

For custom or less common attributes, use the attrs parameter with a list of tuples:

# Add custom data attributes
div("Content", class: "card", attrs: [("data-id", "123"), ("role", "button")])

# Mix named and custom attributes
a("Link", href: "/page", attrs: [("data-track", "click"), ("aria-label", "Go to page")])

Tip: Named parameters work with optional defaults - empty strings are automatically filtered out, so div("Hi", class: "") renders as <div>Hi</div> without an empty class attribute.

Automatic HTML Escaping

Text content is automatically escaped to prevent XSS attacks.

use stdlib.html.{Html, render}

main() = {
    # User input with malicious content
    userInput = "<script>alert('xss')</script>"

    page = Html(p(userInput))
    println(render(page))
    # Safe output: <p>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</p>
}

For raw HTML (use with caution!), use raw():

use stdlib.html.{Html, render}

main() = {
    # Only use raw() for trusted content!
    trustedHtml = "<strong>Bold</strong>"

    page = Html(div([raw(trustedHtml)]))
    println(render(page))
    # Output: <div><strong>Bold</strong></div>
}

Dynamic Content

Use map and list operations to build dynamic HTML.

use stdlib.html.{Html, render}

main() = {
    items = ["Apple", "Banana", "Cherry"]

    page = Html(
        ul(items.map(item => li(item)))
    )

    println(render(page))
    # <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>
}

Components

Create reusable components as functions. Each component uses Html(...) for automatic tag resolution - no need to import individual tags!

use stdlib.html.{Html, render}

# A reusable card component - uses Html(...) for tag resolution
# Type annotations help choose h2(String) over h2(List[Html])
card(title: String, content: String) = Html(
    div([h2(title), p(content)])
)

# Page that uses the card component
main() = {
    page = Html(
        div([
            h1("My Page"),
            card("Welcome", "Thanks for visiting!"),
            card("Features", "Check out our cool features.")
        ])
    )

    println(render(page))
    # <div><h1>My Page</h1><div><h2>Welcome</h2><p>Thanks...</p></div>...</div>
}

Tip: Add type annotations to component parameters (e.g., title: String) when using overloaded tag functions. This helps the compiler choose between h2(String) and h2(List[Html]).

Conditional Rendering

Use if/else expressions and empty() for conditional content.

use stdlib.html.{Html, render}

main() = {
    isLoggedIn = true
    userName = "Alice"

    page = Html(
        div([
            if isLoggedIn then
                span("Welcome, " ++ userName ++ "!")
            else
                span("Please log in"),

            # empty() renders to nothing
            if isLoggedIn then p("You have 3 notifications") else empty()
        ])
    )

    println(render(page))
}

Full Page Example

Here's a complete example building a simple web page with named parameters.

use stdlib.html.{Html, render}

main() = {
    page = Html(
        el("html", [("lang", "en")], [
            headEl([
                meta(charset: "UTF-8"),
                title("My Nostos Page")
            ]),
            body([
                div([
                    h1("Welcome to Nostos", class: "title"),
                    p("A functional programming language with HTML templating."),
                    nav([
                        ul([
                            li([a("About", href: "/about")]),
                            li([a("Documentation", href: "/docs")]),
                            li([a("Examples", href: "/examples")])
                        ], class: "nav-list")
                    ], class: "main-nav")
                ], class: "container", id: "main")
            ])
        ])
    )

    println(render(page))
}

Buffer-Optimized Rendering

The render function uses buffer-based string building internally, avoiding the overhead of repeated string concatenation. This makes rendering efficient even for large HTML trees.

Implementation detail: The VM provides Buffer.new(), Buffer.append(), and Buffer.toString() instructions that the render function uses for O(n) string building instead of O(n²) concatenation.