summaryrefslogtreecommitdiffstats
path: root/internal/builder
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-07 00:23:24 +0200
committerivar <i@oiee.no>2026-04-07 00:23:24 +0200
commit85920b8c7a2696115d1f77c046f48f6f00d639f1 (patch)
tree14ed2043796eadd6ed5b0a95c55e38e48713d638 /internal/builder
downloadiblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.tar.xz
iblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.zip
Init
Diffstat (limited to 'internal/builder')
-rw-r--r--internal/builder/editorjs.go255
1 files changed, 255 insertions, 0 deletions
diff --git a/internal/builder/editorjs.go b/internal/builder/editorjs.go
new file mode 100644
index 0000000..68237d0
--- /dev/null
+++ b/internal/builder/editorjs.go
@@ -0,0 +1,255 @@
+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("<p>%s</p>\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("<h%d>%s</h%d>\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 <figure> 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, "<figure%s>", figAttrs)
+ fmt.Fprintf(&buf, `<img src="%s" alt="%s" loading="lazy" decoding="async">`,
+ template.HTMLEscapeString(d.File.URL),
+ template.HTMLEscapeString(alt),
+ )
+ if d.Caption != "" {
+ fmt.Fprintf(&buf, "<figcaption>%s</figcaption>", template.HTML(d.Caption))
+ }
+ buf.WriteString("</figure>\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, "<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
+ }
+ return fmt.Sprintf("<pre><code>%s</code></pre>\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, "<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
+ }
+ 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))
+ return buf.String(), []string{"/assets/components/" + d.Name + ".js"}, nil
+}