package renderer import ( "bytes" "fmt" "html/template" "os" "sort" "strings" "sync" "github.com/oarkflow/jsonschema" ) // 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}}
`, "input_hidden": ``, // Special template for hidden inputs without wrapper "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, } // Template cache for compiled templates var ( compiledFieldTemplates = make(map[string]*template.Template) compiledGroupTemplate *template.Template templateCacheMutex sync.RWMutex ) // Initialize compiled templates once func init() { var err error compiledGroupTemplate, err = template.New("group").Parse(groupTemplateStr) if err != nil { panic(fmt.Sprintf("Failed to compile group template: %v", err)) } templateCacheMutex.Lock() defer templateCacheMutex.Unlock() for element, tmplStr := range fieldTemplates { compiled, err := template.New(element).Parse(tmplStr) if err == nil { compiledFieldTemplates[element] = compiled } } } 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 FieldPath string // Full path for nested fields (e.g., "user.address.street") Order int Schema *jsonschema.Schema IsRequired bool Validation ValidationInfo // Add validation information } // 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 *jsonschema.Schema HTMLLayout string compiledLayout *template.Template cachedGroups []GroupInfo cachedButtons string formConfig FormConfig cacheMutex sync.RWMutex } // FormConfig holds cached form configuration type FormConfig struct { Class string Action string Method string Enctype string } // NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSchemaRenderer { renderer := &JSONSchemaRenderer{ Schema: schema, HTMLLayout: htmlLayout, } // Pre-compile layout template renderer.compileLayoutTemplate() // Pre-parse and cache groups and form config renderer.precomputeStaticData() return renderer } // compileLayoutTemplate pre-compiles the layout template func (r *JSONSchemaRenderer) compileLayoutTemplate() { tmpl, err := template.New("layout").Funcs(template.FuncMap{ "form_groups": func(groupsHTML string) template.HTML { return template.HTML(groupsHTML) }, "form_buttons": func() template.HTML { return template.HTML(r.cachedButtons) }, "form_attributes": func(formAction string) template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`, r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype)) }, }).Parse(r.HTMLLayout) if err == nil { r.compiledLayout = tmpl } } // precomputeStaticData caches groups and form configuration func (r *JSONSchemaRenderer) precomputeStaticData() { r.cachedGroups = r.parseGroupsFromSchema() r.cachedButtons = r.renderButtons() // Cache form configuration if r.Schema.Form != nil { if class, ok := r.Schema.Form["class"].(string); ok { r.formConfig.Class = class } if action, ok := r.Schema.Form["action"].(string); ok { r.formConfig.Action = action } if method, ok := r.Schema.Form["method"].(string); ok { r.formConfig.Method = method } if enctype, ok := r.Schema.Form["enctype"].(string); ok { r.formConfig.Enctype = enctype } } } // interpolateTemplate replaces template placeholders with actual values func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[string]any) string { if len(data) == 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 data { placeholder := fmt.Sprintf("{{%s}}", key) if valueStr, ok := value.(string); ok { result = strings.ReplaceAll(result, placeholder, valueStr) } } return result } var templateResult bytes.Buffer err = tmpl.Execute(&templateResult, data) 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(data map[string]any) (string, error) { r.cacheMutex.RLock() defer r.cacheMutex.RUnlock() // Use a string builder for efficient string concatenation var groupHTML strings.Builder groupHTML.Grow(1024) // Pre-allocate reasonable capacity for _, group := range r.cachedGroups { groupHTML.WriteString(renderGroupWithData(group, data)) } // Interpolate dynamic form action formAction := r.interpolateTemplate(r.formConfig.Action, data) // Use pre-compiled template if available if r.compiledLayout != nil { var renderedHTML bytes.Buffer templateData := struct { GroupsHTML string FormAction string }{ GroupsHTML: groupHTML.String(), FormAction: formAction, } // Update the template functions with current data updatedTemplate := r.compiledLayout.Funcs(template.FuncMap{ "form_groups": func() template.HTML { return template.HTML(templateData.GroupsHTML) }, "form_buttons": func() template.HTML { return template.HTML(r.cachedButtons) }, "form_attributes": func() template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`, r.formConfig.Class, templateData.FormAction, r.formConfig.Method, r.formConfig.Enctype)) }, }) if err := updatedTemplate.Execute(&renderedHTML, nil); err != nil { return "", fmt.Errorf("failed to execute compiled template: %w", err) } return renderedHTML.String(), nil } // Fallback to original method if compilation failed return r.renderFieldsFallback(data) } // renderFieldsFallback provides fallback rendering when template compilation fails func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string, error) { var groupHTML strings.Builder for _, group := range r.cachedGroups { groupHTML.WriteString(renderGroupWithData(group, data)) } formAction := r.interpolateTemplate(r.formConfig.Action, data) // 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(r.cachedButtons) }, "form_attributes": func() template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`, r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype)) }, }).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) } return renderedHTML.String(), nil } // parseGroupsFromSchema extracts and sorts groups and fields from schema func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo { if r.Schema.Form == nil { return nil } groupsData, ok := r.Schema.Form["groups"] if !ok { return nil } groups, ok := groupsData.([]any) if !ok { return nil } var result []GroupInfo var groupedFields = make(map[string]bool) // Track fields that are already in groups for _, group := range groups { groupMap, ok := group.(map[string]any) if !ok { continue } 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) if groupClass == "" { groupClass = "form-group-fields" } var fields []FieldInfo if fieldsData, ok := groupMap["fields"].([]any); ok { for _, fieldName := range fieldsData { if fieldNameStr, ok := fieldName.(string); ok { // Handle nested field paths fieldInfos := r.extractFieldsFromPath(fieldNameStr, "") fields = append(fields, fieldInfos...) // Mark these fields as grouped for _, field := range fieldInfos { groupedFields[field.FieldPath] = true } } } } // Sort fields by order sort.Slice(fields, func(i, j int) bool { orderI := 0 orderJ := 0 if fields[i].Schema.Order != nil { orderI = *fields[i].Schema.Order } if fields[j].Schema.Order != nil { orderJ = *fields[j].Schema.Order } return orderI < orderJ }) result = append(result, GroupInfo{ Title: groupTitle, Fields: fields, GroupClass: groupClass, }) } // Add ungrouped hidden fields to the first group or create a hidden group if r.Schema.Properties != nil { var hiddenFields []FieldInfo for propName, propSchema := range *r.Schema.Properties { if !groupedFields[propName] { // Check if this is a hidden input if propSchema.UI != nil { if element, ok := propSchema.UI["element"].(string); ok && element == "input" { if inputType, ok := propSchema.UI["type"].(string); ok && inputType == "hidden" { // This is an ungrouped hidden field, add it validation := extractValidationInfo(propSchema, false) hiddenFields = append(hiddenFields, FieldInfo{ Name: propName, FieldPath: propName, Order: 0, Schema: propSchema, IsRequired: false, Validation: validation, }) } } } } } // If we have hidden fields, add them to the first group or create a new hidden group if len(hiddenFields) > 0 { if len(result) > 0 { // Prepend hidden fields to the first group result[0].Fields = append(hiddenFields, result[0].Fields...) } else { // Create a hidden group result = append(result, GroupInfo{ Title: GroupTitle{Text: "", Class: ""}, Fields: hiddenFields, GroupClass: "hidden-fields", }) } } } return result } // extractFieldsFromPath recursively extracts fields from a path, handling nested properties func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) []FieldInfo { var fields []FieldInfo // Build the full path fullPath := fieldPath if parentPath != "" { fullPath = parentPath + "." + fieldPath } // Navigate to the schema at this path schema := r.getSchemaAtPath(fieldPath) if schema == nil { return fields } // For nested paths like "company.address.street", we need to check if the final part // is required in its immediate parent's schema var fieldName string var parentSchemaPath string pathParts := strings.Split(fieldPath, ".") if len(pathParts) > 1 { // Extract the field name (last part) and parent path (all but last) fieldName = pathParts[len(pathParts)-1] parentSchemaPath = strings.Join(pathParts[:len(pathParts)-1], ".") } else { // Single level field fieldName = fieldPath parentSchemaPath = parentPath } // Check if this field is required at the parent level isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath) // If this schema has properties, it's a nested object if schema.Properties != nil && len(*schema.Properties) > 0 { // Recursively process nested properties for propName, propSchema := range *schema.Properties { nestedFields := r.extractFieldsFromNestedSchema(propName, fullPath, propSchema, isRequired) fields = append(fields, nestedFields...) } } else { // This is a leaf field order := 0 if schema.Order != nil { order = *schema.Order } fields = append(fields, FieldInfo{ Name: fieldName, FieldPath: fullPath, Order: order, Schema: schema, IsRequired: isRequired, Validation: extractValidationInfo(schema, isRequired), }) } return fields } // extractFieldsFromNestedSchema processes nested schema properties func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo { var fields []FieldInfo fullPath := propName if parentPath != "" { fullPath = parentPath + "." + propName } // Check if this field is required at its immediate parent level // The parent schema path is the current parentPath, not one level up isRequired := r.isFieldRequiredAtPath(propName, parentPath) // If this property has nested properties, recurse if propSchema.Properties != nil && len(*propSchema.Properties) > 0 { for nestedPropName, nestedPropSchema := range *propSchema.Properties { nestedFields := r.extractFieldsFromNestedSchema(nestedPropName, fullPath, nestedPropSchema, isRequired) fields = append(fields, nestedFields...) } } else { // This is a leaf field order := 0 if propSchema.Order != nil { order = *propSchema.Order } fields = append(fields, FieldInfo{ Name: propName, FieldPath: fullPath, Order: order, Schema: propSchema, IsRequired: isRequired, Validation: extractValidationInfo(propSchema, isRequired), }) } return fields } // getSchemaAtPath navigates to a schema at a given path func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema { if r.Schema.Properties == nil { return nil } parts := strings.Split(path, ".") currentSchema := r.Schema for _, part := range parts { if currentSchema.Properties == nil { return nil } if propSchema, exists := (*currentSchema.Properties)[part]; exists { currentSchema = propSchema } else { return nil } } return currentSchema } // isFieldRequiredAtPath checks if a field is required at a specific schema path func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool { var schema *jsonschema.Schema if schemaPath == "" { // Check at root level schema = r.Schema } else { // Navigate to the schema at the given path schema = r.getSchemaAtPath(schemaPath) } if schema == nil { return false } return contains(schema.Required, fieldName) } // 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 err := tmpl.Execute(&groupHTML, data); err != nil { return "" // Return empty string on error } return groupHTML.String() } // renderGroupWithData uses pre-compiled templates and string builder for better performance with data interpolation func renderGroupWithData(group GroupInfo, data map[string]any) string { // Collect all validations for dependency checking allValidations := make(map[string]ValidationInfo) for _, field := range group.Fields { allValidations[field.FieldPath] = field.Validation } // Use string builder for better performance var fieldsHTML strings.Builder fieldsHTML.Grow(512) // Pre-allocate reasonable capacity for _, field := range group.Fields { fieldsHTML.WriteString(renderFieldWithContextAndData(field, allValidations, data)) } // Use pre-compiled group template templateCacheMutex.RLock() groupTemplate := compiledGroupTemplate templateCacheMutex.RUnlock() if groupTemplate != nil { var groupHTML bytes.Buffer templateData := map[string]any{ "Title": group.Title, "GroupClass": group.GroupClass, "FieldsHTML": template.HTML(fieldsHTML.String()), } if err := groupTemplate.Execute(&groupHTML, templateData); err == nil { return groupHTML.String() } } // Fallback to original method return renderGroup(group) } func renderField(field FieldInfo) string { return renderFieldWithContext(field, make(map[string]ValidationInfo)) } // renderFieldWithContext renders a field with access to all field validations for dependency checking func renderFieldWithContext(field FieldInfo, allValidations map[string]ValidationInfo) string { return renderFieldWithContextAndData(field, allValidations, nil) } // renderFieldWithContextAndData renders a field with access to all field validations and template data for interpolation func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]ValidationInfo, data map[string]any) string { element := determineFieldElement(field.Schema) if element == "" { return "" } allAttributes := buildAttributesWithValidation(field, data) content := getFieldContent(field) contentHTML := getFieldContentHTML(field) var labelHTML string if element != "input" || getInputTypeFromSchema(field.Schema) != "hidden" { labelHTML = generateLabel(field) } optionsHTML := generateOptionsFromSchema(field.Schema) validationJS := generateClientSideValidation(field.FieldPath, field.Validation, allValidations) templateData := map[string]any{ "Element": element, "AllAttributes": template.HTMLAttr(allAttributes), "Content": content, "ContentHTML": template.HTML(contentHTML + validationJS), "LabelHTML": template.HTML(labelHTML), "Class": getFieldWrapperClass(field.Schema), "OptionsHTML": template.HTML(optionsHTML), } var tmplStr string if element == "input" && getInputTypeFromSchema(field.Schema) == "hidden" { if template, exists := fieldTemplates["input_hidden"]; exists { tmplStr = template } else { tmplStr = fieldTemplates["input"] } } else 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, templateData); err != nil { return "" } return buf.String() } // determineFieldElement determines the appropriate HTML element based on JSON Schema func determineFieldElement(schema *jsonschema.Schema) string { if schema.UI != nil { if element, ok := schema.UI["element"].(string); ok { return element } } if shouldUseSelect(schema) { return "select" } if shouldUseTextarea(schema) { return "textarea" } var typeStr string if len(schema.Type) > 0 { typeStr = schema.Type[0] } switch typeStr { case "boolean": return "input" // will be type="checkbox" case "array": return "input" // could be enhanced for array inputs case "object": return "fieldset" // for nested objects default: return "input" } } // buildAttributesWithValidation creates all HTML attributes including validation with data interpolation func buildAttributesWithValidation(field FieldInfo, data map[string]any) string { var builder strings.Builder builder.Grow(512) // Pre-allocate capacity // Check if UI specifies a custom name, otherwise use field path for nested fields var fieldName string if field.Schema.UI != nil { if customName, exists := field.Schema.UI["name"].(string); exists && customName != "" { fieldName = customName } } // Fallback to field path or field name if fieldName == "" { fieldName = field.FieldPath if fieldName == "" { fieldName = field.Name } } // Add name attribute builder.WriteString(`name="`) builder.WriteString(fieldName) builder.WriteString(`"`) // Add id attribute for accessibility fieldId := strings.ReplaceAll(fieldName, ".", "_") builder.WriteString(` id="`) builder.WriteString(fieldId) builder.WriteString(`"`) // Determine and add type attribute for input elements element := determineFieldElement(field.Schema) if element == "input" { inputType := getInputTypeFromSchema(field.Schema) builder.WriteString(` type="`) builder.WriteString(inputType) builder.WriteString(`"`) } // Add validation attributes validationAttrs := generateValidationAttributes(field.Validation) for _, attr := range validationAttrs { builder.WriteString(` `) builder.WriteString(attr) } // Add default value with interpolation defaultValue := getDefaultValue(field.Schema) if field.Schema.UI != nil { if uiValue, exists := field.Schema.UI["value"].(string); exists { // Interpolate template values in UI value if data != nil { defaultValue = interpolateString(uiValue, data) } else { defaultValue = uiValue } } } if defaultValue != "" { builder.WriteString(` value="`) builder.WriteString(defaultValue) builder.WriteString(`"`) } // Add placeholder with interpolation placeholder := getPlaceholder(field.Schema) if field.Schema.UI != nil { if uiPlaceholder, exists := field.Schema.UI["placeholder"].(string); exists { if data != nil { placeholder = interpolateString(uiPlaceholder, data) } else { placeholder = uiPlaceholder } } } if placeholder != "" { builder.WriteString(` placeholder="`) builder.WriteString(placeholder) builder.WriteString(`"`) } // Add standard attributes from UI with interpolation if field.Schema.UI != nil { for _, attr := range standardAttrs { if attr == "name" || attr == "id" || attr == "type" || attr == "required" || attr == "pattern" || attr == "min" || attr == "max" || attr == "minlength" || attr == "maxlength" || attr == "step" || attr == "value" || attr == "placeholder" { continue // Already handled above or in validation } if value, exists := field.Schema.UI[attr]; exists { if attr == "class" && value == "" { continue // Skip empty class } // Apply interpolation to string values if data is available valueStr := fmt.Sprintf("%v", value) if data != nil && strings.Contains(valueStr, "{{") { valueStr = interpolateString(valueStr, data) } builder.WriteString(` `) builder.WriteString(attr) builder.WriteString(`="`) builder.WriteString(valueStr) builder.WriteString(`"`) } } // Add data-* and aria-* attributes with interpolation for key, value := range field.Schema.UI { if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { valueStr := fmt.Sprintf("%v", value) if data != nil && strings.Contains(valueStr, "{{") { valueStr = interpolateString(valueStr, data) } builder.WriteString(` `) builder.WriteString(key) builder.WriteString(`="`) builder.WriteString(valueStr) builder.WriteString(`"`) } } } return builder.String() } // generateOptionsFromSchema generates option HTML from schema enum or UI options func generateOptionsFromSchema(schema *jsonschema.Schema) string { var optionsHTML strings.Builder // Check UI options first if schema.UI != nil { if options, ok := schema.UI["options"].([]any); ok { for _, option := range options { if optionMap, ok := option.(map[string]any); ok { 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 { optionsHTML.WriteString(fmt.Sprintf(``, option, option)) } } return optionsHTML.String() } } // Generate options from enum if len(schema.Enum) > 0 { for _, enumValue := range schema.Enum { optionsHTML.WriteString(fmt.Sprintf(``, enumValue, enumValue)) } } return optionsHTML.String() } // getFieldWrapperClass determines the CSS class for the field wrapper func getFieldWrapperClass(schema *jsonschema.Schema) string { if schema.UI != nil { if class, ok := schema.UI["class"].(string); ok { return class } } return "form-group" // default class } // getDefaultValue extracts default value from schema func getDefaultValue(schema *jsonschema.Schema) string { if schema.Default != nil { return fmt.Sprintf("%v", schema.Default) } return "" } // getPlaceholder extracts placeholder from schema func getPlaceholder(schema *jsonschema.Schema) string { if schema.UI != nil { if placeholder, ok := schema.UI["placeholder"].(string); ok { return placeholder } } // Auto-generate placeholder from title or description if schema.Title != nil { return fmt.Sprintf("Enter %s", strings.ToLower(*schema.Title)) } if schema.Description != nil { return *schema.Description } return "" } func getFieldContent(field FieldInfo) string { // Check for content in UI first if field.Schema.UI != nil { if content, ok := field.Schema.UI["value"].(string); ok { return content } if content, ok := field.Schema.UI["defaultValue"].(string); ok { return content } if content, ok := field.Schema.UI["content"].(string); ok { return content } } return "" } func getFieldContentHTML(field FieldInfo) string { // Check for HTML content in UI if field.Schema.UI != nil { if contentHTML, ok := field.Schema.UI["contentHTML"].(string); ok { return contentHTML } // Check for children elements if children, ok := field.Schema.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 childSchema := &jsonschema.Schema{ UI: childMap, } if title, ok := childMap["title"].(string); ok { childSchema.Title = &title } childField := FieldInfo{ Name: getMapValue(childMap, "name", ""), Schema: childSchema, } result.WriteString(renderField(childField)) } } return result.String() } func generateLabel(field FieldInfo) string { // Check if label should be generated if field.Schema.UI != nil { if showLabel, ok := field.Schema.UI["showLabel"].(bool); !showLabel && ok { return "" } } var title string if field.Schema.Title != nil { title = *field.Schema.Title } if title == "" { return "" } fieldName := field.FieldPath if fieldName == "" { fieldName = field.Name } // Check if field is required requiredSpan := "" if field.IsRequired { requiredSpan = ` *` } return fmt.Sprintf(``, fieldName, title, requiredSpan) } 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, item string) bool { for _, s := range slice { if s == item { return true } } return false } // renderButtons generates HTML for form buttons func (r *JSONSchemaRenderer) renderButtons() string { if r.Schema.Form == nil { return "" } var buttonsHTML bytes.Buffer if submitConfig, ok := r.Schema.Form["submit"].(map[string]any); ok { buttonHTML := renderButtonFromConfig(submitConfig, "submit") buttonsHTML.WriteString(buttonHTML) } if resetConfig, ok := r.Schema.Form["reset"].(map[string]any); ok { buttonHTML := renderButtonFromConfig(resetConfig, "reset") buttonsHTML.WriteString(buttonHTML) } // Support for additional custom buttons if buttons, ok := r.Schema.Form["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) } type RequestSchemaTemplate struct { Schema *jsonschema.Schema `json:"schema"` Renderer *JSONSchemaRenderer `json:"template"` } var ( cache = make(map[string]*RequestSchemaTemplate) mu = &sync.RWMutex{} BaseTemplateDir = "templates" ) // ClearCache clears the template cache func ClearCache() { mu.Lock() defer mu.Unlock() cache = make(map[string]*RequestSchemaTemplate) } // GetCacheSize returns the current cache size func GetCacheSize() int { mu.RLock() defer mu.RUnlock() return len(cache) } func GetFromBytes(schemaContent []byte, template string, templateFiles ...string) (*RequestSchemaTemplate, error) { template = strings.TrimSpace(template) compiler := jsonschema.NewCompiler() schema, err := compiler.Compile(schemaContent) if err != nil { return nil, fmt.Errorf("error compiling schema: %w", err) } var htmlLayout []byte if len(templateFiles) > 0 && templateFiles[0] != "" { templateFile := templateFiles[0] if !strings.Contains(templateFile, "/") { template = fmt.Sprintf("%s/%s", BaseTemplateDir, templateFile) } if !strings.HasSuffix(templateFile, ".html") { template += ".html" } htmlLayout, err = os.ReadFile(templateFile) if err != nil { return nil, fmt.Errorf("failed to load template: %w", err) } } else if template != "" { htmlLayout = []byte(template) } else { htmlLayout = []byte(`
    {{form_groups}}
    {{form_buttons}}
    `) } renderer := NewJSONSchemaRenderer(schema, string(htmlLayout)) cachedTemplate := &RequestSchemaTemplate{ Schema: schema, Renderer: renderer, } return cachedTemplate, nil } func GetFromSchema(schema *jsonschema.Schema, template string, templateFiles ...string) (*RequestSchemaTemplate, error) { template = strings.TrimSpace(template) var htmlLayout []byte var err error if len(templateFiles) > 0 && templateFiles[0] != "" { templateFile := templateFiles[0] if !strings.Contains(templateFile, "/") { template = fmt.Sprintf("%s/%s", BaseTemplateDir, templateFile) } if !strings.HasSuffix(templateFile, ".html") { template += ".html" } htmlLayout, err = os.ReadFile(templateFile) if err != nil { return nil, fmt.Errorf("failed to load template: %w", err) } } else if template != "" { htmlLayout = []byte(template) } else { htmlLayout = []byte(`
    {{form_groups}}
    {{form_buttons}}
    `) } renderer := NewJSONSchemaRenderer(schema, string(htmlLayout)) cachedTemplate := &RequestSchemaTemplate{ Schema: schema, Renderer: renderer, } return cachedTemplate, nil } func GetFromFile(schemaPath, template string, templateFiles ...string) (*JSONSchemaRenderer, error) { path := schemaPath if len(templateFiles) > 0 { templateFile := templateFiles[0] path += fmt.Sprintf(":%s", templateFile) } mu.RLock() if cached, exists := cache[path]; exists { mu.RUnlock() return cached.Renderer, nil } mu.RUnlock() mu.Lock() defer mu.Unlock() if cached, exists := cache[path]; exists { return cached.Renderer, nil } schemaContent, err := os.ReadFile(schemaPath) if err != nil { return nil, fmt.Errorf("error reading schema file: %w", err) } schemaRenderer, err := GetFromBytes(schemaContent, template, templateFiles...) if err != nil { return nil, fmt.Errorf("error creating renderer from bytes: %w", err) } cache[path] = schemaRenderer return schemaRenderer.Renderer, nil } // interpolateString replaces template placeholders in a string with actual values func interpolateString(templateStr string, data map[string]any) string { if len(data) == 0 { return templateStr } // First try Go template interpolation tmpl, err := template.New("interpolate").Parse(templateStr) if err == nil { var templateResult bytes.Buffer err = tmpl.Execute(&templateResult, data) if err == nil { return templateResult.String() } } // Fallback to simple string replacement if template parsing/execution fails result := templateStr for key, value := range data { placeholder := fmt.Sprintf("{{%s}}", key) if valueStr, ok := value.(string); ok { result = strings.ReplaceAll(result, placeholder, valueStr) } else { result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) } } return result }