Lesson 24 of 25 · Larger programs

The layout language

Salam has a small language of its own for building web pages: layout. You describe the page as a tree of elements in Salam, the compiler checks every name and value, and you get HTML and CSS back.

A page

A layout: block is a tree of page elements. Each element opens with its name and a colon and closes with end, and attributes and styles are written as name = value inside it, with no commas between them. The settings at the top of the block, such as title, lang and dir, belong to the page itself:

layout-page.salam
layout:
    title = "My app"
    lang = "english"
    dir = "left to right"
    header:
        background = "#2563eb"
        color = "white"
        padding = "24px"
        heading: size = 1 content = "Welcome to Salam" end
    end
    box:
        padding = "20px"
        paragraph: content = "This page was built from Salam code." end
    end
end

Building the page

salam layout build page.salam turns that file into page.html and page.css, plus a page.js when the page has scripts. --inline puts everything into one self-contained HTML file. Given several files, salam layout build home.salam about.salam writes one HTML file per page and a single style.css they share. With no file name at all it builds salam.salam if there is one, and otherwise every layout page in the folder.

The styles do not end up on the elements. Each element that has styles gets a short generated class, such as .a or .b, and the rules go into the stylesheet, so the HTML stays small.

Elements and attributes

Most HTML elements have a readable name: box is a div, paragraph a p, link an a, list and item are ul and li, and nav, form, label, input and button keep their HTML names. The text of an element is its content. Links and images both take url, which becomes href or src as needed, and some values have words of their own, such as target = "new" for a link that opens in a new tab:

layout-form.salam
layout:
    title = "Contact us"
    nav:
        link: url = "/" content = "Home" end
        link: url = "/blog" content = "Blog" end
        link: url = "https://github.com/SalamLang/Salam" target = "new" content = "Source" end
    end
    image: url = "/logo.png" alt = "Salam logo" end
    form:
        id = "contact"
        label: for = "email" content = "Your email" end
        input: id = "email" type = "email" name = "email" placeholder = "you@example.com" required = true end
        label: for = "message" content = "Message" end
        input: id = "message" type = "text" name = "message" end
        button: type = "submit" content = "Send" end
    end
    list:
        item: content = "We answer within a day" end
        item: content = "No account needed" end
    end
end

A true attribute, such as required = true, becomes a bare HTML attribute, and a false one is left out. Every element and its names, English and Persian, are listed in the element reference.

Styles

CSS properties are written the same way, with spaces instead of dashes: border radius, font family, box sizing. A property with hover in front of it applies while the pointer is over the element. A global element holds rules for the whole page: on its own it styles every element, and with selector it styles whatever the selector matches. Values can be built from constants declared above the layout, so a colour or a spacing is written once:

layout-style.salam
const ACCENT := "#0f766e"
const GAP := 16

layout:
    title = "Pricing"
    global: box sizing = "border-box" end
    global: selector = "body" margin = "0" font family = "sans-serif" end
    section:
        display = "flex"
        gap = GAP + "px"
        padding = GAP * 2 + "px"
        box:
            border = "1px solid #ddd"
            border radius = "12px"
            padding = GAP + "px"
            heading: size = 2 content = "Free" end
            paragraph: content = "For trying things out" end
        end
        box:
            border = "2px solid " + ACCENT
            border radius = "12px"
            padding = GAP + "px"
            heading: size = 2 content = "Pro" color = ACCENT end
            button:
                content = "Upgrade"
                background = ACCENT
                color = "white"
                hover background = "#115e59"
                cursor = "pointer"
            end
        end
    end
end

The Pro box ends up with the rule border:2px solid #0f766e and its button with a :hover rule, all worked out while compiling. One thing to avoid: if you give two elements the same class and different styles, both rules are written under that one class name and the later one wins for both elements. Leave class out on styled elements, or style a shared class once with global. Nearly the whole of CSS is covered this way, including grid, animation, scroll-driven timelines, anchor positioning and the SVG-only properties; the property reference lists every one, English and Persian, with what it accepts.

Making it interactive

An attribute named after a browser event, such as click, submit, key down or change, takes a function instead of a value. A plain function name is compiled through the JavaScript backend and dropped into the page's own script, so the behaviour stays ordinary Salam instead of hand-written JS; an inline function works the same way and gets a generated name of its own:

layout-events.salam
import dom
import conv

mut count := 0

func increment():
    count += 1
    dom.SetText(dom.ById("count"), conv.FormatInt(count))
end

layout:
    title = "Counter"
    box:
        heading: id = "count" content = "0" end
        button: click = increment content = "+1" end
    end
end

Inside the handler, dom.ById and dom.Query are checked against the ids actually used in the same file's layout, so a misspelled id such as dom.ById("counter") is caught before the page ever runs. A plain string such as click = "alert(42)" still works too, for a one-line call into existing JavaScript.

Tables

One name can produce a different element depending on where it sits. Inside table, header, main and footer are thead, tbody and tfoot, and column becomes th in a header row and td everywhere else:

layout-table.salam
layout:
    table:
        header:
            row:
                column: content = "Name" end
                column: content = "Score" end
            end
        end
        main:
            row:
                column: content = "Sara" end
                column: content = "18" end
            end
        end
    end
end

Components

A component is a piece of page you define once and use as often as you like, as if it were a new element. Its parameters are set like attributes. Inside it, content: end marks the slot: whatever you nest inside the component where you use it lands there.

layout-component.salam
component Plan(name, price):
    box:
        border = "1px solid #ddd"
        border radius = "12px"
        padding = "16px"
        heading: size = 2 content = name end
        paragraph: content = "$" + price + " a month" end
        content: end
    end
end

layout:
    title = "Plans"
    Plan: name = "Basic" price = 4 end
    Plan: name = "Pro" price = 9
        list:
            item: content = "Private projects" end
            item: content = "Email support" end
        end
    end
end

Both plans share one generated class, because their styles are identical. To share components between pages, put them in a file of their own and write include = "parts.salam" at the top of the layout; the path is relative to the page that includes it.

Layouts with parameters

A layout can take a name and parameters. It then becomes a function that returns the page as a string of HTML, and a normal Salam program can call it with values it only knows while running:

layout-runtime.salam
layout Card(name: str, score: int):
    title = "Card " + name
    heading: size = 2 content = name end
    if score >= 50:
        paragraph: content = "pass" end
    else:
        paragraph: content = "fail" end
    end
end

func main:
    println Card("Ali", 70)
end
Output<!doctype html><html lang="en" dir="ltr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Card Ali</title></head><body><h2>Ali</h2><p>pass</p></body></html>

Inside such a layout you can use if, else and each, and a component with typed parameters can be called on its own to get just its piece of HTML. Text that comes from a parameter is escaped for you, so a <b> or a <script> in the data shows up as text and never becomes part of the page:

layout-inbox.salam
component Badge(text: str):
    span: content = text background = "#fde68a" end
end

layout Inbox(user: str, subjects: Vector<str>, unread: int):
    title = "Inbox of " + user
    heading: size = 1 content = "Hello, " + user end
    if unread == 0:
        paragraph: content = "No new messages" end
    else:
        paragraph: content = "Unread: " + unread end
        list:
            each i, s in subjects:
                item: content = (i + 1) + ". " + s end
            end
        end
    end
end

func main:
    mut subjects := Vector {} as Vector<str>
    defer subjects.free()
    subjects.push("Meeting at 10")
    subjects.push("<b>FREE</b> prize & more")
    println Inbox("Sara", subjects, subjects.len())
    println Badge("new")
end
Output<!doctype html><html lang="en" dir="ltr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Inbox of Sara</title></head><body><h1>Hello, Sara</h1><p>Unread: 2</p><ul><li>1. Meeting at 10</li><li>2. &lt;b&gt;FREE&lt;/b&gt; prize &amp; more</li></ul></body></html> <style>.badge-a{background:#fde68a}</style><span class="badge-a">new</span>

The structure of the page is still fixed while compiling; only the values change. Two rules keep it that way. A layout with parameters is a pure function, so it cannot do anything else while building the page: even subjects.len() is not allowed inside it, which is why the count arrives as a parameter of its own. And a plain str cannot go where a URL, a colour or a length belongs, because text from a user could break out of it. Parse it first with web.ParseUrl, web.ParseColor or web.ParseLength from the web package, which reject anything unsafe, and give the layout the resulting web.Url, web.Color or web.Length.

A page from a Salam program

Because a layout with parameters is just a function, a web server written in Salam can answer each request with a fresh page. This program serves a greeting at http://localhost:8080/?name=Sara:

layout-server.salam
import net.http
import net.router

layout Greeting(name: str):
    title = "Hello " + name
    heading: size = 1 content = "Hello, " + name end
    paragraph: content = "This page was rendered on the server." end
end

func home(ctx: i64):
    mut name := http.Ctx_query(ctx, "name")
    if name == "":
        name = "guest"
    end
    http.Ctx_html(ctx, Greeting(name))
end

func main:
    mut r := router.NewRouter()
    router.Get(r, "/", home)
    mut s := http.NewServer(8080)
    http.Use(s, r)
    http.Run(s)
end

The server keeps running until you stop it, so the example has no output here. The name comes straight from the address bar, and thanks to the escaping, a visitor who sends ?name=<b> sees the text <b> and nothing else.

Web fonts

The font element produces an @font-face. name and url are required; if you leave type out, Salam works it out from the extension in the URL and then from the bytes of a file sitting next to the program. Use the font afterwards with font family:

layout-font.salam
layout:
    font:
        name = "Estedad"
        url = "https://editor.salamlang.ir/fonts/Estedad-Light.ttf"
        display = "swap"
    end
    box:
        content = "Hello"
        font family = "Estedad"
    end
end

Checked while compiling

Salam checks every element and attribute before it builds the page. A misspelled name, an attribute that element does not have, a malformed value such as a URL in the wrong shape, a required attribute left out, or an element in the wrong place is an error, and the message suggests the nearest valid name. For example, hint = "x" on an input gives "unknown attribute 'hint' on 'input'; did you mean 'enterkeyhint'?", and an item outside a list gives "element 'item' cannot be placed inside 'layout'". Inside a layout with parameters, an unknown element name is only a warning and is built as a div, so read the warnings there too.

The layout language turns into plain HTML and CSS, so anything you know about the web still applies. For the server side, the net.http and net.router packages used above handle requests, query strings and responses.