diff options
| author | ivar <i@oiee.no> | 2026-04-04 16:34:46 +0200 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-04-04 16:34:46 +0200 |
| commit | a6355e7a6530af3335c4cd8af05f1e9c8b978169 (patch) | |
| tree | c9d920d1e996ef1c42d3455825731598df6b56c2 /internal/builder/editorjs.go | |
| parent | 8a093aacd162d3fd9f142b53aab9edfa061fd66a (diff) | |
| download | nebbet.no-a6355e7a6530af3335c4cd8af05f1e9c8b978169.tar.xz nebbet.no-a6355e7a6530af3335c4cd8af05f1e9c8b978169.zip | |
.
Diffstat (limited to 'internal/builder/editorjs.go')
| -rw-r--r-- | internal/builder/editorjs.go | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/internal/builder/editorjs.go b/internal/builder/editorjs.go new file mode 100644 index 0000000..4db4d84 --- /dev/null +++ b/internal/builder/editorjs.go @@ -0,0 +1,247 @@ +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: + // Silently ignore unknown block types + 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 + } + // EditorJS stores inline HTML; wrap in template.HTML to avoid re-escaping + html := fmt.Sprintf("<p>%s</p>\n", template.HTML(d.Text)) + return html, 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 + } + html := fmt.Sprintf("<h%d>%s</h%d>\n", d.Level, template.HTML(d.Text), d.Level) + return html, nil, nil +} + +func renderImage(data json.RawMessage) (string, []string, error) { + var d struct { + File struct { + URL string `json:"url"` + } `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" + } + html := fmt.Sprintf( + "<figure><img src=\"%s\" alt=\"%s\"><figcaption>%s</figcaption></figure>\n", + template.HTMLEscapeString(d.File.URL), + template.HTMLEscapeString(alt), + template.HTML(d.Caption), + ) + return html, 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"` // "ordered" or "unordered" + 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, "<li>%s", template.HTML(item.Content)) + if len(item.Items) > 0 { + renderListItems(buf, item.Items, tag) + } + buf.WriteString("</li>\n") + } + fmt.Fprintf(buf, "</%s>\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 + } + html := fmt.Sprintf("<pre><code>%s</code></pre>\n", template.HTMLEscapeString(d.Code)) + return html, 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, "<blockquote>\n<p>%s</p>\n", template.HTML(d.Text)) + if d.Caption != "" { + fmt.Fprintf(&buf, "<cite>%s</cite>\n", template.HTML(d.Caption)) + } + buf.WriteString("</blockquote>\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 + } + // script blocks don't render inline HTML; return the URL for the template to inject + 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, "></%s>\n", template.HTMLEscapeString(d.Name)) + script := "/assets/components/" + d.Name + ".js" + return buf.String(), []string{script}, nil +} |
