summaryrefslogtreecommitdiffstats
path: root/internal/builder/editorjs.go
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-04 16:34:46 +0200
committerivar <i@oiee.no>2026-04-04 16:34:46 +0200
commita6355e7a6530af3335c4cd8af05f1e9c8b978169 (patch)
treec9d920d1e996ef1c42d3455825731598df6b56c2 /internal/builder/editorjs.go
parent8a093aacd162d3fd9f142b53aab9edfa061fd66a (diff)
downloadnebbet.no-a6355e7a6530af3335c4cd8af05f1e9c8b978169.tar.xz
nebbet.no-a6355e7a6530af3335c4cd8af05f1e9c8b978169.zip
.
Diffstat (limited to 'internal/builder/editorjs.go')
-rw-r--r--internal/builder/editorjs.go247
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
+}