package cabana import ( "fmt" "io/fs" "regexp" "strings" "git.golem15.com/golem15/summercms/modules/pact" ) // widgetTagPattern is a valid custom-element name restricted to lowercase // ASCII: a letter, then at least one hyphenated segment. var widgetTagPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(-[a-z0-9]+)+$`) // reservedWidgetTags are the hyphenated names the HTML specification reserves; // customElements.define refuses them. var reservedWidgetTags = map[string]bool{ "annotation-xml": true, "color-profile": true, "font-face": true, "font-face-src": true, "font-face-uri": true, "font-face-format": true, "font-face-name": true, "missing-glyph": true, } // builtinToolbarActions are the toolbar actions the framework implements // itself (D-14); a controller may not register an action with these names. var builtinToolbarActions = map[string]bool{"create": true, "delete": true} // widgetTagPrefix is the custom-element prefix a plugin's widgets must use: // the plugin ID lowercased with dots and underscores turned into hyphens, // plus a trailing hyphen (acme.conform -> "acme-conform-"). It keeps two // plugins from defining the same element. func widgetTagPrefix(pluginID string) string { return strings.NewReplacer(".", "-", "_", "-").Replace(strings.ToLower(pluginID)) + "-" } // compileExtension validates a controller's runtime admin extension points // after its list and form are compiled and its writable fields are bound: the // registered actions and every `type: widget` field. Every failure stops boot. func compileExtension(pluginID string, cc *CompiledController, fsys fs.FS) error { if cc == nil || cc.Controller == nil { return nil } id := cc.Controller.ID() actions, err := compileActions(cc.Controller) if err != nil { return fmt.Errorf("cabana: admin controller %s/%s: %w", pluginID, id, err) } cc.Actions = actions if cc.Form == nil { return nil } file := cc.Form.fieldsPath if file == "" { file = "fields.yaml" } fields := map[string]FormField{} for _, field := range cc.Form.Fields { fields[field.Name] = field } writable := map[string]bool{} for _, field := range cc.Writable { writable[field.Name] = true } prefix := widgetTagPrefix(pluginID) for i := range cc.Form.Fields { field := &cc.Form.Fields[i] if field.Type != "widget" { continue } if err := checkWidgetTag(field.Widget, prefix); err != nil { return bootErr(pluginID, id, file, fmt.Errorf("field %s: %w", field.Name, err)) } action, ok := actions[field.Action] if !ok { return bootErr(pluginID, id, file, fmt.Errorf("field %s: action %s is not registered by the controller (pact.HasAdminActions)", field.Name, field.Action)) } for _, key := range field.Fill { target, exists := fields[key] if !exists { return bootErr(pluginID, id, file, fmt.Errorf("field %s: fill %s is not a field of this form", field.Name, key)) } if !scalarFormField(target.Type) || !writable[key] { return bootErr(pluginID, id, file, fmt.Errorf("field %s: fill %s is not a writable scalar field", field.Name, key)) } } field.ActionLabel = action.Label } return nil } func checkWidgetTag(tag, prefix string) error { if !widgetTagPattern.MatchString(tag) { return fmt.Errorf("widget %q is not a valid custom-element name (lowercase, with a hyphen)", tag) } if reservedWidgetTags[tag] { return fmt.Errorf("widget %q is a reserved element name", tag) } if !strings.HasPrefix(tag, prefix) { return fmt.Errorf("widget %q must start with the plugin prefix %q", tag, prefix) } return nil } // compileActions collects a controller's registered actions into the single // action namespace (assumption-delta decision: create and delete are reserved). func compileActions(ctl pact.AdminController) (map[string]pact.AdminAction, error) { out := map[string]pact.AdminAction{} src, ok := ctl.(pact.HasAdminActions) if !ok || src == nil { return out, nil } for _, action := range src.AdminActions() { if !identifier(action.Name) { return nil, fmt.Errorf("action name %q is not an identifier", action.Name) } if builtinToolbarActions[action.Name] { return nil, fmt.Errorf("action %s uses a reserved built-in name (create, delete)", action.Name) } if _, dup := out[action.Name]; dup { return nil, fmt.Errorf("duplicate action %s", action.Name) } if action.Run == nil { return nil, fmt.Errorf("action %s has no Run function", action.Name) } out[action.Name] = action } return out, nil }