527 lines
18 KiB
Go
527 lines
18 KiB
Go
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, "<", "<")
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
s = strings.ReplaceAll(s, "'", "'")
|
|
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(§ion, 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(§ion, 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
|
|
}
|