(* OpenUI document format — normative source of truth.
  JSON object member order is insignificant, and trailing commas are invalid. *)

openui_document = "{" document_field { "," document_field } "}" ;
document_field = root_id_field | version_field | type_field | attrs_field | children_field ;

(* A document has exactly one root_id_field, version_field, and type_field.
  It has at most one attrs_field and at most one children_field. *)

ui_element = "{" element_field { "," element_field } "}" ;
element_field = id_field | type_field | attrs_field | children_field ;

(* An element has exactly one id_field and type_field.
  It has at most one attrs_field and at most one children_field. *)

version_field = '"version"' ":" version_value ;
version_value = /"[0-9]+\.[0-9]+\.[0-9]+"/ ;

root_id_field = '"id"' ":" '"root"' ;
id_field = '"id"' ":" id_value ;
type_field = '"type"' ":" type_value ;
attrs_field = '"attrs"' ":" "{" [ attribute_list ] "}" ;
children_field = '"children"' ":" "[" [ ui_element_list ] "]" ;

(* ID: camelCase alphanumeric string *)
id_value = /"[a-z][A-Za-z0-9]*"/ ;

(* Type: html tags, kebab-case, or PascalCase *)
type_value = /"(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[A-Z][A-Za-z0-9]*(?:-[a-z][a-z0-9]*)?)"/ ;

(* Attributes: key-value pairs. The key prefix names the attribute
   category: "uses." (input), "produces." (output) or "behaves."
   (behavior). A key without a prefix carries no category. The name is
   camelCase alphanumeric. A value is a string, null, or a list of
   those. A string quoted inside the string is a literal; an unquoted
   string is a binding or target-language expression. null means the
   attribute is present without a value. *)
attribute_list = attribute_pair { "," attribute_pair } ;
attribute_pair = attr_key ":" attr_value ;
attr_key = /"(?:(?:uses|produces|behaves)\.)?[a-z][A-Za-z0-9]*"/ ;
attr_value = scalar_value | list_value ;
list_value = "[" [ scalar_value { "," scalar_value } ] "]" ;
scalar_value = string | null ;

ui_element_list = ui_element { "," ui_element } ;

string = /"(?:[^"\\]|\\["\\\/bfnrt]|\\u[0-9a-fA-F]{4})*"/ ;
null = "null" ;
