Markup
ALX: markup that lowers to your UI library
UpdatedAn .alx file is luaux markup with Alloy inside the holes. Tags, attributes, fragments, and comments lower to the configured factory. The language server answers hover and completion for tags and attributes itself.
1local React = require(game:GetService("ReplicatedStorage").Packages.React) 2 3local function Row(props: { item: Item }) 4 return ( 5 <TextButton Size={UDim2.new(1, 0, 0, 24)}> 6 {props.item.name} x{props.item.count} 7 </TextButton> 8 ) 9end
1local React = require(game:GetService("ReplicatedStorage").Packages.React) 2 3local function Row(props: { item: Item }) 4 return ( 5 React.createElement("TextButton", { Size = UDim2.new(1, 0, 0, 24), 6 Text = `{props.item.name} x{props.item.count}` } 7 ) 8 ) 9end return nil
{if ... then <Tag/> else nil}and{match ...}work inside a hole, since Alloy syntax does.<> ... </>is a fragment;<!-- -->is a comment between tags.- The grammar colors an element from its head to its matching closing tag, and the semantic tokens of the lowered code stay out of the way.
In depth
An .alx file is Alloy with markup in it: tags, attributes, fragments, and text, the way JSX sits in TypeScript. The markup lowers to calls on a factory the project names, and the Alloy around it compiles as in any .aly.
local function Row(props: { item: Item, on_pick: () -> () }) return ( <TextButton Size={new UDim2(1, 0, 0, 24)} Activated={props.on_pick}> {props.item.name} x{props.item.count} </TextButton> ) end
A tag names a Roblox class or a component in scope; a component starts with a capital and is called with its props. The props are a plain table the lowering builds, so a component declares their type as a type or an interface; a struct is nominal and no tag satisfies it. An attribute is Name={expr} or Name="text"; an event name takes a function. An attribute value must fit the type of its property, or the type of the prop the component declares, and any other value is a type error at the attribute. A nil value leaves a property unset. With interpolate = "wrap", a Roblox tag's value can also be a source, so the check skips it. A bare {props} among the attributes spreads a table into them. {expr} in a body is a child, and text with {holes} interpolates. In a class with a Text property, such as TextLabel, the text between the tags sets Text, and so does a lone {expr}. When an attribute sets Text, each {expr} in the body is a child. With interpolate = "plain", the default of the element form, that value must be a string or a number, or a React binding of one, and any other value is a type error. An attribute of a Roblox tag takes a binding too, and the value the binding holds must fit the property. With "wrap" it can also be a source that a reactive library reads. <> ... </> is a fragment, <!-- --> a comment. A child that depends on a condition wraps in a function, {function() return if open then <Menu /> else nil end}, so a reactive library re-runs it; a bare conditional child is built once, and the static_conditional_child lint says so.
The [alx] table of alloy.toml sets the lowering. The table form, for Vide or Fusion:
[alx.factory] backend = "table" # table: create(name)(props); element: create(name, props, children) create = "create" # the function a tag calls; a bare name must be in scope children = "Children" # the key children go under; unset, they are numeric entries event = "OnEvent" # wraps an event name: OnEvent("Activated") compute = "computed" # wraps interpolated text; `use` names the reader inside it interpolate = "wrap" # wrap: text with holes is a function that reads them; plain: a string merge = "merge" # how a spread group `{props}` combines
The element form, for React, needs a fragment; the table form takes none, and a fragment there is a plain table:
[alx.factory] backend = "element" create = "React.createElement" fragment = "React.Fragment" event = "React.Event." # a trailing dot indexes: React.Event.Activated
Either form renames tags and properties:
[alx.elements] all = "camelCase" # a naming scheme for every class, or one alias per name [alx.properties] Text = "text" # an alias for a property, everywhere or per class
The level of a markup lint sits with the other lints, under alx.:
[lint.rules] alx.static_conditional_child = "warn" # allow, warn, deny
With no [alx], the lowering is the element form for React: a tag calls React.createElement, so React must be in scope, and a fragment is React.Fragment. An [alx.factory] table sets every name itself. [fmt.alx] sets how alloy fmt lays a tag out. An ingot may read a markup attribute, ClassName for Enamel, and rewrite the tag before the lowering.
In the editor a tag and an attribute hover and complete from the Roblox API, and the semantic tokens of the lowered code stay out of the markup.
