GHTML
A .ghtml file is one page, one component or one library of functions, written as HTML with plain Gizlang in braces.
tools/cli/ghtml.giz turns it into a Gizlang module with the same line numbers, so a compile error names the line of the
.ghtml file. A program imports a .ghtml file as it imports a .giz file.
The page says what it shows, what it keeps and what it does. The compiler finds the rest: what is state, what is derived,
what each control changes, and which condition is a database query. There are no $ functions, no stores and no bind:.
1. A page
<script>
import { Todo } from "./model.giz";
let filter = "all"; // let is state
const todos = table<Todo>("id desc"); // a table of the page: its type and its order
function clear(done: bool) { for (const t of todos.where(t => t.done === done)) { todos.remove(t); } }
</script>
<h1>Todos ({todos.count(t => !t.done)} open)</h1>
<form create={todos}>
<input name="text" placeholder="What next?"/>
<button>Add</button>
</form>
<select value={filter}>
<option value="all">All</option>
<option value="open">Open</option>
<option value="done">Done</option>
</select>
<ul>
<li each={t of todos.where(t => filter === "all" || t.done === (filter === "done"))} key={t.id} class:done={t.done}>
<input type="checkbox" checked={t.done}/> {t.text}
<button onclick={todos.remove(t)}>Remove</button>
</li>
</ul>
<p if={todos.length === 0}>Nothing to do.</p>
<style> .done { text-decoration: line-through; } </style>create={todos} adds a row whose fields are the named inputs. value={filter} is two-way, and checked={t.done} writes the row.
The model is a normal @Model.Table type (tests/web/todo/model.giz). Its @Model.MaxLength and @Model.Min are the rules of the
form: a refusal is answered with status 422 and the reason in <output role="alert">.
2. Three kinds of file
| Kind | Name | Module |
|---|---|---|
| Component | Capital first: Card.ghtml | export type Props from its prop lines, and export function Card(_p: Props, children): Html |
| Library | Only <template fn> at the top level, any name | One function for each template; export makes it public |
| Route | Lower case, [ or _ first: index.ghtml, [slug].ghtml, _layout.ghtml | export function page(_p, children): Html and export const style; see §7 |
A file has at most one <script>, before the markup. Its statements are:
| Statement | Meaning |
|---|---|
import …, type …, export type … | Module code. |
prop name: Type; | A prop of a component. prop size: string = "m"; has a default (a literal or an expression of the file) that a caller may leave out, in any package: the prop is an optional field of Props, and the component fills the default. |
let name = value; | State (a string, i64, f64 or bool). Route files only. @url after the value keeps it in the address (?name=value), @local in the browser, @cookie in a cookie; see §4. |
const t = table<T>("order"); | A table of the page: T is a @Model.Table type, the string is its ORDER BY. Route files only. |
const d = expr; | Derived when expr uses a name of the page (it is computed again in each render), else a module constant. |
function f(…) { … } | A function. One that assigns state or writes a table is an action: see §4. |
Any other statement is an error that says it belongs in a function. A component and a layout keep no state, table or event:
the page keeps them and passes props. me, params, request and error are names that a route file has.
3. Markup
{expr}is escaped text;html={expr}on<template>is rawHtml.- Directives are attributes:
if={c},else-if={c},else,each={x of xs}oreach={x, i of xs}withkey={k},class:name={c}. On one elementeachruns first andifruns for each item. <template>groups without an element:<template if={c}>,<template match={v}><template case={Pattern(x)}>…,<template const={x = expr}/>,<template html={expr}/>, and<template fn="name(a: T)" export>…</template>(a function of the file).- A capital name is a component:
<Card title="x">…</Card>.<slot/>is the children. A child withslot="title"(<h2 slot="title">…</h2>) is the content of<slot name="title"/>in the component, and<slot/>then holds the rest. A component needs noimport:Card.ghtmlis found in the folder of the file or itscomponents/folder, then in each folder above, up to the root of the package. Animportin the script wins. <meta>and<link>tags in a page or a layout go to the head of the document, once each (name,propertyorhttp-equivis the key; the innermost page wins), and the kernel swaps them when it opens another page.<title>in a page is the title of the document, once, with the page's values (<title>Todos ({n} open)</title>); without it the first<h1>is the title.- Not ghtml, each with a message that names the line:
{#if}blocks,bind:,use:,$props(),$state(),$effect.
4. Events, state and binding
A page is a function of its state. An event is a button of a form that carries the state; the server runs it and answers the new page, and the kernel morphs it in. Without JavaScript the same page is a plain form post.
onclickgoes on a<button>;onsubmitandcreate={table}go on a<form>. The value is a call (todos.remove(t)), a function name or statements separated by;. A button inside aneachknows its row.- A function that assigns state or writes a table (or calls one) gets the names it uses as extra parameters and returns nothing. A table write returns a reason when the Gate or the model refuses; the function stops there and the page answers 422.
valueorcheckedon aninput,selectortextareais two-way when it is a state name (value={filter}) or a field of the row of aneachover a table (checked={t.done}, needskey). Such a control is named by the page; it has noname. A state is sent on change; a field is written to its row on change, through the same Gate as an action.- State travels in the hidden form
#_g(_s.name), so a server keeps no session. let q = "" @url;is state that the address holds:/?q=xshows the page withqset, and a change ofqis a step of the browser history. A state at its initial value is not in the address.let tab = "a" @local;is state the browser keeps for the next visit (localStorage, by address): the first render has the initial value, then the kernel restores the stored one.let theme = "light" @cookie;is state in the cookieg.theme(one year,Path=/, for every page that names it): the server reads it on a visit that the address does not set, so the first render already shows the person's choice. Without JavaScript none of the three is kept.- A bound control of a row sends what it showed (
_o.…) with its new value. The change is oneUPDATEof that field; if another person changed the field meanwhile the change is refused (a 409 conflict): the page says "That Todo changed meanwhile. Read it again." and shows the new value. A control that shows what it posts did not change and is not written.
5. Tables and queries
table<T>(order) reads a @Model.Table type through the actor of the request (the Gate): the person sees and writes only what
the grants allow. tbl.rows() is every row (each={t of todos}), tbl.length and tbl.count() the total.
tbl.where(row => cond) and tbl.count(row => cond) are queries. The condition is read at build time into SQL with typed
parameters; the page values enter as parameters, never as text. A condition has row.field compared with a value of the page
(===, !==, <, <=, >, >=), &&, ||, !, a boolean field, and row.field.includes(v), .startsWith(v), .endsWith(v).
Anything else is a build error that names the line; a silent scan of rows in memory is a slow path nobody sees.
A table is live: the page carries data-sub, the page's live channel (one WebSocket at /_s) follows the table, and a change by anyone reaches
the page as the least that changed. The build puts a range (<!--g:ID-->…<!--/g:ID-->) around each part whose own code reads a table: a hole, an element with a reading
attribute, an if, a match, an each. Each poll of the channel (with the _s.* state of the page and its address) runs the page again for the
changed table and renders nothing but those ranges, so a count, a title and an empty note change with the row and nothing else is rendered or sent. An
each over a table whose body is one element that reads no other table is a list: only the rows of the changed keys are read (under the Gate, the
condition of the list and the state of the page), and the answer is those rows with their place and the keys that left. A list with a limit, or
whose rows read another table, is one range. The browser keeps the nodes that match (focus, selection, scroll) and changes the rest. A page that
cannot be told (the notice was lost, or a read failed) is fetched again.
GizDB is the default store; Pages.hosted(sys, db, space, shapes, actor) takes any Data database. Without one (Pages.standalone) a
table is empty and a write is refused.
tbl.grid() is the table as HTML: a <giz-grid> around a <table> with a head row of the field labels (data-field the name)
and a row for each row the person may read (data-k its key), with the text of each cell escaped. tbl.editableGrid() is the same
table whose cells of the fields that are not the key are bound controls, as value={t.field} in an each: a change updates that
field through the Gate, and a field that another person changed meanwhile is a conflict. Without script the editable grid is a form;
with the giz-grid enhancement (components) RevoGrid Pro shows the table (sort,
filter, range selection, copy, paste, fill, export), and an edit changes the cell's control. A page with a design writes its own markup.
A page follows its tables only for a client that runs the kernel (a browser: Sec-Fetch-Mode or x-giz on the request). A crawler, a
curl or a link preview gets the page without a watch, so it holds nothing in the database. An engine watch is a cursor of one reader, a
connection holds 128 of them and a view that is not polled keeps its own for the 60 s lease; a page that finds no free watch is served
static until it is loaded again. A watch shared by pages with one query needs a feed in Data and is not built.
tbl.where(row => cond, { order: "text desc", limit: 5 }) reads the first rows of that order (order is a literal of field names, limit a
value of the page). tbl.removeWhere(row => cond) deletes every row that the condition selects, as the person may, and stops at the first refusal.
<form create={tbl}> posts the fields named by its inputs (name="text" is the field text of T); the key is made by the page
and sorts by time.
A refused write keeps its own status: 422 for what the model refuses, 409 for a change that lost to another, 403 and 404 for the Gate. A read
that the database failed is not an empty list: the request answers 500 with the table and the reason. server-timing on each page answer
gives the time to render and the reads and writes it made, for the network panel of the browser. The live channel of a page tells a person of the
changes of rows that the person may read and of the removal of rows, and of nothing else (a row that the person may not read is never sent); it needs the tables of the host
(Pages.hosted(…, shapes, …)).
Tools for an agent. The tables of the host are also tools: GET /_tools lists list_T, get_T, add_T, update_T and remove_T for each
table T, each with a JSON Schema of its input from the same Shape (so a limit of a field is the same limit), and POST /_tools/NAME with a JSON
body runs one. A tool runs as the person of the request through the Gate, reads at most the rows a page reads, and refuses with the same
statuses and a JSON reason. A call must be application/json, so a form on another site cannot make it.
6. Style
<style> is scoped: the elements of the file get a class g-HASH, and the module exports the scoped CSS as style.
Tailwind v4. web/tailwind.giz runs the official standalone compiler (no Node) over the files of the site and writes the style sheet of only the
classes they use. The version is pinned in the module with the SHA-256 of each platform's executable: it is fetched once into
$GIZLANG_CACHE/tailwind/v4.3.3/, checked, and run from there (GIZLANG_TAILWIND names an executable of your own). The sheet is kept by the content of
the files, the tokens and the version, so an unchanged site never starts the compiler.
import {System} from "gizlang:system";
import * as Web from "gizweb";
import * as Pages from "gizweb:pages";
import * as Tailwind from "gizweb:tailwind";
import * as Tokens from "gizweb:tokens";
/** The pages of the site. Each server thread makes its own. */
function site(): Pages.Site { return Pages.site([Pages.markdown("/", "# Home")], [], []); }
/** Serves the site on port 8080, with the style sheet of the Tailwind classes that its files use. */
export function serve(sys: System, brand: string): i64 {
const tokens = Tokens.css(Web.uiTheme().concat([Tokens.Theme("brand", brand)]), true)!; // DTCG JSON -> @theme
const css = Tailwind.stylesheet(sys, ["pages"], tokens, "")!;
return Web.listen(sys, 8080, Pages.shell(css), Web.serving(), (own: System) => Pages.handler(site(), Pages.shell(css), Pages.standalone(own)));
}class="p-4 text-brand-500"andclass:font-bold={bold}both work: the compiler cannot see aclass:directive, so the module lists the names afterclass:in the.ghtmlfiles as@source inline(…).Pages.shell(css)puts the built-in design system in thecomponentslayer, so a utility on an element beats a component's default, and the scoped<style>of a component beats a utility.Pages.shell("")is the design system alone.dark:follows the colour scheme of the browser, anddata-theme="dark"or"light"on the root forces it.
Design tokens. web/tokens.giz reads tokens in the DTCG format and writes CSS custom properties: color.brand.500 is --color-brand-500, so under
Tailwind it gives bg-brand-500; an alias {color.brand.500} is var(--color-brand-500). With Tailwind the base theme is its @theme, else :root.
A second file replaces tokens: dark applies by the colour scheme and by data-theme="dark", any other name (a brand, a tenant) by
data-theme="NAME". A token that an overlay names and the base does not, a value of the wrong shape and JSON that does not parse are errors that name the
file and the token.
The default theme. web/theme/base.json and its dark.json overlay (Web.uiTheme()) are the tokens of the built-in components: colors
(bg, surface, hover, fg, muted, line, line-strong, accent, on-accent, link and the four states), font, text (12 to 28 px),
radius, shadow, spacing (4 px) and control.h (32 px). ui.css uses only these, so a site restyles every component with an overlay of a few
tokens and never copies a rule. Web.uiStyle() is the theme as :root and the components; with Tailwind, the theme goes into @theme and
Pages.shell(css) adds the components alone (Web.uiComponents()).
7. Pages: a folder of files is a site
A program imports site from the module .routes of a folder; the CLI writes that module from the files below the folder:
pages/
_layout.ghtml wraps every page below it
index.ghtml /
about.md /about (Markdown, in the layout)
raw.html /raw (sent as it is)
404.ghtml the page of an unknown address, status 404
blog/
_layout.ghtml wraps the pages of /blog, inside the root layout
index.ghtml /blog
[slug].ghtml /blog/{slug} (params.get("slug"))
Card.ghtml a component: no pageimport {System} from "gizlang:system";
import * as Web from "gizweb";
import * as Pages from "gizweb:pages";
import {site} from "./pages/.routes";
function main(sys: System): i64 {
return Web.listen(sys, 8080, Pages.shell(""), Web.serving(), (own: System) => Pages.handler(site(), Pages.shell(""), Pages.standalone(own)));
}A route file has params (the [name] values), request (the Http.Request) and me (the actor) as names; <slot/> in a layout is the page inside it. A page is the body of the page shell (Web.listen gives the design tokens, the kernel, ETags and HTTPS); the title is its first
<h1>, and the scoped styles of the page and its layouts come first. A page that starts with <!doctype is sent as it is. Two files with one address, or a folder
without a page, stop the build with a message that names the files. A name that starts with . or _ (except
_layout.ghtml), a capital letter first, and other file types are no page.
8. Compiled by the CLI
Every command that builds a program (run, live, check, build, test) reads .ghtml imports. A library (web/) may
hold .ghtml files and import them from each other.
9. Languages
A site in several languages is written in English, with a catalog locales/TAG.json beside its pages for each other language
(MULTILINGUAL-ARCHITECTURE). The text a person reads (a run of text, holes and inline elements, and
attributes such as title, alt, placeholder and aria-label) is a message keyed by itself in MessageFormat 2:
<p>Read the <a href="/terms">terms</a>, {user.name}.</p> → "Read the {#a}terms{/a}, {$name}."- A page has the ambient
locale(the request's language, aLocaleofgizlang:locale) andlanguages(the site's). A component translates its text when it declaresprop locale: Locale;, and a caller passes it:<Card {locale}/>. <template plural={n}>with<template case="one">,case="other"and exact counts (case="=0") is one message whose translation names the plural categories of its own language.translate="no"andpre,code,kbd,samp,varandtextareakeep their text.giz i18n pages --add kostarts a catalog,giz i18n pagesadds the new messages to each catalog, and--checkfails CI on a catalog that is behind. A translation that drops an element or names a missing placeholder stops the build at its line./ko/…is the Korean address of a page,?hl=kothe person's choice, and a browser opens its own language;<LanguagePicker {locale} {languages} path={request.path}/>links the languages. A file with no catalog compiles as it did.