wml

package
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 11 Imported by: 0

README

wml

Load window layouts from XML at run time, like QML or XAML, for the wui Windows GUI library.

doc, err := wml.ParseFile("login.xml")      // 1. parse + validate (any OS, any goroutine)
var f form
view, err := doc.Build(&f, wml.Handlers{    // 2. create the controls (Windows, GUI thread)
    "doLogin": func() { /* ... */ },
})
f.Window.Show()                             // 3. show

Status

Part State
Parser, validator, schema, Marshal, wmlcheck done, unit tests and a fuzz test
Builder, handler binding, struct injection done, Windows only
Designer export/import of .xml not yet (the designer still saves JSON)
Hot reload, <Include>, XSD generation not yet

The code was written without access to a Go compiler. Run go vet ./wml/... and go test ./wml/... (on Windows to include the builder tests) before relying on it, and please report anything that does not compile.

Format

<wml version="1">
  <Window Name="main" Title="Demo" InnerSize="400,300">
    <Font Name="Tahoma" Height="-11"/>
    <Label Text="Name" Position="10,12"/>
    <EditLine Name="name" Bounds="70,10,200,22" HorizontalAnchor="MinAndMax"/>
    <ComboBox Name="lang" Bounds="70,40,120,22" SelectedIndex="0">
      <Item>English</Item>
      <Item>Bangla</Item>
    </ComboBox>
    <Panel Bounds="10,80,380,100" BorderStyle="Sunken">
      <Button Text="OK" Bounds="10,10,80,26" OnClick="doOk"/>
    </Panel>
  </Window>
</wml>
  • The root is <wml version="1"> and contains one or more <Window>.
  • The element name is the control type. Attributes are properties; each one maps to the setter of the same name (Text is SetText). wmlcheck -schema lists every element with its properties and ranges.
  • Name is the identifier used by View.Lookup, Inject and wml:"..." tags. Letters, digits and underscores, unique in the whole file.
  • On... attributes name a handler from wml.Handlers.
  • Only Window and Panel contain controls. A GroupBox only draws a frame: put the controls after it in the same parent.
  • <Font Name=".." Height=".." Bold="true"/> is allowed in any element.
  • Lists (ComboBox items, TabControl tabs) are <Item> / <Tab> children.
Kind Syntax
bool true or false
int 25, range checked per property
float 0.5 (NaN and Inf are rejected)
color #RRGGBB or #RGB
enum a name from the schema, case-insensitive (Alignment="Center")
composite Bounds="x,y,w,h", Position="x,y", Size="w,h", MinMax="min,max", ...

A composite cannot be combined with the properties it sets (Bounds and X in one element is an error). Properties are applied in the order defined by the schema, not the order in the file, so Min/Max always come before Value.

Errors

All problems are collected and reported together, each with file, line and column:

login.xml:21:34: unknown property "Txet" on Button; did you mean "Text"?
login.xml:25:9: <GroupBox> cannot contain <Label>; a GroupBox only draws a frame, ...

Parse and Build return wml.ErrorList for several problems (.Details() prints all of them) or a single *wml.Error.

Safety

  • XML is data. There is no code, expression or script in the format.
  • Limits (defaults: 4 MiB, depth 64, 10,000 elements, 64 KiB per string; change with WithLimits).
  • DOCTYPE, entities, processing instructions and namespaces are rejected.
  • Only the types and properties in the schema can be set. Unknown names are errors (or warnings with Lenient()), never ignored silently.
  • Numbers are range checked before they reach wui, and converted to the exact parameter type of the setter (an Alpha of 300 is rejected, not wrapped).
  • Handlers are type checked against the event's setter before anything is created, so a wrong handler cannot produce a half built window.
  • Build recovers panics and returns them as errors.
  • Version 1 has no file or image properties, so there is no path to escape. ParseFS only accepts valid io/fs paths.

API

Parse, ParseFile, ParseFS read and validate; options WithFileName, WithLimits, Lenient
(*Document).Validate, Marshal check a document built in code; write canonical XML
(*Document).Build(into, handlers) create windows and controls, fill into
(*Document).BuildWith(opts, into, handlers) same, with BuildOptions{AllowMissingHandlers}
(*View).Window, Lookup, Control, Inject access the result
Lookup, TypeNames, TypeSpec, PropSpec read the schema

Document is immutable after Parse, so you can parse once and Build many times (for example one window per call).

Tests

go test ./wml/...                                   # parser, validator, schema
go test -fuzz=FuzzParse -fuzztime=60s ./wml         # Go 1.18+
go test ./wml/...                                   # on Windows: also builds real controls

TestSchemaMatchesWui fails if a property or event in the schema does not exist on the wui type, so changes in wui cannot silently break layouts.

Documentation

Overview

Package wml loads window layouts from XML files at run time, similar to QML or XAML. A layout describes windows and controls of package wui; this package parses it, validates it against a schema and builds the real controls.

<wml version="1">
  <Window Name="main" Title="Login" InnerSize="400,300">
    <Label Text="User" Bounds="10,14,60,18"/>
    <EditLine Name="user" Bounds="80,10,300,22"/>
    <Button Name="ok" Text="Login" Bounds="300,260,90,26" OnClick="doLogin"/>
  </Window>
</wml>

The work happens in three steps:

  1. Parse reads the XML into a Document. It enforces size limits, rejects DOCTYPEs and namespaces, and tracks line and column of every element and attribute. Parse also validates, so a Document is always valid. This step needs no Windows API and is safe to call from any goroutine.
  2. Document.Build creates the wui controls, applies the properties in a fixed, schema-defined order, binds event handlers by name and fills a struct with the controls you want to use. Build must be called on the GUI thread, like every other wui call. It is only available on Windows.
  3. You call Show on the window.

A minimal program:

type form struct {
    Window *wui.Window   `wml:"main"`
    User   *wui.EditLine `wml:"user"`
}

func main() {
    doc, err := wml.ParseFile("login.xml")
    if err != nil { log.Fatal(err) }

    var f form
    _, err = doc.Build(&f, wml.Handlers{
        "doLogin": func() { println(f.User.Text()) },
    })
    if err != nil { log.Fatal(err) }
    f.Window.Show()
}

XML contains data only. It never contains code: events refer to Go functions by name, and every handler is type checked against the event it is bound to.

Index

Constants

View Source
const CurrentVersion = 1

CurrentVersion is the WML format version written by Marshal and the only version understood by this package.

Variables

View Source
var DefaultLimits = Limits{MaxBytes: 4 << 20, MaxDepth: 64, MaxNodes: 10000}

DefaultLimits are used when no WithLimits option is given.

Functions

func TypeNames

func TypeNames() []string

TypeNames returns all element type names in alphabetical order.

Types

type Attr

type Attr struct {
	Name  string
	Value string
	Line  int // position of the attribute name, 0 if unknown
	Col   int
}

Attr is one attribute of an element.

type Document

type Document struct {
	File     string   // file name used in error messages, may be empty
	Version  int      // format version, always CurrentVersion after Parse
	Windows  []*Node  // the <Window> elements in file order
	Warnings []*Error // only filled when parsing with Lenient()
}

Document is a parsed and validated WML file. A Document is immutable once it was returned by Parse: it can be shared between goroutines and built any number of times.

func NewDocument

func NewDocument(windows ...*Node) *Document

NewDocument creates a Document for programmatic construction. Call Validate before building it.

func Parse

func Parse(r io.Reader, opts ...ParseOption) (*Document, error)

Parse reads, checks and validates a WML document. It never panics on bad input. Problems are returned as an *Error or an ErrorList.

func ParseFS

func ParseFS(fsys fs.FS, name string, opts ...ParseOption) (*Document, error)

ParseFS parses the file name in fsys, for example an embed.FS. The name must be a valid io/fs path, so it cannot escape the file system.

func ParseFile

func ParseFile(path string, opts ...ParseOption) (*Document, error)

ParseFile parses the file at path. The path is used as the file name in error messages.

func (*Document) Marshal

func (d *Document) Marshal() []byte

Marshal returns the canonical XML form of the document: two space indentation, attributes in their original order and every special character escaped. Parsing the result gives an equal document, so Marshal is also what the designer and wmlcheck -fmt use to write files.

func (*Document) Validate

func (d *Document) Validate() error

Validate checks the document against the schema. Parse already does this, so it is only needed for documents that were created or changed in code.

type Error

type Error struct {
	File string // file name given with WithFileName/ParseFile, may be empty
	Line int    // 1-based, 0 if unknown
	Col  int    // 1-based byte column, 0 if unknown
	Msg  string
}

Error is a problem found in a WML document. It always carries the position of the offending element or attribute so that messages look like

login.xml:12:9: unknown property "Txet" on Button; did you mean "Text"?

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type ErrorList

type ErrorList []*Error

ErrorList is returned by Parse, Validate and Build when more than one problem was found. All problems are reported at once, not just the first.

func (ErrorList) Details

func (l ErrorList) Details() string

Details returns every error on its own line.

func (ErrorList) Error

func (l ErrorList) Error() string

Error implements the error interface. It shows the first problem and how many more there are; use Details to get all of them.

type Kind

type Kind int

Kind is the value kind of a property.

const (
	KindString Kind = iota
	KindBool
	KindInt
	KindFloat
	KindColor
	KindEnum
	KindInts
	KindList
	KindFloats
)

Property kinds. How each one is written in XML:

KindString  any text
KindBool    true | false
KindInt     a whole number, e.g. 25
KindFloat   a decimal number, e.g. 0.5
KindColor   #RRGGBB or #RGB
KindEnum    one of a fixed list of names, case-insensitive
KindInts    comma separated whole numbers, e.g. Bounds="10,10,100,25"
KindList    not an attribute: repeated child elements, e.g. <Item>
KindFloats  comma separated decimal numbers, e.g. MinMax="0,1.5"

func (Kind) String

func (k Kind) String() string

type Limits

type Limits struct {
	MaxBytes int // size of the whole file, default 4 MiB
	MaxDepth int // element nesting depth, default 64
	MaxNodes int // number of elements, default 10000
}

Limits bound the size of a document so that hostile or broken input cannot use unbounded memory or time. A zero field means "use the default".

type Node

type Node struct {
	Type     string
	Attrs    []Attr
	Children []*Node
	Text     string // trimmed character data, only used by list items
	Line     int    // position of the element's '<', 0 if unknown
	Col      int
}

Node is one element of a WML document. The element name is the control type ("Window", "Button", ...), attributes are properties and events, children are nested controls, Font and list items.

func (*Node) Add

func (n *Node) Add(child *Node) *Node

Add appends a child element and returns it.

func (*Node) Attr

func (n *Node) Attr(name string) (string, bool)

Attr returns the value of the named attribute.

func (*Node) SetAttr

func (n *Node) SetAttr(name, value string)

SetAttr sets an attribute, replacing an existing one with the same name.

type ParseOption

type ParseOption func(*parseConfig)

ParseOption changes how a document is parsed.

func Lenient

func Lenient() ParseOption

Lenient turns unknown properties into warnings (Document.Warnings) instead of errors. Everything else is still checked strictly.

func WithFileName

func WithFileName(name string) ParseOption

WithFileName sets the file name that is shown in error messages.

func WithLimits

func WithLimits(l Limits) ParseOption

WithLimits replaces the default limits. Zero fields keep their default.

type PropSpec

type PropSpec struct {
	Name       string
	Kind       Kind
	Setter     string   // method name on the wui type, default "Set"+Name
	Alt        string   // method name to try if Setter does not exist
	Enum       []string // KindEnum: the names, the index is the numeric value
	N          int      // KindInts: how many numbers
	Min, Max   int64    // KindInt and KindInts: inclusive range
	FMin, FMax float64  // KindFloat: inclusive range
	NonNegTail int      // KindInts: the last NonNegTail numbers must be >= 0
	Elem       string   // KindList: the child element name
	Covers     []string // properties that this one sets as well, they conflict
}

PropSpec describes one property of a control type.

func (PropSpec) Describe

func (p PropSpec) Describe() string

Describe returns a one-line description such as "Width int 0..32767".

type TypeSpec

type TypeSpec struct {
	Name      string
	Container bool       // may contain other controls (Window and Panel)
	Props     []PropSpec // in the order in which they are applied
	Events    []string
}

TypeSpec describes one element type.

func Lookup

func Lookup(typeName string) *TypeSpec

Lookup returns the schema of an element type, or nil.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL