package renderer import ( "bytes" "fmt" "html/template" "sort" "strings" ) // A single template for the entire group structure const groupTemplateStr = `
{{if .Title.Text}} {{if .Title.Class}}
{{.Title.Text}}
{{else}}

{{.Title.Text}}

{{end}} {{end}}
{{.FieldsHTML}}
` // Templates for field rendering - now supports all HTML DOM elements var fieldTemplates = map[string]string{ // Form elements "input": `
{{.LabelHTML}}{{.ContentHTML}}
`, "textarea": `
{{.LabelHTML}}{{.ContentHTML}}
`, "select": `
{{.LabelHTML}}{{.ContentHTML}}
`, "button": ``, "option": ``, "optgroup": `{{.OptionsHTML}}`, "label": ``, "fieldset": `
{{.ContentHTML}}
`, "legend": `{{.Content}}`, "datalist": `{{.OptionsHTML}}`, "output": `{{.Content}}`, "progress": `{{.Content}}`, "meter": `{{.Content}}`, // Text content elements "h1": `

{{.Content}}

`, "h2": `

{{.Content}}

`, "h3": `

{{.Content}}

`, "h4": `

{{.Content}}

`, "h5": `
{{.Content}}
`, "h6": `
{{.Content}}
`, "p": `

{{.Content}}

`, "div": `
{{.ContentHTML}}
`, "span": `{{.Content}}`, "pre": `
{{.Content}}
`, "code": `{{.Content}}`, "blockquote": `
{{.ContentHTML}}
`, "cite": `{{.Content}}`, "strong": `{{.Content}}`, "em": `{{.Content}}`, "small": `{{.Content}}`, "mark": `{{.Content}}`, "del": `{{.Content}}`, "ins": `{{.Content}}`, "sub": `{{.Content}}`, "sup": `{{.Content}}`, "abbr": `{{.Content}}`, "address": `
{{.ContentHTML}}
`, "time": ``, // List elements "ul": ``, "ol": `
    {{.ContentHTML}}
`, "li": `
  • {{.Content}}
  • `, "dl": `
    {{.ContentHTML}}
    `, "dt": `
    {{.Content}}
    `, "dd": `
    {{.Content}}
    `, // Links and media "a": `{{.Content}}`, "img": ``, "figure": `
    {{.ContentHTML}}
    `, "figcaption": `
    {{.Content}}
    `, "audio": ``, "video": ``, "source": ``, "track": ``, // Table elements "table": `{{.ContentHTML}}
    `, "caption": `{{.Content}}`, "thead": `{{.ContentHTML}}`, "tbody": `{{.ContentHTML}}`, "tfoot": `{{.ContentHTML}}`, "tr": `{{.ContentHTML}}`, "th": `{{.Content}}`, "td": `{{.Content}}`, "colgroup": `{{.ContentHTML}}`, "col": ``, // Sectioning elements "article": `
    {{.ContentHTML}}
    `, "section": `
    {{.ContentHTML}}
    `, "nav": ``, "aside": ``, "header": `
    {{.ContentHTML}}
    `, "footer": ``, "main": `
    {{.ContentHTML}}
    `, // Interactive elements "details": `
    {{.ContentHTML}}
    `, "summary": `{{.Content}}`, "dialog": `{{.ContentHTML}}`, // Embedded content "iframe": ``, "embed": ``, "object": `{{.ContentHTML}}`, "param": ``, "picture": `{{.ContentHTML}}`, "canvas": `{{.Content}}`, "svg": `{{.ContentHTML}}`, // Meta elements "br": `
    `, "hr": `
    `, "wbr": ``, // Generic template for any unlisted element "generic": `<{{.Element}} {{.AllAttributes}}>{{.ContentHTML}}`, "void": `<{{.Element}} {{.AllAttributes}} />`, } // Void elements that don't have closing tags var voidElements = map[string]bool{ "area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true, "input": true, "link": true, "meta": true, "param": true, "source": true, "track": true, "wbr": true, } var standardAttrs = []string{ "id", "class", "name", "type", "value", "placeholder", "href", "src", "alt", "title", "target", "rel", "role", "tabindex", "accesskey", "contenteditable", "draggable", "hidden", "spellcheck", "translate", "autocomplete", "autofocus", "disabled", "readonly", "required", "multiple", "checked", "selected", "defer", "async", "loop", "muted", "controls", "autoplay", "preload", "poster", "width", "height", "rows", "cols", "size", "maxlength", "minlength", "min", "max", "step", "pattern", "accept", "capture", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "colspan", "rowspan", "headers", "scope", "start", "reversed", "datetime", "open", "label", "high", "low", "optimum", "span", } // FieldInfo represents metadata for a field extracted from JSONSchema type FieldInfo struct { Name string Order int Definition map[string]any } // GroupInfo represents metadata for a group extracted from JSONSchema type GroupInfo struct { Title GroupTitle Fields []FieldInfo GroupClass string } // GroupTitle represents the title configuration for a group type GroupTitle struct { Text string Class string } // JSONSchemaRenderer is responsible for rendering HTML fields based on JSONSchema type JSONSchemaRenderer struct { Schema map[string]any HTMLLayout string TemplateData map[string]any // Data for template interpolation cachedHTML string // Cached rendered HTML } // NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer func NewJSONSchemaRenderer(schema map[string]any, htmlLayout string) *JSONSchemaRenderer { return &JSONSchemaRenderer{ Schema: schema, HTMLLayout: htmlLayout, TemplateData: make(map[string]any), } } // SetTemplateData sets the data used for template interpolation func (r *JSONSchemaRenderer) SetTemplateData(data map[string]any) { r.TemplateData = data } // interpolateTemplate replaces template placeholders with actual values func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string) string { if len(r.TemplateData) == 0 { return templateStr } tmpl, err := template.New("interpolate").Parse(templateStr) if err != nil { // Fallback to simple string replacement if template parsing fails result := templateStr for key, value := range r.TemplateData { placeholder := fmt.Sprintf("{{%s}}", key) if valueStr, ok := value.(string); ok { result = fmt.Sprintf("%s", bytes.ReplaceAll([]byte(result), []byte(placeholder), []byte(valueStr))) } } return result } var templateResult bytes.Buffer err = tmpl.Execute(&templateResult, r.TemplateData) if err != nil { return templateStr // Return original string if execution fails } return templateResult.String() } // RenderFields generates HTML for fields based on the JSONSchema func (r *JSONSchemaRenderer) RenderFields() (string, error) { // Return cached HTML if available if r.cachedHTML != "" { return r.cachedHTML, nil } groups := parseGroupsFromSchema(r.Schema) var groupHTML bytes.Buffer for _, group := range groups { groupHTML.WriteString(renderGroup(group)) } formConfig := r.Schema["form"].(map[string]any) formClass, _ := formConfig["class"].(string) formAction, _ := formConfig["action"].(string) formMethod, _ := formConfig["method"].(string) formEnctype, _ := formConfig["enctype"].(string) // Interpolate template data into form action formAction = r.interpolateTemplate(formAction) buttonsHTML := renderButtons(formConfig) // Create a new template with the layout and functions tmpl, err := template.New("layout").Funcs(template.FuncMap{ "form_groups": func() template.HTML { return template.HTML(groupHTML.String()) }, "form_buttons": func() template.HTML { return template.HTML(buttonsHTML) }, "form_attributes": func() template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`, formClass, formAction, formMethod, formEnctype)) }, }).Parse(r.HTMLLayout) if err != nil { return "", fmt.Errorf("failed to parse HTML layout: %w", err) } var renderedHTML bytes.Buffer if err := tmpl.Execute(&renderedHTML, nil); err != nil { return "", fmt.Errorf("failed to execute HTML template: %w", err) } r.cachedHTML = renderedHTML.String() return r.cachedHTML, nil } // parseGroupsFromSchema extracts and sorts groups and fields from schema func parseGroupsFromSchema(schema map[string]any) []GroupInfo { formConfig, ok := schema["form"].(map[string]any) if !ok { return nil } properties, ok := schema["properties"].(map[string]any) if !ok { return nil } var requiredFields map[string]bool = make(map[string]bool) if reqFields, ok := schema["required"].([]any); ok { for _, field := range reqFields { if fieldName, ok := field.(string); ok { requiredFields[fieldName] = true } } } var groups []GroupInfo for _, group := range formConfig["groups"].([]any) { groupMap := group.(map[string]any) var groupTitle GroupTitle if titleMap, ok := groupMap["title"].(map[string]any); ok { if text, ok := titleMap["text"].(string); ok { groupTitle.Text = text } if class, ok := titleMap["class"].(string); ok { groupTitle.Class = class } } groupClass, _ := groupMap["class"].(string) var fields []FieldInfo for _, fieldName := range groupMap["fields"].([]any) { fieldDef := properties[fieldName.(string)].(map[string]any) order := 0 if ord, exists := fieldDef["order"].(int); exists { order = ord } fieldDefCopy := make(map[string]any) for k, v := range fieldDef { fieldDefCopy[k] = v } fieldDefCopy["isRequired"] = requiredFields[fieldName.(string)] fields = append(fields, FieldInfo{ Name: fieldName.(string), Order: order, Definition: fieldDefCopy, }) } sort.Slice(fields, func(i, j int) bool { return fields[i].Order < fields[j].Order }) groups = append(groups, GroupInfo{ Title: groupTitle, Fields: fields, GroupClass: groupClass, }) } return groups } // renderGroup generates HTML for a single group func renderGroup(group GroupInfo) string { var groupHTML bytes.Buffer // Render fields var fieldsHTML bytes.Buffer for _, field := range group.Fields { fieldsHTML.WriteString(renderField(field)) } tmpl := template.Must(template.New("group").Parse(groupTemplateStr)) data := map[string]any{ "Title": group.Title, "GroupClass": group.GroupClass, "FieldsHTML": template.HTML(fieldsHTML.String()), } if group.GroupClass == "" { data["GroupClass"] = "form-group-fields" } if err := tmpl.Execute(&groupHTML, data); err != nil { return "" // Return empty string on error } return groupHTML.String() } func renderField(field FieldInfo) string { ui, ok := field.Definition["ui"].(map[string]any) if !ok { return "" } element, _ := ui["element"].(string) if element == "" { return "" } // Build all attributes allAttributes := buildAllAttributes(field, ui) // Get content content := getFieldContent(field.Definition, ui) contentHTML := getFieldContentHTML(field.Definition, ui) // Generate label if needed labelHTML := generateLabel(field, ui) data := map[string]any{ "Element": element, "AllAttributes": template.HTMLAttr(allAttributes), "Content": content, "ContentHTML": template.HTML(contentHTML), "LabelHTML": template.HTML(labelHTML), "Class": getUIValue(ui, "class"), "OptionsHTML": template.HTML(generateOptions(ui)), } // Use specific template if available, otherwise use generic var tmplStr string if template, exists := fieldTemplates[element]; exists { tmplStr = template } else if voidElements[element] { tmplStr = fieldTemplates["void"] } else { tmplStr = fieldTemplates["generic"] } tmpl := template.Must(template.New(element).Parse(tmplStr)) var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return "" } return buf.String() } func buildAllAttributes(field FieldInfo, ui map[string]any) string { var attributes []string // Add standard attributes from ui for _, attr := range standardAttrs { if value, exists := ui[attr]; exists { if attr == "class" && value == "" { continue // Skip empty class } attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value)) } } // Handle required field if isRequired, ok := field.Definition["isRequired"].(bool); ok && isRequired { if !contains(attributes, "required=") { attributes = append(attributes, `required="required"`) } } // Handle input type based on field type element, _ := ui["element"].(string) if element == "input" { if inputType := getInputType(field.Definition, ui); inputType != "" { if !contains(attributes, "type=") { attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType)) } } } // Add data-* and aria-* attributes for key, value := range ui { if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) } } // Add custom attributes from field definition (excluding known schema properties) excludeFields := map[string]bool{ "type": true, "title": true, "ui": true, "placeholder": true, "order": true, "isRequired": true, "content": true, "children": true, } for key, value := range field.Definition { if !excludeFields[key] && !strings.HasPrefix(key, "ui") { attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) } } return strings.Join(attributes, " ") } func getInputType(fieldDef map[string]any, ui map[string]any) string { // Check ui type first if uiType, ok := ui["type"].(string); ok { return uiType } // Map schema types to input types if fieldType, ok := fieldDef["type"].(string); ok { switch fieldType { case "email": return "email" case "password": return "password" case "number", "integer": return "number" case "boolean": return "checkbox" case "date": return "date" case "time": return "time" case "datetime": return "datetime-local" case "url": return "url" case "tel": return "tel" case "color": return "color" case "range": return "range" case "file": return "file" case "hidden": return "hidden" default: return "text" } } return "text" } func getFieldContent(fieldDef map[string]any, ui map[string]any) string { // Check for content in ui first if content, ok := ui["content"].(string); ok { return content } // Check for content in field definition if content, ok := fieldDef["content"].(string); ok { return content } // Use title as fallback for some elements if title, ok := fieldDef["title"].(string); ok { return title } return "" } func getFieldContentHTML(fieldDef map[string]any, ui map[string]any) string { // Check for HTML content in ui if contentHTML, ok := ui["contentHTML"].(string); ok { return contentHTML } // Check for children elements if children, ok := ui["children"].([]any); ok { return renderChildren(children) } return "" } func renderChildren(children []any) string { var result strings.Builder for _, child := range children { if childMap, ok := child.(map[string]any); ok { // Create a temporary field info for the child childField := FieldInfo{ Name: getMapValue(childMap, "name", ""), Definition: childMap, } result.WriteString(renderField(childField)) } } return result.String() } func generateLabel(field FieldInfo, ui map[string]any) string { // Check if label should be generated if showLabel, ok := ui["showLabel"].(bool); !showLabel && ok { return "" } title, _ := field.Definition["title"].(string) if title == "" { return "" } name := getUIValue(ui, "name") if name == "" { name = field.Name } // Check if field is required isRequired, _ := field.Definition["isRequired"].(bool) requiredSpan := "" if isRequired { requiredSpan = ` *` } return fmt.Sprintf(``, name, title, requiredSpan) } func generateOptions(ui map[string]any) string { options, ok := ui["options"].([]any) if !ok { return "" } var optionsHTML strings.Builder for _, option := range options { if optionMap, ok := option.(map[string]any); ok { // Complex option with attributes value := getMapValue(optionMap, "value", "") text := getMapValue(optionMap, "text", value) selected := "" if isSelected, ok := optionMap["selected"].(bool); ok && isSelected { selected = ` selected="selected"` } disabled := "" if isDisabled, ok := optionMap["disabled"].(bool); ok && isDisabled { disabled = ` disabled="disabled"` } optionsHTML.WriteString(fmt.Sprintf(``, value, selected, disabled, text)) } else { // Simple option (just value) optionsHTML.WriteString(fmt.Sprintf(``, option, option)) } } return optionsHTML.String() } func getUIValue(ui map[string]any, key string) string { if value, ok := ui[key].(string); ok { return value } return "" } func getMapValue(m map[string]any, key, defaultValue string) string { if value, ok := m[key].(string); ok { return value } return defaultValue } func contains(slice []string, substr string) bool { for _, item := range slice { if strings.Contains(item, substr) { return true } } return false } // renderButtons generates HTML for form buttons func renderButtons(formConfig map[string]any) string { var buttonsHTML bytes.Buffer if submitConfig, ok := formConfig["submit"].(map[string]any); ok { buttonHTML := renderButtonFromConfig(submitConfig, "submit") buttonsHTML.WriteString(buttonHTML) } if resetConfig, ok := formConfig["reset"].(map[string]any); ok { buttonHTML := renderButtonFromConfig(resetConfig, "reset") buttonsHTML.WriteString(buttonHTML) } // Support for additional custom buttons if buttons, ok := formConfig["buttons"].([]any); ok { for _, button := range buttons { if buttonMap, ok := button.(map[string]any); ok { buttonType := getMapValue(buttonMap, "type", "button") buttonHTML := renderButtonFromConfig(buttonMap, buttonType) buttonsHTML.WriteString(buttonHTML) } } } return buttonsHTML.String() } func renderButtonFromConfig(config map[string]any, defaultType string) string { var attributes []string buttonType := getMapValue(config, "type", defaultType) attributes = append(attributes, fmt.Sprintf(`type="%s"`, buttonType)) if class := getMapValue(config, "class", ""); class != "" { attributes = append(attributes, fmt.Sprintf(`class="%s"`, class)) } // Add other button attributes for key, value := range config { switch key { case "type", "class", "label", "content": continue // Already handled default: attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) } } content := getMapValue(config, "label", getMapValue(config, "content", "Button")) return fmt.Sprintf(``, strings.Join(attributes, " "), content) }