Compare commits

...

2 Commits

Author SHA1 Message Date
d36e1bfd86 add html and server interpreters
these are basic and lack most features.
the server seems to work the best.
the html on the other hand is really rough and doesn't seem to work yet.
but it does build the pages and they have all the shapes and sections we
wanted. More work to come. :)
2025-08-25 00:50:55 -06:00
cf3ad736b7 add an html interpreter 2025-08-25 00:10:18 -06:00
7 changed files with 1452 additions and 128 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/masonry.exe

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ func main() {
tailwindCmd(), tailwindCmd(),
setupCmd(), setupCmd(),
vueGenCmd(), vueGenCmd(),
serveCmd(), // New command for server interpreter
} }
app := &cli.App{ app := &cli.App{

View File

@ -11,9 +11,15 @@ import (
vue_gen "masonry/vue-gen" vue_gen "masonry/vue-gen"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"text/template" "text/template"
"github.com/alecthomas/participle/v2"
"masonry/interpreter"
"masonry/lang"
) )
//go:embed templates/proto/application.proto.tmpl //go:embed templates/proto/application.proto.tmpl
@ -156,10 +162,51 @@ func generateCmd() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "generate", Name: "generate",
Aliases: []string{"g"}, Aliases: []string{"g"},
Usage: "Generate code from proto files", Usage: "Generate code from proto files or Masonry files",
Category: "generator", 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 { 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 {
// 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...") fmt.Println("Generating code...")
protocArgs := []string{ protocArgs := []string{
@ -250,8 +297,6 @@ func generateCmd() *cli.Command {
} }
return nil return nil
},
}
} }
func webappCmd() *cli.Command { func webappCmd() *cli.Command {
@ -368,3 +413,162 @@ 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
}
func serveCmd() *cli.Command {
return &cli.Command{
Name: "serve",
Usage: "Generate and run a simple HTTP server from a Masonry file",
Description: "This command parses a Masonry file and generates a simple Go HTTP server with in-memory database.",
Category: "development",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Usage: "Path to the Masonry file to interpret",
Required: true,
Aliases: []string{"f"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output file for the generated server code",
Value: "server.go",
Aliases: []string{"o"},
},
&cli.BoolFlag{
Name: "run",
Usage: "Run the server after generating it",
Value: false,
Aliases: []string{"r"},
},
},
Action: func(c *cli.Context) error {
masonryFile := c.String("file")
outputFile := c.String("output")
shouldRun := c.Bool("run")
fmt.Printf("Parsing Masonry file: %s\n", masonryFile)
// Read the Masonry file
content, err := os.ReadFile(masonryFile)
if err != nil {
return fmt.Errorf("error reading Masonry file: %w", err)
}
// Parse the Masonry file
parser, err := participle.Build[lang.AST](
participle.Unquote("String"),
)
if err != nil {
return fmt.Errorf("error building parser: %w", err)
}
ast, err := parser.ParseString("", string(content))
if err != nil {
return fmt.Errorf("error parsing Masonry file: %w", err)
}
// Generate server code using the server interpreter
serverInterpreter := interpreter.NewServerInterpreter()
serverCode, err := serverInterpreter.Interpret(*ast)
if err != nil {
return fmt.Errorf("error interpreting Masonry file: %w", err)
}
// Write the generated server code to the output file
err = os.WriteFile(outputFile, []byte(serverCode), 0644)
if err != nil {
return fmt.Errorf("error writing server code to file: %w", err)
}
fmt.Printf("Server code generated successfully: %s\n", outputFile)
if shouldRun {
fmt.Println("Installing dependencies...")
// Initialize go module if it doesn't exist
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
cmd := exec.Command("go", "mod", "init", "masonry-server")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error initializing go module: %w", err)
}
}
// Install required dependencies
dependencies := []string{
"github.com/google/uuid",
"github.com/gorilla/mux",
}
for _, dep := range dependencies {
fmt.Printf("Installing %s...\n", dep)
cmd := exec.Command("go", "get", dep)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error installing dependency %s: %w", dep, err)
}
}
fmt.Printf("Running server from %s...\n", outputFile)
cmd := exec.Command("go", "run", outputFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
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,707 @@
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
server *lang.Server
}
// 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, pages, and server config
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
}
if def.Server != nil {
hi.server = def.Server
}
}
// 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")
// API Base URL configuration
apiBaseURL := "http://localhost:8080"
if hi.server != nil {
host := "localhost"
port := 8080
for _, setting := range hi.server.Settings {
if setting.Host != nil {
host = *setting.Host
}
if setting.Port != nil {
port = *setting.Port
}
}
apiBaseURL = fmt.Sprintf("http://%s:%d", host, port)
}
html.WriteString(fmt.Sprintf(" const API_BASE_URL = '%s';\n", apiBaseURL))
html.WriteString(" \n")
html.WriteString(" // API helper functions\n")
html.WriteString(" async function apiRequest(method, endpoint, data = null) {\n")
html.WriteString(" const config = {\n")
html.WriteString(" method: method,\n")
html.WriteString(" headers: {\n")
html.WriteString(" 'Content-Type': 'application/json',\n")
html.WriteString(" },\n")
html.WriteString(" };\n")
html.WriteString(" \n")
html.WriteString(" if (data) {\n")
html.WriteString(" config.body = JSON.stringify(data);\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" try {\n")
html.WriteString(" const response = await fetch(API_BASE_URL + endpoint, config);\n")
html.WriteString(" \n")
html.WriteString(" if (!response.ok) {\n")
html.WriteString(" throw new Error(`HTTP error! status: ${response.status}`);\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" if (response.status === 204) {\n")
html.WriteString(" return null; // No content\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" return await response.json();\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('API request failed:', error);\n")
html.WriteString(" alert('Error: ' + error.message);\n")
html.WriteString(" throw error;\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Entity-specific API functions\n")
// Generate API functions for each entity
for entityName := range hi.entities {
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
html.WriteString(fmt.Sprintf(" async function list%s() {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('GET', '/%s');\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function get%s(id) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('GET', '/%s/' + id);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function create%s(data) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('POST', '/%s', data);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function update%s(id, data) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('PUT', '/%s/' + id, data);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function delete%s(id) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('DELETE', '/%s/' + id);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \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(" // Enhanced form submission with API integration\n")
html.WriteString(" async function submitForm(formId, entityType = null) {\n")
html.WriteString(" const form = document.getElementById(formId);\n")
html.WriteString(" const formData = new FormData(form);\n")
html.WriteString(" const data = Object.fromEntries(formData);\n")
html.WriteString(" \n")
html.WriteString(" console.log('Form data:', data);\n")
html.WriteString(" \n")
html.WriteString(" if (entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const result = await window['create' + entityType](data);\n")
html.WriteString(" console.log('Created:', result);\n")
html.WriteString(" alert(entityType + ' created successfully!');\n")
html.WriteString(" form.reset();\n")
html.WriteString(" // Refresh any tables showing this entity type\n")
html.WriteString(" await refreshTables(entityType);\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Form submission failed:', error);\n")
html.WriteString(" }\n")
html.WriteString(" } else {\n")
html.WriteString(" alert('Form submitted successfully!');\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Load data into tables\n")
html.WriteString(" async function loadTableData(tableId, entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const data = await window['list' + entityType]();\n")
html.WriteString(" const table = document.getElementById(tableId);\n")
html.WriteString(" const tbody = table.querySelector('tbody');\n")
html.WriteString(" \n")
html.WriteString(" if (!data || data.length === 0) {\n")
html.WriteString(" tbody.innerHTML = '<tr><td colspan=\"100%\">No data found</td></tr>';\n")
html.WriteString(" return;\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" tbody.innerHTML = '';\n")
html.WriteString(" data.forEach(item => {\n")
html.WriteString(" const row = document.createElement('tr');\n")
html.WriteString(" const headers = table.querySelectorAll('thead th');\n")
html.WriteString(" \n")
html.WriteString(" headers.forEach((header, index) => {\n")
html.WriteString(" const cell = document.createElement('td');\n")
html.WriteString(" const fieldName = header.textContent.toLowerCase().replace(/\\s+/g, '_');\n")
html.WriteString(" cell.textContent = item[fieldName] || item[header.textContent.toLowerCase()] || '';\n")
html.WriteString(" row.appendChild(cell);\n")
html.WriteString(" });\n")
html.WriteString(" \n")
html.WriteString(" // Add action buttons\n")
html.WriteString(" const actionCell = document.createElement('td');\n")
html.WriteString(" actionCell.innerHTML = `\n")
html.WriteString(" <button onclick=\"editItem('${item.id}', '${entityType}')\" class=\"button button-primary\">Edit</button>\n")
html.WriteString(" <button onclick=\"deleteItem('${item.id}', '${entityType}')\" class=\"button button-secondary\">Delete</button>\n")
html.WriteString(" `;\n")
html.WriteString(" row.appendChild(actionCell);\n")
html.WriteString(" \n")
html.WriteString(" tbody.appendChild(row);\n")
html.WriteString(" });\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to load table data:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Refresh all tables of a specific entity type\n")
html.WriteString(" async function refreshTables(entityType) {\n")
html.WriteString(" const tables = document.querySelectorAll(`table[data-entity=\"${entityType}\"]`);\n")
html.WriteString(" tables.forEach(table => {\n")
html.WriteString(" loadTableData(table.id, entityType);\n")
html.WriteString(" });\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Edit and delete functions\n")
html.WriteString(" async function editItem(id, entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const item = await window['get' + entityType](id);\n")
html.WriteString(" console.log('Edit item:', item);\n")
html.WriteString(" // TODO: Populate form with item data for editing\n")
html.WriteString(" alert('Edit functionality coming soon!');\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to fetch item for editing:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" async function deleteItem(id, entityType) {\n")
html.WriteString(" if (confirm('Are you sure you want to delete this item?')) {\n")
html.WriteString(" try {\n")
html.WriteString(" await window['delete' + entityType](id);\n")
html.WriteString(" alert('Item deleted successfully!');\n")
html.WriteString(" await refreshTables(entityType);\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to delete item:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Initialize page - load data when page loads\n")
html.WriteString(" document.addEventListener('DOMContentLoaded', function() {\n")
html.WriteString(" // Auto-load data for all tables with data-entity attribute\n")
html.WriteString(" const tables = document.querySelectorAll('table[data-entity]');\n")
html.WriteString(" tables.forEach(table => {\n")
html.WriteString(" const entityType = table.getAttribute('data-entity');\n")
html.WriteString(" loadTableData(table.id, entityType);\n")
html.WriteString(" });\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
}

View File

@ -0,0 +1,386 @@
package interpreter
import (
"fmt"
"go/format"
"masonry/lang"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// ServerInterpreter converts Masonry AST to a simple Go HTTP server
type ServerInterpreter struct {
entities map[string]*lang.Entity
endpoints map[string]*lang.Endpoint
server *lang.Server
}
// NewServerInterpreter creates a new server interpreter
func NewServerInterpreter() *ServerInterpreter {
return &ServerInterpreter{
entities: make(map[string]*lang.Entity),
endpoints: make(map[string]*lang.Endpoint),
}
}
// Interpret processes the AST and generates server code
func (si *ServerInterpreter) Interpret(ast lang.AST) (string, error) {
// First pass: collect all definitions
for _, def := range ast.Definitions {
if def.Server != nil {
si.server = def.Server
}
if def.Entity != nil {
si.entities[def.Entity.Name] = def.Entity
}
if def.Endpoint != nil {
key := fmt.Sprintf("%s_%s", def.Endpoint.Method, strings.ReplaceAll(def.Endpoint.Path, "/", "_"))
si.endpoints[key] = def.Endpoint
}
}
// Generate the server code
rawCode, err := si.generateServer()
if err != nil {
return "", err
}
// Format the code to remove unused imports and fix formatting
formattedCode, err := format.Source([]byte(rawCode))
if err != nil {
// If formatting fails, return the raw code with a warning comment
return "// Warning: Code formatting failed, but code should still be functional\n" + rawCode, nil
}
return string(formattedCode), nil
}
// generateServer creates the complete server code
func (si *ServerInterpreter) generateServer() (string, error) {
var code strings.Builder
// Package and imports - only include what's actually used
code.WriteString(`package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
`)
// Generate entity structs
code.WriteString("// Entity definitions\n")
for _, entity := range si.entities {
code.WriteString(si.generateEntityStruct(entity))
code.WriteString("\n")
}
// Generate in-memory database
code.WriteString(si.generateInMemoryDB())
// Generate handlers
code.WriteString("// HTTP Handlers\n")
for _, entity := range si.entities {
code.WriteString(si.generateEntityHandlers(entity))
}
// Generate custom endpoint handlers
for _, endpoint := range si.endpoints {
if endpoint.Entity == nil {
code.WriteString(si.generateCustomEndpointHandler(endpoint))
}
}
// Generate main function
code.WriteString(si.generateMainFunction())
return code.String(), nil
}
// generateEntityStruct creates a Go struct for an entity
func (si *ServerInterpreter) generateEntityStruct(entity *lang.Entity) string {
var code strings.Builder
if entity.Description != nil {
code.WriteString(fmt.Sprintf("// %s - %s\n", entity.Name, *entity.Description))
}
code.WriteString(fmt.Sprintf("type %s struct {\n", entity.Name))
// Always add ID field
code.WriteString("\tID string `json:\"id\"`\n")
code.WriteString("\tCreatedAt time.Time `json:\"created_at\"`\n")
code.WriteString("\tUpdatedAt time.Time `json:\"updated_at\"`\n")
for _, field := range entity.Fields {
goType := si.convertToGoType(field.Type)
jsonTag := strings.ToLower(field.Name)
if !field.Required {
goType = "*" + goType
}
code.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n",
cases.Title(language.English).String(field.Name), goType, jsonTag))
}
code.WriteString("}\n")
return code.String()
}
// convertToGoType maps Masonry types to Go types
func (si *ServerInterpreter) convertToGoType(masonryType string) string {
switch masonryType {
case "string", "text", "email", "url":
return "string"
case "int", "number":
return "int"
case "float":
return "float64"
case "boolean":
return "bool"
case "uuid":
return "string"
case "timestamp":
return "time.Time"
default:
return "string" // default fallback
}
}
// generateInMemoryDB creates the in-memory database structure
func (si *ServerInterpreter) generateInMemoryDB() string {
var code strings.Builder
code.WriteString("// In-memory database\n")
code.WriteString("type InMemoryDB struct {\n")
code.WriteString("\tmu sync.RWMutex\n")
for entityName := range si.entities {
code.WriteString(fmt.Sprintf("\t%s map[string]*%s\n",
strings.ToLower(entityName)+"s", entityName))
}
code.WriteString("}\n\n")
code.WriteString("var db = &InMemoryDB{\n")
for entityName := range si.entities {
code.WriteString(fmt.Sprintf("\t%s: make(map[string]*%s),\n",
strings.ToLower(entityName)+"s", entityName))
}
code.WriteString("}\n\n")
return code.String()
}
// generateEntityHandlers creates CRUD handlers for an entity
func (si *ServerInterpreter) generateEntityHandlers(entity *lang.Entity) string {
var code strings.Builder
entityName := entity.Name
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
// List handler
code.WriteString(fmt.Sprintf(`func list%sHandler(w http.ResponseWriter, r *http.Request) {
db.mu.RLock()
defer db.mu.RUnlock()
var items []*%s
for _, item := range db.%s {
items = append(items, item)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
`, entityName, entityName, entityPlural))
// Get by ID handler
code.WriteString(fmt.Sprintf(`func get%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.RLock()
item, exists := db.%s[id]
db.mu.RUnlock()
if !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
`, entityName, entityPlural, entityName))
// Create handler
code.WriteString(fmt.Sprintf(`func create%sHandler(w http.ResponseWriter, r *http.Request) {
var item %s
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
item.ID = uuid.New().String()
item.CreatedAt = time.Now()
item.UpdatedAt = time.Now()
db.mu.Lock()
db.%s[item.ID] = &item
db.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(item)
}
`, entityName, entityName, entityPlural))
// Update handler
code.WriteString(fmt.Sprintf(`func update%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.Lock()
defer db.mu.Unlock()
existing, exists := db.%s[id]
if !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
var updates %s
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Preserve ID and timestamps
updates.ID = existing.ID
updates.CreatedAt = existing.CreatedAt
updates.UpdatedAt = time.Now()
db.%s[id] = &updates
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updates)
}
`, entityName, entityPlural, entityName, entityName, entityPlural))
// Delete handler
code.WriteString(fmt.Sprintf(`func delete%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.Lock()
defer db.mu.Unlock()
if _, exists := db.%s[id]; !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
delete(db.%s, id)
w.WriteHeader(http.StatusNoContent)
}
`, entityName, entityPlural, entityName, entityPlural))
return code.String()
}
// generateCustomEndpointHandler creates handlers for custom endpoints
func (si *ServerInterpreter) generateCustomEndpointHandler(endpoint *lang.Endpoint) string {
var code strings.Builder
handlerName := fmt.Sprintf("%s%sHandler",
strings.ToLower(endpoint.Method),
strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", ""))
code.WriteString(fmt.Sprintf(`func %s(w http.ResponseWriter, r *http.Request) {
// Custom endpoint: %s %s
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Custom endpoint %s %s",
"status": "success",
})
}
`, handlerName, endpoint.Method, endpoint.Path, endpoint.Method, endpoint.Path))
return code.String()
}
// generateMainFunction creates the main function with routing
func (si *ServerInterpreter) generateMainFunction() string {
var code strings.Builder
code.WriteString("func main() {\n")
code.WriteString("\tr := mux.NewRouter()\n\n")
// Add routes for each entity
for entityName := range si.entities {
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
code.WriteString(fmt.Sprintf("\t// %s routes\n", entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", list%sHandler).Methods(\"GET\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", get%sHandler).Methods(\"GET\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", create%sHandler).Methods(\"POST\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", update%sHandler).Methods(\"PUT\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", delete%sHandler).Methods(\"DELETE\")\n",
entityPlural, entityName))
code.WriteString("\n")
}
// Add custom endpoint routes
for _, endpoint := range si.endpoints {
if endpoint.Entity == nil {
handlerName := fmt.Sprintf("%s%sHandler",
strings.ToLower(endpoint.Method),
strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", ""))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"%s\", %s).Methods(\"%s\")\n",
endpoint.Path, handlerName, endpoint.Method))
}
}
// Server configuration
host := "localhost"
port := 8080
if si.server != nil {
for _, setting := range si.server.Settings {
if setting.Host != nil {
host = *setting.Host
}
if setting.Port != nil {
port = *setting.Port
}
}
}
code.WriteString(fmt.Sprintf("\n\taddr := \"%s:%d\"\n", host, port))
code.WriteString("\tfmt.Printf(\"Server starting on %s\\n\", addr)\n")
code.WriteString("\tlog.Fatal(http.ListenAndServe(addr, r))\n")
code.WriteString("}\n")
return code.String()
}