Matches the actual Gitea repo location at block/core. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
300 lines
7.2 KiB
Go
300 lines
7.2 KiB
Go
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(`<div class="bg-red-50 border border-red-200 text-red-700 p-4 rounded">
|
|
<strong>Template Error:</strong> %s
|
|
</div>`, 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, `<strong>$1</strong>`)
|
|
text = italicRegex.ReplaceAllString(text, `<em>$1</em>`)
|
|
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(`<span class="text-%s">%s</span>`, 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(`<a href="%s">%s</a>`, html.EscapeString(url), linkText)
|
|
})
|
|
return text
|
|
}
|
|
|
|
// WrapLinesInParagraphs converts textarea newlines to <br> tags.
|
|
func WrapLinesInParagraphs(text string) string {
|
|
return strings.ReplaceAll(text, "\n", "<br>")
|
|
}
|
|
|
|
// 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
|
|
}
|