diff --git a/examples/lang/debug.go b/examples/lang/debug.go
index 4236175..3fe75fc 100644
--- a/examples/lang/debug.go
+++ b/examples/lang/debug.go
@@ -96,7 +96,7 @@ func main() {
for _, def := range ast.Definitions {
if def.Page != nil {
pageCount++
- totalContent := len(def.Page.Meta) + len(def.Page.Sections) + len(def.Page.Components)
+ totalContent := len(def.Page.Meta) + len(def.Page.Elements)
if totalContent > 0 {
fmt.Printf(" ✓ Page '%s' has %d content items (block syntax working)\n", def.Page.Name, totalContent)
}
@@ -110,9 +110,11 @@ func main() {
var totalSections, nestedSections int
for _, def := range ast.Definitions {
if def.Page != nil {
- totalSections += len(def.Page.Sections)
- for _, section := range def.Page.Sections {
- nestedSections += countNestedSections(section)
+ for _, element := range def.Page.Elements {
+ if element.Section != nil {
+ totalSections++
+ nestedSections += countNestedSections(*element.Section)
+ }
}
}
}
diff --git a/examples/react-app-generator/templates/page-admin.tmpl b/examples/react-app-generator/templates/page-admin.tmpl
index ae06d25..3e0837f 100644
--- a/examples/react-app-generator/templates/page-admin.tmpl
+++ b/examples/react-app-generator/templates/page-admin.tmpl
@@ -2,9 +2,9 @@ import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
{{$relativePrefix := relativePrefix .Page.Path}}{{range .AST.Definitions}}{{if .Entity}}import { {{.Entity.Name}} } from '{{$relativePrefix}}types/{{.Entity.Name}}';
{{end}}{{end}}
-{{range .Page.Sections}}import {{.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Name | title}}Section';
-{{end}}{{range .Page.Components}}import {{.Type | title}}Component from '{{$relativePrefix}}components/{{.Type | title}}Component';
-{{end}}
+{{range .Page.Elements}}{{if .Section}}import {{.Section.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Section.Name | title}}Section';
+{{end}}{{if .Component}}import {{.Component.Type | title}}Component from '{{$relativePrefix}}components/{{.Component.Type | title}}Component';
+{{end}}{{end}}
export default function {{.Page.Name}}Page() {
const navigate = useNavigate();
@@ -53,12 +53,13 @@ export default function {{.Page.Name}}Page() {
{{if .Page.Title}}{{.Page.Title | derefString}}{{else}}{{.Page.Name}}{{end}}
- {{range .Page.Sections}}
- <{{.Name | title}}Section />
+ {{range .Page.Elements}}
+ {{if .Section}}
+ <{{.Section.Name | title}}Section />
+ {{end}}
+ {{if .Component}}
+ <{{.Component.Type | title}}Component />
{{end}}
-
- {{range .Page.Components}}
- <{{.Type | title}}Component />
{{end}}
diff --git a/examples/react-app-generator/templates/page-public.tmpl b/examples/react-app-generator/templates/page-public.tmpl
index c747e0e..164731f 100644
--- a/examples/react-app-generator/templates/page-public.tmpl
+++ b/examples/react-app-generator/templates/page-public.tmpl
@@ -2,9 +2,9 @@ import React from 'react';
import { Link } from 'react-router-dom';
{{$relativePrefix := relativePrefix .Page.Path}}{{range .AST.Definitions}}{{if .Entity}}import { {{.Entity.Name}} } from '{{$relativePrefix}}types/{{.Entity.Name}}';
{{end}}{{end}}
-{{range .Page.Sections}}import {{.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Name | title}}Section';
-{{end}}{{range .Page.Components}}import {{.Type | title}}Component from '{{$relativePrefix}}components/{{.Type | title}}Component';
-{{end}}
+{{range .Page.Elements}}{{if .Section}}import {{.Section.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Section.Name | title}}Section';
+{{end}}{{if .Component}}import {{.Component.Type | title}}Component from '{{$relativePrefix}}components/{{.Component.Type | title}}Component';
+{{end}}{{end}}
export default function {{.Page.Name}}Page() {
return (
@@ -36,12 +36,13 @@ export default function {{.Page.Name}}Page() {
{{if .Page.Title}}{{.Page.Title | derefString}}{{else}}{{.Page.Name}}{{end}}
- {{range .Page.Sections}}
- <{{.Name | title}}Section />
+ {{range .Page.Elements}}
+ {{if .Section}}
+ <{{.Section.Name | title}}Section />
+ {{end}}
+ {{if .Component}}
+ <{{.Component.Type | title}}Component />
{{end}}
-
- {{range .Page.Components}}
- <{{.Type | title}}Component />
{{end}}
diff --git a/interpreter/html_interpreter.go b/interpreter/html_interpreter.go
index 418194d..3650897 100644
--- a/interpreter/html_interpreter.go
+++ b/interpreter/html_interpreter.go
@@ -117,22 +117,22 @@ func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
html.WriteString("
\n")
html.WriteString(fmt.Sprintf("
%s
\n", hi.escapeHTML(title)))
- // Generate sections
- for _, section := range page.Sections {
- sectionHTML, err := hi.generateSectionHTML(§ion, 2)
- if err != nil {
- return "", err
+ // Generate page elements
+ for _, element := range page.Elements {
+ if element.Section != nil {
+ sectionHTML, err := hi.generateSectionHTML(element.Section, 2)
+ if err != nil {
+ return "", err
+ }
+ html.WriteString(sectionHTML)
}
- html.WriteString(sectionHTML)
- }
-
- // Generate direct components
- for _, component := range page.Components {
- componentHTML, err := hi.generateComponentHTML(&component, 2)
- if err != nil {
- return "", err
+ if element.Component != nil {
+ componentHTML, err := hi.generateComponentHTML(element.Component, 2)
+ if err != nil {
+ return "", err
+ }
+ html.WriteString(componentHTML)
}
- html.WriteString(componentHTML)
}
html.WriteString(" \n")
diff --git a/lang/lang.go b/lang/lang.go
index f23c9fd..25b9d1a 100644
--- a/lang/lang.go
+++ b/lang/lang.go
@@ -122,15 +122,20 @@ type ResponseSpec struct {
// Page Enhanced Page definitions with unified section model
type Page struct {
- Name string `parser:"'page' @Ident"`
- Path string `parser:"'at' @String"`
- Layout string `parser:"'layout' @Ident"`
- Title *string `parser:"('title' @String)?"`
- Description *string `parser:"('desc' @String)?"`
- Auth bool `parser:"@'auth'?"`
- Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
- Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
- Components []Component `parser:"@@* '}')?"` // Direct components within the block
+ Name string `parser:"'page' @Ident"`
+ Path string `parser:"'at' @String"`
+ Layout string `parser:"'layout' @Ident"`
+ Title *string `parser:"('title' @String)?"`
+ Description *string `parser:"('desc' @String)?"`
+ Auth bool `parser:"@'auth'?"`
+ Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
+ Elements []PageElement `parser:"@@* '}')?"` // Unified elements allowing any order
+}
+
+// PageElement Unified element type for pages allowing sections and components in any order
+type PageElement struct {
+ Section *Section `parser:"@@"`
+ Component *Component `parser:"| @@"`
}
// MetaTag Meta tags for SEO
diff --git a/lang/parser_ui_advanced_test.go b/lang/parser_ui_advanced_test.go
index 9a2490b..42acb59 100644
--- a/lang/parser_ui_advanced_test.go
+++ b/lang/parser_ui_advanced_test.go
@@ -4,13 +4,121 @@ import (
"testing"
)
-func TestParseAdvancedUIFeatures(t *testing.T) {
+func TestParseAdvancedUIStructures(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
+ {
+ name: "complex form with validation and conditional fields",
+ input: `page Test at "/test" layout main {
+ component form for User {
+ field name type text required validate min_length "3"
+ field email type email required validate email
+ field account_type type select options ["personal", "business"] default "personal"
+ when account_type equals "business" {
+ field company_name type text required
+ field tax_id type text validate pattern "[0-9]{9}"
+ }
+ button submit label "Create Account" style "primary"
+ button cancel label "Cancel" style "secondary"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Elements: []PageElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ {Validation: &ComponentValidation{Type: "email"}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "account_type",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"personal", "business"}},
+ {Default: stringPtr("personal")},
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "account_type",
+ Operator: "equals",
+ Value: "business",
+ Fields: []ComponentField{
+ {
+ Name: "company_name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ {
+ Name: "tax_id",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Validation: &ComponentValidation{Type: "pattern", Value: stringPtr("[0-9]{9}")}},
+ },
+ },
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "submit",
+ Label: "Create Account",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "cancel",
+ Label: "Cancel",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
{
name: "complex conditional rendering with multiple operators",
input: `page Test at "/test" layout main {
@@ -42,86 +150,88 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "status",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"active", "inactive", "pending"}},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "status",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"active", "inactive", "pending"}},
+ },
},
},
- },
- {
- When: &WhenCondition{
- Field: "status",
- Operator: "equals",
- Value: "active",
- Fields: []ComponentField{
- {Name: "last_login", Type: "datetime"},
- {Name: "permissions", Type: "multiselect"},
- },
- Buttons: []ComponentButton{
- {
- Name: "deactivate",
- Label: "Deactivate User",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "warning"}},
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "equals",
+ Value: "active",
+ Fields: []ComponentField{
+ {Name: "last_login", Type: "datetime"},
+ {Name: "permissions", Type: "multiselect"},
+ },
+ Buttons: []ComponentButton{
+ {
+ Name: "deactivate",
+ Label: "Deactivate User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "warning"}},
+ },
},
},
},
},
- },
- {
- When: &WhenCondition{
- Field: "status",
- Operator: "not_equals",
- Value: "active",
- Fields: []ComponentField{
- {
- Name: "reason",
- Type: "textarea",
- Attributes: []ComponentFieldAttribute{
- {Placeholder: stringPtr("Reason for status")},
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "not_equals",
+ Value: "active",
+ Fields: []ComponentField{
+ {
+ Name: "reason",
+ Type: "textarea",
+ Attributes: []ComponentFieldAttribute{
+ {Placeholder: stringPtr("Reason for status")},
+ },
},
},
- },
- Buttons: []ComponentButton{
- {
- Name: "activate",
- Label: "Activate User",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "success"}},
+ Buttons: []ComponentButton{
+ {
+ Name: "activate",
+ Label: "Activate User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "success"}},
+ },
},
},
},
},
- },
- {
- When: &WhenCondition{
- Field: "status",
- Operator: "contains",
- Value: "pending",
- Fields: []ComponentField{
- {Name: "approval_date", Type: "date"},
- },
- Buttons: []ComponentButton{
- {
- Name: "approve",
- Label: "Approve",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "primary"}},
- },
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "contains",
+ Value: "pending",
+ Fields: []ComponentField{
+ {Name: "approval_date", Type: "date"},
},
- {
- Name: "reject",
- Label: "Reject",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "danger"}},
+ Buttons: []ComponentButton{
+ {
+ Name: "approve",
+ Label: "Approve",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
+ },
+ {
+ Name: "reject",
+ Label: "Reject",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ },
},
},
},
@@ -203,104 +313,106 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("Product"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Product Name")},
- {Placeholder: stringPtr("Enter product name")},
- {Required: true},
- {Default: stringPtr("New Product")},
- {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
- {Size: stringPtr("large")},
- {Display: stringPtr("block")},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Product"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Product Name")},
+ {Placeholder: stringPtr("Enter product name")},
+ {Required: true},
+ {Default: stringPtr("New Product")},
+ {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ {Size: stringPtr("large")},
+ {Display: stringPtr("block")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "price",
- Type: "number",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Price ($)")},
- {Format: stringPtr("currency")},
- {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
- {Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
+ {
+ Field: &ComponentField{
+ Name: "price",
+ Type: "number",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Price ($)")},
+ {Format: stringPtr("currency")},
+ {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
+ {Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "category",
- Type: "autocomplete",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Category")},
- {Placeholder: stringPtr("Start typing...")},
- {Relates: &FieldRelation{Type: "Category"}},
- {Searchable: true},
- {Source: stringPtr("categories/search")},
+ {
+ Field: &ComponentField{
+ Name: "category",
+ Type: "autocomplete",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Category")},
+ {Placeholder: stringPtr("Start typing...")},
+ {Relates: &FieldRelation{Type: "Category"}},
+ {Searchable: true},
+ {Source: stringPtr("categories/search")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "tags",
- Type: "multiselect",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Tags")},
- {Options: []string{"electronics", "clothing", "books", "home"}},
- {Source: stringPtr("tags/popular")},
+ {
+ Field: &ComponentField{
+ Name: "tags",
+ Type: "multiselect",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Tags")},
+ {Options: []string{"electronics", "clothing", "books", "home"}},
+ {Source: stringPtr("tags/popular")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "description",
- Type: "richtext",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Description")},
- {Rows: intPtr(10)},
- {Placeholder: stringPtr("Describe your product...")},
+ {
+ Field: &ComponentField{
+ Name: "description",
+ Type: "richtext",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Description")},
+ {Rows: intPtr(10)},
+ {Placeholder: stringPtr("Describe your product...")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "thumbnail",
- Type: "image",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Product Image")},
- {Accept: stringPtr("image/jpeg,image/png")},
- {Thumbnail: true},
+ {
+ Field: &ComponentField{
+ Name: "thumbnail",
+ Type: "image",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Product Image")},
+ {Accept: stringPtr("image/jpeg,image/png")},
+ {Thumbnail: true},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "featured",
- Type: "checkbox",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Featured Product")},
- {Default: stringPtr("false")},
- {Value: stringPtr("true")},
+ {
+ Field: &ComponentField{
+ Name: "featured",
+ Type: "checkbox",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Featured Product")},
+ {Default: stringPtr("false")},
+ {Value: stringPtr("true")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "availability",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Availability")},
- {Options: []string{"in_stock", "out_of_stock", "pre_order"}},
- {Default: stringPtr("in_stock")},
- {Sortable: true},
+ {
+ Field: &ComponentField{
+ Name: "availability",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Availability")},
+ {Options: []string{"in_stock", "out_of_stock", "pre_order"}},
+ {Default: stringPtr("in_stock")},
+ {Sortable: true},
+ },
},
},
},
@@ -332,76 +444,78 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("Order"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "status",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"draft", "submitted", "approved"}},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Order"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "status",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"draft", "submitted", "approved"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "save",
- Label: "Save Draft",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "secondary"}},
- {Icon: &ComponentButtonIcon{Value: "save"}},
- {Position: &ComponentButtonPosition{Value: "left"}},
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save Draft",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ {Icon: &ComponentButtonIcon{Value: "save"}},
+ {Position: &ComponentButtonPosition{Value: "left"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "submit",
- Label: "Submit Order",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "primary"}},
- {Icon: &ComponentButtonIcon{Value: "send"}},
- {Loading: &ComponentButtonLoading{Value: "Submitting..."}},
- {Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
+ {
+ Button: &ComponentButton{
+ Name: "submit",
+ Label: "Submit Order",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ {Icon: &ComponentButtonIcon{Value: "send"}},
+ {Loading: &ComponentButtonLoading{Value: "Submitting..."}},
+ {Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "approve",
- Label: "Approve",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "success"}},
- {Loading: &ComponentButtonLoading{Value: "Approving..."}},
- {Disabled: &ComponentButtonDisabled{Value: "status"}},
- {Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
- {Target: &ComponentButtonTarget{Value: "approval_modal"}},
- {Via: &ComponentButtonVia{Value: "api/orders/approve"}},
+ {
+ Button: &ComponentButton{
+ Name: "approve",
+ Label: "Approve",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "success"}},
+ {Loading: &ComponentButtonLoading{Value: "Approving..."}},
+ {Disabled: &ComponentButtonDisabled{Value: "status"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
+ {Target: &ComponentButtonTarget{Value: "approval_modal"}},
+ {Via: &ComponentButtonVia{Value: "api/orders/approve"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "reject",
- Label: "Reject",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "danger"}},
- {Icon: &ComponentButtonIcon{Value: "x"}},
- {Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
+ {
+ Button: &ComponentButton{
+ Name: "reject",
+ Label: "Reject",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ {Icon: &ComponentButtonIcon{Value: "x"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "print",
- Label: "Print",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "outline"}},
- {Icon: &ComponentButtonIcon{Value: "printer"}},
- {Position: &ComponentButtonPosition{Value: "right"}},
+ {
+ Button: &ComponentButton{
+ Name: "print",
+ Label: "Print",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "outline"}},
+ {Icon: &ComponentButtonIcon{Value: "printer"}},
+ {Position: &ComponentButtonPosition{Value: "right"}},
+ },
},
},
},
@@ -482,12 +596,12 @@ func TestParseFieldValidationTypes(t *testing.T) {
}
page := got.Definitions[0].Page
- if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
+ if len(page.Elements) != 1 || page.Elements[0].Component == nil || len(page.Elements[0].Component.Elements) != 1 {
t.Errorf("ParseInput() failed to parse component for validation %s", vt.validation)
return
}
- element := page.Components[0].Elements[0]
+ element := page.Elements[0].Component.Elements[0]
if element.Field == nil || len(element.Field.Attributes) != 1 {
t.Errorf("ParseInput() failed to parse field attributes for validation %s", vt.validation)
return
@@ -527,7 +641,7 @@ func TestParseConditionalOperators(t *testing.T) {
// Verify the when condition was parsed correctly
page := got.Definitions[0].Page
- component := page.Components[0]
+ component := page.Elements[0].Component
whenElement := component.Elements[1].When
if whenElement == nil || whenElement.Operator != op {
diff --git a/lang/parser_ui_component_test.go b/lang/parser_ui_component_test.go
index eaa9b77..98e43cc 100644
--- a/lang/parser_ui_component_test.go
+++ b/lang/parser_ui_component_test.go
@@ -23,10 +23,12 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "table",
- Entity: stringPtr("User"),
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("User"),
+ },
},
},
},
@@ -52,58 +54,60 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Full Name")},
- {Placeholder: stringPtr("Enter your name")},
- {Required: true},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Full Name")},
+ {Placeholder: stringPtr("Enter your name")},
+ {Required: true},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "email",
- Type: "email",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Email Address")},
- {Required: true},
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Email Address")},
+ {Required: true},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "bio",
- Type: "textarea",
- Attributes: []ComponentFieldAttribute{
- {Rows: intPtr(5)},
- {Placeholder: stringPtr("Tell us about yourself")},
+ {
+ Field: &ComponentField{
+ Name: "bio",
+ Type: "textarea",
+ Attributes: []ComponentFieldAttribute{
+ {Rows: intPtr(5)},
+ {Placeholder: stringPtr("Tell us about yourself")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "avatar",
- Type: "file",
- Attributes: []ComponentFieldAttribute{
- {Accept: stringPtr("image/*")},
+ {
+ Field: &ComponentField{
+ Name: "avatar",
+ Type: "file",
+ Attributes: []ComponentFieldAttribute{
+ {Accept: stringPtr("image/*")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "role",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"admin", "user", "guest"}},
- {Default: stringPtr("user")},
+ {
+ Field: &ComponentField{
+ Name: "role",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"admin", "user", "guest"}},
+ {Default: stringPtr("user")},
+ },
},
},
},
@@ -135,70 +139,72 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("Product"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Product"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "price",
- Type: "number",
- Attributes: []ComponentFieldAttribute{
- {Format: stringPtr("currency")},
- {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
+ {
+ Field: &ComponentField{
+ Name: "price",
+ Type: "number",
+ Attributes: []ComponentFieldAttribute{
+ {Format: stringPtr("currency")},
+ {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "category",
- Type: "autocomplete",
- Attributes: []ComponentFieldAttribute{
- {Relates: &FieldRelation{Type: "Category"}},
+ {
+ Field: &ComponentField{
+ Name: "category",
+ Type: "autocomplete",
+ Attributes: []ComponentFieldAttribute{
+ {Relates: &FieldRelation{Type: "Category"}},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "tags",
- Type: "multiselect",
- Attributes: []ComponentFieldAttribute{
- {Source: stringPtr("tags/popular")},
+ {
+ Field: &ComponentField{
+ Name: "tags",
+ Type: "multiselect",
+ Attributes: []ComponentFieldAttribute{
+ {Source: stringPtr("tags/popular")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "description",
- Type: "richtext",
- },
- },
- {
- Field: &ComponentField{
- Name: "featured",
- Type: "checkbox",
- Attributes: []ComponentFieldAttribute{
- {Default: stringPtr("false")},
+ {
+ Field: &ComponentField{
+ Name: "description",
+ Type: "richtext",
},
},
- },
- {
- Field: &ComponentField{
- Name: "thumbnail",
- Type: "image",
- Attributes: []ComponentFieldAttribute{
- {Thumbnail: true},
+ {
+ Field: &ComponentField{
+ Name: "featured",
+ Type: "checkbox",
+ Attributes: []ComponentFieldAttribute{
+ {Default: stringPtr("false")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "thumbnail",
+ Type: "image",
+ Attributes: []ComponentFieldAttribute{
+ {Thumbnail: true},
+ },
},
},
},
@@ -227,44 +233,46 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- },
- },
- {
- Button: &ComponentButton{
- Name: "save",
- Label: "Save User",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "primary"}},
- {Icon: &ComponentButtonIcon{Value: "save"}},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
},
},
- },
- {
- Button: &ComponentButton{
- Name: "cancel",
- Label: "Cancel",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "secondary"}},
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ {Icon: &ComponentButtonIcon{Value: "save"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "delete",
- Label: "Delete",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "danger"}},
- {Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
- {Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
+ {
+ Button: &ComponentButton{
+ Name: "cancel",
+ Label: "Cancel",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "delete",
+ Label: "Delete",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
+ {Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
+ },
},
},
},
@@ -298,55 +306,57 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "account_type",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"personal", "business"}},
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "account_type",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"personal", "business"}},
+ },
},
},
- },
- {
- When: &WhenCondition{
- Field: "account_type",
- Operator: "equals",
- Value: "business",
- Fields: []ComponentField{
- {
- Name: "company_name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
+ {
+ When: &WhenCondition{
+ Field: "account_type",
+ Operator: "equals",
+ Value: "business",
+ Fields: []ComponentField{
+ {
+ Name: "company_name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ {
+ Name: "tax_id",
+ Type: "text",
},
},
- {
- Name: "tax_id",
- Type: "text",
- },
- },
- Buttons: []ComponentButton{
- {
- Name: "verify_business",
- Label: "Verify Business",
+ Buttons: []ComponentButton{
+ {
+ Name: "verify_business",
+ Label: "Verify Business",
+ },
},
},
},
- },
- {
- When: &WhenCondition{
- Field: "account_type",
- Operator: "equals",
- Value: "personal",
- Fields: []ComponentField{
- {
- Name: "date_of_birth",
- Type: "date",
+ {
+ When: &WhenCondition{
+ Field: "account_type",
+ Operator: "equals",
+ Value: "personal",
+ Fields: []ComponentField{
+ {
+ Name: "date_of_birth",
+ Type: "date",
+ },
},
},
},
@@ -383,36 +393,38 @@ func TestParseComponentDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Components: []Component{
+ Elements: []PageElement{
{
- Type: "dashboard",
- Elements: []ComponentElement{
- {
- Section: &Section{
- Name: "stats",
- Type: stringPtr("container"),
- Class: stringPtr("stats-grid"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "metric",
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "total_users",
- Type: "display",
- Attributes: []ComponentFieldAttribute{
- {Value: stringPtr("1,234")},
+ Component: &Component{
+ Type: "dashboard",
+ Elements: []ComponentElement{
+ {
+ Section: &Section{
+ Name: "stats",
+ Type: stringPtr("container"),
+ Class: stringPtr("stats-grid"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "metric",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "total_users",
+ Type: "display",
+ Attributes: []ComponentFieldAttribute{
+ {Value: stringPtr("1,234")},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "revenue",
- Type: "display",
- Attributes: []ComponentFieldAttribute{
- {Format: stringPtr("currency")},
- {Value: stringPtr("45,678")},
+ {
+ Field: &ComponentField{
+ Name: "revenue",
+ Type: "display",
+ Attributes: []ComponentFieldAttribute{
+ {Format: stringPtr("currency")},
+ {Value: stringPtr("45,678")},
+ },
},
},
},
@@ -421,20 +433,20 @@ func TestParseComponentDefinitions(t *testing.T) {
},
},
},
- },
- {
- Section: &Section{
- Name: "charts",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "chart",
- Entity: stringPtr("Analytics"),
- Elements: []ComponentElement{
- {
- Attribute: &ComponentAttr{
- DataSource: stringPtr("analytics/monthly"),
+ {
+ Section: &Section{
+ Name: "charts",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "chart",
+ Entity: stringPtr("Analytics"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ DataSource: stringPtr("analytics/monthly"),
+ },
},
},
},
@@ -461,7 +473,7 @@ func TestParseComponentDefinitions(t *testing.T) {
return
}
if !astEqual(got, tt.want) {
- t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ t.Errorf("ParseInput() got = %+v, want %+v", got, tt.want)
}
})
}
@@ -495,14 +507,20 @@ func TestParseComponentFieldTypes(t *testing.T) {
}
page := got.Definitions[0].Page
- if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
+ if len(page.Elements) != 1 {
t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
return
}
- element := page.Components[0].Elements[0]
- if element.Field == nil || element.Field.Type != fieldType {
- t.Errorf("ParseInput() field type mismatch: got %v, want %s", element.Field, fieldType)
+ element := page.Elements[0]
+ if element.Component == nil || len(element.Component.Elements) != 1 {
+ t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
+ return
+ }
+
+ fieldElement := element.Component.Elements[0]
+ if fieldElement.Field == nil || fieldElement.Field.Type != fieldType {
+ t.Errorf("ParseInput() field type mismatch: got %v, want %s", fieldElement.Field, fieldType)
}
})
}
diff --git a/lang/parser_ui_page_test.go b/lang/parser_ui_page_test.go
index 83efbca..3d1d5e9 100644
--- a/lang/parser_ui_page_test.go
+++ b/lang/parser_ui_page_test.go
@@ -12,14 +12,14 @@ func TestParsePageDefinitions(t *testing.T) {
wantErr bool
}{
{
- name: "basic page with minimal fields",
- input: `page Dashboard at "/dashboard" layout main`,
+ name: "basic page definition",
+ input: `page Home at "/" layout main`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
- Name: "Dashboard",
- Path: "/dashboard",
+ Name: "Home",
+ Path: "/",
Layout: "main",
},
},
@@ -27,17 +27,17 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
- name: "page with all optional fields",
- input: `page UserProfile at "/profile" layout main title "User Profile" desc "Manage user profile settings" auth`,
+ name: "page with optional fields",
+ input: `page Settings at "/settings" layout main title "User Settings" desc "Manage your account settings" auth`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
- Name: "UserProfile",
- Path: "/profile",
+ Name: "Settings",
+ Path: "/settings",
Layout: "main",
- Title: stringPtr("User Profile"),
- Description: stringPtr("Manage user profile settings"),
+ Title: stringPtr("User Settings"),
+ Description: stringPtr("Manage your account settings"),
Auth: true,
},
},
@@ -46,20 +46,22 @@ func TestParsePageDefinitions(t *testing.T) {
},
{
name: "page with meta tags",
- input: `page HomePage at "/" layout main {
- meta description "Welcome to our application"
- meta keywords "app, dashboard, management"
+ input: `page Settings at "/settings" layout main {
+ meta description "Settings page description"
+ meta keywords "settings, user, account"
+ meta author "My App"
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
- Name: "HomePage",
- Path: "/",
+ Name: "Settings",
+ Path: "/settings",
Layout: "main",
Meta: []MetaTag{
- {Name: "description", Content: "Welcome to our application"},
- {Name: "keywords", Content: "app, dashboard, management"},
+ {Name: "description", Content: "Settings page description"},
+ {Name: "keywords", Content: "settings, user, account"},
+ {Name: "author", Content: "My App"},
},
},
},
@@ -67,19 +69,17 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
- name: "page with nested sections",
+ name: "page with sections",
input: `page Settings at "/settings" layout main {
section tabs type tab {
section profile label "Profile" active {
- component form for User {
- field name type text
- }
+ component form for User
}
-
section security label "Security" {
- component form for User {
- field password type password
- }
+ component form for Security
+ }
+ section notifications label "Notifications" {
+ component toggle for NotificationSettings
}
}
}`,
@@ -90,50 +90,50 @@ func TestParsePageDefinitions(t *testing.T) {
Name: "Settings",
Path: "/settings",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "tabs",
- Type: stringPtr("tab"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "profile",
- Label: stringPtr("Profile"),
- Active: true,
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- },
- },
+ Section: &Section{
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "profile",
+ Label: stringPtr("Profile"),
+ Active: true,
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
},
},
},
},
},
- },
- {
- Section: &Section{
- Name: "security",
- Label: stringPtr("Security"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "password",
- Type: "password",
- },
- },
+ {
+ Section: &Section{
+ Name: "security",
+ Label: stringPtr("Security"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Security"),
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "notifications",
+ Label: stringPtr("Notifications"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "toggle",
+ Entity: stringPtr("NotificationSettings"),
},
},
},
@@ -149,71 +149,124 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
- name: "page with modal and panel sections",
- input: `page ProductList at "/products" layout main {
- section main type container {
- component table for Product
+ name: "page with components",
+ input: `page Dashboard at "/dashboard" layout main {
+ component stats for Analytics {
+ field total_users type display
+ field revenue type display format "currency"
}
-
- section editModal type modal trigger "edit-product" {
- component form for Product {
- field name type text required
- button save label "Save Changes" style "primary"
- }
- }
-
- section filters type panel position "left" {
- component form {
- field category type select
- field price_range type range
- }
+ component chart for SalesData {
+ data from "analytics/sales"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
- Name: "ProductList",
- Path: "/products",
+ Name: "Dashboard",
+ Path: "/dashboard",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "main",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "table",
- Entity: stringPtr("Product"),
+ Component: &Component{
+ Type: "stats",
+ Entity: stringPtr("Analytics"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "total_users",
+ Type: "display",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "revenue",
+ Type: "display",
+ Attributes: []ComponentFieldAttribute{
+ {Format: stringPtr("currency")},
+ },
+ },
},
},
},
},
{
- Name: "editModal",
- Type: stringPtr("modal"),
- Trigger: stringPtr("edit-product"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("Product"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- },
- {
- Button: &ComponentButton{
- Name: "save",
- Label: "Save Changes",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "primary"}},
+ Component: &Component{
+ Type: "chart",
+ Entity: stringPtr("SalesData"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ DataSource: stringPtr("analytics/sales"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "page with mixed sections and components",
+ input: `page Home at "/" layout main {
+ component hero for Banner {
+ field title type display
+ field subtitle type display
+ }
+ section content type container {
+ component posts for Post {
+ fields [title, excerpt, date]
+ }
+ }
+ component newsletter for Subscription {
+ field email type email required
+ button subscribe label "Subscribe"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Home",
+ Path: "/",
+ Layout: "main",
+ Elements: []PageElement{
+ {
+ Component: &Component{
+ Type: "hero",
+ Entity: stringPtr("Banner"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "title",
+ Type: "display",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "subtitle",
+ Type: "display",
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "content",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "posts",
+ Entity: stringPtr("Post"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ Fields: []string{"title", "excerpt", "date"},
},
},
},
@@ -223,28 +276,25 @@ func TestParsePageDefinitions(t *testing.T) {
},
},
{
- Name: "filters",
- Type: stringPtr("panel"),
- Position: stringPtr("left"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "category",
- Type: "select",
- },
- },
- {
- Field: &ComponentField{
- Name: "price_range",
- Type: "range",
- },
+ Component: &Component{
+ Type: "newsletter",
+ Entity: stringPtr("Subscription"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
},
},
},
+ {
+ Button: &ComponentButton{
+ Name: "subscribe",
+ Label: "Subscribe",
+ },
+ },
},
},
},
@@ -264,7 +314,66 @@ func TestParsePageDefinitions(t *testing.T) {
return
}
if !astEqual(got, tt.want) {
- t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ t.Errorf("ParseInput() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParsePageLayouts(t *testing.T) {
+ layouts := []string{"main", "admin", "public", "auth", "minimal", "dashboard"}
+
+ for _, layout := range layouts {
+ t.Run("layout_"+layout, func(t *testing.T) {
+ input := `page Test at "/test" layout ` + layout
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for layout %s: %v", layout, err)
+ return
+ }
+
+ if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
+ t.Errorf("ParseInput() failed to parse page for layout %s", layout)
+ return
+ }
+
+ page := got.Definitions[0].Page
+ if page.Layout != layout {
+ t.Errorf("ParseInput() layout mismatch: got %s, want %s", page.Layout, layout)
+ }
+ })
+ }
+}
+
+func TestParsePagePaths(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ }{
+ {"root", "/"},
+ {"simple", "/about"},
+ {"nested", "/admin/users"},
+ {"deep_nested", "/api/v1/users/profile"},
+ {"with_params", "/users/:id"},
+ {"with_multiple_params", "/users/:userId/posts/:postId"},
+ {"with_query", "/search?q=:query"},
+ {"with_extension", "/api/users.json"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := `page Test at "` + tt.path + `" layout main`
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for path %s: %v", tt.path, err)
+ return
+ }
+
+ page := got.Definitions[0].Page
+ if page.Path != tt.path {
+ t.Errorf("ParseInput() path mismatch: got %s, want %s", page.Path, tt.path)
}
})
}
@@ -276,16 +385,26 @@ func TestParsePageErrors(t *testing.T) {
input string
}{
{
- name: "missing layout",
- input: `page Dashboard at "/dashboard"`,
+ name: "missing page name",
+ input: `page at "/" layout main`,
},
{
name: "missing path",
- input: `page Dashboard layout main`,
+ input: `page Test layout main`,
+ },
+ {
+ name: "missing layout",
+ input: `page Test at "/"`,
},
{
name: "invalid path format",
- input: `page Dashboard at dashboard layout main`,
+ input: `page Test at /invalid layout main`,
+ },
+ {
+ name: "unclosed page block",
+ input: `page Test at "/" layout main {
+ section test type container
+ `,
},
}
diff --git a/lang/parser_ui_section_test.go b/lang/parser_ui_section_test.go
index a2bbbc2..3e8a411 100644
--- a/lang/parser_ui_section_test.go
+++ b/lang/parser_ui_section_test.go
@@ -23,10 +23,12 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "main",
- Type: stringPtr("container"),
+ Section: &Section{
+ Name: "main",
+ Type: stringPtr("container"),
+ },
},
},
},
@@ -46,15 +48,57 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "sidebar",
- Type: stringPtr("panel"),
- Position: stringPtr("left"),
- Class: stringPtr("sidebar-nav"),
- Label: stringPtr("Navigation"),
- Trigger: stringPtr("toggle-sidebar"),
- Entity: stringPtr("User"),
+ Section: &Section{
+ Name: "sidebar",
+ Type: stringPtr("panel"),
+ Class: stringPtr("sidebar-nav"),
+ Label: stringPtr("Navigation"),
+ Trigger: stringPtr("toggle-sidebar"),
+ Position: stringPtr("left"),
+ Entity: stringPtr("User"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "sections with separate attributes",
+ input: `page Dashboard at "/dashboard" layout main {
+ section content type container {
+ data from "/api/data"
+ style "padding: 20px"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Dashboard",
+ Path: "/dashboard",
+ Layout: "main",
+ Elements: []PageElement{
+ {
+ Section: &Section{
+ Name: "content",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Attribute: &SectionAttribute{
+ DataSource: stringPtr("/api/data"),
+ },
+ },
+ {
+ Attribute: &SectionAttribute{
+ Style: stringPtr("padding: 20px"),
+ },
+ },
+ },
+ },
},
},
},
@@ -78,28 +122,30 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "tabs",
- Type: stringPtr("tab"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "overview",
- Label: stringPtr("Overview"),
- Active: true,
+ Section: &Section{
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "overview",
+ Label: stringPtr("Overview"),
+ Active: true,
+ },
},
- },
- {
- Section: &Section{
- Name: "details",
- Label: stringPtr("Details"),
+ {
+ Section: &Section{
+ Name: "details",
+ Label: stringPtr("Details"),
+ },
},
- },
- {
- Section: &Section{
- Name: "settings",
- Label: stringPtr("Settings"),
+ {
+ Section: &Section{
+ Name: "settings",
+ Label: stringPtr("Settings"),
+ },
},
},
},
@@ -129,50 +175,52 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "userModal",
- Type: stringPtr("modal"),
- Trigger: stringPtr("edit-user"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
+ Section: &Section{
+ Name: "userModal",
+ Type: stringPtr("modal"),
+ Trigger: stringPtr("edit-user"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
},
},
- },
- {
- Field: &ComponentField{
- Name: "email",
- Type: "email",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "save",
- Label: "Save Changes",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "primary"}},
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save Changes",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
},
},
- },
- {
- Button: &ComponentButton{
- Name: "cancel",
- Label: "Cancel",
- Attributes: []ComponentButtonAttr{
- {Style: &ComponentButtonStyle{Value: "secondary"}},
+ {
+ Button: &ComponentButton{
+ Name: "cancel",
+ Label: "Cancel",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ },
},
},
},
@@ -213,24 +261,26 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "masterDetail",
- Type: stringPtr("master"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "userList",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "table",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Attribute: &ComponentAttr{
- Fields: []string{"name", "email"},
+ Section: &Section{
+ Name: "masterDetail",
+ Type: stringPtr("master"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "userList",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ Fields: []string{"name", "email"},
+ },
},
},
},
@@ -238,35 +288,35 @@ func TestParseSectionDefinitions(t *testing.T) {
},
},
},
- },
- {
- Section: &Section{
- Name: "userDetail",
- Type: stringPtr("detail"),
- Trigger: stringPtr("user-selected"),
- Entity: stringPtr("User"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
+ {
+ Section: &Section{
+ Name: "userDetail",
+ Type: stringPtr("detail"),
+ Trigger: stringPtr("user-selected"),
+ Entity: stringPtr("User"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ },
},
- },
- {
- Field: &ComponentField{
- Name: "email",
- Type: "email",
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ },
},
- },
- {
- Field: &ComponentField{
- Name: "bio",
- Type: "textarea",
+ {
+ Field: &ComponentField{
+ Name: "bio",
+ Type: "textarea",
+ },
},
},
},
@@ -323,27 +373,29 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "mainLayout",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "header",
- Type: stringPtr("container"),
- Class: stringPtr("header"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "navbar",
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "search",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Placeholder: stringPtr("Search...")},
+ Section: &Section{
+ Name: "mainLayout",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "header",
+ Type: stringPtr("container"),
+ Class: stringPtr("header"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "navbar",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "search",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Placeholder: stringPtr("Search...")},
+ },
},
},
},
@@ -352,26 +404,26 @@ func TestParseSectionDefinitions(t *testing.T) {
},
},
},
- },
- {
- Section: &Section{
- Name: "content",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "sidebar",
- Type: stringPtr("panel"),
- Position: stringPtr("left"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "menu",
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "navigation",
- Type: "list",
+ {
+ Section: &Section{
+ Name: "content",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "sidebar",
+ Type: stringPtr("panel"),
+ Position: stringPtr("left"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "menu",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "navigation",
+ Type: "list",
+ },
},
},
},
@@ -379,31 +431,31 @@ func TestParseSectionDefinitions(t *testing.T) {
},
},
},
- },
- {
- Section: &Section{
- Name: "main",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "tabs",
- Type: stringPtr("tab"),
- Elements: []SectionElement{
- {
- Section: &Section{
- Name: "overview",
- Label: stringPtr("Overview"),
- Active: true,
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "dashboard",
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "stats",
- Type: "metric",
+ {
+ Section: &Section{
+ Name: "main",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "overview",
+ Label: stringPtr("Overview"),
+ Active: true,
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "dashboard",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "stats",
+ Type: "metric",
+ },
},
},
},
@@ -411,16 +463,16 @@ func TestParseSectionDefinitions(t *testing.T) {
},
},
},
- },
- {
- Section: &Section{
- Name: "reports",
- Label: stringPtr("Reports"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "table",
- Entity: stringPtr("Report"),
+ {
+ Section: &Section{
+ Name: "reports",
+ Label: stringPtr("Reports"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("Report"),
+ },
},
},
},
@@ -464,37 +516,39 @@ func TestParseSectionDefinitions(t *testing.T) {
Name: "Test",
Path: "/test",
Layout: "main",
- Sections: []Section{
+ Elements: []PageElement{
{
- Name: "adminPanel",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- When: &WhenCondition{
- Field: "user_role",
- Operator: "equals",
- Value: "admin",
- Sections: []Section{
- {
- Name: "userManagement",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "table",
- Entity: stringPtr("User"),
+ Section: &Section{
+ Name: "adminPanel",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ When: &WhenCondition{
+ Field: "user_role",
+ Operator: "equals",
+ Value: "admin",
+ Sections: []Section{
+ {
+ Name: "userManagement",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("User"),
+ },
},
},
},
- },
- {
- Name: "systemSettings",
- Type: stringPtr("container"),
- Elements: []SectionElement{
- {
- Component: &Component{
- Type: "form",
- Entity: stringPtr("Settings"),
+ {
+ Name: "systemSettings",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Settings"),
+ },
},
},
},
@@ -520,7 +574,7 @@ func TestParseSectionDefinitions(t *testing.T) {
return
}
if !astEqual(got, tt.want) {
- t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ t.Errorf("ParseInput() got = %+v, want %+v", got, tt.want)
}
})
}
@@ -549,12 +603,12 @@ func TestParseSectionTypes(t *testing.T) {
}
page := got.Definitions[0].Page
- if len(page.Sections) != 1 {
+ if len(page.Elements) != 1 || page.Elements[0].Section == nil {
t.Errorf("ParseInput() failed to parse section for type %s", sectionType)
return
}
- section := page.Sections[0]
+ section := page.Elements[0].Section
if section.Type == nil || *section.Type != sectionType {
t.Errorf("ParseInput() section type mismatch: got %v, want %s", section.Type, sectionType)
}
diff --git a/lang/test_ui_comparisons.go b/lang/test_ui_comparisons.go
index 88a191e..2c5b855 100644
--- a/lang/test_ui_comparisons.go
+++ b/lang/test_ui_comparisons.go
@@ -30,24 +30,13 @@ func pageEqual(got, want Page) bool {
}
}
- // Compare sections (unified model)
- if len(got.Sections) != len(want.Sections) {
+ // Compare elements (unified model)
+ if len(got.Elements) != len(want.Elements) {
return false
}
- for i, section := range got.Sections {
- if !sectionEqual(section, want.Sections[i]) {
- return false
- }
- }
-
- // Compare components
- if len(got.Components) != len(want.Components) {
- return false
- }
-
- for i, component := range got.Components {
- if !componentEqual(component, want.Components[i]) {
+ for i, element := range got.Elements {
+ if !pageElementEqual(element, want.Elements[i]) {
return false
}
}
@@ -55,6 +44,28 @@ func pageEqual(got, want Page) bool {
return true
}
+func pageElementEqual(got, want PageElement) bool {
+ // Both should have either a Section or Component, but not both
+ if (got.Section == nil) != (want.Section == nil) {
+ return false
+ }
+ if (got.Component == nil) != (want.Component == nil) {
+ return false
+ }
+
+ // Compare sections if present
+ if got.Section != nil && want.Section != nil {
+ return sectionEqual(*got.Section, *want.Section)
+ }
+
+ // Compare components if present
+ if got.Component != nil && want.Component != nil {
+ return componentEqual(*got.Component, *want.Component)
+ }
+
+ return false
+}
+
func metaTagEqual(got, want MetaTag) bool {
return got.Name == want.Name && got.Content == want.Content
}
diff --git a/temp_multi-output-template-plan.md b/temp_multi-output-template-plan.md
index 8b5c37e..7343e47 100644
--- a/temp_multi-output-template-plan.md
+++ b/temp_multi-output-template-plan.md
@@ -583,13 +583,19 @@ export default function {{.Page.Name}}Page() {
{/* Protected content */}
{{end}}
- {{range .Page.Sections}}
-
- {{if .Label}}{{.Label | derefString}}
{{end}}
- {{range .Components}}
- {/* Component: {{.Type}} */}
+ {{range .Page.Elements}}
+ {{if .Section}}
+
+ {{if .Section.Label}}{{.Section.Label | derefString}}
{{end}}
+ {{range .Section.Elements}}
+ {{if .Component}}
+ {/* Component: {{.Component.Type}} */}
+ {{end}}
{{end}}
+ {{else if .Component}}
+ {/* Component: {{.Component.Type}} */}
+ {{end}}
{{end}}