add an html interpreter

This commit is contained in:
2025-08-25 00:10:18 -06:00
parent e71b1c3a23
commit cf3ad736b7
5 changed files with 777 additions and 128 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/html-output/
/masonry.exe

File diff suppressed because one or more lines are too long

View File

@ -11,9 +11,15 @@ import (
vue_gen "masonry/vue-gen"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/alecthomas/participle/v2"
"masonry/interpreter"
"masonry/lang"
)
//go:embed templates/proto/application.proto.tmpl
@ -156,104 +162,143 @@ func generateCmd() *cli.Command {
return &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate code from proto files",
Usage: "Generate code from proto files or Masonry files",
Category: "generator",
Description: "This command will generate code from the proto files in the proto directory and place them in a language folder in the gen folder.",
Description: "This command will generate code from proto files or convert Masonry files to various formats.",
Subcommands: []*cli.Command{
{
Name: "proto",
Usage: "Generate code from proto files",
Action: func(c *cli.Context) error {
return generateProtoCode()
},
},
{
Name: "html",
Usage: "Generate HTML from Masonry files",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Usage: "Input Masonry file path",
Required: true,
Aliases: []string{"i"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output directory for generated HTML files",
Value: "./output",
Aliases: []string{"o"},
},
},
Action: func(c *cli.Context) error {
inputFile := c.String("input")
outputDir := c.String("output")
return generateHTML(inputFile, outputDir)
},
},
},
Action: func(c *cli.Context) error {
fmt.Println("Generating code...")
protocArgs := []string{
"-I",
".",
"--go_out",
"gen/go",
"--go-grpc_out",
"gen/go",
"--go-grpc_opt=require_unimplemented_servers=false",
"--gorm_out",
"gen/go",
"--grpc-gateway_out",
"gen/go",
"--grpc-gateway_opt",
"logtostderr=true",
"--openapiv2_out",
"gen/openapi",
"--openapiv2_opt",
"logtostderr=true",
"--proto_path=./proto_include",
"proto/*.proto",
}
// generate go code
cmd := exec.Command(
"protoc",
protocArgs...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
var buff bytes.Buffer
cmd.Stderr = &buff
fmt.Println(buff.String())
return fmt.Errorf("error generating go code | %w", err)
}
// Generate ts code
// if webapp folder is present, generate typescript code
if _, err := os.Stat("webapp"); err == nil {
err = os.Chdir("webapp")
if err != nil {
return fmt.Errorf("error changing directory to webapp | %w", err)
}
cmd = exec.Command("npx",
"openapi-typescript-codegen",
"--input",
"../gen/openapi/proto/service.swagger.json",
"--output",
"src/generated",
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
// make sure src/generated-sample-components exists
err = os.Mkdir("src/generated-sample-components", 0755)
if err != nil {
return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
// generate vue components
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
err = cmd.Run()
if err != nil {
return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
}
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
}
}
return nil
// Default action - generate proto code for backward compatibility
return generateProtoCode()
},
}
}
// generateProtoCode handles the original proto code generation logic
func generateProtoCode() error {
fmt.Println("Generating code...")
protocArgs := []string{
"-I",
".",
"--go_out",
"gen/go",
"--go-grpc_out",
"gen/go",
"--go-grpc_opt=require_unimplemented_servers=false",
"--gorm_out",
"gen/go",
"--grpc-gateway_out",
"gen/go",
"--grpc-gateway_opt",
"logtostderr=true",
"--openapiv2_out",
"gen/openapi",
"--openapiv2_opt",
"logtostderr=true",
"--proto_path=./proto_include",
"proto/*.proto",
}
// generate go code
cmd := exec.Command(
"protoc",
protocArgs...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
var buff bytes.Buffer
cmd.Stderr = &buff
fmt.Println(buff.String())
return fmt.Errorf("error generating go code | %w", err)
}
// Generate ts code
// if webapp folder is present, generate typescript code
if _, err := os.Stat("webapp"); err == nil {
err = os.Chdir("webapp")
if err != nil {
return fmt.Errorf("error changing directory to webapp | %w", err)
}
cmd = exec.Command("npx",
"openapi-typescript-codegen",
"--input",
"../gen/openapi/proto/service.swagger.json",
"--output",
"src/generated",
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
// make sure src/generated-sample-components exists
err = os.Mkdir("src/generated-sample-components", 0755)
if err != nil {
return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
// generate vue components
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
err = cmd.Run()
if err != nil {
return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
}
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
}
}
return nil
}
func webappCmd() *cli.Command {
return &cli.Command{
Name: "webapp",
@ -368,3 +413,54 @@ func vueGenCmd() *cli.Command {
},
}
}
// generateHTML parses a Masonry file and generates HTML output
func generateHTML(inputFile, outputDir string) error {
fmt.Printf("Generating HTML from %s to %s\n", inputFile, outputDir)
// Read the input file
content, err := os.ReadFile(inputFile)
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
// Create parser
parser, err := participle.Build[lang.AST]()
if err != nil {
return fmt.Errorf("failed to create parser: %w", err)
}
// Parse the Masonry file
ast, err := parser.ParseString(inputFile, string(content))
if err != nil {
return fmt.Errorf("failed to parse Masonry file: %w", err)
}
// Create HTML interpreter
htmlInterpreter := interpreter.NewHTMLInterpreter()
// Generate HTML
htmlFiles, err := htmlInterpreter.GenerateHTML(ast)
if err != nil {
return fmt.Errorf("failed to generate HTML: %w", err)
}
// Create output directory
err = os.MkdirAll(outputDir, 0755)
if err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Write HTML files
for filename, content := range htmlFiles {
outputPath := filepath.Join(outputDir, filename)
err = os.WriteFile(outputPath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
fmt.Printf("Generated: %s\n", outputPath)
}
fmt.Printf("Successfully generated %d HTML file(s)\n", len(htmlFiles))
return nil
}

View File

@ -0,0 +1,61 @@
// Sample Masonry application demonstrating the language features
entity User {
name: string required
email: string required unique
age: number
role: string default "user"
password: string required
}
page UserDashboard at "/dashboard" layout main title "User Dashboard" {
meta description "User management dashboard"
meta keywords "user, dashboard, management"
section header type container class "header" {
component nav {
field logo type text value "MyApp"
button profile label "Profile"
button logout label "Logout"
}
}
section content type tab {
section users label "Users" active {
component table for User {
field name type text label "Full Name" sortable searchable
field email type email label "Email Address" sortable
field role type select options ["admin", "user", "moderator"]
button edit label "Edit"
button delete label "Delete"
}
}
section settings label "Settings" {
component form for User {
field name type text label "Full Name" required placeholder "Enter your name"
field email type email label "Email" required placeholder "Enter your email"
field age type number label "Age" placeholder "Enter your age"
field role type select label "Role" options ["admin", "user", "moderator"] default "user"
field password type password label "Password" required
when role equals "admin" {
field permissions type select label "Admin Permissions" options ["read", "write", "delete"]
}
button save label "Save Changes"
button cancel label "Cancel"
}
}
section profile label "Profile" {
component form {
field avatar type file label "Profile Picture" accept ".jpg,.png"
field bio type textarea label "Biography" rows 4 placeholder "Tell us about yourself"
field theme type select label "Theme" options ["light", "dark"] default "light"
field notifications type checkbox label "Enable Notifications" default "true"
button update label "Update Profile"
}
}
}
}

View File

@ -0,0 +1,526 @@
package interpreter
import (
"fmt"
"masonry/lang"
"strings"
)
// HTMLInterpreter converts Masonry AST to HTML/JavaScript
type HTMLInterpreter struct {
entities map[string]*lang.Entity
pages map[string]*lang.Page
}
// NewHTMLInterpreter creates a new HTML interpreter
func NewHTMLInterpreter() *HTMLInterpreter {
return &HTMLInterpreter{
entities: make(map[string]*lang.Entity),
pages: make(map[string]*lang.Page),
}
}
// cleanString removes surrounding quotes from string literals
func (hi *HTMLInterpreter) cleanString(s string) string {
if len(s) >= 2 && ((s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'')) {
return s[1 : len(s)-1]
}
return s
}
// escapeHTML escapes HTML special characters
func (hi *HTMLInterpreter) escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
s = strings.ReplaceAll(s, "'", "&#39;")
return s
}
// GenerateHTML converts a Masonry AST to HTML output
func (hi *HTMLInterpreter) GenerateHTML(ast *lang.AST) (map[string]string, error) {
// First pass: collect entities and pages
for _, def := range ast.Definitions {
if def.Entity != nil {
hi.entities[def.Entity.Name] = def.Entity
}
if def.Page != nil {
hi.pages[def.Page.Name] = def.Page
}
}
// Second pass: generate HTML for each page
htmlFiles := make(map[string]string)
for pageName, page := range hi.pages {
html, err := hi.generatePageHTML(page)
if err != nil {
return nil, fmt.Errorf("error generating HTML for page %s: %w", pageName, err)
}
htmlFiles[fmt.Sprintf("%s.html", strings.ToLower(pageName))] = html
}
return htmlFiles, nil
}
// generatePageHTML creates HTML for a single page
func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
var html strings.Builder
// HTML document structure
html.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n")
html.WriteString(" <meta charset=\"UTF-8\">\n")
html.WriteString(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n")
// Page title
title := page.Name
if page.Title != nil {
title = hi.cleanString(*page.Title)
}
html.WriteString(fmt.Sprintf(" <title>%s</title>\n", hi.escapeHTML(title)))
// Meta tags
for _, meta := range page.Meta {
cleanName := hi.cleanString(meta.Name)
cleanContent := hi.cleanString(meta.Content)
html.WriteString(fmt.Sprintf(" <meta name=\"%s\" content=\"%s\">\n",
hi.escapeHTML(cleanName), hi.escapeHTML(cleanContent)))
}
// Basic CSS for styling
html.WriteString(" <style>\n")
html.WriteString(" body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }\n")
html.WriteString(" .container { max-width: 1200px; margin: 0 auto; }\n")
html.WriteString(" .section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }\n")
html.WriteString(" .tabs { border-bottom: 2px solid #ddd; margin-bottom: 20px; }\n")
html.WriteString(" .tab-button { padding: 10px 20px; border: none; background: none; cursor: pointer; }\n")
html.WriteString(" .tab-button.active { background: #007bff; color: white; }\n")
html.WriteString(" .tab-content { display: none; }\n")
html.WriteString(" .tab-content.active { display: block; }\n")
html.WriteString(" .form-group { margin: 15px 0; }\n")
html.WriteString(" .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }\n")
html.WriteString(" .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }\n")
html.WriteString(" .button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }\n")
html.WriteString(" .button-primary { background: #007bff; color: white; }\n")
html.WriteString(" .button-secondary { background: #6c757d; color: white; }\n")
html.WriteString(" .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }\n")
html.WriteString(" .modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; min-width: 400px; }\n")
html.WriteString(" </style>\n")
html.WriteString("</head>\n<body>\n")
// Page content
html.WriteString(" <div class=\"container\">\n")
html.WriteString(fmt.Sprintf(" <h1>%s</h1>\n", hi.escapeHTML(title)))
// Generate sections
for _, section := range page.Sections {
sectionHTML, err := hi.generateSectionHTML(&section, 2)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
}
// Generate direct components
for _, component := range page.Components {
componentHTML, err := hi.generateComponentHTML(&component, 2)
if err != nil {
return "", err
}
html.WriteString(componentHTML)
}
html.WriteString(" </div>\n")
// JavaScript for interactivity
html.WriteString(" <script>\n")
html.WriteString(" // Tab functionality\n")
html.WriteString(" function showTab(tabName) {\n")
html.WriteString(" const tabs = document.querySelectorAll('.tab-content');\n")
html.WriteString(" tabs.forEach(tab => tab.classList.remove('active'));\n")
html.WriteString(" const buttons = document.querySelectorAll('.tab-button');\n")
html.WriteString(" buttons.forEach(btn => btn.classList.remove('active'));\n")
html.WriteString(" document.getElementById(tabName).classList.add('active');\n")
html.WriteString(" event.target.classList.add('active');\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Modal functionality\n")
html.WriteString(" function showModal(modalId) {\n")
html.WriteString(" document.getElementById(modalId).style.display = 'block';\n")
html.WriteString(" }\n")
html.WriteString(" function hideModal(modalId) {\n")
html.WriteString(" document.getElementById(modalId).style.display = 'none';\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Form validation and submission\n")
html.WriteString(" function submitForm(formId) {\n")
html.WriteString(" const form = document.getElementById(formId);\n")
html.WriteString(" const formData = new FormData(form);\n")
html.WriteString(" console.log('Form submitted:', Object.fromEntries(formData));\n")
html.WriteString(" alert('Form submitted successfully!');\n")
html.WriteString(" }\n")
html.WriteString(" </script>\n")
html.WriteString("</body>\n</html>")
return html.String(), nil
}
// generateSectionHTML creates HTML for a section
func (hi *HTMLInterpreter) generateSectionHTML(section *lang.Section, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
sectionType := "container"
if section.Type != nil {
sectionType = *section.Type
}
switch sectionType {
case "tab":
html.WriteString(fmt.Sprintf("%s<div class=\"tabs\" id=\"%s\">\n", indentStr, section.Name))
// Generate tab buttons
for _, element := range section.Elements {
if element.Section != nil && element.Section.Label != nil {
activeClass := ""
if element.Section.Active {
activeClass = " active"
}
cleanLabel := hi.cleanString(*element.Section.Label)
html.WriteString(fmt.Sprintf("%s <button class=\"tab-button%s\" onclick=\"showTab('%s')\">%s</button>\n",
indentStr, activeClass, element.Section.Name, hi.escapeHTML(cleanLabel)))
}
}
// Generate tab content
for _, element := range section.Elements {
if element.Section != nil {
activeClass := ""
if element.Section.Active {
activeClass = " active"
}
html.WriteString(fmt.Sprintf("%s <div class=\"tab-content%s\" id=\"%s\">\n",
indentStr, activeClass, element.Section.Name))
sectionHTML, err := hi.generateSectionHTML(element.Section, indent+2)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
html.WriteString(fmt.Sprintf("%s </div>\n", indentStr))
}
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
case "modal":
html.WriteString(fmt.Sprintf("%s<div class=\"modal\" id=\"%s\">\n", indentStr, section.Name))
html.WriteString(fmt.Sprintf("%s <div class=\"modal-content\">\n", indentStr))
if section.Label != nil {
cleanLabel := hi.cleanString(*section.Label)
html.WriteString(fmt.Sprintf("%s <h3>%s</h3>\n", indentStr, hi.escapeHTML(cleanLabel)))
}
// Generate modal content
for _, element := range section.Elements {
elementHTML, err := hi.generateSectionElementHTML(&element, indent+2)
if err != nil {
return "", err
}
html.WriteString(elementHTML)
}
html.WriteString(fmt.Sprintf("%s <button class=\"button button-secondary\" onclick=\"hideModal('%s')\">Close</button>\n", indentStr, section.Name))
html.WriteString(fmt.Sprintf("%s </div>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
default: // container, panel, master, detail
cssClass := "section"
if section.Class != nil {
cssClass = hi.cleanString(*section.Class)
}
html.WriteString(fmt.Sprintf("%s<div class=\"%s\" id=\"%s\">\n", indentStr, hi.escapeHTML(cssClass), section.Name))
if section.Label != nil {
cleanLabel := hi.cleanString(*section.Label)
html.WriteString(fmt.Sprintf("%s <h3>%s</h3>\n", indentStr, hi.escapeHTML(cleanLabel)))
}
// Generate section content
for _, element := range section.Elements {
elementHTML, err := hi.generateSectionElementHTML(&element, indent+1)
if err != nil {
return "", err
}
html.WriteString(elementHTML)
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
}
return html.String(), nil
}
// generateSectionElementHTML creates HTML for section elements
func (hi *HTMLInterpreter) generateSectionElementHTML(element *lang.SectionElement, indent int) (string, error) {
if element.Component != nil {
return hi.generateComponentHTML(element.Component, indent)
}
if element.Section != nil {
return hi.generateSectionHTML(element.Section, indent)
}
if element.When != nil {
return hi.generateWhenConditionHTML(element.When, indent)
}
return "", nil
}
// generateComponentHTML creates HTML for a component
func (hi *HTMLInterpreter) generateComponentHTML(component *lang.Component, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
switch component.Type {
case "form":
formId := fmt.Sprintf("form_%s", component.Type)
if component.Entity != nil {
formId = fmt.Sprintf("form_%s", *component.Entity)
}
html.WriteString(fmt.Sprintf("%s<form id=\"%s\" onsubmit=\"event.preventDefault(); submitForm('%s');\">\n", indentStr, formId, formId))
// Generate form fields
for _, element := range component.Elements {
if element.Field != nil {
fieldHTML, err := hi.generateFieldHTML(element.Field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
if element.Button != nil {
buttonHTML, err := hi.generateButtonHTML(element.Button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
}
html.WriteString(fmt.Sprintf("%s <button type=\"submit\" class=\"button button-primary\">Submit</button>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</form>\n", indentStr))
case "table":
html.WriteString(fmt.Sprintf("%s<table class=\"table\">\n", indentStr))
html.WriteString(fmt.Sprintf("%s <thead><tr>\n", indentStr))
// Generate table headers from fields
for _, element := range component.Elements {
if element.Field != nil {
label := element.Field.Name
for _, attr := range element.Field.Attributes {
if attr.Label != nil {
label = hi.cleanString(*attr.Label)
break
}
}
html.WriteString(fmt.Sprintf("%s <th>%s</th>\n", indentStr, hi.escapeHTML(label)))
}
}
html.WriteString(fmt.Sprintf("%s </tr></thead>\n", indentStr))
html.WriteString(fmt.Sprintf("%s <tbody>\n", indentStr))
html.WriteString(fmt.Sprintf("%s <tr><td colspan=\"100%%\">Data will be loaded here...</td></tr>\n", indentStr))
html.WriteString(fmt.Sprintf("%s </tbody>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</table>\n", indentStr))
default:
html.WriteString(fmt.Sprintf("%s<div class=\"component-%s\">\n", indentStr, component.Type))
// Generate component content
for _, element := range component.Elements {
if element.Field != nil {
fieldHTML, err := hi.generateFieldHTML(element.Field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
if element.Button != nil {
buttonHTML, err := hi.generateButtonHTML(element.Button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
}
return html.String(), nil
}
// generateFieldHTML creates HTML for a field
func (hi *HTMLInterpreter) generateFieldHTML(field *lang.ComponentField, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
// Get field attributes
var label, placeholder, defaultValue string
var required bool
var options []string
label = field.Name
for _, attr := range field.Attributes {
if attr.Label != nil {
label = hi.cleanString(*attr.Label)
}
if attr.Placeholder != nil {
placeholder = hi.cleanString(*attr.Placeholder)
}
if attr.Default != nil {
defaultValue = hi.cleanString(*attr.Default)
}
if attr.Required {
required = true
}
if attr.Options != nil {
// Clean each option in the array
options = make([]string, len(attr.Options))
for i, opt := range attr.Options {
options[i] = hi.cleanString(opt)
}
}
}
html.WriteString(fmt.Sprintf("%s<div class=\"form-group\">\n", indentStr))
requiredAttr := ""
if required {
requiredAttr = " required"
}
html.WriteString(fmt.Sprintf("%s <label for=\"%s\">%s</label>\n", indentStr, field.Name, hi.escapeHTML(label)))
switch field.Type {
case "text", "email", "password", "number", "tel", "url":
html.WriteString(fmt.Sprintf("%s <input type=\"%s\" id=\"%s\" name=\"%s\" placeholder=\"%s\" value=\"%s\"%s>\n",
indentStr, field.Type, field.Name, field.Name, hi.escapeHTML(placeholder), hi.escapeHTML(defaultValue), requiredAttr))
case "textarea":
html.WriteString(fmt.Sprintf("%s <textarea id=\"%s\" name=\"%s\" placeholder=\"%s\"%s>%s</textarea>\n",
indentStr, field.Name, field.Name, hi.escapeHTML(placeholder), requiredAttr, hi.escapeHTML(defaultValue)))
case "select":
html.WriteString(fmt.Sprintf("%s <select id=\"%s\" name=\"%s\"%s>\n", indentStr, field.Name, field.Name, requiredAttr))
if !required {
html.WriteString(fmt.Sprintf("%s <option value=\"\">Select an option</option>\n", indentStr))
}
for _, option := range options {
selected := ""
if option == defaultValue {
selected = " selected"
}
html.WriteString(fmt.Sprintf("%s <option value=\"%s\"%s>%s</option>\n",
indentStr, hi.escapeHTML(option), selected, hi.escapeHTML(option)))
}
html.WriteString(fmt.Sprintf("%s </select>\n", indentStr))
case "checkbox":
checked := ""
if defaultValue == "true" {
checked = " checked"
}
html.WriteString(fmt.Sprintf("%s <input type=\"checkbox\" id=\"%s\" name=\"%s\" value=\"true\"%s%s>\n",
indentStr, field.Name, field.Name, checked, requiredAttr))
case "file":
html.WriteString(fmt.Sprintf("%s <input type=\"file\" id=\"%s\" name=\"%s\"%s>\n",
indentStr, field.Name, field.Name, requiredAttr))
default:
html.WriteString(fmt.Sprintf("%s <input type=\"text\" id=\"%s\" name=\"%s\" placeholder=\"%s\" value=\"%s\"%s>\n",
indentStr, field.Type, field.Name, field.Name, hi.escapeHTML(placeholder), hi.escapeHTML(defaultValue), requiredAttr))
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
return html.String(), nil
}
// generateButtonHTML creates HTML for a button
func (hi *HTMLInterpreter) generateButtonHTML(button *lang.ComponentButton, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
buttonClass := "button button-primary"
onclick := ""
// Process button attributes
for _, attr := range button.Attributes {
if attr.Style != nil {
// Handle button style
}
if attr.Target != nil {
// Handle button target/onclick
}
}
cleanLabel := hi.cleanString(button.Label)
html.WriteString(fmt.Sprintf("%s<button type=\"button\" class=\"%s\" onclick=\"%s\">%s</button>\n",
indentStr, buttonClass, onclick, hi.escapeHTML(cleanLabel)))
return html.String(), nil
}
// generateWhenConditionHTML creates HTML for conditional content
func (hi *HTMLInterpreter) generateWhenConditionHTML(when *lang.WhenCondition, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
// For now, we'll render the content as visible (real implementation would include JavaScript logic)
html.WriteString(fmt.Sprintf("%s<div class=\"conditional-content\" data-when-field=\"%s\" data-when-operator=\"%s\" data-when-value=\"%s\">\n",
indentStr, when.Field, when.Operator, when.Value))
// Generate conditional content
for _, field := range when.Fields {
fieldHTML, err := hi.generateFieldHTML(&field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
for _, section := range when.Sections {
sectionHTML, err := hi.generateSectionHTML(&section, indent+1)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
}
for _, component := range when.Components {
componentHTML, err := hi.generateComponentHTML(&component, indent+1)
if err != nil {
return "", err
}
html.WriteString(componentHTML)
}
for _, button := range when.Buttons {
buttonHTML, err := hi.generateButtonHTML(&button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
return html.String(), nil
}