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:
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
endBuilding 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:
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
endA 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:
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
endThe 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:
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
endInside 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:
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
endComponents
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.
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
endBoth 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 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)
endInside 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:
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")
endThe 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:
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)
endThe 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:
name = "Estedad"
url = "https://editor.salamlang.ir/fonts/Estedad-Light.ttf"
display = "swap"
end
box:
content = "Hello"
font family = "Estedad"
end
endChecked 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.