package builder import ( "encoding/json" "fmt" "html/template" "strings" ) // PageData is passed to HTML templates when rendering posts. type PageData struct { Title string Content template.HTML ComponentScripts []string Date string Tags []string Path string } type EditorDocument struct { Version string `json:"version"` Time int64 `json:"time"` Blocks []EditorBlock `json:"blocks"` } type EditorBlock struct { Type string `json:"type"` Data json.RawMessage `json:"data"` } // RenderEditorJS converts EditorJS block JSON to HTML and extracts script URLs. // Returns the rendered HTML body, script URLs, and any error. func RenderEditorJS(blocksJSON string) (html string, scripts []string, err error) { if blocksJSON == "" || blocksJSON == "[]" { return "", nil, nil } var doc EditorDocument if err := json.Unmarshal([]byte(blocksJSON), &doc); err != nil { return "", nil, err } var buf strings.Builder var scriptURLs []string for _, block := range doc.Blocks { blockHTML, blockScripts, err := renderBlock(block) if err != nil { return "", nil, err } if blockHTML != "" { buf.WriteString(blockHTML) } scriptURLs = append(scriptURLs, blockScripts...) } return buf.String(), scriptURLs, nil } func renderBlock(block EditorBlock) (html string, scripts []string, err error) { switch block.Type { case "paragraph": return renderParagraph(block.Data) case "header": return renderHeader(block.Data) case "image": return renderImage(block.Data) case "list": return renderList(block.Data) case "code": return renderCode(block.Data) case "quote": return renderQuote(block.Data) case "script": return renderScript(block.Data) case "component": return renderComponent(block.Data) default: return "", nil, nil } } func renderParagraph(data json.RawMessage) (string, []string, error) { var d struct { Text string `json:"text"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if strings.TrimSpace(d.Text) == "" { return "", nil, nil } return fmt.Sprintf("

%s

\n", template.HTML(d.Text)), nil, nil } func renderHeader(data json.RawMessage) (string, []string, error) { var d struct { Text string `json:"text"` Level int `json:"level"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if d.Level < 1 || d.Level > 6 { d.Level = 2 } if strings.TrimSpace(d.Text) == "" { return "", nil, nil } return fmt.Sprintf("%s\n", d.Level, template.HTML(d.Text), d.Level), nil, nil } // renderImage renders an EditorJS image block. // The thumbhash (if present) is placed in data-thumbhash on the
so // the client can decode it and use it as a blurry placeholder via JS. func renderImage(data json.RawMessage) (string, []string, error) { var d struct { File struct { URL string `json:"url"` Thumbhash string `json:"thumbhash"` } `json:"file"` Caption string `json:"caption"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if d.File.URL == "" { return "", nil, nil } alt := d.Caption if alt == "" { alt = "image" } var figAttrs string if d.File.Thumbhash != "" { figAttrs = fmt.Sprintf(` data-thumbhash="%s"`, template.HTMLEscapeString(d.File.Thumbhash)) } var buf strings.Builder fmt.Fprintf(&buf, "", figAttrs) fmt.Fprintf(&buf, `%s`, template.HTMLEscapeString(d.File.URL), template.HTMLEscapeString(alt), ) if d.Caption != "" { fmt.Fprintf(&buf, "
%s
", template.HTML(d.Caption)) } buf.WriteString("
\n") return buf.String(), nil, nil } type listItem struct { Content string `json:"content"` Items []listItem `json:"items"` } func renderList(data json.RawMessage) (string, []string, error) { var d struct { Style string `json:"style"` Items []listItem `json:"items"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if len(d.Items) == 0 { return "", nil, nil } tag := "ul" if d.Style == "ordered" { tag = "ol" } var buf strings.Builder renderListItems(&buf, d.Items, tag) return buf.String(), nil, nil } func renderListItems(buf *strings.Builder, items []listItem, tag string) { fmt.Fprintf(buf, "<%s>\n", tag) for _, item := range items { fmt.Fprintf(buf, "
  • %s", template.HTML(item.Content)) if len(item.Items) > 0 { renderListItems(buf, item.Items, tag) } buf.WriteString("
  • \n") } fmt.Fprintf(buf, "\n", tag) } func renderCode(data json.RawMessage) (string, []string, error) { var d struct { Code string `json:"code"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if strings.TrimSpace(d.Code) == "" { return "", nil, nil } return fmt.Sprintf("
    %s
    \n", template.HTMLEscapeString(d.Code)), nil, nil } func renderQuote(data json.RawMessage) (string, []string, error) { var d struct { Text string `json:"text"` Caption string `json:"caption"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if strings.TrimSpace(d.Text) == "" { return "", nil, nil } var buf strings.Builder fmt.Fprintf(&buf, "
    \n

    %s

    \n", template.HTML(d.Text)) if d.Caption != "" { fmt.Fprintf(&buf, "%s\n", template.HTML(d.Caption)) } buf.WriteString("
    \n") return buf.String(), nil, nil } func renderScript(data json.RawMessage) (string, []string, error) { var d struct { Src string `json:"src"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if d.Src == "" { return "", nil, nil } return "", []string{d.Src}, nil } func renderComponent(data json.RawMessage) (string, []string, error) { var d struct { Name string `json:"name"` Props map[string]string `json:"props"` } if err := json.Unmarshal(data, &d); err != nil { return "", nil, err } if d.Name == "" { return "", nil, nil } var buf strings.Builder fmt.Fprintf(&buf, "<%s", template.HTMLEscapeString(d.Name)) for k, v := range d.Props { fmt.Fprintf(&buf, " %s=\"%s\"", template.HTMLEscapeString(k), template.HTMLEscapeString(v)) } fmt.Fprintf(&buf, ">\n", template.HTMLEscapeString(d.Name)) return buf.String(), []string{"/assets/components/" + d.Name + ".js"}, nil }