lloyAlloy on GitHub
MarkupError
Docs

MarkupError

Updated

The rule behind this error is on the Markup page.

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:

alloy.tomlalloy.toml
[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:

alloy.tomlalloy.toml
[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:

alloy.tomlalloy.toml
[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.:

alloy.tomlalloy.toml
[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.