mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-07 08:50:54 +08:00
update
This commit is contained in:
362
renderer/schema.go
Normal file
362
renderer/schema.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Simple string replacement approach as fallback
|
||||
result := templateStr
|
||||
for key, value := range r.TemplateData {
|
||||
placeholder := fmt.Sprintf("{{%s}}", key)
|
||||
if valueStr, ok := value.(string); ok {
|
||||
result = bytes.NewBufferString(result).String()
|
||||
result = fmt.Sprintf("%s", bytes.ReplaceAll([]byte(result), []byte(placeholder), []byte(valueStr)))
|
||||
}
|
||||
}
|
||||
|
||||
// Try Go template parsing as well
|
||||
tmpl, err := template.New("interpolate").Parse(templateStr)
|
||||
if err != nil {
|
||||
return result // Return string replacement result if template parsing fails
|
||||
}
|
||||
|
||||
var templateResult bytes.Buffer
|
||||
err = tmpl.Execute(&templateResult, r.TemplateData)
|
||||
if err != nil {
|
||||
return result // Return string replacement result 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)
|
||||
formAction = r.interpolateTemplate(formAction)
|
||||
buttonsHTML := renderButtons(formConfig)
|
||||
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 {
|
||||
attrs := fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
|
||||
formClass, formAction, formMethod, formEnctype)
|
||||
return template.HTMLAttr(attrs)
|
||||
},
|
||||
}).Parse(r.HTMLLayout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse HTML layout: %w", err)
|
||||
}
|
||||
|
||||
var renderedHTML bytes.Buffer
|
||||
err = tmpl.Execute(&renderedHTML, nil)
|
||||
if 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
|
||||
}
|
||||
|
||||
// Get required fields from schema root
|
||||
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)
|
||||
|
||||
// Parse group title
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Get group 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
|
||||
}
|
||||
|
||||
// Add required field info to field definition
|
||||
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
|
||||
|
||||
groupHTML.WriteString(`<div class="form-group-container">`)
|
||||
|
||||
// Render group title if present
|
||||
if group.Title.Text != "" {
|
||||
if group.Title.Class != "" {
|
||||
groupHTML.WriteString(fmt.Sprintf(`<div class="%s">%s</div>`, group.Title.Class, group.Title.Text))
|
||||
} else {
|
||||
groupHTML.WriteString(fmt.Sprintf(`<h3 class="group-title">%s</h3>`, group.Title.Text))
|
||||
}
|
||||
}
|
||||
|
||||
if group.GroupClass != "" {
|
||||
groupHTML.WriteString(fmt.Sprintf(`<div class="%s">`, group.GroupClass))
|
||||
} else {
|
||||
groupHTML.WriteString(`<div class="form-group-fields">`)
|
||||
}
|
||||
// Render fields
|
||||
for _, field := range group.Fields {
|
||||
groupHTML.WriteString(renderField(field))
|
||||
}
|
||||
|
||||
// Close group container div
|
||||
groupHTML.WriteString(`</div>`)
|
||||
groupHTML.WriteString(`</div>`)
|
||||
return groupHTML.String()
|
||||
}
|
||||
|
||||
// Templates for field rendering
|
||||
var fieldTemplates = map[string]string{
|
||||
"input": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><input type="{{.InputType}}" id="{{.Name}}" name="{{.Name}}" placeholder="{{.Placeholder}}" {{.Required}} {{.AdditionalAttributes}} /></div>`,
|
||||
"textarea": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><textarea id="{{.Name}}" name="{{.Name}}" placeholder="{{.Placeholder}}" {{.Required}} {{.AdditionalAttributes}}></textarea></div>`,
|
||||
"select": `<div class="{{.Class}}"><label for="{{.Name}}">{{.Title}}</label><select id="{{.Name}}" name="{{.Name}}" {{.Required}} {{.AdditionalAttributes}}>{{.OptionsHTML}}</select></div>`,
|
||||
"h": `<{{.Control}} class="{{.Class}}" id="{{.Name}}" {{.AdditionalAttributes}}>{{.Title}}</{{.Control}}>`,
|
||||
"p": `<p class="{{.Class}}" id="{{.Name}}" {{.AdditionalAttributes}}>{{.Title}}</p>`,
|
||||
"a": `<a class="{{.Class}}" id="{{.Name}}" href="{{.Href}}" {{.AdditionalAttributes}}>{{.Title}}</a>`,
|
||||
}
|
||||
|
||||
func renderField(field FieldInfo) string {
|
||||
ui, ok := field.Definition["ui"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
control, _ := ui["element"].(string)
|
||||
class, _ := ui["class"].(string)
|
||||
name, _ := ui["name"].(string)
|
||||
title, _ := field.Definition["title"].(string)
|
||||
placeholder, _ := field.Definition["placeholder"].(string)
|
||||
|
||||
isRequired, _ := field.Definition["isRequired"].(bool)
|
||||
required := ""
|
||||
titleHTML := title
|
||||
if isRequired {
|
||||
required = "required"
|
||||
titleHTML += ` <span class="required">*</span>`
|
||||
}
|
||||
|
||||
inputType := "text"
|
||||
if uiType, ok := ui["type"].(string); ok {
|
||||
inputType = uiType
|
||||
} else if fieldType, ok := field.Definition["type"].(string); ok && fieldType == "email" {
|
||||
inputType = "email"
|
||||
}
|
||||
|
||||
var additionalAttributes bytes.Buffer
|
||||
for key, value := range field.Definition {
|
||||
switch key {
|
||||
case "title", "ui", "placeholder", "type", "order", "isRequired":
|
||||
continue
|
||||
default:
|
||||
additionalAttributes.WriteString(fmt.Sprintf(` %s="%v"`, key, value))
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"Class": class,
|
||||
"Name": name,
|
||||
"Title": template.HTML(titleHTML),
|
||||
"Placeholder": placeholder,
|
||||
"Required": required,
|
||||
"AdditionalAttributes": template.HTML(additionalAttributes.String()),
|
||||
"InputType": inputType,
|
||||
"Control": control,
|
||||
}
|
||||
|
||||
switch control {
|
||||
case "input":
|
||||
tmpl := template.Must(template.New("input").Parse(fieldTemplates["input"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
case "textarea":
|
||||
tmpl := template.Must(template.New("textarea").Parse(fieldTemplates["textarea"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
case "select":
|
||||
options, _ := ui["options"].([]any)
|
||||
var optionsHTML bytes.Buffer
|
||||
for _, option := range options {
|
||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
|
||||
}
|
||||
data["OptionsHTML"] = template.HTML(optionsHTML.String())
|
||||
tmpl := template.Must(template.New("select").Parse(fieldTemplates["select"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
data["Control"] = control
|
||||
tmpl := template.Must(template.New("h").Parse(fieldTemplates["h"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
case "p":
|
||||
tmpl := template.Must(template.New("p").Parse(fieldTemplates["p"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
case "a":
|
||||
href, _ := ui["href"].(string)
|
||||
data["Href"] = href
|
||||
tmpl := template.Must(template.New("a").Parse(fieldTemplates["a"]))
|
||||
var buf bytes.Buffer
|
||||
tmpl.Execute(&buf, data)
|
||||
return buf.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Template for buttons
|
||||
var buttonTemplate = `<button type="{{.Type}}" class="{{.Class}}">{{.Label}}</button>`
|
||||
|
||||
func renderButtons(formConfig map[string]any) string {
|
||||
var buttonsHTML bytes.Buffer
|
||||
if submitConfig, ok := formConfig["submit"].(map[string]any); ok {
|
||||
data := map[string]any{
|
||||
"Type": submitConfig["type"],
|
||||
"Class": submitConfig["class"],
|
||||
"Label": submitConfig["label"],
|
||||
}
|
||||
tmpl := template.Must(template.New("button").Parse(buttonTemplate))
|
||||
tmpl.Execute(&buttonsHTML, data)
|
||||
}
|
||||
if resetConfig, ok := formConfig["reset"].(map[string]any); ok {
|
||||
data := map[string]any{
|
||||
"Type": resetConfig["type"],
|
||||
"Class": resetConfig["class"],
|
||||
"Label": resetConfig["label"],
|
||||
}
|
||||
tmpl := template.Must(template.New("button").Parse(buttonTemplate))
|
||||
tmpl.Execute(&buttonsHTML, data)
|
||||
}
|
||||
return buttonsHTML.String()
|
||||
}
|
Reference in New Issue
Block a user