package builtin import ( "context" "fmt" "html" "maps" "regexp" "strings" "git.dev.alexdunmow.com/block/core/blocks" "github.com/google/uuid" ) var HTMLBlockMeta = blocks.BlockMeta{ Key: "html", Title: "HTML", Description: "Raw HTML content with template variable support", Category: blocks.CategoryContent, } var mediaURLRegex = regexp.MustCompile(`(src|href)=["']media:([^"']+)["']`) func HTMLBlock(ctx context.Context, content map[string]any) string { var htmlTemplate string if htmlContent, ok := content["_html_content"].(string); ok && htmlContent != "" { htmlTemplate = htmlContent } else { htmlTemplate, _ = content["html"].(string) } if htmlTemplate == "" { return "" } enrichedContent := enrichMediaFields(ctx, content) result, err := blocks.RenderTemplate(htmlTemplate, enrichedContent) if err != nil { if blocks.IsEditor(ctx) { return fmt.Sprintf(`
Template Error: %s
`, html.EscapeString(err.Error())) } return "" } result = blocks.ProcessIcons(result) result = ResolveMediaURLs(result) return result } func enrichMediaFields(ctx context.Context, content map[string]any) map[string]any { mediaFields := GetMediaFields(content) if len(mediaFields) == 0 { return content } resolver := blocks.GetMediaResolver(ctx) enriched := make(map[string]any, len(content)) maps.Copy(enriched, content) for fieldName := range mediaFields { value, ok := content[fieldName].(string) if !ok || value == "" { continue } mediaValue := parseMediaValue(ctx, resolver, value) if mediaValue != nil { enriched[fieldName] = mediaValue } } return enriched } func parseMediaValue(ctx context.Context, resolver blocks.MediaResolver, value string) *blocks.MediaValue { if value == "" { return nil } path := value if after, ok := strings.CutPrefix(value, "media:"); ok { path = after } src := path if !strings.HasPrefix(path, "/media/") { src = "/media/" + path } parts := strings.SplitN(path, "/", 2) if len(parts) == 0 || parts[0] == "" { return &blocks.MediaValue{Src: src} } mediaID, err := uuid.Parse(parts[0]) if err != nil { return &blocks.MediaValue{Src: src} } if resolver == nil { return &blocks.MediaValue{Src: src} } resolved := resolver.ResolveMedia(ctx, mediaID) if resolved == nil { return &blocks.MediaValue{Src: src} } resolved.Src = src return resolved } // GetMediaFields extracts field names that have x-editor: "media" from customSchema. func GetMediaFields(content map[string]any) map[string]bool { result := make(map[string]bool) customSchema, ok := content["customSchema"].(map[string]any) if !ok { return result } properties, ok := customSchema["properties"].(map[string]any) if !ok { return result } for key, prop := range properties { propMap, ok := prop.(map[string]any) if !ok { continue } if editor, ok := propMap["x-editor"].(string); ok && editor == "media" { result[key] = true } } return result } // ResolveMediaURLs converts media: prefixed URLs to proper /media/ paths. func ResolveMediaURLs(htmlStr string) string { return mediaURLRegex.ReplaceAllStringFunc(htmlStr, func(match string) string { submatches := mediaURLRegex.FindStringSubmatch(match) if len(submatches) < 3 { return match } attr := submatches[1] path := submatches[2] if strings.HasPrefix(path, "/media/") { return fmt.Sprintf(`%s="%s"`, attr, path) } return fmt.Sprintf(`%s="/media/%s"`, attr, path) }) } var ( boldRegex = regexp.MustCompile(`\*\*(.+?)\*\*`) italicRegex = regexp.MustCompile(`\*([^*]+?)\*`) colorRegex = regexp.MustCompile(`==(?:([a-z-]+):)?(.+?)==`) linkRegex = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) ) var validThemeColors = map[string]bool{ "foreground": true, "background": true, "primary": true, "primary-foreground": true, "secondary": true, "secondary-foreground": true, "muted": true, "muted-foreground": true, "accent": true, "accent-foreground": true, "destructive": true, "destructive-foreground": true, "border": true, "card": true, "card-foreground": true, } // ProcessMarkdown converts markdown-like syntax to HTML. func ProcessMarkdown(text string) string { text = boldRegex.ReplaceAllString(text, `$1`) text = italicRegex.ReplaceAllString(text, `$1`) text = colorRegex.ReplaceAllStringFunc(text, func(match string) string { submatches := colorRegex.FindStringSubmatch(match) if len(submatches) < 3 { return match } colorName := submatches[1] content := submatches[2] if colorName == "" || !validThemeColors[colorName] { colorName = "accent" } return fmt.Sprintf(`%s`, colorName, content) }) text = linkRegex.ReplaceAllStringFunc(text, func(match string) string { submatches := linkRegex.FindStringSubmatch(match) if len(submatches) < 3 { return match } linkText := submatches[1] url := submatches[2] url = strings.ReplaceAll(url, "&", "&") url = strings.ReplaceAll(url, """, "\"") url = strings.ReplaceAll(url, "'", "'") return fmt.Sprintf(`%s`, html.EscapeString(url), linkText) }) return text } // WrapLinesInParagraphs converts textarea newlines to
tags. func WrapLinesInParagraphs(text string) string { return strings.ReplaceAll(text, "\n", "
") } // GetTextareaFields extracts field names that have x-editor: textarea from customSchema. func GetTextareaFields(content map[string]any) map[string]bool { result := make(map[string]bool) customSchema, ok := content["customSchema"].(map[string]any) if !ok { return result } properties, ok := customSchema["properties"].(map[string]any) if !ok { return result } for key, prop := range properties { propMap, ok := prop.(map[string]any) if !ok { continue } if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { result[key] = true } } return result } // GetArrayItemTextareaFields extracts textarea field names from an array's item schema. func GetArrayItemTextareaFields(content map[string]any, collectionName string) map[string]bool { result := make(map[string]bool) customSchema, ok := content["customSchema"].(map[string]any) if !ok { return result } properties, ok := customSchema["properties"].(map[string]any) if !ok { return result } arrayField, ok := properties[collectionName].(map[string]any) if !ok { return result } if itemProperties, ok := arrayField["itemProperties"].(map[string]any); ok { for _, prop := range itemProperties { propMap, ok := prop.(map[string]any) if !ok { continue } if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { if title, ok := propMap["title"].(string); ok && title != "" { result[title] = true } } } return result } items, ok := arrayField["items"].(map[string]any) if !ok { return result } itemProps, ok := items["properties"].(map[string]any) if !ok { return result } for key, prop := range itemProps { propMap, ok := prop.(map[string]any) if !ok { continue } if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { result[key] = true } } return result }